Skip to content

Commit cf0d16b

Browse files
authored
Merge pull request #136 from SenseUnit/utls
uTLS
2 parents 6899715 + 8ad7ed6 commit cf0d16b

File tree

7 files changed

+251
-21
lines changed

7 files changed

+251
-21
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ Supported proxy schemes are:
368368
* `curves` - colon-separated list of enabled TLS key exchange curves.
369369
* `min-tls-version` - minimum TLS version.
370370
* `max-tls-version` - maximum TLS version.
371+
* `utls-fp` - TLS fingerprint parroting with uTLS library. See the [list](https://pkg.go.dev/github.com/refraction-networking/utls#pkg-variables) of allowed client IDs. Example: `utls-fp=HelloChrome_Auto`.
371372
* `http+optimistic` - (EXPERIMENTAL) HTTP proxy dialer which reads the connection success response asynchronously.
372373
* `https+optimistic` - (EXPERIMENTAL) HTTP proxy over TLS dialer which reads the connection success response asynchronously. Options are same as for `https` dialer.
373374
* `h2` - HTTP/2 proxy over TLS connection. Examples: `h2://user:[email protected]`, `h2://example.org?cert=cert.pem&key=key.pem`. This method also supports additional parameters passed in query string:
@@ -381,6 +382,7 @@ Supported proxy schemes are:
381382
* `min-tls-version` - minimum TLS version.
382383
* `max-tls-version` - maximum TLS version.
383384
* `fetchrandom` - request server to send random data in the first request via every new HTTP/2 connection. Useful to trick TLS-in-TLS detection. Value format: length as a number or range `x-y`. Example: `fetchrandom=100000-500000`.
385+
* `utls-fp` - TLS fingerprint parroting with uTLS library. See the [list](https://pkg.go.dev/github.com/refraction-networking/utls#pkg-variables) of allowed client IDs. Example: `utls-fp=HelloChrome_Auto`.
384386
* `h2c` - HTTP/2 proxy over plaintext connection with the CONNECT method support. Examples: `h2c://example.org:8080`.
385387
* `fetchrandom` - request server to send random data in the first request via every new HTTP/2 connection. Useful to trick TLS-in-TLS detection. Value format: length as a number or range `x-y`. Example: `fetchrandom=100000-500000`.
386388
* `socks5`, `socks5h` - SOCKS5 proxy with hostname resolving via remote proxy. Example: `socks5://127.0.0.1:9050`.

dialer/h2.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@ func H2ProxyDialerFromURL(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, error)
3232
port := u.Port()
3333

3434
var (
35-
tlsConfig *tls.Config
36-
err error
37-
h2c bool
38-
scheme string
35+
tlsConfig *tls.Config
36+
tlsFactory func(net.Conn, *tls.Config) net.Conn
37+
err error
38+
h2c bool
39+
scheme string
3940
)
4041
switch strings.ToLower(u.Scheme) {
4142
case "h2c":
@@ -55,6 +56,10 @@ func H2ProxyDialerFromURL(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, error)
5556
if err != nil {
5657
return nil, fmt.Errorf("TLS configuration failed: %w", err)
5758
}
59+
tlsFactory, err = tlsutil.TLSFactoryFromURL(u)
60+
if err != nil {
61+
return nil, fmt.Errorf("TLS configuration failed: %w", err)
62+
}
5863
scheme = "https"
5964
default:
6065
return nil, errors.New("unsupported proxy type")
@@ -120,7 +125,7 @@ func H2ProxyDialerFromURL(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, error)
120125
if err != nil {
121126
return nil, err
122127
}
123-
conn = tls.Client(conn, tlsConfig)
128+
conn = tlsFactory(conn, tlsConfig)
124129
return conn, nil
125130
}
126131
}

dialer/optimistic.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ import (
1717
)
1818

1919
type OptimisticHTTPProxyDialer struct {
20-
address string
21-
tlsConfig *tls.Config
22-
userinfo *url.Userinfo
23-
next Dialer
20+
address string
21+
tlsConfig *tls.Config
22+
tlsFactory func(net.Conn, *tls.Config) net.Conn
23+
userinfo *url.Userinfo
24+
next Dialer
2425
}
2526

2627
func NewOptimisticHTTPProxyDialer(address string, tlsConfig *tls.Config, userinfo *url.Userinfo, next LegacyDialer) *OptimisticHTTPProxyDialer {
@@ -36,8 +37,11 @@ func OptimisticHTTPProxyDialerFromURL(u *url.URL, next xproxy.Dialer) (xproxy.Di
3637
host := u.Hostname()
3738
port := u.Port()
3839

39-
var tlsConfig *tls.Config
40-
var err error
40+
var (
41+
tlsConfig *tls.Config
42+
tlsFactory func(net.Conn, *tls.Config) net.Conn
43+
err error
44+
)
4145
switch strings.ToLower(u.Scheme) {
4246
case "http+optimistic":
4347
if port == "" {
@@ -51,13 +55,23 @@ func OptimisticHTTPProxyDialerFromURL(u *url.URL, next xproxy.Dialer) (xproxy.Di
5155
if err != nil {
5256
return nil, fmt.Errorf("TLS configuration failed: %w", err)
5357
}
58+
tlsFactory, err = tlsutil.TLSFactoryFromURL(u)
59+
if err != nil {
60+
return nil, fmt.Errorf("TLS configuration failed: %w", err)
61+
}
5462
default:
5563
return nil, errors.New("unsupported proxy type")
5664
}
5765

5866
address := net.JoinHostPort(host, port)
5967

60-
return NewOptimisticHTTPProxyDialer(address, tlsConfig, u.User, next), nil
68+
return &OptimisticHTTPProxyDialer{
69+
address: address,
70+
tlsConfig: tlsConfig,
71+
tlsFactory: tlsFactory,
72+
userinfo: u.User,
73+
next: MaybeWrapWithContextDialer(next),
74+
}, nil
6175
}
6276

6377
func (d *OptimisticHTTPProxyDialer) Dial(network, address string) (net.Conn, error) {
@@ -75,7 +89,7 @@ func (d *OptimisticHTTPProxyDialer) DialContext(ctx context.Context, network, ad
7589
return nil, fmt.Errorf("proxy dialer is unable to make connection: %w", err)
7690
}
7791
if d.tlsConfig != nil {
78-
conn = tls.Client(conn, d.tlsConfig)
92+
conn = d.tlsFactory(conn, d.tlsConfig)
7993
}
8094

8195
return &futureH1ProxiedConn{

dialer/upstream.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ import (
2121
)
2222

2323
type HTTPProxyDialer struct {
24-
address string
25-
tlsConfig *tls.Config
26-
userinfo *url.Userinfo
27-
next Dialer
24+
address string
25+
tlsConfig *tls.Config
26+
tlsFactory func(net.Conn, *tls.Config) net.Conn
27+
userinfo *url.Userinfo
28+
next Dialer
2829
}
2930

3031
func NewHTTPProxyDialer(address string, tlsConfig *tls.Config, userinfo *url.Userinfo, next LegacyDialer) *HTTPProxyDialer {
@@ -40,8 +41,11 @@ func HTTPProxyDialerFromURL(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, erro
4041
host := u.Hostname()
4142
port := u.Port()
4243

43-
var tlsConfig *tls.Config
44-
var err error
44+
var (
45+
tlsConfig *tls.Config
46+
tlsFactory func(net.Conn, *tls.Config) net.Conn
47+
err error
48+
)
4549
switch strings.ToLower(u.Scheme) {
4650
case "http":
4751
if port == "" {
@@ -55,13 +59,23 @@ func HTTPProxyDialerFromURL(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, erro
5559
if err != nil {
5660
return nil, fmt.Errorf("TLS configuration failed: %w", err)
5761
}
62+
tlsFactory, err = tlsutil.TLSFactoryFromURL(u)
63+
if err != nil {
64+
return nil, fmt.Errorf("TLS configuration failed: %w", err)
65+
}
5866
default:
5967
return nil, errors.New("unsupported proxy type")
6068
}
6169

6270
address := net.JoinHostPort(host, port)
6371

64-
return NewHTTPProxyDialer(address, tlsConfig, u.User, next), nil
72+
return &HTTPProxyDialer{
73+
address: address,
74+
tlsConfig: tlsConfig,
75+
tlsFactory: tlsFactory,
76+
next: MaybeWrapWithContextDialer(next),
77+
userinfo: u.User,
78+
}, nil
6579
}
6680

6781
func (d *HTTPProxyDialer) Dial(network, address string) (net.Conn, error) {
@@ -79,7 +93,7 @@ func (d *HTTPProxyDialer) DialContext(ctx context.Context, network, address stri
7993
return nil, fmt.Errorf("proxy dialer is unable to make connection: %w", err)
8094
}
8195
if d.tlsConfig != nil {
82-
conn = tls.Client(conn, d.tlsConfig)
96+
conn = d.tlsFactory(conn, d.tlsConfig)
8397
}
8498

8599
stopGuardEvent := make(chan struct{})

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/jellydator/ttlcache/v3 v3.3.0
1313
github.com/libp2p/go-reuseport v0.4.0
1414
github.com/redis/go-redis/v9 v9.8.0
15+
github.com/refraction-networking/utls v1.8.0
1516
github.com/tg123/go-htpasswd v1.2.4
1617
github.com/zeebo/xxh3 v1.0.2
1718
golang.org/x/crypto v0.38.0
@@ -23,12 +24,14 @@ require (
2324

2425
require (
2526
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect
27+
github.com/andybalholm/brotli v1.0.6 // indirect
2628
github.com/cespare/xxhash/v2 v2.3.0 // indirect
2729
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
2830
github.com/dlclark/regexp2 v1.11.5 // indirect
2931
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
3032
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
3133
github.com/hashicorp/errwrap v1.1.0 // indirect
34+
github.com/klauspost/compress v1.17.4 // indirect
3235
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
3336
github.com/pires/go-proxyproto v0.8.1
3437
golang.org/x/sys v0.33.0 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ github.com/Snawoot/uniqueslice v0.1.1 h1:KEfv3FtAXiNEoxvcc79pFQDhnqwYXQyZIkxOM4e
66
github.com/Snawoot/uniqueslice v0.1.1/go.mod h1:K9zIaHO43FGLHbqm6WCDFeY6+CN/du5eiio/vxvDVC8=
77
github.com/Snawoot/xtime v0.0.0-20250501122004-d1ce456948bb h1:PleTDwc/EQenzLsvIal2BgvIXr2D214M88RFac3WkeI=
88
github.com/Snawoot/xtime v0.0.0-20250501122004-d1ce456948bb/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
9+
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
10+
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
911
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
1012
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
1113
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@@ -34,6 +36,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
3436
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
3537
github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc=
3638
github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw=
39+
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
40+
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
3741
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
3842
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
3943
github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s=
@@ -44,6 +48,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
4448
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
4549
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
4650
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
51+
github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
52+
github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
4753
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
4854
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
4955
github.com/tg123/go-htpasswd v1.2.4 h1:HgH8KKCjdmo7jjXWN9k1nefPBd7Be3tFCTjc2jPraPU=

0 commit comments

Comments
 (0)