-
Notifications
You must be signed in to change notification settings - Fork 325
Description
I'm using commit a89e7e6. The examples I have found of using utls with HTTPS all make a single request on a single connection, then throw the connection away. For example, httpGetOverConn in examples.go.
I'm trying to use utls with http.Transport, to take advantage of persistent connections and reasonable default timeouts. To do this, I'm hooking into the DialTLS callback. There is a problem when using a utls fingerprint that includes h2 in ALPN and a server that supports HTTP/2. The server switches to HTTP/2 mode, but the client stays in HTTP/1.1 mode, because net/http disables automatic HTTP/2 support whenever DialTLS is set. The end result is an HTTP/1.1 client speaking to an HTTP/2 server; i.e, a similar problem as what was reported in golang/go#14275 (comment). The error message differs depending on the fingerprint:
HelloFirefox_63net/http: HTTP/1.x transport connection broken: malformed HTTP response "\x00\x00\x12\x04\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00d\x00\x04\x00\x10\x00\x00\x00\x06\x00\x00@\x00\x00\x00\x04\b\x00\x00\x00\x00\x00\x00\x0f\x00\x01\x00\x00\x1e\a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01http2_handshake_failed"HelloChrome_70local error: tls: unexpected messageHelloIOS_11_12019/01/11 14:48:56 Unsolicited response received on idle HTTP channel starting with "\x00\x00\x12\x04\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00d\x00\x04\x00\x10\x00\x00\x00\x06\x00\x00@\x00\x00\x00\x04\b\x00\x00\x00\x00\x00\x00\x0f\x00\x01"; err=<nil>
readLoopPeekFailLocked: <nil>
I get the same results even if I pre-configure the http.Transport with HTTP/2 support by calling http2.ConfigureTransport(tr).
I wrote a test program to reproduce these results. It takes a -utls option to select a utls client hello ID, and a -callhandshake option to control whether to call UConn.Handshake within DialTLS, or allow it to be called implicitly by the next Read or Write. I included the latter option because I found that not calling UConn.Handshake inside DialTLS avoids the HTTP version mismatch; however it also results in a client hello that lacks ALPN and differs from the requested one in other ways, so it's not an adequate workaround.
Click to expand program
package main
import (
"flag"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"strings"
utls "github.com/refraction-networking/utls"
)
func main() {
utlsClientHelloIDName := flag.String("utls", "", "use utls with the given ClientHelloID (e.g. HelloGolang)")
callHandshake := flag.Bool("callhandshake", false, "call UConn.Handshake inside DialTLS")
flag.Parse()
if *callHandshake && *utlsClientHelloIDName == "" {
fmt.Fprintf(os.Stderr, "error: -callhandshake only makes sense with -utls\n")
os.Exit(1)
}
if flag.NArg() != 1 {
fmt.Fprintf(os.Stderr, "error: need a URL\n")
os.Exit(1)
}
url := flag.Arg(0)
utlsClientHelloID, ok := map[string]*utls.ClientHelloID{
"": nil,
"HelloGolang": &utls.HelloGolang,
"HelloRandomized": &utls.HelloRandomized,
"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,
}[*utlsClientHelloIDName]
if !ok {
fmt.Fprintf(os.Stderr, "unknown client hello ID %q\n", *utlsClientHelloIDName)
os.Exit(1)
}
tr := http.DefaultTransport.(*http.Transport)
if utlsClientHelloID != nil {
tr.DialContext = nil
tr.Dial = func(network, addr string) (net.Conn, error) { panic("Dial should not be called") }
tr.DialTLS = func(network, addr string) (net.Conn, error) {
fmt.Printf("DialTLS(%q, %q)\n", network, addr)
if tr.TLSClientConfig != nil {
fmt.Printf("warning: ignoring TLSClientConfig %v\n", tr.TLSClientConfig)
}
conn, err := net.Dial(network, addr)
if err != nil {
return nil, err
}
uconn := utls.UClient(conn, nil, *utlsClientHelloID)
colonPos := strings.LastIndex(addr, ":")
if colonPos == -1 {
colonPos = len(addr)
}
uconn.SetSNI(addr[:colonPos])
if *callHandshake {
err = uconn.Handshake()
}
return uconn, err
}
}
for i := 0; i < 4; i++ {
resp, err := get(tr, url)
if err != nil {
fmt.Printf("%2d err %v\n", i, err)
} else {
fmt.Printf("%2d %s %s\n", i, resp.Proto, resp.Status)
}
}
}
func get(rt http.RoundTripper, url string) (*http.Response, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
resp, err := rt.RoundTrip(req)
if err != nil {
return nil, err
}
// Read and close the body to enable connection reuse with HTTP/1.1.
_, err = io.Copy(ioutil.Discard, resp.Body)
if err != nil {
return nil, err
}
err = resp.Body.Close()
if err != nil {
return nil, err
}
return resp, nil
}
Sample usage:
test -utls HelloFirefox_63 -callhandshake https://golang.org/robots.txt
The output of the program appears in the following table. Things to notice:
DialTLSwithHelloGolangproduces a fingerprint that is different from usinghttp.TransportwithoutDialTLSset.HelloFirefox_63,HelloChrome_70, andHelloIOS_11_1all provide a usable connection (but with an incorrect fingerprint), as long as you don't callUConn.Handshakebefore returning fromDialTLS.HelloFirefox_63,HelloChrome_70, andHelloIOS_11_1all give the correct fingerprint, but fail with an HTTP version mismatch, whenUConn.Handshakeis called insideDialTLS.
| Client Hello ID | call Handshake? |
client ALPN | result |
|---|---|---|---|
| none | N/A | [h2, http/1.1] | ok HTTP/2 |
-utls HelloGolang |
none | ok HTTP/1.1 | |
-utls HelloGolang |
-callhandshake |
none | ok HTTP/1.1 |
-utls HelloFirefox_63 |
none | ok HTTP/1.1 | |
-utls HelloFirefox_63 |
-callhandshake |
[h2, http/1.1] | malformed HTTP response (HTTP/1.1 client, HTTP/2 server) |
-utls HelloChrome_70 |
none | ok HTTP/1.1 | |
-utls HelloChrome_70 |
-callhandshake |
[h2, http/1.1] | local error: tls: unexpected message |
-utls HelloIOS_11_1 |
none | ok HTTP/1.1 | |
-utls HelloIOS_11_1 |
-callhandshake |
[h2, h2-16, h2-15, h2-14, spdy/3.1, spdy/3, http/1.1] | readLoopPeekFailLocked: <nil> (HTTP/1.1 client, HTTP/2 server) |
Is there a way to accomplish what I am trying to do?