338 lines
11 KiB
Go
338 lines
11 KiB
Go
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
|
|
}
|