Add Windows TLS engine
This commit is contained in:
@@ -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)),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user