Add Windows TLS engine

This commit is contained in:
nekohasekai 2026-04-24 09:04:27 +08:00 committed by 世界
parent 3d3c8a2fe9
commit 1610c18a2f
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
35 changed files with 6513 additions and 488 deletions

View file

@ -10,6 +10,7 @@ import (
type CertificateStore interface {
LifecycleService
Pool() *x509.CertPool
ExclusiveAnchors() bool
}
func RootPoolFromContext(ctx context.Context) *x509.CertPool {

View file

@ -0,0 +1,17 @@
//go:build darwin && cgo
package adapter
import "unsafe"
type AppleAnchors interface {
Retain() AppleAnchors
Release()
// Ref returns the underlying CFArrayRef, or nil if the anchor set is empty.
Ref() unsafe.Pointer
}
type AppleCertificateStore interface {
CertificateStore
AppleAnchors() AppleAnchors
}

View file

@ -0,0 +1,17 @@
#ifndef BOX_CERTIFICATE_ANCHORS_DARWIN_H
#define BOX_CERTIFICATE_ANCHORS_DARWIN_H
#include <stddef.h>
#include <stdint.h>
// box_certificate_anchors_from_der wraps an array of DER-encoded certificate
// blobs into a retained CFArrayRef of SecCertificateRef, returned as an opaque
// pointer. The caller owns the returned reference and must call
// box_certificate_release_anchors. Returns NULL when no blobs were accepted.
void *box_certificate_anchors_from_der(const uint8_t *const *ders, const size_t *lens, size_t count);
// box_certificate_release_anchors drops one reference from a CFArray handle
// previously returned by box_certificate_anchors_from_der. No-op on NULL.
void box_certificate_release_anchors(void *anchors);
#endif

View file

@ -0,0 +1,42 @@
#import "anchors_darwin.h"
#import <Foundation/Foundation.h>
#import <Security/Security.h>
void *box_certificate_anchors_from_der(const uint8_t *const *ders, const size_t *lens, size_t count) {
if (count == 0 || ders == NULL || lens == NULL) {
return NULL;
}
CFMutableArrayRef certificates = CFArrayCreateMutable(NULL, (CFIndex)count, &kCFTypeArrayCallBacks);
if (certificates == NULL) {
return NULL;
}
for (size_t index = 0; index < count; index++) {
if (ders[index] == NULL || lens[index] == 0) {
continue;
}
CFDataRef data = CFDataCreate(NULL, ders[index], (CFIndex)lens[index]);
if (data == NULL) {
continue;
}
SecCertificateRef certificate = SecCertificateCreateWithData(NULL, data);
CFRelease(data);
if (certificate == NULL) {
continue;
}
CFArrayAppendValue(certificates, certificate);
CFRelease(certificate);
}
if (CFArrayGetCount(certificates) == 0) {
CFRelease(certificates);
return NULL;
}
return certificates;
}
void box_certificate_release_anchors(void *anchors) {
if (anchors == NULL) {
return;
}
CFRelease((CFTypeRef)anchors);
}

View file

@ -1,6 +1,7 @@
package certificate
import (
"bytes"
"context"
"crypto/x509"
"io/fs"
@ -25,11 +26,11 @@ type Store struct {
storeType string
systemPool *x509.CertPool
currentPool *x509.CertPool
currentPEM []string
certificate string
certificatePaths []string
certificateDirectoryPaths []string
watcher *fswatch.Watcher
platform storePlatform
}
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
@ -114,10 +115,18 @@ func (s *Store) Start(stage adapter.StartStage) error {
}
func (s *Store) Close() error {
if s.watcher != nil {
return s.watcher.Close()
watcher := s.watcher
s.watcher = nil
var closeErr error
if watcher != nil {
closeErr = watcher.Close()
}
return nil
platformErr := s.closePlatform()
if platformErr != nil {
closeErr = platformErr
}
return closeErr
}
func (s *Store) Pool() *x509.CertPool {
@ -130,37 +139,35 @@ func (s *Store) StoreKind() string {
return s.storeType
}
func (s *Store) CurrentPEM() []string {
s.access.RLock()
defer s.access.RUnlock()
return append([]string(nil), s.currentPEM...)
func (s *Store) ExclusiveAnchors() bool {
return s.storeType != C.CertificateStoreSystem
}
func (s *Store) update() error {
currentPool, err := s.newBasePool()
var currentPEM []string
if err != nil {
return err
}
pemBuffer := new(bytes.Buffer)
switch s.storeType {
case C.CertificateStoreMozilla:
pemContent := mozillaIncludedPEM()
if !currentPool.AppendCertsFromPEM([]byte(pemContent)) {
return E.New("invalid Mozilla included certificate PEM")
}
currentPEM = append(currentPEM, pemContent)
appendPEMBlock(pemBuffer, string(pemContent))
case C.CertificateStoreChrome:
pemContent := chromeIncludedPEM()
if !currentPool.AppendCertsFromPEM([]byte(pemContent)) {
return E.New("invalid Chrome included certificate PEM")
}
currentPEM = append(currentPEM, pemContent)
appendPEMBlock(pemBuffer, string(pemContent))
}
if s.certificate != "" {
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
return E.New("invalid certificate PEM strings")
}
currentPEM = append(currentPEM, s.certificate)
appendPEMBlock(pemBuffer, s.certificate)
}
for _, path := range s.certificatePaths {
pemContent, err := os.ReadFile(path)
@ -170,7 +177,7 @@ func (s *Store) update() error {
if !currentPool.AppendCertsFromPEM(pemContent) {
return E.New("invalid certificate PEM file: ", path)
}
currentPEM = append(currentPEM, string(pemContent))
appendPEMBlock(pemBuffer, string(pemContent))
}
var firstErr error
for _, directoryPath := range s.certificateDirectoryPaths {
@ -184,7 +191,7 @@ func (s *Store) update() error {
for _, directoryEntry := range directoryEntries {
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
if err == nil && currentPool.AppendCertsFromPEM(pemContent) {
currentPEM = append(currentPEM, string(pemContent))
appendPEMBlock(pemBuffer, string(pemContent))
}
}
}
@ -194,8 +201,15 @@ func (s *Store) update() error {
s.access.Lock()
defer s.access.Unlock()
s.currentPool = currentPool
s.currentPEM = currentPEM
return nil
return s.updatePlatformLocked(pemBuffer.Bytes())
}
func appendPEMBlock(buffer *bytes.Buffer, block string) {
existing := buffer.Bytes()
if len(existing) > 0 && existing[len(existing)-1] != '\n' {
buffer.WriteByte('\n')
}
buffer.WriteString(block)
}
func (s *Store) newBasePool() (*x509.CertPool, error) {

View file

@ -0,0 +1,167 @@
//go:build darwin && cgo
package certificate
/*
#cgo CFLAGS: -x objective-c -fobjc-arc
#cgo LDFLAGS: -framework Foundation -framework Security
#include <stdlib.h>
#include "anchors_darwin.h"
*/
import "C"
import (
"crypto/sha256"
"encoding/pem"
"runtime"
"sync/atomic"
"unsafe"
"github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions"
)
var (
_ adapter.AppleCertificateStore = (*Store)(nil)
_ adapter.AppleAnchors = (*appleAnchors)(nil)
)
type storePlatform struct {
anchors *appleAnchors
hash [sha256.Size]byte
}
type appleAnchors struct {
cfArray unsafe.Pointer
refs atomic.Int32
}
func newAppleAnchors(pemBytes []byte) (*appleAnchors, error) {
anchors := &appleAnchors{}
anchors.refs.Store(1)
if len(pemBytes) == 0 {
return anchors, nil
}
derBlocks := decodeCertificatePEM(pemBytes)
if len(derBlocks) == 0 {
return nil, E.New("parse certificate PEM")
}
pointerSize := C.size_t(unsafe.Sizeof((*C.uint8_t)(nil)))
lenSize := C.size_t(unsafe.Sizeof(C.size_t(0)))
pointersC := (**C.uint8_t)(C.malloc(pointerSize * C.size_t(len(derBlocks))))
defer C.free(unsafe.Pointer(pointersC))
lensC := (*C.size_t)(C.malloc(lenSize * C.size_t(len(derBlocks))))
defer C.free(unsafe.Pointer(lensC))
pointersSlice := unsafe.Slice(pointersC, len(derBlocks))
lensSlice := unsafe.Slice(lensC, len(derBlocks))
var pinner runtime.Pinner
defer pinner.Unpin()
for index, der := range derBlocks {
pinner.Pin(&der[0])
pointersSlice[index] = (*C.uint8_t)(unsafe.Pointer(&der[0]))
lensSlice[index] = C.size_t(len(der))
}
cfArray := C.box_certificate_anchors_from_der(pointersC, lensC, C.size_t(len(derBlocks)))
if cfArray == nil {
return nil, E.New("parse certificate PEM")
}
anchors.cfArray = cfArray
return anchors, nil
}
// NewAppleAnchors parses the given PEM and returns a ref-counted handle
// wrapping a CFArray of SecCertificateRef. The caller owns the returned
// reference and must call Release when finished. Returns an error when
// pemBytes is non-empty but contains no usable CERTIFICATE blocks.
func NewAppleAnchors(pemBytes []byte) (adapter.AppleAnchors, error) {
return newAppleAnchors(pemBytes)
}
// AcquireAnchors returns a retained AppleAnchors handle, preferring the
// per-config userAnchors over the process-wide certificate store. Returns
// nil when neither source is available. Callers must Release the handle.
func AcquireAnchors(userAnchors adapter.AppleAnchors, store adapter.CertificateStore) adapter.AppleAnchors {
if userAnchors != nil {
return userAnchors.Retain()
}
if store == nil {
return nil
}
apple, loaded := store.(adapter.AppleCertificateStore)
if !loaded {
return nil
}
return apple.AppleAnchors()
}
func (a *appleAnchors) Retain() adapter.AppleAnchors {
a.refs.Add(1)
return a
}
func (a *appleAnchors) Release() {
if a.refs.Add(-1) != 0 {
return
}
if a.cfArray != nil {
C.box_certificate_release_anchors(a.cfArray)
}
}
func (a *appleAnchors) Ref() unsafe.Pointer {
return a.cfArray
}
func (s *Store) AppleAnchors() adapter.AppleAnchors {
s.access.RLock()
defer s.access.RUnlock()
if s.platform.anchors == nil {
return nil
}
return s.platform.anchors.Retain()
}
func (s *Store) updatePlatformLocked(pemBytes []byte) error {
hash := sha256.Sum256(pemBytes)
if s.platform.anchors != nil && s.platform.hash == hash {
return nil
}
newAnchors, err := newAppleAnchors(pemBytes)
if err != nil {
return err
}
old := s.platform.anchors
s.platform.anchors = newAnchors
s.platform.hash = hash
if old != nil {
old.Release()
}
return nil
}
func (s *Store) closePlatform() error {
s.access.Lock()
defer s.access.Unlock()
if s.platform.anchors != nil {
s.platform.anchors.Release()
s.platform.anchors = nil
}
return nil
}
func decodeCertificatePEM(pemBytes []byte) [][]byte {
var blocks [][]byte
rest := pemBytes
for {
block, next := pem.Decode(rest)
if block == nil {
break
}
if block.Type == "CERTIFICATE" && len(block.Bytes) > 0 {
blocks = append(blocks, block.Bytes)
}
rest = next
}
return blocks
}

View file

@ -0,0 +1,13 @@
//go:build !(darwin && cgo)
package certificate
type storePlatform struct{}
func (s *Store) updatePlatformLocked(_ []byte) error {
return nil
}
func (s *Store) closePlatform() error {
return nil
}

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)),

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;

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];
}

View file

@ -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() {

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)

5
common/schannel/doc.go Normal file
View file

@ -0,0 +1,5 @@
// Package schannel wraps the Windows Schannel security provider (SSPI) for
// client-side TLS. The public API is implemented on Windows only; on other
// platforms the package is empty and intended for transitive imports from
// build-tagged callers.
package schannel

View file

@ -0,0 +1,719 @@
package schannel
import (
"bytes"
"crypto/tls"
"encoding/binary"
"os"
"sync"
"syscall"
"unsafe"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/sys/windows"
)
const clientCredentialFlags = schCredManualCredValidation | schCredNoDefaultCreds | schUseStrongCrypto
var versionCheck = sync.OnceValue(func() error {
major, _, build := windows.RtlGetNtVersionNumbers()
build &= 0xffff
if major < 10 || (major == 10 && build < 17763) {
return E.New("Windows TLS engine requires Windows build 17763 or later (Windows 10 version 1809, Windows Server 2019, or newer)")
}
return nil
})
// CheckPlatform returns an error when the running Windows version does not
// support the SCH_CREDENTIALS structure used by this package.
func CheckPlatform() error {
return versionCheck()
}
type clientCredentialKey struct {
disabledProtocols uint32
flags uint32
}
type clientCredential struct {
key clientCredentialKey
once sync.Once
handle secHandle
tlsParams tlsParameters
err error
}
var clientCredentialCache sync.Map
func cachedClientCredential(minVersion, maxVersion uint16) (*clientCredential, error) {
key := clientCredentialKey{
disabledProtocols: disabledProtocolsMask(minVersion, maxVersion),
flags: clientCredentialFlags,
}
actual, _ := clientCredentialCache.LoadOrStore(key, &clientCredential{key: key})
credential := actual.(*clientCredential)
credential.once.Do(func() {
credential.err = credential.acquire()
})
if credential.err != nil {
clientCredentialCache.Delete(key)
return nil, credential.err
}
return credential, nil
}
func (c *clientCredential) acquire() error {
c.tlsParams.grbitDisabledProtocols = c.key.disabledProtocols
sch := schCredentials{
dwVersion: schCredentialsVersion,
dwFlags: c.key.flags,
cTlsParameters: 1,
pTlsParameters: &c.tlsParams,
}
pkg, err := windows.UTF16PtrFromString(unispNameW)
if err != nil {
return err
}
var expiry windows.Filetime
status := sspiAcquireCredentialsHandle(
nil,
pkg,
secPkgCredOutbound,
nil,
unsafe.Pointer(&sch),
0,
0,
&c.handle,
&expiry,
)
if status != secEOK {
return sspiError("AcquireCredentialsHandle", status)
}
return nil
}
// ClientContext owns the per-connection Schannel security context and drives
// it through handshake and application-data phases.
type ClientContext struct {
credential *clientCredential
handle secHandle
targetName *uint16
// alpnBuffer is the SEC_APPLICATION_PROTOCOLS blob; kept alive for the
// duration of the first handshake call.
alpnBuffer []byte
firstCall bool
valid bool
}
// NewClientContext allocates a new client context, reuses the Schannel
// credential handle for the supplied TLS version bounds, and advertises ALPN
// protocols through an SECBUFFER_APPLICATION_PROTOCOLS buffer on the first
// handshake call.
func NewClientContext(minVersion, maxVersion uint16, serverName string, alpn []string) (*ClientContext, error) {
if minVersion != 0 && maxVersion != 0 && minVersion > maxVersion {
return nil, os.ErrInvalid
}
err := CheckPlatform()
if err != nil {
return nil, err
}
targetName, err := windows.UTF16PtrFromString(serverName)
if err != nil {
return nil, err
}
credential, err := cachedClientCredential(minVersion, maxVersion)
if err != nil {
return nil, err
}
c := &ClientContext{
credential: credential,
targetName: targetName,
firstCall: true,
}
if len(alpn) > 0 {
c.alpnBuffer, err = encodeAlpnBuffer(alpn)
if err != nil {
return nil, err
}
}
return c, nil
}
// Close releases the per-connection security context. Safe to call multiple
// times.
func (c *ClientContext) Close() {
if c == nil {
return
}
if c.valid {
sspiDeleteSecurityContext(&c.handle)
c.valid = false
c.handle = secHandle{}
}
}
type StepResult struct {
// Output must be written to the peer verbatim before the next Step call.
// When Done is true, leftover input[Consumed:] is the first application
// ciphertext — not more handshake bytes.
Output []byte
Consumed int
Done bool
Incomplete bool
}
// Step drives one handshake iteration. Input may be nil on the first call.
// Callers must write Output to the peer, append more peer bytes when
// Incomplete is true, and loop until Done is true.
func (c *ClientContext) Step(input []byte) (StepResult, error) {
var inputDesc *secBufferDesc
var inputBufs [2]secBuffer
if c.firstCall {
if len(c.alpnBuffer) > 0 {
inputBufs[0].bufferType = secbufferApplicationProtocols
inputBufs[0].cbBuffer = uint32(len(c.alpnBuffer))
inputBufs[0].pvBuffer = &c.alpnBuffer[0]
inputDesc = &secBufferDesc{
ulVersion: secbufferVersion,
cBuffers: 1,
pBuffers: &inputBufs[0],
}
}
} else {
if len(input) == 0 {
return StepResult{}, E.New("schannel: empty handshake input after first step")
}
inputBufs[0].bufferType = secbufferToken
inputBufs[0].cbBuffer = uint32(len(input))
inputBufs[0].pvBuffer = &input[0]
inputBufs[1].bufferType = secbufferEmpty
inputDesc = &secBufferDesc{
ulVersion: secbufferVersion,
cBuffers: 2,
pBuffers: &inputBufs[0],
}
}
result, terminal, err := c.runInitializeSecurityContext(inputDesc, "InitializeSecurityContext")
if err != nil || terminal {
return result, err
}
switch {
case c.firstCall:
result.Consumed = 0
case inputBufs[1].bufferType == secbufferExtra && inputBufs[1].cbBuffer > 0:
consumed, extraErr := consumedFromExtra(&inputBufs[1], len(input))
if extraErr != nil {
return result, extraErr
}
result.Consumed = consumed
default:
result.Consumed = len(input)
}
c.firstCall = false
c.alpnBuffer = nil
return result, nil
}
// StreamSizes must be called after Step returns Done=true.
func (c *ClientContext) StreamSizes() (header, trailer, maxMessage uint32, err error) {
var sizes secPkgContextStreamSizes
status := sspiQueryContextAttributes(&c.handle, secpkgAttrStreamSizes, unsafe.Pointer(&sizes))
if status != secEOK {
return 0, 0, 0, sspiError("QueryContextAttributes(stream sizes)", status)
}
return sizes.cbHeader, sizes.cbTrailer, sizes.cbMaximumMessage, nil
}
// Encrypt wraps a plaintext chunk into a TLS record using the supplied
// backing buffer which must have room for header + plaintext + trailer bytes.
// Plaintext is copied into buffer starting at `header` offset before calling
// EncryptMessage. Returns the encrypted record as a slice into buffer.
func (c *ClientContext) Encrypt(header, trailer uint32, plaintext []byte, buffer []byte) ([]byte, error) {
if len(buffer) < int(header)+len(plaintext)+int(trailer) {
return nil, E.New("schannel: encrypt buffer too small")
}
copy(buffer[header:], plaintext)
headerPtr := &buffer[0]
dataPtr := &buffer[header]
trailerPtr := &buffer[int(header)+len(plaintext)]
bufs := [4]secBuffer{
{cbBuffer: header, bufferType: secbufferStreamHeader, pvBuffer: headerPtr},
{cbBuffer: uint32(len(plaintext)), bufferType: secbufferData, pvBuffer: dataPtr},
{cbBuffer: trailer, bufferType: secbufferStreamTrailer, pvBuffer: trailerPtr},
{bufferType: secbufferEmpty},
}
desc := secBufferDesc{
ulVersion: secbufferVersion,
cBuffers: 4,
pBuffers: &bufs[0],
}
status := sspiEncryptMessage(&c.handle, 0, &desc, 0)
if status != secEOK {
return nil, sspiError("EncryptMessage", status)
}
total := int(bufs[0].cbBuffer + bufs[1].cbBuffer + bufs[2].cbBuffer)
return buffer[:total], nil
}
type DecryptResult struct {
// Plaintext aliases memory inside the input buffer passed to Decrypt;
// callers must copy before the next Decrypt call reuses that buffer.
Plaintext []byte
// ConsumedTotal is the number of input bytes Schannel consumed, i.e.
// input[ConsumedTotal:] are unprocessed leftover ciphertext.
ConsumedTotal int
// RenegotiateToken aliases the post-handshake token that must be fed back
// through InitializeSecurityContext after SEC_I_RENEGOTIATE.
RenegotiateToken []byte
Incomplete bool
Renegotiate bool
Expired bool
}
// Decrypt processes a chunk of TLS ciphertext in-place. The returned Plaintext
// aliases memory inside input until the next Decrypt call; callers must copy
// the bytes they want to keep.
func (c *ClientContext) Decrypt(input []byte) (DecryptResult, error) {
var result DecryptResult
if len(input) == 0 {
result.Incomplete = true
return result, nil
}
bufs := [4]secBuffer{
{cbBuffer: uint32(len(input)), bufferType: secbufferData, pvBuffer: &input[0]},
{bufferType: secbufferEmpty},
{bufferType: secbufferEmpty},
{bufferType: secbufferEmpty},
}
desc := secBufferDesc{
ulVersion: secbufferVersion,
cBuffers: 4,
pBuffers: &bufs[0],
}
status := sspiDecryptMessage(&c.handle, &desc, 0, nil)
switch status {
case secEOK:
case secEIncompleteMessage:
result.Incomplete = true
return result, nil
case secIContextExpired:
result.Expired = true
return result, nil
case secIRenegotiate:
result.Renegotiate = true
default:
return result, sspiError("DecryptMessage", status)
}
return parseDecryptResult(input, bufs[:], result.Renegotiate)
}
// PostHandshake processes a TLS 1.3 post-handshake message
// (NewSessionTicket, KeyUpdate) after DecryptMessage returned
// SEC_I_RENEGOTIATE. Pass the token preserved from Decrypt on the first call;
// pass more peer bytes on subsequent calls when Incomplete.
func (c *ClientContext) PostHandshake(input []byte) (StepResult, error) {
var inputDesc *secBufferDesc
var inputBufs [2]secBuffer
if len(input) > 0 {
inputBufs[0].bufferType = secbufferToken
inputBufs[0].cbBuffer = uint32(len(input))
inputBufs[0].pvBuffer = &input[0]
inputBufs[1].bufferType = secbufferEmpty
inputDesc = &secBufferDesc{
ulVersion: secbufferVersion,
cBuffers: 2,
pBuffers: &inputBufs[0],
}
}
result, terminal, err := c.runInitializeSecurityContext(inputDesc, "InitializeSecurityContext(post-handshake)")
if err != nil || terminal {
return result, err
}
if len(input) > 0 && inputBufs[1].bufferType == secbufferExtra && inputBufs[1].cbBuffer > 0 {
consumed, extraErr := consumedFromExtra(&inputBufs[1], len(input))
if extraErr != nil {
return result, extraErr
}
result.Consumed = consumed
} else {
result.Consumed = len(input)
}
return result, nil
}
func parseDecryptResult(input []byte, bufs []secBuffer, renegotiate bool) (DecryptResult, error) {
var result DecryptResult
var dataBuffer, extraBuffer *secBuffer
for index := range bufs {
switch bufs[index].bufferType {
case secbufferData:
dataBuffer = &bufs[index]
case secbufferExtra:
extraBuffer = &bufs[index]
}
}
if dataBuffer != nil && dataBuffer.cbBuffer > 0 && dataBuffer.pvBuffer != nil {
result.Plaintext = unsafe.Slice(dataBuffer.pvBuffer, int(dataBuffer.cbBuffer))
}
if extraBuffer != nil && extraBuffer.cbBuffer > 0 {
consumed, err := consumedFromExtra(extraBuffer, len(input))
if err != nil {
return result, err
}
result.ConsumedTotal = consumed
} else {
result.ConsumedTotal = len(input)
}
if renegotiate {
result.Renegotiate = true
if extraBuffer != nil && extraBuffer.cbBuffer > 0 {
result.RenegotiateToken = input[result.ConsumedTotal:]
} else {
result.RenegotiateToken = input
}
}
return result, nil
}
// ApplicationProtocol returns the empty string when ALPN was not negotiated.
func (c *ClientContext) ApplicationProtocol() (string, error) {
var info secPkgContextApplicationProtocol
status := sspiQueryContextAttributes(&c.handle, secpkgAttrApplicationProtocol, unsafe.Pointer(&info))
if status != secEOK {
return "", sspiError("QueryContextAttributes(application protocol)", status)
}
if info.protoNegoStatus != secApplicationProtocolNegotiationStatusSuccess {
return "", nil
}
size := int(info.protocolIDSize)
if size > len(info.protocolID) {
return "", E.New("schannel: invalid ALPN protocol size")
}
return string(info.protocolID[:size]), nil
}
// ConnectionInfo reports the negotiated TLS version and cipher suite.
// cipherSuite may be zero when the Windows build does not return a
// mappable cipher name.
func (c *ClientContext) ConnectionInfo() (version, cipherSuite uint16, err error) {
var info secPkgContextConnectionInfo
status := sspiQueryContextAttributes(&c.handle, secpkgAttrConnectionInfo, unsafe.Pointer(&info))
if status != secEOK {
return 0, 0, sspiError("QueryContextAttributes(connection info)", status)
}
version = sspProtocolToTLSVersion(info.dwProtocol)
var cipherInfo secPkgContextCipherInfo
cipherInfo.dwVersion = 1
status = sspiQueryContextAttributes(&c.handle, secpkgAttrCipherInfo, unsafe.Pointer(&cipherInfo))
if status == secEOK {
cipherSuite = cipherSuiteID(windows.UTF16ToString(cipherInfo.szCipherSuite[:]))
}
return version, cipherSuite, nil
}
func cipherSuiteID(name string) uint16 {
for _, suite := range tls.CipherSuites() {
if suite.Name == name {
return suite.ID
}
}
for _, suite := range tls.InsecureCipherSuites() {
if suite.Name == name {
return suite.ID
}
}
return 0
}
// RemoteCertificateChain returns freshly allocated DER bytes ordered
// leaf → intermediates.
func (c *ClientContext) RemoteCertificateChain() ([][]byte, error) {
var leaf *windows.CertContext
status := sspiQueryContextAttributes(&c.handle, secpkgAttrRemoteCertContext, unsafe.Pointer(&leaf))
if status != secEOK {
return nil, sspiError("QueryContextAttributes(remote cert context)", status)
}
if leaf == nil {
return nil, nil
}
defer windows.CertFreeCertificateContext(leaf)
chain, err := buildCertChainDER(leaf)
if err != nil {
return [][]byte{certContextDER(leaf)}, nil
}
return chain, nil
}
const handshakeContextReq = iscReqSequenceDetect |
iscReqReplayDetect |
iscReqConfidentiality |
iscReqAllocateMemory |
iscReqStream |
iscReqUseSuppliedCreds |
iscReqManualCredValidation |
iscReqExtendedError
// runInitializeSecurityContext returns terminal=true when the result is
// final (error or more-data-needed), signalling that the caller must skip
// extra-buffer post-processing.
func (c *ClientContext) runInitializeSecurityContext(inputDesc *secBufferDesc, opLabel string) (StepResult, bool, error) {
var outputBufs [1]secBuffer
outputBufs[0].bufferType = secbufferToken
outputDesc := secBufferDesc{
ulVersion: secbufferVersion,
cBuffers: 1,
pBuffers: &outputBufs[0],
}
var ctxIn *secHandle
if c.valid {
ctxIn = &c.handle
}
var contextAttr uint32
var expiry windows.Filetime
status := sspiInitializeSecurityContext(
&c.credential.handle,
ctxIn,
c.targetName,
handshakeContextReq,
0,
0,
inputDesc,
0,
&c.handle,
&outputDesc,
&contextAttr,
&expiry,
)
switch status {
case secEOK, secICompleteNeeded, secICompleteAndContinue, secIContinueNeeded:
c.valid = true
}
if status == secICompleteNeeded || status == secICompleteAndContinue {
completeStatus := sspiCompleteAuthToken(&c.handle, &outputDesc)
if completeStatus != secEOK {
if outputBufs[0].pvBuffer != nil {
sspiFreeContextBuffer(outputBufs[0].pvBuffer)
}
return StepResult{}, true, sspiError("CompleteAuthToken", completeStatus)
}
}
var result StepResult
if outputBufs[0].cbBuffer > 0 && outputBufs[0].pvBuffer != nil {
result.Output = unsafeSliceCopy(outputBufs[0].pvBuffer, int(outputBufs[0].cbBuffer))
sspiFreeContextBuffer(outputBufs[0].pvBuffer)
}
switch status {
case secEOK, secICompleteNeeded:
result.Done = true
return result, false, nil
case secIContinueNeeded, secICompleteAndContinue:
return result, false, nil
case secEIncompleteMessage:
c.valid = true
result.Incomplete = true
return result, true, nil
default:
return result, true, sspiError(opLabel, status)
}
}
func consumedFromExtra(extraBuf *secBuffer, inputLen int) (int, error) {
extraLen := int(extraBuf.cbBuffer)
if extraLen > inputLen {
return 0, E.New("schannel: SECBUFFER_EXTRA exceeds input length")
}
return inputLen - extraLen, nil
}
func disabledProtocolsMask(minVersion, maxVersion uint16) uint32 {
allowed := uint32(0)
versions := []struct {
id uint16
mask uint32
}{
{tls.VersionTLS10, spProtTLS10Client},
{tls.VersionTLS11, spProtTLS11Client},
{tls.VersionTLS12, spProtTLS12Client},
{tls.VersionTLS13, spProtTLS13Client},
}
effectiveMin := minVersion
if effectiveMin == 0 {
effectiveMin = tls.VersionTLS12
if maxVersion != 0 && maxVersion < tls.VersionTLS12 {
effectiveMin = versions[0].id
}
}
effectiveMax := maxVersion
if effectiveMax == 0 {
effectiveMax = tls.VersionTLS13
}
for _, v := range versions {
if v.id >= effectiveMin && v.id <= effectiveMax {
allowed |= v.mask
}
}
if allowed == 0 {
return 0
}
return spProtAllTLSClients &^ allowed
}
func sspProtocolToTLSVersion(sp uint32) uint16 {
switch {
case sp&spProtTLS13Client != 0:
return tls.VersionTLS13
case sp&spProtTLS12Client != 0:
return tls.VersionTLS12
case sp&spProtTLS11Client != 0:
return tls.VersionTLS11
case sp&spProtTLS10Client != 0:
return tls.VersionTLS10
}
return 0
}
func encodeAlpnBuffer(protocols []string) ([]byte, error) {
var protoList []byte
for _, proto := range protocols {
if len(proto) == 0 || len(proto) > 255 {
return nil, E.New("schannel: invalid ALPN protocol: ", proto)
}
protoList = append(protoList, byte(len(proto)))
protoList = append(protoList, []byte(proto)...)
}
if len(protoList) > 0xFFFF {
return nil, E.New("schannel: ALPN list too long")
}
// Layout:
// uint32 ProtocolListsSize
// uint32 ProtoNegoExt
// uint16 ProtocolListSize
// bytes ProtocolList
inner := 4 + 2 + len(protoList)
buffer := make([]byte, 4+inner)
binary.LittleEndian.PutUint32(buffer[0:4], uint32(inner))
binary.LittleEndian.PutUint32(buffer[4:8], secApplicationProtocolNegotiationExtALPN)
binary.LittleEndian.PutUint16(buffer[8:10], uint16(len(protoList)))
copy(buffer[10:], protoList)
return buffer, nil
}
func unsafeSliceCopy(ptr *byte, size int) []byte {
if ptr == nil || size <= 0 {
return nil
}
out := make([]byte, size)
copy(out, unsafe.Slice(ptr, size))
return out
}
func certContextDER(ctx *windows.CertContext) []byte {
if ctx == nil || ctx.EncodedCert == nil || ctx.Length == 0 {
return nil
}
out := make([]byte, ctx.Length)
copy(out, unsafe.Slice(ctx.EncodedCert, int(ctx.Length)))
return out
}
func buildCertChainDER(leaf *windows.CertContext) ([][]byte, error) {
var chainPara windows.CertChainPara
chainPara.Size = uint32(unsafe.Sizeof(chainPara))
var chainCtx *windows.CertChainContext
err := windows.CertGetCertificateChain(0, leaf, nil, leaf.Store, &chainPara, 0, 0, &chainCtx)
if err != nil {
return nil, err
}
defer windows.CertFreeCertificateChain(chainCtx)
return extractCertChainDER(chainCtx)
}
func extractCertChainDER(chainCtx *windows.CertChainContext) ([][]byte, error) {
if chainCtx == nil || chainCtx.ChainCount == 0 || chainCtx.Chains == nil {
return nil, E.New("schannel: empty certificate chain")
}
chains := unsafe.Slice(chainCtx.Chains, int(chainCtx.ChainCount))
chain := chains[0]
if chain == nil || chain.NumElements == 0 || chain.Elements == nil {
return nil, E.New("schannel: empty certificate chain")
}
elements := unsafe.Slice(chain.Elements, int(chain.NumElements))
if len(elements) > 1 &&
chain.TrustStatus.ErrorStatus&windows.CERT_TRUST_IS_PARTIAL_CHAIN == 0 &&
isSelfSignedCertContext(elements[len(elements)-1].CertContext) {
elements = elements[:len(elements)-1]
}
derChain := make([][]byte, 0, len(elements))
for index, element := range elements {
if element == nil || element.CertContext == nil {
return nil, E.New("schannel: missing certificate chain element ", index)
}
der := certContextDER(element.CertContext)
if len(der) == 0 {
return nil, E.New("schannel: empty certificate chain element ", index)
}
derChain = append(derChain, der)
}
return derChain, nil
}
func isSelfSignedCertContext(ctx *windows.CertContext) bool {
if ctx == nil || ctx.CertInfo == nil {
return false
}
return bytes.Equal(
certNameBlobBytes(ctx.CertInfo.Issuer),
certNameBlobBytes(ctx.CertInfo.Subject),
)
}
func certNameBlobBytes(blob windows.CertNameBlob) []byte {
if blob.Size == 0 || blob.Data == nil {
return nil
}
return unsafe.Slice(blob.Data, int(blob.Size))
}
func sspiError(where string, status syscall.Errno) error {
return E.New("schannel: ", where, ": ", formatStatus(status))
}
var statusNames = map[syscall.Errno]string{
secEUnsupportedFunc: "SEC_E_UNSUPPORTED_FUNCTION",
secEInternalError: "SEC_E_INTERNAL_ERROR",
secEInvalidToken: "SEC_E_INVALID_TOKEN",
secELogonDenied: "SEC_E_LOGON_DENIED",
secEMessageAltered: "SEC_E_MESSAGE_ALTERED",
secENoAuthenticatingAuthority: "SEC_E_NO_AUTHENTICATING_AUTHORITY",
secEContextExpired: "SEC_E_CONTEXT_EXPIRED",
secEIncompleteMessage: "SEC_E_INCOMPLETE_MESSAGE",
secEIncompleteCreds: "SEC_E_INCOMPLETE_CREDENTIALS",
secEBufferTooSmall: "SEC_E_BUFFER_TOO_SMALL",
secEWrongPrincipal: "SEC_E_WRONG_PRINCIPAL",
secEIllegalMessage: "SEC_E_ILLEGAL_MESSAGE",
secECertUnknown: "SEC_E_CERT_UNKNOWN",
secECertExpired: "SEC_E_CERT_EXPIRED",
secEAlgorithmMismatch: "SEC_E_ALGORITHM_MISMATCH",
}
func formatStatus(status syscall.Errno) string {
name, loaded := statusNames[status]
if !loaded {
return status.Error()
}
return name + ": " + status.Error()
}

View file

@ -0,0 +1,188 @@
//go:build windows
package schannel
import (
"bytes"
"crypto/tls"
"testing"
"unsafe"
"golang.org/x/sys/windows"
)
func TestExtractCertChainDERExcludesSelfSignedRoot(t *testing.T) {
leaf := certContextForTest([]byte("leaf"), []byte("intermediate"), []byte("leaf"))
intermediate := certContextForTest([]byte("intermediate"), []byte("root"), []byte("intermediate"))
root := certContextForTest([]byte("root"), []byte("root"), []byte("root"))
chainCtx := certChainContextForTest(leaf, intermediate, root)
derChain, err := extractCertChainDER(chainCtx)
if err != nil {
t.Fatal(err)
}
if len(derChain) != 2 {
t.Fatalf("expected 2 certificates, got %d", len(derChain))
}
if !bytes.Equal(derChain[0], []byte("leaf")) {
t.Fatalf("unexpected leaf certificate: %q", string(derChain[0]))
}
if !bytes.Equal(derChain[1], []byte("intermediate")) {
t.Fatalf("unexpected intermediate certificate: %q", string(derChain[1]))
}
}
func TestExtractCertChainDERKeepsLastIntermediateWithoutRoot(t *testing.T) {
leaf := certContextForTest([]byte("leaf"), []byte("intermediate"), []byte("leaf"))
intermediate := certContextForTest([]byte("intermediate"), []byte("root"), []byte("intermediate"))
chainCtx := certChainContextForTest(leaf, intermediate)
derChain, err := extractCertChainDER(chainCtx)
if err != nil {
t.Fatal(err)
}
if len(derChain) != 2 {
t.Fatalf("expected 2 certificates, got %d", len(derChain))
}
if !bytes.Equal(derChain[1], []byte("intermediate")) {
t.Fatalf("unexpected last certificate: %q", string(derChain[1]))
}
}
func TestDisabledProtocolsMask(t *testing.T) {
testCases := []struct {
name string
minVersion uint16
maxVersion uint16
want uint32
}{
{
name: "default range",
want: spProtAllTLSClients &^ (spProtTLS12Client | spProtTLS13Client),
},
{
name: "default minimum with explicit max",
maxVersion: tls.VersionTLS12,
want: spProtAllTLSClients &^ spProtTLS12Client,
},
{
name: "explicit tls10 range",
minVersion: tls.VersionTLS10,
maxVersion: tls.VersionTLS13,
want: 0,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
got := disabledProtocolsMask(testCase.minVersion, testCase.maxVersion)
if got != testCase.want {
t.Fatalf("disabledProtocolsMask(%#x, %#x) = %#x, want %#x", testCase.minVersion, testCase.maxVersion, got, testCase.want)
}
})
}
}
func TestClientCredentialCacheReusesVersionRange(t *testing.T) {
if err := CheckPlatform(); err != nil {
t.Skip(err)
}
first, err := NewClientContext(tls.VersionTLS12, tls.VersionTLS13, "localhost", []string{"h2"})
if err != nil {
t.Fatal(err)
}
defer first.Close()
second, err := NewClientContext(tls.VersionTLS12, tls.VersionTLS13, "example.com", []string{"http/1.1"})
if err != nil {
t.Fatal(err)
}
defer second.Close()
if first.credential != second.credential {
t.Fatal("expected same TLS version range to reuse credential")
}
tls12Only, err := NewClientContext(tls.VersionTLS12, tls.VersionTLS12, "localhost", nil)
if err != nil {
t.Fatal(err)
}
defer tls12Only.Close()
if first.credential == tls12Only.credential {
t.Fatal("expected distinct TLS version range to use a distinct credential")
}
if first.credential.key.disabledProtocols != disabledProtocolsMask(tls.VersionTLS12, tls.VersionTLS13) {
t.Fatalf("unexpected cached disabled protocol mask: %#x", first.credential.key.disabledProtocols)
}
}
func TestParseDecryptResultKeepsRenegotiateExtraToken(t *testing.T) {
input := []byte("plain-ticket")
result, err := parseDecryptResult(input, []secBuffer{
{bufferType: secbufferExtra, cbBuffer: 6},
}, true)
if err != nil {
t.Fatal(err)
}
if !result.Renegotiate {
t.Fatal("expected Renegotiate to be true")
}
if result.ConsumedTotal != len(input)-6 {
t.Fatalf("unexpected consumed total: %d", result.ConsumedTotal)
}
if !bytes.Equal(result.RenegotiateToken, []byte("ticket")) {
t.Fatalf("unexpected renegotiate token: %q", string(result.RenegotiateToken))
}
}
func TestParseDecryptResultKeepsRenegotiateWholeBufferWithoutExtra(t *testing.T) {
input := []byte("ticket")
result, err := parseDecryptResult(input, []secBuffer{
{bufferType: secbufferData, cbBuffer: uint32(len(input)), pvBuffer: &input[0]},
}, true)
if err != nil {
t.Fatal(err)
}
if !result.Renegotiate {
t.Fatal("expected Renegotiate to be true")
}
if result.ConsumedTotal != len(input) {
t.Fatalf("unexpected consumed total: %d", result.ConsumedTotal)
}
if !bytes.Equal(result.RenegotiateToken, input) {
t.Fatalf("unexpected renegotiate token: %q", string(result.RenegotiateToken))
}
}
func certChainContextForTest(certs ...*windows.CertContext) *windows.CertChainContext {
elements := make([]*windows.CertChainElement, 0, len(certs))
for _, cert := range certs {
elements = append(elements, &windows.CertChainElement{CertContext: cert})
}
simpleChain := &windows.CertSimpleChain{
NumElements: uint32(len(elements)),
Elements: &elements[0],
}
chains := []*windows.CertSimpleChain{simpleChain}
return &windows.CertChainContext{
ChainCount: 1,
Chains: &chains[0],
}
}
func certContextForTest(der, issuer, subject []byte) *windows.CertContext {
certInfo := &windows.CertInfo{
Issuer: certNameBlobForTest(issuer),
Subject: certNameBlobForTest(subject),
}
return &windows.CertContext{
EncodedCert: &der[0],
Length: uint32(len(der)),
CertInfo: certInfo,
}
}
func certNameBlobForTest(value []byte) windows.CertNameBlob {
return windows.CertNameBlob{
Size: uint32(len(value)),
Data: (*byte)(unsafe.Pointer(&value[0])),
}
}

View file

@ -0,0 +1,28 @@
package schannel
import (
"syscall"
"unsafe"
)
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall_windows.go
// secur32.dll — SSPI / Schannel interface
//sys sspiAcquireCredentialsHandle(principal *uint16, pkgname *uint16, credentialUse uint32, logonID *uint64, authData unsafe.Pointer, getKeyFn uintptr, getKeyArg uintptr, credential *secHandle, expiry *windows.Filetime) (ret syscall.Errno) = secur32.AcquireCredentialsHandleW
//sys sspiFreeCredentialsHandle(credential *secHandle) (ret syscall.Errno) = secur32.FreeCredentialsHandle
//sys sspiInitializeSecurityContext(credential *secHandle, context *secHandle, targetName *uint16, contextReq uint32, reserved1 uint32, targetDataRep uint32, input *secBufferDesc, reserved2 uint32, newContext *secHandle, output *secBufferDesc, contextAttr *uint32, expiry *windows.Filetime) (ret syscall.Errno) = secur32.InitializeSecurityContextW
//sys sspiDeleteSecurityContext(context *secHandle) (ret syscall.Errno) = secur32.DeleteSecurityContext
//sys sspiQueryContextAttributes(context *secHandle, attribute uint32, buffer unsafe.Pointer) (ret syscall.Errno) = secur32.QueryContextAttributesW
//sys sspiEncryptMessage(context *secHandle, qop uint32, message *secBufferDesc, sequenceNumber uint32) (ret syscall.Errno) = secur32.EncryptMessage
//sys sspiDecryptMessage(context *secHandle, message *secBufferDesc, sequenceNumber uint32, qop *uint32) (ret syscall.Errno) = secur32.DecryptMessage
//sys sspiFreeContextBuffer(buffer *byte) (ret syscall.Errno) = secur32.FreeContextBuffer
// mkwinsyscall does not emit CompleteAuthToken for this package, so bind it manually.
var procCompleteAuthToken = modsecur32.NewProc("CompleteAuthToken")
func sspiCompleteAuthToken(context *secHandle, token *secBufferDesc) (ret syscall.Errno) {
r0, _, _ := syscall.SyscallN(procCompleteAuthToken.Addr(), uintptr(unsafe.Pointer(context)), uintptr(unsafe.Pointer(token)))
ret = syscall.Errno(r0)
return
}

View file

@ -0,0 +1,161 @@
package schannel
import (
"syscall"
"golang.org/x/sys/windows"
)
const (
unispNameW = "Microsoft Unified Security Protocol Provider"
schCredentialsVersion = 5
secPkgCredOutbound = 2
iscReqSequenceDetect = 0x00000008
iscReqReplayDetect = 0x00000004
iscReqConfidentiality = 0x00000010
iscReqAllocateMemory = 0x00000100
iscReqStream = 0x00008000
iscReqUseSuppliedCreds = 0x00000080
iscReqManualCredValidation = 0x00080000
iscReqExtendedError = 0x00004000
secbufferEmpty = 0
secbufferData = 1
secbufferToken = 2
secbufferExtra = 5
secbufferStreamTrailer = 6
secbufferStreamHeader = 7
secbufferApplicationProtocols = 18
secbufferVersion = 0
secApplicationProtocolNegotiationExtALPN = 2
secApplicationProtocolNegotiationStatusSuccess = 1
schCredManualCredValidation = 0x00000008
schCredNoDefaultCreds = 0x00000010
schUseStrongCrypto = 0x00400000
spProtTLS10Client = 0x00000080
spProtTLS11Client = 0x00000200
spProtTLS12Client = 0x00000800
spProtTLS13Client = 0x00002000
spProtAllTLSClients = spProtTLS10Client | spProtTLS11Client | spProtTLS12Client | spProtTLS13Client
secpkgAttrStreamSizes = 4
secpkgAttrConnectionInfo = 0x5A
secpkgAttrApplicationProtocol = 0x23
secpkgAttrCipherInfo = 0x64
secpkgAttrRemoteCertContext = 0x53
)
const (
secEOK = syscall.Errno(windows.SEC_E_OK)
secICompleteNeeded = syscall.Errno(windows.SEC_I_COMPLETE_NEEDED)
secICompleteAndContinue = syscall.Errno(windows.SEC_I_COMPLETE_AND_CONTINUE)
secIContinueNeeded = syscall.Errno(windows.SEC_I_CONTINUE_NEEDED)
secIContextExpired = syscall.Errno(windows.SEC_I_CONTEXT_EXPIRED)
secIRenegotiate = syscall.Errno(windows.SEC_I_RENEGOTIATE)
secEIncompleteMessage = syscall.Errno(windows.SEC_E_INCOMPLETE_MESSAGE)
secEIncompleteCreds = syscall.Errno(windows.SEC_E_INCOMPLETE_CREDENTIALS)
secEBufferTooSmall = syscall.Errno(windows.SEC_E_BUFFER_TOO_SMALL)
secEMessageAltered = syscall.Errno(windows.SEC_E_MESSAGE_ALTERED)
secEContextExpired = syscall.Errno(windows.SEC_E_CONTEXT_EXPIRED)
secEUnsupportedFunc = syscall.Errno(windows.SEC_E_UNSUPPORTED_FUNCTION)
secEInvalidToken = syscall.Errno(windows.SEC_E_INVALID_TOKEN)
secELogonDenied = syscall.Errno(windows.SEC_E_LOGON_DENIED)
secEIllegalMessage = syscall.Errno(windows.SEC_E_ILLEGAL_MESSAGE)
secEWrongPrincipal = syscall.Errno(windows.SEC_E_WRONG_PRINCIPAL)
secECertUnknown = syscall.Errno(windows.SEC_E_CERT_UNKNOWN)
secECertExpired = syscall.Errno(windows.SEC_E_CERT_EXPIRED)
secEAlgorithmMismatch = syscall.Errno(windows.SEC_E_ALGORITHM_MISMATCH)
secEInternalError = syscall.Errno(windows.SEC_E_INTERNAL_ERROR)
secENoAuthenticatingAuthority = syscall.Errno(windows.SEC_E_NO_AUTHENTICATING_AUTHORITY)
)
type secHandle struct {
lower uintptr
upper uintptr
}
type secBuffer struct {
cbBuffer uint32
bufferType uint32
pvBuffer *byte
}
type secBufferDesc struct {
ulVersion uint32
cBuffers uint32
pBuffers *secBuffer
}
type schCredentials struct {
dwVersion uint32
dwCredFormat uint32
cCreds uint32
paCred uintptr
hRootStore windows.Handle
cMappers uint32
aphMappers uintptr
dwSessionLifespan uint32
dwFlags uint32
cTlsParameters uint32
pTlsParameters *tlsParameters
}
type tlsParameters struct {
cAlpnIds uint32
rgstrAlpnIds uintptr
grbitDisabledProtocols uint32
cDisabledCrypto uint32
pDisabledCrypto uintptr
dwFlags uint32
}
type secPkgContextStreamSizes struct {
cbHeader uint32
cbTrailer uint32
cbMaximumMessage uint32
cBuffers uint32
cbBlockSize uint32
}
type secPkgContextConnectionInfo struct {
dwProtocol uint32
aiCipher uint32
dwCipherStrength uint32
aiHash uint32
dwHashStrength uint32
aiExch uint32
dwExchStrength uint32
}
type secPkgContextApplicationProtocol struct {
protoNegoStatus uint32
protoNegoExt uint32
protocolIDSize byte
protocolID [255]byte
}
type secPkgContextCipherInfo struct {
dwVersion uint32
dwProtocol uint32
dwCipherSuite uint32
dwBaseCipherSuite uint32
szCipherSuite [64]uint16
szCipher [64]uint16
dwCipherLen uint32
dwCipherBlockLen uint32
szHash [64]uint16
dwHashLen uint32
szExchange [64]uint16
dwMinExchangeLen uint32
dwMaxExchangeLen uint32
szCertificate [64]uint16
dwKeyType uint32
}

View file

@ -0,0 +1,99 @@
// Code generated by 'go generate'; DO NOT EDIT.
package schannel
import (
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
var _ unsafe.Pointer
// Do the interface allocations only once for common
// Errno values.
const (
errnoERROR_IO_PENDING = 997
)
var (
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
errERROR_EINVAL error = syscall.EINVAL
)
// errnoErr returns common boxed Errno values, to prevent
// allocations at runtime.
func errnoErr(e syscall.Errno) error {
switch e {
case 0:
return errERROR_EINVAL
case errnoERROR_IO_PENDING:
return errERROR_IO_PENDING
}
// TODO: add more here, after collecting data on the common
// error values see on Windows. (perhaps when running
// all.bat?)
return e
}
var (
modsecur32 = windows.NewLazySystemDLL("secur32.dll")
procAcquireCredentialsHandleW = modsecur32.NewProc("AcquireCredentialsHandleW")
procDecryptMessage = modsecur32.NewProc("DecryptMessage")
procDeleteSecurityContext = modsecur32.NewProc("DeleteSecurityContext")
procEncryptMessage = modsecur32.NewProc("EncryptMessage")
procFreeContextBuffer = modsecur32.NewProc("FreeContextBuffer")
procFreeCredentialsHandle = modsecur32.NewProc("FreeCredentialsHandle")
procInitializeSecurityContextW = modsecur32.NewProc("InitializeSecurityContextW")
procQueryContextAttributesW = modsecur32.NewProc("QueryContextAttributesW")
)
func sspiAcquireCredentialsHandle(principal *uint16, pkgname *uint16, credentialUse uint32, logonID *uint64, authData unsafe.Pointer, getKeyFn uintptr, getKeyArg uintptr, credential *secHandle, expiry *windows.Filetime) (ret syscall.Errno) {
r0, _, _ := syscall.SyscallN(procAcquireCredentialsHandleW.Addr(), uintptr(unsafe.Pointer(principal)), uintptr(unsafe.Pointer(pkgname)), uintptr(credentialUse), uintptr(unsafe.Pointer(logonID)), uintptr(authData), uintptr(getKeyFn), uintptr(getKeyArg), uintptr(unsafe.Pointer(credential)), uintptr(unsafe.Pointer(expiry)))
ret = syscall.Errno(r0)
return
}
func sspiDecryptMessage(context *secHandle, message *secBufferDesc, sequenceNumber uint32, qop *uint32) (ret syscall.Errno) {
r0, _, _ := syscall.SyscallN(procDecryptMessage.Addr(), uintptr(unsafe.Pointer(context)), uintptr(unsafe.Pointer(message)), uintptr(sequenceNumber), uintptr(unsafe.Pointer(qop)))
ret = syscall.Errno(r0)
return
}
func sspiDeleteSecurityContext(context *secHandle) (ret syscall.Errno) {
r0, _, _ := syscall.SyscallN(procDeleteSecurityContext.Addr(), uintptr(unsafe.Pointer(context)))
ret = syscall.Errno(r0)
return
}
func sspiEncryptMessage(context *secHandle, qop uint32, message *secBufferDesc, sequenceNumber uint32) (ret syscall.Errno) {
r0, _, _ := syscall.SyscallN(procEncryptMessage.Addr(), uintptr(unsafe.Pointer(context)), uintptr(qop), uintptr(unsafe.Pointer(message)), uintptr(sequenceNumber))
ret = syscall.Errno(r0)
return
}
func sspiFreeContextBuffer(buffer *byte) (ret syscall.Errno) {
r0, _, _ := syscall.SyscallN(procFreeContextBuffer.Addr(), uintptr(unsafe.Pointer(buffer)))
ret = syscall.Errno(r0)
return
}
func sspiFreeCredentialsHandle(credential *secHandle) (ret syscall.Errno) {
r0, _, _ := syscall.SyscallN(procFreeCredentialsHandle.Addr(), uintptr(unsafe.Pointer(credential)))
ret = syscall.Errno(r0)
return
}
func sspiInitializeSecurityContext(credential *secHandle, context *secHandle, targetName *uint16, contextReq uint32, reserved1 uint32, targetDataRep uint32, input *secBufferDesc, reserved2 uint32, newContext *secHandle, output *secBufferDesc, contextAttr *uint32, expiry *windows.Filetime) (ret syscall.Errno) {
r0, _, _ := syscall.SyscallN(procInitializeSecurityContextW.Addr(), uintptr(unsafe.Pointer(credential)), uintptr(unsafe.Pointer(context)), uintptr(unsafe.Pointer(targetName)), uintptr(contextReq), uintptr(reserved1), uintptr(targetDataRep), uintptr(unsafe.Pointer(input)), uintptr(reserved2), uintptr(unsafe.Pointer(newContext)), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(contextAttr)), uintptr(unsafe.Pointer(expiry)))
ret = syscall.Errno(r0)
return
}
func sspiQueryContextAttributes(context *secHandle, attribute uint32, buffer unsafe.Pointer) (ret syscall.Errno) {
r0, _, _ := syscall.SyscallN(procQueryContextAttributesW.Addr(), uintptr(unsafe.Pointer(context)), uintptr(attribute), uintptr(buffer))
ret = syscall.Errno(r0)
return
}

View file

@ -4,218 +4,41 @@ package tls
import (
"context"
"net"
"os"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
boxConstant "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/common/certificate"
"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"
)
type appleCertificateStore interface {
StoreKind() string
CurrentPEM() []string
}
const appleTLSEngineName = "Apple TLS engine"
type appleClientConfig struct {
serverName string
nextProtos []string
handshakeTimeout time.Duration
minVersion uint16
maxVersion uint16
insecure bool
anchorPEM string
anchorOnly bool
certificatePublicKeySHA256 [][]byte
timeFunc func() time.Time
}
func (c *appleClientConfig) ServerName() string {
return c.serverName
}
func (c *appleClientConfig) SetServerName(serverName string) {
c.serverName = serverName
}
func (c *appleClientConfig) NextProtos() []string {
return c.nextProtos
}
func (c *appleClientConfig) SetNextProtos(nextProto []string) {
c.nextProtos = append(c.nextProtos[:0], nextProto...)
}
func (c *appleClientConfig) HandshakeTimeout() time.Duration {
return c.handshakeTimeout
}
func (c *appleClientConfig) SetHandshakeTimeout(timeout time.Duration) {
c.handshakeTimeout = timeout
}
func (c *appleClientConfig) STDConfig() (*STDConfig, error) {
return nil, E.New("unsupported usage for Apple TLS engine")
}
func (c *appleClientConfig) Client(conn net.Conn) (Conn, error) {
return nil, os.ErrInvalid
systemTLSConfig
userPEM []byte
}
func (c *appleClientConfig) Clone() Config {
return &appleClientConfig{
serverName: c.serverName,
nextProtos: append([]string(nil), c.nextProtos...),
handshakeTimeout: c.handshakeTimeout,
minVersion: c.minVersion,
maxVersion: c.maxVersion,
insecure: c.insecure,
anchorPEM: c.anchorPEM,
anchorOnly: c.anchorOnly,
certificatePublicKeySHA256: append([][]byte(nil), c.certificatePublicKeySHA256...),
timeFunc: c.timeFunc,
systemTLSConfig: c.systemTLSConfig.clone(),
userPEM: append([]byte(nil), c.userPEM...),
}
}
func (c *appleClientConfig) resolveAnchors() (adapter.AppleAnchors, error) {
if len(c.userPEM) > 0 {
return certificate.NewAppleAnchors(c.userPEM)
}
return certificate.AcquireAnchors(nil, c.store), nil
}
func newAppleClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
validated, err := ValidateAppleTLSOptions(ctx, options, "Apple TLS engine")
base, validated, err := newSystemTLSConfig(ctx, serverAddress, options, allowEmptyServerName, appleTLSEngineName)
if err != nil {
return nil, err
}
var serverName string
if options.ServerName != "" {
serverName = options.ServerName
} else if serverAddress != "" {
serverName = serverAddress
}
if serverName == "" && !options.Insecure && !allowEmptyServerName {
return nil, errMissingServerName
}
var handshakeTimeout time.Duration
if options.HandshakeTimeout > 0 {
handshakeTimeout = options.HandshakeTimeout.Build()
} else {
handshakeTimeout = boxConstant.TCPTimeout
}
return &appleClientConfig{
serverName: serverName,
nextProtos: append([]string(nil), options.ALPN...),
handshakeTimeout: handshakeTimeout,
minVersion: validated.MinVersion,
maxVersion: validated.MaxVersion,
insecure: options.Insecure || len(options.CertificatePublicKeySHA256) > 0,
anchorPEM: validated.AnchorPEM,
anchorOnly: validated.AnchorOnly,
certificatePublicKeySHA256: append([][]byte(nil), options.CertificatePublicKeySHA256...),
timeFunc: ntp.TimeFuncFromContext(ctx),
systemTLSConfig: base,
userPEM: append([]byte(nil), validated.UserPEM...),
}, nil
}
type AppleTLSValidated struct {
MinVersion uint16
MaxVersion uint16
AnchorPEM string
AnchorOnly bool
}
func ValidateAppleTLSOptions(ctx context.Context, options option.OutboundTLSOptions, engineName string) (AppleTLSValidated, error) {
if options.Reality != nil && options.Reality.Enabled {
return AppleTLSValidated{}, E.New("reality is unsupported in ", engineName)
}
if options.UTLS != nil && options.UTLS.Enabled {
return AppleTLSValidated{}, E.New("utls is unsupported in ", engineName)
}
if options.ECH != nil && options.ECH.Enabled {
return AppleTLSValidated{}, E.New("ech is unsupported in ", engineName)
}
if options.DisableSNI {
return AppleTLSValidated{}, E.New("disable_sni is unsupported in ", engineName)
}
if len(options.CipherSuites) > 0 {
return AppleTLSValidated{}, E.New("cipher_suites is unsupported in ", engineName)
}
if len(options.CurvePreferences) > 0 {
return AppleTLSValidated{}, E.New("curve_preferences is unsupported in ", engineName)
}
if len(options.ClientCertificate) > 0 || options.ClientCertificatePath != "" || len(options.ClientKey) > 0 || options.ClientKeyPath != "" {
return AppleTLSValidated{}, E.New("client certificate is unsupported in ", engineName)
}
if options.Fragment || options.RecordFragment {
return AppleTLSValidated{}, E.New("tls fragment is unsupported in ", engineName)
}
if options.KernelTx || options.KernelRx {
return AppleTLSValidated{}, E.New("ktls is unsupported in ", engineName)
}
if options.Spoof != "" || options.SpoofMethod != "" {
return AppleTLSValidated{}, E.New("spoof is unsupported in ", engineName)
}
if len(options.CertificatePublicKeySHA256) > 0 && (len(options.Certificate) > 0 || options.CertificatePath != "") {
return AppleTLSValidated{}, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path")
}
var minVersion uint16
if options.MinVersion != "" {
var err error
minVersion, err = ParseTLSVersion(options.MinVersion)
if err != nil {
return AppleTLSValidated{}, E.Cause(err, "parse min_version")
}
}
var maxVersion uint16
if options.MaxVersion != "" {
var err error
maxVersion, err = ParseTLSVersion(options.MaxVersion)
if err != nil {
return AppleTLSValidated{}, E.Cause(err, "parse max_version")
}
}
anchorPEM, anchorOnly, err := AppleAnchorPEM(ctx, options)
if err != nil {
return AppleTLSValidated{}, err
}
return AppleTLSValidated{
MinVersion: minVersion,
MaxVersion: maxVersion,
AnchorPEM: anchorPEM,
AnchorOnly: anchorOnly,
}, nil
}
func AppleAnchorPEM(ctx context.Context, options option.OutboundTLSOptions) (string, bool, error) {
if len(options.Certificate) > 0 {
return strings.Join(options.Certificate, "\n"), true, nil
}
if options.CertificatePath != "" {
content, err := os.ReadFile(options.CertificatePath)
if err != nil {
return "", false, E.Cause(err, "read certificate")
}
return string(content), true, nil
}
certificateStore := service.FromContext[adapter.CertificateStore](ctx)
if certificateStore == nil {
return "", false, nil
}
store, ok := certificateStore.(appleCertificateStore)
if !ok {
return "", false, nil
}
switch store.StoreKind() {
case boxConstant.CertificateStoreSystem, "":
return strings.Join(store.CurrentPEM(), "\n"), false, nil
case boxConstant.CertificateStoreMozilla, boxConstant.CertificateStoreChrome, boxConstant.CertificateStoreNone:
return strings.Join(store.CurrentPEM(), "\n"), true, nil
default:
return "", false, E.New("unsupported certificate store for Apple TLS engine: ", store.StoreKind())
}
}

View file

@ -20,24 +20,25 @@ import (
"math"
"net"
"os"
"runtime/cgo"
"strings"
"sync"
"syscall"
"time"
"unsafe"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
"golang.org/x/sys/unix"
)
func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (Conn, error) {
rawSyscallConn, ok := common.Cast[syscall.Conn](conn)
tcpConn, ok := N.UnwrapReader(conn).(*net.TCPConn)
if !ok {
return nil, E.New("apple TLS: requires fd-backed TCP connection")
}
syscallConn, err := rawSyscallConn.SyscallConn()
syscallConn, err := tcpConn.SyscallConn()
if err != nil {
return nil, E.Cause(err, "access raw connection")
}
@ -61,8 +62,14 @@ func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn)
alpnPtr := cStringOrNil(alpn)
defer cFree(alpnPtr)
anchorPEMPtr := cStringOrNil(c.anchorPEM)
defer cFree(anchorPEMPtr)
anchors, err := c.resolveAnchors()
if err != nil {
return nil, err
}
var anchorsRef unsafe.Pointer
if anchors != nil {
anchorsRef = anchors.Ref()
}
var (
hasVerifyTime bool
@ -82,13 +89,15 @@ func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn)
C.uint16_t(c.minVersion),
C.uint16_t(c.maxVersion),
C.bool(c.insecure),
anchorPEMPtr,
C.size_t(len(c.anchorPEM)),
anchorsRef,
C.bool(c.anchorOnly),
C.bool(hasVerifyTime),
C.int64_t(verifyTimeUnixMilli),
&errorPtr,
)
if anchors != nil {
anchors.Release()
}
if client == nil {
if errorPtr != nil {
defer C.free(unsafe.Pointer(errorPtr))
@ -138,21 +147,27 @@ func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn)
}, nil
}
const appleTLSHandshakePollInterval = 100 * time.Millisecond
const (
appleTLSHandshakePollInterval = 100 * time.Millisecond
appleTLSWriteChunkSize = 32 * 1024
)
func waitAppleTLSClientReady(ctx context.Context, client *C.box_apple_tls_client_t) error {
for {
if err := ctx.Err(); err != nil {
err := ctx.Err()
if err != nil {
C.box_apple_tls_client_cancel(client)
return err
}
waitTimeout := appleTLSHandshakePollInterval
if deadline, loaded := ctx.Deadline(); loaded {
deadline, loaded := ctx.Deadline()
if loaded {
remaining := time.Until(deadline)
if remaining <= 0 {
C.box_apple_tls_client_cancel(client)
if err := ctx.Err(); err != nil {
err = ctx.Err()
if err != nil {
return err
}
return context.DeadlineExceeded
@ -201,6 +216,11 @@ type appleTLSConn struct {
writeTimedOut bool
}
var (
_ N.ExtendedConn = (*appleTLSConn)(nil)
_ N.ReadWaitCreator = (*appleTLSConn)(nil)
)
func (c *appleTLSConn) Read(p []byte) (int, error) {
c.readAccess.Lock()
defer c.readAccess.Unlock()
@ -211,6 +231,29 @@ func (c *appleTLSConn) Read(p []byte) (int, error) {
return 0, nil
}
return c.readIntoLocked(p)
}
func (c *appleTLSConn) ReadBuffer(buffer *buf.Buffer) error {
c.readAccess.Lock()
defer c.readAccess.Unlock()
if buffer.IsFull() {
return io.ErrShortBuffer
}
startLen := buffer.Len()
n, err := c.readIntoLocked(buffer.FreeBytes())
buffer.Truncate(startLen + n)
return err
}
func (c *appleTLSConn) readIntoLocked(p []byte) (int, error) {
if c.readEOF {
return 0, io.EOF
}
if len(p) == 0 {
return 0, nil
}
timeoutMs, err := c.prepareReadTimeout()
if err != nil {
return 0, err
@ -256,34 +299,55 @@ func (c *appleTLSConn) Write(p []byte) (int, error) {
return 0, nil
}
timeoutMs, err := c.prepareWriteTimeout()
if err != nil {
return 0, err
}
client, err := c.acquireClient()
if err != nil {
return 0, err
}
defer c.releaseClient()
var errorPtr *C.char
n := C.box_apple_tls_client_write(client, unsafe.Pointer(&p[0]), C.size_t(len(p)), C.int(timeoutMs), &errorPtr)
switch {
case n == -2:
c.markWriteTimedOut()
return 0, os.ErrDeadlineExceeded
case n >= 0:
return int(n), nil
deadline, err := c.prepareWriteDeadline()
if err != nil {
return 0, err
}
if errorPtr != nil {
defer C.free(unsafe.Pointer(errorPtr))
if c.isClosed() {
return 0, net.ErrClosed
var written int
for written < len(p) {
timeoutMs, expired := deadlineTimeoutMs(deadline)
if expired {
C.box_apple_tls_client_cancel(client)
c.markWriteTimedOut()
return written, os.ErrDeadlineExceeded
}
return 0, E.New(C.GoString(errorPtr))
chunkSize := min(len(p)-written, appleTLSWriteChunkSize)
chunk := p[written : written+chunkSize]
var errorPtr *C.char
n := C.box_apple_tls_client_write(client, unsafe.Pointer(&chunk[0]), C.size_t(len(chunk)), C.int(timeoutMs), &errorPtr)
switch {
case n == -2:
c.markWriteTimedOut()
return written, os.ErrDeadlineExceeded
case n >= 0:
written += int(n)
if int(n) != len(chunk) {
return written, io.ErrShortWrite
}
continue
}
return written, c.errorFromPointer(errorPtr)
}
return 0, net.ErrClosed
return written, nil
}
func (c *appleTLSConn) WriteBuffer(buffer *buf.Buffer) error {
defer buffer.Release()
_, err := c.Write(buffer.Bytes())
return err
}
func (c *appleTLSConn) CreateReadWaiter() (N.ReadWaiter, bool) {
return &appleTLSReadWaiter{
conn: c,
results: make(chan *C.box_apple_tls_read_result_t, 1),
}, true
}
func (c *appleTLSConn) Close() error {
@ -358,18 +422,18 @@ func (c *appleTLSConn) prepareReadTimeout() (int, error) {
return timeoutMs, nil
}
func (c *appleTLSConn) prepareWriteTimeout() (int, error) {
func (c *appleTLSConn) prepareWriteDeadline() (time.Time, error) {
c.deadlineAccess.Lock()
defer c.deadlineAccess.Unlock()
if c.writeTimedOut {
return 0, os.ErrDeadlineExceeded
return time.Time{}, os.ErrDeadlineExceeded
}
timeoutMs, expired := deadlineTimeoutMs(c.writeDeadline)
_, expired := deadlineTimeoutMs(c.writeDeadline)
if expired {
c.writeTimedOut = true
return 0, os.ErrDeadlineExceeded
return time.Time{}, os.ErrDeadlineExceeded
}
return timeoutMs, nil
return c.writeDeadline, nil
}
func (c *appleTLSConn) markReadTimedOut() {
@ -422,6 +486,138 @@ func (c *appleTLSConn) releaseClient() {
c.ioGroup.Done()
}
func (c *appleTLSConn) errorFromPointer(errorPtr *C.char) error {
if errorPtr != nil {
defer C.free(unsafe.Pointer(errorPtr))
if c.isClosed() {
return net.ErrClosed
}
return E.New(C.GoString(errorPtr))
}
return net.ErrClosed
}
type appleTLSReadWaiter struct {
conn *appleTLSConn
options N.ReadWaitOptions
results chan *C.box_apple_tls_read_result_t
}
var _ N.ReadWaiter = (*appleTLSReadWaiter)(nil)
func (w *appleTLSReadWaiter) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) {
w.options = options
if w.results == nil {
w.results = make(chan *C.box_apple_tls_read_result_t, 1)
}
return false
}
func (w *appleTLSReadWaiter) WaitReadBuffer() (*buf.Buffer, error) {
c := w.conn
c.readAccess.Lock()
defer c.readAccess.Unlock()
if c.readEOF {
return nil, io.EOF
}
maximumLen := readWaitFreeLen(w.options)
if maximumLen <= 0 {
return nil, io.ErrShortBuffer
}
timeoutMs, err := c.prepareReadTimeout()
if err != nil {
return nil, err
}
client, err := c.acquireClient()
if err != nil {
return nil, err
}
defer c.releaseClient()
handle := cgo.NewHandle(w)
defer handle.Delete()
var errorPtr *C.char
if !bool(C.box_apple_tls_client_read_async(client, C.size_t(maximumLen), C.uintptr_t(handle), &errorPtr)) {
return nil, c.errorFromPointer(errorPtr)
}
var result *C.box_apple_tls_read_result_t
if timeoutMs >= 0 {
timer := time.NewTimer(time.Duration(timeoutMs) * time.Millisecond)
defer timer.Stop()
select {
case result = <-w.results:
case <-timer.C:
C.box_apple_tls_client_cancel(client)
result = <-w.results
if result != nil {
C.box_apple_tls_read_result_free(result)
}
c.markReadTimedOut()
return nil, os.ErrDeadlineExceeded
}
} else {
result = <-w.results
}
return c.readWaitResultToBuffer(result, w.options)
}
func (c *appleTLSConn) readWaitResultToBuffer(result *C.box_apple_tls_read_result_t, options N.ReadWaitOptions) (*buf.Buffer, error) {
defer C.box_apple_tls_read_result_free(result)
buffer := options.NewBuffer()
if buffer.IsFull() {
buffer.Release()
return nil, io.ErrShortBuffer
}
startLen := buffer.Len()
var eof C.bool
var errorPtr *C.char
n := C.box_apple_tls_read_result_copy(result, unsafe.Pointer(&buffer.FreeBytes()[0]), C.size_t(buffer.FreeLen()), &eof, &errorPtr)
if n < 0 {
buffer.Release()
return nil, c.errorFromPointer(errorPtr)
}
if bool(eof) {
c.readEOF = true
if n == 0 {
buffer.Release()
return nil, io.EOF
}
}
if n == 0 {
buffer.Release()
return nil, io.ErrNoProgress
}
buffer.Truncate(startLen + int(n))
options.PostReturn(buffer)
return buffer, nil
}
func readWaitFreeLen(options N.ReadWaitOptions) int {
if options.IncreaseBuffer {
return 65535 - options.FrontHeadroom - options.RearHeadroom
}
if options.MTU > 0 {
return options.MTU
}
return buf.BufferSize - options.FrontHeadroom - options.RearHeadroom
}
//export box_apple_tls_read_callback
func box_apple_tls_read_callback(callbackHandle C.uintptr_t, result *C.box_apple_tls_read_result_t) {
handle := cgo.Handle(callbackHandle)
waiter, ok := handle.Value().(*appleTLSReadWaiter)
if !ok {
C.box_apple_tls_read_result_free(result)
return
}
select {
case waiter.results <- result:
default:
C.box_apple_tls_read_result_free(result)
}
}
func (c *appleTLSConn) NetConn() net.Conn {
return c.rawConn
}

View file

@ -0,0 +1,278 @@
//go:build darwin && cgo
package tls
import (
"bytes"
stdtls "crypto/tls"
"crypto/x509"
"errors"
"io"
"net"
"testing"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/json/badoption"
N "github.com/sagernet/sing/common/network"
)
const (
appleTLSBenchmarkReadPayloadSize = 16 * 1024
appleTLSBenchmarkWritePayloadSize = 48 * 1024
)
func BenchmarkAppleClientReadBuffer(b *testing.B) {
payload := bytes.Repeat([]byte{'r'}, appleTLSBenchmarkReadPayloadSize)
start := make(chan struct{})
clientConn, serverResult := newAppleBenchmarkClientConn(b, func(conn *stdtls.Conn) error {
<-start
for range b.N {
if err := writeBenchmarkPayload(conn, payload); err != nil {
return err
}
}
return nil
})
defer clientConn.Close()
extendedConn := clientConn.(N.ExtendedConn)
b.ReportAllocs()
b.SetBytes(int64(len(payload)))
b.ReportMetric(float64(len(payload)), "payload_B")
b.ResetTimer()
close(start)
target := b.N * len(payload)
var received int
for received < target {
buffer := buf.NewSize(len(payload))
err := extendedConn.ReadBuffer(buffer)
if err != nil {
buffer.Release()
b.Fatal(err)
}
received += buffer.Len()
buffer.Release()
}
b.StopTimer()
if err := <-serverResult; err != nil {
b.Fatal(err)
}
}
func BenchmarkAppleClientReadWaiter(b *testing.B) {
payload := bytes.Repeat([]byte{'w'}, appleTLSBenchmarkReadPayloadSize)
start := make(chan struct{})
clientConn, serverResult := newAppleBenchmarkClientConn(b, func(conn *stdtls.Conn) error {
<-start
for range b.N {
if err := writeBenchmarkPayload(conn, payload); err != nil {
return err
}
}
return nil
})
defer clientConn.Close()
readWaiter, ok := clientConn.(N.ReadWaitCreator).CreateReadWaiter()
if !ok {
b.Fatal("expected read waiter")
}
readWaiter.InitializeReadWaiter(N.ReadWaitOptions{
MTU: appleTLSBenchmarkReadPayloadSize,
})
b.ReportAllocs()
b.SetBytes(int64(len(payload)))
b.ReportMetric(float64(len(payload)), "payload_B")
b.ResetTimer()
close(start)
target := b.N * len(payload)
var received int
for received < target {
buffer, err := readWaiter.WaitReadBuffer()
if err != nil {
if errors.Is(err, io.ErrNoProgress) {
continue
}
b.Fatal(err)
}
received += buffer.Len()
buffer.Release()
}
b.StopTimer()
if err := <-serverResult; err != nil {
b.Fatal(err)
}
}
func BenchmarkAppleClientWriteBuffer(b *testing.B) {
payload := bytes.Repeat([]byte{'x'}, appleTLSBenchmarkWritePayloadSize)
start := make(chan struct{})
clientConn, serverResult := newAppleBenchmarkClientConn(b, func(conn *stdtls.Conn) error {
<-start
_, err := io.CopyN(io.Discard, conn, int64(b.N*len(payload)))
return err
})
defer clientConn.Close()
extendedConn := clientConn.(N.ExtendedConn)
b.ReportAllocs()
b.SetBytes(int64(len(payload)))
b.ReportMetric(float64(len(payload)), "payload_B")
b.ReportMetric(float64(appleTLSWriteChunkSize), "write_chunk_B")
b.ResetTimer()
close(start)
for range b.N {
buffer := buf.NewSize(len(payload))
_, err := buffer.Write(payload)
if err != nil {
buffer.Release()
b.Fatal(err)
}
err = extendedConn.WriteBuffer(buffer)
if err != nil {
b.Fatal(err)
}
}
b.StopTimer()
if err := <-serverResult; err != nil {
b.Fatal(err)
}
}
func BenchmarkStdlibClientReadBuffer(b *testing.B) {
payload := bytes.Repeat([]byte{'r'}, appleTLSBenchmarkReadPayloadSize)
start := make(chan struct{})
clientConn, serverResult := newStdlibBenchmarkClientConn(b, func(conn *stdtls.Conn) error {
<-start
for range b.N {
if err := writeBenchmarkPayload(conn, payload); err != nil {
return err
}
}
return nil
})
defer clientConn.Close()
b.ReportAllocs()
b.SetBytes(int64(len(payload)))
b.ReportMetric(float64(len(payload)), "payload_B")
b.ResetTimer()
close(start)
target := b.N * len(payload)
var received int
for received < target {
buffer := buf.NewSize(len(payload))
n, err := clientConn.Read(buffer.FreeBytes())
if n > 0 {
buffer.Truncate(buffer.Len() + n)
}
received += buffer.Len()
buffer.Release()
if err != nil && received < target {
b.Fatal(err)
}
}
b.StopTimer()
if err := <-serverResult; err != nil {
b.Fatal(err)
}
}
func BenchmarkStdlibClientWriteBuffer(b *testing.B) {
payload := bytes.Repeat([]byte{'x'}, appleTLSBenchmarkWritePayloadSize)
start := make(chan struct{})
clientConn, serverResult := newStdlibBenchmarkClientConn(b, func(conn *stdtls.Conn) error {
<-start
_, err := io.CopyN(io.Discard, conn, int64(b.N*len(payload)))
return err
})
defer clientConn.Close()
b.ReportAllocs()
b.SetBytes(int64(len(payload)))
b.ReportMetric(float64(len(payload)), "payload_B")
b.ResetTimer()
close(start)
for range b.N {
buffer := buf.NewSize(len(payload))
_, err := buffer.Write(payload)
if err != nil {
buffer.Release()
b.Fatal(err)
}
_, err = clientConn.Write(buffer.Bytes())
buffer.Release()
if err != nil {
b.Fatal(err)
}
}
b.StopTimer()
if err := <-serverResult; err != nil {
b.Fatal(err)
}
}
func newAppleBenchmarkClientConn(b *testing.B, handler func(*stdtls.Conn) error) (Conn, <-chan error) {
b.Helper()
serverCertificate, serverCertificatePEM := newAppleTestCertificate(b, "localhost")
serverResult, serverAddress := startAppleTLSIOTestServer(b, &stdtls.Config{
Certificates: []stdtls.Certificate{serverCertificate},
MinVersion: stdtls.VersionTLS12,
MaxVersion: stdtls.VersionTLS12,
}, handler)
clientConn, err := newAppleTestClientConn(b, serverAddress, option.OutboundTLSOptions{
Enabled: true,
Engine: "apple",
ServerName: "localhost",
MinVersion: "1.2",
MaxVersion: "1.2",
Certificate: badoption.Listable[string]{serverCertificatePEM},
})
if err != nil {
b.Fatal(err)
}
return clientConn, serverResult
}
func newStdlibBenchmarkClientConn(b *testing.B, handler func(*stdtls.Conn) error) (*stdtls.Conn, <-chan error) {
b.Helper()
serverCertificate, serverCertificatePEM := newAppleTestCertificate(b, "localhost")
serverResult, serverAddress := startAppleTLSIOTestServer(b, &stdtls.Config{
Certificates: []stdtls.Certificate{serverCertificate},
MinVersion: stdtls.VersionTLS12,
MaxVersion: stdtls.VersionTLS12,
}, handler)
roots := x509.NewCertPool()
if !roots.AppendCertsFromPEM([]byte(serverCertificatePEM)) {
b.Fatal("parse benchmark certificate")
}
dialer := &net.Dialer{
Timeout: appleTLSTestTimeout,
}
clientConn, err := stdtls.DialWithDialer(dialer, "tcp", serverAddress, &stdtls.Config{
ServerName: "localhost",
RootCAs: roots,
MinVersion: stdtls.VersionTLS12,
MaxVersion: stdtls.VersionTLS12,
})
if err != nil {
b.Fatal(err)
}
return clientConn, serverResult
}
func writeBenchmarkPayload(writer io.Writer, payload []byte) error {
for len(payload) > 0 {
n, err := writer.Write(payload)
if err != nil {
return err
}
payload = payload[n:]
}
return nil
}

View file

@ -4,6 +4,7 @@
#include <unistd.h>
typedef struct box_apple_tls_client box_apple_tls_client_t;
typedef struct box_apple_tls_read_result box_apple_tls_read_result_t;
typedef struct box_apple_tls_state {
uint16_t version;
@ -22,8 +23,7 @@ box_apple_tls_client_t *box_apple_tls_client_create(
uint16_t min_version,
uint16_t max_version,
bool insecure,
const char *anchor_pem,
size_t anchor_pem_len,
void *anchors_cf,
bool anchor_only,
bool has_verify_time,
int64_t verify_time_unix_millis,
@ -35,5 +35,9 @@ void box_apple_tls_client_cancel(box_apple_tls_client_t *client);
void box_apple_tls_client_free(box_apple_tls_client_t *client);
ssize_t box_apple_tls_client_read(box_apple_tls_client_t *client, void *buffer, size_t buffer_len, int timeout_msec, bool *eof_out, char **error_out);
ssize_t box_apple_tls_client_write(box_apple_tls_client_t *client, const void *buffer, size_t buffer_len, int timeout_msec, char **error_out);
bool box_apple_tls_client_read_async(box_apple_tls_client_t *client, size_t maximum_len, uintptr_t callback_handle, char **error_out);
ssize_t box_apple_tls_read_result_copy(box_apple_tls_read_result_t *result, void *buffer, size_t buffer_len, bool *eof_out, char **error_out);
void box_apple_tls_read_result_free(box_apple_tls_read_result_t *result);
bool box_apple_tls_client_copy_state(box_apple_tls_client_t *client, box_apple_tls_state_t *state, char **error_out);
void box_apple_tls_state_free(box_apple_tls_state_t *state);
ssize_t box_apple_tls_copy_dispatch_data_for_test(const void *first, size_t first_len, const void *second, size_t second_len, void *buffer, size_t buffer_len, char **error_out);

View file

@ -21,6 +21,7 @@ typedef struct box_apple_tls_client {
void *connection;
void *queue;
void *ready_semaphore;
void *anchors;
atomic_int ref_count;
atomic_bool ready;
atomic_bool ready_done;
@ -28,6 +29,14 @@ typedef struct box_apple_tls_client {
box_apple_tls_state_t state;
} box_apple_tls_client_t;
struct box_apple_tls_read_result {
void *content;
bool eof;
char *error;
};
extern void box_apple_tls_read_callback(uintptr_t callback_handle, box_apple_tls_read_result_t *result);
static nw_connection_t box_apple_tls_connection(box_apple_tls_client_t *client) {
if (client == NULL || client->connection == NULL) {
return nil;
@ -49,6 +58,20 @@ static dispatch_semaphore_t box_apple_tls_ready_semaphore(box_apple_tls_client_t
return (__bridge dispatch_semaphore_t)client->ready_semaphore;
}
static NSArray *box_apple_tls_client_anchors(box_apple_tls_client_t *client) {
if (client == NULL || client->anchors == NULL) {
return nil;
}
return (__bridge NSArray *)client->anchors;
}
static dispatch_data_t box_apple_tls_read_result_content(box_apple_tls_read_result_t *result) {
if (result == NULL || result->content == NULL) {
return nil;
}
return (__bridge dispatch_data_t)result->content;
}
static void box_apple_tls_state_reset(box_apple_tls_state_t *state) {
if (state == NULL) {
return;
@ -62,6 +85,9 @@ static void box_apple_tls_state_reset(box_apple_tls_state_t *state) {
static void box_apple_tls_client_destroy(box_apple_tls_client_t *client) {
free(client->ready_error);
box_apple_tls_state_reset(&client->state);
if (client->anchors != NULL) {
CFRelease((CFTypeRef)client->anchors);
}
if (client->ready_semaphore != NULL) {
CFBridgingRelease(client->ready_semaphore);
}
@ -113,6 +139,51 @@ static void box_set_error_from_nw_error(char **error_out, nw_error_t error) {
CFRelease(cfError);
}
static ssize_t box_apple_tls_dispatch_data_copy(dispatch_data_t content, void *buffer, size_t buffer_len, char **error_out) {
if (content == nil) {
return 0;
}
size_t content_size = dispatch_data_get_size(content);
if (content_size == 0) {
return 0;
}
if (buffer == NULL) {
box_set_error_message(error_out, "apple TLS: read buffer unavailable");
return -1;
}
__block size_t copied = 0;
__block bool overflow = false;
bool complete = dispatch_data_apply(content, ^bool(dispatch_data_t region, size_t offset, const void *region_buffer, size_t region_size) {
(void)region;
(void)offset;
if (region_size == 0) {
return true;
}
if (region_buffer == NULL || region_size > buffer_len - copied) {
overflow = true;
return false;
}
memcpy((uint8_t *)buffer + copied, region_buffer, region_size);
copied += region_size;
return true;
});
if (!complete || overflow) {
box_set_error_message(error_out, "apple TLS: read buffer too small");
return -1;
}
return (ssize_t)copied;
}
ssize_t box_apple_tls_copy_dispatch_data_for_test(const void *first, size_t first_len, const void *second, size_t second_len, void *buffer, size_t buffer_len, char **error_out) {
@autoreleasepool {
dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
dispatch_data_t first_data = first_len > 0 ? dispatch_data_create(first, first_len, queue, DISPATCH_DATA_DESTRUCTOR_DEFAULT) : dispatch_data_empty;
dispatch_data_t second_data = second_len > 0 ? dispatch_data_create(second, second_len, queue, DISPATCH_DATA_DESTRUCTOR_DEFAULT) : dispatch_data_empty;
dispatch_data_t content = dispatch_data_create_concat(first_data, second_data);
return box_apple_tls_dispatch_data_copy(content, buffer, buffer_len, error_out);
}
}
static char *box_apple_tls_metadata_copy_negotiated_protocol(sec_protocol_metadata_t metadata) {
static box_sec_protocol_metadata_string_accessor_f copy_fn;
static box_sec_protocol_metadata_string_accessor_f get_fn;
@ -170,44 +241,6 @@ static NSArray<NSString *> *box_split_lines(const char *content, size_t content_
return lines;
}
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(sec_trust_t trust, NSArray *anchors, bool anchor_only, NSDate *verify_date) {
bool result = false;
SecTrustRef trustRef = sec_trust_copy_ref(trust);
@ -328,8 +361,7 @@ box_apple_tls_client_t *box_apple_tls_client_create(
uint16_t min_version,
uint16_t max_version,
bool insecure,
const char *anchor_pem,
size_t anchor_pem_len,
void *anchors_cf,
bool anchor_only,
bool has_verify_time,
int64_t verify_time_unix_millis,
@ -346,9 +378,11 @@ box_apple_tls_client_t *box_apple_tls_client_create(
atomic_init(&client->ref_count, 1);
atomic_init(&client->ready, false);
atomic_init(&client->ready_done, false);
if (anchors_cf != NULL) {
client->anchors = (void *)CFRetain(anchors_cf);
}
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];
@ -372,13 +406,16 @@ box_apple_tls_client_t *box_apple_tls_client_create(
if (client->state.version == 0) {
box_apple_tls_state_load(metadata, &client->state);
}
complete(insecure || box_evaluate_trust(trust, anchors, anchor_only, verifyDate));
complete(insecure || box_evaluate_trust(trust, box_apple_tls_client_anchors(client), anchor_only, verifyDate));
}, box_apple_tls_client_queue(client));
}, NW_PARAMETERS_DEFAULT_CONFIGURATION);
nw_connection_t connection = box_apple_tls_create_connection(connected_socket, parameters);
if (connection == NULL) {
close(connected_socket);
if (client->anchors != NULL) {
CFRelease((CFTypeRef)client->anchors);
}
if (client->ready_semaphore != NULL) {
CFBridgingRelease(client->ready_semaphore);
}
@ -485,128 +522,202 @@ void box_apple_tls_client_free(box_apple_tls_client_t *client) {
}
ssize_t box_apple_tls_client_read(box_apple_tls_client_t *client, void *buffer, size_t buffer_len, int timeout_msec, bool *eof_out, char **error_out) {
nw_connection_t connection = box_apple_tls_connection(client);
if (connection == nil) {
box_set_error_message(error_out, "apple TLS: invalid client");
return -1;
}
@autoreleasepool {
nw_connection_t connection = box_apple_tls_connection(client);
if (connection == nil) {
box_set_error_message(error_out, "apple TLS: invalid client");
return -1;
}
dispatch_semaphore_t read_semaphore = dispatch_semaphore_create(0);
__block NSData *content_data = nil;
__block bool read_eof = false;
__block char *local_error = NULL;
dispatch_semaphore_t read_semaphore = dispatch_semaphore_create(0);
__block size_t content_len = 0;
__block bool read_eof = false;
__block char *local_error = NULL;
nw_connection_receive(connection, 1, (uint32_t)buffer_len, ^(dispatch_data_t content, nw_content_context_t context, bool is_complete, nw_error_t error) {
if (content != NULL) {
const void *mapped = NULL;
size_t mapped_len = 0;
dispatch_data_t mapped_data = dispatch_data_create_map(content, &mapped, &mapped_len);
if (mapped != NULL && mapped_len > 0) {
content_data = [NSData dataWithBytes:mapped length:mapped_len];
nw_connection_receive(connection, 1, (uint32_t)buffer_len, ^(dispatch_data_t content, nw_content_context_t context, bool is_complete, nw_error_t error) {
@autoreleasepool {
if (content != NULL) {
ssize_t copied = box_apple_tls_dispatch_data_copy(content, buffer, buffer_len, &local_error);
if (copied >= 0) {
content_len = (size_t)copied;
}
}
if (error != NULL && content_len == 0 && local_error == NULL) {
box_set_error_from_nw_error(&local_error, error);
}
if (is_complete && (context == NULL || nw_content_context_get_is_final(context))) {
read_eof = true;
}
dispatch_semaphore_signal(read_semaphore);
}
(void)mapped_data;
}
if (error != NULL && content_data.length == 0) {
box_set_error_from_nw_error(&local_error, error);
}
if (is_complete && (context == NULL || nw_content_context_get_is_final(context))) {
read_eof = true;
}
dispatch_semaphore_signal(read_semaphore);
});
});
dispatch_time_t wait_deadline = DISPATCH_TIME_FOREVER;
if (timeout_msec >= 0) {
wait_deadline = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC);
}
long wait_result = dispatch_semaphore_wait(read_semaphore, wait_deadline);
if (wait_result != 0) {
nw_connection_cancel(connection);
dispatch_semaphore_wait(read_semaphore, DISPATCH_TIME_FOREVER);
dispatch_time_t wait_deadline = DISPATCH_TIME_FOREVER;
if (timeout_msec >= 0) {
wait_deadline = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC);
}
long wait_result = dispatch_semaphore_wait(read_semaphore, wait_deadline);
if (wait_result != 0) {
nw_connection_cancel(connection);
dispatch_semaphore_wait(read_semaphore, DISPATCH_TIME_FOREVER);
if (local_error != NULL) {
free(local_error);
local_error = NULL;
}
return -2;
}
if (local_error != NULL) {
free(local_error);
local_error = NULL;
if (error_out != NULL) {
*error_out = local_error;
} else {
free(local_error);
}
return -1;
}
return -2;
}
if (local_error != NULL) {
if (error_out != NULL) {
*error_out = local_error;
} else {
free(local_error);
if (eof_out != NULL) {
*eof_out = read_eof;
}
return -1;
return (ssize_t)content_len;
}
if (eof_out != NULL) {
*eof_out = read_eof;
}
if (content_data == nil || content_data.length == 0) {
return 0;
}
memcpy(buffer, content_data.bytes, content_data.length);
return (ssize_t)content_data.length;
}
ssize_t box_apple_tls_client_write(box_apple_tls_client_t *client, const void *buffer, size_t buffer_len, int timeout_msec, char **error_out) {
nw_connection_t connection = box_apple_tls_connection(client);
if (connection == nil) {
box_set_error_message(error_out, "apple TLS: invalid client");
return -1;
}
if (buffer_len == 0) {
return 0;
}
void *content_copy = malloc(buffer_len);
dispatch_queue_t queue = box_apple_tls_client_queue(client);
if (content_copy == NULL) {
free(content_copy);
box_set_error_message(error_out, "apple TLS: out of memory");
return -1;
}
if (queue == nil) {
free(content_copy);
box_set_error_message(error_out, "apple TLS: invalid client");
return -1;
}
memcpy(content_copy, buffer, buffer_len);
dispatch_data_t content = dispatch_data_create(content_copy, buffer_len, queue, ^{
free(content_copy);
});
dispatch_semaphore_t write_semaphore = dispatch_semaphore_create(0);
__block char *local_error = NULL;
nw_connection_send(connection, content, NW_CONNECTION_DEFAULT_STREAM_CONTEXT, false, ^(nw_error_t error) {
if (error != NULL) {
box_set_error_from_nw_error(&local_error, error);
@autoreleasepool {
nw_connection_t connection = box_apple_tls_connection(client);
if (connection == nil) {
box_set_error_message(error_out, "apple TLS: invalid client");
return -1;
}
if (buffer_len == 0) {
return 0;
}
dispatch_semaphore_signal(write_semaphore);
});
dispatch_time_t wait_deadline = DISPATCH_TIME_FOREVER;
if (timeout_msec >= 0) {
wait_deadline = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC);
}
long wait_result = dispatch_semaphore_wait(write_semaphore, wait_deadline);
if (wait_result != 0) {
nw_connection_cancel(connection);
dispatch_semaphore_wait(write_semaphore, DISPATCH_TIME_FOREVER);
void *content_copy = malloc(buffer_len);
if (content_copy == NULL) {
box_set_error_message(error_out, "apple TLS: out of memory");
return -1;
}
dispatch_queue_t queue = box_apple_tls_client_queue(client);
if (queue == nil) {
free(content_copy);
box_set_error_message(error_out, "apple TLS: invalid client");
return -1;
}
memcpy(content_copy, buffer, buffer_len);
dispatch_data_t content = dispatch_data_create(content_copy, buffer_len, queue, ^{
free(content_copy);
});
dispatch_semaphore_t write_semaphore = dispatch_semaphore_create(0);
__block char *local_error = NULL;
nw_connection_send(connection, content, NW_CONNECTION_DEFAULT_STREAM_CONTEXT, false, ^(nw_error_t error) {
@autoreleasepool {
if (error != NULL) {
box_set_error_from_nw_error(&local_error, error);
}
dispatch_semaphore_signal(write_semaphore);
}
});
dispatch_time_t wait_deadline = DISPATCH_TIME_FOREVER;
if (timeout_msec >= 0) {
wait_deadline = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC);
}
long wait_result = dispatch_semaphore_wait(write_semaphore, wait_deadline);
if (wait_result != 0) {
nw_connection_cancel(connection);
dispatch_semaphore_wait(write_semaphore, DISPATCH_TIME_FOREVER);
if (local_error != NULL) {
free(local_error);
local_error = NULL;
}
return -2;
}
if (local_error != NULL) {
free(local_error);
local_error = NULL;
if (error_out != NULL) {
*error_out = local_error;
} else {
free(local_error);
}
return -1;
}
return -2;
return (ssize_t)buffer_len;
}
if (local_error != NULL) {
if (error_out != NULL) {
*error_out = local_error;
} else {
free(local_error);
}
bool box_apple_tls_client_read_async(box_apple_tls_client_t *client, size_t maximum_len, uintptr_t callback_handle, char **error_out) {
@autoreleasepool {
nw_connection_t connection = box_apple_tls_connection(client);
if (connection == nil) {
box_set_error_message(error_out, "apple TLS: invalid client");
return false;
}
return -1;
if (maximum_len == 0) {
box_set_error_message(error_out, "apple TLS: empty read buffer");
return false;
}
uint32_t receive_len = maximum_len > UINT32_MAX ? UINT32_MAX : (uint32_t)maximum_len;
nw_connection_receive(connection, 1, receive_len, ^(dispatch_data_t content, nw_content_context_t context, bool is_complete, nw_error_t error) {
@autoreleasepool {
box_apple_tls_read_result_t *result = calloc(1, sizeof(box_apple_tls_read_result_t));
if (result == NULL) {
box_apple_tls_read_callback(callback_handle, NULL);
return;
}
size_t content_size = content != NULL ? dispatch_data_get_size(content) : 0;
if (content_size > 0) {
result->content = (__bridge_retained void *)content;
}
if (error != NULL && content_size == 0) {
box_set_error_from_nw_error(&result->error, error);
}
if (is_complete && (context == NULL || nw_content_context_get_is_final(context))) {
result->eof = true;
}
box_apple_tls_read_callback(callback_handle, result);
}
});
return true;
}
return (ssize_t)buffer_len;
}
ssize_t box_apple_tls_read_result_copy(box_apple_tls_read_result_t *result, void *buffer, size_t buffer_len, bool *eof_out, char **error_out) {
@autoreleasepool {
if (result == NULL) {
box_set_error_message(error_out, "apple TLS: read result unavailable");
return -1;
}
if (result->error != NULL) {
if (error_out != NULL) {
*error_out = result->error;
result->error = NULL;
} else {
free(result->error);
result->error = NULL;
}
return -1;
}
if (eof_out != NULL) {
*eof_out = result->eof;
}
dispatch_data_t content = box_apple_tls_read_result_content(result);
if (content == nil) {
return 0;
}
return box_apple_tls_dispatch_data_copy(content, buffer, buffer_len, error_out);
}
}
void box_apple_tls_read_result_free(box_apple_tls_read_result_t *result) {
if (result == NULL) {
return;
}
free(result->error);
if (result->content != NULL) {
CFBridgingRelease(result->content);
}
free(result);
}
bool box_apple_tls_client_copy_state(box_apple_tls_client_t *client, box_apple_tls_state_t *state, char **error_out) {

View file

@ -0,0 +1,50 @@
//go:build darwin && cgo
package tls
import (
"bytes"
"strings"
"testing"
)
func TestAppleTLSDispatchDataCopySegments(t *testing.T) {
first := []byte("hello ")
second := []byte("world")
buffer := make([]byte, len(first)+len(second))
n, errorMessage := appleTLSCopyDispatchDataForTest(first, second, buffer)
if n < 0 {
t.Fatalf("copy failed: %s", errorMessage)
}
if int(n) != len(buffer) {
t.Fatalf("copied %d bytes, want %d", n, len(buffer))
}
if !bytes.Equal(buffer, []byte("hello world")) {
t.Fatalf("unexpected copy result: %q", string(buffer))
}
}
func TestAppleTLSDispatchDataCopyRejectsSmallBuffer(t *testing.T) {
first := []byte("hello")
second := []byte("world")
buffer := make([]byte, len(first)+len(second)-1)
n, errorMessage := appleTLSCopyDispatchDataForTest(first, second, buffer)
if n != -1 {
t.Fatalf("copied %d bytes, want error", n)
}
if !strings.Contains(errorMessage, "read buffer too small") {
t.Fatalf("unexpected error: %q", errorMessage)
}
}
func TestAppleTLSDispatchDataCopyEmpty(t *testing.T) {
n, errorMessage := appleTLSCopyDispatchDataForTest(nil, nil, nil)
if n != 0 {
t.Fatalf("copied %d bytes, want 0", n)
}
if errorMessage != "" {
t.Fatalf("unexpected error: %q", errorMessage)
}
}

View file

@ -0,0 +1,44 @@
//go:build darwin && cgo
package tls
/*
#include <stdlib.h>
#include "apple_client_platform_darwin.h"
*/
import "C"
import "unsafe"
func appleTLSCopyDispatchDataForTest(first, second []byte, buffer []byte) (int, string) {
var firstPtr unsafe.Pointer
if len(first) > 0 {
firstPtr = C.CBytes(first)
defer C.free(firstPtr)
}
var secondPtr unsafe.Pointer
if len(second) > 0 {
secondPtr = C.CBytes(second)
defer C.free(secondPtr)
}
var bufferPtr unsafe.Pointer
if len(buffer) > 0 {
bufferPtr = unsafe.Pointer(&buffer[0])
}
var errPtr *C.char
n := C.box_apple_tls_copy_dispatch_data_for_test(
firstPtr,
C.size_t(len(first)),
secondPtr,
C.size_t(len(second)),
bufferPtr,
C.size_t(len(buffer)),
&errPtr,
)
if errPtr == nil {
return int(n), ""
}
errorMessage := C.GoString(errPtr)
C.free(unsafe.Pointer(errPtr))
return int(n), errorMessage
}

View file

@ -3,17 +3,21 @@
package tls
import (
"bytes"
"context"
stdtls "crypto/tls"
"errors"
"io"
"net"
"os"
"testing"
"time"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/json/badoption"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
)
const appleTLSTestTimeout = 5 * time.Second
@ -28,6 +32,11 @@ type appleTLSServerResult struct {
err error
}
var (
_ N.ExtendedConn = (*appleTLSConn)(nil)
_ N.ReadWaitCreator = (*appleTLSConn)(nil)
)
func TestAppleClientHandshakeAppliesALPNAndVersion(t *testing.T) {
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
for index := 0; index < appleTLSSuccessHandshakeLoops; index++ {
@ -75,6 +84,29 @@ func TestAppleClientHandshakeAppliesALPNAndVersion(t *testing.T) {
}
}
func TestAppleClientHandshakeRejectsOpaqueConn(t *testing.T) {
clientConfig, err := NewClientWithOptions(ClientOptions{
Context: context.Background(),
Logger: logger.NOP(),
Options: option.OutboundTLSOptions{
Enabled: true,
Engine: "apple",
ServerName: "localhost",
Insecure: true,
},
})
if err != nil {
t.Fatal(err)
}
clientConn, serverConn := net.Pipe()
defer clientConn.Close()
defer serverConn.Close()
_, err = ClientHandshake(context.Background(), clientConn, clientConfig)
if err == nil {
t.Fatal("expected handshake to reject non-TCP connection")
}
}
func TestAppleClientHandshakeRejectsVersionMismatch(t *testing.T) {
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
@ -209,6 +241,237 @@ func TestAppleClientHandshakeRecoversAfterFailure(t *testing.T) {
}
}
func TestAppleClientConfigCloneWithInlineCertificate(t *testing.T) {
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
clientConfig, err := NewClientWithOptions(ClientOptions{
Context: context.Background(),
Logger: logger.NOP(),
Options: option.OutboundTLSOptions{
Enabled: true,
Engine: "apple",
ServerName: "localhost",
MinVersion: "1.2",
MaxVersion: "1.2",
ALPN: badoption.Listable[string]{"h2", "http/1.1"},
Certificate: badoption.Listable[string]{serverCertificatePEM},
},
})
if err != nil {
t.Fatal(err)
}
clone := clientConfig.Clone()
clone.SetServerName("other")
clone.SetNextProtos([]string{"http/1.1"})
if clientConfig.ServerName() == "other" {
t.Fatal("Clone shares server name with original")
}
nextProtos := clientConfig.NextProtos()
if len(nextProtos) != 2 || nextProtos[0] != "h2" || nextProtos[1] != "http/1.1" {
t.Fatalf("Clone shares ALPN slice with original: %v", nextProtos)
}
for index := 0; index < appleTLSFailureRecoveryLoops; index++ {
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
Certificates: []stdtls.Certificate{serverCertificate},
MinVersion: stdtls.VersionTLS12,
MaxVersion: stdtls.VersionTLS12,
NextProtos: []string{"h2"},
})
handshakeConfig := clientConfig.Clone()
handshakeConfig.SetNextProtos([]string{"h2"})
clientConn, err := dialAppleTestClientConn(t, serverAddress, handshakeConfig)
if err != nil {
t.Fatalf("iteration %d: %v", index, err)
}
clientState := clientConn.ConnectionState()
if clientState.NegotiatedProtocol != "h2" {
_ = clientConn.Close()
t.Fatalf("iteration %d: unexpected negotiated protocol: %q", index, clientState.NegotiatedProtocol)
}
_ = clientConn.Close()
result := <-serverResult
if result.err != nil {
t.Fatalf("iteration %d: %v", index, result.err)
}
}
}
func TestAppleClientReadBuffer(t *testing.T) {
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
payload := []byte("apple tls read buffer payload")
serverResult, serverAddress := startAppleTLSIOTestServer(t, &stdtls.Config{
Certificates: []stdtls.Certificate{serverCertificate},
MinVersion: stdtls.VersionTLS12,
MaxVersion: stdtls.VersionTLS12,
}, func(conn *stdtls.Conn) error {
_, err := conn.Write(payload)
return err
})
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
Enabled: true,
Engine: "apple",
ServerName: "localhost",
MinVersion: "1.2",
MaxVersion: "1.2",
Certificate: badoption.Listable[string]{serverCertificatePEM},
})
if err != nil {
t.Fatal(err)
}
defer clientConn.Close()
extendedConn := clientConn.(N.ExtendedConn)
const (
frontHeadroom = 17
rearHeadroom = 19
)
buffer := buf.NewSize(frontHeadroom + len(payload) + rearHeadroom)
defer buffer.Release()
buffer.Resize(frontHeadroom, 0)
buffer.Reserve(rearHeadroom)
err = extendedConn.ReadBuffer(buffer)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(buffer.Bytes(), payload) {
t.Fatalf("unexpected payload: %q", buffer.Bytes())
}
if buffer.Start() != frontHeadroom {
t.Fatalf("unexpected front headroom: %d", buffer.Start())
}
if buffer.FreeLen() != 0 {
t.Fatalf("unexpected reserved free length before PostReturn: %d", buffer.FreeLen())
}
buffer.OverCap(rearHeadroom)
if buffer.FreeLen() != rearHeadroom {
t.Fatalf("unexpected rear headroom after PostReturn: %d", buffer.FreeLen())
}
if err = <-serverResult; err != nil {
t.Fatal(err)
}
}
func TestAppleClientWriteBuffer(t *testing.T) {
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
payload := bytes.Repeat([]byte("apple-write-buffer-"), 3000)
serverResult, serverAddress := startAppleTLSIOTestServer(t, &stdtls.Config{
Certificates: []stdtls.Certificate{serverCertificate},
MinVersion: stdtls.VersionTLS12,
MaxVersion: stdtls.VersionTLS12,
}, func(conn *stdtls.Conn) error {
received := make([]byte, len(payload))
_, err := io.ReadFull(conn, received)
if err != nil {
return err
}
if !bytes.Equal(received, payload) {
return errors.New("payload mismatch")
}
return nil
})
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
Enabled: true,
Engine: "apple",
ServerName: "localhost",
MinVersion: "1.2",
MaxVersion: "1.2",
Certificate: badoption.Listable[string]{serverCertificatePEM},
})
if err != nil {
t.Fatal(err)
}
defer clientConn.Close()
extendedConn := clientConn.(N.ExtendedConn)
buffer := buf.NewSize(len(payload))
_, err = buffer.Write(payload)
if err != nil {
t.Fatal(err)
}
err = extendedConn.WriteBuffer(buffer)
if err != nil {
t.Fatal(err)
}
if buffer.RawCap() != 0 {
t.Fatalf("buffer was not released: raw cap %d", buffer.RawCap())
}
if err = <-serverResult; err != nil {
t.Fatal(err)
}
}
func TestAppleClientCreateReadWaiter(t *testing.T) {
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
payload := []byte("apple tls read waiter payload")
serverResult, serverAddress := startAppleTLSIOTestServer(t, &stdtls.Config{
Certificates: []stdtls.Certificate{serverCertificate},
MinVersion: stdtls.VersionTLS12,
MaxVersion: stdtls.VersionTLS12,
}, func(conn *stdtls.Conn) error {
_, err := conn.Write(payload)
return err
})
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
Enabled: true,
Engine: "apple",
ServerName: "localhost",
MinVersion: "1.2",
MaxVersion: "1.2",
Certificate: badoption.Listable[string]{serverCertificatePEM},
})
if err != nil {
t.Fatal(err)
}
defer clientConn.Close()
readWaitCreator := clientConn.(N.ReadWaitCreator)
readWaiter, ok := readWaitCreator.CreateReadWaiter()
if !ok {
t.Fatal("expected read waiter")
}
const (
frontHeadroom = 11
rearHeadroom = 13
)
needCopy := readWaiter.InitializeReadWaiter(N.ReadWaitOptions{
FrontHeadroom: frontHeadroom,
RearHeadroom: rearHeadroom,
MTU: len(payload),
})
if needCopy {
t.Fatal("expected owned read waiter buffer")
}
buffer, err := readWaiter.WaitReadBuffer()
if err != nil {
t.Fatal(err)
}
defer buffer.Release()
if !bytes.Equal(buffer.Bytes(), payload) {
t.Fatalf("unexpected payload: %q", buffer.Bytes())
}
if buffer.Start() != frontHeadroom {
t.Fatalf("unexpected front headroom: %d", buffer.Start())
}
if buffer.FreeLen() != rearHeadroom {
t.Fatalf("unexpected rear headroom: %d", buffer.FreeLen())
}
if buffer.Cap() != buffer.RawCap() {
t.Fatalf("capacity was not restored: cap=%d raw=%d", buffer.Cap(), buffer.RawCap())
}
if err = <-serverResult; err != nil {
t.Fatal(err)
}
}
func TestAppleClientReadDeadline(t *testing.T) {
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
serverDone, serverAddress := startAppleTLSSilentServer(t, &stdtls.Config{
@ -359,7 +622,52 @@ func startAppleTLSSilentServer(t *testing.T, tlsConfig *stdtls.Config) (chan<- s
return done, listener.Addr().String()
}
func newAppleTestCertificate(t *testing.T, serverName string) (stdtls.Certificate, string) {
func startAppleTLSIOTestServer(t testing.TB, tlsConfig *stdtls.Config, handler func(*stdtls.Conn) error) (<-chan error, string) {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
listener.Close()
})
if tcpListener, isTCP := listener.(*net.TCPListener); isTCP {
err = tcpListener.SetDeadline(time.Now().Add(appleTLSTestTimeout))
if err != nil {
t.Fatal(err)
}
}
result := make(chan error, 1)
go func() {
conn, err := listener.Accept()
if err != nil {
result <- err
return
}
defer conn.Close()
err = conn.SetDeadline(time.Now().Add(appleTLSTestTimeout))
if err != nil {
result <- err
return
}
tlsConn := stdtls.Server(conn, tlsConfig)
defer tlsConn.Close()
err = tlsConn.Handshake()
if err != nil {
result <- err
return
}
result <- handler(tlsConn)
}()
return result, listener.Addr().String()
}
func newAppleTestCertificate(t testing.TB, serverName string) (stdtls.Certificate, string) {
t.Helper()
privateKeyPEM, certificatePEM, err := GenerateCertificate(nil, nil, time.Now, serverName, time.Now().Add(time.Hour))
@ -423,14 +731,11 @@ func startAppleTLSTestServer(t *testing.T, tlsConfig *stdtls.Config) (<-chan app
return result, listener.Addr().String()
}
func newAppleTestClientConn(t *testing.T, serverAddress string, options option.OutboundTLSOptions) (Conn, error) {
func newAppleTestClientConn(t testing.TB, serverAddress string, options option.OutboundTLSOptions) (Conn, error) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), appleTLSTestTimeout)
t.Cleanup(cancel)
clientConfig, err := NewClientWithOptions(ClientOptions{
Context: ctx,
Context: context.Background(),
Logger: logger.NOP(),
ServerAddress: "",
Options: options,
@ -438,6 +743,14 @@ func newAppleTestClientConn(t *testing.T, serverAddress string, options option.O
if err != nil {
return nil, err
}
return dialAppleTestClientConn(t, serverAddress, clientConfig)
}
func dialAppleTestClientConn(t testing.TB, serverAddress string, clientConfig Config) (Conn, error) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), appleTLSTestTimeout)
t.Cleanup(cancel)
conn, err := net.DialTimeout("tcp", serverAddress, appleTLSTestTimeout)
if err != nil {

View file

@ -98,9 +98,11 @@ func NewClientWithOptions(options ClientOptions) (Config, error) {
options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx")
}
switch options.Options.Engine {
case C.TLSEngineDefault, "go":
case "", C.TLSEngineGo:
case C.TLSEngineApple:
return newAppleClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
case C.TLSEngineWindows:
return newWindowsClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
default:
return nil, E.New("unknown tls engine: ", options.Options.Engine)
}

View file

@ -271,19 +271,7 @@ func verifyConnection(rootCAs *x509.CertPool, timeFunc func() time.Time, serverN
if serverName == "" {
return errMissingServerName
}
verifyOptions := x509.VerifyOptions{
Roots: rootCAs,
DNSName: serverName,
Intermediates: x509.NewCertPool(),
}
for _, cert := range state.PeerCertificates[1:] {
verifyOptions.Intermediates.AddCert(cert)
}
if timeFunc != nil {
verifyOptions.CurrentTime = timeFunc()
}
_, err := state.PeerCertificates[0].Verify(verifyOptions)
return err
return verifySystemTLSPeer(rootCAs, serverName, timeFunc, state.PeerCertificates)
}
}

218
common/tls/system_client.go Normal file
View file

@ -0,0 +1,218 @@
package tls
import (
"context"
"crypto/x509"
"net"
"os"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service"
)
type systemTLSConfig struct {
serverName string
nextProtos []string
handshakeTimeout time.Duration
minVersion uint16
maxVersion uint16
insecure bool
anchorOnly bool
certificatePublicKeySHA256 [][]byte
timeFunc func() time.Time
store adapter.CertificateStore
}
func (c *systemTLSConfig) ServerName() string {
return c.serverName
}
func (c *systemTLSConfig) SetServerName(serverName string) {
c.serverName = serverName
}
func (c *systemTLSConfig) NextProtos() []string {
return c.nextProtos
}
func (c *systemTLSConfig) SetNextProtos(nextProto []string) {
c.nextProtos = append([]string(nil), nextProto...)
}
func (c *systemTLSConfig) HandshakeTimeout() time.Duration {
return c.handshakeTimeout
}
func (c *systemTLSConfig) SetHandshakeTimeout(timeout time.Duration) {
c.handshakeTimeout = timeout
}
func (c *systemTLSConfig) STDConfig() (*STDConfig, error) {
return nil, E.New("STDConfig is unsupported for the system TLS engine")
}
func (c *systemTLSConfig) Client(conn net.Conn) (Conn, error) {
return nil, os.ErrInvalid
}
func (c *systemTLSConfig) clone() systemTLSConfig {
return systemTLSConfig{
serverName: c.serverName,
nextProtos: append([]string(nil), c.nextProtos...),
handshakeTimeout: c.handshakeTimeout,
minVersion: c.minVersion,
maxVersion: c.maxVersion,
insecure: c.insecure,
anchorOnly: c.anchorOnly,
certificatePublicKeySHA256: append([][]byte(nil), c.certificatePublicKeySHA256...),
timeFunc: c.timeFunc,
store: c.store,
}
}
type SystemTLSValidated struct {
MinVersion uint16
MaxVersion uint16
UserPEM []byte
Exclusive bool
Store adapter.CertificateStore
}
func ValidateSystemTLSOptions(ctx context.Context, options option.OutboundTLSOptions, engineName string) (SystemTLSValidated, error) {
if options.Reality != nil && options.Reality.Enabled {
return SystemTLSValidated{}, E.New("reality is unsupported in ", engineName)
}
if options.UTLS != nil && options.UTLS.Enabled {
return SystemTLSValidated{}, E.New("utls is unsupported in ", engineName)
}
if options.ECH != nil && options.ECH.Enabled {
return SystemTLSValidated{}, E.New("ech is unsupported in ", engineName)
}
if options.DisableSNI {
return SystemTLSValidated{}, E.New("disable_sni is unsupported in ", engineName)
}
if len(options.CipherSuites) > 0 {
return SystemTLSValidated{}, E.New("cipher_suites is unsupported in ", engineName)
}
if len(options.CurvePreferences) > 0 {
return SystemTLSValidated{}, E.New("curve_preferences is unsupported in ", engineName)
}
if len(options.ClientCertificate) > 0 || options.ClientCertificatePath != "" || len(options.ClientKey) > 0 || options.ClientKeyPath != "" {
return SystemTLSValidated{}, E.New("client certificate is unsupported in ", engineName)
}
if options.Fragment || options.RecordFragment {
return SystemTLSValidated{}, E.New("tls fragment is unsupported in ", engineName)
}
if options.KernelTx || options.KernelRx {
return SystemTLSValidated{}, E.New("ktls is unsupported in ", engineName)
}
if options.Spoof != "" || options.SpoofMethod != "" {
return SystemTLSValidated{}, E.New("spoof is unsupported in ", engineName)
}
if len(options.CertificatePublicKeySHA256) > 0 && (len(options.Certificate) > 0 || options.CertificatePath != "") {
return SystemTLSValidated{}, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path")
}
var minVersion uint16
if options.MinVersion != "" {
parsed, err := ParseTLSVersion(options.MinVersion)
if err != nil {
return SystemTLSValidated{}, E.Cause(err, "parse min_version")
}
minVersion = parsed
}
var maxVersion uint16
if options.MaxVersion != "" {
parsed, err := ParseTLSVersion(options.MaxVersion)
if err != nil {
return SystemTLSValidated{}, E.Cause(err, "parse max_version")
}
maxVersion = parsed
}
userPEM, exclusive, store, err := resolveSystemAnchors(ctx, options)
if err != nil {
return SystemTLSValidated{}, err
}
return SystemTLSValidated{
MinVersion: minVersion,
MaxVersion: maxVersion,
UserPEM: userPEM,
Exclusive: exclusive,
Store: store,
}, nil
}
func resolveSystemAnchors(ctx context.Context, options option.OutboundTLSOptions) ([]byte, bool, adapter.CertificateStore, error) {
if len(options.Certificate) > 0 {
return []byte(strings.Join(options.Certificate, "\n")), true, nil, nil
}
if options.CertificatePath != "" {
content, err := os.ReadFile(options.CertificatePath)
if err != nil {
return nil, false, nil, E.Cause(err, "read certificate")
}
return content, true, nil, nil
}
store := service.FromContext[adapter.CertificateStore](ctx)
if store == nil {
return nil, false, nil, nil
}
return nil, store.ExclusiveAnchors(), store, nil
}
func newSystemTLSConfig(ctx context.Context, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool, engineName string) (systemTLSConfig, SystemTLSValidated, error) {
validated, err := ValidateSystemTLSOptions(ctx, options, engineName)
if err != nil {
return systemTLSConfig{}, SystemTLSValidated{}, err
}
var serverName string
if options.ServerName != "" {
serverName = options.ServerName
} else if serverAddress != "" {
serverName = serverAddress
}
if serverName == "" && !options.Insecure && !allowEmptyServerName {
return systemTLSConfig{}, SystemTLSValidated{}, errMissingServerName
}
handshakeTimeout := C.TCPTimeout
if options.HandshakeTimeout > 0 {
handshakeTimeout = options.HandshakeTimeout.Build()
}
return systemTLSConfig{
serverName: serverName,
nextProtos: append([]string(nil), options.ALPN...),
handshakeTimeout: handshakeTimeout,
minVersion: validated.MinVersion,
maxVersion: validated.MaxVersion,
insecure: options.Insecure || len(options.CertificatePublicKeySHA256) > 0,
anchorOnly: validated.Exclusive,
certificatePublicKeySHA256: append([][]byte(nil), options.CertificatePublicKeySHA256...),
timeFunc: ntp.TimeFuncFromContext(ctx),
store: validated.Store,
}, validated, nil
}
func verifySystemTLSPeer(roots *x509.CertPool, serverName string, timeFunc func() time.Time, peerCertificates []*x509.Certificate) error {
if len(peerCertificates) == 0 {
return E.New("no peer certificates")
}
intermediates := x509.NewCertPool()
for _, cert := range peerCertificates[1:] {
intermediates.AddCert(cert)
}
verifyOptions := x509.VerifyOptions{
Roots: roots,
Intermediates: intermediates,
DNSName: serverName,
}
if timeFunc != nil {
verifyOptions.CurrentTime = timeFunc()
}
_, err := peerCertificates[0].Verify(verifyOptions)
return err
}

View file

@ -0,0 +1,848 @@
//go:build windows
package tls
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"io"
"net"
"os"
"sync"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/common/schannel"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
)
const (
windowsTLSEngineName = "Windows TLS engine"
handshakeReadChunkSize = 8192
readScratchSize = 16 * 1024
readWaitCiphertextChunkSize = 4096
)
type windowsClientConfig struct {
systemTLSConfig
userRoots *x509.CertPool
}
func (c *windowsClientConfig) Clone() Config {
return &windowsClientConfig{
systemTLSConfig: c.systemTLSConfig.clone(),
userRoots: c.userRoots,
}
}
func newWindowsClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
err := schannel.CheckPlatform()
if err != nil {
return nil, err
}
base, validated, err := newSystemTLSConfig(ctx, serverAddress, options, allowEmptyServerName, windowsTLSEngineName)
if err != nil {
return nil, err
}
var userRoots *x509.CertPool
if len(validated.UserPEM) > 0 {
userRoots = x509.NewCertPool()
if !userRoots.AppendCertsFromPEM(validated.UserPEM) {
return nil, E.New("parse certificate PEM")
}
}
return &windowsClientConfig{
systemTLSConfig: base,
userRoots: userRoots,
}, nil
}
func (c *windowsClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (Conn, error) {
deadline, hasDeadline := ctx.Deadline()
if hasDeadline {
deadlineErr := conn.SetDeadline(deadline)
if deadlineErr != nil {
return nil, E.Cause(deadlineErr, "set handshake deadline")
}
defer conn.SetDeadline(time.Time{})
}
client, err := schannel.NewClientContext(c.minVersion, c.maxVersion, c.serverName, c.nextProtos)
if err != nil {
return nil, err
}
handshakeOK := false
defer func() {
if !handshakeOK {
client.Close()
}
}()
stopCancel := installHandshakeCancel(ctx, conn)
defer stopCancel()
scratch := make([]byte, handshakeReadChunkSize)
leftover, err := driveHandshake(ctx, conn, client, scratch)
if err != nil {
return nil, err
}
state, rawCerts, err := buildConnectionState(c.serverName, client)
if err != nil {
return nil, err
}
err = c.verifyPeerCertificates(state.PeerCertificates)
if err != nil {
return nil, err
}
if len(c.certificatePublicKeySHA256) > 0 {
err = VerifyPublicKeySHA256(c.certificatePublicKeySHA256, rawCerts)
if err != nil {
return nil, err
}
}
header, trailer, maxMessage, err := client.StreamSizes()
if err != nil {
return nil, err
}
handshakeOK = true
tlsConn := &windowsTLSConn{
rawConn: conn,
client: client,
state: state,
header: header,
trailer: trailer,
maxMessage: maxMessage,
cipher: leftover,
}
return tlsConn, nil
}
func driveHandshake(ctx context.Context, conn net.Conn, client *schannel.ClientContext, scratch []byte) ([]byte, error) {
readMore := func() ([]byte, error) {
more, err := readTLSRaw(conn, scratch, true)
if err != nil {
return nil, handshakeIOError(ctx, err, "read handshake")
}
return more, nil
}
writeOut := func(data []byte) error {
_, err := conn.Write(data)
if err != nil {
return handshakeIOError(ctx, err, "write handshake")
}
return nil
}
leftover, err := driveSteps(nil, client.Step, readMore, writeOut)
if err != nil {
return nil, E.Cause(err, "tls handshake")
}
return leftover, nil
}
func driveSteps(
initial []byte,
step func([]byte) (schannel.StepResult, error),
readMore func() ([]byte, error),
writeOut func([]byte) error,
) ([]byte, error) {
buffer := initial
for {
result, stepErr := step(buffer)
if stepErr != nil {
return nil, stepErr
}
if len(result.Output) > 0 {
writeErr := writeOut(result.Output)
if writeErr != nil {
return nil, writeErr
}
}
if result.Incomplete {
// readMore reuses scratch storage, so keep the buffered handshake
// bytes in stable memory before the next read overwrites them.
buffer = append([]byte(nil), buffer...)
more, readErr := readMore()
if readErr != nil {
return nil, readErr
}
buffer = append(buffer, more...)
continue
}
if result.Consumed > len(buffer) {
return nil, E.New("schannel: Consumed > input length")
}
buffer = buffer[result.Consumed:]
if result.Done {
return buffer, nil
}
if len(buffer) == 0 {
more, readErr := readMore()
if readErr != nil {
return nil, readErr
}
buffer = append(buffer, more...)
}
}
}
// installHandshakeCancel unblocks an in-flight read/write by forcing an
// immediate deadline on conn when ctx is cancelled. The returned cleanup
// waits for a racing cancel to finish and clears the forced deadline.
func installHandshakeCancel(ctx context.Context, conn net.Conn) func() {
var fired atomic.Bool
done := make(chan struct{})
stop := context.AfterFunc(ctx, func() {
defer close(done)
fired.Store(true)
_ = conn.SetDeadline(time.Now())
})
return func() {
if stop() {
return
}
<-done
if fired.Load() {
_ = conn.SetDeadline(time.Time{})
}
}
}
func handshakeIOError(ctx context.Context, err error, message string) error {
ctxErr := ctx.Err()
if ctxErr != nil && isTimeoutError(err) {
return ctxErr
}
return E.Cause(err, message)
}
func readTLSRaw(conn net.Conn, scratch []byte, requireMore bool) ([]byte, error) {
n, err := conn.Read(scratch)
if n > 0 {
return scratch[:n], nil
}
if err != nil {
if requireMore && errors.Is(err, io.EOF) {
return nil, io.ErrUnexpectedEOF
}
return nil, err
}
return nil, io.ErrUnexpectedEOF
}
func isTimeoutError(err error) bool {
if errors.Is(err, os.ErrDeadlineExceeded) {
return true
}
var netErr net.Error
return errors.As(err, &netErr) && netErr.Timeout()
}
func buildConnectionState(serverName string, client *schannel.ClientContext) (tls.ConnectionState, [][]byte, error) {
version, cipherSuite, err := client.ConnectionInfo()
if err != nil {
return tls.ConnectionState{}, nil, err
}
alpn, err := client.ApplicationProtocol()
if err != nil {
return tls.ConnectionState{}, nil, err
}
rawCerts, err := client.RemoteCertificateChain()
if err != nil {
return tls.ConnectionState{}, nil, err
}
peerCertificates := make([]*x509.Certificate, 0, len(rawCerts))
for index, der := range rawCerts {
cert, parseErr := x509.ParseCertificate(der)
if parseErr != nil {
return tls.ConnectionState{}, nil, E.Cause(parseErr, "parse peer certificate ", index)
}
peerCertificates = append(peerCertificates, cert)
}
return tls.ConnectionState{
Version: version,
HandshakeComplete: true,
CipherSuite: cipherSuite,
NegotiatedProtocol: alpn,
ServerName: serverName,
PeerCertificates: peerCertificates,
}, rawCerts, nil
}
func (c *windowsClientConfig) verifyPeerCertificates(peerCertificates []*x509.Certificate) error {
if c.insecure {
return nil
}
var roots *x509.CertPool
switch {
case c.userRoots != nil:
roots = c.userRoots
case c.store != nil:
roots = c.store.Pool()
}
return verifySystemTLSPeer(roots, c.serverName, c.timeFunc, peerCertificates)
}
type windowsTLSConn struct {
rawConn net.Conn
client *schannel.ClientContext
state tls.ConnectionState
header uint32
trailer uint32
maxMessage uint32
readAccess sync.Mutex
writeAccess sync.Mutex
contextAccess sync.RWMutex
writeState sync.Mutex
writeStateOnce sync.Once
writeReady *sync.Cond
postHandshake bool
writeActive bool
cipher []byte
plain []byte
readScratch []byte
writeScratch []byte
readEOF bool
deadlineAccess sync.Mutex
readDeadline time.Time
writeDeadline time.Time
closed atomic.Bool
}
var (
_ N.ExtendedConn = (*windowsTLSConn)(nil)
_ N.ReadWaitCreator = (*windowsTLSConn)(nil)
)
type (
windowsTLSAppendCipherFunc func(requireMore bool) error
windowsTLSReadRawFunc func(requireMore bool) ([]byte, error)
)
func (c *windowsTLSConn) Read(p []byte) (int, error) {
c.readAccess.Lock()
defer c.readAccess.Unlock()
if len(p) == 0 {
return 0, nil
}
if c.isClosed() {
return 0, net.ErrClosed
}
return c.readIntoLocked(p, c.appendRaw, c.readRaw)
}
func (c *windowsTLSConn) ReadBuffer(buffer *buf.Buffer) error {
c.readAccess.Lock()
defer c.readAccess.Unlock()
if buffer.IsFull() {
return io.ErrShortBuffer
}
if c.isClosed() {
return net.ErrClosed
}
startLen := buffer.Len()
n, err := c.readIntoLocked(buffer.FreeBytes(), c.appendRaw, c.readRaw)
buffer.Truncate(startLen + n)
return err
}
func (c *windowsTLSConn) readIntoLocked(p []byte, appendCipher windowsTLSAppendCipherFunc, readRaw windowsTLSReadRawFunc) (int, error) {
plaintext, err := c.readPlaintextLocked(appendCipher, readRaw)
if err != nil {
return 0, err
}
n := copy(p, plaintext)
if n < len(plaintext) {
c.plain = append([]byte(nil), plaintext[n:]...)
}
return n, nil
}
func (c *windowsTLSConn) readPlaintextLocked(appendCipher windowsTLSAppendCipherFunc, readRaw windowsTLSReadRawFunc) ([]byte, error) {
if len(c.plain) > 0 {
plaintext := c.plain
c.plain = nil
return plaintext, nil
}
if c.readEOF {
return nil, io.EOF
}
cleanup, err := c.applyReadDeadline()
if err != nil {
return nil, err
}
defer cleanup()
for {
if len(c.cipher) > 0 {
result, decryptErr := c.decrypt(c.cipher)
if decryptErr != nil {
return nil, decryptErr
}
if result.Expired {
c.readEOF = true
return nil, io.EOF
}
if !result.Incomplete {
plaintext := result.Plaintext
if result.Renegotiate && len(plaintext) > 0 {
plaintext = append([]byte(nil), plaintext...)
}
nextCipher := c.cipher[result.ConsumedTotal:]
if len(result.RenegotiateToken) > 0 {
nextCipher = result.RenegotiateToken
}
c.cipher = nextCipher
if len(c.cipher) == 0 {
c.cipher = nil
}
if result.Renegotiate {
postErr := c.drivePostHandshake(readRaw)
if postErr != nil {
return nil, postErr
}
}
if len(plaintext) > 0 {
return plaintext, nil
}
continue
}
}
err = appendCipher(len(c.cipher) > 0)
if err != nil {
return nil, err
}
}
}
func (c *windowsTLSConn) drivePostHandshake(readRaw windowsTLSReadRawFunc) error {
initial := c.cipher
c.cipher = nil
err := c.beginPostHandshakeWrite()
if err != nil {
return err
}
defer c.finishPostHandshakeWrite()
c.contextAccess.Lock()
if c.client == nil {
c.contextAccess.Unlock()
return net.ErrClosed
}
writeFailed := false
readMore := func() ([]byte, error) {
more, err := readRaw(true)
if err != nil {
return nil, E.Cause(err, "tls post-handshake read")
}
return more, nil
}
writeOut := func(data []byte) error {
err := c.writePostHandshakeReplyLocked(data)
if err != nil {
writeFailed = true
return E.Cause(err, "tls post-handshake write")
}
return nil
}
leftover, err := driveSteps(initial, c.client.PostHandshake, readMore, writeOut)
c.contextAccess.Unlock()
if err != nil {
if writeFailed {
_ = c.Close()
}
return E.Cause(err, "tls post-handshake")
}
if len(leftover) > 0 {
c.cipher = leftover
}
return nil
}
func (c *windowsTLSConn) writePostHandshakeReplyLocked(data []byte) error {
c.deadlineAccess.Lock()
deadline := c.readDeadline
c.deadlineAccess.Unlock()
cleanup, err := c.applyDeadline(deadline, c.rawConn.SetWriteDeadline)
if err != nil {
return err
}
defer cleanup()
_, err = c.rawConn.Write(data)
return err
}
func (c *windowsTLSConn) decrypt(input []byte) (schannel.DecryptResult, error) {
c.contextAccess.RLock()
defer c.contextAccess.RUnlock()
if c.client == nil {
return schannel.DecryptResult{}, net.ErrClosed
}
return c.client.Decrypt(input)
}
func (c *windowsTLSConn) encrypt(plaintext []byte) ([]byte, error) {
c.contextAccess.RLock()
defer c.contextAccess.RUnlock()
if c.client == nil {
return nil, net.ErrClosed
}
if c.writeScratch == nil {
c.writeScratch = make([]byte, int(c.header)+int(c.maxMessage)+int(c.trailer))
}
return c.client.Encrypt(c.header, c.trailer, plaintext, c.writeScratch)
}
func (c *windowsTLSConn) readRaw(requireMore bool) ([]byte, error) {
if c.readScratch == nil {
c.readScratch = make([]byte, readScratchSize)
}
return readTLSRaw(c.rawConn, c.readScratch, requireMore)
}
func (c *windowsTLSConn) appendRaw(requireMore bool) error {
more, err := c.readRaw(requireMore)
if err != nil {
return err
}
c.cipher = append(c.cipher, more...)
return nil
}
func (c *windowsTLSConn) Write(p []byte) (int, error) {
err := c.beginWrite()
if err != nil {
return 0, err
}
defer c.finishWrite()
if len(p) == 0 {
return 0, nil
}
if c.isClosed() {
return 0, net.ErrClosed
}
cleanup, err := c.applyWriteDeadline()
if err != nil {
return 0, err
}
defer cleanup()
total := 0
chunkSize := int(c.maxMessage)
for len(p) > 0 {
chunk := p
if len(chunk) > chunkSize {
chunk = chunk[:chunkSize]
}
encrypted, encryptErr := c.encrypt(chunk)
if encryptErr != nil {
if errors.Is(encryptErr, net.ErrClosed) {
return total, net.ErrClosed
}
return total, E.Cause(encryptErr, "tls encrypt")
}
_, writeErr := c.rawConn.Write(encrypted)
if writeErr != nil {
_ = c.Close()
return total, E.Cause(writeErr, "tls write")
}
total += len(chunk)
p = p[len(chunk):]
}
return total, nil
}
func (c *windowsTLSConn) WriteBuffer(buffer *buf.Buffer) error {
defer buffer.Release()
_, err := c.Write(buffer.Bytes())
return err
}
func (c *windowsTLSConn) CreateReadWaiter() (N.ReadWaiter, bool) {
rawWaiter, ok := bufio.CreateReadWaiter(c.rawConn)
if !ok {
return nil, false
}
return &windowsTLSReadWaiter{
conn: c,
rawWaiter: rawWaiter,
}, true
}
func (c *windowsTLSConn) Close() error {
if !c.closed.CompareAndSwap(false, true) {
return nil
}
ready := c.writeCondition()
c.writeState.Lock()
ready.Broadcast()
c.writeState.Unlock()
closeErr := c.rawConn.Close()
c.contextAccess.Lock()
if c.client != nil {
c.client.Close()
c.client = nil
}
c.contextAccess.Unlock()
return closeErr
}
func (c *windowsTLSConn) LocalAddr() net.Addr {
return c.rawConn.LocalAddr()
}
func (c *windowsTLSConn) RemoteAddr() net.Addr {
return c.rawConn.RemoteAddr()
}
func (c *windowsTLSConn) SetDeadline(t time.Time) error {
c.deadlineAccess.Lock()
defer c.deadlineAccess.Unlock()
err := c.rawConn.SetDeadline(t)
if err != nil {
return err
}
c.readDeadline = t
c.writeDeadline = t
return nil
}
func (c *windowsTLSConn) SetReadDeadline(t time.Time) error {
c.deadlineAccess.Lock()
defer c.deadlineAccess.Unlock()
err := c.rawConn.SetReadDeadline(t)
if err != nil {
return err
}
c.readDeadline = t
return nil
}
func (c *windowsTLSConn) SetWriteDeadline(t time.Time) error {
c.deadlineAccess.Lock()
defer c.deadlineAccess.Unlock()
err := c.rawConn.SetWriteDeadline(t)
if err != nil {
return err
}
c.writeDeadline = t
return nil
}
func (c *windowsTLSConn) NetConn() net.Conn {
return c.rawConn
}
func (c *windowsTLSConn) HandshakeContext(ctx context.Context) error {
return nil
}
func (c *windowsTLSConn) ConnectionState() ConnectionState {
return c.state
}
func (c *windowsTLSConn) applyReadDeadline() (func(), error) {
c.deadlineAccess.Lock()
deadline := c.readDeadline
c.deadlineAccess.Unlock()
return c.applyDeadline(deadline, c.rawConn.SetReadDeadline)
}
func (c *windowsTLSConn) applyWriteDeadline() (func(), error) {
c.deadlineAccess.Lock()
deadline := c.writeDeadline
c.deadlineAccess.Unlock()
return c.applyDeadline(deadline, c.rawConn.SetWriteDeadline)
}
func (c *windowsTLSConn) applyDeadline(deadline time.Time, set func(time.Time) error) (func(), error) {
if deadline.IsZero() {
return func() {}, nil
}
if !deadline.After(time.Now()) {
return nil, os.ErrDeadlineExceeded
}
err := set(deadline)
if err != nil {
return nil, err
}
return func() { _ = set(time.Time{}) }, nil
}
func (c *windowsTLSConn) beginWrite() error {
ready := c.writeCondition()
c.writeState.Lock()
for c.postHandshake || c.writeActive {
if c.closed.Load() {
c.writeState.Unlock()
return net.ErrClosed
}
ready.Wait()
}
c.writeActive = true
c.writeState.Unlock()
c.writeAccess.Lock()
if c.closed.Load() {
c.writeAccess.Unlock()
c.writeState.Lock()
c.writeActive = false
ready.Broadcast()
c.writeState.Unlock()
return net.ErrClosed
}
return nil
}
func (c *windowsTLSConn) finishWrite() {
c.writeAccess.Unlock()
ready := c.writeCondition()
c.writeState.Lock()
c.writeActive = false
ready.Broadcast()
c.writeState.Unlock()
}
func (c *windowsTLSConn) beginPostHandshakeWrite() error {
ready := c.writeCondition()
c.writeState.Lock()
c.postHandshake = true
for c.writeActive {
if c.closed.Load() {
c.postHandshake = false
ready.Broadcast()
c.writeState.Unlock()
return net.ErrClosed
}
ready.Wait()
}
c.writeActive = true
c.writeState.Unlock()
c.writeAccess.Lock()
if c.closed.Load() {
c.writeAccess.Unlock()
c.writeState.Lock()
c.writeActive = false
c.postHandshake = false
ready.Broadcast()
c.writeState.Unlock()
return net.ErrClosed
}
return nil
}
func (c *windowsTLSConn) finishPostHandshakeWrite() {
c.writeAccess.Unlock()
ready := c.writeCondition()
c.writeState.Lock()
c.writeActive = false
c.postHandshake = false
ready.Broadcast()
c.writeState.Unlock()
}
func (c *windowsTLSConn) writeCondition() *sync.Cond {
c.writeStateOnce.Do(func() {
c.writeReady = sync.NewCond(&c.writeState)
})
return c.writeReady
}
func (c *windowsTLSConn) isClosed() bool {
return c.closed.Load()
}
type windowsTLSReadWaiter struct {
conn *windowsTLSConn
rawWaiter N.ReadWaiter
options N.ReadWaitOptions
}
var _ N.ReadWaiter = (*windowsTLSReadWaiter)(nil)
func (w *windowsTLSReadWaiter) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) {
w.options = options
w.rawWaiter.InitializeReadWaiter(N.ReadWaitOptions{
MTU: readWaitCiphertextChunkSize,
})
return false
}
func (w *windowsTLSReadWaiter) WaitReadBuffer() (*buf.Buffer, error) {
c := w.conn
c.readAccess.Lock()
defer c.readAccess.Unlock()
if c.isClosed() {
return nil, net.ErrClosed
}
plaintext, err := c.readPlaintextLocked(w.appendRaw, w.readRaw)
if err != nil {
return nil, err
}
buffer := w.options.NewBuffer()
n, writeErr := buffer.Write(plaintext)
if writeErr != nil {
buffer.Release()
return nil, writeErr
}
if n == 0 {
buffer.Release()
return nil, io.ErrShortBuffer
}
if n < len(plaintext) {
c.plain = append([]byte(nil), plaintext[n:]...)
}
w.options.PostReturn(buffer)
return buffer, nil
}
func (w *windowsTLSReadWaiter) appendRaw(requireMore bool) error {
rawBuffer, err := w.readRawBuffer(requireMore)
if err != nil {
return err
}
w.conn.cipher = append(w.conn.cipher, rawBuffer.Bytes()...)
rawBuffer.Release()
return nil
}
func (w *windowsTLSReadWaiter) readRaw(requireMore bool) ([]byte, error) {
rawBuffer, err := w.readRawBuffer(requireMore)
if err != nil {
return nil, err
}
data := append([]byte(nil), rawBuffer.Bytes()...)
rawBuffer.Release()
return data, nil
}
func (w *windowsTLSReadWaiter) readRawBuffer(requireMore bool) (*buf.Buffer, error) {
rawBuffer, err := w.rawWaiter.WaitReadBuffer()
if err != nil {
if requireMore && errors.Is(err, io.EOF) {
return nil, io.ErrUnexpectedEOF
}
return nil, err
}
if rawBuffer == nil || rawBuffer.Len() == 0 {
if rawBuffer != nil {
rawBuffer.Release()
}
return nil, io.ErrUnexpectedEOF
}
return rawBuffer, nil
}

View file

@ -0,0 +1,15 @@
//go:build !windows
package tls
import (
"context"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
func newWindowsClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
return nil, E.New("Windows TLS engine is not available on non-Windows platforms")
}

File diff suppressed because it is too large Load diff

View file

@ -4,5 +4,7 @@ const ACMETLS1Protocol = "acme-tls/1"
const (
TLSEngineDefault = ""
TLSEngineGo = "go"
TLSEngineApple = "apple"
TLSEngineWindows = "windows"
)

View file

@ -8,6 +8,7 @@ icon: material/new-box
:material-plus: [handshake_timeout](#handshake_timeout)
:material-plus: [spoof](#spoof)
:material-plus: [spoof_method](#spoof_method)
:material-plus: [engine](#engine)
:material-delete-clock: [acme](#acme-fields)
!!! quote "Changes in sing-box 1.13.0"
@ -195,6 +196,8 @@ Enable TLS.
#### engine
!!! question "Since sing-box 1.14.0"
==Client only==
TLS engine to use.
@ -203,15 +206,40 @@ Values:
* `go` (default)
* `apple`
* `windows`
`apple` uses Network.framework, only available on Apple platforms and only supports **direct** TCP TLS client connections.
Supported fields:
!!! warning ""
* `server_name`
* `insecure`
* `alpn`
* `min_version`
* `max_version`
* `certificate` / `certificate_path`
* `certificate_public_key_sha256`
* `handshake_timeout`
Experimental only: due to the high memory overhead of both CGO and Network.framework,
do not use in hot paths on iOS and tvOS.
If you want to circumvent TLS fingerprint-based proxy censorship,
use [NaiveProxy](/configuration/outbound/naive/) instead.
Unsupported fields:
* `disable_sni`
* `cipher_suites`
* `curve_preferences`
* `client_certificate` / `client_certificate_path` / `client_key` / `client_key_path`
* `fragment` / `record_fragment`
* `kernel_tx` / `kernel_rx`
* `ech`
* `utls`
* `reality`
!!! note ""
`windows` uses Schannel via SSPI. Only available on Windows build 17763 or later (Windows 10 version 1809, Windows Server 2019, or newer).
!!! note ""
TLS 1.3 is only negotiated on Windows 11 or Windows Server 2022 and newer. On older Windows versions, Schannel caps the connection at TLS 1.2 even when `max_version` is `1.3`.
The default version range is TLS 1.2 to TLS 1.3, matching the `go` engine.
Supported fields:

View file

@ -8,6 +8,7 @@ icon: material/new-box
:material-plus: [handshake_timeout](#handshake_timeout)
:material-plus: [spoof](#spoof)
:material-plus: [spoof_method](#spoof_method)
:material-plus: [engine](#engine)
:material-delete-clock: [acme](#acme-字段)
!!! quote "sing-box 1.13.0 中的更改"
@ -195,6 +196,8 @@ TLS 版本值:
#### engine
!!! question "自 sing-box 1.14.0 起"
==仅客户端==
要使用的 TLS 引擎。
@ -203,14 +206,40 @@ TLS 版本值:
* `go`(默认)
* `apple`
* `windows`
`apple` 使用 Network.framework仅在 Apple 平台可用,且仅支持 **直接** TCP TLS 客户端连接。
支持的字段:
!!! warning ""
* `server_name`
* `insecure`
* `alpn`
* `min_version`
* `max_version`
* `certificate` / `certificate_path`
* `certificate_public_key_sha256`
* `handshake_timeout`
仅供实验用途:由于 CGO 和 Network.framework 占用的内存都很多,
不应在 iOS 和 tvOS 的热路径中使用。
如果您想规避基于 TLS 指纹的代理审查,应使用 [NaiveProxy](/zh/configuration/outbound/naive/)。
不支持的字段:
* `disable_sni`
* `cipher_suites`
* `curve_preferences`
* `client_certificate` / `client_certificate_path` / `client_key` / `client_key_path`
* `fragment` / `record_fragment`
* `kernel_tx` / `kernel_rx`
* `ech`
* `utls`
* `reality`
!!! note ""
`windows` 通过 SSPI 使用 Schannel仅在 Windows build 17763 及以上可用,包括 Windows 10 版本 1809、Windows Server 2019 及后续版本。
!!! note ""
TLS 1.3 仅在 Windows 11 或 Windows Server 2022 及后续版本上协商。在更早的 Windows 版本上,即使 `max_version` 设为 `1.3`Schannel 也会把连接上限固定在 TLS 1.2。
默认版本范围为 TLS 1.2 到 TLS 1.3,与 `go` 引擎一致。证书验证在 Go 侧基于 Schannel 返回的证书链执行,默认使用系统证书存储。当设置了 `certificate``certificate_path` 时,这些根证书会替代系统存储。
支持的字段: