package dnsttclient // Support code for TLS camouflage using uTLS. import ( "context" "crypto/tls" "fmt" "net" "net/http" "net/url" "strings" "sync" "time" utls "github.com/refraction-networking/utls" "golang.org/x/net/http2" ) // utlsClientHelloIDMap is a correspondence between human-readable labels and // supported utls.ClientHelloIDs. var utlsClientHelloIDMap = []struct { Label string ID *utls.ClientHelloID }{ {"random", &utls.HelloRandomizedALPN}, {"Firefox", &utls.HelloFirefox_Auto}, {"Firefox_55", &utls.HelloFirefox_55}, {"Firefox_56", &utls.HelloFirefox_56}, {"Firefox_63", &utls.HelloFirefox_63}, {"Firefox_65", &utls.HelloFirefox_65}, {"Firefox_99", &utls.HelloFirefox_99}, {"Firefox_102", &utls.HelloFirefox_102}, {"Firefox_105", &utls.HelloFirefox_105}, {"Firefox_120", &utls.HelloFirefox_120}, {"Chrome", &utls.HelloChrome_Auto}, {"Chrome_58", &utls.HelloChrome_58}, {"Chrome_62", &utls.HelloChrome_62}, {"Chrome_70", &utls.HelloChrome_70}, {"Chrome_72", &utls.HelloChrome_72}, {"Chrome_83", &utls.HelloChrome_83}, {"Chrome_87", &utls.HelloChrome_87}, {"Chrome_96", &utls.HelloChrome_96}, {"Chrome_100", &utls.HelloChrome_100}, {"Chrome_102", &utls.HelloChrome_102}, {"Chrome_120", &utls.HelloChrome_120}, {"iOS", &utls.HelloIOS_Auto}, {"iOS_11_1", &utls.HelloIOS_11_1}, {"iOS_12_1", &utls.HelloIOS_12_1}, {"iOS_13", &utls.HelloIOS_13}, {"iOS_14", &utls.HelloIOS_14}, } // utlsLookup returns a *utls.ClientHelloID from utlsClientHelloIDMap by a // case-insensitive label match, or nil if there is no match. func utlsLookup(label string) *utls.ClientHelloID { for _, entry := range utlsClientHelloIDMap { if strings.ToLower(label) == strings.ToLower(entry.Label) { return entry.ID } } return nil } var bootstrapResolverAddrs = []string{ "8.8.8.8:53", "1.1.1.1:53", "8.8.4.4:53", "1.0.0.1:53", } func lookupHostIPv4(ctx context.Context, host string) ([]net.IP, error) { var lastErr error for _, resolverAddr := range bootstrapResolverAddrs { resolverAddr := resolverAddr r := &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { d := &net.Dialer{Timeout: 5 * time.Second} return d.DialContext(ctx, "udp4", resolverAddr) }, } ips, err := r.LookupIP(ctx, "ip4", host) if err == nil && len(ips) > 0 { return ips, nil } if err != nil { lastErr = err } } if lastErr == nil { lastErr = fmt.Errorf("no A records returned") } return nil, fmt.Errorf("bootstrap IPv4 lookup failed for %s: %w", host, lastErr) } func resolveAddrIPv4(ctx context.Context, addr string) (string, string, error) { host, port, err := net.SplitHostPort(addr) if err != nil { return "", "", err } if ip := net.ParseIP(host); ip != nil { ip4 := ip.To4() if ip4 == nil { return "", "", fmt.Errorf("IPv6 address %s cannot be used for forced IPv4 dial", host) } return net.JoinHostPort(ip4.String(), port), host, nil } ips, err := lookupHostIPv4(ctx, host) if err != nil { return "", host, err } return net.JoinHostPort(ips[0].String(), port), host, nil } // utlsDialContext connects to the given network address and initiates a TLS // handshake with the provided ClientHelloID, and returns the resulting TLS // connection. func utlsDialContext(ctx context.Context, network, addr string, config *utls.Config, id *utls.ClientHelloID) (*utls.UConn, error) { return utlsDialContextWithOptions(ctx, network, addr, config, id, false) } func utlsDialContextWithOptions(ctx context.Context, network, addr string, config *utls.Config, id *utls.ClientHelloID, forceIPv4 bool) (*utls.UConn, error) { originalHost, _, err := net.SplitHostPort(addr) if err != nil { return nil, err } if forceIPv4 { addr, _, err = resolveAddrIPv4(ctx, addr) if err != nil { return nil, err } } // Set the SNI from the original addr host, if not already set. if config == nil { config = &utls.Config{} } if config.ServerName == "" { config = config.Clone() config.ServerName = originalHost } dialer := &net.Dialer{} if forceIPv4 && network == "tcp" { network = "tcp4" } conn, err := dialer.DialContext(ctx, network, addr) if err != nil { return nil, err } uconn := utls.UClient(conn, config, *id) // We must call Handshake before returning, or else the UConn may not // actually use the selected ClientHelloID. It depends on whether a Read // or a Write happens first. If a Read happens first, the connection // will use the normal crypto/tls fingerprint. If a Write happens first, // it will use the selected fingerprint as expected. // https://github.com/refraction-networking/utls/issues/75 err = uconn.Handshake() if err != nil { uconn.Close() return nil, err } return uconn, nil } // The goal of utlsRoundTripper is: provide an http.RoundTripper abstraction // that retains the features of http.Transport (e.g., persistent connections and // HTTP/2 support), while making TLS connections using uTLS in place of // crypto/tls. The challenge is: while http.Transport provides a DialTLSContext // hook, setting it to non-nil disables automatic HTTP/2 support in the client. // Most of the uTLS fingerprints contain an ALPN extension containing "h2"; // i.e., they declare support for HTTP/2. If the server also supports HTTP/2, // then uTLS may negotiate an HTTP/2 connection without the http.Transport // knowing it, which leads to an HTTP/1.1 client speaking to an HTTP/2 server, a // protocol error. // // The code here uses an idea adapted from meek_lite in obfs4proxy: // https://gitlab.com/yawning/obfs4/commit/4d453dab2120082b00bf6e63ab4aaeeda6b8d8a3 // Instead of setting DialTLSContext on an http.Transport and exposing it // directly, we expose a wrapper type, utlsRoundTripper, which contains within // it either an http.Transport or an http2.Transport. The first time a caller // calls RoundTrip on the wrapper, we initiate a uTLS connection // (bootstrapConn), then peek at the ALPN-negotiated protocol: if "h2", create // an internal http2.Transport; otherwise, create an internal http.Transport. In // either case, set DialTLSContext (or DialTLS for http2.Transport) on the // created Transport to a function that dials using uTLS. As a special case, the // first time the DialTLS callback is called, it reuses bootstrapConn (the one // made to peek at the ALPN), rather than make a new connection. // // Subsequent calls to RoundTripper on the wrapper just pass the requests though // the previously created http.Transport or http2.Transport. We assume that in // future RoundTrips, the ALPN-negotiated protocol will remain the same as it // was in the initial RoundTrip. At this point it is the http.Transport or // http2.Transport calling DialTLSContext, not us, so we cannot dynamically swap // the underlying transport based on the ALPN. // // https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/meek/29077 // https://github.com/refraction-networking/utls/issues/16 // utlsRoundTripper is an http.RoundTripper that uses uTLS (with a specified // ClientHelloID) to make TLS connections. // // Can only be reused among servers which negotiate the same ALPN. type utlsRoundTripper struct { clientHelloID *utls.ClientHelloID config *utls.Config forceIPv4 bool innerLock sync.Mutex inner http.RoundTripper } // NewUTLSRoundTripper creates a utlsRoundTripper with the given TLS // configuration and ClientHelloID. func NewUTLSRoundTripper(config *utls.Config, id *utls.ClientHelloID, forceIPv4 bool) *utlsRoundTripper { return &utlsRoundTripper{ clientHelloID: id, config: config, forceIPv4: forceIPv4, // inner will be set in the first call to RoundTrip. } } func (rt *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { switch req.URL.Scheme { case "http": // If http, don't invoke uTLS; just pass it to an ordinary http.Transport. return http.DefaultTransport.RoundTrip(req) case "https": default: return nil, fmt.Errorf("unsupported URL scheme %q", req.URL.Scheme) } var err error rt.innerLock.Lock() if rt.inner == nil { // On the first call, make an http.Transport or http2.Transport // as appropriate. rt.inner, err = makeRoundTripper(req, rt.config, rt.clientHelloID, rt.forceIPv4) } rt.innerLock.Unlock() if err != nil { return nil, err } // Forward the request to the inner http.Transport or http2.Transport. return rt.inner.RoundTrip(req) } // makeRoundTripper makes a bootstrap TLS configuration using the given TLS // configuration and ClientHelloID, and creates an http.Transport or // http2.Transport, depending on the negotated ALPN. The Transport is set up to // make future TLS connections using the same TLS configuration and // ClientHelloID. func makeRoundTripper(req *http.Request, config *utls.Config, id *utls.ClientHelloID, forceIPv4 bool) (http.RoundTripper, error) { addr, err := addrForDial(req.URL) if err != nil { return nil, err } bootstrapConn, err := utlsDialContextWithOptions(req.Context(), "tcp", addr, config, id, forceIPv4) if err != nil { return nil, err } // Peek at the ALPN-negotiated protocol. protocol := bootstrapConn.ConnectionState().NegotiatedProtocol // Protects bootstrapConn. var lock sync.Mutex // This is the callback for future dials done by the inner // http.Transport or http2.Transport. dialTLSContext := func(ctx context.Context, network, addr string) (net.Conn, error) { lock.Lock() defer lock.Unlock() // On the first dial, reuse bootstrapConn. if bootstrapConn != nil { uconn := bootstrapConn bootstrapConn = nil return uconn, nil } // Later dials make a new connection. uconn, err := utlsDialContextWithOptions(ctx, "tcp", addr, config, id, forceIPv4) if err != nil { return nil, err } if uconn.ConnectionState().NegotiatedProtocol != protocol { return nil, fmt.Errorf("unexpected switch from ALPN %q to %q", protocol, uconn.ConnectionState().NegotiatedProtocol) } return uconn, nil } // Construct an http.Transport or http2.Transport depending on ALPN. switch protocol { case http2.NextProtoTLS: // Unfortunately http2.Transport does not expose the same // configuration options as http.Transport with regard to // timeouts, etc., so we are at the mercy of the defaults. // https://github.com/golang/go/issues/16581 return &http2.Transport{ DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) { // Ignore the *tls.Config parameter; use our // static config instead. return dialTLSContext(context.Background(), network, addr) }, }, nil default: // With http.Transport, copy important default fields from // http.DefaultTransport, such as TLSHandshakeTimeout and // IdleConnTimeout, before overriding DialTLSContext. tr := http.DefaultTransport.(*http.Transport).Clone() tr.DialTLSContext = dialTLSContext return tr, nil } } // addrForDial extracts a host:port address from a URL, suitable for dialing. func addrForDial(url *url.URL) (string, error) { host := url.Hostname() // net/http would use golang.org/x/net/idna here, to convert a possible // internationalized domain name to ASCII. port := url.Port() if port == "" { // No port? Use the default for the scheme. switch url.Scheme { case "http": port = "80" case "https": port = "443" default: return "", fmt.Errorf("unsupported URL scheme %q", url.Scheme) } } return net.JoinHostPort(host, port), nil }