CHANGELOG for v1.20240513.0.
[dnstt.git] / dnstt-client / utls.go
blobf043726d8a76d271faf684098855ec748c6e2f51
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 {"random", &utls.HelloRandomizedALPN},
26 {"Firefox", &utls.HelloFirefox_Auto},
27 {"Firefox_55", &utls.HelloFirefox_55},
28 {"Firefox_56", &utls.HelloFirefox_56},
29 {"Firefox_63", &utls.HelloFirefox_63},
30 {"Firefox_65", &utls.HelloFirefox_65},
31 {"Firefox_99", &utls.HelloFirefox_99},
32 {"Firefox_102", &utls.HelloFirefox_102},
33 {"Firefox_105", &utls.HelloFirefox_105},
34 {"Firefox_120", &utls.HelloFirefox_120},
35 {"Chrome", &utls.HelloChrome_Auto},
36 {"Chrome_58", &utls.HelloChrome_58},
37 {"Chrome_62", &utls.HelloChrome_62},
38 {"Chrome_70", &utls.HelloChrome_70},
39 {"Chrome_72", &utls.HelloChrome_72},
40 {"Chrome_83", &utls.HelloChrome_83},
41 {"Chrome_87", &utls.HelloChrome_87},
42 {"Chrome_96", &utls.HelloChrome_96},
43 {"Chrome_100", &utls.HelloChrome_100},
44 {"Chrome_102", &utls.HelloChrome_102},
45 {"Chrome_120", &utls.HelloChrome_120},
46 {"iOS", &utls.HelloIOS_Auto},
47 {"iOS_11_1", &utls.HelloIOS_11_1},
48 {"iOS_12_1", &utls.HelloIOS_12_1},
49 {"iOS_13", &utls.HelloIOS_13},
50 {"iOS_14", &utls.HelloIOS_14},
53 // utlsLookup returns a *utls.ClientHelloID from utlsClientHelloIDMap by a
54 // case-insensitive label match, or nil if there is no match.
55 func utlsLookup(label string) *utls.ClientHelloID {
56 for _, entry := range utlsClientHelloIDMap {
57 if strings.ToLower(label) == strings.ToLower(entry.Label) {
58 return entry.ID
61 return nil
64 // utlsDialContext connects to the given network address and initiates a TLS
65 // handshake with the provided ClientHelloID, and returns the resulting TLS
66 // connection.
67 func utlsDialContext(ctx context.Context, network, addr string, config *utls.Config, id *utls.ClientHelloID) (*utls.UConn, error) {
68 // Set the SNI from addr, if not already set.
69 if config == nil {
70 config = &utls.Config{}
72 if config.ServerName == "" {
73 config = config.Clone()
74 host, _, err := net.SplitHostPort(addr)
75 if err != nil {
76 return nil, err
78 config.ServerName = host
80 dialer := &net.Dialer{}
81 conn, err := dialer.DialContext(ctx, network, addr)
82 if err != nil {
83 return nil, err
85 uconn := utls.UClient(conn, config, *id)
86 // We must call Handshake before returning, or else the UConn may not
87 // actually use the selected ClientHelloID. It depends on whether a Read
88 // or a Write happens first. If a Read happens first, the connection
89 // will use the normal crypto/tls fingerprint. If a Write happens first,
90 // it will use the selected fingerprint as expected.
91 // https://github.com/refraction-networking/utls/issues/75
92 err = uconn.Handshake()
93 if err != nil {
94 uconn.Close()
95 return nil, err
97 return uconn, nil
100 // The goal of utlsRoundTripper is: provide an http.RoundTripper abstraction
101 // that retains the features of http.Transport (e.g., persistent connections and
102 // HTTP/2 support), while making TLS connections using uTLS in place of
103 // crypto/tls. The challenge is: while http.Transport provides a DialTLSContext
104 // hook, setting it to non-nil disables automatic HTTP/2 support in the client.
105 // Most of the uTLS fingerprints contain an ALPN extension containing "h2";
106 // i.e., they declare support for HTTP/2. If the server also supports HTTP/2,
107 // then uTLS may negotiate an HTTP/2 connection without the http.Transport
108 // knowing it, which leads to an HTTP/1.1 client speaking to an HTTP/2 server, a
109 // protocol error.
111 // The code here uses an idea adapted from meek_lite in obfs4proxy:
112 // https://gitlab.com/yawning/obfs4/commit/4d453dab2120082b00bf6e63ab4aaeeda6b8d8a3
113 // Instead of setting DialTLSContext on an http.Transport and exposing it
114 // directly, we expose a wrapper type, utlsRoundTripper, which contains within
115 // it either an http.Transport or an http2.Transport. The first time a caller
116 // calls RoundTrip on the wrapper, we initiate a uTLS connection
117 // (bootstrapConn), then peek at the ALPN-negotiated protocol: if "h2", create
118 // an internal http2.Transport; otherwise, create an internal http.Transport. In
119 // either case, set DialTLSContext (or DialTLS for http2.Transport) on the
120 // created Transport to a function that dials using uTLS. As a special case, the
121 // first time the DialTLS callback is called, it reuses bootstrapConn (the one
122 // made to peek at the ALPN), rather than make a new connection.
124 // Subsequent calls to RoundTripper on the wrapper just pass the requests though
125 // the previously created http.Transport or http2.Transport. We assume that in
126 // future RoundTrips, the ALPN-negotiated protocol will remain the same as it
127 // was in the initial RoundTrip. At this point it is the http.Transport or
128 // http2.Transport calling DialTLSContext, not us, so we cannot dynamically swap
129 // the underlying transport based on the ALPN.
131 // https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/meek/29077
132 // https://github.com/refraction-networking/utls/issues/16
134 // utlsRoundTripper is an http.RoundTripper that uses uTLS (with a specified
135 // ClientHelloID) to make TLS connections.
137 // Can only be reused among servers which negotiate the same ALPN.
138 type utlsRoundTripper struct {
139 clientHelloID *utls.ClientHelloID
140 config *utls.Config
141 innerLock sync.Mutex
142 inner http.RoundTripper
145 // NewUTLSRoundTripper creates a utlsRoundTripper with the given TLS
146 // configuration and ClientHelloID.
147 func NewUTLSRoundTripper(config *utls.Config, id *utls.ClientHelloID) *utlsRoundTripper {
148 return &utlsRoundTripper{
149 clientHelloID: id,
150 config: config,
151 // inner will be set in the first call to RoundTrip.
155 func (rt *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
156 switch req.URL.Scheme {
157 case "http":
158 // If http, don't invoke uTLS; just pass it to an ordinary http.Transport.
159 return http.DefaultTransport.RoundTrip(req)
160 case "https":
161 default:
162 return nil, fmt.Errorf("unsupported URL scheme %q", req.URL.Scheme)
165 var err error
166 rt.innerLock.Lock()
167 if rt.inner == nil {
168 // On the first call, make an http.Transport or http2.Transport
169 // as appropriate.
170 rt.inner, err = makeRoundTripper(req, rt.config, rt.clientHelloID)
172 rt.innerLock.Unlock()
173 if err != nil {
174 return nil, err
177 // Forward the request to the inner http.Transport or http2.Transport.
178 return rt.inner.RoundTrip(req)
181 // makeRoundTripper makes a bootstrap TLS configuration using the given TLS
182 // configuration and ClientHelloID, and creates an http.Transport or
183 // http2.Transport, depending on the negotated ALPN. The Transport is set up to
184 // make future TLS connections using the same TLS configuration and
185 // ClientHelloID.
186 func makeRoundTripper(req *http.Request, config *utls.Config, id *utls.ClientHelloID) (http.RoundTripper, error) {
187 addr, err := addrForDial(req.URL)
188 if err != nil {
189 return nil, err
192 bootstrapConn, err := utlsDialContext(req.Context(), "tcp", addr, config, id)
193 if err != nil {
194 return nil, err
197 // Peek at the ALPN-negotiated protocol.
198 protocol := bootstrapConn.ConnectionState().NegotiatedProtocol
200 // Protects bootstrapConn.
201 var lock sync.Mutex
202 // This is the callback for future dials done by the inner
203 // http.Transport or http2.Transport.
204 dialTLSContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
205 lock.Lock()
206 defer lock.Unlock()
208 // On the first dial, reuse bootstrapConn.
209 if bootstrapConn != nil {
210 uconn := bootstrapConn
211 bootstrapConn = nil
212 return uconn, nil
215 // Later dials make a new connection.
216 uconn, err := utlsDialContext(ctx, "tcp", addr, config, id)
217 if err != nil {
218 return nil, err
220 if uconn.ConnectionState().NegotiatedProtocol != protocol {
221 return nil, fmt.Errorf("unexpected switch from ALPN %q to %q",
222 protocol, uconn.ConnectionState().NegotiatedProtocol)
225 return uconn, nil
228 // Construct an http.Transport or http2.Transport depending on ALPN.
229 switch protocol {
230 case http2.NextProtoTLS:
231 // Unfortunately http2.Transport does not expose the same
232 // configuration options as http.Transport with regard to
233 // timeouts, etc., so we are at the mercy of the defaults.
234 // https://github.com/golang/go/issues/16581
235 return &http2.Transport{
236 DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) {
237 // Ignore the *tls.Config parameter; use our
238 // static config instead.
239 return dialTLSContext(context.Background(), network, addr)
241 }, nil
242 default:
243 // With http.Transport, copy important default fields from
244 // http.DefaultTransport, such as TLSHandshakeTimeout and
245 // IdleConnTimeout, before overriding DialTLSContext.
246 tr := http.DefaultTransport.(*http.Transport).Clone()
247 tr.DialTLSContext = dialTLSContext
248 return tr, nil
252 // addrForDial extracts a host:port address from a URL, suitable for dialing.
253 func addrForDial(url *url.URL) (string, error) {
254 host := url.Hostname()
255 // net/http would use golang.org/x/net/idna here, to convert a possible
256 // internationalized domain name to ASCII.
257 port := url.Port()
258 if port == "" {
259 // No port? Use the default for the scheme.
260 switch url.Scheme {
261 case "http":
262 port = "80"
263 case "https":
264 port = "443"
265 default:
266 return "", fmt.Errorf("unsupported URL scheme %q", url.Scheme)
269 return net.JoinHostPort(host, port), nil