From a1350d4985d4c3cc0ca97685d13c7b5f3df3b322 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sat, 17 May 2025 20:50:21 +0800 Subject: [PATCH] feat: add `ech-key` for listeners --- component/ech/key.go | 143 +++++++++++++++++++++++++++++ component/generater/cmd.go | 14 ++- component/tls/utls.go | 12 ++- docs/config.yaml | 63 +++++++++++++ listener/anytls/server.go | 18 +++- listener/config/anytls.go | 1 + listener/config/auth.go | 1 + listener/config/hysteria2.go | 1 + listener/config/trojan.go | 1 + listener/config/tuic.go | 1 + listener/config/vless.go | 1 + listener/config/vmess.go | 1 + listener/http/server.go | 16 +++- listener/inbound/anytls.go | 2 + listener/inbound/anytls_test.go | 10 ++ listener/inbound/common_test.go | 3 + listener/inbound/http.go | 2 + listener/inbound/hysteria2.go | 2 + listener/inbound/hysteria2_test.go | 30 ++++++ listener/inbound/mixed.go | 2 + listener/inbound/socks.go | 2 + listener/inbound/trojan.go | 2 + listener/inbound/trojan_test.go | 70 ++++++++++++++ listener/inbound/tuic.go | 2 + listener/inbound/tuic_test.go | 10 ++ listener/inbound/vless.go | 2 + listener/inbound/vless_test.go | 94 +++++++++++++++++++ listener/inbound/vmess.go | 2 + listener/inbound/vmess_test.go | 50 ++++++++++ listener/mixed/mixed.go | 16 +++- listener/sing_hysteria2/server.go | 17 +++- listener/sing_vless/server.go | 15 ++- listener/sing_vmess/server.go | 16 +++- listener/socks/tcp.go | 16 +++- listener/trojan/server.go | 16 +++- listener/tuic/server.go | 17 +++- transport/gun/gun.go | 4 +- transport/vless/vision/vision.go | 6 ++ 38 files changed, 637 insertions(+), 44 deletions(-) create mode 100644 component/ech/key.go diff --git a/component/ech/key.go b/component/ech/key.go new file mode 100644 index 000000000..afae8098c --- /dev/null +++ b/component/ech/key.go @@ -0,0 +1,143 @@ +package ech + +import ( + "crypto/ecdh" + "crypto/rand" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "os" + + "github.com/metacubex/mihomo/component/ca" + tlsC "github.com/metacubex/mihomo/component/tls" + + "golang.org/x/crypto/cryptobyte" +) + +const ( + AEAD_AES_128_GCM = 0x0001 + AEAD_AES_256_GCM = 0x0002 + AEAD_ChaCha20Poly1305 = 0x0003 +) + +const extensionEncryptedClientHello = 0xfe0d +const DHKEM_X25519_HKDF_SHA256 = 0x0020 +const KDF_HKDF_SHA256 = 0x0001 + +// sortedSupportedAEADs is just a sorted version of hpke.SupportedAEADS. +// We need this so that when we insert them into ECHConfigs the ordering +// is stable. +var sortedSupportedAEADs = []uint16{AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_ChaCha20Poly1305} + +func marshalECHConfig(id uint8, pubKey []byte, publicName string, maxNameLen uint8) []byte { + builder := cryptobyte.NewBuilder(nil) + + builder.AddUint16(extensionEncryptedClientHello) + builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddUint8(id) + + builder.AddUint16(DHKEM_X25519_HKDF_SHA256) // The only DHKEM we support + builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddBytes(pubKey) + }) + builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + for _, aeadID := range sortedSupportedAEADs { + builder.AddUint16(KDF_HKDF_SHA256) // The only KDF we support + builder.AddUint16(aeadID) + } + }) + builder.AddUint8(maxNameLen) + builder.AddUint8LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddBytes([]byte(publicName)) + }) + builder.AddUint16(0) // extensions + }) + + return builder.BytesOrPanic() +} + +func GenECHConfig(publicName string) (configBase64 string, keyPem string, err error) { + echKey, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + return + } + + echConfig := marshalECHConfig(0, echKey.PublicKey().Bytes(), publicName, 0) + + builder := cryptobyte.NewBuilder(nil) + builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddBytes(echConfig) + }) + echConfigList := builder.BytesOrPanic() + + builder2 := cryptobyte.NewBuilder(nil) + builder2.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddBytes(echKey.Bytes()) + }) + builder2.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddBytes(echConfig) + }) + echConfigKeys := builder2.BytesOrPanic() + + configBase64 = base64.StdEncoding.EncodeToString(echConfigList) + keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: echConfigKeys})) + return +} + +func UnmarshalECHKeys(raw []byte) ([]tlsC.EncryptedClientHelloKey, error) { + var keys []tlsC.EncryptedClientHelloKey + rawString := cryptobyte.String(raw) + for !rawString.Empty() { + var key tlsC.EncryptedClientHelloKey + if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.PrivateKey)) { + return nil, errors.New("error parsing private key") + } + if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) { + return nil, errors.New("error parsing config") + } + keys = append(keys, key) + } + if len(keys) == 0 { + return nil, errors.New("empty ECH keys") + } + return keys, nil +} + +func LoadECHKey(key string, tlsConfig *tlsC.Config, path ca.Path) error { + if key == "" { + return nil + } + painTextErr := loadECHKey([]byte(key), tlsConfig) + if painTextErr == nil { + return nil + } + key = path.Resolve(key) + var loadErr error + if !path.IsSafePath(key) { + loadErr = path.ErrNotSafePath(key) + } else { + var echKey []byte + echKey, loadErr = os.ReadFile(key) + if loadErr == nil { + loadErr = loadECHKey(echKey, tlsConfig) + } + } + if loadErr != nil { + return fmt.Errorf("parse ECH keys failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) + } + return nil +} + +func loadECHKey(echKey []byte, tlsConfig *tlsC.Config) error { + block, rest := pem.Decode(echKey) + if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 { + return errors.New("invalid ECH keys pem") + } + echKeys, err := UnmarshalECHKeys(block.Bytes) + if err != nil { + return fmt.Errorf("parse ECH keys: %w", err) + } + tlsConfig.EncryptedClientHelloKeys = echKeys + return nil +} diff --git a/component/generater/cmd.go b/component/generater/cmd.go index 9d2c3d976..96e62d7c2 100644 --- a/component/generater/cmd.go +++ b/component/generater/cmd.go @@ -4,12 +4,14 @@ import ( "encoding/base64" "fmt" + "github.com/metacubex/mihomo/component/ech" + "github.com/gofrs/uuid/v5" ) func Main(args []string) { if len(args) < 1 { - panic("Using: generate uuid/reality-keypair/wg-keypair") + panic("Using: generate uuid/reality-keypair/wg-keypair/ech-keypair") } switch args[0] { case "uuid": @@ -33,5 +35,15 @@ func Main(args []string) { } fmt.Println("PrivateKey: " + privateKey.String()) fmt.Println("PublicKey: " + privateKey.PublicKey().String()) + case "ech-keypair": + if len(args) < 2 { + panic("Using: generate ech-keypair ") + } + configBase64, keyPem, err := ech.GenECHConfig(args[1]) + if err != nil { + panic(err) + } + fmt.Println("Config:", configBase64) + fmt.Println("Key:", keyPem) } } diff --git a/component/tls/utls.go b/component/tls/utls.go index 80b37f38a..3a9312b3f 100644 --- a/component/tls/utls.go +++ b/component/tls/utls.go @@ -26,6 +26,10 @@ func UClient(c net.Conn, config *utls.Config, fingerprint UClientHelloID) *UConn return utls.UClient(c, config, fingerprint) } +func NewListener(inner net.Listener, config *Config) net.Listener { + return utls.NewListener(inner, config) +} + func GetFingerprint(clientFingerprint string) (UClientHelloID, bool) { if len(clientFingerprint) == 0 { clientFingerprint = globalFingerprint @@ -93,7 +97,9 @@ func init() { fingerprints["randomized"] = randomized } -func UCertificates(it tls.Certificate) utls.Certificate { +type Certificate = utls.Certificate + +func UCertificate(it tls.Certificate) utls.Certificate { return utls.Certificate{ Certificate: it.Certificate, PrivateKey: it.PrivateKey, @@ -106,13 +112,15 @@ func UCertificates(it tls.Certificate) utls.Certificate { } } +type EncryptedClientHelloKey = utls.EncryptedClientHelloKey + type Config = utls.Config func UConfig(config *tls.Config) *utls.Config { return &utls.Config{ Rand: config.Rand, Time: config.Time, - Certificates: utils.Map(config.Certificates, UCertificates), + Certificates: utils.Map(config.Certificates, UCertificate), VerifyPeerCertificate: config.VerifyPeerCertificate, RootCAs: config.RootCAs, NextProtos: config.NextProtos, diff --git a/docs/config.yaml b/docs/config.yaml index 221dbe46f..b74bc7c19 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -1155,6 +1155,13 @@ listeners: # 下面两项如果填写则开启 tls(需要同时填写) # certificate: ./server.crt # private-key: ./server.key + # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) + # ech-key: | + # -----BEGIN ECH KEYS----- + # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK + # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz + # dC5jb20AAA== + # -----END ECH KEYS----- - name: http-in-1 type: http @@ -1168,6 +1175,13 @@ listeners: # 下面两项如果填写则开启 tls(需要同时填写) # certificate: ./server.crt # private-key: ./server.key + # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) + # ech-key: | + # -----BEGIN ECH KEYS----- + # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK + # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz + # dC5jb20AAA== + # -----END ECH KEYS----- - name: mixed-in-1 type: mixed # HTTP(S) 和 SOCKS 代理混合 @@ -1182,6 +1196,13 @@ listeners: # 下面两项如果填写则开启 tls(需要同时填写) # certificate: ./server.crt # private-key: ./server.key + # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) + # ech-key: | + # -----BEGIN ECH KEYS----- + # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK + # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz + # dC5jb20AAA== + # -----END ECH KEYS----- - name: reidr-in-1 type: redir @@ -1231,6 +1252,13 @@ listeners: # 下面两项如果填写则开启 tls(需要同时填写) # certificate: ./server.crt # private-key: ./server.key + # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) + # ech-key: | + # -----BEGIN ECH KEYS----- + # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK + # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz + # dC5jb20AAA== + # -----END ECH KEYS----- # 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写) # reality-config: # dest: test.com:443 @@ -1253,6 +1281,13 @@ listeners: # 00000000-0000-0000-0000-000000000001: PASSWORD_1 # certificate: ./server.crt # private-key: ./server.key + # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) + # ech-key: | + # -----BEGIN ECH KEYS----- + # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK + # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz + # dC5jb20AAA== + # -----END ECH KEYS----- # congestion-controller: bbr # max-idle-time: 15000 # authentication-timeout: 1000 @@ -1284,6 +1319,13 @@ listeners: # 下面两项如果填写则开启 tls(需要同时填写) # certificate: ./server.crt # private-key: ./server.key + # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) + # ech-key: | + # -----BEGIN ECH KEYS----- + # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK + # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz + # dC5jb20AAA== + # -----END ECH KEYS----- # 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写) reality-config: dest: test.com:443 @@ -1304,6 +1346,13 @@ listeners: # "certificate" and "private-key" are required certificate: ./server.crt private-key: ./server.key + # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) + # ech-key: | + # -----BEGIN ECH KEYS----- + # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK + # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz + # dC5jb20AAA== + # -----END ECH KEYS----- # padding-scheme: "" # https://github.com/anytls/anytls-go/blob/main/docs/protocol.md#cmdupdatepaddingscheme - name: trojan-in-1 @@ -1320,6 +1369,13 @@ listeners: # 下面两项如果填写则开启 tls(需要同时填写) certificate: ./server.crt private-key: ./server.key + # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) + # ech-key: | + # -----BEGIN ECH KEYS----- + # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK + # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz + # dC5jb20AAA== + # -----END ECH KEYS----- # 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写) # reality-config: # dest: test.com:443 @@ -1345,6 +1401,13 @@ listeners: 00000000-0000-0000-0000-000000000001: PASSWORD_1 # certificate: ./server.crt # private-key: ./server.key + # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) + # ech-key: | + # -----BEGIN ECH KEYS----- + # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK + # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz + # dC5jb20AAA== + # -----END ECH KEYS----- ## up 和 down 均不写或为 0 则使用 BBR 流控 # up: "30 Mbps" # 若不写单位,默认为 Mbps # down: "200 Mbps" # 若不写单位,默认为 Mbps diff --git a/listener/anytls/server.go b/listener/anytls/server.go index db0a5503f..ae6af171e 100644 --- a/listener/anytls/server.go +++ b/listener/anytls/server.go @@ -3,7 +3,6 @@ package anytls import ( "context" "crypto/sha256" - "crypto/tls" "encoding/binary" "errors" "net" @@ -13,6 +12,8 @@ import ( "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/component/ca" + "github.com/metacubex/mihomo/component/ech" + tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing" @@ -28,7 +29,7 @@ type Listener struct { closed bool config LC.AnyTLSServer listeners []net.Listener - tlsConfig *tls.Config + tlsConfig *tlsC.Config userMap map[[32]byte]string padding atomic.TypedValue[*padding.PaddingFactory] } @@ -41,13 +42,20 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition) } } - tlsConfig := &tls.Config{} + tlsConfig := &tlsC.Config{} if config.Certificate != "" && config.PrivateKey != "" { cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)} + + if config.EchKey != "" { + err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + if err != nil { + return nil, err + } + } } sl = &Listener{ @@ -87,7 +95,7 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition) return nil, err } if len(tlsConfig.Certificates) > 0 { - l = tls.NewListener(l, tlsConfig) + l = tlsC.NewListener(l, tlsConfig) } else { return nil, errors.New("disallow using AnyTLS without certificates config") } diff --git a/listener/config/anytls.go b/listener/config/anytls.go index adbafa60f..874b79640 100644 --- a/listener/config/anytls.go +++ b/listener/config/anytls.go @@ -10,6 +10,7 @@ type AnyTLSServer struct { Users map[string]string `yaml:"users" json:"users,omitempty"` Certificate string `yaml:"certificate" json:"certificate"` PrivateKey string `yaml:"private-key" json:"private-key"` + EchKey string `yaml:"ech-key" json:"ech-key"` PaddingScheme string `yaml:"padding-scheme" json:"padding-scheme,omitempty"` } diff --git a/listener/config/auth.go b/listener/config/auth.go index a99f87fb2..cd0430ec8 100644 --- a/listener/config/auth.go +++ b/listener/config/auth.go @@ -12,5 +12,6 @@ type AuthServer struct { AuthStore auth.AuthStore Certificate string PrivateKey string + EchKey string RealityConfig reality.Config } diff --git a/listener/config/hysteria2.go b/listener/config/hysteria2.go index e26fbaa8a..e8042b0d5 100644 --- a/listener/config/hysteria2.go +++ b/listener/config/hysteria2.go @@ -14,6 +14,7 @@ type Hysteria2Server struct { ObfsPassword string `yaml:"obfs-password" json:"obfs-password,omitempty"` Certificate string `yaml:"certificate" json:"certificate"` PrivateKey string `yaml:"private-key" json:"private-key"` + EchKey string `yaml:"ech-key" json:"ech-key,omitempty"` MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` ALPN []string `yaml:"alpn" json:"alpn,omitempty"` Up string `yaml:"up" json:"up,omitempty"` diff --git a/listener/config/trojan.go b/listener/config/trojan.go index 28b6fe7c3..e38a2022c 100644 --- a/listener/config/trojan.go +++ b/listener/config/trojan.go @@ -20,6 +20,7 @@ type TrojanServer struct { GrpcServiceName string Certificate string PrivateKey string + EchKey string RealityConfig reality.Config MuxOption sing.MuxOption TrojanSSOption TrojanSSOption diff --git a/listener/config/tuic.go b/listener/config/tuic.go index 14a468092..d923e9a02 100644 --- a/listener/config/tuic.go +++ b/listener/config/tuic.go @@ -13,6 +13,7 @@ type TuicServer struct { Users map[string]string `yaml:"users" json:"users,omitempty"` Certificate string `yaml:"certificate" json:"certificate"` PrivateKey string `yaml:"private-key" json:"private-key"` + EchKey string `yaml:"ech-key" json:"ech-key"` CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"` MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` AuthenticationTimeout int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"` diff --git a/listener/config/vless.go b/listener/config/vless.go index d656db9fd..52ee3754a 100644 --- a/listener/config/vless.go +++ b/listener/config/vless.go @@ -21,6 +21,7 @@ type VlessServer struct { GrpcServiceName string Certificate string PrivateKey string + EchKey string RealityConfig reality.Config MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` } diff --git a/listener/config/vmess.go b/listener/config/vmess.go index 264b772c4..2a0e0054d 100644 --- a/listener/config/vmess.go +++ b/listener/config/vmess.go @@ -21,6 +21,7 @@ type VmessServer struct { GrpcServiceName string Certificate string PrivateKey string + EchKey string RealityConfig reality.Config MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` } diff --git a/listener/http/server.go b/listener/http/server.go index 52483081e..bacfa844e 100644 --- a/listener/http/server.go +++ b/listener/http/server.go @@ -1,12 +1,13 @@ package http import ( - "crypto/tls" "errors" "net" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/ca" + "github.com/metacubex/mihomo/component/ech" + tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" authStore "github.com/metacubex/mihomo/listener/auth" LC "github.com/metacubex/mihomo/listener/config" @@ -64,7 +65,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A return nil, err } - tlsConfig := &tls.Config{} + tlsConfig := &tlsC.Config{} var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { @@ -72,7 +73,14 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)} + + if config.EchKey != "" { + err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + if err != nil { + return nil, err + } + } } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { @@ -87,7 +95,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A if realityBuilder != nil { l = realityBuilder.NewListener(l) } else if len(tlsConfig.Certificates) > 0 { - l = tls.NewListener(l, tlsConfig) + l = tlsC.NewListener(l, tlsConfig) } hl := &Listener{ diff --git a/listener/inbound/anytls.go b/listener/inbound/anytls.go index 6f1e63508..224b05345 100644 --- a/listener/inbound/anytls.go +++ b/listener/inbound/anytls.go @@ -14,6 +14,7 @@ type AnyTLSOption struct { Users map[string]string `inbound:"users,omitempty"` Certificate string `inbound:"certificate"` PrivateKey string `inbound:"private-key"` + EchKey string `inbound:"ech-key,omitempty"` PaddingScheme string `inbound:"padding-scheme,omitempty"` } @@ -42,6 +43,7 @@ func NewAnyTLS(options *AnyTLSOption) (*AnyTLS, error) { Users: options.Users, Certificate: options.Certificate, PrivateKey: options.PrivateKey, + EchKey: options.EchKey, PaddingScheme: options.PaddingScheme, }, }, nil diff --git a/listener/inbound/anytls_test.go b/listener/inbound/anytls_test.go index 9d172890d..7759b4c7d 100644 --- a/listener/inbound/anytls_test.go +++ b/listener/inbound/anytls_test.go @@ -60,4 +60,14 @@ func TestInboundAnyTLS_TLS(t *testing.T) { Fingerprint: tlsFingerprint, } testInboundAnyTLS(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundAnyTLS(t, inboundOptions, outboundOptions) + }) } diff --git a/listener/inbound/common_test.go b/listener/inbound/common_test.go index 7c5718dc0..14bf52507 100644 --- a/listener/inbound/common_test.go +++ b/listener/inbound/common_test.go @@ -18,6 +18,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/generater" C "github.com/metacubex/mihomo/constant" @@ -38,6 +39,8 @@ var realityPrivateKey, realityPublickey string var realityDest = "itunes.apple.com" var realityShortid = "10f897e26c4b9478" var realityRealDial = false +var echPublicSni = "public.sni" +var echConfigBase64, echKeyPem, _ = ech.GenECHConfig(echPublicSni) func init() { rand.Read(httpData) diff --git a/listener/inbound/http.go b/listener/inbound/http.go index 8a4df0081..16693a21d 100644 --- a/listener/inbound/http.go +++ b/listener/inbound/http.go @@ -16,6 +16,7 @@ type HTTPOption struct { Users AuthUsers `inbound:"users,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` + EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` } @@ -64,6 +65,7 @@ func (h *HTTP) Listen(tunnel C.Tunnel) error { AuthStore: h.config.Users.GetAuthStore(), Certificate: h.config.Certificate, PrivateKey: h.config.PrivateKey, + EchKey: h.config.EchKey, RealityConfig: h.config.RealityConfig.Build(), }, tunnel, diff --git a/listener/inbound/hysteria2.go b/listener/inbound/hysteria2.go index 62d19c829..bcdca0b78 100644 --- a/listener/inbound/hysteria2.go +++ b/listener/inbound/hysteria2.go @@ -16,6 +16,7 @@ type Hysteria2Option struct { ObfsPassword string `inbound:"obfs-password,omitempty"` Certificate string `inbound:"certificate"` PrivateKey string `inbound:"private-key"` + EchKey string `inbound:"ech-key,omitempty"` MaxIdleTime int `inbound:"max-idle-time,omitempty"` ALPN []string `inbound:"alpn,omitempty"` Up string `inbound:"up,omitempty"` @@ -60,6 +61,7 @@ func NewHysteria2(options *Hysteria2Option) (*Hysteria2, error) { ObfsPassword: options.ObfsPassword, Certificate: options.Certificate, PrivateKey: options.PrivateKey, + EchKey: options.EchKey, MaxIdleTime: options.MaxIdleTime, ALPN: options.ALPN, Up: options.Up, diff --git a/listener/inbound/hysteria2_test.go b/listener/inbound/hysteria2_test.go index 2926a9e6c..fd2d41173 100644 --- a/listener/inbound/hysteria2_test.go +++ b/listener/inbound/hysteria2_test.go @@ -60,6 +60,16 @@ func TestInboundHysteria2_TLS(t *testing.T) { Fingerprint: tlsFingerprint, } testInboundHysteria2(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundHysteria2(t, inboundOptions, outboundOptions) + }) } func TestInboundHysteria2_Salamander(t *testing.T) { @@ -75,6 +85,16 @@ func TestInboundHysteria2_Salamander(t *testing.T) { ObfsPassword: userUUID, } testInboundHysteria2(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundHysteria2(t, inboundOptions, outboundOptions) + }) } func TestInboundHysteria2_Brutal(t *testing.T) { @@ -90,4 +110,14 @@ func TestInboundHysteria2_Brutal(t *testing.T) { Down: "200 Mbps", } testInboundHysteria2(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundHysteria2(t, inboundOptions, outboundOptions) + }) } diff --git a/listener/inbound/mixed.go b/listener/inbound/mixed.go index 20c61f2ee..db32512be 100644 --- a/listener/inbound/mixed.go +++ b/listener/inbound/mixed.go @@ -18,6 +18,7 @@ type MixedOption struct { UDP bool `inbound:"udp,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` + EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` } @@ -69,6 +70,7 @@ func (m *Mixed) Listen(tunnel C.Tunnel) error { AuthStore: m.config.Users.GetAuthStore(), Certificate: m.config.Certificate, PrivateKey: m.config.PrivateKey, + EchKey: m.config.EchKey, RealityConfig: m.config.RealityConfig.Build(), }, tunnel, diff --git a/listener/inbound/socks.go b/listener/inbound/socks.go index 6cb9782cf..fa794ae42 100644 --- a/listener/inbound/socks.go +++ b/listener/inbound/socks.go @@ -17,6 +17,7 @@ type SocksOption struct { UDP bool `inbound:"udp,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` + EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` } @@ -89,6 +90,7 @@ func (s *Socks) Listen(tunnel C.Tunnel) error { AuthStore: s.config.Users.GetAuthStore(), Certificate: s.config.Certificate, PrivateKey: s.config.PrivateKey, + EchKey: s.config.EchKey, RealityConfig: s.config.RealityConfig.Build(), }, tunnel, diff --git a/listener/inbound/trojan.go b/listener/inbound/trojan.go index 44c56b0b4..04d73bf82 100644 --- a/listener/inbound/trojan.go +++ b/listener/inbound/trojan.go @@ -16,6 +16,7 @@ type TrojanOption struct { GrpcServiceName string `inbound:"grpc-service-name,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` + EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` SSOption TrojanSSOption `inbound:"ss-option,omitempty"` @@ -67,6 +68,7 @@ func NewTrojan(options *TrojanOption) (*Trojan, error) { GrpcServiceName: options.GrpcServiceName, Certificate: options.Certificate, PrivateKey: options.PrivateKey, + EchKey: options.EchKey, RealityConfig: options.RealityConfig.Build(), MuxOption: options.MuxOption.Build(), TrojanSSOption: LC.TrojanSSOption{ diff --git a/listener/inbound/trojan_test.go b/listener/inbound/trojan_test.go index 320081f8c..5fd584912 100644 --- a/listener/inbound/trojan_test.go +++ b/listener/inbound/trojan_test.go @@ -64,6 +64,16 @@ func TestInboundTrojan_TLS(t *testing.T) { Fingerprint: tlsFingerprint, } testInboundTrojan(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundTrojan(t, inboundOptions, outboundOptions) + }) } func TestInboundTrojan_Wss1(t *testing.T) { @@ -80,6 +90,16 @@ func TestInboundTrojan_Wss1(t *testing.T) { }, } testInboundTrojan(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundTrojan(t, inboundOptions, outboundOptions) + }) } func TestInboundTrojan_Wss2(t *testing.T) { @@ -97,6 +117,16 @@ func TestInboundTrojan_Wss2(t *testing.T) { }, } testInboundTrojan(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundTrojan(t, inboundOptions, outboundOptions) + }) } func TestInboundTrojan_Grpc1(t *testing.T) { @@ -111,6 +141,16 @@ func TestInboundTrojan_Grpc1(t *testing.T) { GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } testInboundTrojan(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundTrojan(t, inboundOptions, outboundOptions) + }) } func TestInboundTrojan_Grpc2(t *testing.T) { @@ -126,6 +166,16 @@ func TestInboundTrojan_Grpc2(t *testing.T) { GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } testInboundTrojan(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundTrojan(t, inboundOptions, outboundOptions) + }) } func TestInboundTrojan_Reality(t *testing.T) { @@ -190,6 +240,16 @@ func TestInboundTrojan_TLS_TrojanSS(t *testing.T) { }, } testInboundTrojan(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundTrojan(t, inboundOptions, outboundOptions) + }) } func TestInboundTrojan_Wss_TrojanSS(t *testing.T) { @@ -216,4 +276,14 @@ func TestInboundTrojan_Wss_TrojanSS(t *testing.T) { }, } testInboundTrojan(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundTrojan(t, inboundOptions, outboundOptions) + }) } diff --git a/listener/inbound/tuic.go b/listener/inbound/tuic.go index e9c2f21bc..67349156c 100644 --- a/listener/inbound/tuic.go +++ b/listener/inbound/tuic.go @@ -15,6 +15,7 @@ type TuicOption struct { Users map[string]string `inbound:"users,omitempty"` Certificate string `inbound:"certificate"` PrivateKey string `inbound:"private-key"` + EchKey string `inbound:"ech-key,omitempty"` CongestionController string `inbound:"congestion-controller,omitempty"` MaxIdleTime int `inbound:"max-idle-time,omitempty"` AuthenticationTimeout int `inbound:"authentication-timeout,omitempty"` @@ -50,6 +51,7 @@ func NewTuic(options *TuicOption) (*Tuic, error) { Users: options.Users, Certificate: options.Certificate, PrivateKey: options.PrivateKey, + EchKey: options.EchKey, CongestionController: options.CongestionController, MaxIdleTime: options.MaxIdleTime, AuthenticationTimeout: options.AuthenticationTimeout, diff --git a/listener/inbound/tuic_test.go b/listener/inbound/tuic_test.go index 24865d833..1cf3991de 100644 --- a/listener/inbound/tuic_test.go +++ b/listener/inbound/tuic_test.go @@ -89,4 +89,14 @@ func TestInboundTuic_TLS(t *testing.T) { Fingerprint: tlsFingerprint, } testInboundTuic(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundTuic(t, inboundOptions, outboundOptions) + }) } diff --git a/listener/inbound/vless.go b/listener/inbound/vless.go index 0cbf214fb..947e3a53e 100644 --- a/listener/inbound/vless.go +++ b/listener/inbound/vless.go @@ -16,6 +16,7 @@ type VlessOption struct { GrpcServiceName string `inbound:"grpc-service-name,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` + EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` } @@ -61,6 +62,7 @@ func NewVless(options *VlessOption) (*Vless, error) { GrpcServiceName: options.GrpcServiceName, Certificate: options.Certificate, PrivateKey: options.PrivateKey, + EchKey: options.EchKey, RealityConfig: options.RealityConfig.Build(), MuxOption: options.MuxOption.Build(), }, diff --git a/listener/inbound/vless_test.go b/listener/inbound/vless_test.go index f19cad348..d2056c05e 100644 --- a/listener/inbound/vless_test.go +++ b/listener/inbound/vless_test.go @@ -66,9 +66,25 @@ func TestInboundVless_TLS(t *testing.T) { } testInboundVless(t, inboundOptions, outboundOptions) t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions outboundOptions.Flow = "xtls-rprx-vision" testInboundVless(t, inboundOptions, outboundOptions) }) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundVless(t, inboundOptions, outboundOptions) + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + }) } func TestInboundVless_Wss1(t *testing.T) { @@ -87,9 +103,25 @@ func TestInboundVless_Wss1(t *testing.T) { } testInboundVless(t, inboundOptions, outboundOptions) t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions outboundOptions.Flow = "xtls-rprx-vision" testInboundVless(t, inboundOptions, outboundOptions) }) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundVless(t, inboundOptions, outboundOptions) + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + }) } func TestInboundVless_Wss2(t *testing.T) { @@ -109,9 +141,25 @@ func TestInboundVless_Wss2(t *testing.T) { } testInboundVless(t, inboundOptions, outboundOptions) t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions outboundOptions.Flow = "xtls-rprx-vision" testInboundVless(t, inboundOptions, outboundOptions) }) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundVless(t, inboundOptions, outboundOptions) + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + }) } func TestInboundVless_Grpc1(t *testing.T) { @@ -127,6 +175,16 @@ func TestInboundVless_Grpc1(t *testing.T) { GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } testInboundVless(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundVless(t, inboundOptions, outboundOptions) + }) } func TestInboundVless_Grpc2(t *testing.T) { @@ -143,6 +201,16 @@ func TestInboundVless_Grpc2(t *testing.T) { GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } testInboundVless(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundVless(t, inboundOptions, outboundOptions) + }) } func TestInboundVless_Reality(t *testing.T) { @@ -165,9 +233,25 @@ func TestInboundVless_Reality(t *testing.T) { } testInboundVless(t, inboundOptions, outboundOptions) t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions outboundOptions.Flow = "xtls-rprx-vision" testInboundVless(t, inboundOptions, outboundOptions) }) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundVless(t, inboundOptions, outboundOptions) + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + }) } func TestInboundVless_Reality_Grpc(t *testing.T) { @@ -192,4 +276,14 @@ func TestInboundVless_Reality_Grpc(t *testing.T) { GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } testInboundVless(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundVless(t, inboundOptions, outboundOptions) + }) } diff --git a/listener/inbound/vmess.go b/listener/inbound/vmess.go index 2212a75db..c04ed0933 100644 --- a/listener/inbound/vmess.go +++ b/listener/inbound/vmess.go @@ -16,6 +16,7 @@ type VmessOption struct { GrpcServiceName string `inbound:"grpc-service-name,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` + EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` } @@ -61,6 +62,7 @@ func NewVmess(options *VmessOption) (*Vmess, error) { GrpcServiceName: options.GrpcServiceName, Certificate: options.Certificate, PrivateKey: options.PrivateKey, + EchKey: options.EchKey, RealityConfig: options.RealityConfig.Build(), MuxOption: options.MuxOption.Build(), }, diff --git a/listener/inbound/vmess_test.go b/listener/inbound/vmess_test.go index 57af5b0b9..58d23dfc5 100644 --- a/listener/inbound/vmess_test.go +++ b/listener/inbound/vmess_test.go @@ -73,6 +73,16 @@ func TestInboundVMess_TLS(t *testing.T) { Fingerprint: tlsFingerprint, } testInboundVMess(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundVMess(t, inboundOptions, outboundOptions) + }) } func TestInboundVMess_Ws(t *testing.T) { @@ -160,6 +170,16 @@ func TestInboundVMess_Wss1(t *testing.T) { }, } testInboundVMess(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundVMess(t, inboundOptions, outboundOptions) + }) } func TestInboundVMess_Wss2(t *testing.T) { @@ -178,6 +198,16 @@ func TestInboundVMess_Wss2(t *testing.T) { }, } testInboundVMess(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundVMess(t, inboundOptions, outboundOptions) + }) } func TestInboundVMess_Grpc1(t *testing.T) { @@ -193,6 +223,16 @@ func TestInboundVMess_Grpc1(t *testing.T) { GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } testInboundVMess(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundVMess(t, inboundOptions, outboundOptions) + }) } func TestInboundVMess_Grpc2(t *testing.T) { @@ -209,6 +249,16 @@ func TestInboundVMess_Grpc2(t *testing.T) { GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } testInboundVMess(t, inboundOptions, outboundOptions) + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundVMess(t, inboundOptions, outboundOptions) + }) } func TestInboundVMess_Reality(t *testing.T) { diff --git a/listener/mixed/mixed.go b/listener/mixed/mixed.go index 6893bb5a1..d9d99ecab 100644 --- a/listener/mixed/mixed.go +++ b/listener/mixed/mixed.go @@ -1,7 +1,6 @@ package mixed import ( - "crypto/tls" "errors" "net" @@ -9,6 +8,8 @@ import ( N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/auth" "github.com/metacubex/mihomo/component/ca" + "github.com/metacubex/mihomo/component/ech" + tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" authStore "github.com/metacubex/mihomo/listener/auth" LC "github.com/metacubex/mihomo/listener/config" @@ -60,7 +61,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A return nil, err } - tlsConfig := &tls.Config{} + tlsConfig := &tlsC.Config{} var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { @@ -68,7 +69,14 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)} + + if config.EchKey != "" { + err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + if err != nil { + return nil, err + } + } } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { @@ -83,7 +91,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A if realityBuilder != nil { l = realityBuilder.NewListener(l) } else if len(tlsConfig.Certificates) > 0 { - l = tls.NewListener(l, tlsConfig) + l = tlsC.NewListener(l, tlsConfig) } ml := &Listener{ diff --git a/listener/sing_hysteria2/server.go b/listener/sing_hysteria2/server.go index 0090926ab..849ed4452 100644 --- a/listener/sing_hysteria2/server.go +++ b/listener/sing_hysteria2/server.go @@ -2,7 +2,6 @@ package sing_hysteria2 import ( "context" - "crypto/tls" "errors" "fmt" "net" @@ -15,6 +14,7 @@ import ( "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/common/sockopt" "github.com/metacubex/mihomo/component/ca" + "github.com/metacubex/mihomo/component/ech" tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" @@ -60,9 +60,16 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi if err != nil { return nil, err } - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS13, - Certificates: []tls.Certificate{cert}, + tlsConfig := &tlsC.Config{ + MinVersion: tlsC.VersionTLS13, + } + tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)} + + if config.EchKey != "" { + err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + if err != nil { + return nil, err + } } if len(config.ALPN) > 0 { tlsConfig.NextProtos = config.ALPN @@ -125,7 +132,7 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi SendBPS: outbound.StringToBps(config.Up), ReceiveBPS: outbound.StringToBps(config.Down), SalamanderPassword: salamanderPassword, - TLSConfig: tlsC.UConfig(tlsConfig), + TLSConfig: tlsConfig, QUICConfig: quicConfig, IgnoreClientBandwidth: config.IgnoreClientBandwidth, UDPTimeout: sing.UDPTimeout, diff --git a/listener/sing_vless/server.go b/listener/sing_vless/server.go index 97a62fe49..90cff6572 100644 --- a/listener/sing_vless/server.go +++ b/listener/sing_vless/server.go @@ -2,7 +2,6 @@ package sing_vless import ( "context" - "crypto/tls" "errors" "net" "net/http" @@ -12,6 +11,7 @@ import ( "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/ca" + "github.com/metacubex/mihomo/component/ech" tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" @@ -82,7 +82,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) sl = &Listener{false, config, nil, service} - tlsConfig := &tls.Config{} + tlsConfig := &tlsC.Config{} var realityBuilder *reality.Builder var httpHandler http.Handler @@ -91,7 +91,14 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)} + + if config.EchKey != "" { + err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + if err != nil { + return nil, err + } + } } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { @@ -137,7 +144,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) if realityBuilder != nil { l = realityBuilder.NewListener(l) } else if len(tlsConfig.Certificates) > 0 { - l = tls.NewListener(l, tlsConfig) + l = tlsC.NewListener(l, tlsConfig) } else { return nil, errors.New("disallow using Vless without both certificates/reality config") } diff --git a/listener/sing_vmess/server.go b/listener/sing_vmess/server.go index b5a9378fd..b65a294bd 100644 --- a/listener/sing_vmess/server.go +++ b/listener/sing_vmess/server.go @@ -2,7 +2,6 @@ package sing_vmess import ( "context" - "crypto/tls" "errors" "net" "net/http" @@ -11,6 +10,8 @@ import ( "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/ca" + "github.com/metacubex/mihomo/component/ech" + tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/reality" @@ -75,7 +76,7 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) sl = &Listener{false, config, nil, service} - tlsConfig := &tls.Config{} + tlsConfig := &tlsC.Config{} var realityBuilder *reality.Builder var httpHandler http.Handler @@ -84,7 +85,14 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)} + + if config.EchKey != "" { + err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + if err != nil { + return nil, err + } + } } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { @@ -130,7 +138,7 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) if realityBuilder != nil { l = realityBuilder.NewListener(l) } else if len(tlsConfig.Certificates) > 0 { - l = tls.NewListener(l, tlsConfig) + l = tlsC.NewListener(l, tlsConfig) } sl.listeners = append(sl.listeners, l) diff --git a/listener/socks/tcp.go b/listener/socks/tcp.go index ab4086a38..33cf02f08 100644 --- a/listener/socks/tcp.go +++ b/listener/socks/tcp.go @@ -1,7 +1,6 @@ package socks import ( - "crypto/tls" "errors" "io" "net" @@ -10,6 +9,8 @@ import ( N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/auth" "github.com/metacubex/mihomo/component/ca" + "github.com/metacubex/mihomo/component/ech" + tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" authStore "github.com/metacubex/mihomo/listener/auth" LC "github.com/metacubex/mihomo/listener/config" @@ -59,7 +60,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A return nil, err } - tlsConfig := &tls.Config{} + tlsConfig := &tlsC.Config{} var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { @@ -67,7 +68,14 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)} + + if config.EchKey != "" { + err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + if err != nil { + return nil, err + } + } } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { @@ -82,7 +90,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A if realityBuilder != nil { l = realityBuilder.NewListener(l) } else if len(tlsConfig.Certificates) > 0 { - l = tls.NewListener(l, tlsConfig) + l = tlsC.NewListener(l, tlsConfig) } sl := &Listener{ diff --git a/listener/trojan/server.go b/listener/trojan/server.go index d3ca98d78..780273f21 100644 --- a/listener/trojan/server.go +++ b/listener/trojan/server.go @@ -1,7 +1,6 @@ package trojan import ( - "crypto/tls" "errors" "io" "net" @@ -10,6 +9,8 @@ import ( "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/ca" + "github.com/metacubex/mihomo/component/ech" + tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/reality" @@ -69,7 +70,7 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) } sl = &Listener{false, config, nil, keys, pickCipher, h} - tlsConfig := &tls.Config{} + tlsConfig := &tlsC.Config{} var realityBuilder *reality.Builder var httpHandler http.Handler @@ -78,7 +79,14 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)} + + if config.EchKey != "" { + err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + if err != nil { + return nil, err + } + } } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { @@ -124,7 +132,7 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) if realityBuilder != nil { l = realityBuilder.NewListener(l) } else if len(tlsConfig.Certificates) > 0 { - l = tls.NewListener(l, tlsConfig) + l = tlsC.NewListener(l, tlsConfig) } else if !config.TrojanSSOption.Enabled { return nil, errors.New("disallow using Trojan without both certificates/reality/ss config") } diff --git a/listener/tuic/server.go b/listener/tuic/server.go index 7bc63a737..2037177e3 100644 --- a/listener/tuic/server.go +++ b/listener/tuic/server.go @@ -1,7 +1,6 @@ package tuic import ( - "crypto/tls" "net" "strings" "time" @@ -9,6 +8,7 @@ import ( "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/sockopt" "github.com/metacubex/mihomo/component/ca" + "github.com/metacubex/mihomo/component/ech" tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" @@ -52,9 +52,16 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) ( if err != nil { return nil, err } - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS13, - Certificates: []tls.Certificate{cert}, + tlsConfig := &tlsC.Config{ + MinVersion: tlsC.VersionTLS13, + } + tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)} + + if config.EchKey != "" { + err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) + if err != nil { + return nil, err + } } if len(config.ALPN) > 0 { tlsConfig.NextProtos = config.ALPN @@ -125,7 +132,7 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) ( option := &tuic.ServerOption{ HandleTcpFn: handleTcpFn, HandleUdpFn: handleUdpFn, - TlsConfig: tlsC.UConfig(tlsConfig), + TlsConfig: tlsConfig, QuicConfig: quicConfig, CongestionController: config.CongestionController, AuthenticationTimeout: time.Duration(config.AuthenticationTimeout) * time.Millisecond, diff --git a/transport/gun/gun.go b/transport/gun/gun.go index 0b387d726..7c9ab3e1e 100644 --- a/transport/gun/gun.go +++ b/transport/gun/gun.go @@ -239,7 +239,7 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, clientFingerprint stri } if clientFingerprint, ok := tlsC.GetFingerprint(clientFingerprint); ok { - tlsConfig := tlsC.UConfig(tlsConfig) + tlsConfig := tlsC.UConfig(cfg) err := echConfig.ClientHandle(ctx, tlsConfig) if err != nil { pconn.Close() @@ -277,7 +277,7 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, clientFingerprint stri } if echConfig != nil { - tlsConfig := tlsC.UConfig(tlsConfig) + tlsConfig := tlsC.UConfig(cfg) err := echConfig.ClientHandle(ctx, tlsConfig) if err != nil { pconn.Close() diff --git a/transport/vless/vision/vision.go b/transport/vless/vision/vision.go index 28fd2fcea..00d0bf09d 100644 --- a/transport/vless/vision/vision.go +++ b/transport/vless/vision/vision.go @@ -45,6 +45,12 @@ func NewConn(conn connWithUpstream, userUUID *uuid.UUID) (*Conn, error) { c.tlsConn = underlying t = reflect.TypeOf(underlying).Elem() p = unsafe.Pointer(underlying) + case *tlsC.Conn: + //log.Debugln("type *tlsC.Conn") + c.Conn = underlying.NetConn() + c.tlsConn = underlying + t = reflect.TypeOf(underlying).Elem() + p = unsafe.Pointer(underlying) case *tlsC.UConn: //log.Debugln("type *tlsC.UConn") c.Conn = underlying.NetConn()