From 0dc5e3051dba21e9498766cfe246bda749324cf1 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sat, 20 Sep 2025 00:19:07 +0800 Subject: [PATCH] feat: add mTLS support for client & server `certificate` and `private-key` for proxies `client-auth-type` and `client-auth-cert` for listeners --- adapter/outbound/anytls.go | 4 + adapter/outbound/http.go | 4 + adapter/outbound/hysteria.go | 4 + adapter/outbound/hysteria2.go | 4 + adapter/outbound/shadowsocks.go | 12 +++ adapter/outbound/socks5.go | 4 + adapter/outbound/trojan.go | 8 ++ adapter/outbound/tuic.go | 4 + adapter/outbound/vless.go | 8 ++ adapter/outbound/vmess.go | 10 ++ component/ca/config.go | 12 +++ component/ca/keypair.go | 35 ++++++- component/tls/auth.go | 45 +++++++++ component/tls/utls.go | 2 + config/config.go | 6 ++ docs/config.yaml | 112 ++++++++++++++++++---- hub/hub.go | 22 +++-- hub/route/server.go | 38 +++++--- listener/anytls/server.go | 13 +++ listener/config/anytls.go | 16 ++-- listener/config/auth.go | 16 ++-- listener/config/hysteria2.go | 2 + listener/config/trojan.go | 2 + listener/config/tuic.go | 2 + listener/config/vless.go | 2 + listener/config/vmess.go | 2 + listener/http/server.go | 13 +++ listener/inbound/anytls.go | 28 +++--- listener/inbound/anytls_test.go | 21 +++++ listener/inbound/common_test.go | 1 + listener/inbound/http.go | 28 +++--- listener/inbound/hysteria2.go | 4 + listener/inbound/hysteria2_test.go | 65 +++++++------ listener/inbound/mixed.go | 30 +++--- listener/inbound/socks.go | 30 +++--- listener/inbound/trojan.go | 4 + listener/inbound/trojan_test.go | 113 ++++++++-------------- listener/inbound/tuic.go | 4 + listener/inbound/tuic_test.go | 21 +++++ listener/inbound/vless.go | 4 + listener/inbound/vless_test.go | 129 +++++++++++++------------- listener/inbound/vmess.go | 4 + listener/inbound/vmess_test.go | 91 ++++++++---------- listener/mixed/mixed.go | 13 +++ listener/sing_hysteria2/server.go | 13 +++ listener/sing_vless/server.go | 13 +++ listener/sing_vmess/server.go | 13 +++ listener/socks/tcp.go | 13 +++ listener/trojan/server.go | 13 +++ listener/tuic/server.go | 13 +++ transport/gost-plugin/websocket.go | 4 + transport/sing-shadowtls/shadowtls.go | 4 + transport/v2ray-plugin/websocket.go | 4 + transport/vmess/tls.go | 4 + 54 files changed, 763 insertions(+), 323 deletions(-) create mode 100644 component/tls/auth.go diff --git a/adapter/outbound/anytls.go b/adapter/outbound/anytls.go index 02541f23..78b1e40c 100644 --- a/adapter/outbound/anytls.go +++ b/adapter/outbound/anytls.go @@ -36,6 +36,8 @@ type AnyTLSOption struct { ClientFingerprint string `proxy:"client-fingerprint,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` UDP bool `proxy:"udp,omitempty"` IdleSessionCheckInterval int `proxy:"idle-session-check-interval,omitempty"` IdleSessionTimeout int `proxy:"idle-session-timeout,omitempty"` @@ -120,6 +122,8 @@ func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) { SkipCertVerify: option.SkipCertVerify, NextProtos: option.ALPN, FingerPrint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, ClientFingerprint: option.ClientFingerprint, ECH: echConfig, } diff --git a/adapter/outbound/http.go b/adapter/outbound/http.go index d7746e47..7b898e2e 100644 --- a/adapter/outbound/http.go +++ b/adapter/outbound/http.go @@ -37,6 +37,8 @@ type HttpOption struct { SNI string `proxy:"sni,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` Headers map[string]string `proxy:"headers,omitempty"` } @@ -173,6 +175,8 @@ func NewHttp(option HttpOption) (*Http, error) { ServerName: sni, }, Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, }) if err != nil { return nil, err diff --git a/adapter/outbound/hysteria.go b/adapter/outbound/hysteria.go index 10876c68..9ba118f3 100644 --- a/adapter/outbound/hysteria.go +++ b/adapter/outbound/hysteria.go @@ -125,6 +125,8 @@ type HysteriaOption struct { ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` ALPN []string `proxy:"alpn,omitempty"` ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"` ReceiveWindow int `proxy:"recv-window,omitempty"` @@ -165,6 +167,8 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) { MinVersion: tls.VersionTLS13, }, Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, }) if err != nil { return nil, err diff --git a/adapter/outbound/hysteria2.go b/adapter/outbound/hysteria2.go index 90c97e54..bc203366 100644 --- a/adapter/outbound/hysteria2.go +++ b/adapter/outbound/hysteria2.go @@ -55,6 +55,8 @@ type Hysteria2Option struct { ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` ALPN []string `proxy:"alpn,omitempty"` CWND int `proxy:"cwnd,omitempty"` UdpMTU int `proxy:"udp-mtu,omitempty"` @@ -146,6 +148,8 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { MinVersion: tls.VersionTLS13, }, Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, }) if err != nil { return nil, err diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index 6ed737b8..a809728e 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -65,6 +65,8 @@ type v2rayObfsOption struct { TLS bool `obfs:"tls,omitempty"` ECHOpts ECHOptions `obfs:"ech-opts,omitempty"` Fingerprint string `obfs:"fingerprint,omitempty"` + Certificate string `obfs:"certificate,omitempty"` + PrivateKey string `obfs:"private-key,omitempty"` Headers map[string]string `obfs:"headers,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` Mux bool `obfs:"mux,omitempty"` @@ -79,6 +81,8 @@ type gostObfsOption struct { TLS bool `obfs:"tls,omitempty"` ECHOpts ECHOptions `obfs:"ech-opts,omitempty"` Fingerprint string `obfs:"fingerprint,omitempty"` + Certificate string `obfs:"certificate,omitempty"` + PrivateKey string `obfs:"private-key,omitempty"` Headers map[string]string `obfs:"headers,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` Mux bool `obfs:"mux,omitempty"` @@ -88,6 +92,8 @@ type shadowTLSOption struct { Password string `obfs:"password,omitempty"` Host string `obfs:"host"` Fingerprint string `obfs:"fingerprint,omitempty"` + Certificate string `obfs:"certificate,omitempty"` + PrivateKey string `obfs:"private-key,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` Version int `obfs:"version,omitempty"` ALPN []string `obfs:"alpn,omitempty"` @@ -302,6 +308,8 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { v2rayOption.TLS = true v2rayOption.SkipCertVerify = opts.SkipCertVerify v2rayOption.Fingerprint = opts.Fingerprint + v2rayOption.Certificate = opts.Certificate + v2rayOption.PrivateKey = opts.PrivateKey echConfig, err := opts.ECHOpts.Parse() if err != nil { @@ -330,6 +338,8 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { gostOption.TLS = true gostOption.SkipCertVerify = opts.SkipCertVerify gostOption.Fingerprint = opts.Fingerprint + gostOption.Certificate = opts.Certificate + gostOption.PrivateKey = opts.PrivateKey echConfig, err := opts.ECHOpts.Parse() if err != nil { @@ -350,6 +360,8 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { Password: opt.Password, Host: opt.Host, Fingerprint: opt.Fingerprint, + Certificate: opt.Certificate, + PrivateKey: opt.PrivateKey, ClientFingerprint: option.ClientFingerprint, SkipCertVerify: opt.SkipCertVerify, Version: opt.Version, diff --git a/adapter/outbound/socks5.go b/adapter/outbound/socks5.go index 91e7d083..00e096c3 100644 --- a/adapter/outbound/socks5.go +++ b/adapter/outbound/socks5.go @@ -39,6 +39,8 @@ type Socks5Option struct { UDP bool `proxy:"udp,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` } // StreamConnContext implements C.ProxyAdapter @@ -200,6 +202,8 @@ func NewSocks5(option Socks5Option) (*Socks5, error) { ServerName: option.Server, }, Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, }) if err != nil { return nil, err diff --git a/adapter/outbound/trojan.go b/adapter/outbound/trojan.go index 96c417e9..5e0de39c 100644 --- a/adapter/outbound/trojan.go +++ b/adapter/outbound/trojan.go @@ -48,6 +48,8 @@ type TrojanOption struct { SNI string `proxy:"sni,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` UDP bool `proxy:"udp,omitempty"` Network string `proxy:"network,omitempty"` ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` @@ -108,6 +110,8 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C. ServerName: t.option.SNI, }, Fingerprint: t.option.Fingerprint, + Certificate: t.option.Certificate, + PrivateKey: t.option.PrivateKey, }) if err != nil { return nil, err @@ -127,6 +131,8 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C. Host: t.option.SNI, SkipCertVerify: t.option.SkipCertVerify, FingerPrint: t.option.Fingerprint, + Certificate: t.option.Certificate, + PrivateKey: t.option.PrivateKey, ClientFingerprint: t.option.ClientFingerprint, NextProtos: alpn, ECH: t.echConfig, @@ -372,6 +378,8 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { ServerName: option.SNI, }, Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, }) if err != nil { return nil, err diff --git a/adapter/outbound/tuic.go b/adapter/outbound/tuic.go index d538dbd2..f76f482f 100644 --- a/adapter/outbound/tuic.go +++ b/adapter/outbound/tuic.go @@ -55,6 +55,8 @@ type TuicOption struct { CWND int `proxy:"cwnd,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"` ReceiveWindow int `proxy:"recv-window,omitempty"` DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` @@ -170,6 +172,8 @@ func NewTuic(option TuicOption) (*Tuic, error) { MinVersion: tls.VersionTLS13, }, Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, }) if err != nil { return nil, err diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index dd5b35db..00e46fdd 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -67,6 +67,8 @@ type VlessOption struct { WSHeaders map[string]string `proxy:"ws-headers,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` ServerName string `proxy:"servername,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } @@ -103,6 +105,8 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M NextProtos: []string{"http/1.1"}, }, Fingerprint: v.option.Fingerprint, + Certificate: v.option.Certificate, + PrivateKey: v.option.PrivateKey, }) if err != nil { return nil, err @@ -206,6 +210,8 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne Host: host, SkipCertVerify: v.option.SkipCertVerify, FingerPrint: v.option.Fingerprint, + Certificate: v.option.Certificate, + PrivateKey: v.option.PrivateKey, ClientFingerprint: v.option.ClientFingerprint, ECH: v.echConfig, Reality: v.realityConfig, @@ -505,6 +511,8 @@ func NewVless(option VlessOption) (*Vless, error) { ServerName: v.option.ServerName, }, Fingerprint: v.option.Fingerprint, + Certificate: v.option.Certificate, + PrivateKey: v.option.PrivateKey, }) if err != nil { return nil, err diff --git a/adapter/outbound/vmess.go b/adapter/outbound/vmess.go index 58e2b00e..c4badf99 100644 --- a/adapter/outbound/vmess.go +++ b/adapter/outbound/vmess.go @@ -58,6 +58,8 @@ type VmessOption struct { ALPN []string `proxy:"alpn,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` ServerName string `proxy:"servername,omitempty"` ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` @@ -130,6 +132,8 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M NextProtos: []string{"http/1.1"}, }, Fingerprint: v.option.Fingerprint, + Certificate: v.option.Certificate, + PrivateKey: v.option.PrivateKey, }) if err != nil { return nil, err @@ -179,6 +183,8 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M Host: host, SkipCertVerify: v.option.SkipCertVerify, FingerPrint: v.option.Fingerprint, + Certificate: v.option.Certificate, + PrivateKey: v.option.PrivateKey, NextProtos: []string{"h2"}, ClientFingerprint: v.option.ClientFingerprint, Reality: v.realityConfig, @@ -209,6 +215,8 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M Host: host, SkipCertVerify: v.option.SkipCertVerify, FingerPrint: v.option.Fingerprint, + Certificate: v.option.Certificate, + PrivateKey: v.option.PrivateKey, ClientFingerprint: v.option.ClientFingerprint, ECH: v.echConfig, Reality: v.realityConfig, @@ -508,6 +516,8 @@ func NewVmess(option VmessOption) (*Vmess, error) { ServerName: v.option.ServerName, }, Fingerprint: v.option.Fingerprint, + Certificate: v.option.Certificate, + PrivateKey: v.option.PrivateKey, }) if err != nil { return nil, err diff --git a/component/ca/config.go b/component/ca/config.go index 0de92ae8..8f96f745 100644 --- a/component/ca/config.go +++ b/component/ca/config.go @@ -11,6 +11,7 @@ import ( "sync" "github.com/metacubex/mihomo/common/once" + C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/ntp" ) @@ -79,6 +80,8 @@ type Option struct { TLSConfig *tls.Config Fingerprint string ZeroTrust bool + Certificate string + PrivateKey string } func GetTLSConfig(opt Option) (tlsConfig *tls.Config, err error) { @@ -101,6 +104,15 @@ func GetTLSConfig(opt Option) (tlsConfig *tls.Config, err error) { } tlsConfig.InsecureSkipVerify = true } + + if len(opt.Certificate) > 0 || len(opt.PrivateKey) > 0 { + var cert tls.Certificate + cert, err = LoadTLSKeyPair(opt.Certificate, opt.PrivateKey, C.Path) + if err != nil { + return nil, err + } + tlsConfig.Certificates = []tls.Certificate{cert} + } return tlsConfig, nil } diff --git a/component/ca/keypair.go b/component/ca/keypair.go index 7fa6c21f..cefd0cf6 100644 --- a/component/ca/keypair.go +++ b/component/ca/keypair.go @@ -12,6 +12,8 @@ import ( "encoding/pem" "fmt" "math/big" + "os" + "time" ) type Path interface { @@ -56,6 +58,33 @@ func LoadTLSKeyPair(certificate, privateKey string, path Path) (tls.Certificate, return cert, nil } +func LoadCertificates(certificate string, path Path) (*x509.CertPool, error) { + pool := x509.NewCertPool() + if pool.AppendCertsFromPEM([]byte(certificate)) { + return pool, nil + } + painTextErr := fmt.Errorf("invalid certificate: %s", certificate) + if path == nil { + return nil, painTextErr + } + + certificate = path.Resolve(certificate) + var loadErr error + if !path.IsSafePath(certificate) { + loadErr = path.ErrNotSafePath(certificate) + } else { + certPEMBlock, err := os.ReadFile(certificate) + if pool.AppendCertsFromPEM(certPEMBlock) { + return pool, nil + } + loadErr = err + } + if loadErr != nil { + return nil, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) + } + return pool, nil +} + type KeyPairType string const ( @@ -85,7 +114,11 @@ func NewRandomTLSKeyPair(keyPairType KeyPairType) (certificate string, privateKe return } - template := x509.Certificate{SerialNumber: big.NewInt(1)} + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + NotBefore: time.Now().Add(-time.Hour * 24 * 365), + NotAfter: time.Now().Add(time.Hour * 24 * 365), + } certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key) if err != nil { return diff --git a/component/tls/auth.go b/component/tls/auth.go new file mode 100644 index 00000000..bfda9f35 --- /dev/null +++ b/component/tls/auth.go @@ -0,0 +1,45 @@ +package tls + +import ( + utls "github.com/metacubex/utls" +) + +type ClientAuthType = utls.ClientAuthType + +const ( + NoClientCert = utls.NoClientCert + RequestClientCert = utls.RequestClientCert + RequireAnyClientCert = utls.RequireAnyClientCert + VerifyClientCertIfGiven = utls.VerifyClientCertIfGiven + RequireAndVerifyClientCert = utls.RequireAndVerifyClientCert +) + +func ClientAuthTypeFromString(s string) ClientAuthType { + switch s { + case "request": + return RequestClientCert + case "require-any": + return RequireAnyClientCert + case "verify-if-given": + return VerifyClientCertIfGiven + case "require-and-verify": + return RequireAndVerifyClientCert + default: + return NoClientCert + } +} + +func ClientAuthTypeToString(t ClientAuthType) string { + switch t { + case RequestClientCert: + return "request" + case RequireAnyClientCert: + return "require-any" + case VerifyClientCertIfGiven: + return "verify-if-given" + case RequireAndVerifyClientCert: + return "require-and-verify" + default: + return "" + } +} diff --git a/component/tls/utls.go b/component/tls/utls.go index fd5f0e54..f68cb997 100644 --- a/component/tls/utls.go +++ b/component/tls/utls.go @@ -135,6 +135,8 @@ func UConfig(config *tls.Config) *utls.Config { RootCAs: config.RootCAs, NextProtos: config.NextProtos, ServerName: config.ServerName, + ClientAuth: utls.ClientAuthType(config.ClientAuth), + ClientCAs: config.ClientCAs, InsecureSkipVerify: config.InsecureSkipVerify, CipherSuites: config.CipherSuites, MinVersion: config.MinVersion, diff --git a/config/config.go b/config/config.go index dde894ae..bcd3cc7a 100644 --- a/config/config.go +++ b/config/config.go @@ -174,6 +174,8 @@ type Profile struct { type TLS struct { Certificate string PrivateKey string + ClientAuthType string + ClientAuthCert string EchKey string CustomTrustCert []string } @@ -368,6 +370,8 @@ type RawSniffingConfig struct { type RawTLS struct { Certificate string `yaml:"certificate" json:"certificate"` PrivateKey string `yaml:"private-key" json:"private-key"` + ClientAuthType string `yaml:"client-auth-type" json:"client-auth-type"` + ClientAuthCert string `yaml:"client-auth-cert" json:"client-auth-cert"` EchKey string `yaml:"ech-key" json:"ech-key"` CustomTrustCert []string `yaml:"custom-certifactes" json:"custom-certifactes"` } @@ -827,6 +831,8 @@ func parseTLS(cfg *RawConfig) (*TLS, error) { return &TLS{ Certificate: cfg.TLS.Certificate, PrivateKey: cfg.TLS.PrivateKey, + ClientAuthType: cfg.TLS.ClientAuthType, + ClientAuthCert: cfg.TLS.ClientAuthCert, EchKey: cfg.TLS.EchKey, CustomTrustCert: cfg.TLS.CustomTrustCert, }, nil diff --git a/docs/config.yaml b/docs/config.yaml index 7a9d484f..adb7294c 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -48,6 +48,9 @@ ipv6: true # 开启 IPv6 总开关,关闭阻断所有 IPv6 链接和屏蔽 DNS tls: certificate: string # 证书 PEM 格式,或者 证书的路径 private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -350,6 +353,9 @@ proxies: # socks5 # password: password # tls: true # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # skip-cert-verify: true # udp: true # ip-version: ipv6 @@ -365,6 +371,9 @@ proxies: # socks5 # skip-cert-verify: true # sni: custom.com # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # ip-version: dual # Snell @@ -433,6 +442,9 @@ proxies: # socks5 mode: websocket # no QUIC now # tls: true # wss # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # ech-opts: # enable: true # 必须手动开启 # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) @@ -471,6 +483,9 @@ proxies: # socks5 mode: websocket # tls: true # wss # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # skip-cert-verify: true # host: bing.com # path: "/" @@ -531,6 +546,9 @@ proxies: # socks5 # udp: true # tls: true # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # client-fingerprint: chrome # Available: "chrome","firefox","safari","ios","random", currently only support TLS transport in TCP/GRPC/WS/HTTP for VLESS/Vmess and trojan. # skip-cert-verify: true # servername: example.com # priority over wss host @@ -558,6 +576,9 @@ proxies: # socks5 network: h2 tls: true # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 h2-opts: host: - http.example.com @@ -593,6 +614,9 @@ proxies: # socks5 network: grpc tls: true # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 servername: example.com # skip-cert-verify: true grpc-opts: @@ -608,6 +632,9 @@ proxies: # socks5 network: tcp servername: example.com # AKA SNI # skip-cert-verify: true + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" # ech-opts: @@ -625,6 +652,9 @@ proxies: # socks5 udp: true flow: xtls-rprx-vision client-fingerprint: chrome + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # skip-cert-verify: true @@ -696,6 +726,9 @@ proxies: # socks5 servername: example.com # priority over wss host # skip-cert-verify: true # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 ws-opts: path: "/" headers: @@ -711,6 +744,9 @@ proxies: # socks5 password: yourpsk # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # udp: true # sni: example.com # aka server name # alpn: @@ -735,6 +771,9 @@ proxies: # socks5 sni: example.com # skip-cert-verify: true # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 udp: true grpc-opts: grpc-service-name: "example" @@ -748,6 +787,9 @@ proxies: # socks5 sni: example.com # skip-cert-verify: true # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 udp: true # ws-opts: # path: /path @@ -767,6 +809,9 @@ proxies: # socks5 # sni: example.com # aka server name # skip-cert-verify: true # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 #hysteria - name: "hysteria" @@ -791,6 +836,9 @@ proxies: # socks5 # recv-window: 52428800 # disable-mtu-discovery: false # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # fast-open: true # 支持 TCP 快速打开,默认为 false #hysteria2 @@ -813,6 +861,9 @@ proxies: # socks5 # config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA # skip-cert-verify: false # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # alpn: # - h3 ###quic-go特殊配置项,不要随意修改除非你知道你在干什么### @@ -1193,8 +1244,11 @@ listeners: # - username: aaa # password: aaa # 下面两项如果填写则开启 tls(需要同时填写) - # certificate: ./server.crt - # private-key: ./server.key + # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1213,8 +1267,11 @@ listeners: # - username: aaa # password: aaa # 下面两项如果填写则开启 tls(需要同时填写) - # certificate: ./server.crt - # private-key: ./server.key + # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1234,8 +1291,11 @@ listeners: # - username: aaa # password: aaa # 下面两项如果填写则开启 tls(需要同时填写) - # certificate: ./server.crt - # private-key: ./server.key + # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1290,8 +1350,11 @@ listeners: # ws-path: "/" # 如果不为空则开启 websocket 传输层 # grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层 # 下面两项如果填写则开启 tls(需要同时填写) - # certificate: ./server.crt - # private-key: ./server.key + # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1329,8 +1392,11 @@ listeners: # users: # tuicV5 填写(可以同时填写 token) # 00000000-0000-0000-0000-000000000000: PASSWORD_0 # 00000000-0000-0000-0000-000000000001: PASSWORD_1 - # certificate: ./server.crt - # private-key: ./server.key + # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1380,8 +1446,11 @@ listeners: # ------------------------- # decryption: "mlkem768x25519plus.native/xorpub/random.600s(300-600s)/0s.(padding len).(padding gap).(X25519 PrivateKey).(ML-KEM-768 Seed)..." # 下面两项如果填写则开启 tls(需要同时填写) - # certificate: ./server.crt - # private-key: ./server.key + # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1417,8 +1486,11 @@ listeners: username1: password1 username2: password2 # "certificate" and "private-key" are required - certificate: ./server.crt + certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 private-key: ./server.key + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1440,8 +1512,11 @@ listeners: # ws-path: "/" # 如果不为空则开启 websocket 传输层 # grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层 # 下面两项如果填写则开启 tls(需要同时填写) - certificate: ./server.crt - private-key: ./server.key + certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 + private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1482,8 +1557,11 @@ listeners: users: 00000000-0000-0000-0000-000000000000: PASSWORD_0 00000000-0000-0000-0000-000000000001: PASSWORD_1 - # certificate: ./server.crt - # private-key: ./server.key + # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- diff --git a/hub/hub.go b/hub/hub.go index fc4fe81a..d751f99c 100644 --- a/hub/hub.go +++ b/hub/hub.go @@ -50,16 +50,18 @@ func applyRoute(cfg *config.Config) { route.SetUIPath(cfg.Controller.ExternalUI) } route.ReCreateServer(&route.Config{ - Addr: cfg.Controller.ExternalController, - TLSAddr: cfg.Controller.ExternalControllerTLS, - UnixAddr: cfg.Controller.ExternalControllerUnix, - PipeAddr: cfg.Controller.ExternalControllerPipe, - 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, + Addr: cfg.Controller.ExternalController, + TLSAddr: cfg.Controller.ExternalControllerTLS, + UnixAddr: cfg.Controller.ExternalControllerUnix, + PipeAddr: cfg.Controller.ExternalControllerPipe, + Secret: cfg.Controller.Secret, + Certificate: cfg.TLS.Certificate, + PrivateKey: cfg.TLS.PrivateKey, + ClientAuthType: cfg.TLS.ClientAuthType, + ClientAuthCert: cfg.TLS.ClientAuthCert, + EchKey: cfg.TLS.EchKey, + DohServer: cfg.Controller.ExternalDohServer, + IsDebug: cfg.General.LogLevel == log.DEBUG, Cors: route.Cors{ AllowOrigins: cfg.Controller.Cors.AllowOrigins, AllowPrivateNetwork: cfg.Controller.Cors.AllowPrivateNetwork, diff --git a/hub/route/server.go b/hub/route/server.go index a3afdfc5..f2a52d42 100644 --- a/hub/route/server.go +++ b/hub/route/server.go @@ -57,17 +57,19 @@ type Memory struct { } type Config struct { - Addr string - TLSAddr string - UnixAddr string - PipeAddr string - Secret string - Certificate string - PrivateKey string - EchKey string - DohServer string - IsDebug bool - Cors Cors + Addr string + TLSAddr string + UnixAddr string + PipeAddr string + Secret string + Certificate string + PrivateKey string + ClientAuthType string + ClientAuthCert string + EchKey string + DohServer string + IsDebug bool + Cors Cors } type Cors struct { @@ -205,6 +207,20 @@ func startTLS(cfg *Config) { tlsConfig := &tlsC.Config{Time: ntp.Now} tlsConfig.NextProtos = []string{"h2", "http/1.1"} tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)} + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(cfg.ClientAuthType) + if len(cfg.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(cfg.ClientAuthCert, C.Path) + if err != nil { + log.Errorln("External controller tls listen error: %s", err) + return + } + tlsConfig.ClientCAs = pool + } if cfg.EchKey != "" { err = ech.LoadECHKey(cfg.EchKey, tlsConfig, C.Path) diff --git a/listener/anytls/server.go b/listener/anytls/server.go index 6c6995c9..99f1d7bb 100644 --- a/listener/anytls/server.go +++ b/listener/anytls/server.go @@ -58,6 +58,19 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition) } } } + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } sl = &Listener{ config: config, diff --git a/listener/config/anytls.go b/listener/config/anytls.go index 874b7964..723d2d4f 100644 --- a/listener/config/anytls.go +++ b/listener/config/anytls.go @@ -5,13 +5,15 @@ import ( ) type AnyTLSServer struct { - Enable bool `yaml:"enable" json:"enable"` - Listen string `yaml:"listen" json:"listen"` - 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"` + Enable bool `yaml:"enable" json:"enable"` + Listen string `yaml:"listen" json:"listen"` + Users map[string]string `yaml:"users" json:"users,omitempty"` + Certificate string `yaml:"certificate" json:"certificate"` + PrivateKey string `yaml:"private-key" json:"private-key"` + ClientAuthType string `yaml:"client-auth-type" json:"client-auth-type,omitempty"` + ClientAuthCert string `yaml:"client-auth-cert" json:"client-auth-cert,omitempty"` + EchKey string `yaml:"ech-key" json:"ech-key"` + PaddingScheme string `yaml:"padding-scheme" json:"padding-scheme,omitempty"` } func (t AnyTLSServer) String() string { diff --git a/listener/config/auth.go b/listener/config/auth.go index cd0430ec..60f8ac4b 100644 --- a/listener/config/auth.go +++ b/listener/config/auth.go @@ -7,11 +7,13 @@ import ( // AuthServer for http/socks/mixed server type AuthServer struct { - Enable bool - Listen string - AuthStore auth.AuthStore - Certificate string - PrivateKey string - EchKey string - RealityConfig reality.Config + Enable bool + Listen string + AuthStore auth.AuthStore + Certificate string + PrivateKey string + ClientAuthType string + ClientAuthCert string + EchKey string + RealityConfig reality.Config } diff --git a/listener/config/hysteria2.go b/listener/config/hysteria2.go index e8042b0d..2b8312a9 100644 --- a/listener/config/hysteria2.go +++ b/listener/config/hysteria2.go @@ -14,6 +14,8 @@ 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"` + ClientAuthType string `yaml:"client-auth-type" json:"client-auth-type,omitempty"` + ClientAuthCert string `yaml:"client-auth-cert" json:"client-auth-cert,omitempty"` 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"` diff --git a/listener/config/trojan.go b/listener/config/trojan.go index e38a2022..40ba3dad 100644 --- a/listener/config/trojan.go +++ b/listener/config/trojan.go @@ -20,6 +20,8 @@ type TrojanServer struct { GrpcServiceName string Certificate string PrivateKey string + ClientAuthType string + ClientAuthCert string EchKey string RealityConfig reality.Config MuxOption sing.MuxOption diff --git a/listener/config/tuic.go b/listener/config/tuic.go index d923e9a0..ad85b541 100644 --- a/listener/config/tuic.go +++ b/listener/config/tuic.go @@ -13,6 +13,8 @@ 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"` + ClientAuthType string `yaml:"client-auth-type" json:"client-auth-type,omitempty"` + ClientAuthCert string `yaml:"client-auth-cert" json:"client-auth-cert,omitempty"` 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"` diff --git a/listener/config/vless.go b/listener/config/vless.go index 135747da..7facaf25 100644 --- a/listener/config/vless.go +++ b/listener/config/vless.go @@ -22,6 +22,8 @@ type VlessServer struct { GrpcServiceName string Certificate string PrivateKey string + ClientAuthType string + ClientAuthCert 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 2a0e0054..5883bd7d 100644 --- a/listener/config/vmess.go +++ b/listener/config/vmess.go @@ -21,6 +21,8 @@ type VmessServer struct { GrpcServiceName string Certificate string PrivateKey string + ClientAuthType string + ClientAuthCert 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 d4f2396a..1bec2555 100644 --- a/listener/http/server.go +++ b/listener/http/server.go @@ -83,6 +83,19 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A } } } + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { return nil, errors.New("certificate is unavailable in reality") diff --git a/listener/inbound/anytls.go b/listener/inbound/anytls.go index 224b0534..9980789d 100644 --- a/listener/inbound/anytls.go +++ b/listener/inbound/anytls.go @@ -11,11 +11,13 @@ import ( type AnyTLSOption struct { BaseOption - 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"` + Users map[string]string `inbound:"users,omitempty"` + Certificate string `inbound:"certificate"` + PrivateKey string `inbound:"private-key"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` + EchKey string `inbound:"ech-key,omitempty"` + PaddingScheme string `inbound:"padding-scheme,omitempty"` } func (o AnyTLSOption) Equal(config C.InboundConfig) bool { @@ -38,13 +40,15 @@ func NewAnyTLS(options *AnyTLSOption) (*AnyTLS, error) { Base: base, config: options, vs: LC.AnyTLSServer{ - Enable: true, - Listen: base.RawAddress(), - Users: options.Users, - Certificate: options.Certificate, - PrivateKey: options.PrivateKey, - EchKey: options.EchKey, - PaddingScheme: options.PaddingScheme, + Enable: true, + Listen: base.RawAddress(), + Users: options.Users, + Certificate: options.Certificate, + PrivateKey: options.PrivateKey, + ClientAuthType: options.ClientAuthType, + ClientAuthCert: options.ClientAuthCert, + EchKey: options.EchKey, + PaddingScheme: options.PaddingScheme, }, }, nil } diff --git a/listener/inbound/anytls_test.go b/listener/inbound/anytls_test.go index 7759b4c7..444c943a 100644 --- a/listener/inbound/anytls_test.go +++ b/listener/inbound/anytls_test.go @@ -70,4 +70,25 @@ func TestInboundAnyTLS_TLS(t *testing.T) { } testInboundAnyTLS(t, inboundOptions, outboundOptions) }) + t.Run("mTLS", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + testInboundAnyTLS(t, inboundOptions, outboundOptions) + }) + t.Run("mTLS+ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + 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 aa5f2770..72874c7b 100644 --- a/listener/inbound/common_test.go +++ b/listener/inbound/common_test.go @@ -37,6 +37,7 @@ var httpData = make([]byte, 2*pool.RelayBufferSize) var remoteAddr = netip.MustParseAddr("1.2.3.4") var userUUID = utils.NewUUIDV4().String() var tlsCertificate, tlsPrivateKey, tlsFingerprint, _ = ca.NewRandomTLSKeyPair(ca.KeyPairTypeP256) +var tlsAuthCertificate, tlsAuthPrivateKey, _, _ = ca.NewRandomTLSKeyPair(ca.KeyPairTypeP256) var tlsConfigCert, _ = tls.X509KeyPair([]byte(tlsCertificate), []byte(tlsPrivateKey)) var tlsConfig = &tls.Config{Certificates: []tls.Certificate{tlsConfigCert}, NextProtos: []string{"h2", "http/1.1"}} var tlsClientConfig, _ = ca.GetTLSConfig(ca.Option{Fingerprint: tlsFingerprint}) diff --git a/listener/inbound/http.go b/listener/inbound/http.go index 16693a21..fdd0e878 100644 --- a/listener/inbound/http.go +++ b/listener/inbound/http.go @@ -13,11 +13,13 @@ import ( type HTTPOption struct { BaseOption - 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"` + Users AuthUsers `inbound:"users,omitempty"` + Certificate string `inbound:"certificate,omitempty"` + PrivateKey string `inbound:"private-key,omitempty"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` + EchKey string `inbound:"ech-key,omitempty"` + RealityConfig RealityConfig `inbound:"reality-config,omitempty"` } func (o HTTPOption) Equal(config C.InboundConfig) bool { @@ -60,13 +62,15 @@ func (h *HTTP) Listen(tunnel C.Tunnel) error { for _, addr := range strings.Split(h.RawAddress(), ",") { l, err := http.NewWithConfig( LC.AuthServer{ - Enable: true, - Listen: addr, - AuthStore: h.config.Users.GetAuthStore(), - Certificate: h.config.Certificate, - PrivateKey: h.config.PrivateKey, - EchKey: h.config.EchKey, - RealityConfig: h.config.RealityConfig.Build(), + Enable: true, + Listen: addr, + AuthStore: h.config.Users.GetAuthStore(), + Certificate: h.config.Certificate, + PrivateKey: h.config.PrivateKey, + ClientAuthType: h.config.ClientAuthType, + ClientAuthCert: h.config.ClientAuthCert, + EchKey: h.config.EchKey, + RealityConfig: h.config.RealityConfig.Build(), }, tunnel, h.Additions()..., diff --git a/listener/inbound/hysteria2.go b/listener/inbound/hysteria2.go index bcdca0b7..d296f544 100644 --- a/listener/inbound/hysteria2.go +++ b/listener/inbound/hysteria2.go @@ -16,6 +16,8 @@ type Hysteria2Option struct { ObfsPassword string `inbound:"obfs-password,omitempty"` Certificate string `inbound:"certificate"` PrivateKey string `inbound:"private-key"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` MaxIdleTime int `inbound:"max-idle-time,omitempty"` ALPN []string `inbound:"alpn,omitempty"` @@ -61,6 +63,8 @@ func NewHysteria2(options *Hysteria2Option) (*Hysteria2, error) { ObfsPassword: options.ObfsPassword, Certificate: options.Certificate, PrivateKey: options.PrivateKey, + ClientAuthType: options.ClientAuthType, + ClientAuthCert: options.ClientAuthCert, EchKey: options.EchKey, MaxIdleTime: options.MaxIdleTime, ALPN: options.ALPN, diff --git a/listener/inbound/hysteria2_test.go b/listener/inbound/hysteria2_test.go index fd2d4117..52fc07b7 100644 --- a/listener/inbound/hysteria2_test.go +++ b/listener/inbound/hysteria2_test.go @@ -51,14 +51,7 @@ func testInboundHysteria2(t *testing.T, inboundOptions inbound.Hysteria2Option, tunnel.DoTest(t, out) } -func TestInboundHysteria2_TLS(t *testing.T) { - inboundOptions := inbound.Hysteria2Option{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - } - outboundOptions := outbound.Hysteria2Option{ - Fingerprint: tlsFingerprint, - } +func testInboundHysteria2TLS(t *testing.T, inboundOptions inbound.Hysteria2Option, outboundOptions outbound.Hysteria2Option) { testInboundHysteria2(t, inboundOptions, outboundOptions) t.Run("ECH", func(t *testing.T) { inboundOptions := inboundOptions @@ -70,6 +63,38 @@ func TestInboundHysteria2_TLS(t *testing.T) { } testInboundHysteria2(t, inboundOptions, outboundOptions) }) + t.Run("mTLS", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + testInboundHysteria2(t, inboundOptions, outboundOptions) + }) + t.Run("mTLS+ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundHysteria2(t, inboundOptions, outboundOptions) + }) +} + +func TestInboundHysteria2_TLS(t *testing.T) { + inboundOptions := inbound.Hysteria2Option{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + } + outboundOptions := outbound.Hysteria2Option{ + Fingerprint: tlsFingerprint, + } + testInboundHysteria2TLS(t, inboundOptions, outboundOptions) } func TestInboundHysteria2_Salamander(t *testing.T) { @@ -84,17 +109,7 @@ func TestInboundHysteria2_Salamander(t *testing.T) { Obfs: "salamander", 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) - }) + testInboundHysteria2TLS(t, inboundOptions, outboundOptions) } func TestInboundHysteria2_Brutal(t *testing.T) { @@ -109,15 +124,5 @@ func TestInboundHysteria2_Brutal(t *testing.T) { Up: "30 Mbps", 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) - }) + testInboundHysteria2TLS(t, inboundOptions, outboundOptions) } diff --git a/listener/inbound/mixed.go b/listener/inbound/mixed.go index db32512b..ab4331bb 100644 --- a/listener/inbound/mixed.go +++ b/listener/inbound/mixed.go @@ -14,12 +14,14 @@ import ( type MixedOption struct { BaseOption - Users AuthUsers `inbound:"users,omitempty"` - 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"` + Users AuthUsers `inbound:"users,omitempty"` + UDP bool `inbound:"udp,omitempty"` + Certificate string `inbound:"certificate,omitempty"` + PrivateKey string `inbound:"private-key,omitempty"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` + EchKey string `inbound:"ech-key,omitempty"` + RealityConfig RealityConfig `inbound:"reality-config,omitempty"` } func (o MixedOption) Equal(config C.InboundConfig) bool { @@ -65,13 +67,15 @@ func (m *Mixed) Listen(tunnel C.Tunnel) error { for _, addr := range strings.Split(m.RawAddress(), ",") { l, err := mixed.NewWithConfig( LC.AuthServer{ - Enable: true, - Listen: addr, - AuthStore: m.config.Users.GetAuthStore(), - Certificate: m.config.Certificate, - PrivateKey: m.config.PrivateKey, - EchKey: m.config.EchKey, - RealityConfig: m.config.RealityConfig.Build(), + Enable: true, + Listen: addr, + AuthStore: m.config.Users.GetAuthStore(), + Certificate: m.config.Certificate, + PrivateKey: m.config.PrivateKey, + ClientAuthType: m.config.ClientAuthType, + ClientAuthCert: m.config.ClientAuthCert, + EchKey: m.config.EchKey, + RealityConfig: m.config.RealityConfig.Build(), }, tunnel, m.Additions()..., diff --git a/listener/inbound/socks.go b/listener/inbound/socks.go index fa794ae4..fa2b75fa 100644 --- a/listener/inbound/socks.go +++ b/listener/inbound/socks.go @@ -13,12 +13,14 @@ import ( type SocksOption struct { BaseOption - Users AuthUsers `inbound:"users,omitempty"` - 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"` + Users AuthUsers `inbound:"users,omitempty"` + UDP bool `inbound:"udp,omitempty"` + Certificate string `inbound:"certificate,omitempty"` + PrivateKey string `inbound:"private-key,omitempty"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` + EchKey string `inbound:"ech-key,omitempty"` + RealityConfig RealityConfig `inbound:"reality-config,omitempty"` } func (o SocksOption) Equal(config C.InboundConfig) bool { @@ -85,13 +87,15 @@ func (s *Socks) Listen(tunnel C.Tunnel) error { for _, addr := range strings.Split(s.RawAddress(), ",") { stl, err := socks.NewWithConfig( LC.AuthServer{ - Enable: true, - Listen: addr, - AuthStore: s.config.Users.GetAuthStore(), - Certificate: s.config.Certificate, - PrivateKey: s.config.PrivateKey, - EchKey: s.config.EchKey, - RealityConfig: s.config.RealityConfig.Build(), + Enable: true, + Listen: addr, + AuthStore: s.config.Users.GetAuthStore(), + Certificate: s.config.Certificate, + PrivateKey: s.config.PrivateKey, + ClientAuthType: s.config.ClientAuthType, + ClientAuthCert: s.config.ClientAuthCert, + EchKey: s.config.EchKey, + RealityConfig: s.config.RealityConfig.Build(), }, tunnel, s.Additions()..., diff --git a/listener/inbound/trojan.go b/listener/inbound/trojan.go index 04d73bf8..a40bcdba 100644 --- a/listener/inbound/trojan.go +++ b/listener/inbound/trojan.go @@ -16,6 +16,8 @@ type TrojanOption struct { GrpcServiceName string `inbound:"grpc-service-name,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` @@ -68,6 +70,8 @@ func NewTrojan(options *TrojanOption) (*Trojan, error) { GrpcServiceName: options.GrpcServiceName, Certificate: options.Certificate, PrivateKey: options.PrivateKey, + ClientAuthType: options.ClientAuthType, + ClientAuthCert: options.ClientAuthCert, EchKey: options.EchKey, RealityConfig: options.RealityConfig.Build(), MuxOption: options.MuxOption.Build(), diff --git a/listener/inbound/trojan_test.go b/listener/inbound/trojan_test.go index 0968243e..7b92b085 100644 --- a/listener/inbound/trojan_test.go +++ b/listener/inbound/trojan_test.go @@ -58,14 +58,7 @@ func testInboundTrojan(t *testing.T, inboundOptions inbound.TrojanOption, outbou testSingMux(t, tunnel, out) } -func TestInboundTrojan_TLS(t *testing.T) { - inboundOptions := inbound.TrojanOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - } - outboundOptions := outbound.TrojanOption{ - Fingerprint: tlsFingerprint, - } +func testInboundTrojanTLS(t *testing.T, inboundOptions inbound.TrojanOption, outboundOptions outbound.TrojanOption) { testInboundTrojan(t, inboundOptions, outboundOptions) t.Run("ECH", func(t *testing.T) { inboundOptions := inboundOptions @@ -77,6 +70,38 @@ func TestInboundTrojan_TLS(t *testing.T) { } testInboundTrojan(t, inboundOptions, outboundOptions) }) + t.Run("mTLS", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + testInboundTrojan(t, inboundOptions, outboundOptions) + }) + t.Run("mTLS+ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundTrojan(t, inboundOptions, outboundOptions) + }) +} + +func TestInboundTrojan_TLS(t *testing.T) { + inboundOptions := inbound.TrojanOption{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + } + outboundOptions := outbound.TrojanOption{ + Fingerprint: tlsFingerprint, + } + testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Wss1(t *testing.T) { @@ -92,17 +117,7 @@ func TestInboundTrojan_Wss1(t *testing.T) { Path: "/ws", }, } - 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) - }) + testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Wss2(t *testing.T) { @@ -119,17 +134,7 @@ func TestInboundTrojan_Wss2(t *testing.T) { Path: "/ws", }, } - 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) - }) + testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Grpc1(t *testing.T) { @@ -143,17 +148,7 @@ func TestInboundTrojan_Grpc1(t *testing.T) { Network: "grpc", 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) - }) + testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Grpc2(t *testing.T) { @@ -168,17 +163,7 @@ func TestInboundTrojan_Grpc2(t *testing.T) { Network: "grpc", 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) - }) + testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Reality(t *testing.T) { @@ -242,17 +227,7 @@ func TestInboundTrojan_TLS_TrojanSS(t *testing.T) { Password: "password", }, } - 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) - }) + testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Wss_TrojanSS(t *testing.T) { @@ -278,15 +253,5 @@ func TestInboundTrojan_Wss_TrojanSS(t *testing.T) { Path: "/ws", }, } - 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) - }) + testInboundTrojanTLS(t, inboundOptions, outboundOptions) } diff --git a/listener/inbound/tuic.go b/listener/inbound/tuic.go index 67349156..a70175d1 100644 --- a/listener/inbound/tuic.go +++ b/listener/inbound/tuic.go @@ -15,6 +15,8 @@ type TuicOption struct { Users map[string]string `inbound:"users,omitempty"` Certificate string `inbound:"certificate"` PrivateKey string `inbound:"private-key"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` CongestionController string `inbound:"congestion-controller,omitempty"` MaxIdleTime int `inbound:"max-idle-time,omitempty"` @@ -51,6 +53,8 @@ func NewTuic(options *TuicOption) (*Tuic, error) { Users: options.Users, Certificate: options.Certificate, PrivateKey: options.PrivateKey, + ClientAuthType: options.ClientAuthType, + ClientAuthCert: options.ClientAuthCert, EchKey: options.EchKey, CongestionController: options.CongestionController, MaxIdleTime: options.MaxIdleTime, diff --git a/listener/inbound/tuic_test.go b/listener/inbound/tuic_test.go index 1cf3991d..f887609d 100644 --- a/listener/inbound/tuic_test.go +++ b/listener/inbound/tuic_test.go @@ -99,4 +99,25 @@ func TestInboundTuic_TLS(t *testing.T) { } testInboundTuic(t, inboundOptions, outboundOptions) }) + t.Run("mTLS", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + testInboundTuic(t, inboundOptions, outboundOptions) + }) + t.Run("mTLS+ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + 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 305df50a..d8109f84 100644 --- a/listener/inbound/vless.go +++ b/listener/inbound/vless.go @@ -17,6 +17,8 @@ type VlessOption struct { GrpcServiceName string `inbound:"grpc-service-name,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` @@ -64,6 +66,8 @@ func NewVless(options *VlessOption) (*Vless, error) { GrpcServiceName: options.GrpcServiceName, Certificate: options.Certificate, PrivateKey: options.PrivateKey, + ClientAuthType: options.ClientAuthType, + ClientAuthCert: options.ClientAuthCert, 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 56bab549..e12e9556 100644 --- a/listener/inbound/vless_test.go +++ b/listener/inbound/vless_test.go @@ -59,21 +59,15 @@ func testInboundVless(t *testing.T, inboundOptions inbound.VlessOption, outbound testSingMux(t, tunnel, out) } -func TestInboundVless_TLS(t *testing.T) { - inboundOptions := inbound.VlessOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - } - outboundOptions := outbound.VlessOption{ - TLS: true, - Fingerprint: tlsFingerprint, - } +func testInboundVlessTLS(t *testing.T, inboundOptions inbound.VlessOption, outboundOptions outbound.VlessOption, testVision bool) { testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { - outboundOptions := outboundOptions - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) - }) + if testVision { + 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 @@ -83,12 +77,61 @@ func TestInboundVless_TLS(t *testing.T) { 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) - }) + if testVision { + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + } }) + t.Run("mTLS", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + testInboundVless(t, inboundOptions, outboundOptions) + if testVision { + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + } + }) + t.Run("mTLS+ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundVless(t, inboundOptions, outboundOptions) + if testVision { + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + } + }) +} + +func TestInboundVless_TLS(t *testing.T) { + inboundOptions := inbound.VlessOption{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + } + outboundOptions := outbound.VlessOption{ + TLS: true, + Fingerprint: tlsFingerprint, + } + testInboundVlessTLS(t, inboundOptions, outboundOptions, true) } func TestInboundVless_Encryption(t *testing.T) { @@ -183,17 +226,7 @@ func TestInboundVless_Wss1(t *testing.T) { Network: "ws", WSOpts: outbound.WSOptions{Path: "/ws"}, } - 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) - }) + testInboundVlessTLS(t, inboundOptions, outboundOptions, false) } func TestInboundVless_Wss2(t *testing.T) { @@ -209,17 +242,7 @@ func TestInboundVless_Wss2(t *testing.T) { Network: "ws", WSOpts: outbound.WSOptions{Path: "/ws"}, } - 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) - }) + testInboundVlessTLS(t, inboundOptions, outboundOptions, false) } func TestInboundVless_Grpc1(t *testing.T) { @@ -234,17 +257,7 @@ func TestInboundVless_Grpc1(t *testing.T) { Network: "grpc", 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) - }) + testInboundVlessTLS(t, inboundOptions, outboundOptions, false) } func TestInboundVless_Grpc2(t *testing.T) { @@ -260,17 +273,7 @@ func TestInboundVless_Grpc2(t *testing.T) { Network: "grpc", 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) - }) + testInboundVlessTLS(t, inboundOptions, outboundOptions, false) } func TestInboundVless_Reality(t *testing.T) { diff --git a/listener/inbound/vmess.go b/listener/inbound/vmess.go index c04ed093..6090f845 100644 --- a/listener/inbound/vmess.go +++ b/listener/inbound/vmess.go @@ -16,6 +16,8 @@ type VmessOption struct { GrpcServiceName string `inbound:"grpc-service-name,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` @@ -62,6 +64,8 @@ func NewVmess(options *VmessOption) (*Vmess, error) { GrpcServiceName: options.GrpcServiceName, Certificate: options.Certificate, PrivateKey: options.PrivateKey, + ClientAuthType: options.ClientAuthType, + ClientAuthCert: options.ClientAuthCert, 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 460cefb9..de3a62d2 100644 --- a/listener/inbound/vmess_test.go +++ b/listener/inbound/vmess_test.go @@ -66,15 +66,7 @@ func TestInboundVMess_Basic(t *testing.T) { testInboundVMess(t, inboundOptions, outboundOptions) } -func TestInboundVMess_TLS(t *testing.T) { - inboundOptions := inbound.VmessOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - } - outboundOptions := outbound.VmessOption{ - TLS: true, - Fingerprint: tlsFingerprint, - } +func testInboundVMessTLS(t *testing.T, inboundOptions inbound.VmessOption, outboundOptions outbound.VmessOption) { testInboundVMess(t, inboundOptions, outboundOptions) t.Run("ECH", func(t *testing.T) { inboundOptions := inboundOptions @@ -86,6 +78,39 @@ func TestInboundVMess_TLS(t *testing.T) { } testInboundVMess(t, inboundOptions, outboundOptions) }) + t.Run("mTLS", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + testInboundVMess(t, inboundOptions, outboundOptions) + }) + t.Run("mTLS+ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundVMess(t, inboundOptions, outboundOptions) + }) +} + +func TestInboundVMess_TLS(t *testing.T) { + inboundOptions := inbound.VmessOption{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + } + outboundOptions := outbound.VmessOption{ + TLS: true, + Fingerprint: tlsFingerprint, + } + testInboundVMessTLS(t, inboundOptions, outboundOptions) } func TestInboundVMess_Ws(t *testing.T) { @@ -172,17 +197,7 @@ func TestInboundVMess_Wss1(t *testing.T) { Path: "/ws", }, } - 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) - }) + testInboundVMessTLS(t, inboundOptions, outboundOptions) } func TestInboundVMess_Wss2(t *testing.T) { @@ -200,17 +215,7 @@ func TestInboundVMess_Wss2(t *testing.T) { Path: "/ws", }, } - 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) - }) + testInboundVMessTLS(t, inboundOptions, outboundOptions) } func TestInboundVMess_Grpc1(t *testing.T) { @@ -225,17 +230,7 @@ func TestInboundVMess_Grpc1(t *testing.T) { Network: "grpc", 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) - }) + testInboundVMessTLS(t, inboundOptions, outboundOptions) } func TestInboundVMess_Grpc2(t *testing.T) { @@ -251,17 +246,7 @@ func TestInboundVMess_Grpc2(t *testing.T) { Network: "grpc", 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) - }) + testInboundVMessTLS(t, inboundOptions, outboundOptions) } func TestInboundVMess_Reality(t *testing.T) { diff --git a/listener/mixed/mixed.go b/listener/mixed/mixed.go index 71759278..38c3393c 100644 --- a/listener/mixed/mixed.go +++ b/listener/mixed/mixed.go @@ -79,6 +79,19 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A } } } + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { return nil, errors.New("certificate is unavailable in reality") diff --git a/listener/sing_hysteria2/server.go b/listener/sing_hysteria2/server.go index b12953b8..2d172aab 100644 --- a/listener/sing_hysteria2/server.go +++ b/listener/sing_hysteria2/server.go @@ -66,6 +66,19 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi MinVersion: tlsC.VersionTLS13, } tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)} + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } if config.EchKey != "" { err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) diff --git a/listener/sing_vless/server.go b/listener/sing_vless/server.go index efd66026..48e9001a 100644 --- a/listener/sing_vless/server.go +++ b/listener/sing_vless/server.go @@ -94,6 +94,19 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) } } } + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { return nil, errors.New("certificate is unavailable in reality") diff --git a/listener/sing_vmess/server.go b/listener/sing_vmess/server.go index def2ed6e..1ce90259 100644 --- a/listener/sing_vmess/server.go +++ b/listener/sing_vmess/server.go @@ -94,6 +94,19 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) } } } + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { return nil, errors.New("certificate is unavailable in reality") diff --git a/listener/socks/tcp.go b/listener/socks/tcp.go index 22058e5a..1ec3a1f1 100644 --- a/listener/socks/tcp.go +++ b/listener/socks/tcp.go @@ -78,6 +78,19 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A } } } + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { return nil, errors.New("certificate is unavailable in reality") diff --git a/listener/trojan/server.go b/listener/trojan/server.go index e4a60c98..df5f3aba 100644 --- a/listener/trojan/server.go +++ b/listener/trojan/server.go @@ -89,6 +89,19 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) } } } + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { return nil, errors.New("certificate is unavailable in reality") diff --git a/listener/tuic/server.go b/listener/tuic/server.go index 4f65b512..f105c51a 100644 --- a/listener/tuic/server.go +++ b/listener/tuic/server.go @@ -58,6 +58,19 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) ( MinVersion: tlsC.VersionTLS13, } tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)} + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } if config.EchKey != "" { err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) diff --git a/transport/gost-plugin/websocket.go b/transport/gost-plugin/websocket.go index 4ff943dc..fbe1ec32 100644 --- a/transport/gost-plugin/websocket.go +++ b/transport/gost-plugin/websocket.go @@ -22,6 +22,8 @@ type Option struct { ECHConfig *ech.Config SkipCertVerify bool Fingerprint string + Certificate string + PrivateKey string Mux bool } @@ -67,6 +69,8 @@ func NewGostWebsocket(ctx context.Context, conn net.Conn, option *Option) (net.C NextProtos: []string{"http/1.1"}, }, Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, }) if err != nil { return nil, err diff --git a/transport/sing-shadowtls/shadowtls.go b/transport/sing-shadowtls/shadowtls.go index 07157670..501e080f 100644 --- a/transport/sing-shadowtls/shadowtls.go +++ b/transport/sing-shadowtls/shadowtls.go @@ -26,6 +26,8 @@ type ShadowTLSOption struct { Password string Host string Fingerprint string + Certificate string + PrivateKey string ClientFingerprint string SkipCertVerify bool Version int @@ -41,6 +43,8 @@ func NewShadowTLS(ctx context.Context, conn net.Conn, option *ShadowTLSOption) ( ServerName: option.Host, }, Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, }) if err != nil { return nil, err diff --git a/transport/v2ray-plugin/websocket.go b/transport/v2ray-plugin/websocket.go index 250590a6..b2e37926 100644 --- a/transport/v2ray-plugin/websocket.go +++ b/transport/v2ray-plugin/websocket.go @@ -21,6 +21,8 @@ type Option struct { ECHConfig *ech.Config SkipCertVerify bool Fingerprint string + Certificate string + PrivateKey string Mux bool V2rayHttpUpgrade bool V2rayHttpUpgradeFastOpen bool @@ -53,6 +55,8 @@ func NewV2rayObfs(ctx context.Context, conn net.Conn, option *Option) (net.Conn, NextProtos: []string{"http/1.1"}, }, Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, }) if err != nil { return nil, err diff --git a/transport/vmess/tls.go b/transport/vmess/tls.go index c9dd50cc..7239ebd2 100644 --- a/transport/vmess/tls.go +++ b/transport/vmess/tls.go @@ -15,6 +15,8 @@ type TLSConfig struct { Host string SkipCertVerify bool FingerPrint string + Certificate string + PrivateKey string ClientFingerprint string NextProtos []string ECH *ech.Config @@ -33,6 +35,8 @@ func StreamTLSConn(ctx context.Context, conn net.Conn, cfg *TLSConfig) (net.Conn NextProtos: cfg.NextProtos, }, Fingerprint: cfg.FingerPrint, + Certificate: cfg.Certificate, + PrivateKey: cfg.PrivateKey, }) if err != nil { return nil, err