From 0e7c3c79e39bbfaeedca573d0180b0c1c9997da9 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Wed, 15 Jul 2026 16:52:21 +0800 Subject: [PATCH] feat: support restls for anytls outbound and listener --- adapter/outbound/anytls.go | 18 ++++++++++++++++-- docs/config.yaml | 15 +++++++++++++-- listener/anytls/server.go | 21 ++++++++++++++++++++- listener/config/anytls.go | 1 + listener/inbound/anytls.go | 2 ++ listener/inbound/anytls_test.go | 30 ++++++++++++++++++++++++++++++ 6 files changed, 82 insertions(+), 5 deletions(-) diff --git a/adapter/outbound/anytls.go b/adapter/outbound/anytls.go index 19fecf49..ce8f4f8d 100644 --- a/adapter/outbound/anytls.go +++ b/adapter/outbound/anytls.go @@ -33,6 +33,7 @@ type AnyTLSOption struct { SNI string `proxy:"sni,omitempty"` ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` ShadowTLSOpts ShadowTLSOptions `proxy:"shadow-tls-opts,omitempty"` + RestlsOpts RestlsOptions `proxy:"restls-opts,omitempty"` JLSOpts JLSOptions `proxy:"jls-opts,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` @@ -123,12 +124,24 @@ func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) { if err != nil { return nil, err } + restlsConfig, err := option.RestlsOpts.Parse(option.SNI, option.ClientFingerprint) + if err != nil { + return nil, err + } jlsConfig, err := option.JLSOpts.Parse() if err != nil { return nil, err } - if shadowTLSConfig != nil && jlsConfig != nil { - return nil, errors.New("ShadowTLS is incompatible with JLS") + if shadowTLSConfig != nil { + if restlsConfig != nil { + return nil, errors.New("ShadowTLS is incompatible with Restls") + } + if jlsConfig != nil { + return nil, errors.New("ShadowTLS is incompatible with JLS") + } + } + if restlsConfig != nil && jlsConfig != nil { + return nil, errors.New("Restls is incompatible with JLS") } tlsConfig := &vmess.TLSConfig{ Host: option.SNI, @@ -141,6 +154,7 @@ func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) { ClientFingerprint: option.ClientFingerprint, ECH: echConfig, ShadowTLS: shadowTLSConfig, + Restls: restlsConfig, JLS: jlsConfig, } if tlsConfig.Host == "" { diff --git a/docs/config.yaml b/docs/config.yaml index 11f9077d..69c27384 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -1579,6 +1579,10 @@ proxies: # socks5 # shadow-tls-opts: # 使用 sni 作为 ShadowTLS SNI # version: 3 # 支持 v1/v2/v3;留空时默认为 v2 # password: shadow-tls-password + # restls-opts: # 使用 sni 作为 Restls SNI + # password: restls-password + # version-hint: tls13 # 可选值:tls12、tls13 + # # restls-script: "" # jls-opts: # 使用 sni 作为 JLS SNI # username: jls-user # password: jls-password @@ -2368,7 +2372,7 @@ listeners: users: username1: password1 username2: password2 - # "shadow-tls" 和 "jls-config" 均未启用且 "allow-insecure" 不为 true 时,必须填写 "certificate" 和 "private-key";启用 ShadowTLS 或 JLS 时不要填写 + # "shadow-tls"、"res-tls" 和 "jls-config" 均未启用且 "allow-insecure" 不为 true 时,必须填写 "certificate" 和 "private-key";启用 ShadowTLS、ResTLS 或 JLS 时不要填写 certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 private-key: ./server.key # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 @@ -2391,6 +2395,13 @@ listeners: # handshake: # dest: www.example.com:443 # # proxy: "" + # res-tls: + # enable: true + # dest: www.example.com:443 + # password: restls-password + # # restls-script: "" + # # min-record-len: 0 + # # proxy: "" # jls-config: # JLS 替代普通 TLS;未认证连接回落到 dest # enable: true # users: @@ -2401,7 +2412,7 @@ listeners: # # alpn: [h2, http/1.1] # # proxy: "" # # rate-limit: 0 # fallback 转发限速,单位 bit/s;0 表示不限速 - ### 注意,anytls listener, 如果 "allow-insecure" 不为 true, 至少需要填写 “certificate和private-key” 或 “shadow-tls” 或 “jls-config” 的其中一项 ### + ### 注意,anytls listener, 如果 "allow-insecure" 不为 true, 至少需要填写 “certificate和private-key” 或 “shadow-tls” 或 “res-tls” 或 “jls-config” 的其中一项 ### # allow-insecure: false # 是否允许不开启tls加密(注意:仅用于有 nginx, caddy 前置的情况) # padding-scheme: "" # https://github.com/anytls/anytls-go/blob/main/docs/protocol.md#cmdupdatepaddingscheme diff --git a/listener/anytls/server.go b/listener/anytls/server.go index ff18f99b..11c01f1d 100644 --- a/listener/anytls/server.go +++ b/listener/anytls/server.go @@ -16,6 +16,7 @@ import ( C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/jls" + "github.com/metacubex/mihomo/listener/restls" "github.com/metacubex/mihomo/listener/shadowtls" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/ntp" @@ -46,6 +47,7 @@ func New(config LC.AnyTLSServer, lc C.InboundListenConfig, tunnel C.Tunnel, addi } var shadowTLSBuilder *shadowtls.Builder + var restlsBuilder *restls.Builder var jlsBuilder *jls.Builder tlsConfig := &tls.Config{Time: ntp.Now} if config.Certificate != "" && config.PrivateKey != "" { @@ -89,6 +91,18 @@ func New(config LC.AnyTLSServer, lc C.InboundListenConfig, tunnel C.Tunnel, addi return nil, err } } + if config.ResTLS.Enable { + if tlsConfig.GetCertificate != nil { + return nil, errors.New("certificate is unavailable in Restls") + } + if tlsConfig.ClientAuth != tls.NoClientCert { + return nil, errors.New("client-auth is unavailable in Restls") + } + if shadowTLSBuilder != nil { + return nil, errors.New("ShadowTLS is unavailable in Restls") + } + restlsBuilder = restls.New(config.ResTLS, tunnel) + } if config.JLSConfig.Enable { if tlsConfig.GetCertificate != nil { return nil, errors.New("certificate is unavailable in JLS") @@ -99,6 +113,9 @@ func New(config LC.AnyTLSServer, lc C.InboundListenConfig, tunnel C.Tunnel, addi if shadowTLSBuilder != nil { return nil, errors.New("ShadowTLS is unavailable in JLS") } + if restlsBuilder != nil { + return nil, errors.New("Restls is unavailable in JLS") + } jlsBuilder, err = jls.New(config.JLSConfig, tunnel) if err != nil { return nil, err @@ -143,12 +160,14 @@ func New(config LC.AnyTLSServer, lc C.InboundListenConfig, tunnel C.Tunnel, addi } if shadowTLSBuilder != nil { l = shadowTLSBuilder.NewListener(l) + } else if restlsBuilder != nil { + l = restlsBuilder.NewListener(l) } else if jlsBuilder != nil { l = jlsBuilder.NewListener(l) } else if tlsConfig.GetCertificate != nil { l = tls.NewListener(l, tlsConfig) } else if !config.AllowInsecure { - return nil, errors.New("disallow using AnyTLS without certificates/shadow-tls/jls/allow-insecure config") + return nil, errors.New("disallow using AnyTLS without certificates/shadow-tls/res-tls/jls/allow-insecure config") } sl.listeners = append(sl.listeners, l) diff --git a/listener/config/anytls.go b/listener/config/anytls.go index f853ec0c..a108de6a 100644 --- a/listener/config/anytls.go +++ b/listener/config/anytls.go @@ -14,6 +14,7 @@ type AnyTLSServer struct { ClientAuthCert string `yaml:"client-auth-cert" json:"client-auth-cert,omitempty"` EchKey string `yaml:"ech-key" json:"ech-key,omitempty"` ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"` + ResTLS ResTLS `yaml:"res-tls" json:"res-tls,omitempty"` JLSConfig JLSConfig `yaml:"jls-config" json:"jls-config,omitempty"` AllowInsecure bool `yaml:"allow-insecure" json:"allow-insecure,omitempty"` PaddingScheme string `yaml:"padding-scheme" json:"padding-scheme,omitempty"` diff --git a/listener/inbound/anytls.go b/listener/inbound/anytls.go index 42f2ed35..bcb9958a 100644 --- a/listener/inbound/anytls.go +++ b/listener/inbound/anytls.go @@ -18,6 +18,7 @@ type AnyTLSOption struct { ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"` + ResTLS ResTLS `inbound:"res-tls,omitempty"` JLSConfig JLSConfig `inbound:"jls-config,omitempty"` AllowInsecure bool `inbound:"allow-insecure,omitempty"` PaddingScheme string `inbound:"padding-scheme,omitempty"` @@ -52,6 +53,7 @@ func NewAnyTLS(options *AnyTLSOption) (*AnyTLS, error) { ClientAuthCert: options.ClientAuthCert, EchKey: options.EchKey, ShadowTLS: options.ShadowTLS.Build(), + ResTLS: options.ResTLS.Build(), JLSConfig: options.JLSConfig.Build(), AllowInsecure: options.AllowInsecure, PaddingScheme: options.PaddingScheme, diff --git a/listener/inbound/anytls_test.go b/listener/inbound/anytls_test.go index 97b12ef8..2b889feb 100644 --- a/listener/inbound/anytls_test.go +++ b/listener/inbound/anytls_test.go @@ -127,6 +127,36 @@ func TestInboundAnyTLS_ShadowTLS(t *testing.T) { testInboundAnyTLSShadowTLS(t, inboundOptions, outboundOptions) } +func testInboundAnyTLSRestls(t *testing.T, inboundOptions inbound.AnyTLSOption, outboundOptions outbound.AnyTLSOption) { + t.Parallel() + t.Run("Conn", func(t *testing.T) { + inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value + testInboundAnyTLS(t, inboundOptions, outboundOptions) + }) + t.Run("UConn", func(t *testing.T) { + inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value + outboundOptions.ClientFingerprint = "chrome" + testInboundAnyTLS(t, inboundOptions, outboundOptions) + }) +} + +func TestInboundAnyTLS_Restls(t *testing.T) { + const password = "restls-password" + inboundOptions := inbound.AnyTLSOption{ + ResTLS: inbound.ResTLS{ + Enable: true, + Dest: net.JoinHostPort(realityDest, "443"), + Password: password, + }, + } + outboundOptions := outbound.AnyTLSOption{ + SNI: realityDest, + Fingerprint: tlsFingerprint, + RestlsOpts: outbound.RestlsOptions{Password: password, VersionHint: "tls13"}, + } + testInboundAnyTLSRestls(t, inboundOptions, outboundOptions) +} + func testInboundAnyTLSJLS(t *testing.T, inboundOptions inbound.AnyTLSOption, outboundOptions outbound.AnyTLSOption) { t.Parallel() t.Run("Conn", func(t *testing.T) {