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:
parent
61b3b4f775
commit
2ee0f634e6
11 changed files with 285 additions and 104 deletions
|
@ -44,6 +44,7 @@ type TrojanOption struct {
|
|||
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) {
|
||||
|
@ -218,6 +219,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
|||
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{
|
||||
|
|
|
@ -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 (
|
||||
|
@ -66,6 +66,7 @@ type VlessOption struct {
|
|||
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{},
|
||||
}
|
||||
|
||||
|
@ -182,6 +184,7 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error)
|
|||
Host: host,
|
||||
SkipCertVerify: v.option.SkipCertVerify,
|
||||
FingerPrint: v.option.Fingerprint,
|
||||
ClientFingerprint: v.option.ClientFingerprint,
|
||||
}
|
||||
|
||||
if isH2 {
|
||||
|
@ -528,6 +531,7 @@ func NewVless(option VlessOption) (*Vless, error) {
|
|||
gunConfig := &gun.Config{
|
||||
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
|
||||
|
|
|
@ -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{},
|
||||
}
|
||||
|
||||
|
@ -136,6 +137,7 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||
tlsOpts := &clashVMess.TLSConfig{
|
||||
Host: host,
|
||||
SkipCertVerify: v.option.SkipCertVerify,
|
||||
ClientFingerprint: v.option.ClientFingerprint,
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
|
@ -163,6 +165,7 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||
Host: host,
|
||||
SkipCertVerify: v.option.SkipCertVerify,
|
||||
NextProtos: []string{"h2"},
|
||||
ClientFingerprint: v.option.ClientFingerprint,
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
|
@ -189,6 +192,7 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||
tlsOpts := &clashVMess.TLSConfig{
|
||||
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 {
|
||||
|
@ -404,6 +408,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
|||
gunConfig := &gun.Config{
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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 记录
|
||||
|
@ -239,7 +238,7 @@ dns:
|
|||
nameserver-policy:
|
||||
# 'www.baidu.com': '114.114.114.114'
|
||||
# '+.internal.crop.com': '10.0.0.1'
|
||||
'geosite:cn': 'https://doh.pub/dns-query'
|
||||
"geosite:cn": "https://doh.pub/dns-query"
|
||||
|
||||
proxies:
|
||||
# Shadowsocks
|
||||
|
@ -255,8 +254,7 @@ proxies:
|
|||
server: server
|
||||
port: 443
|
||||
cipher: chacha20-ietf-poly1305
|
||||
password:
|
||||
"password"
|
||||
password: "password"
|
||||
# udp: true
|
||||
# udp-over-tcp: false
|
||||
# ip-version: ipv4 # 设置节点使用 IP 版本,可选:dual,ipv4,ipv6,ipv4-prefer,ipv6-prefer。默认使用 dual
|
||||
|
@ -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
4
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
|
||||
|
|
8
go.sum
8
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=
|
||||
|
|
|
@ -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"
|
||||
|
@ -53,6 +55,7 @@ type Conn struct {
|
|||
type Config struct {
|
||||
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 := cn.ConnectionState()
|
||||
state := utlsConn.(*U.UConn).ConnectionState()
|
||||
if p := state.NegotiatedProtocol; p != http2.NextProtoTLS {
|
||||
cn.Close()
|
||||
utlsConn.Close()
|
||||
return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http2.NextProtoTLS)
|
||||
}
|
||||
return cn, nil
|
||||
return utlsConn, nil
|
||||
}
|
||||
}
|
||||
|
||||
conn := tls.Client(pconn, cfg)
|
||||
if err := conn.HandshakeContext(ctx); err != nil {
|
||||
pconn.Close()
|
||||
return nil, err
|
||||
}
|
||||
state := conn.ConnectionState()
|
||||
if p := state.NegotiatedProtocol; p != http2.NextProtoTLS {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http2.NextProtoTLS)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ type Option struct {
|
|||
Fingerprint string
|
||||
Flow string
|
||||
FlowShow bool
|
||||
ClientFingerprint string
|
||||
}
|
||||
|
||||
type WebsocketOption struct {
|
||||
|
|
|
@ -7,12 +7,15 @@ 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
|
||||
ClientFingerprint string
|
||||
NextProtos []string
|
||||
}
|
||||
|
||||
|
@ -32,11 +35,27 @@ func StreamTLSConn(conn net.Conn, cfg *TLSConfig) (net.Conn, error) {
|
|||
}
|
||||
}
|
||||
|
||||
tlsConn := tls.Client(conn, tlsConfig)
|
||||
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,
|
||||
})
|
||||
|
||||
// fix tls handshake not timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
||||
defer cancel()
|
||||
|
||||
err := utlsConn.(*UConn).HandshakeContext(ctx)
|
||||
return utlsConn, err
|
||||
}
|
||||
}
|
||||
|
||||
tlsConn := tls.Client(conn, tlsConfig)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
||||
defer cancel()
|
||||
|
||||
err := tlsConn.HandshakeContext(ctx)
|
||||
return tlsConn, err
|
||||
}
|
||||
|
|
90
transport/vmess/utls.go
Normal file
90
transport/vmess/utls.go
Normal 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()
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue