From c33a2bd3e93ec2b8fdbd540c009a7246b82679d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 14 Apr 2026 14:04:35 +0800 Subject: [PATCH] draft: cronet HTTP client & Accept ntp time --- common/httpclient/apple_transport_darwin.go | 28 +- common/httpclient/apple_transport_darwin.h | 2 + common/httpclient/apple_transport_darwin.m | 34 ++- common/httpclient/client.go | 15 ++ common/httpclient/cronet_transport_stub.go | 17 ++ .../httpclient/cronet_transport_supported.go | 249 ++++++++++++++++++ common/tls/apple_client.go | 4 + common/tls/apple_client_platform.go | 11 + common/tls/apple_client_platform.h | 2 + common/tls/apple_client_platform.m | 16 +- constant/tls.go | 1 + docs/configuration/shared/http-client.md | 40 +++ docs/configuration/shared/http-client.zh.md | 40 +++ go.mod | 2 + go.sum | 2 - 15 files changed, 440 insertions(+), 23 deletions(-) create mode 100644 common/httpclient/cronet_transport_stub.go create mode 100644 common/httpclient/cronet_transport_supported.go diff --git a/common/httpclient/apple_transport_darwin.go b/common/httpclient/apple_transport_darwin.go index 638089736..d279cc95d 100644 --- a/common/httpclient/apple_transport_darwin.go +++ b/common/httpclient/apple_transport_darwin.go @@ -22,6 +22,7 @@ import ( "strings" "sync" "sync/atomic" + "time" "unsafe" "github.com/sagernet/sing-box/adapter" @@ -32,6 +33,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/ntp" ) const applePinnedHashSize = sha256.Size @@ -72,6 +74,7 @@ type appleTransportShared struct { logger logger.ContextLogger bridge *proxybridge.Bridge config appleSessionConfig + timeFunc func() time.Time refs atomic.Int32 } @@ -99,6 +102,7 @@ func newAppleTransport(ctx context.Context, logger logger.ContextLogger, rawDial logger: logger, bridge: bridge, config: sessionConfig, + timeFunc: ntp.TimeFuncFromContext(ctx), } shared.refs.Store(1) session, err := shared.newSession() @@ -279,14 +283,24 @@ func (t *appleTransport) RoundTrip(request *http.Request) (*http.Response, error bodyPointer = (*C.uint8_t)(C.CBytes(body)) defer C.free(unsafe.Pointer(bodyPointer)) } + var ( + hasVerifyTime bool + verifyTimeUnixMilli int64 + ) + if t.shared.timeFunc != nil { + hasVerifyTime = true + verifyTimeUnixMilli = t.shared.timeFunc().UnixMilli() + } cRequest := C.box_apple_http_request_t{ - method: cMethod, - url: cURL, - header_keys: (**C.char)(headerKeysPointer), - header_values: (**C.char)(headerValuesPointer), - header_count: C.size_t(len(cHeaderKeys)), - body: bodyPointer, - body_len: C.size_t(len(body)), + method: cMethod, + url: cURL, + header_keys: (**C.char)(headerKeysPointer), + header_values: (**C.char)(headerValuesPointer), + header_count: C.size_t(len(cHeaderKeys)), + body: bodyPointer, + body_len: C.size_t(len(body)), + has_verify_time: C.bool(hasVerifyTime), + verify_time_unix_millis: C.int64_t(verifyTimeUnixMilli), } var cErr *C.char var task *C.box_apple_http_task_t diff --git a/common/httpclient/apple_transport_darwin.h b/common/httpclient/apple_transport_darwin.h index 656fc1661..26d6a77bc 100644 --- a/common/httpclient/apple_transport_darwin.h +++ b/common/httpclient/apple_transport_darwin.h @@ -28,6 +28,8 @@ typedef struct box_apple_http_request { size_t header_count; const uint8_t *body; size_t body_len; + bool has_verify_time; + int64_t verify_time_unix_millis; } box_apple_http_request_t; typedef struct box_apple_http_response { diff --git a/common/httpclient/apple_transport_darwin.m b/common/httpclient/apple_transport_darwin.m index 74d619aa7..d7c09350c 100644 --- a/common/httpclient/apple_transport_darwin.m +++ b/common/httpclient/apple_transport_darwin.m @@ -18,6 +18,8 @@ typedef struct box_apple_http_task { char *error; } box_apple_http_task_t; +static NSString *const box_apple_http_verify_time_key = @"sing-box.verify-time"; + static void box_set_error_string(char **error_out, NSString *message) { if (error_out == NULL || *error_out != NULL) { return; @@ -72,10 +74,13 @@ static NSArray *box_parse_certificates_from_pem(const char *pem, size_t pem_len) return certificates; } -static bool box_evaluate_trust(SecTrustRef trustRef, NSArray *anchors, bool anchor_only) { +static bool box_evaluate_trust(SecTrustRef trustRef, NSArray *anchors, bool anchor_only, NSDate *verifyDate) { if (trustRef == NULL) { return false; } + if (verifyDate != nil && SecTrustSetVerifyDate(trustRef, (__bridge CFDateRef)verifyDate) != errSecSuccess) { + return false; + } if (anchors.count > 0 || anchor_only) { CFMutableArrayRef anchorArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); for (id certificate in anchors) { @@ -93,6 +98,17 @@ static bool box_evaluate_trust(SecTrustRef trustRef, NSArray *anchors, bool anch return result; } +static NSDate *box_apple_http_verify_date_for_request(NSURLRequest *request) { + if (request == nil) { + return nil; + } + id value = [NSURLProtocol propertyForKey:box_apple_http_verify_time_key inRequest:request]; + if (![value isKindOfClass:[NSNumber class]]) { + return nil; + } + return [NSDate dateWithTimeIntervalSince1970:[(NSNumber *)value longLongValue] / 1000.0]; +} + static box_apple_http_response_t *box_create_response(NSHTTPURLResponse *httpResponse, NSData *data) { box_apple_http_response_t *response = calloc(1, sizeof(box_apple_http_response_t)); response->status_code = (int)httpResponse.statusCode; @@ -148,22 +164,15 @@ didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); return; } - BOOL needsCustomHandling = self.insecure || self.anchorOnly || self.anchors.count > 0 || self.pinnedPublicKeyHashes.length > 0; + NSDate *verifyDate = box_apple_http_verify_date_for_request(task.currentRequest ?: task.originalRequest); + BOOL needsCustomHandling = self.insecure || self.anchorOnly || self.anchors.count > 0 || self.pinnedPublicKeyHashes.length > 0 || verifyDate != nil; if (!needsCustomHandling) { completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); return; } BOOL ok = YES; if (!self.insecure) { - if (self.anchorOnly || self.anchors.count > 0) { - ok = box_evaluate_trust(trustRef, self.anchors, self.anchorOnly); - } else { - CFErrorRef error = NULL; - ok = SecTrustEvaluateWithError(trustRef, &error); - if (error != NULL) { - CFRelease(error); - } - } + ok = box_evaluate_trust(trustRef, self.anchors, self.anchorOnly, verifyDate); } if (ok && self.pinnedPublicKeyHashes.length > 0) { CFArrayRef certificateChain = SecTrustCopyCertificateChain(trustRef); @@ -306,6 +315,9 @@ box_apple_http_task_t *box_apple_http_session_send_async( if (request->body != NULL && request->body_len > 0) { urlRequest.HTTPBody = [NSData dataWithBytes:request->body length:request->body_len]; } + if (request->has_verify_time) { + [NSURLProtocol setProperty:@(request->verify_time_unix_millis) forKey:box_apple_http_verify_time_key inRequest:urlRequest]; + } box_apple_http_task_t *task = calloc(1, sizeof(box_apple_http_task_t)); dispatch_semaphore_t doneSemaphore = dispatch_semaphore_create(0); task->done_semaphore = (__bridge_retained void *)doneSemaphore; diff --git a/common/httpclient/client.go b/common/httpclient/client.go index 84f08dd75..965971fde 100644 --- a/common/httpclient/client.go +++ b/common/httpclient/client.go @@ -53,6 +53,21 @@ func NewTransport(ctx context.Context, logger logger.ContextLogger, tag string, host: host, tag: tag, }, nil + case C.TLSEngineCronet: + transport, transportErr := newCronetTransport(ctx, logger, rawDialer, options) + if transportErr != nil { + return nil, transportErr + } + headers := options.Headers.Build() + host := headers.Get("Host") + headers.Del("Host") + return &Transport{ + transport: transport, + dialer: rawDialer, + headers: headers, + host: host, + tag: tag, + }, nil case C.TLSEngineDefault, "go": default: return nil, E.New("unknown HTTP engine: ", options.Engine) diff --git a/common/httpclient/cronet_transport_stub.go b/common/httpclient/cronet_transport_stub.go new file mode 100644 index 000000000..df8fc5225 --- /dev/null +++ b/common/httpclient/cronet_transport_stub.go @@ -0,0 +1,17 @@ +//go:build !with_naive_outbound + +package httpclient + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" +) + +func newCronetTransport(ctx context.Context, logger logger.ContextLogger, rawDialer N.Dialer, options option.HTTPClientOptions) (adapter.HTTPTransport, error) { + return nil, E.New("Cronet HTTP engine is not available on current platform/build") +} diff --git a/common/httpclient/cronet_transport_supported.go b/common/httpclient/cronet_transport_supported.go new file mode 100644 index 000000000..d7c419d10 --- /dev/null +++ b/common/httpclient/cronet_transport_supported.go @@ -0,0 +1,249 @@ +//go:build with_naive_outbound + +package httpclient + +import ( + "context" + "encoding/pem" + "net/http" + "os" + "strings" + "sync/atomic" + "time" + + "github.com/sagernet/cronet-go" + _ "github.com/sagernet/cronet-go/all" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + singLogger "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" + + mDNS "github.com/miekg/dns" +) + +type cronetTransportShared struct { + roundTripper *cronet.RoundTripper + refs atomic.Int32 +} + +type cronetTransport struct { + shared *cronetTransportShared +} + +func newCronetTransport(ctx context.Context, logger singLogger.ContextLogger, rawDialer N.Dialer, options option.HTTPClientOptions) (adapter.HTTPTransport, error) { + version := options.Version + if version == 0 { + version = 2 + } + switch version { + case 1, 2, 3: + default: + return nil, E.New("unknown HTTP version: ", version) + } + if options.DisableVersionFallback && version != 3 { + return nil, E.New("disable_version_fallback is unsupported in Cronet HTTP engine unless version is 3") + } + switch version { + case 1: + if options.HTTP2Options != (option.HTTP2Options{}) { + return nil, E.New("HTTP/2 options are unsupported in Cronet HTTP engine for HTTP/1.1") + } + if options.HTTP3Options != (option.QUICOptions{}) { + return nil, E.New("QUIC options are unsupported in Cronet HTTP engine for HTTP/1.1") + } + case 2: + if options.HTTP2Options.IdleTimeout != 0 || + options.HTTP2Options.KeepAlivePeriod != 0 || + options.HTTP2Options.MaxConcurrentStreams > 0 { + return nil, E.New("selected HTTP/2 options are unsupported in Cronet HTTP engine") + } + case 3: + if options.HTTP3Options.KeepAlivePeriod != 0 || + options.HTTP3Options.InitialPacketSize > 0 || + options.HTTP3Options.DisablePathMTUDiscovery || + options.HTTP3Options.MaxConcurrentStreams > 0 { + return nil, E.New("selected QUIC options are unsupported in Cronet HTTP engine") + } + } + + tlsOptions := common.PtrValueOrDefault(options.TLS) + if tlsOptions.Engine != "" { + return nil, E.New("tls.engine is unsupported in Cronet HTTP engine") + } + if tlsOptions.DisableSNI { + return nil, E.New("disable_sni is unsupported in Cronet HTTP engine") + } + if tlsOptions.Insecure { + return nil, E.New("tls.insecure is unsupported in Cronet HTTP engine") + } + if len(tlsOptions.ALPN) > 0 { + return nil, E.New("tls.alpn is unsupported in Cronet HTTP engine") + } + if tlsOptions.MinVersion != "" || tlsOptions.MaxVersion != "" { + return nil, E.New("tls.min_version/max_version is unsupported in Cronet HTTP engine") + } + if len(tlsOptions.CipherSuites) > 0 { + return nil, E.New("cipher_suites is unsupported in Cronet HTTP engine") + } + if len(tlsOptions.CurvePreferences) > 0 { + return nil, E.New("curve_preferences is unsupported in Cronet HTTP engine") + } + if len(tlsOptions.ClientCertificate) > 0 || len(tlsOptions.ClientCertificatePath) > 0 { + return nil, E.New("client certificate is unsupported in Cronet HTTP engine") + } + if len(tlsOptions.ClientKey) > 0 || tlsOptions.ClientKeyPath != "" { + return nil, E.New("client key is unsupported in Cronet HTTP engine") + } + if tlsOptions.Fragment || tlsOptions.RecordFragment { + return nil, E.New("tls fragment is unsupported in Cronet HTTP engine") + } + if tlsOptions.KernelTx || tlsOptions.KernelRx { + return nil, E.New("ktls is unsupported in Cronet HTTP engine") + } + if tlsOptions.HandshakeTimeout != 0 { + return nil, E.New("tls.handshake_timeout is unsupported in Cronet HTTP engine") + } + if tlsOptions.UTLS != nil && tlsOptions.UTLS.Enabled { + return nil, E.New("utls is unsupported in Cronet HTTP engine") + } + if tlsOptions.Reality != nil && tlsOptions.Reality.Enabled { + return nil, E.New("reality is unsupported in Cronet HTTP engine") + } + + trustedRootCertificates, err := readCronetRootCertificates(tlsOptions) + if err != nil { + return nil, err + } + + echEnabled, echConfigList, echQueryServerName, err := readCronetECH(tlsOptions) + if err != nil { + return nil, err + } + + dnsResolver := newCronetDNSResolver(ctx, logger, rawDialer) + if echEnabled && dnsResolver == nil { + return nil, E.New("ECH requires a configured DNS resolver in Cronet HTTP engine") + } + + roundTripper, err := cronet.NewRoundTripper(cronet.RoundTripperOptions{ + Logger: logger, + Version: version, + DisableVersionFallback: options.DisableVersionFallback, + Dialer: rawDialer, + DNSResolver: dnsResolver, + TLS: cronet.RoundTripperTLSOptions{ + ServerName: tlsOptions.ServerName, + TrustedRootCertificates: trustedRootCertificates, + PinnedPublicKeySHA256: tlsOptions.CertificatePublicKeySHA256, + ECHEnabled: echEnabled, + ECHConfigList: echConfigList, + ECHQueryServerName: echQueryServerName, + }, + HTTP2: cronet.RoundTripperHTTP2Options{ + StreamReceiveWindow: options.HTTP2Options.StreamReceiveWindow.Value(), + ConnectionReceiveWindow: options.HTTP2Options.ConnectionReceiveWindow.Value(), + }, + QUIC: cronet.RoundTripperQUICOptions{ + StreamReceiveWindow: options.HTTP3Options.StreamReceiveWindow.Value(), + ConnectionReceiveWindow: options.HTTP3Options.ConnectionReceiveWindow.Value(), + IdleTimeout: time.Duration(options.HTTP3Options.IdleTimeout), + }, + }) + if err != nil { + return nil, err + } + shared := &cronetTransportShared{ + roundTripper: roundTripper, + } + shared.refs.Store(1) + return &cronetTransport{shared: shared}, nil +} + +func readCronetRootCertificates(tlsOptions option.OutboundTLSOptions) (string, error) { + if len(tlsOptions.Certificate) > 0 { + return strings.Join(tlsOptions.Certificate, "\n"), nil + } + if tlsOptions.CertificatePath == "" { + return "", nil + } + content, err := os.ReadFile(tlsOptions.CertificatePath) + if err != nil { + return "", E.Cause(err, "read certificate") + } + return string(content), nil +} + +func readCronetECH(tlsOptions option.OutboundTLSOptions) (bool, []byte, string, error) { + if tlsOptions.ECH == nil || !tlsOptions.ECH.Enabled { + return false, nil, "", nil + } + //nolint:staticcheck + if tlsOptions.ECH.PQSignatureSchemesEnabled || tlsOptions.ECH.DynamicRecordSizingDisabled { + return false, nil, "", E.New("legacy ECH options are deprecated in sing-box 1.12.0 and removed in sing-box 1.13.0") + } + var echConfig []byte + if len(tlsOptions.ECH.Config) > 0 { + echConfig = []byte(strings.Join(tlsOptions.ECH.Config, "\n")) + } else if tlsOptions.ECH.ConfigPath != "" { + content, err := os.ReadFile(tlsOptions.ECH.ConfigPath) + if err != nil { + return false, nil, "", E.Cause(err, "read ECH config") + } + echConfig = content + } + if len(echConfig) == 0 { + return true, nil, tlsOptions.ECH.QueryServerName, nil + } + block, rest := pem.Decode(echConfig) + if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 { + return false, nil, "", E.New("invalid ECH configs pem") + } + return true, block.Bytes, tlsOptions.ECH.QueryServerName, nil +} + +func newCronetDNSResolver(ctx context.Context, logger singLogger.ContextLogger, rawDialer N.Dialer) cronet.DNSResolverFunc { + resolveDialer, isResolveDialer := rawDialer.(dialer.ResolveDialer) + if !isResolveDialer { + return nil + } + dnsRouter := service.FromContext[adapter.DNSRouter](ctx) + if dnsRouter == nil { + return nil + } + queryOptions := resolveDialer.QueryOptions() + return func(dnsContext context.Context, request *mDNS.Msg) *mDNS.Msg { + response, err := dnsRouter.Exchange(dnsContext, request, queryOptions) + if err == nil { + return response + } + logger.Error("DNS exchange failed: ", err) + failure := new(mDNS.Msg) + failure.SetReply(request) + failure.Rcode = mDNS.RcodeServerFailure + return failure + } +} + +func (t *cronetTransport) RoundTrip(request *http.Request) (*http.Response, error) { + return t.shared.roundTripper.RoundTrip(request) +} + +func (t *cronetTransport) CloseIdleConnections() { + t.shared.roundTripper.CloseIdleConnections() +} + +func (t *cronetTransport) Clone() adapter.HTTPTransport { + t.shared.refs.Add(1) + return &cronetTransport{shared: t.shared} +} + +func (t *cronetTransport) Close() error { + if t.shared.refs.Add(-1) == 0 { + return t.shared.roundTripper.Close() + } + return nil +} diff --git a/common/tls/apple_client.go b/common/tls/apple_client.go index bc6e2b393..4b84a31b2 100644 --- a/common/tls/apple_client.go +++ b/common/tls/apple_client.go @@ -14,6 +14,7 @@ import ( "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/service" ) @@ -32,6 +33,7 @@ type appleClientConfig struct { anchorPEM string anchorOnly bool certificatePublicKeySHA256 [][]byte + timeFunc func() time.Time } func (c *appleClientConfig) ServerName() string { @@ -77,6 +79,7 @@ func (c *appleClientConfig) Clone() Config { anchorPEM: c.anchorPEM, anchorOnly: c.anchorOnly, certificatePublicKeySHA256: append([][]byte(nil), c.certificatePublicKeySHA256...), + timeFunc: c.timeFunc, } } @@ -113,6 +116,7 @@ func newAppleClient(ctx context.Context, logger logger.ContextLogger, serverAddr anchorPEM: validated.AnchorPEM, anchorOnly: validated.AnchorOnly, certificatePublicKeySHA256: append([][]byte(nil), options.CertificatePublicKeySHA256...), + timeFunc: ntp.TimeFuncFromContext(ctx), }, nil } diff --git a/common/tls/apple_client_platform.go b/common/tls/apple_client_platform.go index 0e69fd9f7..903e8deb0 100644 --- a/common/tls/apple_client_platform.go +++ b/common/tls/apple_client_platform.go @@ -64,6 +64,15 @@ func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) anchorPEMPtr := cStringOrNil(c.anchorPEM) defer cFree(anchorPEMPtr) + var ( + hasVerifyTime bool + verifyTimeUnixMilli int64 + ) + if c.timeFunc != nil { + hasVerifyTime = true + verifyTimeUnixMilli = c.timeFunc().UnixMilli() + } + var errorPtr *C.char client := C.box_apple_tls_client_create( C.int(dupFD), @@ -76,6 +85,8 @@ func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) anchorPEMPtr, C.size_t(len(c.anchorPEM)), C.bool(c.anchorOnly), + C.bool(hasVerifyTime), + C.int64_t(verifyTimeUnixMilli), &errorPtr, ) if client == nil { diff --git a/common/tls/apple_client_platform.h b/common/tls/apple_client_platform.h index e8b828b9e..064a4732a 100644 --- a/common/tls/apple_client_platform.h +++ b/common/tls/apple_client_platform.h @@ -25,6 +25,8 @@ box_apple_tls_client_t *box_apple_tls_client_create( const char *anchor_pem, size_t anchor_pem_len, bool anchor_only, + bool has_verify_time, + int64_t verify_time_unix_millis, char **error_out ); diff --git a/common/tls/apple_client_platform.m b/common/tls/apple_client_platform.m index 403f12e42..0ca385656 100644 --- a/common/tls/apple_client_platform.m +++ b/common/tls/apple_client_platform.m @@ -208,12 +208,16 @@ static NSArray *box_parse_certificates_from_pem(const char *pem, size_t pem_len) return certificates; } -static bool box_evaluate_trust(sec_trust_t trust, NSArray *anchors, bool anchor_only) { +static bool box_evaluate_trust(sec_trust_t trust, NSArray *anchors, bool anchor_only, NSDate *verify_date) { bool result = false; SecTrustRef trustRef = sec_trust_copy_ref(trust); if (trustRef == NULL) { return false; } + if (verify_date != nil && SecTrustSetVerifyDate(trustRef, (__bridge CFDateRef)verify_date) != errSecSuccess) { + CFRelease(trustRef); + return false; + } if (anchors.count > 0 || anchor_only) { CFMutableArrayRef anchorArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); for (id certificate in anchors) { @@ -347,6 +351,8 @@ box_apple_tls_client_t *box_apple_tls_client_create( const char *anchor_pem, size_t anchor_pem_len, bool anchor_only, + bool has_verify_time, + int64_t verify_time_unix_millis, char **error_out ) { box_apple_tls_client_t *client = calloc(1, sizeof(box_apple_tls_client_t)); @@ -363,6 +369,10 @@ box_apple_tls_client_t *box_apple_tls_client_create( NSArray *alpnList = box_split_lines(alpn, alpn_len); NSArray *anchors = box_parse_certificates_from_pem(anchor_pem, anchor_pem_len); + NSDate *verifyDate = nil; + if (has_verify_time) { + verifyDate = [NSDate dateWithTimeIntervalSince1970:(NSTimeInterval)verify_time_unix_millis / 1000.0]; + } nw_parameters_t parameters = nw_parameters_create_secure_tcp(^(nw_protocol_options_t tls_options) { sec_protocol_options_t sec_options = nw_tls_copy_sec_protocol_options(tls_options); if (min_version != 0) { @@ -382,9 +392,9 @@ box_apple_tls_client_t *box_apple_tls_client_create( sec_protocol_options_set_verify_block(sec_options, ^(sec_protocol_metadata_t metadata, sec_trust_t trust, sec_protocol_verify_complete_t complete) { complete(true); }, box_apple_tls_client_queue(client)); - } else if (anchors.count > 0 || anchor_only) { + } else if (verifyDate != nil || anchors.count > 0 || anchor_only) { sec_protocol_options_set_verify_block(sec_options, ^(sec_protocol_metadata_t metadata, sec_trust_t trust, sec_protocol_verify_complete_t complete) { - complete(box_evaluate_trust(trust, anchors, anchor_only)); + complete(box_evaluate_trust(trust, anchors, anchor_only, verifyDate)); }, box_apple_tls_client_queue(client)); } }, NW_PARAMETERS_DEFAULT_CONFIGURATION); diff --git a/constant/tls.go b/constant/tls.go index c81740a49..16cc485ed 100644 --- a/constant/tls.go +++ b/constant/tls.go @@ -5,4 +5,5 @@ const ACMETLS1Protocol = "acme-tls/1" const ( TLSEngineDefault = "" TLSEngineApple = "apple" + TLSEngineCronet = "cronet" ) diff --git a/docs/configuration/shared/http-client.md b/docs/configuration/shared/http-client.md index a0aa9d230..39a455ead 100644 --- a/docs/configuration/shared/http-client.md +++ b/docs/configuration/shared/http-client.md @@ -37,6 +37,7 @@ Values: * `go` (default) * `apple` +* `cronet` `apple` uses NSURLSession, only available on Apple platforms. @@ -73,6 +74,45 @@ Unsupported fields: * `tls.utls` * `tls.reality` +`cronet` uses Cronet, only available when sing-box is built with the `with_naive_outbound` tag. + +Supported fields: + +* `version` +* `disable_version_fallback` (`version = 3` only) +* `headers` +* `tls.server_name` (must match request host) +* `tls.certificate` / `tls.certificate_path` +* `tls.certificate_public_key_sha256` +* `tls.ech.config` / `tls.ech.config_path` / `tls.ech.query_server_name` +* `http2.stream_receive_window` / `http2.connection_receive_window` +* `quic.stream_receive_window` / `quic.connection_receive_window` +* `quic.idle_timeout` +* Dial Fields + +Unsupported fields: + +* `tls.engine` +* `tls.disable_sni` +* `tls.insecure` +* `tls.alpn` +* `tls.min_version` / `tls.max_version` +* `tls.cipher_suites` +* `tls.curve_preferences` +* `tls.client_certificate` / `tls.client_certificate_path` / `tls.client_key` / `tls.client_key_path` +* `tls.fragment` / `tls.record_fragment` +* `tls.kernel_tx` / `tls.kernel_rx` +* `tls.handshake_timeout` +* `tls.utls` +* `tls.reality` +* `http2.idle_timeout` +* `http2.keep_alive_period` +* `http2.max_concurrent_streams` +* `quic.keep_alive_period` +* `quic.initial_packet_size` +* `quic.disable_path_mtu_discovery` +* `quic.max_concurrent_streams` + #### version HTTP version. diff --git a/docs/configuration/shared/http-client.zh.md b/docs/configuration/shared/http-client.zh.md index 5c05968ad..3fbbd2eb1 100644 --- a/docs/configuration/shared/http-client.zh.md +++ b/docs/configuration/shared/http-client.zh.md @@ -37,6 +37,7 @@ icon: material/new-box * `go`(默认) * `apple` +* `cronet` `apple` 使用 NSURLSession,仅在 Apple 平台可用。 @@ -73,6 +74,45 @@ icon: material/new-box * `tls.utls` * `tls.reality` +`cronet` 使用 Cronet,仅在 sing-box 使用 `with_naive_outbound` 标签构建时可用。 + +支持的字段: + +* `version` +* `disable_version_fallback`(仅 `version = 3`) +* `headers` +* `tls.server_name`(必须与请求主机匹配) +* `tls.certificate` / `tls.certificate_path` +* `tls.certificate_public_key_sha256` +* `tls.ech.config` / `tls.ech.config_path` / `tls.ech.query_server_name` +* `http2.stream_receive_window` / `http2.connection_receive_window` +* `quic.stream_receive_window` / `quic.connection_receive_window` +* `quic.idle_timeout` +* 拨号字段 + +不支持的字段: + +* `tls.engine` +* `tls.disable_sni` +* `tls.insecure` +* `tls.alpn` +* `tls.min_version` / `tls.max_version` +* `tls.cipher_suites` +* `tls.curve_preferences` +* `tls.client_certificate` / `tls.client_certificate_path` / `tls.client_key` / `tls.client_key_path` +* `tls.fragment` / `tls.record_fragment` +* `tls.kernel_tx` / `tls.kernel_rx` +* `tls.handshake_timeout` +* `tls.utls` +* `tls.reality` +* `http2.idle_timeout` +* `http2.keep_alive_period` +* `http2.max_concurrent_streams` +* `quic.keep_alive_period` +* `quic.initial_packet_size` +* `quic.disable_path_mtu_discovery` +* `quic.max_concurrent_streams` + #### version HTTP 版本。 diff --git a/go.mod b/go.mod index c88cfe65e..eca2c5b4e 100644 --- a/go.mod +++ b/go.mod @@ -66,6 +66,8 @@ require ( howett.net/plist v1.0.1 ) +replace github.com/sagernet/cronet-go => ../cronet-go + require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/ajg/form v1.5.1 // indirect diff --git a/go.sum b/go.sum index 36986ec4c..8f7a968e2 100644 --- a/go.sum +++ b/go.sum @@ -168,8 +168,6 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= -github.com/sagernet/cronet-go v0.0.0-20260410123506-30af64155529 h1:tG9OIgS2yHlPAV3JdeUxsRB5v/G+SLKJpxqzpp6k8Oo= -github.com/sagernet/cronet-go v0.0.0-20260410123506-30af64155529/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= github.com/sagernet/cronet-go/all v0.0.0-20260410123506-30af64155529 h1:lVtf0GEzHsM7kB8pOgLMXDtiHRdLd9YHJxiFJYA+UtY= github.com/sagernet/cronet-go/all v0.0.0-20260410123506-30af64155529/go.mod h1:9ojC4hR6aYIFjEJYlWDdVsirXLugrOW+ww6n4qoU4rk= github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260410122836-cce5e03076fc h1:kIrWkB7LXP+ff8d93ZgRJxY3CMMDCQ3UpJXXCtCN7g4=