mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-09-15 21:00:27 +00:00
Add Windows TLS engine
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
|||||||
type CertificateStore interface {
|
type CertificateStore interface {
|
||||||
LifecycleService
|
LifecycleService
|
||||||
Pool() *x509.CertPool
|
Pool() *x509.CertPool
|
||||||
|
ExclusiveAnchors() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#import "anchors_darwin.h"
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <Security/Security.h>
|
||||||
|
|
||||||
|
void *box_certificate_anchors_from_der(const uint8_t *const *ders, const size_t *lens, size_t count) {
|
||||||
|
if (count == 0 || ders == NULL || lens == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
CFMutableArrayRef certificates = CFArrayCreateMutable(NULL, (CFIndex)count, &kCFTypeArrayCallBacks);
|
||||||
|
if (certificates == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
for (size_t index = 0; index < count; index++) {
|
||||||
|
if (ders[index] == NULL || lens[index] == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CFDataRef data = CFDataCreate(NULL, ders[index], (CFIndex)lens[index]);
|
||||||
|
if (data == NULL) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SecCertificateRef certificate = SecCertificateCreateWithData(NULL, data);
|
||||||
|
CFRelease(data);
|
||||||
|
if (certificate == NULL) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CFArrayAppendValue(certificates, certificate);
|
||||||
|
CFRelease(certificate);
|
||||||
|
}
|
||||||
|
if (CFArrayGetCount(certificates) == 0) {
|
||||||
|
CFRelease(certificates);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return certificates;
|
||||||
|
}
|
||||||
|
|
||||||
|
void box_certificate_release_anchors(void *anchors) {
|
||||||
|
if (anchors == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CFRelease((CFTypeRef)anchors);
|
||||||
|
}
|
||||||
+30
-16
@@ -1,6 +1,7 @@
|
|||||||
package certificate
|
package certificate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@@ -25,11 +26,11 @@ type Store struct {
|
|||||||
storeType string
|
storeType string
|
||||||
systemPool *x509.CertPool
|
systemPool *x509.CertPool
|
||||||
currentPool *x509.CertPool
|
currentPool *x509.CertPool
|
||||||
currentPEM []string
|
|
||||||
certificate string
|
certificate string
|
||||||
certificatePaths []string
|
certificatePaths []string
|
||||||
certificateDirectoryPaths []string
|
certificateDirectoryPaths []string
|
||||||
watcher *fswatch.Watcher
|
watcher *fswatch.Watcher
|
||||||
|
platform storePlatform
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
|
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 {
|
func (s *Store) Close() error {
|
||||||
if s.watcher != nil {
|
watcher := s.watcher
|
||||||
return s.watcher.Close()
|
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 {
|
func (s *Store) Pool() *x509.CertPool {
|
||||||
@@ -130,37 +139,35 @@ func (s *Store) StoreKind() string {
|
|||||||
return s.storeType
|
return s.storeType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) CurrentPEM() []string {
|
func (s *Store) ExclusiveAnchors() bool {
|
||||||
s.access.RLock()
|
return s.storeType != C.CertificateStoreSystem
|
||||||
defer s.access.RUnlock()
|
|
||||||
return append([]string(nil), s.currentPEM...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) update() error {
|
func (s *Store) update() error {
|
||||||
currentPool, err := s.newBasePool()
|
currentPool, err := s.newBasePool()
|
||||||
var currentPEM []string
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
pemBuffer := new(bytes.Buffer)
|
||||||
switch s.storeType {
|
switch s.storeType {
|
||||||
case C.CertificateStoreMozilla:
|
case C.CertificateStoreMozilla:
|
||||||
pemContent := mozillaIncludedPEM()
|
pemContent := mozillaIncludedPEM()
|
||||||
if !currentPool.AppendCertsFromPEM([]byte(pemContent)) {
|
if !currentPool.AppendCertsFromPEM([]byte(pemContent)) {
|
||||||
return E.New("invalid Mozilla included certificate PEM")
|
return E.New("invalid Mozilla included certificate PEM")
|
||||||
}
|
}
|
||||||
currentPEM = append(currentPEM, pemContent)
|
appendPEMBlock(pemBuffer, string(pemContent))
|
||||||
case C.CertificateStoreChrome:
|
case C.CertificateStoreChrome:
|
||||||
pemContent := chromeIncludedPEM()
|
pemContent := chromeIncludedPEM()
|
||||||
if !currentPool.AppendCertsFromPEM([]byte(pemContent)) {
|
if !currentPool.AppendCertsFromPEM([]byte(pemContent)) {
|
||||||
return E.New("invalid Chrome included certificate PEM")
|
return E.New("invalid Chrome included certificate PEM")
|
||||||
}
|
}
|
||||||
currentPEM = append(currentPEM, pemContent)
|
appendPEMBlock(pemBuffer, string(pemContent))
|
||||||
}
|
}
|
||||||
if s.certificate != "" {
|
if s.certificate != "" {
|
||||||
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
||||||
return E.New("invalid certificate PEM strings")
|
return E.New("invalid certificate PEM strings")
|
||||||
}
|
}
|
||||||
currentPEM = append(currentPEM, s.certificate)
|
appendPEMBlock(pemBuffer, s.certificate)
|
||||||
}
|
}
|
||||||
for _, path := range s.certificatePaths {
|
for _, path := range s.certificatePaths {
|
||||||
pemContent, err := os.ReadFile(path)
|
pemContent, err := os.ReadFile(path)
|
||||||
@@ -170,7 +177,7 @@ func (s *Store) update() error {
|
|||||||
if !currentPool.AppendCertsFromPEM(pemContent) {
|
if !currentPool.AppendCertsFromPEM(pemContent) {
|
||||||
return E.New("invalid certificate PEM file: ", path)
|
return E.New("invalid certificate PEM file: ", path)
|
||||||
}
|
}
|
||||||
currentPEM = append(currentPEM, string(pemContent))
|
appendPEMBlock(pemBuffer, string(pemContent))
|
||||||
}
|
}
|
||||||
var firstErr error
|
var firstErr error
|
||||||
for _, directoryPath := range s.certificateDirectoryPaths {
|
for _, directoryPath := range s.certificateDirectoryPaths {
|
||||||
@@ -184,7 +191,7 @@ func (s *Store) update() error {
|
|||||||
for _, directoryEntry := range directoryEntries {
|
for _, directoryEntry := range directoryEntries {
|
||||||
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
|
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
|
||||||
if err == nil && currentPool.AppendCertsFromPEM(pemContent) {
|
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()
|
s.access.Lock()
|
||||||
defer s.access.Unlock()
|
defer s.access.Unlock()
|
||||||
s.currentPool = currentPool
|
s.currentPool = currentPool
|
||||||
s.currentPEM = currentPEM
|
return s.updatePlatformLocked(pemBuffer.Bytes())
|
||||||
return nil
|
}
|
||||||
|
|
||||||
|
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) {
|
func (s *Store) newBasePool() (*x509.CertPool, error) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/certificate"
|
||||||
"github.com/sagernet/sing-box/common/proxybridge"
|
"github.com/sagernet/sing-box/common/proxybridge"
|
||||||
boxTLS "github.com/sagernet/sing-box/common/tls"
|
boxTLS "github.com/sagernet/sing-box/common/tls"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
@@ -37,6 +39,15 @@ import (
|
|||||||
|
|
||||||
const applePinnedHashSize = sha256.Size
|
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 {
|
func verifyApplePinnedPublicKeySHA256(flatHashes []byte, leafCertificate []byte) error {
|
||||||
if len(flatHashes)%applePinnedHashSize != 0 {
|
if len(flatHashes)%applePinnedHashSize != 0 {
|
||||||
return E.New("invalid pinned public key list")
|
return E.New("invalid pinned public key list")
|
||||||
@@ -64,8 +75,9 @@ type appleSessionConfig struct {
|
|||||||
minVersion uint16
|
minVersion uint16
|
||||||
maxVersion uint16
|
maxVersion uint16
|
||||||
insecure bool
|
insecure bool
|
||||||
anchorPEM string
|
|
||||||
anchorOnly bool
|
anchorOnly bool
|
||||||
|
userAnchors adapter.AppleAnchors
|
||||||
|
store adapter.CertificateStore
|
||||||
pinnedPublicKeySHA256s []byte
|
pinnedPublicKeySHA256s []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +101,13 @@ func newAppleTransport(ctx context.Context, logger logger.ContextLogger, rawDial
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -100,11 +118,13 @@ func newAppleTransport(ctx context.Context, logger logger.ContextLogger, rawDial
|
|||||||
timeFunc: ntp.TimeFuncFromContext(ctx),
|
timeFunc: ntp.TimeFuncFromContext(ctx),
|
||||||
}
|
}
|
||||||
shared.refs.Store(1)
|
shared.refs.Store(1)
|
||||||
session, err := shared.newSession()
|
sessionRef, err := newAppleTransportSession(shared)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bridge.Close()
|
bridge.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
session := (*C.box_apple_http_session_t)(sessionRef)
|
||||||
|
releaseConfig = false
|
||||||
return &appleTransport{
|
return &appleTransport{
|
||||||
shared: shared,
|
shared: shared,
|
||||||
session: session,
|
session: session,
|
||||||
@@ -142,7 +162,7 @@ func newAppleSessionConfig(ctx context.Context, options option.HTTPClientOptions
|
|||||||
if len(tlsOptions.ALPN) > 0 {
|
if len(tlsOptions.ALPN) > 0 {
|
||||||
return appleSessionConfig{}, E.New("tls.alpn is unsupported in Apple HTTP engine")
|
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 {
|
if err != nil {
|
||||||
return appleSessionConfig{}, err
|
return appleSessionConfig{}, err
|
||||||
}
|
}
|
||||||
@@ -152,13 +172,23 @@ func newAppleSessionConfig(ctx context.Context, options option.HTTPClientOptions
|
|||||||
minVersion: validated.MinVersion,
|
minVersion: validated.MinVersion,
|
||||||
maxVersion: validated.MaxVersion,
|
maxVersion: validated.MaxVersion,
|
||||||
insecure: tlsOptions.Insecure || len(tlsOptions.CertificatePublicKeySHA256) > 0,
|
insecure: tlsOptions.Insecure || len(tlsOptions.CertificatePublicKeySHA256) > 0,
|
||||||
anchorPEM: validated.AnchorPEM,
|
anchorOnly: validated.Exclusive,
|
||||||
anchorOnly: validated.AnchorOnly,
|
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 {
|
if len(tlsOptions.CertificatePublicKeySHA256) > 0 {
|
||||||
config.pinnedPublicKeySHA256s = make([]byte, 0, len(tlsOptions.CertificatePublicKeySHA256)*applePinnedHashSize)
|
config.pinnedPublicKeySHA256s = make([]byte, 0, len(tlsOptions.CertificatePublicKeySHA256)*applePinnedHashSize)
|
||||||
for _, hashValue := range tlsOptions.CertificatePublicKeySHA256 {
|
for _, hashValue := range tlsOptions.CertificatePublicKeySHA256 {
|
||||||
if len(hashValue) != applePinnedHashSize {
|
if len(hashValue) != applePinnedHashSize {
|
||||||
|
if config.userAnchors != nil {
|
||||||
|
config.userAnchors.Release()
|
||||||
|
}
|
||||||
return appleSessionConfig{}, E.New("invalid certificate_public_key_sha256 length: ", len(hashValue))
|
return appleSessionConfig{}, E.New("invalid certificate_public_key_sha256 length: ", len(hashValue))
|
||||||
}
|
}
|
||||||
config.pinnedPublicKeySHA256s = append(config.pinnedPublicKeySHA256s, hashValue...)
|
config.pinnedPublicKeySHA256s = append(config.pinnedPublicKeySHA256s, hashValue...)
|
||||||
@@ -167,12 +197,20 @@ func newAppleSessionConfig(ctx context.Context, options option.HTTPClientOptions
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *appleSessionConfig) close() {
|
||||||
|
if c.userAnchors != nil {
|
||||||
|
c.userAnchors.Release()
|
||||||
|
c.userAnchors = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *appleTransportShared) retain() {
|
func (s *appleTransportShared) retain() {
|
||||||
s.refs.Add(1)
|
s.refs.Add(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appleTransportShared) release() error {
|
func (s *appleTransportShared) release() error {
|
||||||
if s.refs.Add(-1) == 0 {
|
if s.refs.Add(-1) == 0 {
|
||||||
|
s.config.close()
|
||||||
return s.bridge.Close()
|
return s.bridge.Close()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -185,16 +223,17 @@ func (s *appleTransportShared) newSession() (*C.box_apple_http_session_t, error)
|
|||||||
defer C.free(unsafe.Pointer(cProxyUsername))
|
defer C.free(unsafe.Pointer(cProxyUsername))
|
||||||
cProxyPassword := C.CString(s.bridge.Password())
|
cProxyPassword := C.CString(s.bridge.Password())
|
||||||
defer C.free(unsafe.Pointer(cProxyPassword))
|
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
|
var pinnedPointer *C.uint8_t
|
||||||
if len(s.config.pinnedPublicKeySHA256s) > 0 {
|
if len(s.config.pinnedPublicKeySHA256s) > 0 {
|
||||||
pinnedPointer = (*C.uint8_t)(C.CBytes(s.config.pinnedPublicKeySHA256s))
|
pinnedPointer = (*C.uint8_t)(C.CBytes(s.config.pinnedPublicKeySHA256s))
|
||||||
defer C.free(unsafe.Pointer(pinnedPointer))
|
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{
|
cConfig := C.box_apple_http_session_config_t{
|
||||||
proxy_host: cProxyHost,
|
proxy_host: cProxyHost,
|
||||||
proxy_port: C.int(s.bridge.Port()),
|
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),
|
min_tls_version: C.uint16_t(s.config.minVersion),
|
||||||
max_tls_version: C.uint16_t(s.config.maxVersion),
|
max_tls_version: C.uint16_t(s.config.maxVersion),
|
||||||
insecure: C.bool(s.config.insecure),
|
insecure: C.bool(s.config.insecure),
|
||||||
anchor_pem: cAnchorPEM,
|
anchors_cf: anchorsRef,
|
||||||
anchor_pem_len: C.size_t(len(s.config.anchorPEM)),
|
|
||||||
anchor_only: C.bool(s.config.anchorOnly),
|
anchor_only: C.bool(s.config.anchorOnly),
|
||||||
pinned_public_key_sha256: pinnedPointer,
|
pinned_public_key_sha256: pinnedPointer,
|
||||||
pinned_public_key_sha256_len: C.size_t(len(s.config.pinnedPublicKeySHA256s)),
|
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 min_tls_version;
|
||||||
uint16_t max_tls_version;
|
uint16_t max_tls_version;
|
||||||
bool insecure;
|
bool insecure;
|
||||||
const char *anchor_pem;
|
void *anchors_cf;
|
||||||
size_t anchor_pem_len;
|
|
||||||
bool anchor_only;
|
bool anchor_only;
|
||||||
const uint8_t *pinned_public_key_sha256;
|
const uint8_t *pinned_public_key_sha256;
|
||||||
size_t pinned_public_key_sha256_len;
|
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);
|
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) {
|
static bool box_evaluate_trust(SecTrustRef trustRef, NSArray *anchors, bool anchor_only, NSDate *verifyDate) {
|
||||||
if (trustRef == NULL) {
|
if (trustRef == NULL) {
|
||||||
return false;
|
return false;
|
||||||
@@ -249,7 +211,11 @@ box_apple_http_session_t *box_apple_http_session_create(
|
|||||||
if (config != NULL) {
|
if (config != NULL) {
|
||||||
delegate.insecure = config->insecure;
|
delegate.insecure = config->insecure;
|
||||||
delegate.anchorOnly = config->anchor_only;
|
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) {
|
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];
|
delegate.pinnedPublicKeyHashes = [NSData dataWithBytes:config->pinned_public_key_sha256 length:config->pinned_public_key_sha256_len];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/proxybridge"
|
||||||
boxTLS "github.com/sagernet/sing-box/common/tls"
|
boxTLS "github.com/sagernet/sing-box/common/tls"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-box/route"
|
"github.com/sagernet/sing-box/route"
|
||||||
"github.com/sagernet/sing/common/json/badoption"
|
"github.com/sagernet/sing/common/json/badoption"
|
||||||
|
commonLogger "github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
@@ -58,6 +61,23 @@ type appleHTTPTestServer struct {
|
|||||||
publicKeyHash []byte
|
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) {
|
func TestNewAppleSessionConfig(t *testing.T) {
|
||||||
serverCertificate, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost")
|
serverCertificate, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost")
|
||||||
serverHash := certificatePublicKeySHA256(t, serverCertificate.Certificate[0])
|
serverHash := certificatePublicKeySHA256(t, serverCertificate.Certificate[0])
|
||||||
@@ -103,8 +123,14 @@ func TestNewAppleSessionConfig(t *testing.T) {
|
|||||||
if !config.anchorOnly {
|
if !config.anchorOnly {
|
||||||
t.Fatal("expected anchor_only")
|
t.Fatal("expected anchor_only")
|
||||||
}
|
}
|
||||||
if !strings.Contains(config.anchorPEM, "BEGIN CERTIFICATE") {
|
if config.userAnchors == nil {
|
||||||
t.Fatalf("unexpected anchor pem: %q", config.anchorPEM)
|
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 {
|
if len(config.pinnedPublicKeySHA256s) != 0 {
|
||||||
t.Fatalf("unexpected pinned hashes: %d", len(config.pinnedPublicKeySHA256s))
|
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) {
|
if !bytes.Equal(config.pinnedPublicKeySHA256s[applePinnedHashSize:], otherHash) {
|
||||||
t.Fatal("unexpected second pin")
|
t.Fatal("unexpected second pin")
|
||||||
}
|
}
|
||||||
if config.anchorPEM != "" {
|
if config.userAnchors != nil {
|
||||||
t.Fatalf("unexpected anchor pem: %q", config.anchorPEM)
|
t.Fatal("unexpected user anchors")
|
||||||
}
|
}
|
||||||
if config.anchorOnly {
|
if config.anchorOnly {
|
||||||
t.Fatal("unexpected anchor_only")
|
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) {
|
func TestAppleTransportRoundTripHTTPS(t *testing.T) {
|
||||||
requests := make(chan appleHTTPObservedRequest, 1)
|
requests := make(chan appleHTTPObservedRequest, 1)
|
||||||
server := startAppleHTTPTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
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"))
|
assertAppleHTTPSucceeds(t, transport, server.URL("/reset"))
|
||||||
|
|
||||||
innerTransport := transport.(*appleTransport)
|
innerTransport := transport.(*appleTransport)
|
||||||
if err := innerTransport.Close(); err != nil {
|
err := innerTransport.Close()
|
||||||
|
if err != nil {
|
||||||
t.Fatal(err)
|
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 {
|
func newAppleHTTPTestTransport(t *testing.T, server *appleHTTPTestServer, options option.HTTPClientOptions) innerTransport {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
ctx := service.ContextWith[adapter.ConnectionManager](
|
ctx := newAppleHTTPTestContext()
|
||||||
context.Background(),
|
|
||||||
route.NewConnectionManager(log.NewNOPFactory().NewLogger("connection")),
|
|
||||||
)
|
|
||||||
dialer := &appleHTTPTestDialer{
|
dialer := &appleHTTPTestDialer{
|
||||||
hostMap: make(map[string]string),
|
hostMap: make(map[string]string),
|
||||||
}
|
}
|
||||||
@@ -743,6 +807,39 @@ func newAppleHTTPTestTransport(t *testing.T, server *appleHTTPTestServer, option
|
|||||||
return transport
|
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) {
|
func (d *appleHTTPTestDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
host := destination.AddrString()
|
host := destination.AddrString()
|
||||||
if destination.IsDomain() {
|
if destination.IsDomain() {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func NewTransport(ctx context.Context, logger logger.ContextLogger, tag string,
|
|||||||
}
|
}
|
||||||
managedTransport.epoch.Store(&transportEpoch{transport: inner})
|
managedTransport.epoch.Store(&transportEpoch{transport: inner})
|
||||||
return managedTransport, nil
|
return managedTransport, nil
|
||||||
case C.TLSEngineDefault, "go":
|
case "", C.TLSEngineGo:
|
||||||
cheapRebuild = true
|
cheapRebuild = true
|
||||||
default:
|
default:
|
||||||
return nil, E.New("unknown HTTP engine: ", options.Engine)
|
return nil, E.New("unknown HTTP engine: ", options.Engine)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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])),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// Code generated by 'go generate'; DO NOT EDIT.
|
||||||
|
|
||||||
|
package schannel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ unsafe.Pointer
|
||||||
|
|
||||||
|
// Do the interface allocations only once for common
|
||||||
|
// Errno values.
|
||||||
|
const (
|
||||||
|
errnoERROR_IO_PENDING = 997
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||||
|
errERROR_EINVAL error = syscall.EINVAL
|
||||||
|
)
|
||||||
|
|
||||||
|
// errnoErr returns common boxed Errno values, to prevent
|
||||||
|
// allocations at runtime.
|
||||||
|
func errnoErr(e syscall.Errno) error {
|
||||||
|
switch e {
|
||||||
|
case 0:
|
||||||
|
return errERROR_EINVAL
|
||||||
|
case errnoERROR_IO_PENDING:
|
||||||
|
return errERROR_IO_PENDING
|
||||||
|
}
|
||||||
|
// TODO: add more here, after collecting data on the common
|
||||||
|
// error values see on Windows. (perhaps when running
|
||||||
|
// all.bat?)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
modsecur32 = windows.NewLazySystemDLL("secur32.dll")
|
||||||
|
|
||||||
|
procAcquireCredentialsHandleW = modsecur32.NewProc("AcquireCredentialsHandleW")
|
||||||
|
procDecryptMessage = modsecur32.NewProc("DecryptMessage")
|
||||||
|
procDeleteSecurityContext = modsecur32.NewProc("DeleteSecurityContext")
|
||||||
|
procEncryptMessage = modsecur32.NewProc("EncryptMessage")
|
||||||
|
procFreeContextBuffer = modsecur32.NewProc("FreeContextBuffer")
|
||||||
|
procFreeCredentialsHandle = modsecur32.NewProc("FreeCredentialsHandle")
|
||||||
|
procInitializeSecurityContextW = modsecur32.NewProc("InitializeSecurityContextW")
|
||||||
|
procQueryContextAttributesW = modsecur32.NewProc("QueryContextAttributesW")
|
||||||
|
)
|
||||||
|
|
||||||
|
func sspiAcquireCredentialsHandle(principal *uint16, pkgname *uint16, credentialUse uint32, logonID *uint64, authData unsafe.Pointer, getKeyFn uintptr, getKeyArg uintptr, credential *secHandle, expiry *windows.Filetime) (ret syscall.Errno) {
|
||||||
|
r0, _, _ := syscall.SyscallN(procAcquireCredentialsHandleW.Addr(), uintptr(unsafe.Pointer(principal)), uintptr(unsafe.Pointer(pkgname)), uintptr(credentialUse), uintptr(unsafe.Pointer(logonID)), uintptr(authData), uintptr(getKeyFn), uintptr(getKeyArg), uintptr(unsafe.Pointer(credential)), uintptr(unsafe.Pointer(expiry)))
|
||||||
|
ret = syscall.Errno(r0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func sspiDecryptMessage(context *secHandle, message *secBufferDesc, sequenceNumber uint32, qop *uint32) (ret syscall.Errno) {
|
||||||
|
r0, _, _ := syscall.SyscallN(procDecryptMessage.Addr(), uintptr(unsafe.Pointer(context)), uintptr(unsafe.Pointer(message)), uintptr(sequenceNumber), uintptr(unsafe.Pointer(qop)))
|
||||||
|
ret = syscall.Errno(r0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func sspiDeleteSecurityContext(context *secHandle) (ret syscall.Errno) {
|
||||||
|
r0, _, _ := syscall.SyscallN(procDeleteSecurityContext.Addr(), uintptr(unsafe.Pointer(context)))
|
||||||
|
ret = syscall.Errno(r0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func sspiEncryptMessage(context *secHandle, qop uint32, message *secBufferDesc, sequenceNumber uint32) (ret syscall.Errno) {
|
||||||
|
r0, _, _ := syscall.SyscallN(procEncryptMessage.Addr(), uintptr(unsafe.Pointer(context)), uintptr(qop), uintptr(unsafe.Pointer(message)), uintptr(sequenceNumber))
|
||||||
|
ret = syscall.Errno(r0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func sspiFreeContextBuffer(buffer *byte) (ret syscall.Errno) {
|
||||||
|
r0, _, _ := syscall.SyscallN(procFreeContextBuffer.Addr(), uintptr(unsafe.Pointer(buffer)))
|
||||||
|
ret = syscall.Errno(r0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func sspiFreeCredentialsHandle(credential *secHandle) (ret syscall.Errno) {
|
||||||
|
r0, _, _ := syscall.SyscallN(procFreeCredentialsHandle.Addr(), uintptr(unsafe.Pointer(credential)))
|
||||||
|
ret = syscall.Errno(r0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func sspiInitializeSecurityContext(credential *secHandle, context *secHandle, targetName *uint16, contextReq uint32, reserved1 uint32, targetDataRep uint32, input *secBufferDesc, reserved2 uint32, newContext *secHandle, output *secBufferDesc, contextAttr *uint32, expiry *windows.Filetime) (ret syscall.Errno) {
|
||||||
|
r0, _, _ := syscall.SyscallN(procInitializeSecurityContextW.Addr(), uintptr(unsafe.Pointer(credential)), uintptr(unsafe.Pointer(context)), uintptr(unsafe.Pointer(targetName)), uintptr(contextReq), uintptr(reserved1), uintptr(targetDataRep), uintptr(unsafe.Pointer(input)), uintptr(reserved2), uintptr(unsafe.Pointer(newContext)), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(contextAttr)), uintptr(unsafe.Pointer(expiry)))
|
||||||
|
ret = syscall.Errno(r0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func sspiQueryContextAttributes(context *secHandle, attribute uint32, buffer unsafe.Pointer) (ret syscall.Errno) {
|
||||||
|
r0, _, _ := syscall.SyscallN(procQueryContextAttributesW.Addr(), uintptr(unsafe.Pointer(context)), uintptr(attribute), uintptr(buffer))
|
||||||
|
ret = syscall.Errno(r0)
|
||||||
|
return
|
||||||
|
}
|
||||||
+16
-193
@@ -4,218 +4,41 @@ package tls
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"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"
|
"github.com/sagernet/sing-box/option"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
"github.com/sagernet/sing/common/ntp"
|
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type appleCertificateStore interface {
|
const appleTLSEngineName = "Apple TLS engine"
|
||||||
StoreKind() string
|
|
||||||
CurrentPEM() []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type appleClientConfig struct {
|
type appleClientConfig struct {
|
||||||
serverName string
|
systemTLSConfig
|
||||||
nextProtos []string
|
userPEM []byte
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *appleClientConfig) Clone() Config {
|
func (c *appleClientConfig) Clone() Config {
|
||||||
return &appleClientConfig{
|
return &appleClientConfig{
|
||||||
serverName: c.serverName,
|
systemTLSConfig: c.systemTLSConfig.clone(),
|
||||||
nextProtos: append([]string(nil), c.nextProtos...),
|
userPEM: append([]byte(nil), c.userPEM...),
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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{
|
return &appleClientConfig{
|
||||||
serverName: serverName,
|
systemTLSConfig: base,
|
||||||
nextProtos: append([]string(nil), options.ALPN...),
|
userPEM: append([]byte(nil), validated.UserPEM...),
|
||||||
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),
|
|
||||||
}, nil
|
}, 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"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime/cgo"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common/buf"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (Conn, error) {
|
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 {
|
if !ok {
|
||||||
return nil, E.New("apple TLS: requires fd-backed TCP connection")
|
return nil, E.New("apple TLS: requires fd-backed TCP connection")
|
||||||
}
|
}
|
||||||
syscallConn, err := rawSyscallConn.SyscallConn()
|
syscallConn, err := tcpConn.SyscallConn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "access raw connection")
|
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)
|
alpnPtr := cStringOrNil(alpn)
|
||||||
defer cFree(alpnPtr)
|
defer cFree(alpnPtr)
|
||||||
|
|
||||||
anchorPEMPtr := cStringOrNil(c.anchorPEM)
|
anchors, err := c.resolveAnchors()
|
||||||
defer cFree(anchorPEMPtr)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var anchorsRef unsafe.Pointer
|
||||||
|
if anchors != nil {
|
||||||
|
anchorsRef = anchors.Ref()
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
hasVerifyTime bool
|
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.minVersion),
|
||||||
C.uint16_t(c.maxVersion),
|
C.uint16_t(c.maxVersion),
|
||||||
C.bool(c.insecure),
|
C.bool(c.insecure),
|
||||||
anchorPEMPtr,
|
anchorsRef,
|
||||||
C.size_t(len(c.anchorPEM)),
|
|
||||||
C.bool(c.anchorOnly),
|
C.bool(c.anchorOnly),
|
||||||
C.bool(hasVerifyTime),
|
C.bool(hasVerifyTime),
|
||||||
C.int64_t(verifyTimeUnixMilli),
|
C.int64_t(verifyTimeUnixMilli),
|
||||||
&errorPtr,
|
&errorPtr,
|
||||||
)
|
)
|
||||||
|
if anchors != nil {
|
||||||
|
anchors.Release()
|
||||||
|
}
|
||||||
if client == nil {
|
if client == nil {
|
||||||
if errorPtr != nil {
|
if errorPtr != nil {
|
||||||
defer C.free(unsafe.Pointer(errorPtr))
|
defer C.free(unsafe.Pointer(errorPtr))
|
||||||
@@ -138,21 +147,27 @@ func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn)
|
|||||||
}, nil
|
}, 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 {
|
func waitAppleTLSClientReady(ctx context.Context, client *C.box_apple_tls_client_t) error {
|
||||||
for {
|
for {
|
||||||
if err := ctx.Err(); err != nil {
|
err := ctx.Err()
|
||||||
|
if err != nil {
|
||||||
C.box_apple_tls_client_cancel(client)
|
C.box_apple_tls_client_cancel(client)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
waitTimeout := appleTLSHandshakePollInterval
|
waitTimeout := appleTLSHandshakePollInterval
|
||||||
if deadline, loaded := ctx.Deadline(); loaded {
|
deadline, loaded := ctx.Deadline()
|
||||||
|
if loaded {
|
||||||
remaining := time.Until(deadline)
|
remaining := time.Until(deadline)
|
||||||
if remaining <= 0 {
|
if remaining <= 0 {
|
||||||
C.box_apple_tls_client_cancel(client)
|
C.box_apple_tls_client_cancel(client)
|
||||||
if err := ctx.Err(); err != nil {
|
err = ctx.Err()
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return context.DeadlineExceeded
|
return context.DeadlineExceeded
|
||||||
@@ -201,6 +216,11 @@ type appleTLSConn struct {
|
|||||||
writeTimedOut bool
|
writeTimedOut bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ N.ExtendedConn = (*appleTLSConn)(nil)
|
||||||
|
_ N.ReadWaitCreator = (*appleTLSConn)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
func (c *appleTLSConn) Read(p []byte) (int, error) {
|
func (c *appleTLSConn) Read(p []byte) (int, error) {
|
||||||
c.readAccess.Lock()
|
c.readAccess.Lock()
|
||||||
defer c.readAccess.Unlock()
|
defer c.readAccess.Unlock()
|
||||||
@@ -211,6 +231,29 @@ func (c *appleTLSConn) Read(p []byte) (int, error) {
|
|||||||
return 0, nil
|
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()
|
timeoutMs, err := c.prepareReadTimeout()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -256,34 +299,55 @@ func (c *appleTLSConn) Write(p []byte) (int, error) {
|
|||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
timeoutMs, err := c.prepareWriteTimeout()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := c.acquireClient()
|
client, err := c.acquireClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
defer c.releaseClient()
|
defer c.releaseClient()
|
||||||
|
|
||||||
var errorPtr *C.char
|
deadline, err := c.prepareWriteDeadline()
|
||||||
n := C.box_apple_tls_client_write(client, unsafe.Pointer(&p[0]), C.size_t(len(p)), C.int(timeoutMs), &errorPtr)
|
if err != nil {
|
||||||
switch {
|
return 0, err
|
||||||
case n == -2:
|
|
||||||
c.markWriteTimedOut()
|
|
||||||
return 0, os.ErrDeadlineExceeded
|
|
||||||
case n >= 0:
|
|
||||||
return int(n), nil
|
|
||||||
}
|
}
|
||||||
if errorPtr != nil {
|
var written int
|
||||||
defer C.free(unsafe.Pointer(errorPtr))
|
for written < len(p) {
|
||||||
if c.isClosed() {
|
timeoutMs, expired := deadlineTimeoutMs(deadline)
|
||||||
return 0, net.ErrClosed
|
if expired {
|
||||||
|
C.box_apple_tls_client_cancel(client)
|
||||||
|
c.markWriteTimedOut()
|
||||||
|
return written, os.ErrDeadlineExceeded
|
||||||
}
|
}
|
||||||
return 0, E.New(C.GoString(errorPtr))
|
chunkSize := min(len(p)-written, appleTLSWriteChunkSize)
|
||||||
|
chunk := p[written : written+chunkSize]
|
||||||
|
var errorPtr *C.char
|
||||||
|
n := C.box_apple_tls_client_write(client, unsafe.Pointer(&chunk[0]), C.size_t(len(chunk)), C.int(timeoutMs), &errorPtr)
|
||||||
|
switch {
|
||||||
|
case n == -2:
|
||||||
|
c.markWriteTimedOut()
|
||||||
|
return written, os.ErrDeadlineExceeded
|
||||||
|
case n >= 0:
|
||||||
|
written += int(n)
|
||||||
|
if int(n) != len(chunk) {
|
||||||
|
return written, io.ErrShortWrite
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return written, c.errorFromPointer(errorPtr)
|
||||||
}
|
}
|
||||||
return 0, net.ErrClosed
|
return written, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *appleTLSConn) WriteBuffer(buffer *buf.Buffer) error {
|
||||||
|
defer buffer.Release()
|
||||||
|
_, err := c.Write(buffer.Bytes())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *appleTLSConn) CreateReadWaiter() (N.ReadWaiter, bool) {
|
||||||
|
return &appleTLSReadWaiter{
|
||||||
|
conn: c,
|
||||||
|
results: make(chan *C.box_apple_tls_read_result_t, 1),
|
||||||
|
}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *appleTLSConn) Close() error {
|
func (c *appleTLSConn) Close() error {
|
||||||
@@ -358,18 +422,18 @@ func (c *appleTLSConn) prepareReadTimeout() (int, error) {
|
|||||||
return timeoutMs, nil
|
return timeoutMs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *appleTLSConn) prepareWriteTimeout() (int, error) {
|
func (c *appleTLSConn) prepareWriteDeadline() (time.Time, error) {
|
||||||
c.deadlineAccess.Lock()
|
c.deadlineAccess.Lock()
|
||||||
defer c.deadlineAccess.Unlock()
|
defer c.deadlineAccess.Unlock()
|
||||||
if c.writeTimedOut {
|
if c.writeTimedOut {
|
||||||
return 0, os.ErrDeadlineExceeded
|
return time.Time{}, os.ErrDeadlineExceeded
|
||||||
}
|
}
|
||||||
timeoutMs, expired := deadlineTimeoutMs(c.writeDeadline)
|
_, expired := deadlineTimeoutMs(c.writeDeadline)
|
||||||
if expired {
|
if expired {
|
||||||
c.writeTimedOut = true
|
c.writeTimedOut = true
|
||||||
return 0, os.ErrDeadlineExceeded
|
return time.Time{}, os.ErrDeadlineExceeded
|
||||||
}
|
}
|
||||||
return timeoutMs, nil
|
return c.writeDeadline, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *appleTLSConn) markReadTimedOut() {
|
func (c *appleTLSConn) markReadTimedOut() {
|
||||||
@@ -422,6 +486,138 @@ func (c *appleTLSConn) releaseClient() {
|
|||||||
c.ioGroup.Done()
|
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 {
|
func (c *appleTLSConn) NetConn() net.Conn {
|
||||||
return c.rawConn
|
return c.rawConn
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,278 @@
|
|||||||
|
//go:build darwin && cgo
|
||||||
|
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
stdtls "crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
"github.com/sagernet/sing/common/json/badoption"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
appleTLSBenchmarkReadPayloadSize = 16 * 1024
|
||||||
|
appleTLSBenchmarkWritePayloadSize = 48 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkAppleClientReadBuffer(b *testing.B) {
|
||||||
|
payload := bytes.Repeat([]byte{'r'}, appleTLSBenchmarkReadPayloadSize)
|
||||||
|
start := make(chan struct{})
|
||||||
|
clientConn, serverResult := newAppleBenchmarkClientConn(b, func(conn *stdtls.Conn) error {
|
||||||
|
<-start
|
||||||
|
for range b.N {
|
||||||
|
if err := writeBenchmarkPayload(conn, payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
defer clientConn.Close()
|
||||||
|
|
||||||
|
extendedConn := clientConn.(N.ExtendedConn)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(payload)))
|
||||||
|
b.ReportMetric(float64(len(payload)), "payload_B")
|
||||||
|
b.ResetTimer()
|
||||||
|
close(start)
|
||||||
|
target := b.N * len(payload)
|
||||||
|
var received int
|
||||||
|
for received < target {
|
||||||
|
buffer := buf.NewSize(len(payload))
|
||||||
|
err := extendedConn.ReadBuffer(buffer)
|
||||||
|
if err != nil {
|
||||||
|
buffer.Release()
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
received += buffer.Len()
|
||||||
|
buffer.Release()
|
||||||
|
}
|
||||||
|
b.StopTimer()
|
||||||
|
if err := <-serverResult; err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAppleClientReadWaiter(b *testing.B) {
|
||||||
|
payload := bytes.Repeat([]byte{'w'}, appleTLSBenchmarkReadPayloadSize)
|
||||||
|
start := make(chan struct{})
|
||||||
|
clientConn, serverResult := newAppleBenchmarkClientConn(b, func(conn *stdtls.Conn) error {
|
||||||
|
<-start
|
||||||
|
for range b.N {
|
||||||
|
if err := writeBenchmarkPayload(conn, payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
defer clientConn.Close()
|
||||||
|
|
||||||
|
readWaiter, ok := clientConn.(N.ReadWaitCreator).CreateReadWaiter()
|
||||||
|
if !ok {
|
||||||
|
b.Fatal("expected read waiter")
|
||||||
|
}
|
||||||
|
readWaiter.InitializeReadWaiter(N.ReadWaitOptions{
|
||||||
|
MTU: appleTLSBenchmarkReadPayloadSize,
|
||||||
|
})
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(payload)))
|
||||||
|
b.ReportMetric(float64(len(payload)), "payload_B")
|
||||||
|
b.ResetTimer()
|
||||||
|
close(start)
|
||||||
|
target := b.N * len(payload)
|
||||||
|
var received int
|
||||||
|
for received < target {
|
||||||
|
buffer, err := readWaiter.WaitReadBuffer()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.ErrNoProgress) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
received += buffer.Len()
|
||||||
|
buffer.Release()
|
||||||
|
}
|
||||||
|
b.StopTimer()
|
||||||
|
if err := <-serverResult; err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAppleClientWriteBuffer(b *testing.B) {
|
||||||
|
payload := bytes.Repeat([]byte{'x'}, appleTLSBenchmarkWritePayloadSize)
|
||||||
|
start := make(chan struct{})
|
||||||
|
clientConn, serverResult := newAppleBenchmarkClientConn(b, func(conn *stdtls.Conn) error {
|
||||||
|
<-start
|
||||||
|
_, err := io.CopyN(io.Discard, conn, int64(b.N*len(payload)))
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
defer clientConn.Close()
|
||||||
|
|
||||||
|
extendedConn := clientConn.(N.ExtendedConn)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(payload)))
|
||||||
|
b.ReportMetric(float64(len(payload)), "payload_B")
|
||||||
|
b.ReportMetric(float64(appleTLSWriteChunkSize), "write_chunk_B")
|
||||||
|
b.ResetTimer()
|
||||||
|
close(start)
|
||||||
|
for range b.N {
|
||||||
|
buffer := buf.NewSize(len(payload))
|
||||||
|
_, err := buffer.Write(payload)
|
||||||
|
if err != nil {
|
||||||
|
buffer.Release()
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
err = extendedConn.WriteBuffer(buffer)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.StopTimer()
|
||||||
|
if err := <-serverResult; err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStdlibClientReadBuffer(b *testing.B) {
|
||||||
|
payload := bytes.Repeat([]byte{'r'}, appleTLSBenchmarkReadPayloadSize)
|
||||||
|
start := make(chan struct{})
|
||||||
|
clientConn, serverResult := newStdlibBenchmarkClientConn(b, func(conn *stdtls.Conn) error {
|
||||||
|
<-start
|
||||||
|
for range b.N {
|
||||||
|
if err := writeBenchmarkPayload(conn, payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
defer clientConn.Close()
|
||||||
|
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(payload)))
|
||||||
|
b.ReportMetric(float64(len(payload)), "payload_B")
|
||||||
|
b.ResetTimer()
|
||||||
|
close(start)
|
||||||
|
target := b.N * len(payload)
|
||||||
|
var received int
|
||||||
|
for received < target {
|
||||||
|
buffer := buf.NewSize(len(payload))
|
||||||
|
n, err := clientConn.Read(buffer.FreeBytes())
|
||||||
|
if n > 0 {
|
||||||
|
buffer.Truncate(buffer.Len() + n)
|
||||||
|
}
|
||||||
|
received += buffer.Len()
|
||||||
|
buffer.Release()
|
||||||
|
if err != nil && received < target {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.StopTimer()
|
||||||
|
if err := <-serverResult; err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStdlibClientWriteBuffer(b *testing.B) {
|
||||||
|
payload := bytes.Repeat([]byte{'x'}, appleTLSBenchmarkWritePayloadSize)
|
||||||
|
start := make(chan struct{})
|
||||||
|
clientConn, serverResult := newStdlibBenchmarkClientConn(b, func(conn *stdtls.Conn) error {
|
||||||
|
<-start
|
||||||
|
_, err := io.CopyN(io.Discard, conn, int64(b.N*len(payload)))
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
defer clientConn.Close()
|
||||||
|
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(payload)))
|
||||||
|
b.ReportMetric(float64(len(payload)), "payload_B")
|
||||||
|
b.ResetTimer()
|
||||||
|
close(start)
|
||||||
|
for range b.N {
|
||||||
|
buffer := buf.NewSize(len(payload))
|
||||||
|
_, err := buffer.Write(payload)
|
||||||
|
if err != nil {
|
||||||
|
buffer.Release()
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = clientConn.Write(buffer.Bytes())
|
||||||
|
buffer.Release()
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.StopTimer()
|
||||||
|
if err := <-serverResult; err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAppleBenchmarkClientConn(b *testing.B, handler func(*stdtls.Conn) error) (Conn, <-chan error) {
|
||||||
|
b.Helper()
|
||||||
|
|
||||||
|
serverCertificate, serverCertificatePEM := newAppleTestCertificate(b, "localhost")
|
||||||
|
serverResult, serverAddress := startAppleTLSIOTestServer(b, &stdtls.Config{
|
||||||
|
Certificates: []stdtls.Certificate{serverCertificate},
|
||||||
|
MinVersion: stdtls.VersionTLS12,
|
||||||
|
MaxVersion: stdtls.VersionTLS12,
|
||||||
|
}, handler)
|
||||||
|
|
||||||
|
clientConn, err := newAppleTestClientConn(b, serverAddress, option.OutboundTLSOptions{
|
||||||
|
Enabled: true,
|
||||||
|
Engine: "apple",
|
||||||
|
ServerName: "localhost",
|
||||||
|
MinVersion: "1.2",
|
||||||
|
MaxVersion: "1.2",
|
||||||
|
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
return clientConn, serverResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStdlibBenchmarkClientConn(b *testing.B, handler func(*stdtls.Conn) error) (*stdtls.Conn, <-chan error) {
|
||||||
|
b.Helper()
|
||||||
|
|
||||||
|
serverCertificate, serverCertificatePEM := newAppleTestCertificate(b, "localhost")
|
||||||
|
serverResult, serverAddress := startAppleTLSIOTestServer(b, &stdtls.Config{
|
||||||
|
Certificates: []stdtls.Certificate{serverCertificate},
|
||||||
|
MinVersion: stdtls.VersionTLS12,
|
||||||
|
MaxVersion: stdtls.VersionTLS12,
|
||||||
|
}, handler)
|
||||||
|
|
||||||
|
roots := x509.NewCertPool()
|
||||||
|
if !roots.AppendCertsFromPEM([]byte(serverCertificatePEM)) {
|
||||||
|
b.Fatal("parse benchmark certificate")
|
||||||
|
}
|
||||||
|
dialer := &net.Dialer{
|
||||||
|
Timeout: appleTLSTestTimeout,
|
||||||
|
}
|
||||||
|
clientConn, err := stdtls.DialWithDialer(dialer, "tcp", serverAddress, &stdtls.Config{
|
||||||
|
ServerName: "localhost",
|
||||||
|
RootCAs: roots,
|
||||||
|
MinVersion: stdtls.VersionTLS12,
|
||||||
|
MaxVersion: stdtls.VersionTLS12,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
return clientConn, serverResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeBenchmarkPayload(writer io.Writer, payload []byte) error {
|
||||||
|
for len(payload) > 0 {
|
||||||
|
n, err := writer.Write(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
payload = payload[n:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
typedef struct box_apple_tls_client box_apple_tls_client_t;
|
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 {
|
typedef struct box_apple_tls_state {
|
||||||
uint16_t version;
|
uint16_t version;
|
||||||
@@ -22,8 +23,7 @@ box_apple_tls_client_t *box_apple_tls_client_create(
|
|||||||
uint16_t min_version,
|
uint16_t min_version,
|
||||||
uint16_t max_version,
|
uint16_t max_version,
|
||||||
bool insecure,
|
bool insecure,
|
||||||
const char *anchor_pem,
|
void *anchors_cf,
|
||||||
size_t anchor_pem_len,
|
|
||||||
bool anchor_only,
|
bool anchor_only,
|
||||||
bool has_verify_time,
|
bool has_verify_time,
|
||||||
int64_t verify_time_unix_millis,
|
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);
|
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_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);
|
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);
|
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);
|
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 *connection;
|
||||||
void *queue;
|
void *queue;
|
||||||
void *ready_semaphore;
|
void *ready_semaphore;
|
||||||
|
void *anchors;
|
||||||
atomic_int ref_count;
|
atomic_int ref_count;
|
||||||
atomic_bool ready;
|
atomic_bool ready;
|
||||||
atomic_bool ready_done;
|
atomic_bool ready_done;
|
||||||
@@ -28,6 +29,14 @@ typedef struct box_apple_tls_client {
|
|||||||
box_apple_tls_state_t state;
|
box_apple_tls_state_t state;
|
||||||
} box_apple_tls_client_t;
|
} 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) {
|
static nw_connection_t box_apple_tls_connection(box_apple_tls_client_t *client) {
|
||||||
if (client == NULL || client->connection == NULL) {
|
if (client == NULL || client->connection == NULL) {
|
||||||
return nil;
|
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;
|
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) {
|
static void box_apple_tls_state_reset(box_apple_tls_state_t *state) {
|
||||||
if (state == NULL) {
|
if (state == NULL) {
|
||||||
return;
|
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) {
|
static void box_apple_tls_client_destroy(box_apple_tls_client_t *client) {
|
||||||
free(client->ready_error);
|
free(client->ready_error);
|
||||||
box_apple_tls_state_reset(&client->state);
|
box_apple_tls_state_reset(&client->state);
|
||||||
|
if (client->anchors != NULL) {
|
||||||
|
CFRelease((CFTypeRef)client->anchors);
|
||||||
|
}
|
||||||
if (client->ready_semaphore != NULL) {
|
if (client->ready_semaphore != NULL) {
|
||||||
CFBridgingRelease(client->ready_semaphore);
|
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);
|
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 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 copy_fn;
|
||||||
static box_sec_protocol_metadata_string_accessor_f get_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;
|
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) {
|
static bool box_evaluate_trust(sec_trust_t trust, NSArray *anchors, bool anchor_only, NSDate *verify_date) {
|
||||||
bool result = false;
|
bool result = false;
|
||||||
SecTrustRef trustRef = sec_trust_copy_ref(trust);
|
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 min_version,
|
||||||
uint16_t max_version,
|
uint16_t max_version,
|
||||||
bool insecure,
|
bool insecure,
|
||||||
const char *anchor_pem,
|
void *anchors_cf,
|
||||||
size_t anchor_pem_len,
|
|
||||||
bool anchor_only,
|
bool anchor_only,
|
||||||
bool has_verify_time,
|
bool has_verify_time,
|
||||||
int64_t verify_time_unix_millis,
|
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->ref_count, 1);
|
||||||
atomic_init(&client->ready, false);
|
atomic_init(&client->ready, false);
|
||||||
atomic_init(&client->ready_done, 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<NSString *> *alpnList = box_split_lines(alpn, alpn_len);
|
||||||
NSArray *anchors = box_parse_certificates_from_pem(anchor_pem, anchor_pem_len);
|
|
||||||
NSDate *verifyDate = nil;
|
NSDate *verifyDate = nil;
|
||||||
if (has_verify_time) {
|
if (has_verify_time) {
|
||||||
verifyDate = [NSDate dateWithTimeIntervalSince1970:(NSTimeInterval)verify_time_unix_millis / 1000.0];
|
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) {
|
if (client->state.version == 0) {
|
||||||
box_apple_tls_state_load(metadata, &client->state);
|
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));
|
}, box_apple_tls_client_queue(client));
|
||||||
}, NW_PARAMETERS_DEFAULT_CONFIGURATION);
|
}, NW_PARAMETERS_DEFAULT_CONFIGURATION);
|
||||||
|
|
||||||
nw_connection_t connection = box_apple_tls_create_connection(connected_socket, parameters);
|
nw_connection_t connection = box_apple_tls_create_connection(connected_socket, parameters);
|
||||||
if (connection == NULL) {
|
if (connection == NULL) {
|
||||||
close(connected_socket);
|
close(connected_socket);
|
||||||
|
if (client->anchors != NULL) {
|
||||||
|
CFRelease((CFTypeRef)client->anchors);
|
||||||
|
}
|
||||||
if (client->ready_semaphore != NULL) {
|
if (client->ready_semaphore != NULL) {
|
||||||
CFBridgingRelease(client->ready_semaphore);
|
CFBridgingRelease(client->ready_semaphore);
|
||||||
}
|
}
|
||||||
@@ -485,128 +522,202 @@ void box_apple_tls_client_free(box_apple_tls_client_t *client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ssize_t box_apple_tls_client_read(box_apple_tls_client_t *client, void *buffer, size_t buffer_len, int timeout_msec, bool *eof_out, char **error_out) {
|
ssize_t box_apple_tls_client_read(box_apple_tls_client_t *client, void *buffer, size_t buffer_len, int timeout_msec, bool *eof_out, char **error_out) {
|
||||||
nw_connection_t connection = box_apple_tls_connection(client);
|
@autoreleasepool {
|
||||||
if (connection == nil) {
|
nw_connection_t connection = box_apple_tls_connection(client);
|
||||||
box_set_error_message(error_out, "apple TLS: invalid client");
|
if (connection == nil) {
|
||||||
return -1;
|
box_set_error_message(error_out, "apple TLS: invalid client");
|
||||||
}
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch_semaphore_t read_semaphore = dispatch_semaphore_create(0);
|
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 bool read_eof = false;
|
||||||
__block char *local_error = NULL;
|
__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) {
|
nw_connection_receive(connection, 1, (uint32_t)buffer_len, ^(dispatch_data_t content, nw_content_context_t context, bool is_complete, nw_error_t error) {
|
||||||
if (content != NULL) {
|
@autoreleasepool {
|
||||||
const void *mapped = NULL;
|
if (content != NULL) {
|
||||||
size_t mapped_len = 0;
|
ssize_t copied = box_apple_tls_dispatch_data_copy(content, buffer, buffer_len, &local_error);
|
||||||
dispatch_data_t mapped_data = dispatch_data_create_map(content, &mapped, &mapped_len);
|
if (copied >= 0) {
|
||||||
if (mapped != NULL && mapped_len > 0) {
|
content_len = (size_t)copied;
|
||||||
content_data = [NSData dataWithBytes:mapped length:mapped_len];
|
}
|
||||||
|
}
|
||||||
|
if (error != NULL && content_len == 0 && local_error == NULL) {
|
||||||
|
box_set_error_from_nw_error(&local_error, error);
|
||||||
|
}
|
||||||
|
if (is_complete && (context == NULL || nw_content_context_get_is_final(context))) {
|
||||||
|
read_eof = true;
|
||||||
|
}
|
||||||
|
dispatch_semaphore_signal(read_semaphore);
|
||||||
}
|
}
|
||||||
(void)mapped_data;
|
});
|
||||||
}
|
|
||||||
if (error != NULL && content_data.length == 0) {
|
|
||||||
box_set_error_from_nw_error(&local_error, error);
|
|
||||||
}
|
|
||||||
if (is_complete && (context == NULL || nw_content_context_get_is_final(context))) {
|
|
||||||
read_eof = true;
|
|
||||||
}
|
|
||||||
dispatch_semaphore_signal(read_semaphore);
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch_time_t wait_deadline = DISPATCH_TIME_FOREVER;
|
dispatch_time_t wait_deadline = DISPATCH_TIME_FOREVER;
|
||||||
if (timeout_msec >= 0) {
|
if (timeout_msec >= 0) {
|
||||||
wait_deadline = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC);
|
wait_deadline = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC);
|
||||||
}
|
}
|
||||||
long wait_result = dispatch_semaphore_wait(read_semaphore, wait_deadline);
|
long wait_result = dispatch_semaphore_wait(read_semaphore, wait_deadline);
|
||||||
if (wait_result != 0) {
|
if (wait_result != 0) {
|
||||||
nw_connection_cancel(connection);
|
nw_connection_cancel(connection);
|
||||||
dispatch_semaphore_wait(read_semaphore, DISPATCH_TIME_FOREVER);
|
dispatch_semaphore_wait(read_semaphore, DISPATCH_TIME_FOREVER);
|
||||||
|
if (local_error != NULL) {
|
||||||
|
free(local_error);
|
||||||
|
local_error = NULL;
|
||||||
|
}
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
if (local_error != NULL) {
|
if (local_error != NULL) {
|
||||||
free(local_error);
|
if (error_out != NULL) {
|
||||||
local_error = NULL;
|
*error_out = local_error;
|
||||||
|
} else {
|
||||||
|
free(local_error);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
return -2;
|
if (eof_out != NULL) {
|
||||||
}
|
*eof_out = read_eof;
|
||||||
if (local_error != NULL) {
|
|
||||||
if (error_out != NULL) {
|
|
||||||
*error_out = local_error;
|
|
||||||
} else {
|
|
||||||
free(local_error);
|
|
||||||
}
|
}
|
||||||
return -1;
|
return (ssize_t)content_len;
|
||||||
}
|
}
|
||||||
if (eof_out != NULL) {
|
|
||||||
*eof_out = read_eof;
|
|
||||||
}
|
|
||||||
if (content_data == nil || content_data.length == 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
memcpy(buffer, content_data.bytes, content_data.length);
|
|
||||||
return (ssize_t)content_data.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ssize_t box_apple_tls_client_write(box_apple_tls_client_t *client, const void *buffer, size_t buffer_len, int timeout_msec, char **error_out) {
|
ssize_t box_apple_tls_client_write(box_apple_tls_client_t *client, const void *buffer, size_t buffer_len, int timeout_msec, char **error_out) {
|
||||||
nw_connection_t connection = box_apple_tls_connection(client);
|
@autoreleasepool {
|
||||||
if (connection == nil) {
|
nw_connection_t connection = box_apple_tls_connection(client);
|
||||||
box_set_error_message(error_out, "apple TLS: invalid client");
|
if (connection == nil) {
|
||||||
return -1;
|
box_set_error_message(error_out, "apple TLS: invalid client");
|
||||||
}
|
return -1;
|
||||||
if (buffer_len == 0) {
|
}
|
||||||
return 0;
|
if (buffer_len == 0) {
|
||||||
}
|
return 0;
|
||||||
|
|
||||||
void *content_copy = malloc(buffer_len);
|
|
||||||
dispatch_queue_t queue = box_apple_tls_client_queue(client);
|
|
||||||
if (content_copy == NULL) {
|
|
||||||
free(content_copy);
|
|
||||||
box_set_error_message(error_out, "apple TLS: out of memory");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (queue == nil) {
|
|
||||||
free(content_copy);
|
|
||||||
box_set_error_message(error_out, "apple TLS: invalid client");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
memcpy(content_copy, buffer, buffer_len);
|
|
||||||
dispatch_data_t content = dispatch_data_create(content_copy, buffer_len, queue, ^{
|
|
||||||
free(content_copy);
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch_semaphore_t write_semaphore = dispatch_semaphore_create(0);
|
|
||||||
__block char *local_error = NULL;
|
|
||||||
|
|
||||||
nw_connection_send(connection, content, NW_CONNECTION_DEFAULT_STREAM_CONTEXT, false, ^(nw_error_t error) {
|
|
||||||
if (error != NULL) {
|
|
||||||
box_set_error_from_nw_error(&local_error, error);
|
|
||||||
}
|
}
|
||||||
dispatch_semaphore_signal(write_semaphore);
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch_time_t wait_deadline = DISPATCH_TIME_FOREVER;
|
void *content_copy = malloc(buffer_len);
|
||||||
if (timeout_msec >= 0) {
|
if (content_copy == NULL) {
|
||||||
wait_deadline = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC);
|
box_set_error_message(error_out, "apple TLS: out of memory");
|
||||||
}
|
return -1;
|
||||||
long wait_result = dispatch_semaphore_wait(write_semaphore, wait_deadline);
|
}
|
||||||
if (wait_result != 0) {
|
dispatch_queue_t queue = box_apple_tls_client_queue(client);
|
||||||
nw_connection_cancel(connection);
|
if (queue == nil) {
|
||||||
dispatch_semaphore_wait(write_semaphore, DISPATCH_TIME_FOREVER);
|
free(content_copy);
|
||||||
|
box_set_error_message(error_out, "apple TLS: invalid client");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(content_copy, buffer, buffer_len);
|
||||||
|
dispatch_data_t content = dispatch_data_create(content_copy, buffer_len, queue, ^{
|
||||||
|
free(content_copy);
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch_semaphore_t write_semaphore = dispatch_semaphore_create(0);
|
||||||
|
__block char *local_error = NULL;
|
||||||
|
|
||||||
|
nw_connection_send(connection, content, NW_CONNECTION_DEFAULT_STREAM_CONTEXT, false, ^(nw_error_t error) {
|
||||||
|
@autoreleasepool {
|
||||||
|
if (error != NULL) {
|
||||||
|
box_set_error_from_nw_error(&local_error, error);
|
||||||
|
}
|
||||||
|
dispatch_semaphore_signal(write_semaphore);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch_time_t wait_deadline = DISPATCH_TIME_FOREVER;
|
||||||
|
if (timeout_msec >= 0) {
|
||||||
|
wait_deadline = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC);
|
||||||
|
}
|
||||||
|
long wait_result = dispatch_semaphore_wait(write_semaphore, wait_deadline);
|
||||||
|
if (wait_result != 0) {
|
||||||
|
nw_connection_cancel(connection);
|
||||||
|
dispatch_semaphore_wait(write_semaphore, DISPATCH_TIME_FOREVER);
|
||||||
|
if (local_error != NULL) {
|
||||||
|
free(local_error);
|
||||||
|
local_error = NULL;
|
||||||
|
}
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
if (local_error != NULL) {
|
if (local_error != NULL) {
|
||||||
free(local_error);
|
if (error_out != NULL) {
|
||||||
local_error = NULL;
|
*error_out = local_error;
|
||||||
|
} else {
|
||||||
|
free(local_error);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
return -2;
|
return (ssize_t)buffer_len;
|
||||||
}
|
}
|
||||||
if (local_error != NULL) {
|
}
|
||||||
if (error_out != NULL) {
|
|
||||||
*error_out = local_error;
|
bool box_apple_tls_client_read_async(box_apple_tls_client_t *client, size_t maximum_len, uintptr_t callback_handle, char **error_out) {
|
||||||
} else {
|
@autoreleasepool {
|
||||||
free(local_error);
|
nw_connection_t connection = box_apple_tls_connection(client);
|
||||||
|
if (connection == nil) {
|
||||||
|
box_set_error_message(error_out, "apple TLS: invalid client");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return -1;
|
if (maximum_len == 0) {
|
||||||
|
box_set_error_message(error_out, "apple TLS: empty read buffer");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint32_t receive_len = maximum_len > UINT32_MAX ? UINT32_MAX : (uint32_t)maximum_len;
|
||||||
|
nw_connection_receive(connection, 1, receive_len, ^(dispatch_data_t content, nw_content_context_t context, bool is_complete, nw_error_t error) {
|
||||||
|
@autoreleasepool {
|
||||||
|
box_apple_tls_read_result_t *result = calloc(1, sizeof(box_apple_tls_read_result_t));
|
||||||
|
if (result == NULL) {
|
||||||
|
box_apple_tls_read_callback(callback_handle, NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t content_size = content != NULL ? dispatch_data_get_size(content) : 0;
|
||||||
|
if (content_size > 0) {
|
||||||
|
result->content = (__bridge_retained void *)content;
|
||||||
|
}
|
||||||
|
if (error != NULL && content_size == 0) {
|
||||||
|
box_set_error_from_nw_error(&result->error, error);
|
||||||
|
}
|
||||||
|
if (is_complete && (context == NULL || nw_content_context_get_is_final(context))) {
|
||||||
|
result->eof = true;
|
||||||
|
}
|
||||||
|
box_apple_tls_read_callback(callback_handle, result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return (ssize_t)buffer_len;
|
}
|
||||||
|
|
||||||
|
ssize_t box_apple_tls_read_result_copy(box_apple_tls_read_result_t *result, void *buffer, size_t buffer_len, bool *eof_out, char **error_out) {
|
||||||
|
@autoreleasepool {
|
||||||
|
if (result == NULL) {
|
||||||
|
box_set_error_message(error_out, "apple TLS: read result unavailable");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (result->error != NULL) {
|
||||||
|
if (error_out != NULL) {
|
||||||
|
*error_out = result->error;
|
||||||
|
result->error = NULL;
|
||||||
|
} else {
|
||||||
|
free(result->error);
|
||||||
|
result->error = NULL;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (eof_out != NULL) {
|
||||||
|
*eof_out = result->eof;
|
||||||
|
}
|
||||||
|
dispatch_data_t content = box_apple_tls_read_result_content(result);
|
||||||
|
if (content == nil) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return box_apple_tls_dispatch_data_copy(content, buffer, buffer_len, error_out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void box_apple_tls_read_result_free(box_apple_tls_read_result_t *result) {
|
||||||
|
if (result == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
free(result->error);
|
||||||
|
if (result->content != NULL) {
|
||||||
|
CFBridgingRelease(result->content);
|
||||||
|
}
|
||||||
|
free(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool box_apple_tls_client_copy_state(box_apple_tls_client_t *client, box_apple_tls_state_t *state, char **error_out) {
|
bool box_apple_tls_client_copy_state(box_apple_tls_client_t *client, box_apple_tls_state_t *state, char **error_out) {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
//go:build darwin && cgo
|
||||||
|
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAppleTLSDispatchDataCopySegments(t *testing.T) {
|
||||||
|
first := []byte("hello ")
|
||||||
|
second := []byte("world")
|
||||||
|
|
||||||
|
buffer := make([]byte, len(first)+len(second))
|
||||||
|
n, errorMessage := appleTLSCopyDispatchDataForTest(first, second, buffer)
|
||||||
|
if n < 0 {
|
||||||
|
t.Fatalf("copy failed: %s", errorMessage)
|
||||||
|
}
|
||||||
|
if int(n) != len(buffer) {
|
||||||
|
t.Fatalf("copied %d bytes, want %d", n, len(buffer))
|
||||||
|
}
|
||||||
|
if !bytes.Equal(buffer, []byte("hello world")) {
|
||||||
|
t.Fatalf("unexpected copy result: %q", string(buffer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppleTLSDispatchDataCopyRejectsSmallBuffer(t *testing.T) {
|
||||||
|
first := []byte("hello")
|
||||||
|
second := []byte("world")
|
||||||
|
|
||||||
|
buffer := make([]byte, len(first)+len(second)-1)
|
||||||
|
n, errorMessage := appleTLSCopyDispatchDataForTest(first, second, buffer)
|
||||||
|
if n != -1 {
|
||||||
|
t.Fatalf("copied %d bytes, want error", n)
|
||||||
|
}
|
||||||
|
if !strings.Contains(errorMessage, "read buffer too small") {
|
||||||
|
t.Fatalf("unexpected error: %q", errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppleTLSDispatchDataCopyEmpty(t *testing.T) {
|
||||||
|
n, errorMessage := appleTLSCopyDispatchDataForTest(nil, nil, nil)
|
||||||
|
if n != 0 {
|
||||||
|
t.Fatalf("copied %d bytes, want 0", n)
|
||||||
|
}
|
||||||
|
if errorMessage != "" {
|
||||||
|
t.Fatalf("unexpected error: %q", errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
//go:build darwin && cgo
|
||||||
|
|
||||||
|
package tls
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include "apple_client_platform_darwin.h"
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
func appleTLSCopyDispatchDataForTest(first, second []byte, buffer []byte) (int, string) {
|
||||||
|
var firstPtr unsafe.Pointer
|
||||||
|
if len(first) > 0 {
|
||||||
|
firstPtr = C.CBytes(first)
|
||||||
|
defer C.free(firstPtr)
|
||||||
|
}
|
||||||
|
var secondPtr unsafe.Pointer
|
||||||
|
if len(second) > 0 {
|
||||||
|
secondPtr = C.CBytes(second)
|
||||||
|
defer C.free(secondPtr)
|
||||||
|
}
|
||||||
|
var bufferPtr unsafe.Pointer
|
||||||
|
if len(buffer) > 0 {
|
||||||
|
bufferPtr = unsafe.Pointer(&buffer[0])
|
||||||
|
}
|
||||||
|
var errPtr *C.char
|
||||||
|
n := C.box_apple_tls_copy_dispatch_data_for_test(
|
||||||
|
firstPtr,
|
||||||
|
C.size_t(len(first)),
|
||||||
|
secondPtr,
|
||||||
|
C.size_t(len(second)),
|
||||||
|
bufferPtr,
|
||||||
|
C.size_t(len(buffer)),
|
||||||
|
&errPtr,
|
||||||
|
)
|
||||||
|
if errPtr == nil {
|
||||||
|
return int(n), ""
|
||||||
|
}
|
||||||
|
errorMessage := C.GoString(errPtr)
|
||||||
|
C.free(unsafe.Pointer(errPtr))
|
||||||
|
return int(n), errorMessage
|
||||||
|
}
|
||||||
@@ -3,17 +3,21 @@
|
|||||||
package tls
|
package tls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
stdtls "crypto/tls"
|
stdtls "crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
"github.com/sagernet/sing/common/json/badoption"
|
"github.com/sagernet/sing/common/json/badoption"
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
const appleTLSTestTimeout = 5 * time.Second
|
const appleTLSTestTimeout = 5 * time.Second
|
||||||
@@ -28,6 +32,11 @@ type appleTLSServerResult struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ N.ExtendedConn = (*appleTLSConn)(nil)
|
||||||
|
_ N.ReadWaitCreator = (*appleTLSConn)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
func TestAppleClientHandshakeAppliesALPNAndVersion(t *testing.T) {
|
func TestAppleClientHandshakeAppliesALPNAndVersion(t *testing.T) {
|
||||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
||||||
for index := 0; index < appleTLSSuccessHandshakeLoops; index++ {
|
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) {
|
func TestAppleClientHandshakeRejectsVersionMismatch(t *testing.T) {
|
||||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
||||||
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
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) {
|
func TestAppleClientReadDeadline(t *testing.T) {
|
||||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
||||||
serverDone, serverAddress := startAppleTLSSilentServer(t, &stdtls.Config{
|
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()
|
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()
|
t.Helper()
|
||||||
|
|
||||||
privateKeyPEM, certificatePEM, err := GenerateCertificate(nil, nil, time.Now, serverName, time.Now().Add(time.Hour))
|
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()
|
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()
|
t.Helper()
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), appleTLSTestTimeout)
|
|
||||||
t.Cleanup(cancel)
|
|
||||||
|
|
||||||
clientConfig, err := NewClientWithOptions(ClientOptions{
|
clientConfig, err := NewClientWithOptions(ClientOptions{
|
||||||
Context: ctx,
|
Context: context.Background(),
|
||||||
Logger: logger.NOP(),
|
Logger: logger.NOP(),
|
||||||
ServerAddress: "",
|
ServerAddress: "",
|
||||||
Options: options,
|
Options: options,
|
||||||
@@ -438,6 +743,14 @@ func newAppleTestClientConn(t *testing.T, serverAddress string, options option.O
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
conn, err := net.DialTimeout("tcp", serverAddress, appleTLSTestTimeout)
|
||||||
if err != nil {
|
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")
|
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 {
|
switch options.Options.Engine {
|
||||||
case C.TLSEngineDefault, "go":
|
case "", C.TLSEngineGo:
|
||||||
case C.TLSEngineApple:
|
case C.TLSEngineApple:
|
||||||
return newAppleClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
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:
|
default:
|
||||||
return nil, E.New("unknown tls engine: ", options.Options.Engine)
|
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 == "" {
|
if serverName == "" {
|
||||||
return errMissingServerName
|
return errMissingServerName
|
||||||
}
|
}
|
||||||
verifyOptions := x509.VerifyOptions{
|
return verifySystemTLSPeer(rootCAs, serverName, timeFunc, state.PeerCertificates)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newWindowsClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||||
|
return nil, E.New("Windows TLS engine is not available on non-Windows platforms")
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -4,5 +4,7 @@ const ACMETLS1Protocol = "acme-tls/1"
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
TLSEngineDefault = ""
|
TLSEngineDefault = ""
|
||||||
|
TLSEngineGo = "go"
|
||||||
TLSEngineApple = "apple"
|
TLSEngineApple = "apple"
|
||||||
|
TLSEngineWindows = "windows"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ icon: material/new-box
|
|||||||
:material-plus: [handshake_timeout](#handshake_timeout)
|
:material-plus: [handshake_timeout](#handshake_timeout)
|
||||||
:material-plus: [spoof](#spoof)
|
:material-plus: [spoof](#spoof)
|
||||||
:material-plus: [spoof_method](#spoof_method)
|
:material-plus: [spoof_method](#spoof_method)
|
||||||
|
:material-plus: [engine](#engine)
|
||||||
:material-delete-clock: [acme](#acme-fields)
|
:material-delete-clock: [acme](#acme-fields)
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.13.0"
|
!!! quote "Changes in sing-box 1.13.0"
|
||||||
@@ -195,6 +196,8 @@ Enable TLS.
|
|||||||
|
|
||||||
#### engine
|
#### engine
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.14.0"
|
||||||
|
|
||||||
==Client only==
|
==Client only==
|
||||||
|
|
||||||
TLS engine to use.
|
TLS engine to use.
|
||||||
@@ -203,15 +206,40 @@ Values:
|
|||||||
|
|
||||||
* `go` (default)
|
* `go` (default)
|
||||||
* `apple`
|
* `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,
|
Unsupported fields:
|
||||||
do not use in hot paths on iOS and tvOS.
|
|
||||||
If you want to circumvent TLS fingerprint-based proxy censorship,
|
* `disable_sni`
|
||||||
use [NaiveProxy](/configuration/outbound/naive/) instead.
|
* `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:
|
Supported fields:
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ icon: material/new-box
|
|||||||
:material-plus: [handshake_timeout](#handshake_timeout)
|
:material-plus: [handshake_timeout](#handshake_timeout)
|
||||||
:material-plus: [spoof](#spoof)
|
:material-plus: [spoof](#spoof)
|
||||||
:material-plus: [spoof_method](#spoof_method)
|
:material-plus: [spoof_method](#spoof_method)
|
||||||
|
:material-plus: [engine](#engine)
|
||||||
:material-delete-clock: [acme](#acme-字段)
|
:material-delete-clock: [acme](#acme-字段)
|
||||||
|
|
||||||
!!! quote "sing-box 1.13.0 中的更改"
|
!!! quote "sing-box 1.13.0 中的更改"
|
||||||
@@ -195,6 +196,8 @@ TLS 版本值:
|
|||||||
|
|
||||||
#### engine
|
#### engine
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.14.0 起"
|
||||||
|
|
||||||
==仅客户端==
|
==仅客户端==
|
||||||
|
|
||||||
要使用的 TLS 引擎。
|
要使用的 TLS 引擎。
|
||||||
@@ -203,14 +206,40 @@ TLS 版本值:
|
|||||||
|
|
||||||
* `go`(默认)
|
* `go`(默认)
|
||||||
* `apple`
|
* `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` 时,这些根证书会替代系统存储。
|
||||||
|
|
||||||
支持的字段:
|
支持的字段:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user