feat: add ech-opts for anytls/shadowsocks/trojan/vmess/vless outbound

This commit is contained in:
wwqgtxx 2025-05-17 13:53:21 +08:00
parent bb8c47d83d
commit c6d7ef8cb8
16 changed files with 271 additions and 30 deletions

View File

@ -34,6 +34,7 @@ type AnyTLSOption struct {
Password string `proxy:"password"` Password string `proxy:"password"`
ALPN []string `proxy:"alpn,omitempty"` ALPN []string `proxy:"alpn,omitempty"`
SNI string `proxy:"sni,omitempty"` SNI string `proxy:"sni,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"`
@ -115,12 +116,17 @@ func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) {
IdleSessionTimeout: time.Duration(option.IdleSessionTimeout) * time.Second, IdleSessionTimeout: time.Duration(option.IdleSessionTimeout) * time.Second,
MinIdleSession: option.MinIdleSession, MinIdleSession: option.MinIdleSession,
} }
echConfig, err := option.ECHOpts.Parse()
if err != nil {
return nil, err
}
tlsConfig := &vmess.TLSConfig{ tlsConfig := &vmess.TLSConfig{
Host: option.SNI, Host: option.SNI,
SkipCertVerify: option.SkipCertVerify, SkipCertVerify: option.SkipCertVerify,
NextProtos: option.ALPN, NextProtos: option.ALPN,
FingerPrint: option.Fingerprint, FingerPrint: option.Fingerprint,
ClientFingerprint: option.ClientFingerprint, ClientFingerprint: option.ClientFingerprint,
ECH: echConfig,
} }
if tlsConfig.Host == "" { if tlsConfig.Host == "" {
tlsConfig.Host = option.Server tlsConfig.Host = option.Server

28
adapter/outbound/ech.go Normal file
View File

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

View File

@ -64,6 +64,7 @@ type v2rayObfsOption struct {
Host string `obfs:"host,omitempty"` Host string `obfs:"host,omitempty"`
Path string `obfs:"path,omitempty"` Path string `obfs:"path,omitempty"`
TLS bool `obfs:"tls,omitempty"` TLS bool `obfs:"tls,omitempty"`
ECHOpts ECHOptions `obfs:"ech-opts,omitempty"`
Fingerprint string `obfs:"fingerprint,omitempty"` Fingerprint string `obfs:"fingerprint,omitempty"`
Headers map[string]string `obfs:"headers,omitempty"` Headers map[string]string `obfs:"headers,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
@ -77,6 +78,7 @@ type gostObfsOption struct {
Host string `obfs:"host,omitempty"` Host string `obfs:"host,omitempty"`
Path string `obfs:"path,omitempty"` Path string `obfs:"path,omitempty"`
TLS bool `obfs:"tls,omitempty"` TLS bool `obfs:"tls,omitempty"`
ECHOpts ECHOptions `obfs:"ech-opts,omitempty"`
Fingerprint string `obfs:"fingerprint,omitempty"` Fingerprint string `obfs:"fingerprint,omitempty"`
Headers map[string]string `obfs:"headers,omitempty"` Headers map[string]string `obfs:"headers,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
@ -303,6 +305,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
v2rayOption.TLS = true v2rayOption.TLS = true
v2rayOption.SkipCertVerify = opts.SkipCertVerify v2rayOption.SkipCertVerify = opts.SkipCertVerify
v2rayOption.Fingerprint = opts.Fingerprint 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" { } else if option.Plugin == "gost-plugin" {
opts := gostObfsOption{Host: "bing.com", Mux: true} opts := gostObfsOption{Host: "bing.com", Mux: true}
@ -325,6 +333,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
gostOption.TLS = true gostOption.TLS = true
gostOption.SkipCertVerify = opts.SkipCertVerify gostOption.SkipCertVerify = opts.SkipCertVerify
gostOption.Fingerprint = opts.Fingerprint 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 { } else if option.Plugin == shadowtls.Mode {
obfsMode = shadowtls.Mode obfsMode = shadowtls.Mode

View File

@ -12,6 +12,7 @@ import (
N "github.com/metacubex/mihomo/common/net" N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
tlsC "github.com/metacubex/mihomo/component/tls" tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
@ -32,6 +33,7 @@ type Trojan struct {
transport *gun.TransportWrap transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig realityConfig *tlsC.RealityConfig
echConfig *ech.Config
ssCipher core.Cipher ssCipher core.Cipher
} }
@ -48,6 +50,7 @@ type TrojanOption struct {
Fingerprint string `proxy:"fingerprint,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-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, V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen, V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: t.option.ClientFingerprint, ClientFingerprint: t.option.ClientFingerprint,
ECHConfig: t.echConfig,
Headers: http.Header{}, 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) c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
case "grpc": 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:
// default tcp network // default tcp network
// handle TLS // handle TLS
@ -124,6 +128,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
FingerPrint: t.option.Fingerprint, FingerPrint: t.option.Fingerprint,
ClientFingerprint: t.option.ClientFingerprint, ClientFingerprint: t.option.ClientFingerprint,
NextProtos: alpn, NextProtos: alpn,
ECH: t.echConfig,
Reality: t.realityConfig, Reality: t.realityConfig,
}) })
} }
@ -321,6 +326,11 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
return nil, err return nil, err
} }
t.echConfig, err = option.ECHOpts.Parse()
if err != nil {
return nil, err
}
if option.SSOpts.Enabled { if option.SSOpts.Enabled {
if option.SSOpts.Password == "" { if option.SSOpts.Password == "" {
return nil, errors.New("empty password") return nil, errors.New("empty password")
@ -365,7 +375,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
return nil, err 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.gunTLSConfig = tlsConfig
t.gunConfig = &gun.Config{ t.gunConfig = &gun.Config{

View File

@ -17,6 +17,7 @@ import (
"github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
tlsC "github.com/metacubex/mihomo/component/tls" tlsC "github.com/metacubex/mihomo/component/tls"
@ -46,6 +47,7 @@ type Vless struct {
transport *gun.TransportWrap transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig realityConfig *tlsC.RealityConfig
echConfig *ech.Config
} }
type VlessOption struct { type VlessOption struct {
@ -62,6 +64,7 @@ type VlessOption struct {
XUDP bool `proxy:"xudp,omitempty"` XUDP bool `proxy:"xudp,omitempty"`
PacketEncoding string `proxy:"packet-encoding,omitempty"` PacketEncoding string `proxy:"packet-encoding,omitempty"`
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-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, V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen, V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
ECHConfig: v.echConfig,
Headers: http.Header{}, 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) c, err = vmess.StreamH2Conn(ctx, c, h2Opts)
case "grpc": 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:
// default tcp network // default tcp network
// handle TLS // handle TLS
@ -206,6 +210,7 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint, FingerPrint: v.option.Fingerprint,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig, Reality: v.realityConfig,
NextProtos: v.option.ALPN, NextProtos: v.option.ALPN,
} }
@ -563,6 +568,11 @@ func NewVless(option VlessOption) (*Vless, error) {
return nil, err return nil, err
} }
v.echConfig, err = v.option.ECHOpts.Parse()
if err != nil {
return nil, err
}
switch option.Network { switch option.Network {
case "h2": case "h2":
if len(option.HTTP2Opts.Host) == 0 { if len(option.HTTP2Opts.Host) == 0 {
@ -611,7 +621,7 @@ func NewVless(option VlessOption) (*Vless, error) {
v.gunTLSConfig = tlsConfig v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig 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 return v, nil

View File

@ -15,6 +15,7 @@ import (
"github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
tlsC "github.com/metacubex/mihomo/component/tls" tlsC "github.com/metacubex/mihomo/component/tls"
@ -41,6 +42,7 @@ type Vmess struct {
transport *gun.TransportWrap transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig realityConfig *tlsC.RealityConfig
echConfig *ech.Config
} }
type VmessOption struct { type VmessOption struct {
@ -58,6 +60,7 @@ type VmessOption struct {
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"`
ServerName string `proxy:"servername,omitempty"` ServerName string `proxy:"servername,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-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, V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen, V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
ECHConfig: v.echConfig,
Headers: http.Header{}, Headers: http.Header{},
} }
@ -146,6 +150,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig, Reality: v.realityConfig,
NextProtos: v.option.ALPN, 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) c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts)
case "grpc": 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:
// handle TLS // handle TLS
if v.option.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, SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint, FingerPrint: v.option.Fingerprint,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig, Reality: v.realityConfig,
NextProtos: v.option.ALPN, NextProtos: v.option.ALPN,
} }
@ -474,6 +480,11 @@ func NewVmess(option VmessOption) (*Vmess, error) {
return nil, err return nil, err
} }
v.echConfig, err = v.option.ECHOpts.Parse()
if err != nil {
return nil, err
}
switch option.Network { switch option.Network {
case "h2": case "h2":
if len(option.HTTP2Opts.Host) == 0 { if len(option.HTTP2Opts.Host) == 0 {
@ -522,7 +533,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
v.gunTLSConfig = tlsConfig v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig 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 return v, nil

35
component/ech/ech.go Normal file
View File

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

View File

@ -49,6 +49,7 @@ type Resolver interface {
LookupIP(ctx context.Context, host string) (ips []netip.Addr, err error) LookupIP(ctx context.Context, host string) (ips []netip.Addr, err error)
LookupIPv4(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) 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) ExchangeContext(ctx context.Context, m *dns.Msg) (msg *dns.Msg, err error)
Invalid() bool Invalid() bool
ClearCache() ClearCache()
@ -216,6 +217,17 @@ func ResolveIPPrefer6(ctx context.Context, host string) (netip.Addr, error) {
return ResolveIPPrefer6WithResolver(ctx, host, DefaultResolver) 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() { func ResetConnection() {
if DefaultResolver != nil { if DefaultResolver != nil {
go DefaultResolver.ResetConnection() go DefaultResolver.ResetConnection()

View File

@ -39,7 +39,7 @@ type RealityConfig struct {
SupportX25519MLKEM768 bool 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++ { for retry := 0; ; retry++ {
verifier := &realityVerifier{ verifier := &realityVerifier{
serverName: tlsConfig.ServerName, serverName: tlsConfig.ServerName,

View File

@ -127,6 +127,28 @@ func (r *Resolver) shouldIPFallback(ip netip.Addr) bool {
return false 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 // 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) { func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
if len(m.Question) == 0 { if len(m.Question) == 0 {

View File

@ -427,6 +427,10 @@ proxies: # socks5
# 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取
# 配置指纹将实现 SSL Pining 效果 # 配置指纹将实现 SSL Pining 效果
# fingerprint: xxxx # fingerprint: xxxx
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# skip-cert-verify: true # skip-cert-verify: true
# host: bing.com # host: bing.com
# path: "/" # path: "/"
@ -527,6 +531,10 @@ proxies: # socks5
# skip-cert-verify: true # skip-cert-verify: true
# servername: example.com # priority over wss host # servername: example.com # priority over wss host
# network: ws # network: ws
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# ws-opts: # ws-opts:
# path: /path # path: /path
# headers: # headers:
@ -599,6 +607,10 @@ proxies: # socks5
# skip-cert-verify: true # skip-cert-verify: true
# fingerprint: xxxx # fingerprint: xxxx
# client-fingerprint: random # Available: "chrome","firefox","safari","random","none" # 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" - name: "vless-vision"
type: vless type: vless
@ -683,6 +695,10 @@ proxies: # socks5
# enabled: false # enabled: false
# method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305 # method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305
# password: "example" # password: "example"
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
- name: trojan-grpc - name: trojan-grpc
server: server server: server

View File

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/mihomo/transport/vmess"
smux "github.com/metacubex/smux" smux "github.com/metacubex/smux"
) )
@ -18,6 +19,7 @@ type Option struct {
Path string Path string
Headers map[string]string Headers map[string]string
TLS bool TLS bool
ECHConfig *ech.Config
SkipCertVerify bool SkipCertVerify bool
Fingerprint string Fingerprint string
Mux bool Mux bool
@ -51,6 +53,7 @@ func NewGostWebsocket(ctx context.Context, conn net.Conn, option *Option) (net.C
Host: option.Host, Host: option.Host,
Port: option.Port, Port: option.Port,
Path: option.Path, Path: option.Path,
ECHConfig: option.ECHConfig,
Headers: header, Headers: header,
} }

View File

@ -21,6 +21,7 @@ import (
"github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/atomic"
"github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/common/buf"
"github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/common/pool"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls" tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
@ -224,7 +225,7 @@ func (g *Conn) SetDeadline(t time.Time) error {
return nil 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) { dialFunc := func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout) ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout)
defer cancel() defer cancel()
@ -238,8 +239,15 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, clientFingerprint stri
} }
if clientFingerprint, ok := tlsC.GetFingerprint(clientFingerprint); ok { 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 { if realityConfig == nil {
tlsConn := tlsC.UClient(pconn, tlsC.UConfig(cfg), clientFingerprint) tlsConn := tlsC.UClient(pconn, tlsConfig, clientFingerprint)
if err := tlsConn.HandshakeContext(ctx); err != nil { if err := tlsConn.HandshakeContext(ctx); err != nil {
pconn.Close() pconn.Close()
return nil, err return nil, err
@ -251,7 +259,7 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, clientFingerprint stri
} }
return tlsConn, nil return tlsConn, nil
} else { } else {
realityConn, err := tlsC.GetRealityConn(ctx, pconn, clientFingerprint, cfg, realityConfig) realityConn, err := tlsC.GetRealityConn(ctx, pconn, clientFingerprint, tlsConfig, realityConfig)
if err != nil { if err != nil {
pconn.Close() pconn.Close()
return nil, err 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") 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) conn := tls.Client(pconn, cfg)
if err := conn.HandshakeContext(ctx); err != nil { if err := conn.HandshakeContext(ctx); err != nil {
pconn.Close() pconn.Close()
@ -345,12 +374,12 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er
return conn, nil 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) { dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
return conn, nil return conn, nil
} }
transport := NewHTTP2Client(dialFn, tlsConfig, cfg.ClientFingerprint, realityConfig) transport := NewHTTP2Client(dialFn, tlsConfig, cfg.ClientFingerprint, echConfig, realityConfig)
c, err := StreamGunWithTransport(transport, cfg) c, err := StreamGunWithTransport(transport, cfg)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/mihomo/transport/vmess"
) )
@ -17,6 +18,7 @@ type Option struct {
Path string Path string
Headers map[string]string Headers map[string]string
TLS bool TLS bool
ECHConfig *ech.Config
SkipCertVerify bool SkipCertVerify bool
Fingerprint string Fingerprint string
Mux bool Mux bool
@ -37,6 +39,7 @@ func NewV2rayObfs(ctx context.Context, conn net.Conn, option *Option) (net.Conn,
Path: option.Path, Path: option.Path,
V2rayHttpUpgrade: option.V2rayHttpUpgrade, V2rayHttpUpgrade: option.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: option.V2rayHttpUpgradeFastOpen, V2rayHttpUpgradeFastOpen: option.V2rayHttpUpgradeFastOpen,
ECHConfig: option.ECHConfig,
Headers: header, Headers: header,
} }

View File

@ -7,6 +7,7 @@ import (
"net" "net"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls" tlsC "github.com/metacubex/mihomo/component/tls"
) )
@ -16,9 +17,14 @@ type TLSConfig struct {
FingerPrint string FingerPrint string
ClientFingerprint string ClientFingerprint string
NextProtos []string NextProtos []string
ECH *ech.Config
Reality *tlsC.RealityConfig Reality *tlsC.RealityConfig
} }
type ECHConfig struct {
Enable bool
}
func StreamTLSConn(ctx context.Context, conn net.Conn, cfg *TLSConfig) (net.Conn, error) { func StreamTLSConn(ctx context.Context, conn net.Conn, cfg *TLSConfig) (net.Conn, error) {
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
ServerName: cfg.Host, 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 { 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 { if cfg.Reality == nil {
tlsConn := tlsC.UClient(conn, tlsC.UConfig(tlsConfig), clientFingerprint) tlsConn := tlsC.UClient(conn, tlsConfig, clientFingerprint)
err = tlsConn.HandshakeContext(ctx) err = tlsConn.HandshakeContext(ctx)
if err != nil { if err != nil {
return nil, err 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") 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) tlsConn := tls.Client(conn, tlsConfig)
err = tlsConn.HandshakeContext(ctx) err = tlsConn.HandshakeContext(ctx)

View File

@ -21,6 +21,7 @@ import (
"github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/common/buf"
N "github.com/metacubex/mihomo/common/net" N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls" tlsC "github.com/metacubex/mihomo/component/tls"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
@ -56,6 +57,7 @@ type WebsocketConfig struct {
Headers http.Header Headers http.Header
TLS bool TLS bool
TLSConfig *tls.Config TLSConfig *tls.Config
ECHConfig *ech.Config
MaxEarlyData int MaxEarlyData int
EarlyDataHeaderName string EarlyDataHeaderName string
ClientFingerprint 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 { 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) tlsConn := tlsC.UClient(conn, tlsC.UConfig(config), clientFingerprint)
if err = tlsC.BuildWebsocketHandshakeState(tlsConn); err != nil { if err = tlsC.BuildWebsocketHandshakeState(tlsConn); err != nil {
return nil, fmt.Errorf("parse url %s error: %w", c.Path, err) 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 return nil, err
} }
conn = tlsConn 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 { } else {
tlsConn := tls.Client(conn, config) tlsConn := tls.Client(conn, config)
err = tlsConn.HandshakeContext(ctx) err = tlsConn.HandshakeContext(ctx)