Add Roller: Dialer that cycles thru ClientHellos

This commit is contained in:
Sergey Frolov 2018-07-28 18:37:20 -04:00 committed by sergeyfrolov
parent 4c28dcf6db
commit db1b65d230
3 changed files with 169 additions and 3 deletions

View file

@ -3,10 +3,14 @@ package main
import ( import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
tls "github.com/refraction-networking/utls"
"net" "net"
"net/http"
"net/http/httputil"
"strings" "strings"
"time" "time"
tls "github.com/refraction-networking/utls"
"golang.org/x/net/http2"
) )
var ( var (
@ -185,7 +189,7 @@ func HttpGetCustom(hostname string, addr string) (string, error) {
}, },
Extensions: []tls.TLSExtension{ Extensions: []tls.TLSExtension{
&tls.SNIExtension{}, &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.SupportedPointsExtension{SupportedPoints: []byte{0}},
&tls.SessionTicketExtension{}, &tls.SessionTicketExtension{},
&tls.ALPNExtension{AlpnProtocols: []string{"myFancyProtocol", "h2", "http/1.1"}}, &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 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() { func main() {
var response string var response string
var err error var err error
requestHostname := "tlsfingerprint.io" requestHostname := "tlsfingerprint.io"
requestAddr := "54.145.209.94:443" requestAddr := "54.145.209.94:443"
@ -276,6 +327,15 @@ func main() {
fmt.Printf("#> HttpGetCustom() response: %+s\n", getFirstLine(response)) 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 return
} }

View file

@ -177,7 +177,7 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) {
func (uconn *UConn) applyPresetByID(id ClientHelloID) (err error) { func (uconn *UConn) applyPresetByID(id ClientHelloID) (err error) {
var spec ClientHelloSpec var spec ClientHelloSpec
// choose/generate the spec // choose/generate the spec
switch uconn.clientHelloID { switch id {
case HelloRandomized: case HelloRandomized:
if tossBiasedCoin(0.5) { if tossBiasedCoin(0.5) {
return uconn.applyPresetByID(HelloRandomizedALPN) return uconn.applyPresetByID(HelloRandomizedALPN)

106
u_roller.go Normal file
View file

@ -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
}