From f91a586da85a99fe8686eb79269001843211e9ed Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 13 May 2025 19:00:32 +0800 Subject: [PATCH 01/23] fix: inline proxy provider's healthcheck not work --- adapter/provider/healthcheck.go | 2 +- adapter/provider/provider.go | 31 ++++++++++++++----------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/adapter/provider/healthcheck.go b/adapter/provider/healthcheck.go index f16718e927..822a00d518 100644 --- a/adapter/provider/healthcheck.go +++ b/adapter/provider/healthcheck.go @@ -61,7 +61,7 @@ func (hc *HealthCheck) process() { } } -func (hc *HealthCheck) setProxy(proxies []C.Proxy) { +func (hc *HealthCheck) setProxies(proxies []C.Proxy) { hc.proxies = proxies } diff --git a/adapter/provider/provider.go b/adapter/provider/provider.go index 8c88d1e8bc..3c73d36140 100644 --- a/adapter/provider/provider.go +++ b/adapter/provider/provider.go @@ -57,6 +57,13 @@ func (bp *baseProvider) Version() uint32 { return bp.version } +func (bp *baseProvider) Initial() error { + if bp.healthCheck.auto() { + go bp.healthCheck.process() + } + return nil +} + func (bp *baseProvider) HealthCheck() { bp.healthCheck.check() } @@ -88,7 +95,7 @@ func (bp *baseProvider) RegisterHealthCheckTask(url string, expectedStatus utils func (bp *baseProvider) setProxies(proxies []C.Proxy) { bp.proxies = proxies bp.version += 1 - bp.healthCheck.setProxy(proxies) + bp.healthCheck.setProxies(proxies) if bp.healthCheck.auto() { go bp.healthCheck.check() } @@ -133,8 +140,8 @@ func (pp *proxySetProvider) Update() error { } func (pp *proxySetProvider) Initial() error { - if pp.healthCheck.auto() { - go pp.healthCheck.process() + if err := pp.baseProvider.Initial(); err != nil { + return err } _, err := pp.Fetcher.Initial() if err != nil { @@ -184,6 +191,8 @@ func NewProxySetProvider(name string, interval time.Duration, payload []map[stri return nil, err } pd.proxies = proxies + // direct call setProxies on hc to avoid starting a health check process immediately, it should be done by Initial() + hc.setProxies(proxies) } fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, parser, pd.setProxies) @@ -233,13 +242,6 @@ func (ip *inlineProvider) VehicleType() types.VehicleType { return types.Inline } -func (ip *inlineProvider) Initial() error { - if ip.healthCheck.auto() { - go ip.healthCheck.process() - } - return nil -} - func (ip *inlineProvider) Update() error { // make api update happy ip.updateAt = time.Now() @@ -256,6 +258,8 @@ func NewInlineProvider(name string, payload []map[string]any, parser resource.Pa if err != nil { return nil, err } + // direct call setProxies on hc to avoid starting a health check process immediately, it should be done by Initial() + hc.setProxies(proxies) ip := &inlineProvider{ baseProvider: baseProvider{ @@ -299,13 +303,6 @@ func (cp *compatibleProvider) Update() error { return nil } -func (cp *compatibleProvider) Initial() error { - if cp.healthCheck.auto() { - go cp.healthCheck.process() - } - return nil -} - func (cp *compatibleProvider) VehicleType() types.VehicleType { return types.Compatible } From 90ed01ed533a47a8e0856fa59f1822b560573821 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Wed, 14 May 2025 21:45:12 +0800 Subject: [PATCH 02/23] fix: backoff not reset when the file unchanged --- component/resource/fetcher.go | 1 + 1 file changed, 1 insertion(+) diff --git a/component/resource/fetcher.go b/component/resource/fetcher.go index 3658e1a18b..f2c9b5bdd9 100644 --- a/component/resource/fetcher.go +++ b/component/resource/fetcher.go @@ -105,6 +105,7 @@ func (f *Fetcher[V]) loadBuf(buf []byte, hash utils.HashType, updateFile bool) ( _ = os.Chtimes(f.vehicle.Path(), now, now) } f.updatedAt = now + f.backoff.Reset() // no error, reset backoff return lo.Empty[V](), true, nil } From 83213d493eb7ece7b0c8389c4b892c7fbfa01539 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Wed, 14 May 2025 21:51:18 +0800 Subject: [PATCH 03/23] chore: adjust min backoff from 1s to 10s --- component/resource/fetcher.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/component/resource/fetcher.go b/component/resource/fetcher.go index f2c9b5bdd9..c384277cc9 100644 --- a/component/resource/fetcher.go +++ b/component/resource/fetcher.go @@ -221,6 +221,10 @@ func (f *Fetcher[V]) updateWithLog() { func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicle, parser Parser[V], onUpdate func(V)) *Fetcher[V] { ctx, cancel := context.WithCancel(context.Background()) + minBackoff := 10 * time.Second + if interval < minBackoff { + minBackoff = interval + } return &Fetcher[V]{ ctx: ctx, ctxCancel: cancel, @@ -232,7 +236,7 @@ func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicl backoff: slowdown.Backoff{ Factor: 2, Jitter: false, - Min: time.Second, + Min: minBackoff, Max: interval, }, } From 5cf0f18c29a0bb791fafa01a74a85eb9148af6be Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Thu, 15 May 2025 10:14:18 +0800 Subject: [PATCH 04/23] feat: reality add `support-x25519mlkem768`, it only works with new version server --- adapter/outbound/reality.go | 3 +++ component/tls/reality.go | 48 ++++++++++++++++++------------------- go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/adapter/outbound/reality.go b/adapter/outbound/reality.go index e5b090a8b8..55d3cba9c7 100644 --- a/adapter/outbound/reality.go +++ b/adapter/outbound/reality.go @@ -13,11 +13,14 @@ import ( type RealityOptions struct { PublicKey string `proxy:"public-key"` ShortID string `proxy:"short-id"` + + SupportX25519MLKEM768 bool `proxy:"support-x25519mlkem768"` } func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) { if o.PublicKey != "" { config := new(tlsC.RealityConfig) + config.SupportX25519MLKEM768 = o.SupportX25519MLKEM768 const x25519ScalarSize = 32 publicKey, err := base64.RawURLEncoding.DecodeString(o.PublicKey) diff --git a/component/tls/reality.go b/component/tls/reality.go index 6a5cdc5fe1..b16676f026 100644 --- a/component/tls/reality.go +++ b/component/tls/reality.go @@ -35,6 +35,8 @@ const RealityMaxShortIDLen = 8 type RealityConfig struct { PublicKey *ecdh.PublicKey ShortID [RealityMaxShortIDLen]byte + + SupportX25519MLKEM768 bool } func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHelloID, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) { @@ -48,38 +50,36 @@ func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHello SessionTicketsDisabled: true, VerifyPeerCertificate: verifier.VerifyPeerCertificate, } - clientID := utls.ClientHelloID{ - Client: fingerprint.Client, - Version: fingerprint.Version, - Seed: fingerprint.Seed, - } - uConn := utls.UClient(conn, uConfig, clientID) + + uConn := utls.UClient(conn, uConfig, fingerprint) verifier.UConn = uConn err := uConn.BuildHandshakeState() if err != nil { return nil, err } - // ------for X25519MLKEM768 does not work properly with reality------- - // Iterate over extensions and check - for _, extension := range uConn.Extensions { - if ce, ok := extension.(*utls.SupportedCurvesExtension); ok { - ce.Curves = slices.DeleteFunc(ce.Curves, func(curveID utls.CurveID) bool { - return curveID == utls.X25519MLKEM768 - }) + if !realityConfig.SupportX25519MLKEM768 { + // ------for X25519MLKEM768 does not work properly with the old reality server------- + // Iterate over extensions and check + for _, extension := range uConn.Extensions { + if ce, ok := extension.(*utls.SupportedCurvesExtension); ok { + ce.Curves = slices.DeleteFunc(ce.Curves, func(curveID utls.CurveID) bool { + return curveID == utls.X25519MLKEM768 + }) + } + if ks, ok := extension.(*utls.KeyShareExtension); ok { + ks.KeyShares = slices.DeleteFunc(ks.KeyShares, func(share utls.KeyShare) bool { + return share.Group == utls.X25519MLKEM768 + }) + } } - if ks, ok := extension.(*utls.KeyShareExtension); ok { - ks.KeyShares = slices.DeleteFunc(ks.KeyShares, func(share utls.KeyShare) bool { - return share.Group == utls.X25519MLKEM768 - }) + // Rebuild the client hello + err = uConn.BuildHandshakeState() + if err != nil { + return nil, err } + // -------------------------------------------------------------------- } - // Rebuild the client hello - err = uConn.BuildHandshakeState() - if err != nil { - return nil, err - } - // -------------------------------------------------------------------- hello := uConn.HandshakeState.Hello rawSessionID := hello.Raw[39 : 39+32] // the location of session ID @@ -144,7 +144,7 @@ func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHello log.Debugln("REALITY Authentication: %v, AEAD: %T", verifier.verified, aeadCipher) if !verifier.verified { - go realityClientFallback(uConn, uConfig.ServerName, clientID) + go realityClientFallback(uConn, uConfig.ServerName, fingerprint) return nil, errors.New("REALITY authentication failed") } diff --git a/go.mod b/go.mod index a4ecb0fbd4..b378a23fb6 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f github.com/metacubex/smux v0.0.0-20250503055512-501391591dee github.com/metacubex/tfo-go v0.0.0-20250503140532-decbcfccbfdf - github.com/metacubex/utls v1.7.0-alpha.3 + github.com/metacubex/utls v1.7.3 github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 github.com/miekg/dns v1.1.63 // lastest version compatible with golang1.20 github.com/mroth/weightedrand/v2 v2.1.0 diff --git a/go.sum b/go.sum index 0840c49a44..890d1712d4 100644 --- a/go.sum +++ b/go.sum @@ -138,8 +138,8 @@ github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113a github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE= github.com/metacubex/tfo-go v0.0.0-20250503140532-decbcfccbfdf h1:LwID1wz4tzypidd412dd4dC1H0m1TgRCQ/XvRvMJDFM= github.com/metacubex/tfo-go v0.0.0-20250503140532-decbcfccbfdf/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= -github.com/metacubex/utls v1.7.0-alpha.3 h1:cp1cEMUnoifiWrGHRzo+nCwPRveN9yPD8QaRFmfcYxA= -github.com/metacubex/utls v1.7.0-alpha.3/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU= +github.com/metacubex/utls v1.7.3 h1:yDcMEWojFh+t8rU9X0HPcZDPAoFze/rIIyssqivzj8A= +github.com/metacubex/utls v1.7.3/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU= github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ= github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= From bb8c47d83df74d9873933067c689495944dea314 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Thu, 15 May 2025 18:07:55 +0800 Subject: [PATCH 05/23] fix: error typo --- log/level.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/log/level.go b/log/level.go index a4c6ecbd41..b1aa8f3ffe 100644 --- a/log/level.go +++ b/log/level.go @@ -31,7 +31,7 @@ func (l *LogLevel) UnmarshalYAML(unmarshal func(any) error) error { unmarshal(&tp) level, exist := LogLevelMapping[strings.ToLower(tp)] if !exist { - return errors.New("invalid mode") + return errors.New("invalid log-level") } *l = level return nil @@ -43,7 +43,7 @@ func (l *LogLevel) UnmarshalJSON(data []byte) error { json.Unmarshal(data, &tp) level, exist := LogLevelMapping[strings.ToLower(tp)] if !exist { - return errors.New("invalid mode") + return errors.New("invalid log-level") } *l = level return nil @@ -53,7 +53,7 @@ func (l *LogLevel) UnmarshalJSON(data []byte) error { func (l *LogLevel) UnmarshalText(data []byte) error { level, exist := LogLevelMapping[strings.ToLower(string(data))] if !exist { - return errors.New("invalid mode") + return errors.New("invalid log-level") } *l = level return nil From c6d7ef8cb8cc66b2fe47e7c6b1603e6ad3baf852 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sat, 17 May 2025 13:53:21 +0800 Subject: [PATCH 06/23] 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 0e3b07de39..1dea557995 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 0000000000..342e25601b --- /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 0b215ca45f..fc2d5c4db1 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 a321caf0e6..0397126aa4 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 b075e7199b..6412d4da9b 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 23da3bebf7..8496efc164 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 0000000000..57a1e60f78 --- /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 add691adf5..46038303a3 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 b16676f026..2dcffabcb5 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 0dfeadd28d..b0807863ce 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 999bd35f0a..26895341c8 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 23d06b9462..daedb53274 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 13d4046d75..0b387d726a 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 90ff5efe20..983698c774 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 588c159aa8..3bfcb46ac5 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 7e8886b6b7..8fe436328d 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) From 8a5f3b89095f7385baab3489f1710dfa1c051ff2 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sat, 17 May 2025 17:06:38 +0800 Subject: [PATCH 07/23] chore: simplify port hop costs --- adapter/outbound/hysteria2.go | 14 +++++--------- go.mod | 2 +- go.sum | 2 ++ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/adapter/outbound/hysteria2.go b/adapter/outbound/hysteria2.go index 131a8b3c03..3e028e0e91 100644 --- a/adapter/outbound/hysteria2.go +++ b/adapter/outbound/hysteria2.go @@ -20,7 +20,6 @@ import ( tuicCommon "github.com/metacubex/mihomo/transport/tuic/common" "github.com/metacubex/quic-go" - "github.com/metacubex/randv2" "github.com/metacubex/sing-quic/hysteria2" M "github.com/metacubex/sing/common/metadata" ) @@ -186,30 +185,27 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { } var ranges utils.IntRanges[uint16] - var serverAddress []string + var serverPorts []uint16 if option.Ports != "" { ranges, err = utils.NewUnsignedRanges[uint16](option.Ports) if err != nil { return nil, err } ranges.Range(func(port uint16) bool { - serverAddress = append(serverAddress, net.JoinHostPort(option.Server, strconv.Itoa(int(port)))) + serverPorts = append(serverPorts, port) return true }) - if len(serverAddress) > 0 { - clientOptions.ServerAddress = func(ctx context.Context) (*net.UDPAddr, error) { - return resolveUDPAddr(ctx, "udp", serverAddress[randv2.IntN(len(serverAddress))], C.NewDNSPrefer(option.IPVersion)) - } - + if len(serverPorts) > 0 { if option.HopInterval == 0 { option.HopInterval = defaultHopInterval } else if option.HopInterval < minHopInterval { option.HopInterval = minHopInterval } clientOptions.HopInterval = time.Duration(option.HopInterval) * time.Second + clientOptions.ServerPorts = serverPorts } } - if option.Port == 0 && len(serverAddress) == 0 { + if option.Port == 0 && len(serverPorts) == 0 { return nil, errors.New("invalid port") } diff --git a/go.mod b/go.mod index b378a23fb6..ba5d225da8 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/metacubex/randv2 v0.2.0 github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7 github.com/metacubex/sing-mux v0.3.2 - github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2 + github.com/metacubex/sing-quic v0.0.0-20250517090120-462e75d27336 github.com/metacubex/sing-shadowsocks v0.2.9 github.com/metacubex/sing-shadowsocks2 v0.2.3 github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 diff --git a/go.sum b/go.sum index 890d1712d4..7fb37c7b29 100644 --- a/go.sum +++ b/go.sum @@ -122,6 +122,8 @@ github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7 github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw= github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2 h1:wfmYgtECbEYo1slMtyo+2kMqscYYDSjU/TVgS3018F4= github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2/go.mod h1:P1kd57U6XXmXv9PbwWdznUGT0k9bKgFJXF0fEORbIlk= +github.com/metacubex/sing-quic v0.0.0-20250517090120-462e75d27336 h1:5BgpaFkTzkePwF1A8rmhCqgyOMG79BLsAhFR8W8SiRo= +github.com/metacubex/sing-quic v0.0.0-20250517090120-462e75d27336/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM= github.com/metacubex/sing-shadowsocks v0.2.9 h1:2e++13WNN7EGjGtvrGLUzW1xrCdQbW2gIFpgw5GEw00= github.com/metacubex/sing-shadowsocks v0.2.9/go.mod h1:CJSEGO4FWQAWe+ZiLZxCweGdjRR60A61SIoVjdjQeBA= github.com/metacubex/sing-shadowsocks2 v0.2.3 h1:v3rNS/5Ywh0NIZ6VU/NmdERQIN5RePzyxCFeQsU4Cx0= From dc958e6a391c2c5422afa0f02644795ced3545a6 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sat, 17 May 2025 17:36:25 +0800 Subject: [PATCH 08/23] feat: add `ech-opts` for hysteria/hysteria2/tuic outbound --- adapter/outbound/hysteria.go | 78 ++++++++++++++++++++++------------- adapter/outbound/hysteria2.go | 55 +++++++++++++++--------- adapter/outbound/tuic.go | 47 ++++++++++++++------- docs/config.yaml | 12 ++++++ 4 files changed, 129 insertions(+), 63 deletions(-) diff --git a/adapter/outbound/hysteria.go b/adapter/outbound/hysteria.go index 55caf58b83..bd92260a13 100644 --- a/adapter/outbound/hysteria.go +++ b/adapter/outbound/hysteria.go @@ -12,6 +12,7 @@ import ( "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" @@ -44,6 +45,9 @@ type Hysteria struct { option *HysteriaOption client *core.Client + + tlsConfig *tlsC.Config + echConfig *ech.Config } func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { @@ -79,7 +83,15 @@ func (h *Hysteria) genHdc(ctx context.Context) utils.PacketDialer { return cDialer.ListenPacket(ctx, network, "", rAddrPort) }, remoteAddr: func(addr string) (net.Addr, error) { - return resolveUDPAddr(ctx, "udp", addr, h.prefer) + udpAddr, err := resolveUDPAddr(ctx, "udp", addr, h.prefer) + if err != nil { + return nil, err + } + err = h.echConfig.ClientHandle(ctx, h.tlsConfig) + if err != nil { + return nil, err + } + return udpAddr, nil }, } } @@ -93,30 +105,31 @@ func (h *Hysteria) ProxyInfo() C.ProxyInfo { type HysteriaOption struct { BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port,omitempty"` - Ports string `proxy:"ports,omitempty"` - Protocol string `proxy:"protocol,omitempty"` - ObfsProtocol string `proxy:"obfs-protocol,omitempty"` // compatible with Stash - Up string `proxy:"up"` - UpSpeed int `proxy:"up-speed,omitempty"` // compatible with Stash - Down string `proxy:"down"` - DownSpeed int `proxy:"down-speed,omitempty"` // compatible with Stash - Auth string `proxy:"auth,omitempty"` - AuthString string `proxy:"auth-str,omitempty"` - Obfs string `proxy:"obfs,omitempty"` - SNI string `proxy:"sni,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - ALPN []string `proxy:"alpn,omitempty"` - CustomCA string `proxy:"ca,omitempty"` - CustomCAString string `proxy:"ca-str,omitempty"` - ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"` - ReceiveWindow int `proxy:"recv-window,omitempty"` - DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` - FastOpen bool `proxy:"fast-open,omitempty"` - HopInterval int `proxy:"hop-interval,omitempty"` + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port,omitempty"` + Ports string `proxy:"ports,omitempty"` + Protocol string `proxy:"protocol,omitempty"` + ObfsProtocol string `proxy:"obfs-protocol,omitempty"` // compatible with Stash + Up string `proxy:"up"` + UpSpeed int `proxy:"up-speed,omitempty"` // compatible with Stash + Down string `proxy:"down"` + DownSpeed int `proxy:"down-speed,omitempty"` // compatible with Stash + Auth string `proxy:"auth,omitempty"` + AuthString string `proxy:"auth-str,omitempty"` + Obfs string `proxy:"obfs,omitempty"` + SNI string `proxy:"sni,omitempty"` + ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` + SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` + Fingerprint string `proxy:"fingerprint,omitempty"` + ALPN []string `proxy:"alpn,omitempty"` + CustomCA string `proxy:"ca,omitempty"` + CustomCAString string `proxy:"ca-str,omitempty"` + ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"` + ReceiveWindow int `proxy:"recv-window,omitempty"` + DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` + FastOpen bool `proxy:"fast-open,omitempty"` + HopInterval int `proxy:"hop-interval,omitempty"` } func (c *HysteriaOption) Speed() (uint64, uint64, error) { @@ -161,6 +174,13 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) { } else { tlsConfig.NextProtos = []string{DefaultALPN} } + + echConfig, err := option.ECHOpts.Parse() + if err != nil { + return nil, err + } + tlsClientConfig := tlsC.UConfig(tlsConfig) + quicConfig := &quic.Config{ InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn), MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn), @@ -215,7 +235,7 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) { down = uint64(option.DownSpeed * mbpsToBps) } client, err := core.NewClient( - addr, ports, option.Protocol, auth, tlsC.UConfig(tlsConfig), quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl { + addr, ports, option.Protocol, auth, tlsClientConfig, quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl { return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS)) }, obfuscator, hopInterval, option.FastOpen, ) @@ -233,8 +253,10 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) { rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), }, - option: &option, - client: client, + option: &option, + client: client, + tlsConfig: tlsClientConfig, + echConfig: echConfig, } return outbound, nil diff --git a/adapter/outbound/hysteria2.go b/adapter/outbound/hysteria2.go index 3e028e0e91..b9e41c4663 100644 --- a/adapter/outbound/hysteria2.go +++ b/adapter/outbound/hysteria2.go @@ -41,24 +41,25 @@ type Hysteria2 struct { type Hysteria2Option struct { BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port,omitempty"` - Ports string `proxy:"ports,omitempty"` - HopInterval int `proxy:"hop-interval,omitempty"` - Up string `proxy:"up,omitempty"` - Down string `proxy:"down,omitempty"` - Password string `proxy:"password,omitempty"` - Obfs string `proxy:"obfs,omitempty"` - ObfsPassword string `proxy:"obfs-password,omitempty"` - SNI string `proxy:"sni,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - ALPN []string `proxy:"alpn,omitempty"` - CustomCA string `proxy:"ca,omitempty"` - CustomCAString string `proxy:"ca-str,omitempty"` - CWND int `proxy:"cwnd,omitempty"` - UdpMTU int `proxy:"udp-mtu,omitempty"` + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port,omitempty"` + Ports string `proxy:"ports,omitempty"` + HopInterval int `proxy:"hop-interval,omitempty"` + Up string `proxy:"up,omitempty"` + Down string `proxy:"down,omitempty"` + Password string `proxy:"password,omitempty"` + Obfs string `proxy:"obfs,omitempty"` + ObfsPassword string `proxy:"obfs-password,omitempty"` + SNI string `proxy:"sni,omitempty"` + ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` + SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` + Fingerprint string `proxy:"fingerprint,omitempty"` + ALPN []string `proxy:"alpn,omitempty"` + CustomCA string `proxy:"ca,omitempty"` + CustomCAString string `proxy:"ca-str,omitempty"` + CWND int `proxy:"cwnd,omitempty"` + UdpMTU int `proxy:"udp-mtu,omitempty"` // quic-go special config InitialStreamReceiveWindow uint64 `proxy:"initial-stream-receive-window,omitempty"` @@ -153,6 +154,12 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { tlsConfig.NextProtos = option.ALPN } + tlsClientConfig := tlsC.UConfig(tlsConfig) + echConfig, err := option.ECHOpts.Parse() + if err != nil { + return nil, err + } + if option.UdpMTU == 0 { // "1200" from quic-go's MaxDatagramSize // "-3" from quic-go's DatagramFrame.MaxDataLen @@ -174,13 +181,21 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { ReceiveBPS: StringToBps(option.Down), SalamanderPassword: salamanderPassword, Password: option.Password, - TLSConfig: tlsC.UConfig(tlsConfig), + TLSConfig: tlsClientConfig, QUICConfig: quicConfig, UDPDisabled: false, CWND: option.CWND, UdpMTU: option.UdpMTU, ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) { - return resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion)) + udpAddr, err := resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion)) + if err != nil { + return nil, err + } + err = echConfig.ClientHandle(ctx, tlsClientConfig) + if err != nil { + return nil, err + } + return udpAddr, nil }, } diff --git a/adapter/outbound/tuic.go b/adapter/outbound/tuic.go index f062f83027..525f9ec623 100644 --- a/adapter/outbound/tuic.go +++ b/adapter/outbound/tuic.go @@ -12,6 +12,7 @@ import ( "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" @@ -28,6 +29,9 @@ type Tuic struct { *Base option *TuicOption client *tuic.PoolClient + + tlsConfig *tlsC.Config + echConfig *ech.Config } type TuicOption struct { @@ -48,18 +52,19 @@ type TuicOption struct { DisableSni bool `proxy:"disable-sni,omitempty"` MaxUdpRelayPacketSize int `proxy:"max-udp-relay-packet-size,omitempty"` - FastOpen bool `proxy:"fast-open,omitempty"` - MaxOpenStreams int `proxy:"max-open-streams,omitempty"` - CWND int `proxy:"cwnd,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - CustomCA string `proxy:"ca,omitempty"` - CustomCAString string `proxy:"ca-str,omitempty"` - ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"` - ReceiveWindow int `proxy:"recv-window,omitempty"` - DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` - MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"` - SNI string `proxy:"sni,omitempty"` + FastOpen bool `proxy:"fast-open,omitempty"` + MaxOpenStreams int `proxy:"max-open-streams,omitempty"` + CWND int `proxy:"cwnd,omitempty"` + SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` + Fingerprint string `proxy:"fingerprint,omitempty"` + CustomCA string `proxy:"ca,omitempty"` + CustomCAString string `proxy:"ca-str,omitempty"` + ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"` + ReceiveWindow int `proxy:"recv-window,omitempty"` + DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` + MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"` + SNI string `proxy:"sni,omitempty"` + ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` UDPOverStream bool `proxy:"udp-over-stream,omitempty"` UDPOverStreamVersion int `proxy:"udp-over-stream-version,omitempty"` @@ -135,6 +140,10 @@ func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (transport * if err != nil { return nil, nil, err } + err = t.echConfig.ClientHandle(ctx, t.tlsConfig) + if err != nil { + return nil, nil, err + } addr = udpAddr var pc net.PacketConn pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort()) @@ -249,6 +258,12 @@ func NewTuic(option TuicOption) (*Tuic, error) { tlsConfig.InsecureSkipVerify = true // tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config } + tlsClientConfig := tlsC.UConfig(tlsConfig) + echConfig, err := option.ECHOpts.Parse() + if err != nil { + return nil, err + } + switch option.UDPOverStreamVersion { case uot.Version, uot.LegacyVersion: case 0: @@ -268,7 +283,9 @@ func NewTuic(option TuicOption) (*Tuic, error) { rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), }, - option: &option, + option: &option, + tlsConfig: tlsClientConfig, + echConfig: echConfig, } clientMaxOpenStreams := int64(option.MaxOpenStreams) @@ -285,7 +302,7 @@ func NewTuic(option TuicOption) (*Tuic, error) { if len(option.Token) > 0 { tkn := tuic.GenTKN(option.Token) clientOption := &tuic.ClientOptionV4{ - TlsConfig: tlsC.UConfig(tlsConfig), + TlsConfig: tlsClientConfig, QuicConfig: quicConfig, Token: tkn, UdpRelayMode: udpRelayMode, @@ -305,7 +322,7 @@ func NewTuic(option TuicOption) (*Tuic, error) { maxUdpRelayPacketSize = tuic.MaxFragSizeV5 } clientOption := &tuic.ClientOptionV5{ - TlsConfig: tlsC.UConfig(tlsConfig), + TlsConfig: tlsClientConfig, QuicConfig: quicConfig, Uuid: uuid.FromStringOrNil(option.UUID), Password: option.Password, diff --git a/docs/config.yaml b/docs/config.yaml index 26895341c8..221dbe46f5 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -756,6 +756,10 @@ proxies: # socks5 up: "30 Mbps" # 若不写单位,默认为 Mbps down: "200 Mbps" # 若不写单位,默认为 Mbps # sni: server.com + # ech-opts: + # enable: true # 必须手动开启 + # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) + # config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA # skip-cert-verify: false # recv-window-conn: 12582912 # recv-window: 52428800 @@ -779,6 +783,10 @@ proxies: # socks5 # obfs: salamander # 默认为空,如果填写则开启 obfs,目前仅支持 salamander # obfs-password: yourpassword # sni: server.com + # ech-opts: + # enable: true # 必须手动开启 + # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) + # config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA # skip-cert-verify: false # fingerprint: xxxx # alpn: @@ -854,6 +862,10 @@ proxies: # socks5 # skip-cert-verify: true # max-open-streams: 20 # default 100, too many open streams may hurt performance # sni: example.com + # ech-opts: + # enable: true # 必须手动开启 + # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) + # config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA # # meta 和 sing-box 私有扩展,将 ss-uot 用于 udp 中继,开启此选项后 udp-relay-mode 将失效 # 警告,与原版 tuic 不兼容!!! From a1350d4985d4c3cc0ca97685d13c7b5f3df3b322 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sat, 17 May 2025 20:50:21 +0800 Subject: [PATCH 09/23] 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 0000000000..afae8098c2 --- /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 9d2c3d976b..96e62d7c23 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 80b37f38a8..3a9312b3fe 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 221dbe46f5..b74bc7c19a 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 db0a5503fd..ae6af171e2 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 adbafa60fa..874b796404 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 a99f87fb2d..cd0430ec83 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 e26fbaa8aa..e8042b0d5d 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 28b6fe7c32..e38a2022c8 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 14a4680927..d923e9a02a 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 d656db9fdc..52ee3754a9 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 264b772c40..2a0e0054d2 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 52483081eb..bacfa844eb 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 6f1e635084..224b053454 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 9d172890d7..7759b4c7d9 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 7c5718dc06..14bf52507a 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 8a4df00813..16693a21d8 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 62d19c829e..bcdca0b784 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 2926a9e6ca..fd2d411735 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 20c61f2eec..db32512be4 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 6cb9782cf0..fa794ae42e 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 44c56b0b43..04d73bf82c 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 320081f8c6..5fd584912e 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 e9c2f21bc1..67349156ce 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 24865d8339..1cf3991de1 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 0cbf214fb5..947e3a53e7 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 f19cad348f..d2056c05e2 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 2212a75db7..c04ed0933c 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 57af5b0b90..58d23dfc55 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 6893bb5a15..d9d99ecab4 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 0090926aba..849ed4452b 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 97a62fe491..90cff65722 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 b5a9378fd6..b65a294bd2 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 ab4086a38b..33cf02f088 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 d3ca98d783..780273f215 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 7bc63a737b..2037177e3a 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 0b387d726a..7c9ab3e1e8 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 28fd2fceaf..00d0bf09dd 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() From 188372cb04e8d1754c07c8b4892095e2a776b0da Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sat, 17 May 2025 21:21:02 +0800 Subject: [PATCH 10/23] feat: add `tls.ech-key` for `external-controller-tls` --- config/config.go | 3 +++ docs/config.yaml | 7 +++++++ hub/hub.go | 1 + hub/route/server.go | 27 ++++++++++++++++++++------- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/config/config.go b/config/config.go index 56b2aa7bce..294f5cf87c 100644 --- a/config/config.go +++ b/config/config.go @@ -174,6 +174,7 @@ type Profile struct { type TLS struct { Certificate string PrivateKey string + EchKey string CustomTrustCert []string } @@ -360,6 +361,7 @@ type RawSniffingConfig struct { type RawTLS struct { Certificate string `yaml:"certificate" json:"certificate"` PrivateKey string `yaml:"private-key" json:"private-key"` + EchKey string `yaml:"ech-key" json:"ech-key"` CustomTrustCert []string `yaml:"custom-certifactes" json:"custom-certifactes"` } @@ -814,6 +816,7 @@ func parseTLS(cfg *RawConfig) (*TLS, error) { return &TLS{ Certificate: cfg.TLS.Certificate, PrivateKey: cfg.TLS.PrivateKey, + EchKey: cfg.TLS.EchKey, CustomTrustCert: cfg.TLS.CustomTrustCert, }, nil } diff --git a/docs/config.yaml b/docs/config.yaml index b74bc7c19a..774a6e7125 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -48,6 +48,13 @@ ipv6: true # 开启 IPv6 总开关,关闭阻断所有 IPv6 链接和屏蔽 DNS tls: certificate: string # 证书 PEM 格式,或者 证书的路径 private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径 + # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) + # ech-key: | + # -----BEGIN ECH KEYS----- + # ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK + # madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz + # dC5jb20AAA== + # -----END ECH KEYS----- custom-certifactes: - | -----BEGIN CERTIFICATE----- diff --git a/hub/hub.go b/hub/hub.go index 69f627ffa8..fc4fe81a5d 100644 --- a/hub/hub.go +++ b/hub/hub.go @@ -57,6 +57,7 @@ func applyRoute(cfg *config.Config) { Secret: cfg.Controller.Secret, Certificate: cfg.TLS.Certificate, PrivateKey: cfg.TLS.PrivateKey, + EchKey: cfg.TLS.EchKey, DohServer: cfg.Controller.ExternalDohServer, IsDebug: cfg.General.LogLevel == log.DEBUG, Cors: route.Cors{ diff --git a/hub/route/server.go b/hub/route/server.go index 2ccd8596d7..c41f51a5e5 100644 --- a/hub/route/server.go +++ b/hub/route/server.go @@ -3,7 +3,6 @@ package route import ( "bytes" "crypto/subtle" - "crypto/tls" "encoding/json" "net" "net/http" @@ -17,6 +16,8 @@ import ( "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/utils" "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" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/tunnel/statistic" @@ -27,6 +28,8 @@ import ( "github.com/gobwas/ws" "github.com/gobwas/ws/wsutil" "github.com/sagernet/cors" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" ) var ( @@ -62,6 +65,7 @@ type Config struct { Secret string Certificate string PrivateKey string + EchKey string DohServer string IsDebug bool Cors Cors @@ -186,7 +190,7 @@ func startTLS(cfg *Config) { // handle tlsAddr if len(cfg.TLSAddr) > 0 { - c, err := ca.LoadTLSKeyPair(cfg.Certificate, cfg.PrivateKey, C.Path) + cert, err := ca.LoadTLSKeyPair(cfg.Certificate, cfg.PrivateKey, C.Path) if err != nil { log.Errorln("External controller tls listen error: %s", err) return @@ -199,14 +203,23 @@ func startTLS(cfg *Config) { } log.Infoln("RESTful API tls listening at: %s", l.Addr().String()) + tlsConfig := &tlsC.Config{} + tlsConfig.NextProtos = []string{"h2", "http/1.1"} + tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)} + + if cfg.EchKey != "" { + err = ech.LoadECHKey(cfg.EchKey, tlsConfig, C.Path) + if err != nil { + log.Errorln("External controller tls serve error: %s", err) + return + } + } server := &http.Server{ - Handler: router(cfg.IsDebug, cfg.Secret, cfg.DohServer, cfg.Cors), - TLSConfig: &tls.Config{ - Certificates: []tls.Certificate{c}, - }, + // using h2c.NewHandler to ensure we can work in plain http2 and some tls conn is not *tls.Conn + Handler: h2c.NewHandler(router(cfg.IsDebug, cfg.Secret, cfg.DohServer, cfg.Cors), &http2.Server{}), } tlsServer = server - if err = server.ServeTLS(l, "", ""); err != nil { + if err = server.Serve(tlsC.NewListener(l, tlsConfig)); err != nil { log.Errorln("External controller tls serve error: %s", err) } } From 41b57afb3fcae76a35c8c288556b9573ffd30697 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sun, 18 May 2025 00:25:02 +0800 Subject: [PATCH 11/23] fix: grpc deadline implement --- transport/gun/gun.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/transport/gun/gun.go b/transport/gun/gun.go index 7c9ab3e1e8..ce9d027951 100644 --- a/transport/gun/gun.go +++ b/transport/gun/gun.go @@ -214,6 +214,13 @@ func (g *Conn) SetReadDeadline(t time.Time) error { return g.SetDeadline(t) } func (g *Conn) SetWriteDeadline(t time.Time) error { return g.SetDeadline(t) } func (g *Conn) SetDeadline(t time.Time) error { + if t.IsZero() { + if g.deadline != nil { + g.deadline.Stop() + g.deadline = nil + } + return nil + } d := time.Until(t) if g.deadline != nil { g.deadline.Reset(d) From 1672750c476e5630069f92507d59414cd6834ade Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sun, 18 May 2025 00:49:15 +0800 Subject: [PATCH 12/23] chore: simplifying the old fingerprint processing method --- component/tls/reality.go | 28 ++++------------------------ component/tls/utls.go | 28 +++++++++++++++++++--------- 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/component/tls/reality.go b/component/tls/reality.go index 2dcffabcb5..d99780fce1 100644 --- a/component/tls/reality.go +++ b/component/tls/reality.go @@ -26,7 +26,6 @@ import ( utls "github.com/metacubex/utls" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/hkdf" - "golang.org/x/exp/slices" "golang.org/x/net/http2" ) @@ -51,6 +50,10 @@ func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHello VerifyPeerCertificate: verifier.VerifyPeerCertificate, } + if !realityConfig.SupportX25519MLKEM768 && fingerprint == HelloChrome_Auto { + fingerprint = HelloChrome_120 // old reality server doesn't work with X25519MLKEM768 + } + uConn := utls.UClient(conn, uConfig, fingerprint) verifier.UConn = uConn err := uConn.BuildHandshakeState() @@ -58,29 +61,6 @@ func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHello return nil, err } - if !realityConfig.SupportX25519MLKEM768 { - // ------for X25519MLKEM768 does not work properly with the old reality server------- - // Iterate over extensions and check - for _, extension := range uConn.Extensions { - if ce, ok := extension.(*utls.SupportedCurvesExtension); ok { - ce.Curves = slices.DeleteFunc(ce.Curves, func(curveID utls.CurveID) bool { - return curveID == utls.X25519MLKEM768 - }) - } - if ks, ok := extension.(*utls.KeyShareExtension); ok { - ks.KeyShares = slices.DeleteFunc(ks.KeyShares, func(share utls.KeyShare) bool { - return share.Group == utls.X25519MLKEM768 - }) - } - } - // Rebuild the client hello - err = uConn.BuildHandshakeState() - if err != nil { - return nil, err - } - // -------------------------------------------------------------------- - } - hello := uConn.HandshakeState.Hello rawSessionID := hello.Raw[39 : 39+32] // the location of session ID for i := range rawSessionID { // https://github.com/golang/go/issues/5373 diff --git a/component/tls/utls.go b/component/tls/utls.go index 3a9312b3fe..fd5f0e5449 100644 --- a/component/tls/utls.go +++ b/component/tls/utls.go @@ -16,6 +16,7 @@ type Conn = utls.Conn type UConn = utls.UConn type UClientHelloID = utls.ClientHelloID +const VersionTLS12 = utls.VersionTLS12 const VersionTLS13 = utls.VersionTLS13 func Client(c net.Conn, config *utls.Config) *Conn { @@ -26,6 +27,10 @@ func UClient(c net.Conn, config *utls.Config, fingerprint UClientHelloID) *UConn return utls.UClient(c, config, fingerprint) } +func Server(c net.Conn, config *utls.Config) *Conn { + return utls.Server(c, config) +} + func NewListener(inner net.Listener, config *Config) net.Listener { return utls.NewListener(inner, config) } @@ -69,21 +74,26 @@ var randomFingerprint = once.OnceValue(func() UClientHelloID { return fingerprint }) +var HelloChrome_Auto = utls.HelloChrome_Auto +var HelloChrome_120 = utls.HelloChrome_120 // special fingerprint for some old protocols doesn't work with HelloChrome_Auto + var fingerprints = map[string]UClientHelloID{ - "chrome": utls.HelloChrome_Auto, + "chrome": utls.HelloChrome_Auto, + "firefox": utls.HelloFirefox_Auto, + "safari": utls.HelloSafari_Auto, + "ios": utls.HelloIOS_Auto, + "android": utls.HelloAndroid_11_OkHttp, + "edge": utls.HelloEdge_Auto, + "360": utls.Hello360_Auto, + "qq": utls.HelloQQ_Auto, + "random": {}, + + // deprecated fingerprints should not be used "chrome_psk": utls.HelloChrome_100_PSK, "chrome_psk_shuffle": utls.HelloChrome_106_Shuffle, "chrome_padding_psk_shuffle": utls.HelloChrome_114_Padding_PSK_Shuf, "chrome_pq": utls.HelloChrome_115_PQ, "chrome_pq_psk": utls.HelloChrome_115_PQ_PSK, - "firefox": utls.HelloFirefox_Auto, - "safari": utls.HelloSafari_Auto, - "ios": utls.HelloIOS_Auto, - "android": utls.HelloAndroid_11_OkHttp, - "edge": utls.HelloEdge_Auto, - "360": utls.Hello360_Auto, - "qq": utls.HelloQQ_Auto, - "random": {}, "randomized": utls.HelloRandomized, } From d900c7121452015eecf311ac57781ceca36bd2a2 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sun, 18 May 2025 00:50:00 +0800 Subject: [PATCH 13/23] fix: shadowtls v2 not work with X25519MLKEM768 --- transport/sing-shadowtls/shadowtls.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/transport/sing-shadowtls/shadowtls.go b/transport/sing-shadowtls/shadowtls.go index 904bcd6311..4f9c3b51b4 100644 --- a/transport/sing-shadowtls/shadowtls.go +++ b/transport/sing-shadowtls/shadowtls.go @@ -49,7 +49,7 @@ func NewShadowTLS(ctx context.Context, conn net.Conn, option *ShadowTLSOption) ( return nil, err } - tlsHandshake := uTLSHandshakeFunc(tlsConfig, option.ClientFingerprint) + tlsHandshake := uTLSHandshakeFunc(tlsConfig, option.ClientFingerprint, option.Version) client, err := shadowtls.NewClient(shadowtls.ClientConfig{ Version: option.Version, Password: option.Password, @@ -62,15 +62,19 @@ func NewShadowTLS(ctx context.Context, conn net.Conn, option *ShadowTLSOption) ( return client.DialContextConn(ctx, conn) } -func uTLSHandshakeFunc(config *tls.Config, clientFingerprint string) shadowtls.TLSHandshakeFunc { +func uTLSHandshakeFunc(config *tls.Config, clientFingerprint string, version int) shadowtls.TLSHandshakeFunc { return func(ctx context.Context, conn net.Conn, sessionIDGenerator shadowtls.TLSSessionIDGeneratorFunc) error { tlsConfig := tlsC.UConfig(config) tlsConfig.SessionIDGenerator = sessionIDGenerator - if config.MaxVersion == tls.VersionTLS12 { // for ShadowTLS v1 + if version == 1 { + tlsConfig.MaxVersion = tlsC.VersionTLS12 // ShadowTLS v1 only support TLS 1.2 tlsConn := tlsC.Client(conn, tlsConfig) return tlsConn.HandshakeContext(ctx) } if clientFingerprint, ok := tlsC.GetFingerprint(clientFingerprint); ok { + if version == 2 && clientFingerprint == tlsC.HelloChrome_Auto { + clientFingerprint = tlsC.HelloChrome_120 // ShadowTLS v2 not work with X25519MLKEM768 + } tlsConn := tlsC.UClient(conn, tlsConfig, clientFingerprint) if slices.Equal(tlsConfig.NextProtos, WsALPN) { err := tlsC.BuildWebsocketHandshakeState(tlsConn) From d036d981286a04a64df86ba9ddeacba12d533047 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sun, 18 May 2025 22:32:25 +0800 Subject: [PATCH 14/23] fix: http server does not handle http2 logic correctly --- component/tls/httpserver.go | 68 +++++++++++++++++++++++++++++++++ hub/route/server.go | 7 +--- listener/inbound/common_test.go | 27 ++++++++----- listener/inbound/vless_test.go | 18 ++------- listener/sing_vless/server.go | 18 +++++---- listener/sing_vmess/server.go | 18 +++++---- listener/trojan/server.go | 18 +++++---- 7 files changed, 125 insertions(+), 49 deletions(-) create mode 100644 component/tls/httpserver.go diff --git a/component/tls/httpserver.go b/component/tls/httpserver.go new file mode 100644 index 0000000000..a8c9ed7f0e --- /dev/null +++ b/component/tls/httpserver.go @@ -0,0 +1,68 @@ +package tls + +import ( + "context" + "net" + "net/http" + "time" + + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/log" + + "golang.org/x/net/http2" +) + +func extractTlsHandshakeTimeoutFromServer(s *http.Server) time.Duration { + var ret time.Duration + for _, v := range [...]time.Duration{ + s.ReadHeaderTimeout, + s.ReadTimeout, + s.WriteTimeout, + } { + if v <= 0 { + continue + } + if ret == 0 || v < ret { + ret = v + } + } + return ret +} + +// NewListenerForHttps returns a net.Listener for (*http.Server).Serve() +// the "func (c *conn) serve(ctx context.Context)" in http\server.go +// only do tls handshake and check NegotiatedProtocol with std's *tls.Conn +// so we do the same logic to let http2 (not h2c) work fine +func NewListenerForHttps(l net.Listener, httpServer *http.Server, tlsConfig *Config) net.Listener { + http2Server := &http2.Server{} + _ = http2.ConfigureServer(httpServer, http2Server) + return N.NewHandleContextListener(context.Background(), l, func(ctx context.Context, conn net.Conn) (net.Conn, error) { + c := Server(conn, tlsConfig) + + tlsTO := extractTlsHandshakeTimeoutFromServer(httpServer) + if tlsTO > 0 { + dl := time.Now().Add(tlsTO) + _ = conn.SetReadDeadline(dl) + _ = conn.SetWriteDeadline(dl) + } + + err := c.HandshakeContext(ctx) + if err != nil { + return nil, err + } + + // Restore Conn-level deadlines. + if tlsTO > 0 { + _ = conn.SetReadDeadline(time.Time{}) + _ = conn.SetWriteDeadline(time.Time{}) + } + + if c.ConnectionState().NegotiatedProtocol == http2.NextProtoTLS { + http2Server.ServeConn(c, &http2.ServeConnOpts{BaseConfig: httpServer}) + return nil, net.ErrClosed + } + return c, nil + }, func(a any) { + log.Errorln("https server panic: %s", a) + }) +} diff --git a/hub/route/server.go b/hub/route/server.go index c41f51a5e5..7f3977ed66 100644 --- a/hub/route/server.go +++ b/hub/route/server.go @@ -28,8 +28,6 @@ import ( "github.com/gobwas/ws" "github.com/gobwas/ws/wsutil" "github.com/sagernet/cors" - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" ) var ( @@ -215,11 +213,10 @@ func startTLS(cfg *Config) { } } server := &http.Server{ - // using h2c.NewHandler to ensure we can work in plain http2 and some tls conn is not *tls.Conn - Handler: h2c.NewHandler(router(cfg.IsDebug, cfg.Secret, cfg.DohServer, cfg.Cors), &http2.Server{}), + Handler: router(cfg.IsDebug, cfg.Secret, cfg.DohServer, cfg.Cors), } tlsServer = server - if err = server.Serve(tlsC.NewListener(l, tlsConfig)); err != nil { + if err = server.Serve(tlsC.NewListenerForHttps(l, server, tlsConfig)); err != nil { log.Errorln("External controller tls serve error: %s", err) } } diff --git a/listener/inbound/common_test.go b/listener/inbound/common_test.go index 14bf52507a..5b8a6f17e6 100644 --- a/listener/inbound/common_test.go +++ b/listener/inbound/common_test.go @@ -20,11 +20,13 @@ import ( "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/ech" "github.com/metacubex/mihomo/component/generater" + tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/stretchr/testify/assert" + "golang.org/x/net/http2" ) var httpPath = "/inbound_test" @@ -134,7 +136,10 @@ func NewHttpTestTunnel() *TestTunnel { r.Get(httpPath, func(w http.ResponseWriter, r *http.Request) { render.Data(w, r, httpData) }) - go http.Serve(ln, r) + h2Server := &http2.Server{} + server := http.Server{Handler: r} + _ = http2.ConfigureServer(&server, h2Server) + go server.Serve(ln) testFn := func(t *testing.T, proxy C.ProxyAdapter, proto string) { req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s://%s%s", proto, remoteAddr, httpPath), nil) if !assert.NoError(t, err) { @@ -208,23 +213,27 @@ func NewHttpTestTunnel() *TestTunnel { ch: make(chan struct{}), } if metadata.DstPort == 443 { - tlsConn := tls.Server(c, tlsConfig.Clone()) + tlsConn := tlsC.Server(c, tlsC.UConfig(tlsConfig)) if metadata.Host == realityDest { // ignore the tls handshake error for realityDest if realityRealDial { rconn, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress()) if err != nil { panic(err) } - N.Relay(rconn, tlsConn) - return - } - ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout) - defer cancel() - if err := tlsConn.HandshakeContext(ctx); err != nil { + N.Relay(rconn, conn) return } } - ln.ch <- tlsConn + ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout) + defer cancel() + if err := tlsConn.HandshakeContext(ctx); err != nil { + return + } + if tlsConn.ConnectionState().NegotiatedProtocol == http2.NextProtoTLS { + h2Server.ServeConn(tlsConn, &http2.ServeConnOpts{BaseConfig: &server}) + } else { + ln.ch <- tlsConn + } } else { ln.ch <- c } diff --git a/listener/inbound/vless_test.go b/listener/inbound/vless_test.go index d2056c05e2..1bec3d5574 100644 --- a/listener/inbound/vless_test.go +++ b/listener/inbound/vless_test.go @@ -237,14 +237,9 @@ func TestInboundVless_Reality(t *testing.T) { outboundOptions.Flow = "xtls-rprx-vision" testInboundVless(t, inboundOptions, outboundOptions) }) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions + t.Run("X25519MLKEM768", func(t *testing.T) { outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } + outboundOptions.RealityOpts.SupportX25519MLKEM768 = true testInboundVless(t, inboundOptions, outboundOptions) t.Run("xtls-rprx-vision", func(t *testing.T) { outboundOptions := outboundOptions @@ -276,14 +271,9 @@ 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 + t.Run("X25519MLKEM768", func(t *testing.T) { outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } + outboundOptions.RealityOpts.SupportX25519MLKEM768 = true testInboundVless(t, inboundOptions, outboundOptions) }) } diff --git a/listener/sing_vless/server.go b/listener/sing_vless/server.go index 90cff65722..16aa1c654a 100644 --- a/listener/sing_vless/server.go +++ b/listener/sing_vless/server.go @@ -84,7 +84,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) tlsConfig := &tlsC.Config{} var realityBuilder *reality.Builder - var httpHandler http.Handler + var httpServer http.Server if config.Certificate != "" && config.PrivateKey != "" { cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) @@ -119,16 +119,16 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) } sl.HandleConn(conn, tunnel, additions...) }) - httpHandler = httpMux + httpServer.Handler = httpMux tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") } if config.GrpcServiceName != "" { - httpHandler = gun.NewServerHandler(gun.ServerOption{ + httpServer.Handler = gun.NewServerHandler(gun.ServerOption{ ServiceName: config.GrpcServiceName, ConnHandler: func(conn net.Conn) { sl.HandleConn(conn, tunnel, additions...) }, - HttpHandler: httpHandler, + HttpHandler: httpServer.Handler, }) tlsConfig.NextProtos = append([]string{"h2"}, tlsConfig.NextProtos...) // h2 must before http/1.1 } @@ -144,15 +144,19 @@ 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 = tlsC.NewListener(l, tlsConfig) + if httpServer.Handler != nil { + l = tlsC.NewListenerForHttps(l, &httpServer, tlsConfig) + } else { + l = tlsC.NewListener(l, tlsConfig) + } } else { return nil, errors.New("disallow using Vless without both certificates/reality config") } sl.listeners = append(sl.listeners, l) go func() { - if httpHandler != nil { - _ = http.Serve(l, httpHandler) + if httpServer.Handler != nil { + _ = httpServer.Serve(l) return } for { diff --git a/listener/sing_vmess/server.go b/listener/sing_vmess/server.go index b65a294bd2..0b4d013a8b 100644 --- a/listener/sing_vmess/server.go +++ b/listener/sing_vmess/server.go @@ -78,7 +78,7 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) tlsConfig := &tlsC.Config{} var realityBuilder *reality.Builder - var httpHandler http.Handler + var httpServer http.Server if config.Certificate != "" && config.PrivateKey != "" { cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) @@ -113,16 +113,16 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) } sl.HandleConn(conn, tunnel, additions...) }) - httpHandler = httpMux + httpServer.Handler = httpMux tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") } if config.GrpcServiceName != "" { - httpHandler = gun.NewServerHandler(gun.ServerOption{ + httpServer.Handler = gun.NewServerHandler(gun.ServerOption{ ServiceName: config.GrpcServiceName, ConnHandler: func(conn net.Conn) { sl.HandleConn(conn, tunnel, additions...) }, - HttpHandler: httpHandler, + HttpHandler: httpServer.Handler, }) tlsConfig.NextProtos = append([]string{"h2"}, tlsConfig.NextProtos...) // h2 must before http/1.1 } @@ -138,13 +138,17 @@ 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 = tlsC.NewListener(l, tlsConfig) + if httpServer.Handler != nil { + l = tlsC.NewListenerForHttps(l, &httpServer, tlsConfig) + } else { + l = tlsC.NewListener(l, tlsConfig) + } } sl.listeners = append(sl.listeners, l) go func() { - if httpHandler != nil { - _ = http.Serve(l, httpHandler) + if httpServer.Handler != nil { + _ = httpServer.Serve(l) return } for { diff --git a/listener/trojan/server.go b/listener/trojan/server.go index 780273f215..3ea7c3879f 100644 --- a/listener/trojan/server.go +++ b/listener/trojan/server.go @@ -72,7 +72,7 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) tlsConfig := &tlsC.Config{} var realityBuilder *reality.Builder - var httpHandler http.Handler + var httpServer http.Server if config.Certificate != "" && config.PrivateKey != "" { cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) @@ -107,16 +107,16 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) } sl.HandleConn(conn, tunnel, additions...) }) - httpHandler = httpMux + httpServer.Handler = httpMux tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") } if config.GrpcServiceName != "" { - httpHandler = gun.NewServerHandler(gun.ServerOption{ + httpServer.Handler = gun.NewServerHandler(gun.ServerOption{ ServiceName: config.GrpcServiceName, ConnHandler: func(conn net.Conn) { sl.HandleConn(conn, tunnel, additions...) }, - HttpHandler: httpHandler, + HttpHandler: httpServer.Handler, }) tlsConfig.NextProtos = append([]string{"h2"}, tlsConfig.NextProtos...) // h2 must before http/1.1 } @@ -132,15 +132,19 @@ 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 = tlsC.NewListener(l, tlsConfig) + if httpServer.Handler != nil { + l = tlsC.NewListenerForHttps(l, &httpServer, tlsConfig) + } else { + l = tlsC.NewListener(l, tlsConfig) + } } else if !config.TrojanSSOption.Enabled { return nil, errors.New("disallow using Trojan without both certificates/reality/ss config") } sl.listeners = append(sl.listeners, l) go func() { - if httpHandler != nil { - _ = http.Serve(l, httpHandler) + if httpServer.Handler != nil { + _ = httpServer.Serve(l) return } for { From 608ddb1b442c1e83579454e0ffd784e141e77cea Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Mon, 19 May 2025 23:11:52 +0800 Subject: [PATCH 15/23] fix: `external-ui-name` must in local --- config/config.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/config.go b/config/config.go index 294f5cf87c..d79ab65c5b 100644 --- a/config/config.go +++ b/config/config.go @@ -7,6 +7,7 @@ import ( "net" "net/netip" "net/url" + "path/filepath" "strings" "time" _ "unsafe" @@ -759,6 +760,9 @@ func parseController(cfg *RawConfig) (*Controller, error) { if path := cfg.ExternalUI; path != "" && !C.Path.IsSafePath(path) { return nil, C.Path.ErrNotSafePath(path) } + if uiName := cfg.ExternalUIName; uiName != "" && !filepath.IsLocal(uiName) { + return nil, fmt.Errorf("external UI name is not local: %s", uiName) + } return &Controller{ ExternalController: cfg.ExternalController, ExternalUI: cfg.ExternalUI, From ed42c4feb88ed5ffaabc0b61457779161ed84d9f Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Mon, 19 May 2025 23:42:39 +0800 Subject: [PATCH 16/23] chore: disallow symlink in unzip --- component/updater/update_ui.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/component/updater/update_ui.go b/component/updater/update_ui.go index 94bc27de58..5fa912e15a 100644 --- a/component/updater/update_ui.go +++ b/component/updater/update_ui.go @@ -224,10 +224,14 @@ func unzip(src, dest string) (string, error) { if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { return "", fmt.Errorf("invalid file path: %s", fpath) } - if f.FileInfo().IsDir() { + info := f.FileInfo() + if info.IsDir() { os.MkdirAll(fpath, os.ModePerm) continue } + if info.Mode()&os.ModeSymlink != 0 { + continue // disallow symlink + } if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { return "", err } From a93479124c110e63465a8dc14f2c5a5fabd32bc9 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 20 May 2025 00:00:07 +0800 Subject: [PATCH 17/23] chore: stricter path checking when unpacking zip/tgz --- component/updater/update_ui.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/component/updater/update_ui.go b/component/updater/update_ui.go index 5fa912e15a..4d1e98eddb 100644 --- a/component/updater/update_ui.go +++ b/component/updater/update_ui.go @@ -221,7 +221,7 @@ func unzip(src, dest string) (string, error) { fpath = filepath.Join(extractedFolder, f.Name) } - if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { + if !inDest(fpath, dest) { return "", fmt.Errorf("invalid file path: %s", fpath) } info := f.FileInfo() @@ -344,7 +344,7 @@ func untgz(src, dest string) (string, error) { fpath = filepath.Join(extractedFolder, cleanTarPath(header.Name)) } - if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { + if !inDest(fpath, dest) { return "", fmt.Errorf("invalid file path: %s", fpath) } @@ -421,3 +421,12 @@ func cleanup(root string) error { return nil }) } + +func inDest(fpath, dest string) bool { + if rel, err := filepath.Rel(dest, fpath); err == nil { + if filepath.IsLocal(rel) { + return true + } + } + return false +} From 9f7a2a36c1f8990bd2ae8d5bd4b354f6e5888cc8 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 20 May 2025 01:57:57 +0800 Subject: [PATCH 18/23] chore: unpack externalUI in a separate temporary directory to avoid malicious compressed packages from polluting workdir --- component/updater/update_ui.go | 144 ++++++++++++++------------------- 1 file changed, 62 insertions(+), 82 deletions(-) diff --git a/component/updater/update_ui.go b/component/updater/update_ui.go index 4d1e98eddb..3cb603819b 100644 --- a/component/updater/update_ui.go +++ b/component/updater/update_ui.go @@ -3,6 +3,7 @@ package updater import ( "archive/tar" "archive/zip" + "bytes" "compress/gzip" "fmt" "io" @@ -32,6 +33,17 @@ const ( typeTarGzip ) +func (t compressionType) String() string { + switch t { + case typeZip: + return "zip" + case typeTarGzip: + return "tar.gz" + default: + return "unknown" + } +} + var DefaultUiUpdater = &UIUpdater{} func NewUiUpdater(externalUI, externalUIURL, externalUIName string) *UIUpdater { @@ -99,48 +111,35 @@ func detectFileType(data []byte) compressionType { } func (u *UIUpdater) downloadUI() error { - err := u.prepareUIPath() - if err != nil { - return fmt.Errorf("prepare UI path failed: %w", err) - } - data, err := downloadForBytes(u.externalUIURL) if err != nil { return fmt.Errorf("can't download file: %w", err) } - fileType := detectFileType(data) - if fileType == typeUnknown { - return fmt.Errorf("unknown or unsupported file type") - } - - ext := ".zip" - if fileType == typeTarGzip { - ext = ".tgz" - } - - saved := path.Join(C.Path.HomeDir(), "download"+ext) - log.Debugln("compression Type: %s", ext) - if err = saveFile(data, saved); err != nil { - return fmt.Errorf("can't save compressed file: %w", err) + tmpDir := C.Path.Resolve("downloadUI.tmp") + defer os.RemoveAll(tmpDir) + extractedFolder, err := extract(data, tmpDir) + if err != nil { + return fmt.Errorf("can't extract compressed file: %w", err) } - defer os.Remove(saved) - err = cleanup(u.externalUIPath) + log.Debugln("cleanupFolder: %s", u.externalUIPath) + err = cleanup(u.externalUIPath) // cleanup files in dir don't remove dir itself if err != nil { if !os.IsNotExist(err) { return fmt.Errorf("cleanup exist file error: %w", err) } } - extractedFolder, err := extract(saved, C.Path.HomeDir()) + err = u.prepareUIPath() if err != nil { - return fmt.Errorf("can't extract compressed file: %w", err) + return fmt.Errorf("prepare UI path failed: %w", err) } - err = os.Rename(extractedFolder, u.externalUIPath) + log.Debugln("moveFolder from %s to %s", extractedFolder, u.externalUIPath) + err = moveDir(extractedFolder, u.externalUIPath) // move files from tmp to target if err != nil { - return fmt.Errorf("rename UI folder failed: %w", err) + return fmt.Errorf("move UI folder failed: %w", err) } return nil } @@ -155,12 +154,11 @@ func (u *UIUpdater) prepareUIPath() error { return nil } -func unzip(src, dest string) (string, error) { - r, err := zip.OpenReader(src) +func unzip(data []byte, dest string) (string, error) { + r, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { return "", err } - defer r.Close() // check whether or not only exists singleRoot dir rootDir := "" @@ -199,17 +197,7 @@ func unzip(src, dest string) (string, error) { log.Debugln("extractedFolder: %s", extractedFolder) } else { log.Debugln("Match the multiRoot") - // or put the files/dirs into new dir - baseName := filepath.Base(src) - baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName)) - extractedFolder = filepath.Join(dest, baseName) - - for i := 1; ; i++ { - if _, err := os.Stat(extractedFolder); os.IsNotExist(err) { - break - } - extractedFolder = filepath.Join(dest, fmt.Sprintf("%s_%d", baseName, i)) - } + extractedFolder = dest log.Debugln("extractedFolder: %s", extractedFolder) } @@ -253,14 +241,8 @@ func unzip(src, dest string) (string, error) { return extractedFolder, nil } -func untgz(src, dest string) (string, error) { - file, err := os.Open(src) - if err != nil { - return "", err - } - defer file.Close() - - gzr, err := gzip.NewReader(file) +func untgz(data []byte, dest string) (string, error) { + gzr, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { return "", err } @@ -303,8 +285,7 @@ func untgz(src, dest string) (string, error) { isSingleRoot = false } - file.Seek(0, 0) - gzr, _ = gzip.NewReader(file) + _ = gzr.Reset(bytes.NewReader(data)) tr = tar.NewReader(gzr) var extractedFolder string @@ -314,17 +295,7 @@ func untgz(src, dest string) (string, error) { log.Debugln("extractedFolder: %s", extractedFolder) } else { log.Debugln("Match the multiRoot") - baseName := filepath.Base(src) - baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName)) - baseName = strings.TrimSuffix(baseName, ".tar") - extractedFolder = filepath.Join(dest, baseName) - - for i := 1; ; i++ { - if _, err := os.Stat(extractedFolder); os.IsNotExist(err) { - break - } - extractedFolder = filepath.Join(dest, fmt.Sprintf("%s_%d", baseName, i)) - } + extractedFolder = dest log.Debugln("extractedFolder: %s", extractedFolder) } @@ -371,16 +342,16 @@ func untgz(src, dest string) (string, error) { return extractedFolder, nil } -func extract(src, dest string) (string, error) { - srcLower := strings.ToLower(src) - switch { - case strings.HasSuffix(srcLower, ".tar.gz") || - strings.HasSuffix(srcLower, ".tgz"): - return untgz(src, dest) - case strings.HasSuffix(srcLower, ".zip"): - return unzip(src, dest) +func extract(data []byte, dest string) (string, error) { + fileType := detectFileType(data) + log.Debugln("compression Type: %s", fileType) + switch fileType { + case typeZip: + return unzip(data, dest) + case typeTarGzip: + return untgz(data, dest) default: - return "", fmt.Errorf("unsupported file format: %s", src) + return "", fmt.Errorf("unknown or unsupported file type") } } @@ -402,24 +373,33 @@ func cleanTarPath(path string) string { } func cleanup(root string) error { - if _, err := os.Stat(root); os.IsNotExist(err) { - return nil + dirEntryList, err := os.ReadDir(root) + if err != nil { + return err } - return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + + for _, dirEntry := range dirEntryList { + err = os.RemoveAll(filepath.Join(root, dirEntry.Name())) if err != nil { return err } - if info.IsDir() { - if err := os.RemoveAll(path); err != nil { - return err - } - } else { - if err := os.Remove(path); err != nil { - return err - } + } + return nil +} + +func moveDir(src string, dst string) error { + dirEntryList, err := os.ReadDir(src) + if err != nil { + return err + } + + for _, dirEntry := range dirEntryList { + err = os.Rename(filepath.Join(src, dirEntry.Name()), filepath.Join(dst, dirEntry.Name())) + if err != nil { + return err } - return nil - }) + } + return nil } func inDest(fpath, dest string) bool { From 8f92b1de13151932a9218b37cca57e7bca256c46 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 20 May 2025 09:48:05 +0800 Subject: [PATCH 19/23] chore: simplify the single root decompression process --- component/updater/update_ui.go | 155 ++++++++------------------------- 1 file changed, 36 insertions(+), 119 deletions(-) diff --git a/component/updater/update_ui.go b/component/updater/update_ui.go index 3cb603819b..e1368aea1a 100644 --- a/component/updater/update_ui.go +++ b/component/updater/update_ui.go @@ -118,7 +118,10 @@ func (u *UIUpdater) downloadUI() error { tmpDir := C.Path.Resolve("downloadUI.tmp") defer os.RemoveAll(tmpDir) - extractedFolder, err := extract(data, tmpDir) + + os.RemoveAll(tmpDir) // cleanup tmp dir before extract + log.Debugln("extractedFolder: %s", tmpDir) + err = extract(data, tmpDir) if err != nil { return fmt.Errorf("can't extract compressed file: %w", err) } @@ -136,8 +139,8 @@ func (u *UIUpdater) downloadUI() error { return fmt.Errorf("prepare UI path failed: %w", err) } - log.Debugln("moveFolder from %s to %s", extractedFolder, u.externalUIPath) - err = moveDir(extractedFolder, u.externalUIPath) // move files from tmp to target + log.Debugln("moveFolder from %s to %s", tmpDir, u.externalUIPath) + err = moveDir(tmpDir, u.externalUIPath) // move files from tmp to target if err != nil { return fmt.Errorf("move UI folder failed: %w", err) } @@ -154,63 +157,19 @@ func (u *UIUpdater) prepareUIPath() error { return nil } -func unzip(data []byte, dest string) (string, error) { +func unzip(data []byte, dest string) error { r, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { - return "", err + return err } // check whether or not only exists singleRoot dir - rootDir := "" - isSingleRoot := true - rootItemCount := 0 - for _, f := range r.File { - parts := strings.Split(strings.Trim(f.Name, "/"), "/") - if len(parts) == 0 { - continue - } - - if len(parts) == 1 { - isDir := strings.HasSuffix(f.Name, "/") - if !isDir { - isSingleRoot = false - break - } - - if rootDir == "" { - rootDir = parts[0] - } - rootItemCount++ - } - } - - if rootItemCount != 1 { - isSingleRoot = false - } - - // build the dir of extraction - var extractedFolder string - if isSingleRoot && rootDir != "" { - // if the singleRoot, use it directly - log.Debugln("Match the singleRoot") - extractedFolder = filepath.Join(dest, rootDir) - log.Debugln("extractedFolder: %s", extractedFolder) - } else { - log.Debugln("Match the multiRoot") - extractedFolder = dest - log.Debugln("extractedFolder: %s", extractedFolder) - } for _, f := range r.File { - var fpath string - if isSingleRoot && rootDir != "" { - fpath = filepath.Join(dest, f.Name) - } else { - fpath = filepath.Join(extractedFolder, f.Name) - } + fpath := filepath.Join(dest, f.Name) if !inDest(fpath, dest) { - return "", fmt.Errorf("invalid file path: %s", fpath) + return fmt.Errorf("invalid file path: %s", fpath) } info := f.FileInfo() if info.IsDir() { @@ -221,128 +180,77 @@ func unzip(data []byte, dest string) (string, error) { continue // disallow symlink } if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { - return "", err + return err } outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) if err != nil { - return "", err + return err } rc, err := f.Open() if err != nil { - return "", err + return err } _, err = io.Copy(outFile, rc) outFile.Close() rc.Close() if err != nil { - return "", err + return err } } - return extractedFolder, nil + return nil } -func untgz(data []byte, dest string) (string, error) { +func untgz(data []byte, dest string) error { gzr, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { - return "", err + return err } defer gzr.Close() tr := tar.NewReader(gzr) - rootDir := "" - isSingleRoot := true - rootItemCount := 0 - for { - header, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - return "", err - } - - parts := strings.Split(cleanTarPath(header.Name), string(os.PathSeparator)) - if len(parts) == 0 { - continue - } - - if len(parts) == 1 { - isDir := header.Typeflag == tar.TypeDir - if !isDir { - isSingleRoot = false - break - } - - if rootDir == "" { - rootDir = parts[0] - } - rootItemCount++ - } - } - - if rootItemCount != 1 { - isSingleRoot = false - } - _ = gzr.Reset(bytes.NewReader(data)) tr = tar.NewReader(gzr) - var extractedFolder string - if isSingleRoot && rootDir != "" { - log.Debugln("Match the singleRoot") - extractedFolder = filepath.Join(dest, rootDir) - log.Debugln("extractedFolder: %s", extractedFolder) - } else { - log.Debugln("Match the multiRoot") - extractedFolder = dest - log.Debugln("extractedFolder: %s", extractedFolder) - } - for { header, err := tr.Next() if err == io.EOF { break } if err != nil { - return "", err + return err } - var fpath string - if isSingleRoot && rootDir != "" { - fpath = filepath.Join(dest, cleanTarPath(header.Name)) - } else { - fpath = filepath.Join(extractedFolder, cleanTarPath(header.Name)) - } + fpath := filepath.Join(dest, header.Name) if !inDest(fpath, dest) { - return "", fmt.Errorf("invalid file path: %s", fpath) + return fmt.Errorf("invalid file path: %s", fpath) } switch header.Typeflag { case tar.TypeDir: if err = os.MkdirAll(fpath, os.FileMode(header.Mode)); err != nil { - return "", err + return err } case tar.TypeReg: if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { - return "", err + return err } outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode)) if err != nil { - return "", err + return err } if _, err := io.Copy(outFile, tr); err != nil { outFile.Close() - return "", err + return err } outFile.Close() } } - return extractedFolder, nil + return nil } -func extract(data []byte, dest string) (string, error) { +func extract(data []byte, dest string) error { fileType := detectFileType(data) log.Debugln("compression Type: %s", fileType) switch fileType { @@ -351,7 +259,7 @@ func extract(data []byte, dest string) (string, error) { case typeTarGzip: return untgz(data, dest) default: - return "", fmt.Errorf("unknown or unsupported file type") + return fmt.Errorf("unknown or unsupported file type") } } @@ -393,6 +301,15 @@ func moveDir(src string, dst string) error { return err } + if len(dirEntryList) == 1 && dirEntryList[0].IsDir() { + src = filepath.Join(src, dirEntryList[0].Name()) + log.Debugln("match the singleRoot: %s", src) + dirEntryList, err = os.ReadDir(src) + if err != nil { + return err + } + } + for _, dirEntry := range dirEntryList { err = os.Rename(filepath.Join(src, dirEntry.Name()), filepath.Join(dst, dirEntry.Name())) if err != nil { From c489c5260b86bba7c6ddb792592a086b53908b0c Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 20 May 2025 10:56:14 +0800 Subject: [PATCH 20/23] fix: hysteria2 hop ports init https://github.com/MetaCubeX/mihomo/issues/2056 --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index ba5d225da8..69c456e7ee 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/metacubex/randv2 v0.2.0 github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7 github.com/metacubex/sing-mux v0.3.2 - github.com/metacubex/sing-quic v0.0.0-20250517090120-462e75d27336 + github.com/metacubex/sing-quic v0.0.0-20250520025433-6e556a6bef7a github.com/metacubex/sing-shadowsocks v0.2.9 github.com/metacubex/sing-shadowsocks2 v0.2.3 github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 diff --git a/go.sum b/go.sum index 7fb37c7b29..cca20fe8a1 100644 --- a/go.sum +++ b/go.sum @@ -120,10 +120,8 @@ github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7 h1:m4nSxvw46JEgxM github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw= github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw= -github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2 h1:wfmYgtECbEYo1slMtyo+2kMqscYYDSjU/TVgS3018F4= -github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2/go.mod h1:P1kd57U6XXmXv9PbwWdznUGT0k9bKgFJXF0fEORbIlk= -github.com/metacubex/sing-quic v0.0.0-20250517090120-462e75d27336 h1:5BgpaFkTzkePwF1A8rmhCqgyOMG79BLsAhFR8W8SiRo= -github.com/metacubex/sing-quic v0.0.0-20250517090120-462e75d27336/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM= +github.com/metacubex/sing-quic v0.0.0-20250520025433-6e556a6bef7a h1:Ho73vGiB94LmtK5T+tKVwtCNEi/YiHmPjlqpHSAmAVs= +github.com/metacubex/sing-quic v0.0.0-20250520025433-6e556a6bef7a/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM= github.com/metacubex/sing-shadowsocks v0.2.9 h1:2e++13WNN7EGjGtvrGLUzW1xrCdQbW2gIFpgw5GEw00= github.com/metacubex/sing-shadowsocks v0.2.9/go.mod h1:CJSEGO4FWQAWe+ZiLZxCweGdjRR60A61SIoVjdjQeBA= github.com/metacubex/sing-shadowsocks2 v0.2.3 h1:v3rNS/5Ywh0NIZ6VU/NmdERQIN5RePzyxCFeQsU4Cx0= From 257fead53820cc889ee42a6fc83f4427293bb598 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 20 May 2025 11:08:42 +0800 Subject: [PATCH 21/23] docs: update config.yaml follow 5cf0f18c --- docs/config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/config.yaml b/docs/config.yaml index 774a6e7125..42a3307c3f 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -645,6 +645,7 @@ proxies: # socks5 reality-opts: public-key: xxx short-id: xxx # optional + support-x25519mlkem768: false # 如果服务端支持可手动设置为true client-fingerprint: chrome # cannot be empty - name: "vless-reality-grpc" @@ -664,6 +665,7 @@ proxies: # socks5 reality-opts: public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE short-id: 10f897e26c4b9478 + support-x25519mlkem768: false # 如果服务端支持可手动设置为true - name: "vless-ws" type: vless From d5a03901d2a40acb1c1d933943af2c73e89a8963 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 20 May 2025 16:15:04 +0800 Subject: [PATCH 22/23] fix: race in close grpc transport --- transport/gun/gun.go | 60 +++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/transport/gun/gun.go b/transport/gun/gun.go index ce9d027951..08598b2552 100644 --- a/transport/gun/gun.go +++ b/transport/gun/gun.go @@ -18,7 +18,6 @@ import ( "sync" "time" - "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/component/ech" @@ -42,16 +41,19 @@ type DialFn = func(ctx context.Context, network, addr string) (net.Conn, error) type Conn struct { initFn func() (io.ReadCloser, netAddr, error) - writer io.Writer + writer io.Writer // writer must not nil closer io.Closer netAddr - reader io.ReadCloser - once sync.Once - closed atomic.Bool - err error - remain int - br *bufio.Reader + initOnce sync.Once + initErr error + reader io.ReadCloser + br *bufio.Reader + remain int + + closeMutex sync.Mutex + closed bool + // deadlines deadline *time.Timer } @@ -65,7 +67,7 @@ type Config struct { func (g *Conn) initReader() { reader, addr, err := g.initFn() if err != nil { - g.err = err + g.initErr = err if closer, ok := g.writer.(io.Closer); ok { closer.Close() } @@ -73,17 +75,21 @@ func (g *Conn) initReader() { } g.netAddr = addr - if !g.closed.Load() { - g.reader = reader - g.br = bufio.NewReader(reader) - } else { - reader.Close() + g.closeMutex.Lock() + defer g.closeMutex.Unlock() + if g.closed { // if g.Close() be called between g.initFn(), direct close the initFn returned reader + _ = reader.Close() + g.initErr = net.ErrClosed + return } + + g.reader = reader + g.br = bufio.NewReader(reader) } func (g *Conn) Init() error { - g.once.Do(g.initReader) - return g.err + g.initOnce.Do(g.initReader) + return g.initErr } func (g *Conn) Read(b []byte) (n int, err error) { @@ -100,8 +106,6 @@ func (g *Conn) Read(b []byte) (n int, err error) { n, err = io.ReadFull(g.br, b[:size]) g.remain -= n return - } else if g.reader == nil { - return 0, net.ErrClosed } // 0x00 grpclength(uint32) 0x0A uleb128 payload @@ -147,8 +151,8 @@ func (g *Conn) Write(b []byte) (n int, err error) { buf.Write(b) _, err = g.writer.Write(buf.Bytes()) - if err == io.ErrClosedPipe && g.err != nil { - err = g.err + if err == io.ErrClosedPipe && g.initErr != nil { + err = g.initErr } if flusher, ok := g.writer.(http.Flusher); ok { @@ -170,8 +174,8 @@ func (g *Conn) WriteBuffer(buffer *buf.Buffer) error { binary.PutUvarint(header[6:], uint64(dataLen)) _, err := g.writer.Write(buffer.Bytes()) - if err == io.ErrClosedPipe && g.err != nil { - err = g.err + if err == io.ErrClosedPipe && g.initErr != nil { + err = g.initErr } if flusher, ok := g.writer.(http.Flusher); ok { @@ -186,7 +190,17 @@ func (g *Conn) FrontHeadroom() int { } func (g *Conn) Close() error { - g.closed.Store(true) + g.initOnce.Do(func() { // if initReader not called, it should not be run anymore + g.initErr = net.ErrClosed + }) + + g.closeMutex.Lock() + defer g.closeMutex.Unlock() + if g.closed { + return nil + } + g.closed = true + var errorArr []error if reader := g.reader; reader != nil { From fd959feff28581d212b2dc7588be986f50834e28 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Wed, 21 May 2025 21:37:20 +0800 Subject: [PATCH 23/23] chore: update dependencies --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 69c456e7ee..173aae8133 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 github.com/mdlayher/netlink v1.7.2 github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab - github.com/metacubex/bart v0.19.0 + github.com/metacubex/bart v0.20.5 github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 github.com/metacubex/chacha v0.1.2 github.com/metacubex/fswatch v0.1.1 @@ -35,7 +35,7 @@ require ( github.com/metacubex/sing-vmess v0.2.1 github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f github.com/metacubex/smux v0.0.0-20250503055512-501391591dee - github.com/metacubex/tfo-go v0.0.0-20250503140532-decbcfccbfdf + github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 github.com/metacubex/utls v1.7.3 github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 github.com/miekg/dns v1.1.63 // lastest version compatible with golang1.20 @@ -72,7 +72,7 @@ require ( github.com/buger/jsonparser v1.1.1 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615 // indirect + github.com/ebitengine/purego v0.8.3 // indirect github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect diff --git a/go.sum b/go.sum index cca20fe8a1..aacf6f81a0 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615 h1:W7mpP4uiOAbBOdDnRXT9EUdauFv7bz+ERT5rPIord00= -github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc= +github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98= github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo= @@ -97,8 +97,8 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4= github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI= -github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY= -github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI= +github.com/metacubex/bart v0.20.5 h1:XkgLZ17QxfxkqKdGsojoM2Zu01mmHyyQSFzt2/calTM= +github.com/metacubex/bart v0.20.5/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI= github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig= github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro= github.com/metacubex/chacha v0.1.2 h1:QulCq3eVm3TO6+4nVIWJtmSe7BT2GMrgVHuAoqRQnlc= @@ -136,8 +136,8 @@ github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYK github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80= github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113awp7P6odM2okB5s60HUyF0FMqKmo= github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE= -github.com/metacubex/tfo-go v0.0.0-20250503140532-decbcfccbfdf h1:LwID1wz4tzypidd412dd4dC1H0m1TgRCQ/XvRvMJDFM= -github.com/metacubex/tfo-go v0.0.0-20250503140532-decbcfccbfdf/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= +github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 h1:j1VRTiC9JLR4nUbSikx9OGdu/3AgFDqgcLj4GoqyQkc= +github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/utls v1.7.3 h1:yDcMEWojFh+t8rU9X0HPcZDPAoFze/rIIyssqivzj8A= github.com/metacubex/utls v1.7.3/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU= github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=