From c6d7ef8cb8cc66b2fe47e7c6b1603e6ad3baf852 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sat, 17 May 2025 13:53:21 +0800 Subject: [PATCH] feat: add `ech-opts` for anytls/shadowsocks/trojan/vmess/vless outbound --- adapter/outbound/anytls.go | 32 +++++++++++++---------- adapter/outbound/ech.go | 28 +++++++++++++++++++++ adapter/outbound/shadowsocks.go | 14 +++++++++++ adapter/outbound/trojan.go | 14 +++++++++-- adapter/outbound/vless.go | 14 +++++++++-- adapter/outbound/vmess.go | 15 +++++++++-- component/ech/ech.go | 35 ++++++++++++++++++++++++++ component/resolver/resolver.go | 12 +++++++++ component/tls/reality.go | 2 +- dns/resolver.go | 22 ++++++++++++++++ docs/config.yaml | 16 ++++++++++++ transport/gost-plugin/websocket.go | 11 +++++--- transport/gun/gun.go | 39 +++++++++++++++++++++++++---- transport/v2ray-plugin/websocket.go | 3 +++ transport/vmess/tls.go | 27 +++++++++++++++++++- transport/vmess/websocket.go | 17 +++++++++++++ 16 files changed, 271 insertions(+), 30 deletions(-) create mode 100644 adapter/outbound/ech.go create mode 100644 component/ech/ech.go diff --git a/adapter/outbound/anytls.go b/adapter/outbound/anytls.go index 0e3b07de..1dea5579 100644 --- a/adapter/outbound/anytls.go +++ b/adapter/outbound/anytls.go @@ -28,19 +28,20 @@ type AnyTLS struct { type AnyTLSOption 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"` - ClientFingerprint string `proxy:"client-fingerprint,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - UDP bool `proxy:"udp,omitempty"` - IdleSessionCheckInterval int `proxy:"idle-session-check-interval,omitempty"` - IdleSessionTimeout int `proxy:"idle-session-timeout,omitempty"` - MinIdleSession int `proxy:"min-idle-session,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"` + ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` + ClientFingerprint string `proxy:"client-fingerprint,omitempty"` + SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` + Fingerprint string `proxy:"fingerprint,omitempty"` + UDP bool `proxy:"udp,omitempty"` + IdleSessionCheckInterval int `proxy:"idle-session-check-interval,omitempty"` + IdleSessionTimeout int `proxy:"idle-session-timeout,omitempty"` + MinIdleSession int `proxy:"min-idle-session,omitempty"` } func (t *AnyTLS) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { @@ -115,12 +116,17 @@ func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) { IdleSessionTimeout: time.Duration(option.IdleSessionTimeout) * time.Second, MinIdleSession: option.MinIdleSession, } + echConfig, err := option.ECHOpts.Parse() + if err != nil { + return nil, err + } tlsConfig := &vmess.TLSConfig{ Host: option.SNI, SkipCertVerify: option.SkipCertVerify, NextProtos: option.ALPN, FingerPrint: option.Fingerprint, ClientFingerprint: option.ClientFingerprint, + ECH: echConfig, } if tlsConfig.Host == "" { tlsConfig.Host = option.Server diff --git a/adapter/outbound/ech.go b/adapter/outbound/ech.go new file mode 100644 index 00000000..342e2560 --- /dev/null +++ b/adapter/outbound/ech.go @@ -0,0 +1,28 @@ +package outbound + +import ( + "encoding/base64" + "fmt" + + "github.com/metacubex/mihomo/component/ech" +) + +type ECHOptions struct { + Enable bool `proxy:"enable,omitempty" obfs:"enable,omitempty"` + Config string `proxy:"config,omitempty" obfs:"config,omitempty"` +} + +func (o ECHOptions) Parse() (*ech.Config, error) { + if !o.Enable { + return nil, nil + } + echConfig := &ech.Config{} + if o.Config != "" { + list, err := base64.StdEncoding.DecodeString(o.Config) + if err != nil { + return nil, fmt.Errorf("base64 decode ech config string failed: %v", err) + } + echConfig.EncryptedClientHelloConfigList = list + } + return echConfig, nil +} diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index 0b215ca4..fc2d5c4d 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -64,6 +64,7 @@ type v2rayObfsOption struct { Host string `obfs:"host,omitempty"` Path string `obfs:"path,omitempty"` TLS bool `obfs:"tls,omitempty"` + ECHOpts ECHOptions `obfs:"ech-opts,omitempty"` Fingerprint string `obfs:"fingerprint,omitempty"` Headers map[string]string `obfs:"headers,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` @@ -77,6 +78,7 @@ type gostObfsOption struct { Host string `obfs:"host,omitempty"` Path string `obfs:"path,omitempty"` TLS bool `obfs:"tls,omitempty"` + ECHOpts ECHOptions `obfs:"ech-opts,omitempty"` Fingerprint string `obfs:"fingerprint,omitempty"` Headers map[string]string `obfs:"headers,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` @@ -303,6 +305,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { v2rayOption.TLS = true v2rayOption.SkipCertVerify = opts.SkipCertVerify v2rayOption.Fingerprint = opts.Fingerprint + + echConfig, err := opts.ECHOpts.Parse() + if err != nil { + return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %w", addr, err) + } + v2rayOption.ECHConfig = echConfig } } else if option.Plugin == "gost-plugin" { opts := gostObfsOption{Host: "bing.com", Mux: true} @@ -325,6 +333,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { gostOption.TLS = true gostOption.SkipCertVerify = opts.SkipCertVerify gostOption.Fingerprint = opts.Fingerprint + + echConfig, err := opts.ECHOpts.Parse() + if err != nil { + return nil, fmt.Errorf("ss %s initialize gost-plugin error: %w", addr, err) + } + gostOption.ECHConfig = echConfig } } else if option.Plugin == shadowtls.Mode { obfsMode = shadowtls.Mode diff --git a/adapter/outbound/trojan.go b/adapter/outbound/trojan.go index a321caf0..0397126a 100644 --- a/adapter/outbound/trojan.go +++ b/adapter/outbound/trojan.go @@ -12,6 +12,7 @@ import ( N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/ech" "github.com/metacubex/mihomo/component/proxydialer" tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" @@ -32,6 +33,7 @@ type Trojan struct { transport *gun.TransportWrap realityConfig *tlsC.RealityConfig + echConfig *ech.Config ssCipher core.Cipher } @@ -48,6 +50,7 @@ type TrojanOption struct { Fingerprint string `proxy:"fingerprint,omitempty"` UDP bool `proxy:"udp,omitempty"` Network string `proxy:"network,omitempty"` + ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"` @@ -77,6 +80,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C. V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade, V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen, ClientFingerprint: t.option.ClientFingerprint, + ECHConfig: t.echConfig, Headers: http.Header{}, } @@ -110,7 +114,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C. c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts) case "grpc": - c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.realityConfig) + c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.echConfig, t.realityConfig) default: // default tcp network // handle TLS @@ -124,6 +128,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C. FingerPrint: t.option.Fingerprint, ClientFingerprint: t.option.ClientFingerprint, NextProtos: alpn, + ECH: t.echConfig, Reality: t.realityConfig, }) } @@ -321,6 +326,11 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { return nil, err } + t.echConfig, err = option.ECHOpts.Parse() + if err != nil { + return nil, err + } + if option.SSOpts.Enabled { if option.SSOpts.Password == "" { return nil, errors.New("empty password") @@ -365,7 +375,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { return nil, err } - t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.realityConfig) + t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.echConfig, t.realityConfig) t.gunTLSConfig = tlsConfig t.gunConfig = &gun.Config{ diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index b075e719..6412d4da 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -17,6 +17,7 @@ import ( "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/ech" "github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/resolver" tlsC "github.com/metacubex/mihomo/component/tls" @@ -46,6 +47,7 @@ type Vless struct { transport *gun.TransportWrap realityConfig *tlsC.RealityConfig + echConfig *ech.Config } type VlessOption struct { @@ -62,6 +64,7 @@ type VlessOption struct { XUDP bool `proxy:"xudp,omitempty"` PacketEncoding string `proxy:"packet-encoding,omitempty"` Network string `proxy:"network,omitempty"` + ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` @@ -88,6 +91,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade, V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen, ClientFingerprint: v.option.ClientFingerprint, + ECHConfig: v.echConfig, Headers: http.Header{}, } @@ -151,7 +155,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M c, err = vmess.StreamH2Conn(ctx, c, h2Opts) case "grpc": - c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) + c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.echConfig, v.realityConfig) default: // default tcp network // handle TLS @@ -206,6 +210,7 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne SkipCertVerify: v.option.SkipCertVerify, FingerPrint: v.option.Fingerprint, ClientFingerprint: v.option.ClientFingerprint, + ECH: v.echConfig, Reality: v.realityConfig, NextProtos: v.option.ALPN, } @@ -563,6 +568,11 @@ func NewVless(option VlessOption) (*Vless, error) { return nil, err } + v.echConfig, err = v.option.ECHOpts.Parse() + if err != nil { + return nil, err + } + switch option.Network { case "h2": if len(option.HTTP2Opts.Host) == 0 { @@ -611,7 +621,7 @@ func NewVless(option VlessOption) (*Vless, error) { v.gunTLSConfig = tlsConfig v.gunConfig = gunConfig - v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig) + v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.echConfig, v.realityConfig) } return v, nil diff --git a/adapter/outbound/vmess.go b/adapter/outbound/vmess.go index 23da3beb..8496efc1 100644 --- a/adapter/outbound/vmess.go +++ b/adapter/outbound/vmess.go @@ -15,6 +15,7 @@ import ( "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/ech" "github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/resolver" tlsC "github.com/metacubex/mihomo/component/tls" @@ -41,6 +42,7 @@ type Vmess struct { transport *gun.TransportWrap realityConfig *tlsC.RealityConfig + echConfig *ech.Config } type VmessOption struct { @@ -58,6 +60,7 @@ type VmessOption struct { SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` ServerName string `proxy:"servername,omitempty"` + ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` @@ -109,6 +112,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade, V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen, ClientFingerprint: v.option.ClientFingerprint, + ECHConfig: v.echConfig, Headers: http.Header{}, } @@ -146,6 +150,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M Host: host, SkipCertVerify: v.option.SkipCertVerify, ClientFingerprint: v.option.ClientFingerprint, + ECH: v.echConfig, Reality: v.realityConfig, NextProtos: v.option.ALPN, } @@ -195,7 +200,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts) case "grpc": - c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) + c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.echConfig, v.realityConfig) default: // handle TLS if v.option.TLS { @@ -205,6 +210,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M SkipCertVerify: v.option.SkipCertVerify, FingerPrint: v.option.Fingerprint, ClientFingerprint: v.option.ClientFingerprint, + ECH: v.echConfig, Reality: v.realityConfig, NextProtos: v.option.ALPN, } @@ -474,6 +480,11 @@ func NewVmess(option VmessOption) (*Vmess, error) { return nil, err } + v.echConfig, err = v.option.ECHOpts.Parse() + if err != nil { + return nil, err + } + switch option.Network { case "h2": if len(option.HTTP2Opts.Host) == 0 { @@ -522,7 +533,7 @@ func NewVmess(option VmessOption) (*Vmess, error) { v.gunTLSConfig = tlsConfig v.gunConfig = gunConfig - v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig) + v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.echConfig, v.realityConfig) } return v, nil diff --git a/component/ech/ech.go b/component/ech/ech.go new file mode 100644 index 00000000..57a1e60f --- /dev/null +++ b/component/ech/ech.go @@ -0,0 +1,35 @@ +package ech + +import ( + "context" + "fmt" + + "github.com/metacubex/mihomo/component/resolver" + tlsC "github.com/metacubex/mihomo/component/tls" +) + +type Config struct { + EncryptedClientHelloConfigList []byte +} + +func (cfg *Config) ClientHandle(ctx context.Context, tlsConfig *tlsC.Config) (err error) { + if cfg == nil { + return nil + } + echConfigList := cfg.EncryptedClientHelloConfigList + if len(echConfigList) == 0 { + echConfigList, err = resolver.ResolveECH(ctx, tlsConfig.ServerName) + if err != nil { + return fmt.Errorf("resolve ECH config error: %w", err) + } + } + + tlsConfig.EncryptedClientHelloConfigList = echConfigList + if tlsConfig.MinVersion != 0 && tlsConfig.MinVersion < tlsC.VersionTLS13 { + tlsConfig.MinVersion = tlsC.VersionTLS13 + } + if tlsConfig.MaxVersion != 0 && tlsConfig.MaxVersion < tlsC.VersionTLS13 { + tlsConfig.MaxVersion = tlsC.VersionTLS13 + } + return nil +} diff --git a/component/resolver/resolver.go b/component/resolver/resolver.go index add691ad..46038303 100644 --- a/component/resolver/resolver.go +++ b/component/resolver/resolver.go @@ -49,6 +49,7 @@ type Resolver interface { LookupIP(ctx context.Context, host string) (ips []netip.Addr, err error) LookupIPv4(ctx context.Context, host string) (ips []netip.Addr, err error) LookupIPv6(ctx context.Context, host string) (ips []netip.Addr, err error) + ResolveECH(ctx context.Context, host string) ([]byte, error) ExchangeContext(ctx context.Context, m *dns.Msg) (msg *dns.Msg, err error) Invalid() bool ClearCache() @@ -216,6 +217,17 @@ func ResolveIPPrefer6(ctx context.Context, host string) (netip.Addr, error) { return ResolveIPPrefer6WithResolver(ctx, host, DefaultResolver) } +func ResolveECHWithResolver(ctx context.Context, host string, r Resolver) ([]byte, error) { + if r != nil && r.Invalid() { + return r.ResolveECH(ctx, host) + } + return SystemResolver.ResolveECH(ctx, host) +} + +func ResolveECH(ctx context.Context, host string) ([]byte, error) { + return ResolveECHWithResolver(ctx, host, DefaultResolver) +} + func ResetConnection() { if DefaultResolver != nil { go DefaultResolver.ResetConnection() diff --git a/component/tls/reality.go b/component/tls/reality.go index b16676f0..2dcffabc 100644 --- a/component/tls/reality.go +++ b/component/tls/reality.go @@ -39,7 +39,7 @@ type RealityConfig struct { SupportX25519MLKEM768 bool } -func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHelloID, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) { +func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHelloID, tlsConfig *Config, realityConfig *RealityConfig) (net.Conn, error) { for retry := 0; ; retry++ { verifier := &realityVerifier{ serverName: tlsConfig.ServerName, diff --git a/dns/resolver.go b/dns/resolver.go index 0dfeadd2..b0807863 100644 --- a/dns/resolver.go +++ b/dns/resolver.go @@ -127,6 +127,28 @@ func (r *Resolver) shouldIPFallback(ip netip.Addr) bool { return false } +func (r *Resolver) ResolveECH(ctx context.Context, host string) ([]byte, error) { + query := &D.Msg{} + query.SetQuestion(D.Fqdn(host), D.TypeHTTPS) + + msg, err := r.ExchangeContext(ctx, query) + if err != nil { + return nil, err + } + + for _, rr := range msg.Answer { + switch resource := rr.(type) { + case *D.HTTPS: + for _, value := range resource.Value { + if echConfig, ok := value.(*D.SVCBECHConfig); ok { + return echConfig.ECH, nil + } + } + } + } + return nil, errors.New("no ECH config found in DNS records") +} + // ExchangeContext a batch of dns request with context.Context, and it use cache func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { if len(m.Question) == 0 { diff --git a/docs/config.yaml b/docs/config.yaml index 999bd35f..26895341 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -427,6 +427,10 @@ proxies: # socks5 # 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 配置指纹将实现 SSL Pining 效果 # fingerprint: xxxx + # ech-opts: + # enable: true # 必须手动开启 + # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) + # config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA # skip-cert-verify: true # host: bing.com # path: "/" @@ -527,6 +531,10 @@ proxies: # socks5 # skip-cert-verify: true # servername: example.com # priority over wss host # network: ws + # ech-opts: + # enable: true # 必须手动开启 + # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) + # config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA # ws-opts: # path: /path # headers: @@ -599,6 +607,10 @@ proxies: # socks5 # skip-cert-verify: true # fingerprint: xxxx # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" + # ech-opts: + # enable: true # 必须手动开启 + # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) + # config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA - name: "vless-vision" type: vless @@ -683,6 +695,10 @@ proxies: # socks5 # enabled: false # method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305 # password: "example" + # ech-opts: + # enable: true # 必须手动开启 + # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) + # config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA - name: trojan-grpc server: server diff --git a/transport/gost-plugin/websocket.go b/transport/gost-plugin/websocket.go index 23d06b94..daedb532 100644 --- a/transport/gost-plugin/websocket.go +++ b/transport/gost-plugin/websocket.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/metacubex/mihomo/component/ca" + "github.com/metacubex/mihomo/component/ech" "github.com/metacubex/mihomo/transport/vmess" smux "github.com/metacubex/smux" ) @@ -18,6 +19,7 @@ type Option struct { Path string Headers map[string]string TLS bool + ECHConfig *ech.Config SkipCertVerify bool Fingerprint string Mux bool @@ -48,10 +50,11 @@ func NewGostWebsocket(ctx context.Context, conn net.Conn, option *Option) (net.C } config := &vmess.WebsocketConfig{ - Host: option.Host, - Port: option.Port, - Path: option.Path, - Headers: header, + Host: option.Host, + Port: option.Port, + Path: option.Path, + ECHConfig: option.ECHConfig, + Headers: header, } if option.TLS { diff --git a/transport/gun/gun.go b/transport/gun/gun.go index 13d4046d..0b387d72 100644 --- a/transport/gun/gun.go +++ b/transport/gun/gun.go @@ -21,6 +21,7 @@ import ( "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/common/pool" + "github.com/metacubex/mihomo/component/ech" tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" @@ -224,7 +225,7 @@ func (g *Conn) SetDeadline(t time.Time) error { return nil } -func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, clientFingerprint string, realityConfig *tlsC.RealityConfig) *TransportWrap { +func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, clientFingerprint string, echConfig *ech.Config, realityConfig *tlsC.RealityConfig) *TransportWrap { dialFunc := func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout) defer cancel() @@ -238,8 +239,15 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, clientFingerprint stri } if clientFingerprint, ok := tlsC.GetFingerprint(clientFingerprint); ok { + tlsConfig := tlsC.UConfig(tlsConfig) + err := echConfig.ClientHandle(ctx, tlsConfig) + if err != nil { + pconn.Close() + return nil, err + } + if realityConfig == nil { - tlsConn := tlsC.UClient(pconn, tlsC.UConfig(cfg), clientFingerprint) + tlsConn := tlsC.UClient(pconn, tlsConfig, clientFingerprint) if err := tlsConn.HandshakeContext(ctx); err != nil { pconn.Close() return nil, err @@ -251,7 +259,7 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, clientFingerprint stri } return tlsConn, nil } else { - realityConn, err := tlsC.GetRealityConn(ctx, pconn, clientFingerprint, cfg, realityConfig) + realityConn, err := tlsC.GetRealityConn(ctx, pconn, clientFingerprint, tlsConfig, realityConfig) if err != nil { pconn.Close() return nil, err @@ -268,6 +276,27 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, clientFingerprint stri return nil, errors.New("REALITY is based on uTLS, please set a client-fingerprint") } + if echConfig != nil { + tlsConfig := tlsC.UConfig(tlsConfig) + err := echConfig.ClientHandle(ctx, tlsConfig) + if err != nil { + pconn.Close() + return nil, err + } + + conn := tlsC.Client(pconn, tlsConfig) + 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 + } + conn := tls.Client(pconn, cfg) if err := conn.HandshakeContext(ctx); err != nil { pconn.Close() @@ -345,12 +374,12 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er return conn, nil } -func StreamGunWithConn(conn net.Conn, tlsConfig *tls.Config, cfg *Config, realityConfig *tlsC.RealityConfig) (net.Conn, error) { +func StreamGunWithConn(conn net.Conn, tlsConfig *tls.Config, cfg *Config, echConfig *ech.Config, realityConfig *tlsC.RealityConfig) (net.Conn, error) { dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) { return conn, nil } - transport := NewHTTP2Client(dialFn, tlsConfig, cfg.ClientFingerprint, realityConfig) + transport := NewHTTP2Client(dialFn, tlsConfig, cfg.ClientFingerprint, echConfig, realityConfig) c, err := StreamGunWithTransport(transport, cfg) if err != nil { return nil, err diff --git a/transport/v2ray-plugin/websocket.go b/transport/v2ray-plugin/websocket.go index 90ff5efe..983698c7 100644 --- a/transport/v2ray-plugin/websocket.go +++ b/transport/v2ray-plugin/websocket.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/metacubex/mihomo/component/ca" + "github.com/metacubex/mihomo/component/ech" "github.com/metacubex/mihomo/transport/vmess" ) @@ -17,6 +18,7 @@ type Option struct { Path string Headers map[string]string TLS bool + ECHConfig *ech.Config SkipCertVerify bool Fingerprint string Mux bool @@ -37,6 +39,7 @@ func NewV2rayObfs(ctx context.Context, conn net.Conn, option *Option) (net.Conn, Path: option.Path, V2rayHttpUpgrade: option.V2rayHttpUpgrade, V2rayHttpUpgradeFastOpen: option.V2rayHttpUpgradeFastOpen, + ECHConfig: option.ECHConfig, Headers: header, } diff --git a/transport/vmess/tls.go b/transport/vmess/tls.go index 588c159a..3bfcb46a 100644 --- a/transport/vmess/tls.go +++ b/transport/vmess/tls.go @@ -7,6 +7,7 @@ import ( "net" "github.com/metacubex/mihomo/component/ca" + "github.com/metacubex/mihomo/component/ech" tlsC "github.com/metacubex/mihomo/component/tls" ) @@ -16,9 +17,14 @@ type TLSConfig struct { FingerPrint string ClientFingerprint string NextProtos []string + ECH *ech.Config Reality *tlsC.RealityConfig } +type ECHConfig struct { + Enable bool +} + func StreamTLSConn(ctx context.Context, conn net.Conn, cfg *TLSConfig) (net.Conn, error) { tlsConfig := &tls.Config{ ServerName: cfg.Host, @@ -33,8 +39,14 @@ func StreamTLSConn(ctx context.Context, conn net.Conn, cfg *TLSConfig) (net.Conn } if clientFingerprint, ok := tlsC.GetFingerprint(cfg.ClientFingerprint); ok { + tlsConfig := tlsC.UConfig(tlsConfig) + err = cfg.ECH.ClientHandle(ctx, tlsConfig) + if err != nil { + return nil, err + } + if cfg.Reality == nil { - tlsConn := tlsC.UClient(conn, tlsC.UConfig(tlsConfig), clientFingerprint) + tlsConn := tlsC.UClient(conn, tlsConfig, clientFingerprint) err = tlsConn.HandshakeContext(ctx) if err != nil { return nil, err @@ -48,6 +60,19 @@ func StreamTLSConn(ctx context.Context, conn net.Conn, cfg *TLSConfig) (net.Conn return nil, errors.New("REALITY is based on uTLS, please set a client-fingerprint") } + if cfg.ECH != nil { + tlsConfig := tlsC.UConfig(tlsConfig) + err = cfg.ECH.ClientHandle(ctx, tlsConfig) + if err != nil { + return nil, err + } + + tlsConn := tlsC.Client(conn, tlsConfig) + + err = tlsConn.HandshakeContext(ctx) + return tlsConn, err + } + tlsConn := tls.Client(conn, tlsConfig) err = tlsConn.HandshakeContext(ctx) diff --git a/transport/vmess/websocket.go b/transport/vmess/websocket.go index 7e8886b6..8fe43632 100644 --- a/transport/vmess/websocket.go +++ b/transport/vmess/websocket.go @@ -21,6 +21,7 @@ import ( "github.com/metacubex/mihomo/common/buf" N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/ech" tlsC "github.com/metacubex/mihomo/component/tls" "github.com/metacubex/mihomo/log" @@ -56,6 +57,7 @@ type WebsocketConfig struct { Headers http.Header TLS bool TLSConfig *tls.Config + ECHConfig *ech.Config MaxEarlyData int EarlyDataHeaderName string ClientFingerprint string @@ -355,6 +357,11 @@ func streamWebsocketConn(ctx context.Context, conn net.Conn, c *WebsocketConfig, } if clientFingerprint, ok := tlsC.GetFingerprint(c.ClientFingerprint); ok { + tlsConfig := tlsC.UConfig(config) + err = c.ECHConfig.ClientHandle(ctx, tlsConfig) + if err != nil { + return nil, err + } tlsConn := tlsC.UClient(conn, tlsC.UConfig(config), clientFingerprint) if err = tlsC.BuildWebsocketHandshakeState(tlsConn); err != nil { return nil, fmt.Errorf("parse url %s error: %w", c.Path, err) @@ -364,6 +371,16 @@ func streamWebsocketConn(ctx context.Context, conn net.Conn, c *WebsocketConfig, return nil, err } conn = tlsConn + } else if c.ECHConfig != nil { + tlsConfig := tlsC.UConfig(config) + err = c.ECHConfig.ClientHandle(ctx, tlsConfig) + if err != nil { + return nil, err + } + tlsConn := tlsC.Client(conn, tlsConfig) + + err = tlsConn.HandshakeContext(ctx) + conn = tlsConn } else { tlsConn := tls.Client(conn, config) err = tlsConn.HandshakeContext(ctx)