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:
ginuerzh
2026-06-20 15:43:25 +08:00
parent 66f502b153
commit 9fcd7d6890
6 changed files with 263 additions and 1 deletions
+106
View File
@@ -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
}
+33
View File
@@ -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
}
+37
View File
@@ -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
}