mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-15 05:08:33 +10:00
draft: cronet HTTP client & Accept ntp time
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
17
common/httpclient/cronet_transport_stub.go
Normal file
17
common/httpclient/cronet_transport_stub.go
Normal file
@@ -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")
|
||||
}
|
||||
249
common/httpclient/cronet_transport_supported.go
Normal file
249
common/httpclient/cronet_transport_supported.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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<NSString *> *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);
|
||||
|
||||
@@ -5,4 +5,5 @@ const ACMETLS1Protocol = "acme-tls/1"
|
||||
const (
|
||||
TLSEngineDefault = ""
|
||||
TLSEngineApple = "apple"
|
||||
TLSEngineCronet = "cronet"
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 版本。
|
||||
|
||||
2
go.mod
2
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
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
Reference in New Issue
Block a user