From 1ca32a7af8b5b445b307c7423ee3843fecea21b5 Mon Sep 17 00:00:00 2001 From: Fangliding Date: Wed, 17 Jun 2026 23:08:41 +0800 Subject: [PATCH] Add test & support utls --- common/protocol/tls/cert/cert.go | 6 + testing/scenarios/tls_test.go | 237 +++++++++++++++++++++++++++++++ transport/internet/tls/tls.go | 40 ++++++ 3 files changed, 283 insertions(+) diff --git a/common/protocol/tls/cert/cert.go b/common/protocol/tls/cert/cert.go index 0a581e073..464cab0a4 100644 --- a/common/protocol/tls/cert/cert.go +++ b/common/protocol/tls/cert/cert.go @@ -82,6 +82,12 @@ func KeyUsage(usage x509.KeyUsage) Option { } } +func ExtKeyUsage(usage []x509.ExtKeyUsage) Option { + return func(c *x509.Certificate) { + c.ExtKeyUsage = usage + } +} + func Organization(org string) Option { return func(c *x509.Certificate) { c.Subject.Organization = []string{org} diff --git a/testing/scenarios/tls_test.go b/testing/scenarios/tls_test.go index c76ce4f60..7d5540e65 100644 --- a/testing/scenarios/tls_test.go +++ b/testing/scenarios/tls_test.go @@ -126,6 +126,243 @@ func TestSimpleTLSConnection(t *testing.T) { } } +func TestMTLSConnection(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverCert, serverCertHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + + // CA that issues client certificates; the server trusts it to verify client certs. + // ExtKeyUsage must allow ClientAuth on the CA too, otherwise the chain fails the + // server's ClientAuth key-usage check. + clientCA, _ := cert.MustGenerate(nil, cert.Authority(true), + cert.KeyUsage(x509.KeyUsageCertSign|x509.KeyUsageDigitalSignature), + cert.ExtKeyUsage([]x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})) + clientCACertPEM, _ := clientCA.ToPEM() + + // Client certificate signed by the CA. It must carry ClientAuth ext key usage, + // otherwise crypto/tls rejects it during client-cert verification. + clientCert, _ := cert.MustGenerate(clientCA, cert.CommonName("client"), + cert.ExtKeyUsage([]x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})) + clientCertPEM, clientKeyPEM := clientCert.ToPEM() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{ + tls.ParseCertificate(serverCert), + { + Certificate: clientCACertPEM, + Usage: tls.Certificate_MTLS_CLIENT_CA, + }, + }, + ClientAuth: "requireandverifyclientcert", + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{ + FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}, + }), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + RewriteAddress: net.NewIPOrDomain(dest.Address), + RewritePort: uint32(dest.Port), + AllowedNetworks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + PinnedPeerCertSha256: [][]byte{serverCertHash[:]}, + Certificate: []*tls.Certificate{ + { + Certificate: clientCertPEM, + Key: clientKeyPEM, + Usage: tls.Certificate_MTLS_CLIENT_CERT, + }, + }, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*20)(); err != nil { + t.Fatal(err) + } +} + +func TestMTLSConnectionMissingClientCert(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverCert, serverCertHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + + clientCA, _ := cert.MustGenerate(nil, cert.Authority(true), + cert.KeyUsage(x509.KeyUsageCertSign|x509.KeyUsageDigitalSignature)) + clientCACertPEM, _ := clientCA.ToPEM() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{ + tls.ParseCertificate(serverCert), + { + Certificate: clientCACertPEM, + Usage: tls.Certificate_MTLS_CLIENT_CA, + }, + }, + ClientAuth: "requireandverifyclientcert", + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{ + FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}, + }), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + RewriteAddress: net.NewIPOrDomain(dest.Address), + RewritePort: uint32(dest.Port), + AllowedNetworks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + // No MTLS_CLIENT_CERT: the client presents nothing, + // so the server must reject the handshake. + PinnedPeerCertSha256: [][]byte{serverCertHash[:]}, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*20)(); err == nil { + t.Fatal("expected handshake failure when the client presents no certificate") + } +} + func TestAutoIssuingCertificate(t *testing.T) { if runtime.GOOS == "windows" { // Not supported on Windows yet. diff --git a/transport/internet/tls/tls.go b/transport/internet/tls/tls.go index df5d1cbd7..7ea6aa4be 100644 --- a/transport/internet/tls/tls.go +++ b/transport/internet/tls/tls.go @@ -146,6 +146,45 @@ func GeneraticUClient(c net.Conn, config *tls.Config) *utls.UConn { return utls.UClient(c, copyConfig(config), utls.HelloChrome_Auto) } +// Adapt a crypto/tls GetClientCertificate callback to the utls signature. +func uGetClientCertificate(originFunc func(*tls.CertificateRequestInfo) (*tls.Certificate, error)) func(*utls.CertificateRequestInfo) (*utls.Certificate, error) { + if originFunc == nil { + return nil + } + return func(info *utls.CertificateRequestInfo) (*utls.Certificate, error) { + schemes := make([]tls.SignatureScheme, len(info.SignatureSchemes)) + for i, s := range info.SignatureSchemes { + schemes[i] = tls.SignatureScheme(s) + } + cert, err := originFunc(&tls.CertificateRequestInfo{ + AcceptableCAs: info.AcceptableCAs, + SignatureSchemes: schemes, + Version: info.Version, + }) + if err != nil { + return nil, err + } + if cert == nil { + return &utls.Certificate{}, nil + } + var uSchemes []utls.SignatureScheme + if cert.SupportedSignatureAlgorithms != nil { + uSchemes = make([]utls.SignatureScheme, len(cert.SupportedSignatureAlgorithms)) + for i, s := range cert.SupportedSignatureAlgorithms { + uSchemes[i] = utls.SignatureScheme(s) + } + } + return &utls.Certificate{ + Certificate: cert.Certificate, + PrivateKey: cert.PrivateKey, + SupportedSignatureAlgorithms: uSchemes, + OCSPStaple: cert.OCSPStaple, + SignedCertificateTimestamps: cert.SignedCertificateTimestamps, + Leaf: cert.Leaf, + }, nil + } +} + func copyConfig(c *tls.Config) *utls.Config { config := &utls.Config{ Rand: c.Rand, @@ -156,6 +195,7 @@ func copyConfig(c *tls.Config) *utls.Config { KeyLogWriter: c.KeyLogWriter, EncryptedClientHelloConfigList: c.EncryptedClientHelloConfigList, NextProtos: c.NextProtos, + GetClientCertificate: uGetClientCertificate(c.GetClientCertificate), } return config }