diff --git a/adapter/inbound.go b/adapter/inbound.go index f32b804d..0e5a1c4a 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -48,6 +48,7 @@ type InboundContext struct { ProcessInfo *process.Info QueryType uint16 FakeIP bool + IPCIDRMatchSource bool // rule cache diff --git a/common/tls/std_client.go b/common/tls/std_client.go index 90f51821..c6be4d57 100644 --- a/common/tls/std_client.go +++ b/common/tls/std_client.go @@ -128,5 +128,40 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb } tlsConfig.RootCAs = certPool } + if options.ClientAuth { + // explicitly call mutual verification + var cert []byte + var key []byte + + if len(options.ClientCertificate) > 0 { + cert = []byte(strings.Join(options.ClientCertificate, "\n")) + } else if options.ClientCertificatePath != "" { + content, err := os.ReadFile(options.ClientCertificatePath) + if err != nil { + return nil, E.Cause(err, "read client certificate") + } + cert = content + } + if len(options.ClientKey) > 0 { + key = []byte(strings.Join(options.ClientKey, "\n")) + } else if options.ClientKeyPath != "" { + content, err := os.ReadFile(options.ClientKeyPath) + if err != nil { + return nil, E.Cause(err, "read client key") + } + key = content + } + if cert == nil { + return nil, E.New("missing client certificate") + } else if key == nil { + return nil, E.New("missing client key") + } + + keyPair, err := tls.X509KeyPair(cert, key) + if err != nil { + return nil, E.New(err, "parse x509 key pair") + } + tlsConfig.Certificates = []tls.Certificate{keyPair} + } return &STDClientConfig{&tlsConfig}, nil } diff --git a/common/tls/std_server.go b/common/tls/std_server.go index 28a94cf1..3ee25cf1 100644 --- a/common/tls/std_server.go +++ b/common/tls/std_server.go @@ -3,6 +3,7 @@ package tls import ( "context" "crypto/tls" + "crypto/x509" "net" "os" "strings" @@ -212,6 +213,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound } var certificate []byte var key []byte + var clientca []byte if acmeService == nil { if len(options.Certificate) > 0 { certificate = []byte(strings.Join(options.Certificate, "\n")) @@ -249,6 +251,29 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound tlsConfig.Certificates = []tls.Certificate{keyPair} } } + if options.ClientAuth { + // require client authentication + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + if len(options.ClientCA) > 0 { + clientca = []byte(strings.Join(options.ClientCA, "\n")) + } else if options.ClientCAPath != "" { + content, err := os.ReadFile(options.ClientCAPath) + if err != nil { + return nil, E.Cause(err, "read client CA") + } + clientca = content + } + // add client ca certpool + if len(clientca) > 0 { + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(clientca) { + return nil, E.New("failed to parse client CA:\n\n", clientca) + } + tlsConfig.ClientCAs = certPool + } + } else { + tlsConfig.ClientAuth = tls.NoClientCert + } return &STDServerConfig{ config: tlsConfig, logger: logger, @@ -257,5 +282,6 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound key: key, certificatePath: options.CertificatePath, keyPath: options.KeyPath, + watcher: &fsnotify.Watcher{}, }, nil } diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index 9ac0f7c0..0e5b93a1 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -12,6 +12,9 @@ "certificate_path": "", "key": [], "key_path": "", + "clientAuth": false, + "clientCA": [], + "clientCA_path": "", "acme": { "domain": [], "data_directory": "", @@ -64,8 +67,13 @@ "min_version": "", "max_version": "", "cipher_suites": [], - "certificate": "", + "certificate": [], "certificate_path": "", + "clientAuth": false, + "client_key": [], + "client_key_path": "", + "client_certificate": [], + "client_certificate_path": "", "ech": { "enabled": false, "pq_signature_schemes_enabled": false, @@ -189,6 +197,46 @@ The server private key line array, in PEM format. The path to the server private key, in PEM format. +#### clientAuth + +Enable TLS Client Authentication + +#### clientCA + +==Server only== + +The server's client CA Certificate line array, in PEM format. + +#### clientCA_path + +==Server only== + +The path to the server's client CA certificate, in PEM format. + +#### client_certificate + +==Client only== + +The client certificate line array, in PEM format. + +#### certificate_path + +==Client only== + +The path to the client certificate, in PEM format. + +#### client_key + +==Client only== + +The client private key line array, in PEM format. + +#### client_key_path + +==Client only== + +The path to the client private, in PEM format. + ## Custom TLS support !!! info "QUIC support" @@ -237,7 +285,7 @@ It is recommended to match the parameters of `sing-box generate ech-keypair`. Disables adaptive sizing of TLS records. -When true, the largest possible TLS record size is always used. +When true, the largest possible TLS record size is always used. When false, the size of TLS records may be adjusted in an attempt to improve latency. #### key @@ -385,4 +433,4 @@ Check disabled if empty. ### Reload -For server configuration, certificate, key and ECH key will be automatically reloaded if modified. \ No newline at end of file +For server configuration, certificate, key and ECH key will be automatically reloaded if modified. diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md index f3a7a1a3..c7ae669e 100644 --- a/docs/configuration/shared/tls.zh.md +++ b/docs/configuration/shared/tls.zh.md @@ -12,6 +12,9 @@ "certificate_path": "", "key": [], "key_path": "", + "clientAuth": false, + "clientCA": [], + "clientCA_path": "", "acme": { "domain": [], "data_directory": "", @@ -66,6 +69,11 @@ "cipher_suites": [], "certificate": [], "certificate_path": "", + "clientAuth": false, + "client_key": [], + "client_key_path": "", + "client_certificate": [], + "client_certificate_path": "", "ech": { "enabled": false, "pq_signature_schemes_enabled": false, @@ -189,6 +197,46 @@ TLS 版本值: 服务器 PEM 私钥路径。 +#### clientAuth + +启用客户端验证 + +#### clientCA + +==仅服务器== + +服务器 PEM 验证客户端 CA 证书行数组 + +#### clientCA_path + +==仅服务器== + +服务器 PEM 验证客户端 CA 证书路径 + +#### client_certificate + +==仅客户端== + +客户端 PEM 证书行数组。 + +#### certificate_path + +==仅客户端== + +客户端 PEM 证书路径。 + +#### client_key + +==仅客户端== + +客户端 PEM 私钥行数组。 + +#### client_key_path + +==仅客户端== + +客户端 PEM 私钥路径。 + #### utls ==仅客户端== @@ -373,4 +421,4 @@ ACME DNS01 验证字段。如果配置,将禁用其他验证方法。 ### 重载 -对于服务器配置,如果修改,证书和密钥将自动重新加载。 \ No newline at end of file +对于服务器配置,如果修改,证书和密钥将自动重新加载。 diff --git a/option/tls.go b/option/tls.go index a38b4ef5..94788149 100644 --- a/option/tls.go +++ b/option/tls.go @@ -1,36 +1,45 @@ package option type InboundTLSOptions struct { - Enabled bool `json:"enabled,omitempty"` - ServerName string `json:"server_name,omitempty"` - Insecure bool `json:"insecure,omitempty"` - ALPN Listable[string] `json:"alpn,omitempty"` - MinVersion string `json:"min_version,omitempty"` - MaxVersion string `json:"max_version,omitempty"` - CipherSuites Listable[string] `json:"cipher_suites,omitempty"` - Certificate Listable[string] `json:"certificate,omitempty"` - CertificatePath string `json:"certificate_path,omitempty"` - Key Listable[string] `json:"key,omitempty"` - KeyPath string `json:"key_path,omitempty"` - ACME *InboundACMEOptions `json:"acme,omitempty"` - ECH *InboundECHOptions `json:"ech,omitempty"` - Reality *InboundRealityOptions `json:"reality,omitempty"` + Enabled bool `json:"enabled,omitempty"` + ServerName string `json:"server_name,omitempty"` + Insecure bool `json:"insecure,omitempty"` + ALPN Listable[string] `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites Listable[string] `json:"cipher_suites,omitempty"` + Certificate Listable[string] `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + Key Listable[string] `json:"key,omitempty"` + KeyPath string `json:"key_path,omitempty"` + ClientAuth bool `json:"clientAuth,omitempty"` + // , Requireandverify + ClientCA Listable[string] `json:"clientCA,omitempty"` + ClientCAPath string `json:"clientCA_path,omitempty"` + ACME *InboundACMEOptions `json:"acme,omitempty"` + ECH *InboundECHOptions `json:"ech,omitempty"` + Reality *InboundRealityOptions `json:"reality,omitempty"` } type OutboundTLSOptions struct { - Enabled bool `json:"enabled,omitempty"` - DisableSNI bool `json:"disable_sni,omitempty"` - ServerName string `json:"server_name,omitempty"` - Insecure bool `json:"insecure,omitempty"` - ALPN Listable[string] `json:"alpn,omitempty"` - MinVersion string `json:"min_version,omitempty"` - MaxVersion string `json:"max_version,omitempty"` - CipherSuites Listable[string] `json:"cipher_suites,omitempty"` - Certificate Listable[string] `json:"certificate,omitempty"` - CertificatePath string `json:"certificate_path,omitempty"` - ECH *OutboundECHOptions `json:"ech,omitempty"` - UTLS *OutboundUTLSOptions `json:"utls,omitempty"` - Reality *OutboundRealityOptions `json:"reality,omitempty"` + Enabled bool `json:"enabled,omitempty"` + DisableSNI bool `json:"disable_sni,omitempty"` + ServerName string `json:"server_name,omitempty"` + Insecure bool `json:"insecure,omitempty"` + ALPN Listable[string] `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites Listable[string] `json:"cipher_suites,omitempty"` + Certificate Listable[string] `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + ClientAuth bool `json:"clientAuth,omitempty"` + ClientKey Listable[string] `json:"client_key,omitempty"` + ClientKeyPath string `json:"client_key_path,omitempty"` + ClientCertificate Listable[string] `json:"client_certificate,omitempty"` + ClientCertificatePath string `json:"client_certificate_path,omitempty"` + ECH *OutboundECHOptions `json:"ech,omitempty"` + UTLS *OutboundUTLSOptions `json:"utls,omitempty"` + Reality *OutboundRealityOptions `json:"reality,omitempty"` } type InboundRealityOptions struct { diff --git a/route/router.go b/route/router.go index 6c793b70..e6012961 100644 --- a/route/router.go +++ b/route/router.go @@ -597,6 +597,18 @@ func (r *Router) Start() error { return nil } +func (r *Router) PostStart() error { + if len(r.ruleSets) > 0 { + for i, ruleSet := range r.ruleSets { + err := ruleSet.PostStart() + if err != nil { + return E.Cause(err, "post start rule-set[", i, "]") + } + } + } + return nil +} + func (r *Router) Close() error { monitor := taskmonitor.New(r.logger, C.DefaultStopTimeout) var err error