diff --git a/adapter/outbound/trojan.go b/adapter/outbound/trojan.go index cd0f1476..5fe946b0 100644 --- a/adapter/outbound/trojan.go +++ b/adapter/outbound/trojan.go @@ -15,6 +15,7 @@ import ( "github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/trojan" "github.com/Dreamacro/clash/transport/vless" + "github.com/Dreamacro/clash/transport/vmess" ) type Trojan struct { @@ -77,6 +78,11 @@ func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) { // StreamConn implements C.ProxyAdapter func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { var err error + + if vmess.HaveGlobalFingerprint() && len(t.option.ClientFingerprint) == 0 { + t.option.ClientFingerprint = vmess.GetGlobalFingerprint() + } + if t.transport != nil { c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig) } else { diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index 96345a6d..0550fd7a 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -71,6 +71,11 @@ type VlessOption struct { func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { var err error + + if vmess.HaveGlobalFingerprint() && len(v.option.ClientFingerprint) == 0 { + v.option.ClientFingerprint = vmess.GetGlobalFingerprint() + } + switch v.option.Network { case "ws": diff --git a/adapter/outbound/vmess.go b/adapter/outbound/vmess.go index 1e90a1bc..a2dff6ee 100644 --- a/adapter/outbound/vmess.go +++ b/adapter/outbound/vmess.go @@ -87,6 +87,11 @@ type WSOptions struct { // StreamConn implements C.ProxyAdapter func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { var err error + + if clashVMess.HaveGlobalFingerprint() && (len(v.option.ClientFingerprint) == 0) { + v.option.ClientFingerprint = clashVMess.GetGlobalFingerprint() + } + switch v.option.Network { case "ws": diff --git a/adapter/parser.go b/adapter/parser.go index 86fe96f9..97663540 100644 --- a/adapter/parser.go +++ b/adapter/parser.go @@ -2,9 +2,11 @@ package adapter import ( "fmt" + "github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/common/structure" C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/vmess" ) func ParseProxy(mapping map[string]any) (C.Proxy, error) { @@ -54,6 +56,11 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { Path: []string{"/"}, }, } + + if GlobalUtlsClient := vmess.GetGlobalFingerprint(); len(GlobalUtlsClient) != 0 { + vmessOption.ClientFingerprint = GlobalUtlsClient + } + err = decoder.Decode(mapping, vmessOption) if err != nil { break @@ -61,6 +68,11 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { proxy, err = outbound.NewVmess(*vmessOption) case "vless": vlessOption := &outbound.VlessOption{} + + if GlobalUtlsClient := vmess.GetGlobalFingerprint(); len(GlobalUtlsClient) != 0 { + vlessOption.ClientFingerprint = GlobalUtlsClient + } + err = decoder.Decode(mapping, vlessOption) if err != nil { break @@ -75,6 +87,11 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { proxy, err = outbound.NewSnell(*snellOption) case "trojan": trojanOption := &outbound.TrojanOption{} + + if GlobalUtlsClient := vmess.GetGlobalFingerprint(); len(GlobalUtlsClient) != 0 { + trojanOption.ClientFingerprint = GlobalUtlsClient + } + err = decoder.Decode(mapping, trojanOption) if err != nil { break diff --git a/config/config.go b/config/config.go index fd8621c3..be2e49cf 100644 --- a/config/config.go +++ b/config/config.go @@ -14,9 +14,6 @@ import ( "strings" "time" - P "github.com/Dreamacro/clash/component/process" - SNIFF "github.com/Dreamacro/clash/component/sniffer" - "github.com/Dreamacro/clash/adapter" "github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outboundgroup" @@ -27,6 +24,8 @@ import ( "github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/component/geodata" "github.com/Dreamacro/clash/component/geodata/router" + P "github.com/Dreamacro/clash/component/process" + SNIFF "github.com/Dreamacro/clash/component/sniffer" "github.com/Dreamacro/clash/component/trie" C "github.com/Dreamacro/clash/constant" providerTypes "github.com/Dreamacro/clash/constant/provider" @@ -37,6 +36,7 @@ import ( "github.com/Dreamacro/clash/log" R "github.com/Dreamacro/clash/rules" RP "github.com/Dreamacro/clash/rules/provider" + "github.com/Dreamacro/clash/transport/vmess" T "github.com/Dreamacro/clash/tunnel" "gopkg.in/yaml.v3" @@ -46,18 +46,19 @@ import ( type General struct { Inbound Controller - Mode T.TunnelMode `json:"mode"` - UnifiedDelay bool - LogLevel log.LogLevel `json:"log-level"` - IPv6 bool `json:"ipv6"` - Interface string `json:"interface-name"` - RoutingMark int `json:"-"` - GeodataMode bool `json:"geodata-mode"` - GeodataLoader string `json:"geodata-loader"` - TCPConcurrent bool `json:"tcp-concurrent"` - FindProcessMode P.FindProcessMode `json:"find-process-mode"` - Sniffing bool `json:"sniffing"` - EBpf EBpf `json:"-"` + Mode T.TunnelMode `json:"mode"` + UnifiedDelay bool + LogLevel log.LogLevel `json:"log-level"` + IPv6 bool `json:"ipv6"` + Interface string `json:"interface-name"` + RoutingMark int `json:"-"` + GeodataMode bool `json:"geodata-mode"` + GeodataLoader string `json:"geodata-loader"` + TCPConcurrent bool `json:"tcp-concurrent"` + FindProcessMode P.FindProcessMode `json:"find-process-mode"` + Sniffing bool `json:"sniffing"` + EBpf EBpf `json:"-"` + GlobalClientFingerprint string `json:"global-client-fingerprint"` } // Inbound config @@ -234,32 +235,33 @@ type RawTuicServer struct { } type RawConfig struct { - Port int `yaml:"port"` - SocksPort int `yaml:"socks-port"` - RedirPort int `yaml:"redir-port"` - TProxyPort int `yaml:"tproxy-port"` - MixedPort int `yaml:"mixed-port"` - ShadowSocksConfig string `yaml:"ss-config"` - VmessConfig string `yaml:"vmess-config"` - InboundTfo bool `yaml:"inbound-tfo"` - Authentication []string `yaml:"authentication"` - AllowLan bool `yaml:"allow-lan"` - BindAddress string `yaml:"bind-address"` - Mode T.TunnelMode `yaml:"mode"` - UnifiedDelay bool `yaml:"unified-delay"` - LogLevel log.LogLevel `yaml:"log-level"` - IPv6 bool `yaml:"ipv6"` - ExternalController string `yaml:"external-controller"` - ExternalControllerTLS string `yaml:"external-controller-tls"` - ExternalUI string `yaml:"external-ui"` - Secret string `yaml:"secret"` - Interface string `yaml:"interface-name"` - RoutingMark int `yaml:"routing-mark"` - Tunnels []LC.Tunnel `yaml:"tunnels"` - GeodataMode bool `yaml:"geodata-mode"` - GeodataLoader string `yaml:"geodata-loader"` - TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"` - FindProcessMode P.FindProcessMode `yaml:"find-process-mode" json:"find-process-mode"` + Port int `yaml:"port"` + SocksPort int `yaml:"socks-port"` + RedirPort int `yaml:"redir-port"` + TProxyPort int `yaml:"tproxy-port"` + MixedPort int `yaml:"mixed-port"` + ShadowSocksConfig string `yaml:"ss-config"` + VmessConfig string `yaml:"vmess-config"` + InboundTfo bool `yaml:"inbound-tfo"` + Authentication []string `yaml:"authentication"` + AllowLan bool `yaml:"allow-lan"` + BindAddress string `yaml:"bind-address"` + Mode T.TunnelMode `yaml:"mode"` + UnifiedDelay bool `yaml:"unified-delay"` + LogLevel log.LogLevel `yaml:"log-level"` + IPv6 bool `yaml:"ipv6"` + ExternalController string `yaml:"external-controller"` + ExternalControllerTLS string `yaml:"external-controller-tls"` + ExternalUI string `yaml:"external-ui"` + Secret string `yaml:"secret"` + Interface string `yaml:"interface-name"` + RoutingMark int `yaml:"routing-mark"` + Tunnels []LC.Tunnel `yaml:"tunnels"` + GeodataMode bool `yaml:"geodata-mode"` + GeodataLoader string `yaml:"geodata-loader"` + TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"` + FindProcessMode P.FindProcessMode `yaml:"find-process-mode" json:"find-process-mode"` + GlobalClientFingerprint string `yaml:"global-client-fingerprint"` Sniffer RawSniffer `yaml:"sniffer"` ProxyProvider map[string]map[string]any `yaml:"proxy-providers"` @@ -519,6 +521,11 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { elapsedTime := time.Since(startTime) / time.Millisecond // duration in ms log.Infoln("Initial configuration complete, total time: %dms", elapsedTime) //Segment finished in xxm + if len(config.General.GlobalClientFingerprint) != 0 { + log.Debugln("GlobalClientFingerprint:%s", config.General.GlobalClientFingerprint) + vmess.SetGlobalUtlsClient(config.General.GlobalClientFingerprint) + } + return config, nil } @@ -552,17 +559,18 @@ func parseGeneral(cfg *RawConfig) (*General, error) { Secret: cfg.Secret, ExternalControllerTLS: cfg.ExternalControllerTLS, }, - UnifiedDelay: cfg.UnifiedDelay, - Mode: cfg.Mode, - LogLevel: cfg.LogLevel, - IPv6: cfg.IPv6, - Interface: cfg.Interface, - RoutingMark: cfg.RoutingMark, - GeodataMode: cfg.GeodataMode, - GeodataLoader: cfg.GeodataLoader, - TCPConcurrent: cfg.TCPConcurrent, - FindProcessMode: cfg.FindProcessMode, - EBpf: cfg.EBpf, + UnifiedDelay: cfg.UnifiedDelay, + Mode: cfg.Mode, + LogLevel: cfg.LogLevel, + IPv6: cfg.IPv6, + Interface: cfg.Interface, + RoutingMark: cfg.RoutingMark, + GeodataMode: cfg.GeodataMode, + GeodataLoader: cfg.GeodataLoader, + TCPConcurrent: cfg.TCPConcurrent, + FindProcessMode: cfg.FindProcessMode, + EBpf: cfg.EBpf, + GlobalClientFingerprint: cfg.GlobalClientFingerprint, }, nil } diff --git a/docs/config.yaml b/docs/config.yaml index 63bda11b..9fbfb62e 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -15,6 +15,11 @@ bind-address: "*" # 绑定IP地址,仅作用于 allow-lan 为 true,'*'表示 # - off, 不匹配进程,推荐在路由器上使用此模式 find-process-mode: strict +# global-client-fingerprint:全局TLS指纹,优先低于proxy内的 client-fingerprint +# accepts "chrome","firefox","safari","ios","random","none" options. +# Utls is currently support TLS transport in TCP/grpc/WS/HTTP for VLESS/Vmess and trojan. +global-client-fingerprint: chrome + mode: rule #自定义 geox-url @@ -423,7 +428,7 @@ proxies: server: server port: 443 password: yourpsk - # client-fingerprint: random # Available: "chrome","firefox","safari","random" + # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" # fingerprint: xxxx # udp: true # sni: example.com # aka server name @@ -483,7 +488,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" + # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" - name: "vless-ws" type: vless @@ -493,7 +498,7 @@ proxies: udp: true tls: true network: ws - # client-fingerprint: random # Available: "chrome","firefox","safari","random" + # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" servername: example.com # priority over wss host # skip-cert-verify: true # fingerprint: xxxx diff --git a/transport/vmess/utls.go b/transport/vmess/utls.go index 93a08111..cae7d52f 100644 --- a/transport/vmess/utls.go +++ b/transport/vmess/utls.go @@ -15,20 +15,42 @@ type UConn struct { } var initRandomFingerprint *utls.ClientHelloID +var initUtlsClient string 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 SetGlobalUtlsClient(Client string) { + initUtlsClient = Client +} + +func HaveGlobalFingerprint() bool { + if len(initUtlsClient) != 0 && initUtlsClient != "none" { + return true + } + return false +} + +func GetGlobalFingerprint() string { + return initUtlsClient +} + func GetFingerprint(ClientFingerprint string) (*utls.ClientHelloID, bool) { + if ClientFingerprint == "none" { + return nil, false + } + 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