diff --git a/common/tls/reality_server.go b/common/tls/reality_server.go index b0b3e6e39..5fc684756 100644 --- a/common/tls/reality_server.go +++ b/common/tls/reality_server.go @@ -68,7 +68,10 @@ func NewRealityServer(ctx context.Context, logger log.ContextLogger, options opt return nil, E.New("unknown cipher_suite: ", cipherSuite) } } - if len(options.Certificate) > 0 || options.CertificatePath != "" { + if len(options.CurvePreferences) > 0 { + return nil, E.New("curve preferences is unavailable in reality") + } + if len(options.Certificate) > 0 || options.CertificatePath != "" || len(options.ClientCertificatePublicKeySHA256) > 0 { return nil, E.New("certificate is unavailable in reality") } if len(options.Key) > 0 || options.KeyPath != "" { diff --git a/common/tls/std_client.go b/common/tls/std_client.go index 8aebc3f60..265444ab7 100644 --- a/common/tls/std_client.go +++ b/common/tls/std_client.go @@ -1,9 +1,12 @@ package tls import ( + "bytes" "context" + "crypto/sha256" "crypto/tls" "crypto/x509" + "encoding/base64" "net" "os" "strings" @@ -108,6 +111,15 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres return err } } + if len(options.CertificatePublicKeySHA256) > 0 { + if len(options.Certificate) > 0 || options.CertificatePath != "" { + return nil, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path") + } + tlsConfig.InsecureSkipVerify = true + tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + return verifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts, tlsConfig.Time) + } + } if len(options.ALPN) > 0 { tlsConfig.NextProtos = options.ALPN } @@ -137,6 +149,9 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres return nil, E.New("unknown cipher_suite: ", cipherSuite) } } + for _, curve := range options.CurvePreferences { + tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, tls.CurveID(curve)) + } var certificate []byte if len(options.Certificate) > 0 { certificate = []byte(strings.Join(options.Certificate, "\n")) @@ -175,3 +190,22 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres } return config, nil } + +func verifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte, timeFunc func() time.Time) error { + leafCertificate, err := x509.ParseCertificate(rawCerts[0]) + if err != nil { + return E.Cause(err, "failed to parse leaf certificate") + } + + pubKeyBytes, err := x509.MarshalPKIXPublicKey(leafCertificate.PublicKey) + if err != nil { + return E.Cause(err, "failed to marshal public key") + } + hashValue := sha256.Sum256(pubKeyBytes) + for _, value := range knownHashValues { + if bytes.Equal(value, hashValue[:]) { + return nil + } + } + return E.New("unrecognized remote public key: ", base64.StdEncoding.EncodeToString(hashValue[:])) +} diff --git a/common/tls/std_server.go b/common/tls/std_server.go index d162ceb7c..760c4b3a7 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" @@ -22,16 +23,17 @@ import ( var errInsecureUnused = E.New("tls: insecure unused") type STDServerConfig struct { - access sync.RWMutex - config *tls.Config - logger log.Logger - acmeService adapter.SimpleLifecycle - certificate []byte - key []byte - certificatePath string - keyPath string - echKeyPath string - watcher *fswatch.Watcher + access sync.RWMutex + config *tls.Config + logger log.Logger + acmeService adapter.SimpleLifecycle + certificate []byte + key []byte + certificatePath string + keyPath string + clientCertificatePath []string + echKeyPath string + watcher *fswatch.Watcher } func (c *STDServerConfig) ServerName() string { @@ -111,6 +113,9 @@ func (c *STDServerConfig) startWatcher() error { if c.echKeyPath != "" { watchPath = append(watchPath, c.echKeyPath) } + if len(c.clientCertificatePath) > 0 { + watchPath = append(watchPath, c.clientCertificatePath...) + } if len(watchPath) == 0 { return nil } @@ -159,6 +164,30 @@ func (c *STDServerConfig) certificateUpdated(path string) error { c.config = config c.access.Unlock() c.logger.Info("reloaded TLS certificate") + } else if common.Contains(c.clientCertificatePath, path) { + clientCertificateCA := x509.NewCertPool() + var reloaded bool + for _, certPath := range c.clientCertificatePath { + content, err := os.ReadFile(certPath) + if err != nil { + c.logger.Error(E.Cause(err, "reload certificate from ", c.clientCertificatePath)) + continue + } + if !clientCertificateCA.AppendCertsFromPEM(content) { + c.logger.Error(E.New("invalid client certificate file: ", certPath)) + continue + } + reloaded = true + } + if !reloaded { + return E.New("client certificates is empty") + } + c.access.Lock() + config := c.config.Clone() + config.ClientCAs = clientCertificateCA + c.config = config + c.access.Unlock() + c.logger.Info("reloaded client certificates") } else if path == c.echKeyPath { echKey, err := os.ReadFile(c.echKeyPath) if err != nil { @@ -235,8 +264,14 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option. return nil, E.New("unknown cipher_suite: ", cipherSuite) } } - var certificate []byte - var key []byte + for _, curveID := range options.CurvePreferences { + tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, tls.CurveID(curveID)) + } + tlsConfig.ClientAuth = tls.ClientAuthType(options.ClientAuthentication) + var ( + certificate []byte + key []byte + ) if acmeService == nil { if len(options.Certificate) > 0 { certificate = []byte(strings.Join(options.Certificate, "\n")) @@ -278,6 +313,43 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option. tlsConfig.Certificates = []tls.Certificate{keyPair} } } + if len(options.ClientCertificate) > 0 || len(options.ClientCertificatePath) > 0 { + if tlsConfig.ClientAuth == tls.NoClientCert { + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { + if len(options.ClientCertificate) > 0 { + clientCertificateCA := x509.NewCertPool() + if !clientCertificateCA.AppendCertsFromPEM([]byte(strings.Join(options.ClientCertificate, "\n"))) { + return nil, E.New("invalid client certificate strings") + } + tlsConfig.ClientCAs = clientCertificateCA + } else if len(options.ClientCertificatePath) > 0 { + clientCertificateCA := x509.NewCertPool() + for _, path := range options.ClientCertificatePath { + content, err := os.ReadFile(path) + if err != nil { + return nil, E.Cause(err, "read client certificate from ", path) + } + if !clientCertificateCA.AppendCertsFromPEM(content) { + return nil, E.New("invalid client certificate file: ", path) + } + } + tlsConfig.ClientCAs = clientCertificateCA + } else if len(options.ClientCertificatePublicKeySHA256) > 0 { + if tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { + tlsConfig.ClientAuth = tls.RequireAnyClientCert + } else if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven { + tlsConfig.ClientAuth = tls.RequestClientCert + } + tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + return verifyPublicKeySHA256(options.ClientCertificatePublicKeySHA256, rawCerts, tlsConfig.Time) + } + } else { + return nil, E.New("missing client_certificate, client_certificate_path or client_certificate_public_key_sha256 for client authentication") + } + } var echKeyPath string if options.ECH != nil && options.ECH.Enabled { err = parseECHServerConfig(ctx, options, tlsConfig, &echKeyPath) @@ -286,14 +358,15 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option. } } serverConfig := &STDServerConfig{ - config: tlsConfig, - logger: logger, - acmeService: acmeService, - certificate: certificate, - key: key, - certificatePath: options.CertificatePath, - keyPath: options.KeyPath, - echKeyPath: echKeyPath, + config: tlsConfig, + logger: logger, + acmeService: acmeService, + certificate: certificate, + key: key, + certificatePath: options.CertificatePath, + clientCertificatePath: options.ClientCertificatePath, + keyPath: options.KeyPath, + echKeyPath: echKeyPath, } serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) { serverConfig.access.Lock() diff --git a/common/tls/utls_client.go b/common/tls/utls_client.go index 9f8138d7d..a277b992a 100644 --- a/common/tls/utls_client.go +++ b/common/tls/utls_client.go @@ -167,6 +167,15 @@ func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre } tlsConfig.InsecureServerNameToVerify = serverName } + if len(options.CertificatePublicKeySHA256) > 0 { + if len(options.Certificate) > 0 || options.CertificatePath != "" { + return nil, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path") + } + tlsConfig.InsecureSkipVerify = true + tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + return verifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts, tlsConfig.Time) + } + } if len(options.ALPN) > 0 { tlsConfig.NextProtos = options.ALPN } diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index d7838878f..5533d8aeb 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -5,7 +5,13 @@ icon: material/new-box !!! quote "Changes in sing-box 1.13.0" :material-plus: [kernel_tx](#kernel_tx) - :material-plus: [kernel_rx](#kernel_rx) + :material-plus: [kernel_rx](#kernel_rx) + :material-plus: [curve_preferences](#curve_preferences) + :material-plus: [certificate_public_key_sha256](#certificate_public_key_sha256) + :material-plus: [client_authentication](#client_authentication) + :material-plus: [client_certificate](#client_certificate) + :material-plus: [client_certificate_path](#client_certificate_path) + :material-plus: [client_certificate_public_key_sha256](#client_certificate_public_key_sha256) !!! quote "Changes in sing-box 1.12.0" @@ -29,8 +35,13 @@ icon: material/new-box "min_version": "", "max_version": "", "cipher_suites": [], + "curve_preferences": [], "certificate": [], "certificate_path": "", + "client_authentication": "", + "client_certificate": [], + "client_certificate_path": [], + "client_certificate_public_key_sha256": [], "key": [], "key_path": "", "kernel_tx": false, @@ -92,6 +103,7 @@ icon: material/new-box "cipher_suites": [], "certificate": "", "certificate_path": "", + "certificate_public_key_sha256": [], "fragment": false, "fragment_fallback_delay": "", "record_fragment": false, @@ -195,14 +207,29 @@ By default, the maximum version is currently TLS 1.3. #### cipher_suites -A list of enabled TLS 1.0–1.2 cipher suites. The order of the list is ignored. +List of enabled TLS 1.0–1.2 cipher suites. The order of the list is ignored. Note that TLS 1.3 cipher suites are not configurable. If empty, a safe default list is used. The default cipher suites might change over time. +#### curve_preferences + +!!! question "Since sing-box 1.13.0" + +Set of supported key exchange mechanisms. The order of the list is ignored, and key exchange mechanisms are chosen +from this list using an internal preference order by Golang. + +Available values, also the default list: + +* `P256` +* `P384` +* `P521` +* `X25519` +* `X25519MLKEM768` + #### certificate -The server certificate line array, in PEM format. +Server certificates chain line array, in PEM format. #### certificate_path @@ -210,7 +237,26 @@ The server certificate line array, in PEM format. Will be automatically reloaded if file modified. -The path to the server certificate, in PEM format. +The path to server certificate chain, in PEM format. + + +#### certificate_public_key_sha256 + +!!! question "Since sing-box 1.13.0" + +==Client only== + +List of SHA-256 hashes of server certificate public keys, in base64 format. + +To generate the SHA-256 hash for a certificate's public key, use the following commands: + +```bash +# For a certificate file +openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 + +# For a certificate from a remote server +echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 +``` #### key @@ -228,6 +274,63 @@ The server private key line array, in PEM format. The path to the server private key, in PEM format. +#### client_authentication + +!!! question "Since sing-box 1.13.0" + +==Server only== + +The type of client authentication to use. + +Available values: + +* `no` (default) +* `request` +* `require-any` +* `verify-if-given` +* `require-and-verify` + +One of `client_certificate`, `client_certificate_path`, or `client_certificate_public_key_sha256` is required +if this option is set to `verify-if-given`, or `require-and-verify`. + +#### client_certificate + +!!! question "Since sing-box 1.13.0" + +==Server only== + +Client certificate chain line array, in PEM format. + +#### client_certificate_path + +!!! question "Since sing-box 1.13.0" + +==Server only== + +!!! note "" + + Will be automatically reloaded if file modified. + +List of path to client certificate chain, in PEM format. + +#### client_certificate_public_key_sha256 + +!!! question "Since sing-box 1.13.0" + +==Server only== + +List of SHA-256 hashes of client certificate public keys, in base64 format. + +To generate the SHA-256 hash for a certificate's public key, use the following commands: + +```bash +# For a certificate file +openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 + +# For a certificate from a remote server +echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 +``` + #### kernel_tx !!! question "Since sing-box 1.13.0" diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md index 96ee4d87a..e6468d452 100644 --- a/docs/configuration/shared/tls.zh.md +++ b/docs/configuration/shared/tls.zh.md @@ -1,18 +1,24 @@ --- -icon: material/alert-decagram +icon: material/new-box --- !!! quote "sing-box 1.13.0 中的更改" - :material-plus: [kernel_tx](#kernel_tx) + :material-plus: [kernel_tx](#kernel_tx) :material-plus: [kernel_rx](#kernel_rx) + :material-plus: [curve_preferences](#curve_preferences) + :material-plus: [certificate_public_key_sha256](#certificate_public_key_sha256) + :material-plus: [client_authentication](#client_authentication) + :material-plus: [client_certificate](#client_certificate) + :material-plus: [client_certificate_path](#client_certificate_path) + :material-plus: [client_certificate_public_key_sha256](#client_certificate_public_key_sha256) !!! quote "sing-box 1.12.0 中的更改" - :material-plus: [tls_fragment](#tls_fragment) - :material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay) - :material-plus: [tls_record_fragment](#tls_record_fragment) - :material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled) + :material-plus: [fragment](#fragment) + :material-plus: [fragment_fallback_delay](#fragment_fallback_delay) + :material-plus: [record_fragment](#record_fragment) + :material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled) :material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled) !!! quote "sing-box 1.10.0 中的更改" @@ -29,8 +35,13 @@ icon: material/alert-decagram "min_version": "", "max_version": "", "cipher_suites": [], + "curve_preferences": [], "certificate": [], "certificate_path": "", + "client_authentication": "", + "client_certificate": [], + "client_certificate_path": [], + "client_certificate_public_key_sha256": [], "key": [], "key_path": "", "kernel_tx": false, @@ -90,17 +101,20 @@ icon: material/alert-decagram "min_version": "", "max_version": "", "cipher_suites": [], - "certificate": [], + "certificate": "", "certificate_path": "", + "certificate_public_key_sha256": [], "fragment": false, "fragment_fallback_delay": "", "record_fragment": false, "ech": { "enabled": false, - "pq_signature_schemes_enabled": false, - "dynamic_record_sizing_disabled": false, "config": [], - "config_path": "" + "config_path": "", + + // 废弃的 + "pq_signature_schemes_enabled": false, + "dynamic_record_sizing_disabled": false }, "utls": { "enabled": false, @@ -191,13 +205,27 @@ TLS 版本值: #### cipher_suites -启用的 TLS 1.0-1.2密码套件的列表。列表的顺序被忽略。请注意,TLS 1.3 的密码套件是不可配置的。 +启用的 TLS 1.0–1.2 密码套件列表。列表的顺序被忽略。请注意,TLS 1.3 的密码套件是不可配置的。 如果为空,则使用安全的默认列表。默认密码套件可能会随着时间的推移而改变。 +#### curve_preferences + +!!! question "自 sing-box 1.13.0 起" + +支持的密钥交换机制集合。列表的顺序被忽略,密钥交换机制通过 Golang 的内部偏好顺序从此列表中选择。 + +可用值,同时也是默认列表: + +* `P256` +* `P384` +* `P521` +* `X25519` +* `X25519MLKEM768` + #### certificate -服务器 PEM 证书行数组。 +服务器证书链行数组,PEM 格式。 #### certificate_path @@ -205,7 +233,25 @@ TLS 版本值: 文件更改时将自动重新加载。 -服务器 PEM 证书路径。 +服务器证书链路径,PEM 格式。 + +#### certificate_public_key_sha256 + +!!! question "自 sing-box 1.13.0 起" + +==仅客户端== + +服务器证书公钥的 SHA-256 哈希列表,base64 格式。 + +要生成证书公钥的 SHA-256 哈希,请使用以下命令: + +```bash +# 对于证书文件 +openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 + +# 对于远程服务器的证书 +echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 +``` #### key @@ -221,7 +267,68 @@ TLS 版本值: ==仅服务器== -服务器 PEM 私钥路径。 +!!! note "" + + 文件更改时将自动重新加载。 + +服务器私钥路径,PEM 格式。 + +#### client_authentication + +!!! question "自 sing-box 1.13.0 起" + +==仅服务器== + +要使用的客户端身份验证类型。 + +可用值: + +* `no`(默认) +* `request` +* `require-any` +* `verify-if-given` +* `require-and-verify` + +如果此选项设置为 `verify-if-given` 或 `require-and-verify`, +则需要 `client_certificate`、`client_certificate_path` 或 `client_certificate_public_key_sha256` 中的一个。 + +#### client_certificate + +!!! question "自 sing-box 1.13.0 起" + +==仅服务器== + +客户端证书链行数组,PEM 格式。 + +#### client_certificate_path + +!!! question "自 sing-box 1.13.0 起" + +==仅服务器== + +!!! note "" + + 文件更改时将自动重新加载。 + +客户端证书链路径列表,PEM 格式。 + +#### client_certificate_public_key_sha256 + +!!! question "自 sing-box 1.13.0 起" + +==仅服务器== + +客户端证书公钥的 SHA-256 哈希列表,base64 格式。 + +要生成证书公钥的 SHA-256 哈希,请使用以下命令: + +```bash +# 对于证书文件 +openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 + +# 对于远程服务器的证书 +echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 +``` #### kernel_tx @@ -300,44 +407,11 @@ uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻 默认使用 chrome 指纹。 -## ECH 字段 +### ECH 字段 -ECH (Encrypted Client Hello) 是一个 TLS 扩展,它允许客户端加密其 ClientHello 的第一部分 -信息。 +ECH (Encrypted Client Hello) 是一个 TLS 扩展,它允许客户端加密其 ClientHello 的第一部分信息。 -ECH 配置和密钥可以通过 `sing-box generate ech-keypair [--pq-signature-schemes-enabled]` 生成。 - -#### key - -==仅服务器== - -ECH PEM 密钥行数组 - -#### key_path - -==仅服务器== - -!!! note "" - - 文件更改时将自动重新加载。 - -ECH PEM 密钥路径 - -#### config - -==仅客户端== - -ECH PEM 配置行数组 - -如果为空,将尝试从 DNS 加载。 - -#### config_path - -==仅客户端== - -ECH PEM 配置路径 - -如果为空,将尝试从 DNS 加载。 +ECH 密钥和配置可以通过 `sing-box generate ech-keypair` 生成。 #### pq_signature_schemes_enabled @@ -347,8 +421,6 @@ ECH PEM 配置路径 启用对后量子对等证书签名方案的支持。 -建议匹配 `sing-box generate ech-keypair` 的参数。 - #### dynamic_record_sizing_disabled !!! failure "已在 sing-box 1.12.0 废弃" @@ -357,57 +429,91 @@ ECH PEM 配置路径 禁用 TLS 记录的自适应大小调整。 -如果为 true,则始终使用最大可能的 TLS 记录大小。 -如果为 false,则可能会调整 TLS 记录的大小以尝试改善延迟。 +当为 true 时,总是使用最大可能的 TLS 记录大小。 +当为 false 时,可能会调整 TLS 记录的大小以尝试改善延迟。 -#### tls_fragment +#### key + +==仅服务器== + +ECH 密钥行数组,PEM 格式。 + +#### key_path + +==仅服务器== + +!!! note "" + + 文件更改时将自动重新加载。 + +ECH 密钥路径,PEM 格式。 + +#### config + +==仅客户端== + +ECH 配置行数组,PEM 格式。 + +如果为空,将尝试从 DNS 加载。 + +#### config_path + +==仅客户端== + +ECH 配置路径,PEM 格式。 + +如果为空,将尝试从 DNS 加载。 + +#### fragment !!! question "自 sing-box 1.12.0 起" ==仅客户端== -通过分段 TLS 握手数据包来绕过防火墙检测。 +通过分段 TLS 握手数据包来绕过防火墙。 -此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。 +此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真正的审查。 -由于性能不佳,请首先尝试 `tls_record_fragment`,且仅应用于已知被阻止的服务器名称。 +由于性能不佳,请首先尝试 `record_fragment`,且仅应用于已知被阻止的服务器名称。 -在 Linux、Apple 平台和需要管理员权限的 Windows 系统上,可自动检测等待时间。 -若无法自动检测,将回退使用 `tls_fragment_fallback_delay` 指定的固定等待时间。 +在 Linux、Apple 平台和(需要管理员权限的)Windows 系统上, +可以自动检测等待时间。否则,将回退到 +等待 `fragment_fallback_delay` 指定的固定时间。 -此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。 +此外,如果实际等待时间少于 20ms,也会回退到等待固定时间, +因为目标被认为是本地的或在透明代理后面。 -#### tls_fragment_fallback_delay +#### fragment_fallback_delay !!! question "自 sing-box 1.12.0 起" ==仅客户端== -当 TLS 分片功能无法自动判定等待时间时使用的回退值。 +当 TLS 分段无法自动确定等待时间时使用的回退值。 默认使用 `500ms`。 -#### tls_record_fragment - -==仅客户端== +#### record_fragment !!! question "自 sing-box 1.12.0 起" -通过分段 TLS 握手数据包到多个 TLS 记录来绕过防火墙检测。 +==仅客户端== + +将 TLS 握手分段为多个 TLS 记录以绕过防火墙。 ### ACME 字段 #### domain -一组域名。 +域名列表。 -默认禁用 ACME。 +如果为空则禁用 ACME。 #### data_directory -ACME 数据目录。 +ACME 数据存储目录。 -默认使用 `$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic`。 +如果为空则使用 `$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic`。 #### default_server_name @@ -445,12 +551,11 @@ ACME 数据目录。 #### external_account -EAB(外部帐户绑定)包含将 ACME 帐户绑定或映射到其他已知帐户所需的信息由 CA。 +EAB(外部帐户绑定)包含将 ACME 帐户绑定或映射到 CA 已知的其他帐户所需的信息。 -外部帐户绑定“用于将 ACME 帐户与非 ACME 系统中的现有帐户相关联,例如 CA 客户数据库。 +外部帐户绑定"用于将 ACME 帐户与非 ACME 系统中的现有帐户相关联,例如 CA 客户数据库。 -为了启用 ACME 帐户绑定,运行 ACME 服务器的 CA 需要向 ACME 客户端提供 MAC 密钥和密钥标识符,使用 ACME 之外的一些机制。 -§7.3.4 +为了启用 ACME 帐户绑定,运行 ACME 服务器的 CA 需要使用 ACME 之外的某种机制向 ACME 客户端提供 MAC 密钥和密钥标识符。§7.3.4 #### external_account.key_id @@ -500,6 +605,8 @@ ACME DNS01 验证字段。如果配置,将禁用其他验证方法。 #### max_time_difference -服务器与和客户端之间允许的最大时间差。 +==仅服务器== -默认禁用检查。 +服务器和客户端之间的最大时间差。 + +如果为空则禁用检查。 diff --git a/option/tls.go b/option/tls.go index db51ed1a3..90f9a55a9 100644 --- a/option/tls.go +++ b/option/tls.go @@ -1,24 +1,80 @@ package option -import "github.com/sagernet/sing/common/json/badoption" +import ( + "crypto/tls" + "encoding/json" + "strings" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badoption" +) type InboundTLSOptions struct { - Enabled bool `json:"enabled,omitempty"` - ServerName string `json:"server_name,omitempty"` - Insecure bool `json:"insecure,omitempty"` - ALPN badoption.Listable[string] `json:"alpn,omitempty"` - MinVersion string `json:"min_version,omitempty"` - MaxVersion string `json:"max_version,omitempty"` - CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"` - Certificate badoption.Listable[string] `json:"certificate,omitempty"` - CertificatePath string `json:"certificate_path,omitempty"` - Key badoption.Listable[string] `json:"key,omitempty"` - KeyPath string `json:"key_path,omitempty"` - KernelTx bool `json:"kernel_tx,omitempty"` - KernelRx bool `json:"kernel_rx,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 badoption.Listable[string] `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"` + CurvePreferences badoption.Listable[CurvePreference] `json:"curve_preferences,omitempty"` + Certificate badoption.Listable[string] `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + ClientAuthentication ClientAuthType `json:"client_authentication,omitempty"` + ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"` + ClientCertificatePath badoption.Listable[string] `json:"client_certificate_path,omitempty"` + ClientCertificatePublicKeySHA256 badoption.Listable[[]byte] `json:"client_certificate_public_key_sha256,omitempty"` + Key badoption.Listable[string] `json:"key,omitempty"` + KeyPath string `json:"key_path,omitempty"` + KernelTx bool `json:"kernel_tx,omitempty"` + KernelRx bool `json:"kernel_rx,omitempty"` + ACME *InboundACMEOptions `json:"acme,omitempty"` + ECH *InboundECHOptions `json:"ech,omitempty"` + Reality *InboundRealityOptions `json:"reality,omitempty"` +} + +type ClientAuthType tls.ClientAuthType + +func (t ClientAuthType) MarshalJSON() ([]byte, error) { + var stringValue string + switch t { + case ClientAuthType(tls.NoClientCert): + stringValue = "no" + case ClientAuthType(tls.RequestClientCert): + stringValue = "request" + case ClientAuthType(tls.RequireAnyClientCert): + stringValue = "require-any" + case ClientAuthType(tls.VerifyClientCertIfGiven): + stringValue = "verify-if-given" + case ClientAuthType(tls.RequireAndVerifyClientCert): + stringValue = "require-and-verify" + default: + return nil, E.New("unknown client authentication type: ", int(t)) + } + return json.Marshal(stringValue) +} + +func (t *ClientAuthType) UnmarshalJSON(data []byte) error { + var stringValue string + err := json.Unmarshal(data, &stringValue) + if err != nil { + return err + } + switch stringValue { + case "no": + *t = ClientAuthType(tls.NoClientCert) + case "request": + *t = ClientAuthType(tls.RequestClientCert) + case "require-any": + *t = ClientAuthType(tls.RequireAnyClientCert) + case "verify-if-given": + *t = ClientAuthType(tls.VerifyClientCertIfGiven) + case "require-and-verify": + *t = ClientAuthType(tls.RequireAndVerifyClientCert) + default: + return E.New("unknown client authentication type: ", stringValue) + } + return nil } type InboundTLSOptionsContainer struct { @@ -39,24 +95,26 @@ func (o *InboundTLSOptionsContainer) ReplaceInboundTLSOptions(options *InboundTL } 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 badoption.Listable[string] `json:"alpn,omitempty"` - MinVersion string `json:"min_version,omitempty"` - MaxVersion string `json:"max_version,omitempty"` - CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"` - Certificate badoption.Listable[string] `json:"certificate,omitempty"` - CertificatePath string `json:"certificate_path,omitempty"` - Fragment bool `json:"fragment,omitempty"` - FragmentFallbackDelay badoption.Duration `json:"fragment_fallback_delay,omitempty"` - RecordFragment bool `json:"record_fragment,omitempty"` - KernelTx bool `json:"kernel_tx,omitempty"` - KernelRx bool `json:"kernel_rx,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 badoption.Listable[string] `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"` + CurvePreferences badoption.Listable[CurvePreference] `json:"curve_preferences,omitempty"` + Certificate badoption.Listable[string] `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + CertificatePublicKeySHA256 badoption.Listable[[]byte] `json:"certificate_public_key_sha256,omitempty"` + Fragment bool `json:"fragment,omitempty"` + FragmentFallbackDelay badoption.Duration `json:"fragment_fallback_delay,omitempty"` + RecordFragment bool `json:"record_fragment,omitempty"` + KernelTx bool `json:"kernel_tx,omitempty"` + KernelRx bool `json:"kernel_rx,omitempty"` + ECH *OutboundECHOptions `json:"ech,omitempty"` + UTLS *OutboundUTLSOptions `json:"utls,omitempty"` + Reality *OutboundRealityOptions `json:"reality,omitempty"` } type OutboundTLSOptionsContainer struct { @@ -76,6 +134,58 @@ func (o *OutboundTLSOptionsContainer) ReplaceOutboundTLSOptions(options *Outboun o.TLS = options } +type CurvePreference tls.CurveID + +const ( + CurveP256 = 23 + CurveP384 = 24 + CurveP521 = 25 + X25519 = 29 + X25519MLKEM768 = 4588 +) + +func (c CurvePreference) MarshalJSON() ([]byte, error) { + var stringValue string + switch c { + case CurvePreference(CurveP256): + stringValue = "P256" + case CurvePreference(CurveP384): + stringValue = "P384" + case CurvePreference(CurveP521): + stringValue = "P521" + case CurvePreference(X25519): + stringValue = "X25519" + case CurvePreference(X25519MLKEM768): + stringValue = "X25519MLKEM768" + default: + return nil, E.New("unknown curve id: ", int(c)) + } + return json.Marshal(stringValue) +} + +func (c *CurvePreference) UnmarshalJSON(data []byte) error { + var stringValue string + err := json.Unmarshal(data, &stringValue) + if err != nil { + return err + } + switch strings.ToUpper(stringValue) { + case "P256": + *c = CurvePreference(CurveP256) + case "P384": + *c = CurvePreference(CurveP384) + case "P521": + *c = CurvePreference(CurveP521) + case "X25519": + *c = CurvePreference(X25519) + case "X25519MLKEM768": + *c = CurvePreference(X25519MLKEM768) + default: + return E.New("unknown curve name: ", stringValue) + } + return nil +} + type InboundRealityOptions struct { Enabled bool `json:"enabled,omitempty"` Handshake InboundRealityHandshakeOptions `json:"handshake,omitempty"`