Remove Chrome fingerprints from the default uTLS distribution.
[dnstt.git] / dnstt-client / utls.go
blob7a6af5632fab261a76bdce8fc54fe5e4f057955e
1 package main
3 // Support code for TLS camouflage using uTLS.
5 import (
6 "context"
7 "crypto/tls"
8 "fmt"
9 "net"
10 "net/http"
11 "net/url"
12 "strings"
13 "sync"
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 {
22 Label string
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) {
46 return entry.ID
49 return nil
52 // utlsDialContext connects to the given network address and initiates a TLS
53 // handshake with the provided ClientHelloID, and returns the resulting TLS
54 // connection.
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.
57 if config == nil {
58 config = &utls.Config{}
60 if config.ServerName == "" {
61 config = config.Clone()
62 host, _, err := net.SplitHostPort(addr)
63 if err != nil {
64 return nil, err
66 config.ServerName = host
68 dialer := &net.Dialer{}
69 conn, err := dialer.DialContext(ctx, network, addr)
70 if err != nil {
71 return nil, err
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()
78 if err != nil {
79 uconn.Close()
80 return nil, err
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()
90 if err != nil {
91 uconn.Close()
92 return nil, err
94 return uconn, nil
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
106 // protocol error.
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
137 config *utls.Config
138 innerLock sync.Mutex
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{
146 clientHelloID: id,
147 config: config,
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 {
154 case "http":
155 // If http, don't invoke uTLS; just pass it to an ordinary http.Transport.
156 return http.DefaultTransport.RoundTrip(req)
157 case "https":
158 default:
159 return nil, fmt.Errorf("unsupported URL scheme %q", req.URL.Scheme)
162 var err error
163 rt.innerLock.Lock()
164 if rt.inner == nil {
165 // On the first call, make an http.Transport or http2.Transport
166 // as appropriate.
167 rt.inner, err = makeRoundTripper(req, rt.config, rt.clientHelloID)
169 rt.innerLock.Unlock()
170 if err != nil {
171 return nil, err
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
182 // ClientHelloID.
183 func makeRoundTripper(req *http.Request, config *utls.Config, id *utls.ClientHelloID) (http.RoundTripper, error) {
184 addr, err := addrForDial(req.URL)
185 if err != nil {
186 return nil, err
189 bootstrapConn, err := utlsDialContext(req.Context(), "tcp", addr, config, id)
190 if err != nil {
191 return nil, err
194 // Peek at the ALPN-negotiated protocol.
195 protocol := bootstrapConn.ConnectionState().NegotiatedProtocol
197 // Protects bootstrapConn.
198 var lock sync.Mutex
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) {
202 lock.Lock()
203 defer lock.Unlock()
205 // On the first dial, reuse bootstrapConn.
206 if bootstrapConn != nil {
207 uconn := bootstrapConn
208 bootstrapConn = nil
209 return uconn, nil
212 // Later dials make a new connection.
213 uconn, err := utlsDialContext(ctx, "tcp", addr, config, id)
214 if err != nil {
215 return nil, err
217 if uconn.ConnectionState().NegotiatedProtocol != protocol {
218 return nil, fmt.Errorf("unexpected switch from ALPN %q to %q",
219 protocol, uconn.ConnectionState().NegotiatedProtocol)
222 return uconn, nil
225 // Construct an http.Transport or http2.Transport depending on ALPN.
226 switch protocol {
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)
238 }, nil
239 default:
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
245 return tr, nil
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.
254 port := url.Port()
255 if port == "" {
256 // No port? Use the default for the scheme.
257 switch url.Scheme {
258 case "http":
259 port = "80"
260 case "https":
261 port = "443"
262 default:
263 return "", fmt.Errorf("unsupported URL scheme %q", url.Scheme)
266 return net.JoinHostPort(host, port), nil