diff --git a/adapter/outbound/http.go b/adapter/outbound/http.go index 81bf2e57..6cd2fa91 100644 --- a/adapter/outbound/http.go +++ b/adapter/outbound/http.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "errors" "fmt" + tlsC "github.com/Dreamacro/clash/common/tls" "io" "net" "net/http" @@ -149,7 +150,7 @@ func NewHttp(option HttpOption) *Http { }, user: option.UserName, pass: option.Password, - tlsConfig: tlsConfig, + tlsConfig: tlsC.MixinTLSConfig(tlsConfig), option: &option, } } diff --git a/adapter/outbound/hysteria.go b/adapter/outbound/hysteria.go index ddf5816c..aaaac2d4 100644 --- a/adapter/outbound/hysteria.go +++ b/adapter/outbound/hysteria.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "crypto/x509" "fmt" + tlsC "github.com/Dreamacro/clash/common/tls" "github.com/Dreamacro/clash/transport/hysteria/core" "github.com/Dreamacro/clash/transport/hysteria/obfs" "github.com/Dreamacro/clash/transport/hysteria/pmtud_fix" @@ -121,11 +122,11 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) { if option.SNI != "" { serverName = option.SNI } - tlsConfig := &tls.Config{ + tlsConfig := tlsC.MixinTLSConfig(&tls.Config{ ServerName: serverName, InsecureSkipVerify: option.SkipCertVerify, MinVersion: tls.VersionTLS13, - } + }) if len(option.ALPN) > 0 { tlsConfig.NextProtos = []string{option.ALPN} } else { diff --git a/adapter/outbound/socks5.go b/adapter/outbound/socks5.go index 398ee3b2..f0bee502 100644 --- a/adapter/outbound/socks5.go +++ b/adapter/outbound/socks5.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "errors" "fmt" + tlsC "github.com/Dreamacro/clash/common/tls" "io" "net" "strconv" @@ -160,7 +161,7 @@ func NewSocks5(option Socks5Option) *Socks5 { pass: option.Password, tls: option.TLS, skipCertVerify: option.SkipCertVerify, - tlsConfig: tlsConfig, + tlsConfig: tlsC.MixinTLSConfig(tlsConfig), } } diff --git a/adapter/outbound/trojan.go b/adapter/outbound/trojan.go index 09d000b0..692c9a99 100644 --- a/adapter/outbound/trojan.go +++ b/adapter/outbound/trojan.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "fmt" + tlsC "github.com/Dreamacro/clash/common/tls" "net" "net/http" "strconv" @@ -227,12 +228,12 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { return c, nil } - tlsConfig := &tls.Config{ + tlsConfig := tlsC.MixinTLSConfig(&tls.Config{ NextProtos: option.ALPN, MinVersion: tls.VersionTLS12, InsecureSkipVerify: tOption.SkipCertVerify, ServerName: tOption.ServerName, - } + }) if t.option.Flow != "" { t.transport = gun.NewHTTP2XTLSClient(dialFn, tlsConfig) diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index 51bf104e..1de979ef 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "github.com/Dreamacro/clash/common/convert" + tlsC "github.com/Dreamacro/clash/common/tls" "io" "net" "net/http" @@ -80,12 +81,12 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { } if v.option.TLS { wsOpts.TLS = true - wsOpts.TLSConfig = &tls.Config{ + wsOpts.TLSConfig = tlsC.MixinTLSConfig(&tls.Config{ MinVersion: tls.VersionTLS12, ServerName: host, InsecureSkipVerify: v.option.SkipCertVerify, NextProtos: []string{"http/1.1"}, - } + }) if v.option.ServerName != "" { wsOpts.TLSConfig.ServerName = v.option.ServerName } else if host := wsOpts.Headers.Get("Host"); host != "" { @@ -436,10 +437,10 @@ func NewVless(option VlessOption) (*Vless, error) { ServiceName: v.option.GrpcOpts.GrpcServiceName, Host: v.option.ServerName, } - tlsConfig := &tls.Config{ + tlsConfig := tlsC.MixinTLSConfig(&tls.Config{ InsecureSkipVerify: v.option.SkipCertVerify, ServerName: v.option.ServerName, - } + }) if v.option.ServerName == "" { host, _, _ := net.SplitHostPort(v.addr) diff --git a/adapter/outbound/vmess.go b/adapter/outbound/vmess.go index 12bcfaba..6df62ab7 100644 --- a/adapter/outbound/vmess.go +++ b/adapter/outbound/vmess.go @@ -5,13 +5,13 @@ import ( "crypto/tls" "errors" "fmt" + tlsC "github.com/Dreamacro/clash/common/tls" "net" "net/http" "strconv" "strings" "sync" - "github.com/Dreamacro/clash/common/convert" "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/resolver" C "github.com/Dreamacro/clash/constant" @@ -100,21 +100,16 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { if v.option.TLS { wsOpts.TLS = true - wsOpts.TLSConfig = &tls.Config{ + wsOpts.TLSConfig = tlsC.MixinTLSConfig(&tls.Config{ ServerName: host, InsecureSkipVerify: v.option.SkipCertVerify, NextProtos: []string{"http/1.1"}, - } + }) if v.option.ServerName != "" { wsOpts.TLSConfig.ServerName = v.option.ServerName } else if host := wsOpts.Headers.Get("Host"); host != "" { wsOpts.TLSConfig.ServerName = host } - } else { - if host := wsOpts.Headers.Get("Host"); host == "" { - wsOpts.Headers.Set("Host", convert.RandHost()) - convert.SetUserAgent(wsOpts.Headers) - } } c, err = clashVMess.StreamWebsocketConn(c, wsOpts) case "http": diff --git a/common/tls/config.go b/common/tls/config.go new file mode 100644 index 00000000..70aa390d --- /dev/null +++ b/common/tls/config.go @@ -0,0 +1,80 @@ +package tls + +import ( + "bytes" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "encoding/hex" + "fmt" + "strings" + "sync" + "time" +) + +var fingerprints [][32]byte +var rwLock sync.Mutex +var defaultTLSConfig = &tls.Config{ + InsecureSkipVerify: true, + VerifyPeerCertificate: verifyPeerCertificate, +} +var verifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + fingerprints := fingerprints + + var preErr error + for i := range rawCerts { + rawCert := rawCerts[i] + cert, err := x509.ParseCertificate(rawCert) + if err == nil { + opts := x509.VerifyOptions{ + CurrentTime: time.Now(), + } + + if _, err := cert.Verify(opts); err == nil { + return nil + } else { + fingerprint := sha256.Sum256(cert.Raw) + for _, fp := range fingerprints { + if bytes.Equal(fingerprint[:], fp[:]) { + return nil + } + } + + preErr = err + } + } + } + + return preErr +} + +func AddCertFingerprint(fingerprint string) error { + fp := strings.Replace(fingerprint, ":", "", -1) + fpByte, err := hex.DecodeString(fp) + if err != nil { + return err + } + + if len(fpByte) != 32 { + return fmt.Errorf("fingerprint string length error,need sha25 fingerprint") + } + + rwLock.Lock() + fingerprints = append(fingerprints, *(*[32]byte)(fpByte)) + rwLock.Unlock() + return nil +} + +func GetDefaultTLSConfig() *tls.Config { + return defaultTLSConfig +} + +func MixinTLSConfig(tlsConfig *tls.Config) *tls.Config { + if tlsConfig == nil { + return GetDefaultTLSConfig() + } + + tlsConfig.InsecureSkipVerify = true + tlsConfig.VerifyPeerCertificate = verifyPeerCertificate + return tlsConfig +} diff --git a/component/http/http.go b/component/http/http.go index c534b2ee..41041775 100644 --- a/component/http/http.go +++ b/component/http/http.go @@ -2,6 +2,7 @@ package http import ( "context" + "github.com/Dreamacro/clash/common/tls" "github.com/Dreamacro/clash/listener/inner" "github.com/Dreamacro/clash/log" "io" @@ -56,6 +57,7 @@ func HttpRequest(ctx context.Context, url, method string, header map[string][]st conn := inner.HandleTcp(address, urlRes.Hostname()) return conn, nil }, + TLSClientConfig: tls.GetDefaultTLSConfig(), } client := http.Client{Transport: transport} diff --git a/config/config.go b/config/config.go index cac2002b..bb67000b 100644 --- a/config/config.go +++ b/config/config.go @@ -136,7 +136,9 @@ type Sniffer struct { } // Experimental config -type Experimental struct{} +type Experimental struct { + Fingerprints []string `yaml:"fingerprints"` +} // Config is clash config manager type Config struct { diff --git a/dns/client.go b/dns/client.go index 0bbe671f..966f5cf8 100644 --- a/dns/client.go +++ b/dns/client.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "fmt" + tlsC "github.com/Dreamacro/clash/common/tls" "go.uber.org/atomic" "net" "net/netip" @@ -77,7 +78,7 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) ch := make(chan result, 1) go func() { if strings.HasSuffix(c.Client.Net, "tls") { - conn = tls.Client(conn, c.Client.TLSConfig) + conn = tls.Client(conn, tlsC.MixinTLSConfig(c.Client.TLSConfig)) } msg, _, err := c.Client.ExchangeWithConn(m, &D.Conn{ diff --git a/dns/doh.go b/dns/doh.go index 07381f88..1ad6046f 100644 --- a/dns/doh.go +++ b/dns/doh.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/tls" + tls2 "github.com/Dreamacro/clash/common/tls" "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/resolver" "github.com/lucas-clemente/quic-go" @@ -119,6 +120,7 @@ func newDohTransport(r *Resolver, preferH3 bool, proxyAdapter string) *dohTransp return dialContextExtra(ctx, proxyAdapter, "tcp", ip, port) } }, + TLSClientConfig: tls2.GetDefaultTLSConfig(), }, preferH3: preferH3, } @@ -156,6 +158,7 @@ func newDohTransport(r *Resolver, preferH3 bool, proxyAdapter string) *dohTransp } } }, + TLSClientConfig: tls2.GetDefaultTLSConfig(), } } diff --git a/dns/doq.go b/dns/doq.go index aafea97f..6d4e2fb8 100644 --- a/dns/doq.go +++ b/dns/doq.go @@ -5,6 +5,7 @@ import ( "context" "crypto/tls" "fmt" + tlsC "github.com/Dreamacro/clash/common/tls" "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/resolver" "github.com/lucas-clemente/quic-go" @@ -128,13 +129,15 @@ func (dc *quicClient) getSession(ctx context.Context) (quic.Connection, error) { } func (dc *quicClient) openSession(ctx context.Context) (quic.Connection, error) { - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, - NextProtos: []string{ - NextProtoDQ, - }, - SessionTicketsDisabled: false, - } + tlsConfig := tlsC.MixinTLSConfig( + &tls.Config{ + InsecureSkipVerify: false, + NextProtos: []string{ + NextProtoDQ, + }, + SessionTicketsDisabled: false, + }) + quicConfig := &quic.Config{ ConnectionIDLength: 12, HandshakeIdleTimeout: time.Second * 8, diff --git a/docs/config.yaml b/docs/config.yaml index d0dfcddb..7b5d1b89 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -25,7 +25,12 @@ external-ui: /path/to/ui/folder # 配置WEB UI目录,使用http://{{external-c # interface-name: en0 # 设置出口网卡 # routing-mark: 6666 # 配置fwmark 仅用于Linux - +experimental: + # 具体配置待定 + # 证书指纹,SHA256格式,补充校验TLS证书 + # 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + fingerprints: + - "8F:11:1F:A9:AD:3C:D8:E9:17:A1:18:52:2C:AC:39:EA:33:74:1B:3B:BE:73:F9:1C:EC:E5:48:D5:CC:B0:E5:E8" # 类似于/etc/hosts, 仅支持配置单个IP hosts: # '*.clash.dev': 127.0.0.1 diff --git a/hub/executor/executor.go b/hub/executor/executor.go index 28717bae..43bff436 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -2,6 +2,7 @@ package executor import ( "fmt" + "github.com/Dreamacro/clash/common/tls" "github.com/Dreamacro/clash/listener/inner" "net/netip" "os" @@ -72,7 +73,7 @@ func ParseWithBytes(buf []byte) (*config.Config, error) { func ApplyConfig(cfg *config.Config, force bool) { mux.Lock() defer mux.Unlock() - + preUpdateExperimental(cfg) updateUsers(cfg.Users) updateProxies(cfg.Proxies, cfg.Providers) updateRules(cfg.Rules, cfg.RuleProviders) @@ -134,6 +135,14 @@ func updateExperimental(c *config.Config) { runtime.GC() } +func preUpdateExperimental(c *config.Config) { + for _, fingerprint := range c.Experimental.Fingerprints { + if err := tls.AddCertFingerprint(fingerprint); err != nil { + log.Warnln("fingerprint[%s] is err, %s", fingerprint, err.Error()) + } + } +} + func updateDNS(c *config.DNS, generalIPv6 bool) { if !c.Enable { resolver.DisableIPv6 = !generalIPv6 diff --git a/transport/vmess/tls.go b/transport/vmess/tls.go index e4f29a2f..871980c1 100644 --- a/transport/vmess/tls.go +++ b/transport/vmess/tls.go @@ -3,6 +3,7 @@ package vmess import ( "context" "crypto/tls" + tlsC "github.com/Dreamacro/clash/common/tls" "net" C "github.com/Dreamacro/clash/constant" @@ -15,11 +16,11 @@ type TLSConfig struct { } func StreamTLSConn(conn net.Conn, cfg *TLSConfig) (net.Conn, error) { - tlsConfig := &tls.Config{ + tlsConfig := tlsC.MixinTLSConfig(&tls.Config{ ServerName: cfg.Host, InsecureSkipVerify: cfg.SkipCertVerify, NextProtos: cfg.NextProtos, - } + }) tlsConn := tls.Client(conn, tlsConfig)