[tor-commits] [meek/master] Integrate uTLS as an option for TLS camouflage.

dcf at torproject.org dcf at torproject.org
Thu May 9 19:37:25 UTC 2019


commit b8fb876145cda3c14d335a3fc88b5e422a926150
Author: David Fifield <david at bamsoftware.com>
Date:   Tue Jan 22 23:38:26 2019 -0700

    Integrate uTLS as an option for TLS camouflage.
    
    This adapts a technique that Yawning used in obfs4proxy for meek_lite:
    https://gitlab.com/yawning/obfs4/commit/4d453dab2120082b00bf6e63ab4aaeeda6b8d8a3
    https://lists.torproject.org/pipermail/tor-dev/2019-January/013633.html
    
    It's activated by the new utls= SOCKS arg or --utls command line option.
    The argument is the name of a uTLS ClientHelloID; e.g.,
    HelloChrome_Auto. We omit HelloCustom (not useful externally),
    HelloGolang (just don't use utls), and HelloRandomized (may negotiate
    HTTP/1.1 or HTTP/2 at different times, which is incompatible with the
    way the integration works).
    
    When using HTTP/1, we copy default timeouts etc. from the Go
    net/http.DefaultTransport. Unfortunately I don't know a way to do the
    same for HTTP/2. Configuring an http.Transport and then calling
    http2.ConfigureTransport on it doesn't work; it leads to the same
    problem of an HTTP/1 client speaking to an HTTP/2 server.
    
    We recognize utls=none and utls=HelloGolang as an alias for omitting
    utls=. This is for compatibility with obfs4proxy meek_lite.
    https://bugs.torproject.org/29077#comment:13
---
 meek-client/meek-client.go |  30 +++++-
 meek-client/utls.go        | 260 +++++++++++++++++++++++++++++++++++++++++++++
 meek-client/utls_test.go   | 232 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 518 insertions(+), 4 deletions(-)

diff --git a/meek-client/meek-client.go b/meek-client/meek-client.go
index 73ca214..74ef2c5 100644
--- a/meek-client/meek-client.go
+++ b/meek-client/meek-client.go
@@ -79,9 +79,9 @@ const (
 	helperWriteTimeout      = 2 * time.Second
 )
 
-// We use this RoundTripper to make all our requests when --helper is not
-// in effect. We use the defaults, except we take control of the Proxy setting
-// (notably, disabling the default ProxyFromEnvironment).
+// We use this RoundTripper to make all our requests when neither --helper nor
+// utls is in effect. We use the defaults, except we take control of the Proxy
+// setting (notably, disabling the default ProxyFromEnvironment).
 var httpRoundTripper *http.Transport = http.DefaultTransport.(*http.Transport)
 
 // We use this RoundTripper when --helper is in effect.
@@ -96,6 +96,7 @@ var options struct {
 	Front     string
 	ProxyURL  *url.URL
 	UseHelper bool
+	UTLSName  string
 }
 
 // RequestInfo encapsulates all the configuration used for a request–response
@@ -307,9 +308,29 @@ func handler(conn *pt.SocksConn) error {
 		info.URL.Host = front
 	}
 
-	info.RoundTripper = httpRoundTripper
+	// First check utls= SOCKS arg, then --utls option.
+	utlsName, utlsOK := conn.Req.Args.Get("utls")
+	if utlsOK {
+	} else if options.UTLSName != "" {
+		utlsName = options.UTLSName
+		utlsOK = true
+	}
+
+	// First we check --helper: if it was specified, then we always use the
+	// helper, and utls is disallowed. Otherwise, we use utls if requested;
+	// or else fall back to native net/http.
 	if options.UseHelper {
+		if utlsOK {
+			return fmt.Errorf("cannot use utls with --helper")
+		}
 		info.RoundTripper = helperRoundTripper
+	} else if utlsOK {
+		info.RoundTripper, err = NewUTLSRoundTripper(utlsName, nil)
+		if err != nil {
+			return err
+		}
+	} else {
+		info.RoundTripper = httpRoundTripper
 	}
 
 	return copyLoop(conn, &info)
