3 // Support code for TLS camouflage using uTLS.
15 utls
"github.com/refraction-networking/utls"
16 "golang.org/x/net/http2"
19 // utlsClientHelloIDMap is a correspondence between human-readable labels and
20 // supported utls.ClientHelloIDs.
21 var utlsClientHelloIDMap
= []struct {
23 ID
*utls
.ClientHelloID
25 {"Firefox", &utls
.HelloFirefox_Auto
},
26 {"Firefox_55", &utls
.HelloFirefox_55
},
27 {"Firefox_56", &utls
.HelloFirefox_56
},
28 {"Firefox_63", &utls
.HelloFirefox_63
},
29 {"Firefox_65", &utls
.HelloFirefox_65
},
30 {"Chrome", &utls
.HelloChrome_Auto
},
31 {"Chrome_58", &utls
.HelloChrome_58
},
32 {"Chrome_62", &utls
.HelloChrome_62
},
33 {"Chrome_70", &utls
.HelloChrome_70
},
34 {"Chrome_72", &utls
.HelloChrome_72
},
35 {"Chrome_83", &utls
.HelloChrome_83
},
36 {"iOS", &utls
.HelloIOS_Auto
},
37 {"iOS_11_1", &utls
.HelloIOS_11_1
},
38 {"iOS_12_1", &utls
.HelloIOS_12_1
},
41 // utlsLookup returns a *utls.ClientHelloID from utlsClientHelloIDMap by a
42 // case-insensitive label match, or nil if there is no match.
43 func utlsLookup(label
string) *utls
.ClientHelloID
{
44 for _
, entry
:= range utlsClientHelloIDMap
{
45 if strings
.ToLower(label
) == strings
.ToLower(entry
.Label
) {
52 // utlsDialContext connects to the given network address and initiates a TLS
53 // handshake with the provided ClientHelloID, and returns the resulting TLS
55 func utlsDialContext(ctx context
.Context
, network
, addr
string, config
*utls
.Config
, id
*utls
.ClientHelloID
) (*utls
.UConn
, error
) {
56 // Set the SNI from addr, if not already set.
58 config
= &utls
.Config
{}
60 if config
.ServerName
== "" {
61 config
= config
.Clone()
62 host
, _
, err
:= net
.SplitHostPort(addr
)
66 config
.ServerName
= host
68 dialer
:= &net
.Dialer
{}
69 conn
, err
:= dialer
.DialContext(ctx
, network
, addr
)
73 uconn
:= utls
.UClient(conn
, config
, *id
)
74 // Manually remove the SNI if it contains an IP address.
75 // https://github.com/refraction-networking/utls/issues/96
76 if net
.ParseIP(config
.ServerName
) != nil {
77 err
:= uconn
.RemoveSNIExtension()
83 // We must call Handshake before returning, or else the UConn may not
84 // actually use the selected ClientHelloID. It depends on whether a Read
85 // or a Write happens first. If a Read happens first, the connection
86 // will use the normal crypto/tls fingerprint. If a Write happens first,
87 // it will use the selected fingerprint as expected.
88 // https://github.com/refraction-networking/utls/issues/75
89 err
= uconn
.Handshake()
97 // The goal of utlsRoundTripper is: provide an http.RoundTripper abstraction
98 // that retains the features of http.Transport (e.g., persistent connections and
99 // HTTP/2 support), while making TLS connections using uTLS in place of
100 // crypto/tls. The challenge is: while http.Transport provides a DialTLSContext
101 // hook, setting it to non-nil disables automatic HTTP/2 support in the client.
102 // Most of the uTLS fingerprints contain an ALPN extension containing "h2";
103 // i.e., they declare support for HTTP/2. If the server also supports HTTP/2,
104 // then uTLS may negotiate an HTTP/2 connection without the http.Transport
105 // knowing it, which leads to an HTTP/1.1 client speaking to an HTTP/2 server, a
108 // The code here uses an idea adapted from meek_lite in obfs4proxy:
109 // https://gitlab.com/yawning/obfs4/commit/4d453dab2120082b00bf6e63ab4aaeeda6b8d8a3
110 // Instead of setting DialTLSContext on an http.Transport and exposing it
111 // directly, we expose a wrapper type, utlsRoundTripper, which contains within
112 // it either an http.Transport or an http2.Transport. The first time a caller
113 // calls RoundTrip on the wrapper, we initiate a uTLS connection
114 // (bootstrapConn), then peek at the ALPN-negotiated protocol: if "h2", create
115 // an internal http2.Transport; otherwise, create an internal http.Transport. In
116 // either case, set DialTLSContext (or DialTLS for http2.Transport) on the
117 // created Transport to a function that dials using uTLS. As a special case, the
118 // first time the DialTLS callback is called, it reuses bootstrapConn (the one
119 // made to peek at the ALPN), rather than make a new connection.
121 // Subsequent calls to RoundTripper on the wrapper just pass the requests though
122 // the previously created http.Transport or http2.Transport. We assume that in
123 // future RoundTrips, the ALPN-negotiated protocol will remain the same as it
124 // was in the initial RoundTrip. At this point it is the http.Transport or
125 // http2.Transport calling DialTLSContext, not us, so we cannot dynamically swap
126 // the underlying transport based on the ALPN.
128 // https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/meek/29077
129 // https://github.com/refraction-networking/utls/issues/16
131 // utlsRoundTripper is an http.RoundTripper that uses uTLS (with a specified
132 // ClientHelloID) to make TLS connections.
134 // Can only be reused among servers which negotiate the same ALPN.
135 type utlsRoundTripper
struct {
136 clientHelloID
*utls
.ClientHelloID
139 inner http
.RoundTripper
142 // NewUTLSRoundTripper creates a utlsRoundTripper with the given TLS
143 // configuration and ClientHelloID.
144 func NewUTLSRoundTripper(config
*utls
.Config
, id
*utls
.ClientHelloID
) *utlsRoundTripper
{
145 return &utlsRoundTripper
{
148 // inner will be set in the first call to RoundTrip.
152 func (rt
*utlsRoundTripper
) RoundTrip(req
*http
.Request
) (*http
.Response
, error
) {
153 switch req
.URL
.Scheme
{
155 // If http, don't invoke uTLS; just pass it to an ordinary http.Transport.
156 return http
.DefaultTransport
.RoundTrip(req
)
159 return nil, fmt
.Errorf("unsupported URL scheme %q", req
.URL
.Scheme
)
165 // On the first call, make an http.Transport or http2.Transport
167 rt
.inner
, err
= makeRoundTripper(req
, rt
.config
, rt
.clientHelloID
)
169 rt
.innerLock
.Unlock()
174 // Forward the request to the inner http.Transport or http2.Transport.
175 return rt
.inner
.RoundTrip(req
)
178 // makeRoundTripper makes a bootstrap TLS configuration using the given TLS
179 // configuration and ClientHelloID, and creates an http.Transport or
180 // http2.Transport, depending on the negotated ALPN. The Transport is set up to
181 // make future TLS connections using the same TLS configuration and
183 func makeRoundTripper(req
*http
.Request
, config
*utls
.Config
, id
*utls
.ClientHelloID
) (http
.RoundTripper
, error
) {
184 addr
, err
:= addrForDial(req
.URL
)
189 bootstrapConn
, err
:= utlsDialContext(req
.Context(), "tcp", addr
, config
, id
)
194 // Peek at the ALPN-negotiated protocol.
195 protocol
:= bootstrapConn
.ConnectionState().NegotiatedProtocol
197 // Protects bootstrapConn.
199 // This is the callback for future dials done by the inner
200 // http.Transport or http2.Transport.
201 dialTLSContext
:= func(ctx context
.Context
, network
, addr
string) (net
.Conn
, error
) {
205 // On the first dial, reuse bootstrapConn.
206 if bootstrapConn
!= nil {
207 uconn
:= bootstrapConn
212 // Later dials make a new connection.
213 uconn
, err
:= utlsDialContext(ctx
, "tcp", addr
, config
, id
)
217 if uconn
.ConnectionState().NegotiatedProtocol
!= protocol
{
218 return nil, fmt
.Errorf("unexpected switch from ALPN %q to %q",
219 protocol
, uconn
.ConnectionState().NegotiatedProtocol
)
225 // Construct an http.Transport or http2.Transport depending on ALPN.
227 case http2
.NextProtoTLS
:
228 // Unfortunately http2.Transport does not expose the same
229 // configuration options as http.Transport with regard to
230 // timeouts, etc., so we are at the mercy of the defaults.
231 // https://github.com/golang/go/issues/16581
232 return &http2
.Transport
{
233 DialTLS
: func(network
, addr
string, _
*tls
.Config
) (net
.Conn
, error
) {
234 // Ignore the *tls.Config parameter; use our
235 // static config instead.
236 return dialTLSContext(context
.Background(), network
, addr
)
240 // With http.Transport, copy important default fields from
241 // http.DefaultTransport, such as TLSHandshakeTimeout and
242 // IdleConnTimeout, before overriding DialTLSContext.
243 tr
:= http
.DefaultTransport
.(*http
.Transport
).Clone()
244 tr
.DialTLSContext
= dialTLSContext
249 // addrForDial extracts a host:port address from a URL, suitable for dialing.
250 func addrForDial(url
*url
.URL
) (string, error
) {
251 host
:= url
.Hostname()
252 // net/http would use golang.org/x/net/idna here, to convert a possible
253 // internationalized domain name to ASCII.
256 // No port? Use the default for the scheme.
263 return "", fmt
.Errorf("unsupported URL scheme %q", url
.Scheme
)
266 return net
.JoinHostPort(host
, port
), nil