feat(dialer): add utls dialer for TLS ClientHello fingerprint simulation
Add a new "utls" dialer type that uses the refraction-networking/utls library to mimic browser TLS fingerprints (Chrome, Firefox, Safari, etc.). - x/dialer/utls/dialer.go: Registered as "utls", mirrors the TLS dialer with Handshake() using utls.UClient() for fingerprint emulation. - x/dialer/utls/fingerprint.go: Maps string names (chrome, firefox, ios, safari, edge, randomized, golang, custom) to utls.ClientHelloID presets. - x/dialer/utls/metadata.go: Parses fingerprint from dialer metadata. - go.mod: Add github.com/refraction-networking/utls v1.8.2. The fingerprint is dialer-only metadata — the standard TLS dialer is untouched. Falls back to crypto/tls if no fingerprint is configured. Closes go-gost/gost#31
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package utls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-gost/core/dialer"
|
||||
"github.com/go-gost/core/logger"
|
||||
md "github.com/go-gost/core/metadata"
|
||||
xctx "github.com/go-gost/x/ctx"
|
||||
xnet "github.com/go-gost/x/internal/net"
|
||||
"github.com/go-gost/x/internal/net/proxyproto"
|
||||
"github.com/go-gost/x/registry"
|
||||
utls "github.com/refraction-networking/utls"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.DialerRegistry().Register("utls", NewDialer)
|
||||
}
|
||||
|
||||
type utlsDialer struct {
|
||||
md metadata
|
||||
log logger.Logger
|
||||
options dialer.Options
|
||||
}
|
||||
|
||||
func NewDialer(opts ...dialer.Option) dialer.Dialer {
|
||||
options := dialer.Options{}
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
return &utlsDialer{
|
||||
log: options.Logger,
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *utlsDialer) Init(md md.Metadata) (err error) {
|
||||
return d.parseMetadata(md)
|
||||
}
|
||||
|
||||
func (d *utlsDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) {
|
||||
var options dialer.DialOptions
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
conn, err := options.Dialer.Dial(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
d.log.Error(err)
|
||||
}
|
||||
|
||||
if d.md.keepalive {
|
||||
xnet.ApplyKeepalive(conn, net.KeepAliveConfig{
|
||||
Enable: true,
|
||||
Idle: d.md.keepaliveIdle,
|
||||
Interval: d.md.keepaliveInterval,
|
||||
Count: d.md.keepaliveCount,
|
||||
})
|
||||
}
|
||||
|
||||
conn = proxyproto.WrapClientConn(
|
||||
d.options.ProxyProtocol,
|
||||
xctx.SrcAddrFromContext(ctx),
|
||||
xctx.DstAddrFromContext(ctx),
|
||||
conn)
|
||||
|
||||
return conn, err
|
||||
}
|
||||
|
||||
// Handshake implements dialer.Handshaker.
|
||||
func (d *utlsDialer) Handshake(ctx context.Context, conn net.Conn, options ...dialer.HandshakeOption) (net.Conn, error) {
|
||||
if d.md.handshakeTimeout > 0 {
|
||||
conn.SetDeadline(time.Now().Add(d.md.handshakeTimeout))
|
||||
defer conn.SetDeadline(time.Time{})
|
||||
}
|
||||
|
||||
clientHelloID, ok := GetClientHelloID(d.md.fingerprint)
|
||||
if ok {
|
||||
d.log.Debugf("utls handshake with fingerprint: %s", d.md.fingerprint)
|
||||
utlsCfg := (*utls.Config)(unsafe.Pointer(d.options.TLSConfig))
|
||||
uconn := utls.UClient(conn, utlsCfg, clientHelloID)
|
||||
if err := uconn.HandshakeContext(ctx); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return uconn, nil
|
||||
}
|
||||
|
||||
// fingerprint not set, empty, "golang", or unknown: fall through to
|
||||
// standard crypto/tls.
|
||||
if d.md.fingerprint != "" {
|
||||
d.log.Warnf("unknown utls fingerprint: %s, falling back to standard TLS", d.md.fingerprint)
|
||||
}
|
||||
tlsConn := tls.Client(conn, d.options.TLSConfig)
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tlsConn, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package utls
|
||||
|
||||
import (
|
||||
utls "github.com/refraction-networking/utls"
|
||||
)
|
||||
|
||||
// fingerprintMap maps user-facing string names to uTLS ClientHelloID presets.
|
||||
// Each _Auto alias automatically tracks the latest known fingerprint for that
|
||||
// browser in the uTLS library.
|
||||
var fingerprintMap = map[string]utls.ClientHelloID{
|
||||
"chrome": utls.HelloChrome_Auto,
|
||||
"firefox": utls.HelloFirefox_Auto,
|
||||
"ios": utls.HelloIOS_Auto,
|
||||
"safari": utls.HelloSafari_Auto,
|
||||
"edge": utls.HelloEdge_Auto,
|
||||
"randomized": utls.HelloRandomized,
|
||||
"randomized-alpn": utls.HelloRandomizedALPN,
|
||||
"randomized-noalpn": utls.HelloRandomizedNoALPN,
|
||||
"golang": utls.HelloGolang,
|
||||
"custom": utls.HelloCustom,
|
||||
}
|
||||
|
||||
// GetClientHelloID returns the utls.ClientHelloID for the given fingerprint
|
||||
// name. The empty string and "golang" both return ok=false, signalling the
|
||||
// caller to fall through to standard crypto/tls. Unknown names also return
|
||||
// ok=false.
|
||||
func GetClientHelloID(name string) (utls.ClientHelloID, bool) {
|
||||
if name == "" || name == "golang" {
|
||||
return utls.ClientHelloID{}, false
|
||||
}
|
||||
id, ok := fingerprintMap[name]
|
||||
return id, ok
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package utls
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
mdata "github.com/go-gost/core/metadata"
|
||||
mdutil "github.com/go-gost/x/metadata/util"
|
||||
)
|
||||
|
||||
type metadata struct {
|
||||
handshakeTimeout time.Duration
|
||||
|
||||
keepalive bool
|
||||
keepaliveIdle time.Duration
|
||||
keepaliveInterval time.Duration
|
||||
keepaliveCount int
|
||||
|
||||
fingerprint string
|
||||
}
|
||||
|
||||
func (d *utlsDialer) parseMetadata(md mdata.Metadata) (err error) {
|
||||
const (
|
||||
handshakeTimeout = "handshakeTimeout"
|
||||
fingerprint = "fingerprint"
|
||||
)
|
||||
|
||||
d.md.handshakeTimeout = mdutil.GetDuration(md, handshakeTimeout)
|
||||
|
||||
d.md.keepalive = mdutil.GetBool(md, "keepalive")
|
||||
d.md.keepaliveIdle = mdutil.GetDuration(md, "keepalive.idle")
|
||||
d.md.keepaliveInterval = mdutil.GetDuration(md, "keepalive.interval")
|
||||
d.md.keepaliveCount = mdutil.GetInt(md, "keepalive.count")
|
||||
|
||||
d.md.fingerprint = mdutil.GetString(md, fingerprint)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
# Plan: Add uTLS Fingerprint Dialer (`utls`)
|
||||
|
||||
## Context
|
||||
|
||||
**Issue**: [go-gost/gost#31](https://github.com/go-gost/gost/issues/31) — Feature request to add TLS ClientHello fingerprint simulation using the [uTLS](https://github.com/refraction-networking/utls) library. GOST currently uses Go's `crypto/tls` which produces a distinctive Go TLS fingerprint that's easily blocked. Users want to mimic Chrome/Firefox/Safari/etc. fingerprints.
|
||||
|
||||
**Approach**: New `"utls"` dialer type (standalone, not modifying the existing `"tls"` dialer). The fingerprint is dialer-only metadata — not on the shared `TLSConfig` struct. This keeps the standard TLS dialer untouched and avoids pulling in the uTLS dependency for all users.
|
||||
|
||||
## Files to Create
|
||||
|
||||
### 1. `x/dialer/utls/fingerprint.go` — String→ClientHelloID map
|
||||
|
||||
Maps user-facing fingerprint names to `utls.ClientHelloID` presets. Supported names: `chrome`, `firefox`, `ios`, `safari`, `edge`, `randomized`, `randomized-alpn`, `randomized-noalpn`, `golang`, `custom`. Empty string and `"golang"` both mean "fall through to standard crypto/tls" (return `ok=false`). Unknown names get a warning log and also fall through.
|
||||
|
||||
### 2. `x/dialer/utls/metadata.go` — Metadata parsing
|
||||
|
||||
Mirrors `dialer/tls/metadata.go` exactly, adding a `fingerprint string` field. Parsed keys:
|
||||
- `handshakeTimeout` (duration)
|
||||
- `keepalive`, `keepalive.idle`, `keepalive.interval`, `keepalive.count` (TCP keepalive)
|
||||
- `fingerprint` (string) — **new**
|
||||
|
||||
### 3. `x/dialer/utls/dialer.go` — Core dialer
|
||||
|
||||
Mirrors `dialer/tls/dialer.go`. Registered as `"utls"`. The `Dial()` method is identical to the TLS dialer. The `Handshake()` method diverges: looks up the fingerprint; if `ok`, uses `utls.UClient(conn, tlsConfig, clientHelloID)`; otherwise falls through to `crypto/tls.Client()`.
|
||||
|
||||
## Files to Modify
|
||||
|
||||
### 4. `x/go.mod` — Add uTLS dependency
|
||||
|
||||
Add `github.com/refraction-networking/utls v1.8.2` to the require block, then run `go mod tidy` from `x/`.
|
||||
|
||||
### 5. `gost/cmd/gost/register.go` — Blank import
|
||||
|
||||
Add `_ "github.com/go-gost/x/dialer/utls"` in the "Register dialers" section, alphabetically between the `unix` and `ws` imports.
|
||||
|
||||
## No Changes Needed
|
||||
|
||||
- **Config parsing** — `node/parse.go` already passes `dialCfg.Metadata` to `d.Init()`; no changes required to any parsing code.
|
||||
- **`config/config.go`** — `DialerConfig.Metadata` already supports arbitrary keys; `fingerprint` flows through naturally.
|
||||
- **Metadata key constants** — No new constant in `config/parsing/parse.go`; local `const` in `parseMetadata()` follows existing dialer convention.
|
||||
|
||||
## Usage Example
|
||||
|
||||
```yaml
|
||||
services:
|
||||
- name: service-0
|
||||
addr: :8080
|
||||
handler:
|
||||
type: tcp
|
||||
listener:
|
||||
type: tcp
|
||||
forwarder:
|
||||
nodes:
|
||||
- name: target-0
|
||||
addr: example.com:443
|
||||
dialer:
|
||||
type: utls
|
||||
metadata:
|
||||
fingerprint: chrome
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# 1. Tidy dependencies
|
||||
cd x && go mod tidy
|
||||
|
||||
# 2. Build + vet the x module
|
||||
cd x && go build ./... && go vet ./...
|
||||
|
||||
# 3. Build + vet the gost binary (verifies blank import)
|
||||
cd gost && go build ./cmd/gost/... && go vet ./...
|
||||
```
|
||||
|
||||
No unit tests needed — the existing codebase has no tests in `x/dialer/`.
|
||||
@@ -28,6 +28,7 @@ require (
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/quic-go/quic-go v0.59.1
|
||||
github.com/quic-go/webtransport-go v0.10.0
|
||||
github.com/refraction-networking/utls v1.8.2
|
||||
github.com/rs/xid v1.3.0
|
||||
github.com/shadowsocks/go-shadowsocks2 v0.1.5
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
||||
@@ -56,7 +57,11 @@ require (
|
||||
gvisor.dev/gvisor v0.0.0-20250523182742-eede7a881b20
|
||||
)
|
||||
|
||||
require github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alessio/shellescape v1.4.1 // indirect
|
||||
|
||||
@@ -4,6 +4,8 @@ github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAu
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
|
||||
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ=
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -123,6 +125,8 @@ github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9q
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
@@ -188,6 +192,8 @@ github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBi
|
||||
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/quic-go/webtransport-go v0.10.0 h1:LqXXPOXuETY5Xe8ITdGisBzTYmUOy5eSj+9n4hLTjHI=
|
||||
github.com/quic-go/webtransport-go v0.10.0/go.mod h1:LeGIXr5BQKE3UsynwVBeQrU1TPrbh73MGoC6jd+V7ow=
|
||||
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
|
||||
Reference in New Issue
Block a user