From 2ee0f634e6e4ad04bdb05a159502f407e15ab26a Mon Sep 17 00:00:00 2001 From: Larvan2 <78135608+Larvan2@users.noreply.github.com> Date: Wed, 1 Feb 2023 22:16:06 +0800 Subject: [PATCH] feat: Add utls for modifying client's fingerprint. Currently only support TLS transport in TCP/GRPC/WS/HTTP for VLESS/Vmess and trojan-grpc. --- adapter/outbound/trojan.go | 44 +++++++++--------- adapter/outbound/vless.go | 68 ++++++++++++++------------- adapter/outbound/vmess.go | 37 +++++++++------ docs/config.yaml | 19 ++++---- go.mod | 4 ++ go.sum | 8 ++++ transport/gun/gun.go | 43 +++++++++++++---- transport/trojan/trojan.go | 15 +++--- transport/vmess/tls.go | 29 ++++++++++-- transport/vmess/utls.go | 90 ++++++++++++++++++++++++++++++++++++ transport/vmess/websocket.go | 32 ++++++++++--- 11 files changed, 285 insertions(+), 104 deletions(-) create mode 100644 transport/vmess/utls.go diff --git a/adapter/outbound/trojan.go b/adapter/outbound/trojan.go index c90ee377..cd0f1476 100644 --- a/adapter/outbound/trojan.go +++ b/adapter/outbound/trojan.go @@ -30,20 +30,21 @@ type Trojan struct { type TrojanOption struct { BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port"` - Password string `proxy:"password"` - ALPN []string `proxy:"alpn,omitempty"` - SNI string `proxy:"sni,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - UDP bool `proxy:"udp,omitempty"` - Network string `proxy:"network,omitempty"` - GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` - WSOpts WSOptions `proxy:"ws-opts,omitempty"` - Flow string `proxy:"flow,omitempty"` - FlowShow bool `proxy:"flow-show,omitempty"` + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + Password string `proxy:"password"` + ALPN []string `proxy:"alpn,omitempty"` + SNI string `proxy:"sni,omitempty"` + SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` + Fingerprint string `proxy:"fingerprint,omitempty"` + UDP bool `proxy:"udp,omitempty"` + Network string `proxy:"network,omitempty"` + GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` + WSOpts WSOptions `proxy:"ws-opts,omitempty"` + Flow string `proxy:"flow,omitempty"` + FlowShow bool `proxy:"flow-show,omitempty"` + ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) { @@ -212,12 +213,13 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) tOption := &trojan.Option{ - Password: option.Password, - ALPN: option.ALPN, - ServerName: option.Server, - SkipCertVerify: option.SkipCertVerify, - FlowShow: option.FlowShow, - Fingerprint: option.Fingerprint, + Password: option.Password, + ALPN: option.ALPN, + ServerName: option.Server, + SkipCertVerify: option.SkipCertVerify, + FlowShow: option.FlowShow, + Fingerprint: option.Fingerprint, + ClientFingerprint: option.ClientFingerprint, } switch option.Network { @@ -277,7 +279,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { } } - t.transport = gun.NewHTTP2Client(dialFn, tlsConfig) + t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, tOption.ClientFingerprint) t.gunTLSConfig = tlsConfig t.gunConfig = &gun.Config{ diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index a14bd528..96345a6d 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -12,10 +12,6 @@ import ( "strconv" "sync" - vmessSing "github.com/sagernet/sing-vmess" - "github.com/sagernet/sing-vmess/packetaddr" - M "github.com/sagernet/sing/common/metadata" - "github.com/Dreamacro/clash/common/convert" "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/resolver" @@ -25,6 +21,10 @@ import ( "github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/vless" "github.com/Dreamacro/clash/transport/vmess" + + vmessSing "github.com/sagernet/sing-vmess" + "github.com/sagernet/sing-vmess/packetaddr" + M "github.com/sagernet/sing/common/metadata" ) const ( @@ -45,27 +45,28 @@ type Vless struct { type VlessOption struct { BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port"` - UUID string `proxy:"uuid"` - Flow string `proxy:"flow,omitempty"` - FlowShow bool `proxy:"flow-show,omitempty"` - TLS bool `proxy:"tls,omitempty"` - UDP bool `proxy:"udp,omitempty"` - PacketAddr bool `proxy:"packet-addr,omitempty"` - XUDP bool `proxy:"xudp,omitempty"` - PacketEncoding string `proxy:"packet-encoding,omitempty"` - Network string `proxy:"network,omitempty"` - HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` - HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` - GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` - WSOpts WSOptions `proxy:"ws-opts,omitempty"` - WSPath string `proxy:"ws-path,omitempty"` - WSHeaders map[string]string `proxy:"ws-headers,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - ServerName string `proxy:"servername,omitempty"` + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + UUID string `proxy:"uuid"` + Flow string `proxy:"flow,omitempty"` + FlowShow bool `proxy:"flow-show,omitempty"` + TLS bool `proxy:"tls,omitempty"` + UDP bool `proxy:"udp,omitempty"` + PacketAddr bool `proxy:"packet-addr,omitempty"` + XUDP bool `proxy:"xudp,omitempty"` + PacketEncoding string `proxy:"packet-encoding,omitempty"` + Network string `proxy:"network,omitempty"` + HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` + HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` + GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` + WSOpts WSOptions `proxy:"ws-opts,omitempty"` + WSPath string `proxy:"ws-path,omitempty"` + WSHeaders map[string]string `proxy:"ws-headers,omitempty"` + SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` + Fingerprint string `proxy:"fingerprint,omitempty"` + ServerName string `proxy:"servername,omitempty"` + ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { @@ -80,6 +81,7 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { Path: v.option.WSOpts.Path, MaxEarlyData: v.option.WSOpts.MaxEarlyData, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, + ClientFingerprint: v.option.ClientFingerprint, Headers: http.Header{}, } @@ -179,9 +181,10 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) } else if v.option.TLS { tlsOpts := vmess.TLSConfig{ - Host: host, - SkipCertVerify: v.option.SkipCertVerify, - FingerPrint: v.option.Fingerprint, + Host: host, + SkipCertVerify: v.option.SkipCertVerify, + FingerPrint: v.option.Fingerprint, + ClientFingerprint: v.option.ClientFingerprint, } if isH2 { @@ -526,8 +529,9 @@ func NewVless(option VlessOption) (*Vless, error) { } gunConfig := &gun.Config{ - ServiceName: v.option.GrpcOpts.GrpcServiceName, - Host: v.option.ServerName, + ServiceName: v.option.GrpcOpts.GrpcServiceName, + Host: v.option.ServerName, + ClientFingerprint: v.option.ClientFingerprint, } tlsConfig := tlsC.GetGlobalTLSConfig(&tls.Config{ InsecureSkipVerify: v.option.SkipCertVerify, @@ -542,7 +546,9 @@ func NewVless(option VlessOption) (*Vless, error) { v.gunTLSConfig = tlsConfig v.gunConfig = gunConfig - v.transport = gun.NewHTTP2Client(dialFn, tlsConfig) + + v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint) + } return v, nil diff --git a/adapter/outbound/vmess.go b/adapter/outbound/vmess.go index 727da2ee..1e90a1bc 100644 --- a/adapter/outbound/vmess.go +++ b/adapter/outbound/vmess.go @@ -11,15 +11,14 @@ import ( "strings" "sync" - tlsC "github.com/Dreamacro/clash/component/tls" - vmess "github.com/sagernet/sing-vmess" - "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/resolver" + tlsC "github.com/Dreamacro/clash/component/tls" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/transport/gun" clashVMess "github.com/Dreamacro/clash/transport/vmess" + vmess "github.com/sagernet/sing-vmess" "github.com/sagernet/sing-vmess/packetaddr" M "github.com/sagernet/sing/common/metadata" ) @@ -60,6 +59,7 @@ type VmessOption struct { PacketEncoding string `proxy:"packet-encoding,omitempty"` GlobalPadding bool `proxy:"global-padding,omitempty"` AuthenticatedLength bool `proxy:"authenticated-length,omitempty"` + ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } type HTTPOptions struct { @@ -97,6 +97,7 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { Path: v.option.WSOpts.Path, MaxEarlyData: v.option.WSOpts.MaxEarlyData, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, + ClientFingerprint: v.option.ClientFingerprint, Headers: http.Header{}, } @@ -134,8 +135,9 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { if v.option.TLS { host, _, _ := net.SplitHostPort(v.addr) tlsOpts := &clashVMess.TLSConfig{ - Host: host, - SkipCertVerify: v.option.SkipCertVerify, + Host: host, + SkipCertVerify: v.option.SkipCertVerify, + ClientFingerprint: v.option.ClientFingerprint, } if v.option.ServerName != "" { @@ -160,9 +162,10 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { case "h2": host, _, _ := net.SplitHostPort(v.addr) tlsOpts := clashVMess.TLSConfig{ - Host: host, - SkipCertVerify: v.option.SkipCertVerify, - NextProtos: []string{"h2"}, + Host: host, + SkipCertVerify: v.option.SkipCertVerify, + NextProtos: []string{"h2"}, + ClientFingerprint: v.option.ClientFingerprint, } if v.option.ServerName != "" { @@ -187,8 +190,9 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { if v.option.TLS { host, _, _ := net.SplitHostPort(v.addr) tlsOpts := &clashVMess.TLSConfig{ - Host: host, - SkipCertVerify: v.option.SkipCertVerify, + Host: host, + SkipCertVerify: v.option.SkipCertVerify, + ClientFingerprint: v.option.ClientFingerprint, } if v.option.ServerName != "" { @@ -252,7 +256,7 @@ func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta // ListenPacketContext implements C.ProxyAdapter func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { - // vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr + // vmess use stream-oriented udp with a special address, so we need a net.UDPAddr if !metadata.Resolved() { ip, err := resolver.ResolveIP(ctx, metadata.Host) if err != nil { @@ -295,7 +299,7 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o // ListenPacketWithDialer implements C.ProxyAdapter func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { - // vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr + // vmess use stream-oriented udp with a special address, so we need a net.UDPAddr if !metadata.Resolved() { ip, err := resolver.ResolveIP(ctx, metadata.Host) if err != nil { @@ -402,8 +406,9 @@ func NewVmess(option VmessOption) (*Vmess, error) { } gunConfig := &gun.Config{ - ServiceName: v.option.GrpcOpts.GrpcServiceName, - Host: v.option.ServerName, + ServiceName: v.option.GrpcOpts.GrpcServiceName, + Host: v.option.ServerName, + ClientFingerprint: v.option.ClientFingerprint, } tlsConfig := &tls.Config{ InsecureSkipVerify: v.option.SkipCertVerify, @@ -418,7 +423,9 @@ func NewVmess(option VmessOption) (*Vmess, error) { v.gunTLSConfig = tlsConfig v.gunConfig = gunConfig - v.transport = gun.NewHTTP2Client(dialFn, tlsConfig) + + v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint) + } return v, nil } diff --git a/docs/config.yaml b/docs/config.yaml index 1215b21d..705867ff 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -23,7 +23,6 @@ geox-url: geosite: "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat" mmdb: "https://cdn.jsdelivr.net/gh/Loyalsoldier/geoip@release/Country.mmdb" - log-level: debug # 日志等级 silent/error/warning/info/debug ipv6: true # 开启 IPv6 总开关,关闭阻断所有 IPv6 链接和屏蔽 DNS 请求 AAAA 记录 @@ -237,9 +236,9 @@ dns: # 配置查询域名使用的 DNS 服务器 nameserver-policy: - # 'www.baidu.com': '114.114.114.114' - # '+.internal.crop.com': '10.0.0.1' - 'geosite:cn': 'https://doh.pub/dns-query' + # 'www.baidu.com': '114.114.114.114' + # '+.internal.crop.com': '10.0.0.1' + "geosite:cn": "https://doh.pub/dns-query" proxies: # Shadowsocks @@ -255,9 +254,8 @@ proxies: server: server port: 443 cipher: chacha20-ietf-poly1305 - password: - "password" - # udp: true + password: "password" + # udp: true # udp-over-tcp: false # ip-version: ipv4 # 设置节点使用 IP 版本,可选:dual,ipv4,ipv6,ipv4-prefer,ipv6-prefer。默认使用 dual # ipv4:仅使用 IPv4 ipv6:仅使用 IPv6 @@ -319,6 +317,7 @@ proxies: # udp: true # tls: true # fingerprint: xxxx + # client-fingerprint: random # Available: "chrome","firefox","safari","random" # skip-cert-verify: true # servername: example.com # priority over wss host # network: ws @@ -483,6 +482,7 @@ proxies: # flow: xtls-rprx-direct # xtls-rprx-origin # enable XTLS # skip-cert-verify: true # fingerprint: xxxx + # client-fingerprint: random # Available: "chrome","firefox","safari","random" - name: "vless-ws" type: vless @@ -492,6 +492,7 @@ proxies: udp: true tls: true network: ws + # client-fingerprint: random # Available: "chrome","firefox","safari","random" servername: example.com # priority over wss host # skip-cert-verify: true # fingerprint: xxxx @@ -535,9 +536,7 @@ proxies: private-key: eCtXsJZ27+4PbhDkHnB923tkUn2Gj59wZw5wFA75MnU= public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo= udp: true -# reserved: 'U4An' - - + # reserved: 'U4An' - name: tuic server: www.example.com port: 10443 diff --git a/go.mod b/go.mod index bccc2135..8773ecef 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,9 @@ require ( github.com/metacubex/sing-tun v0.1.1-0.20230129141228-645f74b2208b github.com/metacubex/sing-wireguard v0.0.0-20230129141512-65b25e764f8e github.com/miekg/dns v1.1.50 + github.com/mroth/weightedrand/v2 v2.0.0 github.com/oschwald/geoip2-golang v1.8.0 + github.com/refraction-networking/utls v1.2.0 github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 github.com/sagernet/sing v0.1.6 github.com/sagernet/sing-vmess v0.1.1 @@ -48,6 +50,7 @@ require ( require ( github.com/ajg/form v1.5.1 // indirect + github.com/andybalholm/brotli v1.0.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect @@ -56,6 +59,7 @@ require ( github.com/google/go-cmp v0.5.9 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/josharian/native v1.1.0 // indirect + github.com/klauspost/compress v1.15.12 // indirect github.com/klauspost/cpuid/v2 v2.0.12 // indirect github.com/marten-seemann/qpack v0.3.0 // indirect github.com/marten-seemann/qtls-go1-18 v0.1.3 // indirect diff --git a/go.sum b/go.sum index b850cce9..16829ad0 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmH github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -67,6 +69,8 @@ github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGu github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM= +github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= @@ -103,6 +107,8 @@ github.com/metacubex/sing-wireguard v0.0.0-20230129141512-65b25e764f8e h1:ZpzW8y github.com/metacubex/sing-wireguard v0.0.0-20230129141512-65b25e764f8e/go.mod h1:hF5lqFsfWeDrImIQ5XkOTS8aucCWvK4GOoCUNYKTrPU= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/mroth/weightedrand/v2 v2.0.0 h1:ADehnByWbliEDIazDAKFdBHoqgHSXAkgyKqM/9YsPoo= +github.com/mroth/weightedrand/v2 v2.0.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= @@ -113,6 +119,8 @@ github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYx github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/refraction-networking/utls v1.2.0 h1:U5f8wkij2NVinfLuJdFP3gCMwIHs+EzvhxmYdXgiapo= +github.com/refraction-networking/utls v1.2.0/go.mod h1:NPq+cVqzH7D1BeOkmOcb5O/8iVewAsiVt2x1/eO0hgQ= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e h1:5CFRo8FJbCuf5s/eTBdZpmMbn8Fe2eSMLNAYfKanA34= github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e/go.mod h1:qbt0dWObotCfcjAJJ9AxtFPNSDUfZF+6dCpgKEOBn/g= diff --git a/transport/gun/gun.go b/transport/gun/gun.go index 0ff68fca..a176e8fb 100644 --- a/transport/gun/gun.go +++ b/transport/gun/gun.go @@ -19,6 +19,8 @@ import ( "github.com/Dreamacro/clash/common/buf" "github.com/Dreamacro/clash/common/pool" + U "github.com/Dreamacro/clash/transport/vmess" + utls "github.com/refraction-networking/utls" "go.uber.org/atomic" "golang.org/x/net/http2" @@ -51,8 +53,9 @@ type Conn struct { } type Config struct { - ServiceName string - Host string + ServiceName string + Host string + ClientFingerprint string } func (g *Conn) initRequest() { @@ -188,8 +191,9 @@ func (g *Conn) SetDeadline(t time.Time) error { return nil } -func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config) *TransportWrap { +func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, Fingerprint string) *TransportWrap { wrap := TransportWrap{} + dialFunc := func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { pconn, err := dialFn(network, addr) if err != nil { @@ -197,17 +201,38 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config) *TransportWrap { } wrap.remoteAddr = pconn.RemoteAddr() - cn := tls.Client(pconn, cfg) - if err := cn.HandshakeContext(ctx); err != nil { + + if len(Fingerprint) != 0 { + if fingerprint, exists := U.GetFingerprint(Fingerprint); exists { + utlsConn := U.UClient(pconn, cfg, &utls.ClientHelloID{ + Client: fingerprint.Client, + Version: fingerprint.Version, + Seed: nil, + }) + if err := utlsConn.(*U.UConn).HandshakeContext(ctx); err != nil { + pconn.Close() + return nil, err + } + state := utlsConn.(*U.UConn).ConnectionState() + if p := state.NegotiatedProtocol; p != http2.NextProtoTLS { + utlsConn.Close() + return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http2.NextProtoTLS) + } + return utlsConn, nil + } + } + + conn := tls.Client(pconn, cfg) + if err := conn.HandshakeContext(ctx); err != nil { pconn.Close() return nil, err } - state := cn.ConnectionState() + state := conn.ConnectionState() if p := state.NegotiatedProtocol; p != http2.NextProtoTLS { - cn.Close() + conn.Close() return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http2.NextProtoTLS) } - return cn, nil + return conn, nil } wrap.Transport = &http2.Transport{ @@ -260,6 +285,6 @@ func StreamGunWithConn(conn net.Conn, tlsConfig *tls.Config, cfg *Config) (net.C return conn, nil } - transport := NewHTTP2Client(dialFn, tlsConfig) + transport := NewHTTP2Client(dialFn, tlsConfig, cfg.ClientFingerprint) return StreamGunWithTransport(transport, cfg) } diff --git a/transport/trojan/trojan.go b/transport/trojan/trojan.go index ca7b9425..ef52712f 100644 --- a/transport/trojan/trojan.go +++ b/transport/trojan/trojan.go @@ -46,13 +46,14 @@ const ( ) type Option struct { - Password string - ALPN []string - ServerName string - SkipCertVerify bool - Fingerprint string - Flow string - FlowShow bool + Password string + ALPN []string + ServerName string + SkipCertVerify bool + Fingerprint string + Flow string + FlowShow bool + ClientFingerprint string } type WebsocketOption struct { diff --git a/transport/vmess/tls.go b/transport/vmess/tls.go index 02442771..e60b117e 100644 --- a/transport/vmess/tls.go +++ b/transport/vmess/tls.go @@ -7,13 +7,16 @@ import ( tlsC "github.com/Dreamacro/clash/component/tls" C "github.com/Dreamacro/clash/constant" + + utls "github.com/refraction-networking/utls" ) type TLSConfig struct { - Host string - SkipCertVerify bool - FingerPrint string - NextProtos []string + Host string + SkipCertVerify bool + FingerPrint string + ClientFingerprint string + NextProtos []string } func StreamTLSConn(conn net.Conn, cfg *TLSConfig) (net.Conn, error) { @@ -32,11 +35,27 @@ func StreamTLSConn(conn net.Conn, cfg *TLSConfig) (net.Conn, error) { } } + if len(cfg.ClientFingerprint) != 0 { + if fingerprint, exists := GetFingerprint(cfg.ClientFingerprint); exists { + utlsConn := UClient(conn, tlsConfig, &utls.ClientHelloID{ + Client: fingerprint.Client, + Version: fingerprint.Version, + Seed: nil, + }) + + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) + defer cancel() + + err := utlsConn.(*UConn).HandshakeContext(ctx) + return utlsConn, err + } + } + tlsConn := tls.Client(conn, tlsConfig) - // fix tls handshake not timeout ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) defer cancel() + err := tlsConn.HandshakeContext(ctx) return tlsConn, err } diff --git a/transport/vmess/utls.go b/transport/vmess/utls.go new file mode 100644 index 00000000..4e53bed7 --- /dev/null +++ b/transport/vmess/utls.go @@ -0,0 +1,90 @@ +package vmess + +import ( + "crypto/tls" + "net" + + "github.com/Dreamacro/clash/log" + + "github.com/mroth/weightedrand/v2" + utls "github.com/refraction-networking/utls" +) + +type UConn struct { + *utls.UConn +} + +var initRandomFingerprint *utls.ClientHelloID + +func UClient(c net.Conn, config *tls.Config, fingerprint *utls.ClientHelloID) net.Conn { + utlsConn := utls.UClient(c, CopyConfig(config), *fingerprint) + return &UConn{UConn: utlsConn} +} + +func GetFingerprint(ClientFingerprint string) (*utls.ClientHelloID, bool) { + if initRandomFingerprint == nil { + initRandomFingerprint, _ = RollFingerprint() + } + if ClientFingerprint == "random" { + log.Debugln("use initial random HelloID:%s", initRandomFingerprint.Client) + return initRandomFingerprint, true + } + fingerprint, ok := Fingerprints[ClientFingerprint] + log.Debugln("use specified fingerprint:%s", fingerprint.Client) + return fingerprint, ok +} + +func RollFingerprint() (*utls.ClientHelloID, bool) { + chooser, _ := weightedrand.NewChooser( + weightedrand.NewChoice("chrome", 6), + weightedrand.NewChoice("safari", 3), + weightedrand.NewChoice("firefox", 1), + ) + initClient := chooser.Pick() + log.Debugln("initial random HelloID:%s", initClient) + fingerprint, ok := Fingerprints[initClient] + return fingerprint, ok +} + +var Fingerprints = map[string]*utls.ClientHelloID{ + "chrome": &utls.HelloChrome_Auto, + "firefox": &utls.HelloFirefox_Auto, + "safari": &utls.HelloSafari_Auto, + "randomized": &utls.HelloRandomized, +} + +func CopyConfig(c *tls.Config) *utls.Config { + return &utls.Config{ + RootCAs: c.RootCAs, + ServerName: c.ServerName, + InsecureSkipVerify: c.InsecureSkipVerify, + VerifyPeerCertificate: c.VerifyPeerCertificate, + } +} + +// WebsocketHandshake basically calls UConn.Handshake inside it but it will only send +// http/1.1 in its ALPN. +func (c *UConn) WebsocketHandshake() error { + // Build the handshake state. This will apply every variable of the TLS of the + // fingerprint in the UConn + if err := c.BuildHandshakeState(); err != nil { + return err + } + // Iterate over extensions and check for utls.ALPNExtension + hasALPNExtension := false + for _, extension := range c.Extensions { + if alpn, ok := extension.(*utls.ALPNExtension); ok { + hasALPNExtension = true + alpn.AlpnProtocols = []string{"http/1.1"} + break + } + } + if !hasALPNExtension { // Append extension if doesn't exists + c.Extensions = append(c.Extensions, &utls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}}) + } + // Rebuild the client hello and do the handshake + if err := c.BuildHandshakeState(); err != nil { + return err + } + return c.Handshake() +} diff --git a/transport/vmess/websocket.go b/transport/vmess/websocket.go index b38c0006..a7ca82b3 100644 --- a/transport/vmess/websocket.go +++ b/transport/vmess/websocket.go @@ -8,6 +8,7 @@ import ( "encoding/binary" "errors" "fmt" + "io" "math/rand" "net" @@ -20,8 +21,9 @@ import ( "github.com/Dreamacro/clash/common/buf" N "github.com/Dreamacro/clash/common/net" - "github.com/gorilla/websocket" + + utls "github.com/refraction-networking/utls" ) type websocketConn struct { @@ -56,6 +58,7 @@ type WebsocketConfig struct { TLSConfig *tls.Config MaxEarlyData int EarlyDataHeaderName string + ClientFingerprint string } // Read implements net.Conn.Read() @@ -136,15 +139,15 @@ func (wsc *websocketConn) Upstream() any { } func (wsc *websocketConn) Close() error { - var errors []string + var e []string if err := wsc.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second*5)); err != nil { - errors = append(errors, err.Error()) + e = append(e, err.Error()) } if err := wsc.conn.Close(); err != nil { - errors = append(errors, err.Error()) + e = append(e, err.Error()) } - if len(errors) > 0 { - return fmt.Errorf("failed to close connection: %s", strings.Join(errors, ",")) + if len(e) > 0 { + return fmt.Errorf("failed to close connection: %s", strings.Join(e, ",")) } return nil } @@ -316,6 +319,7 @@ func streamWebsocketWithEarlyDataConn(conn net.Conn, c *WebsocketConfig) (net.Co } func streamWebsocketConn(conn net.Conn, c *WebsocketConfig, earlyData *bytes.Buffer) (net.Conn, error) { + dialer := &websocket.Dialer{ NetDial: func(network, addr string) (net.Conn, error) { return conn, nil @@ -329,6 +333,22 @@ func streamWebsocketConn(conn net.Conn, c *WebsocketConfig, earlyData *bytes.Buf if c.TLS { scheme = "wss" dialer.TLSClientConfig = c.TLSConfig + if len(c.ClientFingerprint) != 0 { + if fingerprint, exists := GetFingerprint(c.ClientFingerprint); exists { + dialer.NetDialTLSContext = func(_ context.Context, _, addr string) (net.Conn, error) { + utlsConn := UClient(conn, c.TLSConfig, &utls.ClientHelloID{ + Client: fingerprint.Client, + Version: fingerprint.Version, + Seed: fingerprint.Seed, + }) + + if err := utlsConn.(*UConn).WebsocketHandshake(); err != nil { + return nil, fmt.Errorf("parse url %s error: %w", c.Path, err) + } + return utlsConn, nil + } + } + } } u, err := url.Parse(c.Path)