feat: Add utls for modifying client's fingerprint.

Currently only support TLS transport in TCP/GRPC/WS/HTTP for VLESS/Vmess and trojan-grpc.
This commit is contained in:
Larvan2 2023-02-01 22:16:06 +08:00
parent 61b3b4f775
commit 2ee0f634e6
11 changed files with 285 additions and 104 deletions

View file

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

View file

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

View file

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

View file

@ -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 版本可选dualipv4ipv6ipv4-preferipv6-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

4
go.mod
View file

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

8
go.sum
View file

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

View file

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

View file

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

View file

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

90
transport/vmess/utls.go Normal file
View file

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

View file

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