From db1b65d2300d3a59616a43d2df4ea556b4a7d277 Mon Sep 17 00:00:00 2001 From: Sergey Frolov Date: Sat, 28 Jul 2018 18:37:20 -0400 Subject: [PATCH] Add Roller: Dialer that cycles thru ClientHellos --- examples/examples.go | 64 +++++++++++++++++++++++++- u_parrots.go | 2 +- u_roller.go | 106 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 u_roller.go diff --git a/examples/examples.go b/examples/examples.go index cb77c77..0c75d87 100644 --- a/examples/examples.go +++ b/examples/examples.go @@ -3,10 +3,14 @@ package main import ( "encoding/hex" "fmt" - tls "github.com/refraction-networking/utls" "net" + "net/http" + "net/http/httputil" "strings" "time" + + tls "github.com/refraction-networking/utls" + "golang.org/x/net/http2" ) var ( @@ -185,7 +189,7 @@ func HttpGetCustom(hostname string, addr string) (string, error) { }, Extensions: []tls.TLSExtension{ &tls.SNIExtension{}, - &tls.SupportedCurvesExtension{[]tls.CurveID{tls.X25519, tls.CurveP256}}, + &tls.SupportedCurvesExtension{Curves: []tls.CurveID{tls.X25519, tls.CurveP256}}, &tls.SupportedPointsExtension{SupportedPoints: []byte{0}}, &tls.SessionTicketExtension{}, &tls.ALPNExtension{AlpnProtocols: []string{"myFancyProtocol", "h2", "http/1.1"}}, @@ -221,9 +225,56 @@ func HttpGetCustom(hostname string, addr string) (string, error) { return string(buf), nil } +var roller *tls.Roller + +func HttpGetGoogleWithRoller() (string, error) { + var err error + hostname := "www.google.com" + if roller == nil { + roller, err = tls.NewRoller() + if err != nil { + return "", err + } + } + + // As of 2018-07-24 this tries to connect with Chrome, fails due to ChannelID extension + // being selected by Google, but not supported by utls, and seamlessly moves on to either + // Firefox or iOS fingerprints, which work. + c, err := roller.Dial("tcp4", hostname+":443", hostname) + if err != nil { + return "", err + } + if c.ConnectionState().NegotiatedProtocol == "h2" { + t := http2.Transport{} + h2c, err := t.NewClientConn(c) + if err != nil { + return "", err + } + req, err := http.NewRequest("GET", "/", nil) + if err != nil { + return "", err + } + resp, err := h2c.RoundTrip(req) + if err != nil { + return "", err + } + respbytes, err := httputil.DumpResponse(resp, true) + if err != nil { + return "", err + } + return string(respbytes), nil + } else { + c.Write([]byte("GET / HTTP/1.1\r\nHost: " + hostname + "\r\n\r\n")) + buf := make([]byte, 14096) + c.Read(buf) + return string(buf), nil + } +} + func main() { var response string var err error + requestHostname := "tlsfingerprint.io" requestAddr := "54.145.209.94:443" @@ -276,6 +327,15 @@ func main() { fmt.Printf("#> HttpGetCustom() response: %+s\n", getFirstLine(response)) } + for i := 0; i < 5; i++ { + response, err = HttpGetGoogleWithRoller() + if err != nil { + fmt.Printf("#> HttpGetGoogleWithRoller() failed: %+v\n", err) + } else { + fmt.Printf("#> HttpGetGoogleWithRoller() response: %+s\n", getFirstLine(response)) + } + } + return } diff --git a/u_parrots.go b/u_parrots.go index 138073d..3e34407 100644 --- a/u_parrots.go +++ b/u_parrots.go @@ -177,7 +177,7 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { func (uconn *UConn) applyPresetByID(id ClientHelloID) (err error) { var spec ClientHelloSpec // choose/generate the spec - switch uconn.clientHelloID { + switch id { case HelloRandomized: if tossBiasedCoin(0.5) { return uconn.applyPresetByID(HelloRandomizedALPN) diff --git a/u_roller.go b/u_roller.go new file mode 100644 index 0000000..8bb225e --- /dev/null +++ b/u_roller.go @@ -0,0 +1,106 @@ +package tls + +import ( + "net" + "sync" + "time" +) + +type Roller struct { + HelloIDs []ClientHelloID + HelloIDMu sync.Mutex + WorkingHelloID *ClientHelloID + TcpDialTimeout time.Duration + TlsHandshakeTimeout time.Duration +} + +// NewRoller creates Roller object with default range of HelloIDs to cycle through until a +// working/unblocked one is found. +func NewRoller() (*Roller, error) { + tcpDialTimeoutInc, err := getRandInt(14) + if err != nil { + return nil, err + } + tcpDialTimeoutInc = 7 + tcpDialTimeoutInc + + tlsHandshakeTimeoutInc, err := getRandInt(20) + if err != nil { + return nil, err + } + tlsHandshakeTimeoutInc = 11 + tlsHandshakeTimeoutInc + + return &Roller{ + HelloIDs: []ClientHelloID{ + HelloChrome_Auto, + HelloFirefox_Auto, + HelloIOS_Auto, + HelloRandomized, + }, + TcpDialTimeout: time.Second * time.Duration(tcpDialTimeoutInc), + TlsHandshakeTimeout: time.Second * time.Duration(tlsHandshakeTimeoutInc), + }, nil +} + +// Dial attempts to establish connection to given address using different HelloIDs. +// If a working HelloID is found, it is used again for subsequent Dials. +// If tcp connection fails or all HelloIDs are tried, returns with last error. +// +// Usage examples: +// Dial("tcp4", "google.com:443", "google.com") +// Dial("tcp", "10.23.144.22:443", "mywebserver.org") +func (c *Roller) Dial(network, addr, serverName string) (*UConn, error) { + helloIDs, err := shuffleClientHelloIDs(c.HelloIDs) + if err != nil { + return nil, err + } + + c.HelloIDMu.Lock() + workingHelloId := c.WorkingHelloID // keep using same helloID, if it works + c.HelloIDMu.Unlock() + if workingHelloId != nil { + for i, ID := range helloIDs { + if ID == *workingHelloId { + helloIDs[i] = helloIDs[0] + helloIDs[0] = *workingHelloId // push working hello ID first + break + } + } + } + + var tcpConn net.Conn + for _, helloID := range helloIDs { + tcpConn, err = net.DialTimeout(network, addr, c.TcpDialTimeout) + if err != nil { + return nil, err // on tcp Dial failure return with error right away + } + + client := UClient(tcpConn, nil, helloID) + client.SetSNI(serverName) + client.SetDeadline(time.Now().Add(c.TlsHandshakeTimeout)) + err = client.Handshake() + client.SetDeadline(time.Time{}) // unset timeout + if err != nil { + continue // on tls Dial error keep trying HelloIDs + } + + c.HelloIDMu.Lock() + c.WorkingHelloID = &helloID + c.HelloIDMu.Unlock() + return client, err + } + return nil, err +} + +// returns a shuffled copy of input +func shuffleClientHelloIDs(helloIDs []ClientHelloID) ([]ClientHelloID, error) { + perm, err := getRandPerm(len(helloIDs)) + if err != nil { + return nil, err + } + + shuffled := make([]ClientHelloID, len(helloIDs)) + for i, randI := range perm { + shuffled[i] = helloIDs[randI] + } + return shuffled, nil +}