Add Windows TLS engine

This commit is contained in:
nekohasekai
2026-04-24 09:04:27 +08:00
committed by 世界
parent b40e8c5f19
commit 0807b71b14
35 changed files with 6513 additions and 488 deletions
+51 -13
View File
@@ -25,6 +25,8 @@ import (
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/certificate"
"github.com/sagernet/sing-box/common/proxybridge"
boxTLS "github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/option"
@@ -37,6 +39,15 @@ import (
const applePinnedHashSize = sha256.Size
var (
newAppleUserAnchors = certificate.NewAppleAnchors
newAppleProxyBridge = proxybridge.New
newAppleTransportSession = func(shared *appleTransportShared) (unsafe.Pointer, error) {
session, err := shared.newSession()
return unsafe.Pointer(session), err
}
)
func verifyApplePinnedPublicKeySHA256(flatHashes []byte, leafCertificate []byte) error {
if len(flatHashes)%applePinnedHashSize != 0 {
return E.New("invalid pinned public key list")
@@ -64,8 +75,9 @@ type appleSessionConfig struct {
minVersion uint16
maxVersion uint16
insecure bool
anchorPEM string
anchorOnly bool
userAnchors adapter.AppleAnchors
store adapter.CertificateStore
pinnedPublicKeySHA256s []byte
}
@@ -89,7 +101,13 @@ func newAppleTransport(ctx context.Context, logger logger.ContextLogger, rawDial
if err != nil {
return nil, err
}
bridge, err := proxybridge.New(ctx, logger, "apple http proxy", rawDialer)
releaseConfig := true
defer func() {
if releaseConfig {
sessionConfig.close()
}
}()
bridge, err := newAppleProxyBridge(ctx, logger, "apple http proxy", rawDialer)
if err != nil {
return nil, err
}
@@ -100,11 +118,13 @@ func newAppleTransport(ctx context.Context, logger logger.ContextLogger, rawDial
timeFunc: ntp.TimeFuncFromContext(ctx),
}
shared.refs.Store(1)
session, err := shared.newSession()
sessionRef, err := newAppleTransportSession(shared)
if err != nil {
bridge.Close()
return nil, err
}
session := (*C.box_apple_http_session_t)(sessionRef)
releaseConfig = false
return &appleTransport{
shared: shared,
session: session,
@@ -142,7 +162,7 @@ func newAppleSessionConfig(ctx context.Context, options option.HTTPClientOptions
if len(tlsOptions.ALPN) > 0 {
return appleSessionConfig{}, E.New("tls.alpn is unsupported in Apple HTTP engine")
}
validated, err := boxTLS.ValidateAppleTLSOptions(ctx, tlsOptions, "Apple HTTP engine")
validated, err := boxTLS.ValidateSystemTLSOptions(ctx, tlsOptions, "Apple HTTP engine")
if err != nil {
return appleSessionConfig{}, err
}
@@ -152,13 +172,23 @@ func newAppleSessionConfig(ctx context.Context, options option.HTTPClientOptions
minVersion: validated.MinVersion,
maxVersion: validated.MaxVersion,
insecure: tlsOptions.Insecure || len(tlsOptions.CertificatePublicKeySHA256) > 0,
anchorPEM: validated.AnchorPEM,
anchorOnly: validated.AnchorOnly,
anchorOnly: validated.Exclusive,
store: validated.Store,
}
if len(validated.UserPEM) > 0 {
userAnchors, anchorsErr := newAppleUserAnchors(validated.UserPEM)
if anchorsErr != nil {
return appleSessionConfig{}, anchorsErr
}
config.userAnchors = userAnchors
}
if len(tlsOptions.CertificatePublicKeySHA256) > 0 {
config.pinnedPublicKeySHA256s = make([]byte, 0, len(tlsOptions.CertificatePublicKeySHA256)*applePinnedHashSize)
for _, hashValue := range tlsOptions.CertificatePublicKeySHA256 {
if len(hashValue) != applePinnedHashSize {
if config.userAnchors != nil {
config.userAnchors.Release()
}
return appleSessionConfig{}, E.New("invalid certificate_public_key_sha256 length: ", len(hashValue))
}
config.pinnedPublicKeySHA256s = append(config.pinnedPublicKeySHA256s, hashValue...)
@@ -167,12 +197,20 @@ func newAppleSessionConfig(ctx context.Context, options option.HTTPClientOptions
return config, nil
}
func (c *appleSessionConfig) close() {
if c.userAnchors != nil {
c.userAnchors.Release()
c.userAnchors = nil
}
}
func (s *appleTransportShared) retain() {
s.refs.Add(1)
}
func (s *appleTransportShared) release() error {
if s.refs.Add(-1) == 0 {
s.config.close()
return s.bridge.Close()
}
return nil
@@ -185,16 +223,17 @@ func (s *appleTransportShared) newSession() (*C.box_apple_http_session_t, error)
defer C.free(unsafe.Pointer(cProxyUsername))
cProxyPassword := C.CString(s.bridge.Password())
defer C.free(unsafe.Pointer(cProxyPassword))
var cAnchorPEM *C.char
if s.config.anchorPEM != "" {
cAnchorPEM = C.CString(s.config.anchorPEM)
defer C.free(unsafe.Pointer(cAnchorPEM))
}
var pinnedPointer *C.uint8_t
if len(s.config.pinnedPublicKeySHA256s) > 0 {
pinnedPointer = (*C.uint8_t)(C.CBytes(s.config.pinnedPublicKeySHA256s))
defer C.free(unsafe.Pointer(pinnedPointer))
}
anchors := certificate.AcquireAnchors(s.config.userAnchors, s.config.store)
var anchorsRef unsafe.Pointer
if anchors != nil {
anchorsRef = anchors.Ref()
defer anchors.Release()
}
cConfig := C.box_apple_http_session_config_t{
proxy_host: cProxyHost,
proxy_port: C.int(s.bridge.Port()),
@@ -203,8 +242,7 @@ func (s *appleTransportShared) newSession() (*C.box_apple_http_session_t, error)
min_tls_version: C.uint16_t(s.config.minVersion),
max_tls_version: C.uint16_t(s.config.maxVersion),
insecure: C.bool(s.config.insecure),
anchor_pem: cAnchorPEM,
anchor_pem_len: C.size_t(len(s.config.anchorPEM)),
anchors_cf: anchorsRef,
anchor_only: C.bool(s.config.anchorOnly),
pinned_public_key_sha256: pinnedPointer,
pinned_public_key_sha256_len: C.size_t(len(s.config.pinnedPublicKeySHA256s)),
+1 -2
View File
@@ -13,8 +13,7 @@ typedef struct box_apple_http_session_config {
uint16_t min_tls_version;
uint16_t max_tls_version;
bool insecure;
const char *anchor_pem;
size_t anchor_pem_len;
void *anchors_cf;
bool anchor_only;
const uint8_t *pinned_public_key_sha256;
size_t pinned_public_key_sha256_len;
+5 -39
View File
@@ -36,44 +36,6 @@ static void box_set_error_from_nserror(char **error_out, NSError *error) {
box_set_error_string(error_out, error.localizedDescription ?: error.description);
}
static NSArray *box_parse_certificates_from_pem(const char *pem, size_t pem_len) {
if (pem == NULL || pem_len == 0) {
return @[];
}
NSString *content = [[NSString alloc] initWithBytes:pem length:pem_len encoding:NSUTF8StringEncoding];
if (content == nil) {
return @[];
}
NSString *beginMarker = @"-----BEGIN CERTIFICATE-----";
NSString *endMarker = @"-----END CERTIFICATE-----";
NSMutableArray *certificates = [NSMutableArray array];
NSUInteger searchFrom = 0;
while (searchFrom < content.length) {
NSRange beginRange = [content rangeOfString:beginMarker options:0 range:NSMakeRange(searchFrom, content.length - searchFrom)];
if (beginRange.location == NSNotFound) {
break;
}
NSUInteger bodyStart = beginRange.location + beginRange.length;
NSRange endRange = [content rangeOfString:endMarker options:0 range:NSMakeRange(bodyStart, content.length - bodyStart)];
if (endRange.location == NSNotFound) {
break;
}
NSString *base64Section = [content substringWithRange:NSMakeRange(bodyStart, endRange.location - bodyStart)];
NSArray<NSString *> *components = [base64Section componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString *base64Content = [components componentsJoinedByString:@""];
NSData *der = [[NSData alloc] initWithBase64EncodedString:base64Content options:0];
if (der != nil) {
SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)der);
if (certificate != NULL) {
[certificates addObject:(__bridge id)certificate];
CFRelease(certificate);
}
}
searchFrom = endRange.location + endRange.length;
}
return certificates;
}
static bool box_evaluate_trust(SecTrustRef trustRef, NSArray *anchors, bool anchor_only, NSDate *verifyDate) {
if (trustRef == NULL) {
return false;
@@ -249,7 +211,11 @@ box_apple_http_session_t *box_apple_http_session_create(
if (config != NULL) {
delegate.insecure = config->insecure;
delegate.anchorOnly = config->anchor_only;
delegate.anchors = box_parse_certificates_from_pem(config->anchor_pem, config->anchor_pem_len);
if (config->anchors_cf != NULL) {
delegate.anchors = (__bridge NSArray *)config->anchors_cf;
} else {
delegate.anchors = @[];
}
if (config->pinned_public_key_sha256 != NULL && config->pinned_public_key_sha256_len > 0) {
delegate.pinnedPublicKeyHashes = [NSData dataWithBytes:config->pinned_public_key_sha256 length:config->pinned_public_key_sha256_len];
}
@@ -19,13 +19,16 @@ import (
"strings"
"testing"
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/proxybridge"
boxTLS "github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/route"
"github.com/sagernet/sing/common/json/badoption"
commonLogger "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
@@ -58,6 +61,23 @@ type appleHTTPTestServer struct {
publicKeyHash []byte
}
type appleTestAnchors struct {
ref unsafe.Pointer
releases int
}
func (a *appleTestAnchors) Retain() adapter.AppleAnchors {
return a
}
func (a *appleTestAnchors) Release() {
a.releases++
}
func (a *appleTestAnchors) Ref() unsafe.Pointer {
return a.ref
}
func TestNewAppleSessionConfig(t *testing.T) {
serverCertificate, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost")
serverHash := certificatePublicKeySHA256(t, serverCertificate.Certificate[0])
@@ -103,8 +123,14 @@ func TestNewAppleSessionConfig(t *testing.T) {
if !config.anchorOnly {
t.Fatal("expected anchor_only")
}
if !strings.Contains(config.anchorPEM, "BEGIN CERTIFICATE") {
t.Fatalf("unexpected anchor pem: %q", config.anchorPEM)
if config.userAnchors == nil {
t.Fatal("expected user anchors")
}
if config.userAnchors.Ref() == nil {
t.Fatal("expected non-empty user anchors")
}
if config.store != nil {
t.Fatal("unexpected store reference")
}
if len(config.pinnedPublicKeySHA256s) != 0 {
t.Fatalf("unexpected pinned hashes: %d", len(config.pinnedPublicKeySHA256s))
@@ -137,8 +163,8 @@ func TestNewAppleSessionConfig(t *testing.T) {
if !bytes.Equal(config.pinnedPublicKeySHA256s[applePinnedHashSize:], otherHash) {
t.Fatal("unexpected second pin")
}
if config.anchorPEM != "" {
t.Fatalf("unexpected anchor pem: %q", config.anchorPEM)
if config.userAnchors != nil {
t.Fatal("unexpected user anchors")
}
if config.anchorOnly {
t.Fatal("unexpected anchor_only")
@@ -392,6 +418,46 @@ func TestAppleTransportVerifyPublicKeySHA256(t *testing.T) {
}
}
func TestNewAppleTransportClosesSessionConfigOnBridgeFailure(t *testing.T) {
_, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost")
restoreAppleTransportFactories(t)
testAnchors := &appleTestAnchors{ref: unsafe.Pointer(new(int))}
newAppleUserAnchors = func([]byte) (adapter.AppleAnchors, error) {
return testAnchors, nil
}
newAppleProxyBridge = func(context.Context, commonLogger.ContextLogger, string, N.Dialer) (*proxybridge.Bridge, error) {
return nil, errors.New("bridge boom")
}
_, err := newAppleTransport(newAppleHTTPTestContext(), log.NewNOPFactory().NewLogger("httpclient"), &appleHTTPTestDialer{}, appleTransportAnchorOptions(serverCertificatePEM))
if err == nil || !strings.Contains(err.Error(), "bridge boom") {
t.Fatalf("unexpected error: %v", err)
}
if testAnchors.releases != 1 {
t.Fatalf("expected 1 anchor release, got %d", testAnchors.releases)
}
}
func TestNewAppleTransportClosesSessionConfigOnSessionFailure(t *testing.T) {
_, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost")
restoreAppleTransportFactories(t)
testAnchors := &appleTestAnchors{ref: unsafe.Pointer(new(int))}
newAppleUserAnchors = func([]byte) (adapter.AppleAnchors, error) {
return testAnchors, nil
}
newAppleTransportSession = func(*appleTransportShared) (unsafe.Pointer, error) {
return nil, errors.New("session boom")
}
_, err := newAppleTransport(newAppleHTTPTestContext(), log.NewNOPFactory().NewLogger("httpclient"), &appleHTTPTestDialer{}, appleTransportAnchorOptions(serverCertificatePEM))
if err == nil || !strings.Contains(err.Error(), "session boom") {
t.Fatalf("unexpected error: %v", err)
}
if testAnchors.releases != 1 {
t.Fatalf("expected 1 anchor release, got %d", testAnchors.releases)
}
}
func TestAppleTransportRoundTripHTTPS(t *testing.T) {
requests := make(chan appleHTTPObservedRequest, 1)
server := startAppleHTTPTestServer(t, func(w http.ResponseWriter, r *http.Request) {
@@ -665,7 +731,8 @@ func TestAppleTransportLifecycle(t *testing.T) {
assertAppleHTTPSucceeds(t, transport, server.URL("/reset"))
innerTransport := transport.(*appleTransport)
if err := innerTransport.Close(); err != nil {
err := innerTransport.Close()
if err != nil {
t.Fatal(err)
}
@@ -722,10 +789,7 @@ func (s *appleHTTPTestServer) URL(path string) string {
func newAppleHTTPTestTransport(t *testing.T, server *appleHTTPTestServer, options option.HTTPClientOptions) innerTransport {
t.Helper()
ctx := service.ContextWith[adapter.ConnectionManager](
context.Background(),
route.NewConnectionManager(log.NewNOPFactory().NewLogger("connection")),
)
ctx := newAppleHTTPTestContext()
dialer := &appleHTTPTestDialer{
hostMap: make(map[string]string),
}
@@ -743,6 +807,39 @@ func newAppleHTTPTestTransport(t *testing.T, server *appleHTTPTestServer, option
return transport
}
func newAppleHTTPTestContext() context.Context {
return service.ContextWith[adapter.ConnectionManager](
context.Background(),
route.NewConnectionManager(log.NewNOPFactory().NewLogger("connection")),
)
}
func appleTransportAnchorOptions(certificatePEM string) option.HTTPClientOptions {
return option.HTTPClientOptions{
Version: 2,
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
TLS: &option.OutboundTLSOptions{
Enabled: true,
ServerName: "localhost",
MinVersion: "1.2",
Certificate: badoption.Listable[string]{certificatePEM},
},
},
}
}
func restoreAppleTransportFactories(t *testing.T) {
t.Helper()
oldAnchors := newAppleUserAnchors
oldBridge := newAppleProxyBridge
oldSession := newAppleTransportSession
t.Cleanup(func() {
newAppleUserAnchors = oldAnchors
newAppleProxyBridge = oldBridge
newAppleTransportSession = oldSession
})
}
func (d *appleHTTPTestDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
host := destination.AddrString()
if destination.IsDomain() {
+1 -1
View File
@@ -50,7 +50,7 @@ func NewTransport(ctx context.Context, logger logger.ContextLogger, tag string,
}
managedTransport.epoch.Store(&transportEpoch{transport: inner})
return managedTransport, nil
case C.TLSEngineDefault, "go":
case "", C.TLSEngineGo:
cheapRebuild = true
default:
return nil, E.New("unknown HTTP engine: ", options.Engine)