Add Windows TLS engine
This commit is contained in:
parent
b40e8c5f19
commit
0807b71b14
35 changed files with 6513 additions and 488 deletions
|
|
@ -10,6 +10,7 @@ import (
|
|||
type CertificateStore interface {
|
||||
LifecycleService
|
||||
Pool() *x509.CertPool
|
||||
ExclusiveAnchors() bool
|
||||
}
|
||||
|
||||
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
||||
|
|
|
|||
17
adapter/certificate_darwin.go
Normal file
17
adapter/certificate_darwin.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//go:build darwin && cgo
|
||||
|
||||
package adapter
|
||||
|
||||
import "unsafe"
|
||||
|
||||
type AppleAnchors interface {
|
||||
Retain() AppleAnchors
|
||||
Release()
|
||||
// Ref returns the underlying CFArrayRef, or nil if the anchor set is empty.
|
||||
Ref() unsafe.Pointer
|
||||
}
|
||||
|
||||
type AppleCertificateStore interface {
|
||||
CertificateStore
|
||||
AppleAnchors() AppleAnchors
|
||||
}
|
||||
17
common/certificate/anchors_darwin.h
Normal file
17
common/certificate/anchors_darwin.h
Normal 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
common/certificate/anchors_darwin.m
Normal file
42
common/certificate/anchors_darwin.m
Normal 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);
|
||||
}
|
||||
|
|
@ -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
common/certificate/store_darwin.go
Normal file
167
common/certificate/store_darwin.go
Normal 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
common/certificate/store_other.go
Normal file
13
common/certificate/store_other.go
Normal 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
|
||||
}
|
||||
|
|
@ -25,6 +25,8 @@ import (
|
|||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/certificate"
|
||||
"github.com/sagernet/sing-box/common/proxybridge"
|
||||
boxTLS "github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
|
|
@ -37,6 +39,15 @@ import (
|
|||
|
||||
const applePinnedHashSize = sha256.Size
|
||||
|
||||
var (
|
||||
newAppleUserAnchors = certificate.NewAppleAnchors
|
||||
newAppleProxyBridge = proxybridge.New
|
||||
newAppleTransportSession = func(shared *appleTransportShared) (unsafe.Pointer, error) {
|
||||
session, err := shared.newSession()
|
||||
return unsafe.Pointer(session), err
|
||||
}
|
||||
)
|
||||
|
||||
func verifyApplePinnedPublicKeySHA256(flatHashes []byte, leafCertificate []byte) error {
|
||||
if len(flatHashes)%applePinnedHashSize != 0 {
|
||||
return E.New("invalid pinned public key list")
|
||||
|
|
@ -64,8 +75,9 @@ type appleSessionConfig struct {
|
|||
minVersion uint16
|
||||
maxVersion uint16
|
||||
insecure bool
|
||||
anchorPEM string
|
||||
anchorOnly bool
|
||||
userAnchors adapter.AppleAnchors
|
||||
store adapter.CertificateStore
|
||||
pinnedPublicKeySHA256s []byte
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +101,13 @@ func newAppleTransport(ctx context.Context, logger logger.ContextLogger, rawDial
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bridge, err := proxybridge.New(ctx, logger, "apple http proxy", rawDialer)
|
||||
releaseConfig := true
|
||||
defer func() {
|
||||
if releaseConfig {
|
||||
sessionConfig.close()
|
||||
}
|
||||
}()
|
||||
bridge, err := newAppleProxyBridge(ctx, logger, "apple http proxy", rawDialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -100,11 +118,13 @@ func newAppleTransport(ctx context.Context, logger logger.ContextLogger, rawDial
|
|||
timeFunc: ntp.TimeFuncFromContext(ctx),
|
||||
}
|
||||
shared.refs.Store(1)
|
||||
session, err := shared.newSession()
|
||||
sessionRef, err := newAppleTransportSession(shared)
|
||||
if err != nil {
|
||||
bridge.Close()
|
||||
return nil, err
|
||||
}
|
||||
session := (*C.box_apple_http_session_t)(sessionRef)
|
||||
releaseConfig = false
|
||||
return &appleTransport{
|
||||
shared: shared,
|
||||
session: session,
|
||||
|
|
@ -142,7 +162,7 @@ func newAppleSessionConfig(ctx context.Context, options option.HTTPClientOptions
|
|||
if len(tlsOptions.ALPN) > 0 {
|
||||
return appleSessionConfig{}, E.New("tls.alpn is unsupported in Apple HTTP engine")
|
||||
}
|
||||
validated, err := boxTLS.ValidateAppleTLSOptions(ctx, tlsOptions, "Apple HTTP engine")
|
||||
validated, err := boxTLS.ValidateSystemTLSOptions(ctx, tlsOptions, "Apple HTTP engine")
|
||||
if err != nil {
|
||||
return appleSessionConfig{}, err
|
||||
}
|
||||
|
|
@ -152,13 +172,23 @@ func newAppleSessionConfig(ctx context.Context, options option.HTTPClientOptions
|
|||
minVersion: validated.MinVersion,
|
||||
maxVersion: validated.MaxVersion,
|
||||
insecure: tlsOptions.Insecure || len(tlsOptions.CertificatePublicKeySHA256) > 0,
|
||||
anchorPEM: validated.AnchorPEM,
|
||||
anchorOnly: validated.AnchorOnly,
|
||||
anchorOnly: validated.Exclusive,
|
||||
store: validated.Store,
|
||||
}
|
||||
if len(validated.UserPEM) > 0 {
|
||||
userAnchors, anchorsErr := newAppleUserAnchors(validated.UserPEM)
|
||||
if anchorsErr != nil {
|
||||
return appleSessionConfig{}, anchorsErr
|
||||
}
|
||||
config.userAnchors = userAnchors
|
||||
}
|
||||
if len(tlsOptions.CertificatePublicKeySHA256) > 0 {
|
||||
config.pinnedPublicKeySHA256s = make([]byte, 0, len(tlsOptions.CertificatePublicKeySHA256)*applePinnedHashSize)
|
||||
for _, hashValue := range tlsOptions.CertificatePublicKeySHA256 {
|
||||
if len(hashValue) != applePinnedHashSize {
|
||||
if config.userAnchors != nil {
|
||||
config.userAnchors.Release()
|
||||
}
|
||||
return appleSessionConfig{}, E.New("invalid certificate_public_key_sha256 length: ", len(hashValue))
|
||||
}
|
||||
config.pinnedPublicKeySHA256s = append(config.pinnedPublicKeySHA256s, hashValue...)
|
||||
|
|
@ -167,12 +197,20 @@ func newAppleSessionConfig(ctx context.Context, options option.HTTPClientOptions
|
|||
return config, nil
|
||||
}
|
||||
|
||||
func (c *appleSessionConfig) close() {
|
||||
if c.userAnchors != nil {
|
||||
c.userAnchors.Release()
|
||||
c.userAnchors = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *appleTransportShared) retain() {
|
||||
s.refs.Add(1)
|
||||
}
|
||||
|
||||
func (s *appleTransportShared) release() error {
|
||||
if s.refs.Add(-1) == 0 {
|
||||
s.config.close()
|
||||
return s.bridge.Close()
|
||||
}
|
||||
return nil
|
||||
|
|
@ -185,16 +223,17 @@ func (s *appleTransportShared) newSession() (*C.box_apple_http_session_t, error)
|
|||
defer C.free(unsafe.Pointer(cProxyUsername))
|
||||
cProxyPassword := C.CString(s.bridge.Password())
|
||||
defer C.free(unsafe.Pointer(cProxyPassword))
|
||||
var cAnchorPEM *C.char
|
||||
if s.config.anchorPEM != "" {
|
||||
cAnchorPEM = C.CString(s.config.anchorPEM)
|
||||
defer C.free(unsafe.Pointer(cAnchorPEM))
|
||||
}
|
||||
var pinnedPointer *C.uint8_t
|
||||
if len(s.config.pinnedPublicKeySHA256s) > 0 {
|
||||
pinnedPointer = (*C.uint8_t)(C.CBytes(s.config.pinnedPublicKeySHA256s))
|
||||
defer C.free(unsafe.Pointer(pinnedPointer))
|
||||
}
|
||||
anchors := certificate.AcquireAnchors(s.config.userAnchors, s.config.store)
|
||||
var anchorsRef unsafe.Pointer
|
||||
if anchors != nil {
|
||||
anchorsRef = anchors.Ref()
|
||||
defer anchors.Release()
|
||||
}
|
||||
cConfig := C.box_apple_http_session_config_t{
|
||||
proxy_host: cProxyHost,
|
||||
proxy_port: C.int(s.bridge.Port()),
|
||||
|
|
@ -203,8 +242,7 @@ func (s *appleTransportShared) newSession() (*C.box_apple_http_session_t, error)
|
|||
min_tls_version: C.uint16_t(s.config.minVersion),
|
||||
max_tls_version: C.uint16_t(s.config.maxVersion),
|
||||
insecure: C.bool(s.config.insecure),
|
||||
anchor_pem: cAnchorPEM,
|
||||
anchor_pem_len: C.size_t(len(s.config.anchorPEM)),
|
||||
anchors_cf: anchorsRef,
|
||||
anchor_only: C.bool(s.config.anchorOnly),
|
||||
pinned_public_key_sha256: pinnedPointer,
|
||||
pinned_public_key_sha256_len: C.size_t(len(s.config.pinnedPublicKeySHA256s)),
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ typedef struct box_apple_http_session_config {
|
|||
uint16_t min_tls_version;
|
||||
uint16_t max_tls_version;
|
||||
bool insecure;
|
||||
const char *anchor_pem;
|
||||
size_t anchor_pem_len;
|
||||
void *anchors_cf;
|
||||
bool anchor_only;
|
||||
const uint8_t *pinned_public_key_sha256;
|
||||
size_t pinned_public_key_sha256_len;
|
||||
|
|
|
|||
|
|
@ -36,44 +36,6 @@ static void box_set_error_from_nserror(char **error_out, NSError *error) {
|
|||
box_set_error_string(error_out, error.localizedDescription ?: error.description);
|
||||
}
|
||||
|
||||
static NSArray *box_parse_certificates_from_pem(const char *pem, size_t pem_len) {
|
||||
if (pem == NULL || pem_len == 0) {
|
||||
return @[];
|
||||
}
|
||||
NSString *content = [[NSString alloc] initWithBytes:pem length:pem_len encoding:NSUTF8StringEncoding];
|
||||
if (content == nil) {
|
||||
return @[];
|
||||
}
|
||||
NSString *beginMarker = @"-----BEGIN CERTIFICATE-----";
|
||||
NSString *endMarker = @"-----END CERTIFICATE-----";
|
||||
NSMutableArray *certificates = [NSMutableArray array];
|
||||
NSUInteger searchFrom = 0;
|
||||
while (searchFrom < content.length) {
|
||||
NSRange beginRange = [content rangeOfString:beginMarker options:0 range:NSMakeRange(searchFrom, content.length - searchFrom)];
|
||||
if (beginRange.location == NSNotFound) {
|
||||
break;
|
||||
}
|
||||
NSUInteger bodyStart = beginRange.location + beginRange.length;
|
||||
NSRange endRange = [content rangeOfString:endMarker options:0 range:NSMakeRange(bodyStart, content.length - bodyStart)];
|
||||
if (endRange.location == NSNotFound) {
|
||||
break;
|
||||
}
|
||||
NSString *base64Section = [content substringWithRange:NSMakeRange(bodyStart, endRange.location - bodyStart)];
|
||||
NSArray<NSString *> *components = [base64Section componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
NSString *base64Content = [components componentsJoinedByString:@""];
|
||||
NSData *der = [[NSData alloc] initWithBase64EncodedString:base64Content options:0];
|
||||
if (der != nil) {
|
||||
SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)der);
|
||||
if (certificate != NULL) {
|
||||
[certificates addObject:(__bridge id)certificate];
|
||||
CFRelease(certificate);
|
||||
}
|
||||
}
|
||||
searchFrom = endRange.location + endRange.length;
|
||||
}
|
||||
return certificates;
|
||||
}
|
||||
|
||||
static bool box_evaluate_trust(SecTrustRef trustRef, NSArray *anchors, bool anchor_only, NSDate *verifyDate) {
|
||||
if (trustRef == NULL) {
|
||||
return false;
|
||||
|
|
@ -249,7 +211,11 @@ box_apple_http_session_t *box_apple_http_session_create(
|
|||
if (config != NULL) {
|
||||
delegate.insecure = config->insecure;
|
||||
delegate.anchorOnly = config->anchor_only;
|
||||
delegate.anchors = box_parse_certificates_from_pem(config->anchor_pem, config->anchor_pem_len);
|
||||
if (config->anchors_cf != NULL) {
|
||||
delegate.anchors = (__bridge NSArray *)config->anchors_cf;
|
||||
} else {
|
||||
delegate.anchors = @[];
|
||||
}
|
||||
if (config->pinned_public_key_sha256 != NULL && config->pinned_public_key_sha256_len > 0) {
|
||||
delegate.pinnedPublicKeyHashes = [NSData dataWithBytes:config->pinned_public_key_sha256 length:config->pinned_public_key_sha256_len];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,13 +19,16 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/proxybridge"
|
||||
boxTLS "github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/route"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
commonLogger "github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
|
@ -58,6 +61,23 @@ type appleHTTPTestServer struct {
|
|||
publicKeyHash []byte
|
||||
}
|
||||
|
||||
type appleTestAnchors struct {
|
||||
ref unsafe.Pointer
|
||||
releases int
|
||||
}
|
||||
|
||||
func (a *appleTestAnchors) Retain() adapter.AppleAnchors {
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *appleTestAnchors) Release() {
|
||||
a.releases++
|
||||
}
|
||||
|
||||
func (a *appleTestAnchors) Ref() unsafe.Pointer {
|
||||
return a.ref
|
||||
}
|
||||
|
||||
func TestNewAppleSessionConfig(t *testing.T) {
|
||||
serverCertificate, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost")
|
||||
serverHash := certificatePublicKeySHA256(t, serverCertificate.Certificate[0])
|
||||
|
|
@ -103,8 +123,14 @@ func TestNewAppleSessionConfig(t *testing.T) {
|
|||
if !config.anchorOnly {
|
||||
t.Fatal("expected anchor_only")
|
||||
}
|
||||
if !strings.Contains(config.anchorPEM, "BEGIN CERTIFICATE") {
|
||||
t.Fatalf("unexpected anchor pem: %q", config.anchorPEM)
|
||||
if config.userAnchors == nil {
|
||||
t.Fatal("expected user anchors")
|
||||
}
|
||||
if config.userAnchors.Ref() == nil {
|
||||
t.Fatal("expected non-empty user anchors")
|
||||
}
|
||||
if config.store != nil {
|
||||
t.Fatal("unexpected store reference")
|
||||
}
|
||||
if len(config.pinnedPublicKeySHA256s) != 0 {
|
||||
t.Fatalf("unexpected pinned hashes: %d", len(config.pinnedPublicKeySHA256s))
|
||||
|
|
@ -137,8 +163,8 @@ func TestNewAppleSessionConfig(t *testing.T) {
|
|||
if !bytes.Equal(config.pinnedPublicKeySHA256s[applePinnedHashSize:], otherHash) {
|
||||
t.Fatal("unexpected second pin")
|
||||
}
|
||||
if config.anchorPEM != "" {
|
||||
t.Fatalf("unexpected anchor pem: %q", config.anchorPEM)
|
||||
if config.userAnchors != nil {
|
||||
t.Fatal("unexpected user anchors")
|
||||
}
|
||||
if config.anchorOnly {
|
||||
t.Fatal("unexpected anchor_only")
|
||||
|
|
@ -392,6 +418,46 @@ func TestAppleTransportVerifyPublicKeySHA256(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNewAppleTransportClosesSessionConfigOnBridgeFailure(t *testing.T) {
|
||||
_, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost")
|
||||
restoreAppleTransportFactories(t)
|
||||
testAnchors := &appleTestAnchors{ref: unsafe.Pointer(new(int))}
|
||||
newAppleUserAnchors = func([]byte) (adapter.AppleAnchors, error) {
|
||||
return testAnchors, nil
|
||||
}
|
||||
newAppleProxyBridge = func(context.Context, commonLogger.ContextLogger, string, N.Dialer) (*proxybridge.Bridge, error) {
|
||||
return nil, errors.New("bridge boom")
|
||||
}
|
||||
|
||||
_, err := newAppleTransport(newAppleHTTPTestContext(), log.NewNOPFactory().NewLogger("httpclient"), &appleHTTPTestDialer{}, appleTransportAnchorOptions(serverCertificatePEM))
|
||||
if err == nil || !strings.Contains(err.Error(), "bridge boom") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if testAnchors.releases != 1 {
|
||||
t.Fatalf("expected 1 anchor release, got %d", testAnchors.releases)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAppleTransportClosesSessionConfigOnSessionFailure(t *testing.T) {
|
||||
_, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost")
|
||||
restoreAppleTransportFactories(t)
|
||||
testAnchors := &appleTestAnchors{ref: unsafe.Pointer(new(int))}
|
||||
newAppleUserAnchors = func([]byte) (adapter.AppleAnchors, error) {
|
||||
return testAnchors, nil
|
||||
}
|
||||
newAppleTransportSession = func(*appleTransportShared) (unsafe.Pointer, error) {
|
||||
return nil, errors.New("session boom")
|
||||
}
|
||||
|
||||
_, err := newAppleTransport(newAppleHTTPTestContext(), log.NewNOPFactory().NewLogger("httpclient"), &appleHTTPTestDialer{}, appleTransportAnchorOptions(serverCertificatePEM))
|
||||
if err == nil || !strings.Contains(err.Error(), "session boom") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if testAnchors.releases != 1 {
|
||||
t.Fatalf("expected 1 anchor release, got %d", testAnchors.releases)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppleTransportRoundTripHTTPS(t *testing.T) {
|
||||
requests := make(chan appleHTTPObservedRequest, 1)
|
||||
server := startAppleHTTPTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -665,7 +731,8 @@ func TestAppleTransportLifecycle(t *testing.T) {
|
|||
assertAppleHTTPSucceeds(t, transport, server.URL("/reset"))
|
||||
|
||||
innerTransport := transport.(*appleTransport)
|
||||
if err := innerTransport.Close(); err != nil {
|
||||
err := innerTransport.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
|
@ -722,10 +789,7 @@ func (s *appleHTTPTestServer) URL(path string) string {
|
|||
func newAppleHTTPTestTransport(t *testing.T, server *appleHTTPTestServer, options option.HTTPClientOptions) innerTransport {
|
||||
t.Helper()
|
||||
|
||||
ctx := service.ContextWith[adapter.ConnectionManager](
|
||||
context.Background(),
|
||||
route.NewConnectionManager(log.NewNOPFactory().NewLogger("connection")),
|
||||
)
|
||||
ctx := newAppleHTTPTestContext()
|
||||
dialer := &appleHTTPTestDialer{
|
||||
hostMap: make(map[string]string),
|
||||
}
|
||||
|
|
@ -743,6 +807,39 @@ func newAppleHTTPTestTransport(t *testing.T, server *appleHTTPTestServer, option
|
|||
return transport
|
||||
}
|
||||
|
||||
func newAppleHTTPTestContext() context.Context {
|
||||
return service.ContextWith[adapter.ConnectionManager](
|
||||
context.Background(),
|
||||
route.NewConnectionManager(log.NewNOPFactory().NewLogger("connection")),
|
||||
)
|
||||
}
|
||||
|
||||
func appleTransportAnchorOptions(certificatePEM string) option.HTTPClientOptions {
|
||||
return option.HTTPClientOptions{
|
||||
Version: 2,
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "localhost",
|
||||
MinVersion: "1.2",
|
||||
Certificate: badoption.Listable[string]{certificatePEM},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func restoreAppleTransportFactories(t *testing.T) {
|
||||
t.Helper()
|
||||
oldAnchors := newAppleUserAnchors
|
||||
oldBridge := newAppleProxyBridge
|
||||
oldSession := newAppleTransportSession
|
||||
t.Cleanup(func() {
|
||||
newAppleUserAnchors = oldAnchors
|
||||
newAppleProxyBridge = oldBridge
|
||||
newAppleTransportSession = oldSession
|
||||
})
|
||||
}
|
||||
|
||||
func (d *appleHTTPTestDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
host := destination.AddrString()
|
||||
if destination.IsDomain() {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ func NewTransport(ctx context.Context, logger logger.ContextLogger, tag string,
|
|||
}
|
||||
managedTransport.epoch.Store(&transportEpoch{transport: inner})
|
||||
return managedTransport, nil
|
||||
case C.TLSEngineDefault, "go":
|
||||
case "", C.TLSEngineGo:
|
||||
cheapRebuild = true
|
||||
default:
|
||||
return nil, E.New("unknown HTTP engine: ", options.Engine)
|
||||
|
|
|
|||
5
common/schannel/doc.go
Normal file
5
common/schannel/doc.go
Normal 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
common/schannel/schannel_windows.go
Normal file
719
common/schannel/schannel_windows.go
Normal 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
common/schannel/schannel_windows_test.go
Normal file
188
common/schannel/schannel_windows_test.go
Normal 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
common/schannel/syscall_windows.go
Normal file
28
common/schannel/syscall_windows.go
Normal 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
common/schannel/types_windows.go
Normal file
161
common/schannel/types_windows.go
Normal 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
common/schannel/zsyscall_windows.go
Normal file
99
common/schannel/zsyscall_windows.go
Normal 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
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
deadline, err := c.prepareWriteDeadline()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
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
|
||||
}
|
||||
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(&p[0]), C.size_t(len(p)), C.int(timeoutMs), &errorPtr)
|
||||
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 0, os.ErrDeadlineExceeded
|
||||
return written, os.ErrDeadlineExceeded
|
||||
case n >= 0:
|
||||
return int(n), nil
|
||||
written += int(n)
|
||||
if int(n) != len(chunk) {
|
||||
return written, io.ErrShortWrite
|
||||
}
|
||||
if errorPtr != nil {
|
||||
defer C.free(unsafe.Pointer(errorPtr))
|
||||
if c.isClosed() {
|
||||
return 0, net.ErrClosed
|
||||
continue
|
||||
}
|
||||
return 0, E.New(C.GoString(errorPtr))
|
||||
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
|
||||
}
|
||||
|
|
|
|||
278
common/tls/apple_client_platform_benchmark_test.go
Normal file
278
common/tls/apple_client_platform_benchmark_test.go
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
//go:build darwin && cgo
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
stdtls "crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
const (
|
||||
appleTLSBenchmarkReadPayloadSize = 16 * 1024
|
||||
appleTLSBenchmarkWritePayloadSize = 48 * 1024
|
||||
)
|
||||
|
||||
func BenchmarkAppleClientReadBuffer(b *testing.B) {
|
||||
payload := bytes.Repeat([]byte{'r'}, appleTLSBenchmarkReadPayloadSize)
|
||||
start := make(chan struct{})
|
||||
clientConn, serverResult := newAppleBenchmarkClientConn(b, func(conn *stdtls.Conn) error {
|
||||
<-start
|
||||
for range b.N {
|
||||
if err := writeBenchmarkPayload(conn, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
defer clientConn.Close()
|
||||
|
||||
extendedConn := clientConn.(N.ExtendedConn)
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(payload)))
|
||||
b.ReportMetric(float64(len(payload)), "payload_B")
|
||||
b.ResetTimer()
|
||||
close(start)
|
||||
target := b.N * len(payload)
|
||||
var received int
|
||||
for received < target {
|
||||
buffer := buf.NewSize(len(payload))
|
||||
err := extendedConn.ReadBuffer(buffer)
|
||||
if err != nil {
|
||||
buffer.Release()
|
||||
b.Fatal(err)
|
||||
}
|
||||
received += buffer.Len()
|
||||
buffer.Release()
|
||||
}
|
||||
b.StopTimer()
|
||||
if err := <-serverResult; err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppleClientReadWaiter(b *testing.B) {
|
||||
payload := bytes.Repeat([]byte{'w'}, appleTLSBenchmarkReadPayloadSize)
|
||||
start := make(chan struct{})
|
||||
clientConn, serverResult := newAppleBenchmarkClientConn(b, func(conn *stdtls.Conn) error {
|
||||
<-start
|
||||
for range b.N {
|
||||
if err := writeBenchmarkPayload(conn, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
defer clientConn.Close()
|
||||
|
||||
readWaiter, ok := clientConn.(N.ReadWaitCreator).CreateReadWaiter()
|
||||
if !ok {
|
||||
b.Fatal("expected read waiter")
|
||||
}
|
||||
readWaiter.InitializeReadWaiter(N.ReadWaitOptions{
|
||||
MTU: appleTLSBenchmarkReadPayloadSize,
|
||||
})
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(payload)))
|
||||
b.ReportMetric(float64(len(payload)), "payload_B")
|
||||
b.ResetTimer()
|
||||
close(start)
|
||||
target := b.N * len(payload)
|
||||
var received int
|
||||
for received < target {
|
||||
buffer, err := readWaiter.WaitReadBuffer()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.ErrNoProgress) {
|
||||
continue
|
||||
}
|
||||
b.Fatal(err)
|
||||
}
|
||||
received += buffer.Len()
|
||||
buffer.Release()
|
||||
}
|
||||
b.StopTimer()
|
||||
if err := <-serverResult; err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppleClientWriteBuffer(b *testing.B) {
|
||||
payload := bytes.Repeat([]byte{'x'}, appleTLSBenchmarkWritePayloadSize)
|
||||
start := make(chan struct{})
|
||||
clientConn, serverResult := newAppleBenchmarkClientConn(b, func(conn *stdtls.Conn) error {
|
||||
<-start
|
||||
_, err := io.CopyN(io.Discard, conn, int64(b.N*len(payload)))
|
||||
return err
|
||||
})
|
||||
defer clientConn.Close()
|
||||
|
||||
extendedConn := clientConn.(N.ExtendedConn)
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(payload)))
|
||||
b.ReportMetric(float64(len(payload)), "payload_B")
|
||||
b.ReportMetric(float64(appleTLSWriteChunkSize), "write_chunk_B")
|
||||
b.ResetTimer()
|
||||
close(start)
|
||||
for range b.N {
|
||||
buffer := buf.NewSize(len(payload))
|
||||
_, err := buffer.Write(payload)
|
||||
if err != nil {
|
||||
buffer.Release()
|
||||
b.Fatal(err)
|
||||
}
|
||||
err = extendedConn.WriteBuffer(buffer)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.StopTimer()
|
||||
if err := <-serverResult; err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStdlibClientReadBuffer(b *testing.B) {
|
||||
payload := bytes.Repeat([]byte{'r'}, appleTLSBenchmarkReadPayloadSize)
|
||||
start := make(chan struct{})
|
||||
clientConn, serverResult := newStdlibBenchmarkClientConn(b, func(conn *stdtls.Conn) error {
|
||||
<-start
|
||||
for range b.N {
|
||||
if err := writeBenchmarkPayload(conn, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
defer clientConn.Close()
|
||||
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(payload)))
|
||||
b.ReportMetric(float64(len(payload)), "payload_B")
|
||||
b.ResetTimer()
|
||||
close(start)
|
||||
target := b.N * len(payload)
|
||||
var received int
|
||||
for received < target {
|
||||
buffer := buf.NewSize(len(payload))
|
||||
n, err := clientConn.Read(buffer.FreeBytes())
|
||||
if n > 0 {
|
||||
buffer.Truncate(buffer.Len() + n)
|
||||
}
|
||||
received += buffer.Len()
|
||||
buffer.Release()
|
||||
if err != nil && received < target {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.StopTimer()
|
||||
if err := <-serverResult; err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStdlibClientWriteBuffer(b *testing.B) {
|
||||
payload := bytes.Repeat([]byte{'x'}, appleTLSBenchmarkWritePayloadSize)
|
||||
start := make(chan struct{})
|
||||
clientConn, serverResult := newStdlibBenchmarkClientConn(b, func(conn *stdtls.Conn) error {
|
||||
<-start
|
||||
_, err := io.CopyN(io.Discard, conn, int64(b.N*len(payload)))
|
||||
return err
|
||||
})
|
||||
defer clientConn.Close()
|
||||
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(payload)))
|
||||
b.ReportMetric(float64(len(payload)), "payload_B")
|
||||
b.ResetTimer()
|
||||
close(start)
|
||||
for range b.N {
|
||||
buffer := buf.NewSize(len(payload))
|
||||
_, err := buffer.Write(payload)
|
||||
if err != nil {
|
||||
buffer.Release()
|
||||
b.Fatal(err)
|
||||
}
|
||||
_, err = clientConn.Write(buffer.Bytes())
|
||||
buffer.Release()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.StopTimer()
|
||||
if err := <-serverResult; err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func newAppleBenchmarkClientConn(b *testing.B, handler func(*stdtls.Conn) error) (Conn, <-chan error) {
|
||||
b.Helper()
|
||||
|
||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(b, "localhost")
|
||||
serverResult, serverAddress := startAppleTLSIOTestServer(b, &stdtls.Config{
|
||||
Certificates: []stdtls.Certificate{serverCertificate},
|
||||
MinVersion: stdtls.VersionTLS12,
|
||||
MaxVersion: stdtls.VersionTLS12,
|
||||
}, handler)
|
||||
|
||||
clientConn, err := newAppleTestClientConn(b, serverAddress, option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
Engine: "apple",
|
||||
ServerName: "localhost",
|
||||
MinVersion: "1.2",
|
||||
MaxVersion: "1.2",
|
||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
return clientConn, serverResult
|
||||
}
|
||||
|
||||
func newStdlibBenchmarkClientConn(b *testing.B, handler func(*stdtls.Conn) error) (*stdtls.Conn, <-chan error) {
|
||||
b.Helper()
|
||||
|
||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(b, "localhost")
|
||||
serverResult, serverAddress := startAppleTLSIOTestServer(b, &stdtls.Config{
|
||||
Certificates: []stdtls.Certificate{serverCertificate},
|
||||
MinVersion: stdtls.VersionTLS12,
|
||||
MaxVersion: stdtls.VersionTLS12,
|
||||
}, handler)
|
||||
|
||||
roots := x509.NewCertPool()
|
||||
if !roots.AppendCertsFromPEM([]byte(serverCertificatePEM)) {
|
||||
b.Fatal("parse benchmark certificate")
|
||||
}
|
||||
dialer := &net.Dialer{
|
||||
Timeout: appleTLSTestTimeout,
|
||||
}
|
||||
clientConn, err := stdtls.DialWithDialer(dialer, "tcp", serverAddress, &stdtls.Config{
|
||||
ServerName: "localhost",
|
||||
RootCAs: roots,
|
||||
MinVersion: stdtls.VersionTLS12,
|
||||
MaxVersion: stdtls.VersionTLS12,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
return clientConn, serverResult
|
||||
}
|
||||
|
||||
func writeBenchmarkPayload(writer io.Writer, payload []byte) error {
|
||||
for len(payload) > 0 {
|
||||
n, err := writer.Write(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload = payload[n:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,6 +522,7 @@ 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) {
|
||||
@autoreleasepool {
|
||||
nw_connection_t connection = box_apple_tls_connection(client);
|
||||
if (connection == nil) {
|
||||
box_set_error_message(error_out, "apple TLS: invalid client");
|
||||
|
|
@ -492,27 +530,26 @@ ssize_t box_apple_tls_client_read(box_apple_tls_client_t *client, void *buffer,
|
|||
}
|
||||
|
||||
dispatch_semaphore_t read_semaphore = dispatch_semaphore_create(0);
|
||||
__block NSData *content_data = nil;
|
||||
__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) {
|
||||
@autoreleasepool {
|
||||
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];
|
||||
ssize_t copied = box_apple_tls_dispatch_data_copy(content, buffer, buffer_len, &local_error);
|
||||
if (copied >= 0) {
|
||||
content_len = (size_t)copied;
|
||||
}
|
||||
(void)mapped_data;
|
||||
}
|
||||
if (error != NULL && content_data.length == 0) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
dispatch_time_t wait_deadline = DISPATCH_TIME_FOREVER;
|
||||
|
|
@ -540,14 +577,12 @@ ssize_t box_apple_tls_client_read(box_apple_tls_client_t *client, void *buffer,
|
|||
if (eof_out != NULL) {
|
||||
*eof_out = read_eof;
|
||||
}
|
||||
if (content_data == nil || content_data.length == 0) {
|
||||
return 0;
|
||||
return (ssize_t)content_len;
|
||||
}
|
||||
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) {
|
||||
@autoreleasepool {
|
||||
nw_connection_t connection = box_apple_tls_connection(client);
|
||||
if (connection == nil) {
|
||||
box_set_error_message(error_out, "apple TLS: invalid client");
|
||||
|
|
@ -558,12 +593,11 @@ ssize_t box_apple_tls_client_write(box_apple_tls_client_t *client, const void *b
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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");
|
||||
|
|
@ -578,10 +612,12 @@ ssize_t box_apple_tls_client_write(box_apple_tls_client_t *client, const void *b
|
|||
__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;
|
||||
|
|
@ -608,6 +644,81 @@ ssize_t box_apple_tls_client_write(box_apple_tls_client_t *client, const void *b
|
|||
}
|
||||
return (ssize_t)buffer_len;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
dispatch_queue_t queue = box_apple_tls_client_queue(client);
|
||||
|
|
|
|||
50
common/tls/apple_client_platform_dispatch_test.go
Normal file
50
common/tls/apple_client_platform_dispatch_test.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
//go:build darwin && cgo
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAppleTLSDispatchDataCopySegments(t *testing.T) {
|
||||
first := []byte("hello ")
|
||||
second := []byte("world")
|
||||
|
||||
buffer := make([]byte, len(first)+len(second))
|
||||
n, errorMessage := appleTLSCopyDispatchDataForTest(first, second, buffer)
|
||||
if n < 0 {
|
||||
t.Fatalf("copy failed: %s", errorMessage)
|
||||
}
|
||||
if int(n) != len(buffer) {
|
||||
t.Fatalf("copied %d bytes, want %d", n, len(buffer))
|
||||
}
|
||||
if !bytes.Equal(buffer, []byte("hello world")) {
|
||||
t.Fatalf("unexpected copy result: %q", string(buffer))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppleTLSDispatchDataCopyRejectsSmallBuffer(t *testing.T) {
|
||||
first := []byte("hello")
|
||||
second := []byte("world")
|
||||
|
||||
buffer := make([]byte, len(first)+len(second)-1)
|
||||
n, errorMessage := appleTLSCopyDispatchDataForTest(first, second, buffer)
|
||||
if n != -1 {
|
||||
t.Fatalf("copied %d bytes, want error", n)
|
||||
}
|
||||
if !strings.Contains(errorMessage, "read buffer too small") {
|
||||
t.Fatalf("unexpected error: %q", errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppleTLSDispatchDataCopyEmpty(t *testing.T) {
|
||||
n, errorMessage := appleTLSCopyDispatchDataForTest(nil, nil, nil)
|
||||
if n != 0 {
|
||||
t.Fatalf("copied %d bytes, want 0", n)
|
||||
}
|
||||
if errorMessage != "" {
|
||||
t.Fatalf("unexpected error: %q", errorMessage)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -271,19 +271,7 @@ func verifyConnection(rootCAs *x509.CertPool, timeFunc func() time.Time, serverN
|
|||
if serverName == "" {
|
||||
return errMissingServerName
|
||||
}
|
||||
verifyOptions := x509.VerifyOptions{
|
||||
Roots: rootCAs,
|
||||
DNSName: serverName,
|
||||
Intermediates: x509.NewCertPool(),
|
||||
}
|
||||
for _, cert := range state.PeerCertificates[1:] {
|
||||
verifyOptions.Intermediates.AddCert(cert)
|
||||
}
|
||||
if timeFunc != nil {
|
||||
verifyOptions.CurrentTime = timeFunc()
|
||||
}
|
||||
_, err := state.PeerCertificates[0].Verify(verifyOptions)
|
||||
return err
|
||||
return verifySystemTLSPeer(rootCAs, serverName, timeFunc, state.PeerCertificates)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
218
common/tls/system_client.go
Normal file
218
common/tls/system_client.go
Normal 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
common/tls/windows_client.go
Normal file
848
common/tls/windows_client.go
Normal 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
common/tls/windows_client_stub.go
Normal file
15
common/tls/windows_client_stub.go
Normal 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")
|
||||
}
|
||||
2505
common/tls/windows_client_test.go
Normal file
2505
common/tls/windows_client_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -4,5 +4,7 @@ const ACMETLS1Protocol = "acme-tls/1"
|
|||
|
||||
const (
|
||||
TLSEngineDefault = ""
|
||||
TLSEngineGo = "go"
|
||||
TLSEngineApple = "apple"
|
||||
TLSEngineWindows = "windows"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ icon: material/new-box
|
|||
:material-plus: [handshake_timeout](#handshake_timeout)
|
||||
:material-plus: [spoof](#spoof)
|
||||
:material-plus: [spoof_method](#spoof_method)
|
||||
:material-plus: [engine](#engine)
|
||||
:material-delete-clock: [acme](#acme-fields)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
|
@ -195,6 +196,8 @@ Enable TLS.
|
|||
|
||||
#### engine
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
==Client only==
|
||||
|
||||
TLS engine to use.
|
||||
|
|
@ -203,15 +206,40 @@ Values:
|
|||
|
||||
* `go` (default)
|
||||
* `apple`
|
||||
* `windows`
|
||||
|
||||
`apple` uses Network.framework, only available on Apple platforms and only supports **direct** TCP TLS client connections.
|
||||
Supported fields:
|
||||
|
||||
!!! warning ""
|
||||
* `server_name`
|
||||
* `insecure`
|
||||
* `alpn`
|
||||
* `min_version`
|
||||
* `max_version`
|
||||
* `certificate` / `certificate_path`
|
||||
* `certificate_public_key_sha256`
|
||||
* `handshake_timeout`
|
||||
|
||||
Experimental only: due to the high memory overhead of both CGO and Network.framework,
|
||||
do not use in hot paths on iOS and tvOS.
|
||||
If you want to circumvent TLS fingerprint-based proxy censorship,
|
||||
use [NaiveProxy](/configuration/outbound/naive/) instead.
|
||||
Unsupported fields:
|
||||
|
||||
* `disable_sni`
|
||||
* `cipher_suites`
|
||||
* `curve_preferences`
|
||||
* `client_certificate` / `client_certificate_path` / `client_key` / `client_key_path`
|
||||
* `fragment` / `record_fragment`
|
||||
* `kernel_tx` / `kernel_rx`
|
||||
* `ech`
|
||||
* `utls`
|
||||
* `reality`
|
||||
|
||||
!!! note ""
|
||||
|
||||
`windows` uses Schannel via SSPI. Only available on Windows build 17763 or later (Windows 10 version 1809, Windows Server 2019, or newer).
|
||||
|
||||
!!! note ""
|
||||
|
||||
TLS 1.3 is only negotiated on Windows 11 or Windows Server 2022 and newer. On older Windows versions, Schannel caps the connection at TLS 1.2 even when `max_version` is `1.3`.
|
||||
|
||||
The default version range is TLS 1.2 to TLS 1.3, matching the `go` engine.
|
||||
|
||||
Supported fields:
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ icon: material/new-box
|
|||
:material-plus: [handshake_timeout](#handshake_timeout)
|
||||
:material-plus: [spoof](#spoof)
|
||||
:material-plus: [spoof_method](#spoof_method)
|
||||
:material-plus: [engine](#engine)
|
||||
:material-delete-clock: [acme](#acme-字段)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
|
@ -195,6 +196,8 @@ TLS 版本值:
|
|||
|
||||
#### engine
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
==仅客户端==
|
||||
|
||||
要使用的 TLS 引擎。
|
||||
|
|
@ -203,14 +206,40 @@ TLS 版本值:
|
|||
|
||||
* `go`(默认)
|
||||
* `apple`
|
||||
* `windows`
|
||||
|
||||
`apple` 使用 Network.framework,仅在 Apple 平台可用,且仅支持 **直接** TCP TLS 客户端连接。
|
||||
支持的字段:
|
||||
|
||||
!!! warning ""
|
||||
* `server_name`
|
||||
* `insecure`
|
||||
* `alpn`
|
||||
* `min_version`
|
||||
* `max_version`
|
||||
* `certificate` / `certificate_path`
|
||||
* `certificate_public_key_sha256`
|
||||
* `handshake_timeout`
|
||||
|
||||
仅供实验用途:由于 CGO 和 Network.framework 占用的内存都很多,
|
||||
不应在 iOS 和 tvOS 的热路径中使用。
|
||||
如果您想规避基于 TLS 指纹的代理审查,应使用 [NaiveProxy](/zh/configuration/outbound/naive/)。
|
||||
不支持的字段:
|
||||
|
||||
* `disable_sni`
|
||||
* `cipher_suites`
|
||||
* `curve_preferences`
|
||||
* `client_certificate` / `client_certificate_path` / `client_key` / `client_key_path`
|
||||
* `fragment` / `record_fragment`
|
||||
* `kernel_tx` / `kernel_rx`
|
||||
* `ech`
|
||||
* `utls`
|
||||
* `reality`
|
||||
|
||||
!!! note ""
|
||||
|
||||
`windows` 通过 SSPI 使用 Schannel,仅在 Windows build 17763 及以上可用,包括 Windows 10 版本 1809、Windows Server 2019 及后续版本。
|
||||
|
||||
!!! note ""
|
||||
|
||||
TLS 1.3 仅在 Windows 11 或 Windows Server 2022 及后续版本上协商。在更早的 Windows 版本上,即使 `max_version` 设为 `1.3`,Schannel 也会把连接上限固定在 TLS 1.2。
|
||||
|
||||
默认版本范围为 TLS 1.2 到 TLS 1.3,与 `go` 引擎一致。证书验证在 Go 侧基于 Schannel 返回的证书链执行,默认使用系统证书存储。当设置了 `certificate` 或 `certificate_path` 时,这些根证书会替代系统存储。
|
||||
|
||||
支持的字段:
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue