Add Windows TLS engine

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