@@ -387,6 +408,7 @@ func main() {
 	flag.StringVar(&logFilename, "log", "", "name of log file")
 	flag.StringVar(&proxy, "proxy", "", "proxy URL")
 	flag.StringVar(&options.URL, "url", "", "URL to request if no url= SOCKS arg")
+	flag.StringVar(&options.UTLSName, "utls", "", "uTLS Client Hello ID")
 	flag.Parse()
 
 	ptInfo, err := pt.ClientSetup(nil)
diff --git a/meek-client/utls.go b/meek-client/utls.go
new file mode 100644
index 0000000..e8d0bc0
--- /dev/null
+++ b/meek-client/utls.go
@@ -0,0 +1,260 @@
+// Support code for TLS camouflage using uTLS.
+//
+// The goal 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 DialTLS 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 DialTLS on an http.Transport and exposing it directly, we
+// expose a wrapper type, UTLSRoundTripper, that 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 DialTLS 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 DialTLS, not us, so we can't dynamically swap the
+// underlying transport based on the ALPN.
+//
+// https://bugs.torproject.org/29077
+// https://github.com/refraction-networking/utls/issues/16
+package main
+
+import (
+	"crypto/tls"
+	"fmt"
+	"net"
+	"net/http"
+	"net/url"
+	"reflect"
+	"strings"
+	"sync"
+
+	utls "github.com/refraction-networking/utls"
+	"golang.org/x/net/http2"
+)
+
+// Copy the public fields (fields for which CanSet is true) from src to dst.
+// src and dst must be pointers to the same type. We use this to make copies of
+// httpRoundTripper. We cannot use struct assignment, because http.Transport
+// contains private mutexes. The idea of using reflection to copy only the
+// public fields comes from a post by Nick Craig-Wood:
+// https://groups.google.com/d/msg/Golang-Nuts/SDiGYNVE8iY/89hRKTF4BAAJ
+func copyPublicFields(dst, src interface{}) {
+	if reflect.TypeOf(dst) != reflect.TypeOf(src) {
+		panic("unequal types")
+	}
+	dstValue := reflect.ValueOf(dst).Elem()
+	srcValue := reflect.ValueOf(src).Elem()
+	for i := 0; i < dstValue.NumField(); i++ {
+		if dstValue.Field(i).CanSet() {
+			dstValue.Field(i).Set(srcValue.Field(i))
+		}
+	}
+}
+
+// Extract a host:port address from a URL, suitable for passing to net.Dial.
+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
+}
+
+// Analogous to tls.Dial. Connect to the given address and initiate a TLS
+// handshake using the given ClientHelloID, returning the resulting connection.
+func dialUTLS(network, addr string, cfg *utls.Config, clientHelloID *utls.ClientHelloID) (*utls.UConn, error) {
+	if options.ProxyURL != nil {
+		return nil, fmt.Errorf("no proxy allowed with uTLS")
+	}
+
+	conn, err := net.Dial(network, addr)
+	if err != nil {
+		return nil, err
+	}
+	uconn := utls.UClient(conn, cfg, *clientHelloID)
+	if cfg == nil || cfg.ServerName == "" {
+		serverName, _, err := net.SplitHostPort(addr)
+		if err != nil {
+			return nil, err
+		}
+		uconn.SetSNI(serverName)
+	}
+	err = uconn.Handshake()
+	if err != nil {
+		return nil, err
+	}
+	return uconn, nil
+}
+
+// A http.RoundTripper that uses uTLS (with a specified Client Hello ID) to make
+// TLS connections.
+//
+// Can only be reused among servers which negotiate the same ALPN.
+type UTLSRoundTripper struct {
+	sync.Mutex
+
+	clientHelloID *utls.ClientHelloID
+	config        *utls.Config
+	rt            http.RoundTripper
+}
+
+func (rt *UTLSRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	switch req.URL.Scheme {
+	case "http":
+		// If http, we don't invoke uTLS; just pass it to the global http.Transport.
+		return httpRoundTripper.RoundTrip(req)
+	case "https":
+	default:
+		return nil, fmt.Errorf("unsupported URL scheme %q", req.URL.Scheme)
+	}
+
+	rt.Lock()
+	defer rt.Unlock()
+
+	if rt.rt == nil {
+		// On the first call, make an http.Transport or http2.Transport
+		// as appropriate.
+		var err error
+		rt.rt, err = makeRoundTripper(req.URL, rt.clientHelloID, rt.config)
+		if err != nil {
+			return nil, err
+		}
+	}
+	// Forward the request to the internal http.Transport or http2.Transport.
+	return rt.rt.RoundTrip(req)
+}
+
+func makeRoundTripper(url *url.URL, clientHelloID *utls.ClientHelloID, cfg *utls.Config) (http.RoundTripper, error) {
+	addr, err := addrForDial(url)
+	if err != nil {
+		return nil, err
+	}
+
+	// Connect to the given address and initiate a TLS handshake using
+	// the given ClientHelloID. Return the resulting connection.
+	dial := func(network, addr string) (*utls.UConn, error) {
+		return dialUTLS(network, addr, cfg, clientHelloID)
+	}
+
+	bootstrapConn, err := dial("tcp", addr)
+	if err != nil {
+		return nil, err
+	}
+
+	// Peek at what protocol we negotiated.
+	protocol := bootstrapConn.ConnectionState().NegotiatedProtocol
+
+	// Protects bootstrapConn.
+	var lock sync.Mutex
+	// This is the callback for future dials done by the internal
+	// http.Transport or http2.Transport.
+	dialTLS := func(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 := dial(network, addr)
+		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 cfg instead.
+				return dialTLS(network, addr)
+			},
+		}, nil
+	default:
+		// With http.Transport, copy important default fields from
+		// http.DefaultTransport, such as TLSHandshakeTimeout and
+		// IdleConnTimeout.
+		tr := &http.Transport{}
+		copyPublicFields(tr, httpRoundTripper)
+		tr.DialTLS = dialTLS
+		return tr, nil
+	}
+}
+
+// When you update this map, also update the man page in doc/meek-client.1.txt.
+var clientHelloIDMap = map[string]*utls.ClientHelloID{
+	// No HelloCustom: not useful for external configuration.
+	// No HelloRandomized: doesn't negotiate consistent ALPN.
+	"none":                  nil, // special case: disable uTLS
+	"hellogolang":           nil, // special case: disable uTLS
+	"hellorandomizedalpn":   &utls.HelloRandomizedALPN,
+	"hellorandomizednoalpn": &utls.HelloRandomizedNoALPN,
+	"hellofirefox_auto":     &utls.HelloFirefox_Auto,
+	"hellofirefox_55":       &utls.HelloFirefox_55,
+	"hellofirefox_56":       &utls.HelloFirefox_56,
+	"hellofirefox_63":       &utls.HelloFirefox_63,
+	"hellochrome_auto":      &utls.HelloChrome_Auto,
+	"hellochrome_58":        &utls.HelloChrome_58,
+	"hellochrome_62":        &utls.HelloChrome_62,
+	"hellochrome_70":        &utls.HelloChrome_70,
+	"helloios_auto":         &utls.HelloIOS_Auto,
+	"helloios_11_1":         &utls.HelloIOS_11_1,
+}
+
+func NewUTLSRoundTripper(name string, cfg *utls.Config) (http.RoundTripper, error) {
+	// Lookup is case-insensitive.
+	clientHelloID, ok := clientHelloIDMap[strings.ToLower(name)]
+	if !ok {
+		return nil, fmt.Errorf("no uTLS Client Hello ID named %q", name)
+	}
+	if clientHelloID == nil {
+		// Special case for "none" and HelloGolang.
+		return httpRoundTripper, nil
+	}
+	return &UTLSRoundTripper{
+		clientHelloID: clientHelloID,
+		config:        cfg,
+	}, nil
+}
diff --git a/meek-client/utls_test.go b/meek-client/utls_test.go
new file mode 100644
index 0000000..2eb72af
--- /dev/null
+++ b/meek-client/utls_test.go
@@ -0,0 +1,232 @@
+package main
+
+import (
+	"bytes"
+	"io"
+	"net"
+	"net/http"
+	"net/url"
+	"testing"
+
+	utls "github.com/refraction-networking/utls"
+)
+
+func TestCopyPublicFieldsHTTPTransport(t *testing.T) {
+	src := http.DefaultTransport.(*http.Transport)
+	dst := &http.Transport{}
+	copyPublicFields(dst, src)
+	// Test various fields that we might care about a copy of http.Transport
+	// having.
+	if dst.DisableKeepAlives != src.DisableKeepAlives {
+		t.Errorf("mismatch on DisableKeepAlives")
+	}
+	if dst.DisableCompression != src.DisableCompression {
+		t.Errorf("mismatch on DisableCompression")
+	}
+	if dst.MaxIdleConns != src.MaxIdleConns {
+		t.Errorf("mismatch on MaxIdleConns")
+	}
+	if dst.MaxIdleConnsPerHost != src.MaxIdleConnsPerHost {
+		t.Errorf("mismatch on MaxIdleConnsPerHost")
+	}
+	if dst.MaxConnsPerHost != src.MaxConnsPerHost {
+		t.Errorf("mismatch on MaxConnsPerHost")
+	}
+	if dst.IdleConnTimeout != src.IdleConnTimeout {
+		t.Errorf("mismatch on IdleConnTimeout")
+	}
+	if dst.ResponseHeaderTimeout != src.ResponseHeaderTimeout {
+		t.Errorf("mismatch on ResponseHeaderTimeout")
+	}
+	if dst.ExpectContinueTimeout != src.ExpectContinueTimeout {
+		t.Errorf("mismatch on ExpectContinueTimeout")
+	}
+	if dst.MaxResponseHeaderBytes != src.MaxResponseHeaderBytes {
+		t.Errorf("mismatch on MaxResponseHeaderBytes")
+	}
+}
+
+// Test that the name lookup of NewUTLSRoundTripper is case-insensitive.
+func TestNewUTLSRoundTripperCase(t *testing.T) {
+	mixed, err := NewUTLSRoundTripper("HelloFirefox_Auto", nil, nil)
+	if err != nil {
+		t.Fatalf("error on %q: %v", "HelloFirefox_Auto", err)
+	}
+	upper, err := NewUTLSRoundTripper("HELLOFIREFOX_AUTO", nil, nil)
+	if err != nil {
+		t.Fatalf("error on %q: %v", "HELLOFIREFOX_AUTO", err)
+	}
+	lower, err := NewUTLSRoundTripper("hellofirefox_auto", nil, nil)
+	if err != nil {
+		t.Fatalf("error on %q: %v", "hellofirefox_auto", err)
+	}
+	if mixed.(*UTLSRoundTripper).clientHelloID != upper.(*UTLSRoundTripper).clientHelloID ||
+		upper.(*UTLSRoundTripper).clientHelloID != lower.(*UTLSRoundTripper).clientHelloID {
+		t.Fatalf("mismatch %p %p %p",
+			mixed.(*UTLSRoundTripper).clientHelloID,
+			upper.(*UTLSRoundTripper).clientHelloID,
+			lower.(*UTLSRoundTripper).clientHelloID)
+	}
+}
+
+// Return a byte slice which is the ClientHello sent when rt does a RoundTrip.
+// Opens a temporary listener on an ephemeral port on localhost. The host you
+// provide can be an IP address like "127.0.0.1" or a name like "localhost", but
+// it has to resolve to localhost.
+func clientHelloResultingFromRoundTrip(t *testing.T, host string, rt *UTLSRoundTripper) ([]byte, error) {
+	ch := make(chan []byte, 1)
+
+	ln, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		return nil, err
+	}
+	defer ln.Close()
+
+	go func() {
+		defer func() {
+			close(ch)
+		}()
+		conn, err := ln.Accept()
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		defer conn.Close()
+		buf := make([]byte, 1024)
+		n, err := conn.Read(buf)
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		ch <- buf[:n]
+	}()
+
+	_, port, err := net.SplitHostPort(ln.Addr().String())
+	if err != nil {
+		return nil, err
+	}
+	u := &url.URL{
+		Scheme: "https",
+		Host:   net.JoinHostPort(host, port),
+	}
+	req, err := http.NewRequest("POST", u.String(), nil)
+	if err != nil {
+		return nil, err
+	}
+	// The RoundTrip fails because the goroutine "server" hangs up. So
+	// ignore an EOF error.
+	_, err = rt.RoundTrip(req)
+	if err != nil && err != io.EOF {
+		return nil, err
+	}
+
+	return <-ch, nil
+}
+
+// Test that a uTLS RoundTripper actually does something to the TLS Client
+// Hello. We don't check all the ClientHelloIDs; this is just a guard against a
+// catastrophic incompatibility or something else that makes uTLS stop working.
+func TestUTLSClientHello(t *testing.T) {
+	// We use HelloIOS_11_1 because its lengthy ALPN means we will not
+	// confuse it with a native Go fingerprint, and lack of GREASE means we
+	// do not have to account for many variations.
+	rt, err := NewUTLSRoundTripper("HelloIOS_11_1", &utls.Config{InsecureSkipVerify: true, ServerName: "localhost"}, nil)
+	if err != nil {
+		panic(err)
+	}
+
+	buf, err := clientHelloResultingFromRoundTrip(t, "127.0.0.1", rt.(*UTLSRoundTripper))
+	// A poor man's regexp matching because the regexp package only works on
+	// UTF-8–encoded strings, not arbitrary byte slices. Every byte matches
+	// itself, except '.' which matches anything. NB '.' and '\x2e' are the
+	// same.
+	pattern := "" +
+		// Handshake, Client Hello, TLS 1.2, Client Random
+		"\x16\x03\x01\x01\x01\x01\x00\x00\xfd\x03\x03................................" +
+		// Session ID
+		"\x20................................" +
+		// Ciphersuites and compression methods
+		"\x00\x28\xc0\x2c\xc0\x2b\xc0\x24\xc0\x23\xc0\x0a\xc0\x09\xcc\xa9\xc0\x30\xc0\x2f\xc0\x28\xc0\x27\xc0\x14\xc0\x13\xcc\xa8\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x01\x00" +
+		// Extensions
+		"\x00\x8c\xff\x01\x00\x01\x00" +
+		"\x00\x00\x00\x0e\x00\x0c\x00\x00\x09localhost" +
+		"\x00\x17\x00\x00" +
+		"\x00\x0d\x00\x14\x00\x12\x04\x03\x08\x04\x04\x01\x05\x03\x08\x05\x05\x01\x08\x06\x06\x01\x02\x01" +
+		"\x00\x05\x00\x05\x01\x00\x00\x00\x00" +
+		"\x33\x74\x00\x00" +
+		"\x00\x12\x00\x00" +
+		"\x00\x10\x00\x30\x00\x2e\x02\x68\x32\x05\x68\x32\x2d\x31\x36\x05\x68\x32\x2d\x31\x35\x05\x68\x32\x2d\x31\x34\x08\x73\x70\x64\x79\x2f\x33\x2e\x31\x06\x73\x70\x64\x79\x2f\x33\x08\x68\x74\x74\x70\x2f\x31\x2e\x31" +
+		"\x00\x0b\x00\x02\x01\x00" +
+		"\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x18\x00\x19"
+	if len(buf) != len(pattern) {
+		t.Errorf("fingerprint was not as expected: %+q", buf)
+	}
+	for i := 0; i < len(pattern); i++ {
+		a := buf[i]
+		b := pattern[i]
+		if b != '.' && a != b {
+			t.Fatalf("fingerprint mismatch a position %v: %+q", i, buf)
+		}
+	}
+}
+
+func TestUTLSServerName(t *testing.T) {
+	const clientHelloIDName = "HelloFirefox_63"
+
+	// No ServerName, dial IP address. Results in an invalid server_name
+	// extension with a 0-length host_name. Not sure if that's what it
+	// should do, but check if the behavior ever changes.
+	rt, err := NewUTLSRoundTripper(clientHelloIDName, &utls.Config{InsecureSkipVerify: true}, nil)
+	if err != nil {
+		panic(err)
+	}
+	buf, err := clientHelloResultingFromRoundTrip(t, "127.0.0.1", rt.(*UTLSRoundTripper))
+	if err != nil {
+		panic(err)
+	}
+	if !bytes.Contains(buf, []byte("\x00\x00\x00\x05\x00\x03\x00\x00\x00")) {
+		t.Errorf("expected 0-length server_name extension with no ServerName and IP address dial")
+	}
+
+	// No ServerName, dial hostname. server_name extension should come from
+	// the dial address.
+	rt, err = NewUTLSRoundTripper(clientHelloIDName, &utls.Config{InsecureSkipVerify: true}, nil)
+	if err != nil {
+		panic(err)
+	}
+	buf, err = clientHelloResultingFromRoundTrip(t, "localhost", rt.(*UTLSRoundTripper))
+	if err != nil {
+		panic(err)
+	}
+	if !bytes.Contains(buf, []byte("\x00\x00\x00\x0e\x00\x0c\x00\x00\x09localhost")) {
+		t.Errorf("expected \"localhost\" server_name extension with no ServerName and hostname dial")
+	}
+
+	// Given ServerName, dial IP address. server_name extension should from
+	// the ServerName.
+	rt, err = NewUTLSRoundTripper(clientHelloIDName, &utls.Config{InsecureSkipVerify: true, ServerName: "test.example"}, nil)
+	if err != nil {
+		panic(err)
+	}
+	buf, err = clientHelloResultingFromRoundTrip(t, "127.0.0.1", rt.(*UTLSRoundTripper))
+	if err != nil {
+		panic(err)
+	}
+	if !bytes.Contains(buf, []byte("\x00\x00\x00\x11\x00\x0f\x00\x00\x0ctest.example")) {
+		t.Errorf("expected \"test.example\" server_name extension with given ServerName and IP address dial")
+	}
+
+	// Given ServerName, dial hostname. server_name extension should from
+	// the ServerName.
+	rt, err = NewUTLSRoundTripper(clientHelloIDName, &utls.Config{InsecureSkipVerify: true, ServerName: "test.example"}, nil)
+	if err != nil {
+		panic(err)
+	}
+	buf, err = clientHelloResultingFromRoundTrip(t, "localhost", rt.(*UTLSRoundTripper))
+	if err != nil {
+		panic(err)
+	}
+	if !bytes.Contains(buf, []byte("\x00\x00\x00\x11\x00\x0f\x00\x00\x0ctest.example")) {
+		t.Errorf("expected \"test.example\" server_name extension with given ServerName and hostname dial")
+	}
+}





More information about the tor-commits mailing list