sing-box/common/tls/apple_client_platform.go
2026-06-08 08:51:45 +08:00

713 lines
16 KiB
Go

//go:build darwin && cgo
package tls
/*
#cgo CFLAGS: -x objective-c -fobjc-arc
#cgo LDFLAGS: -framework Foundation -framework Network -framework Security
#include <stdlib.h>
#include "apple_client_platform_darwin.h"
*/
import "C"
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/binary"
"io"
"math"
"net"
"os"
"runtime/cgo"
"strings"
"sync"
"time"
"unsafe"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
"golang.org/x/sys/unix"
)
func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (Conn, error) {
tcpConn, ok := N.UnwrapReader(conn).(*net.TCPConn)
if !ok {
return nil, E.New("apple TLS: requires fd-backed TCP connection")
}
syscallConn, err := tcpConn.SyscallConn()
if err != nil {
return nil, E.Cause(err, "access raw connection")
}
var dupFD int
controlErr := syscallConn.Control(func(fd uintptr) {
dupFD, err = unix.Dup(int(fd))
})
if controlErr != nil {
return nil, E.Cause(controlErr, "access raw connection")
}
if err != nil {
return nil, E.Cause(err, "duplicate raw connection")
}
serverName := c.serverName
serverNamePtr := cStringOrNil(serverName)
defer cFree(serverNamePtr)
alpn := strings.Join(c.nextProtos, "\n")
alpnPtr := cStringOrNil(alpn)
defer cFree(alpnPtr)
anchors, err := c.resolveAnchors()
if err != nil {
return nil, err
}
var anchorsRef unsafe.Pointer
if anchors != nil {
anchorsRef = anchors.Ref()
}
var (
hasVerifyTime bool
verifyTimeUnixMilli int64
)
if c.timeFunc != nil {
hasVerifyTime = true
verifyTimeUnixMilli = c.timeFunc().UnixMilli()
}
var errorPtr *C.char
client := C.box_apple_tls_client_create(
C.int(dupFD),
serverNamePtr,
alpnPtr,
C.size_t(len(alpn)),
C.uint16_t(c.minVersion),
C.uint16_t(c.maxVersion),
C.bool(c.insecure),
anchorsRef,
C.bool(c.anchorOnly),
C.bool(hasVerifyTime),
C.int64_t(verifyTimeUnixMilli),
&errorPtr,
)
if anchors != nil {
anchors.Release()
}
if client == nil {
if errorPtr != nil {
defer C.free(unsafe.Pointer(errorPtr))
return nil, E.New(C.GoString(errorPtr))
}
return nil, E.New("apple TLS: create connection")
}
if err = waitAppleTLSClientReady(ctx, client); err != nil {
C.box_apple_tls_client_cancel(client)
C.box_apple_tls_client_free(client)
return nil, err
}
var state C.box_apple_tls_state_t
stateOK := C.box_apple_tls_client_copy_state(client, &state, &errorPtr)
if !bool(stateOK) {
C.box_apple_tls_client_cancel(client)
C.box_apple_tls_client_free(client)
if errorPtr != nil {
defer C.free(unsafe.Pointer(errorPtr))
return nil, E.New(C.GoString(errorPtr))
}
return nil, E.New("apple TLS: read metadata")
}
defer C.box_apple_tls_state_free(&state)
connectionState, rawCerts, err := parseAppleTLSState(&state)
if err != nil {
C.box_apple_tls_client_cancel(client)
C.box_apple_tls_client_free(client)
return nil, err
}
if len(c.certificatePublicKeySHA256) > 0 {
err = VerifyPublicKeySHA256(c.certificatePublicKeySHA256, rawCerts)
if err != nil {
C.box_apple_tls_client_cancel(client)
C.box_apple_tls_client_free(client)
return nil, err
}
}
return &appleTLSConn{
rawConn: conn,
client: client,
state: connectionState,
closed: make(chan struct{}),
}, nil
}
const (
appleTLSHandshakePollInterval = 100 * time.Millisecond
appleTLSWriteChunkSize = 32 * 1024
)
func waitAppleTLSClientReady(ctx context.Context, client *C.box_apple_tls_client_t) error {
for {
err := ctx.Err()
if err != nil {
C.box_apple_tls_client_cancel(client)
return err
}
waitTimeout := appleTLSHandshakePollInterval
deadline, loaded := ctx.Deadline()
if loaded {
remaining := time.Until(deadline)
if remaining <= 0 {
C.box_apple_tls_client_cancel(client)
err = ctx.Err()
if err != nil {
return err
}
return context.DeadlineExceeded
}
if remaining < waitTimeout {
waitTimeout = remaining
}
}
var errorPtr *C.char
waitResult := C.box_apple_tls_client_wait_ready(client, C.int(timeoutFromDuration(waitTimeout)), &errorPtr)
switch waitResult {
case 1:
return nil
case -2:
continue
case 0:
if errorPtr != nil {
defer C.free(unsafe.Pointer(errorPtr))
return E.New(C.GoString(errorPtr))
}
return E.New("apple TLS: handshake failed")
default:
return E.New("apple TLS: invalid handshake state")
}
}
}
type appleTLSConn struct {
rawConn net.Conn
client *C.box_apple_tls_client_t
state tls.ConnectionState
readAccess sync.Mutex
writeAccess sync.Mutex
stateAccess sync.RWMutex
closeOnce sync.Once
ioAccess sync.Mutex
ioGroup sync.WaitGroup
closed chan struct{}
readEOF bool
deadlineAccess sync.Mutex
readDeadline time.Time
writeDeadline time.Time
readTimedOut bool
writeTimedOut bool
}
var (
_ N.ExtendedConn = (*appleTLSConn)(nil)
_ N.ReadWaitCreator = (*appleTLSConn)(nil)
)
func (c *appleTLSConn) Read(p []byte) (int, error) {
c.readAccess.Lock()
defer c.readAccess.Unlock()
if c.readEOF {
return 0, io.EOF
}
if len(p) == 0 {
return 0, nil
}
return c.readIntoLocked(p)
}
func (c *appleTLSConn) ReadBuffer(buffer *buf.Buffer) error {
c.readAccess.Lock()
defer c.readAccess.Unlock()
if buffer.IsFull() {
return io.ErrShortBuffer
}
startLen := buffer.Len()
n, err := c.readIntoLocked(buffer.FreeBytes())
buffer.Truncate(startLen + n)
return err
}
func (c *appleTLSConn) readIntoLocked(p []byte) (int, error) {
if c.readEOF {
return 0, io.EOF
}
if len(p) == 0 {
return 0, nil
}
timeoutMs, err := c.prepareReadTimeout()
if err != nil {
return 0, err
}
client, err := c.acquireClient()
if err != nil {
return 0, err
}
defer c.releaseClient()
var eof C.bool
var errorPtr *C.char
n := C.box_apple_tls_client_read(client, unsafe.Pointer(&p[0]), C.size_t(len(p)), C.int(timeoutMs), &eof, &errorPtr)
switch {
case n == -2:
c.markReadTimedOut()
return 0, os.ErrDeadlineExceeded
case n >= 0:
if bool(eof) {
c.readEOF = true
if n == 0 {
return 0, io.EOF
}
}
return int(n), nil
default:
if errorPtr != nil {
defer C.free(unsafe.Pointer(errorPtr))
if c.isClosed() {
return 0, net.ErrClosed
}
return 0, E.New(C.GoString(errorPtr))
}
return 0, net.ErrClosed
}
}
func (c *appleTLSConn) Write(p []byte) (int, error) {
c.writeAccess.Lock()
defer c.writeAccess.Unlock()
if len(p) == 0 {
return 0, nil
}
client, err := c.acquireClient()
if err != nil {
return 0, err
}
defer c.releaseClient()
deadline, err := c.prepareWriteDeadline()
if err != nil {
return 0, err
}
var written int
for written < len(p) {
timeoutMs, expired := deadlineTimeoutMs(deadline)
if expired {
C.box_apple_tls_client_cancel(client)
c.markWriteTimedOut()
return written, os.ErrDeadlineExceeded
}
chunkSize := min(len(p)-written, appleTLSWriteChunkSize)
chunk := p[written : written+chunkSize]
var errorPtr *C.char
n := C.box_apple_tls_client_write(client, unsafe.Pointer(&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 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 {
var closeErr error
c.closeOnce.Do(func() {
close(c.closed)
C.box_apple_tls_client_cancel(c.client)
closeErr = c.rawConn.Close()
c.ioAccess.Lock()
c.ioGroup.Wait()
C.box_apple_tls_client_free(c.client)
c.client = nil
c.ioAccess.Unlock()
})
return closeErr
}
func (c *appleTLSConn) LocalAddr() net.Addr {
return c.rawConn.LocalAddr()
}
func (c *appleTLSConn) RemoteAddr() net.Addr {
return c.rawConn.RemoteAddr()
}
// SetDeadline installs deadlines for subsequent Read and Write calls.
//
// Deadlines only apply to subsequent Read or Write calls; an in-flight call
// does not observe later updates to its deadline. Callers that need to cancel
// an in-flight I/O must Close the connection instead.
//
// Once an active Read or Write trips its deadline, the underlying
// nw_connection is cancelled and the conn is no longer usable — callers must
// Close after a deadline error.
func (c *appleTLSConn) SetDeadline(t time.Time) error {
c.deadlineAccess.Lock()
c.readDeadline = t
c.writeDeadline = t
c.readTimedOut = false
c.writeTimedOut = false
c.deadlineAccess.Unlock()
return nil
}
func (c *appleTLSConn) SetReadDeadline(t time.Time) error {
c.deadlineAccess.Lock()
c.readDeadline = t
c.readTimedOut = false
c.deadlineAccess.Unlock()
return nil
}
func (c *appleTLSConn) SetWriteDeadline(t time.Time) error {
c.deadlineAccess.Lock()
c.writeDeadline = t
c.writeTimedOut = false
c.deadlineAccess.Unlock()
return nil
}
func (c *appleTLSConn) prepareReadTimeout() (int, error) {
c.deadlineAccess.Lock()
defer c.deadlineAccess.Unlock()
if c.readTimedOut {
return 0, os.ErrDeadlineExceeded
}
timeoutMs, expired := deadlineTimeoutMs(c.readDeadline)
if expired {
c.readTimedOut = true
return 0, os.ErrDeadlineExceeded
}
return timeoutMs, nil
}
func (c *appleTLSConn) prepareWriteDeadline() (time.Time, error) {
c.deadlineAccess.Lock()
defer c.deadlineAccess.Unlock()
if c.writeTimedOut {
return time.Time{}, os.ErrDeadlineExceeded
}
_, expired := deadlineTimeoutMs(c.writeDeadline)
if expired {
c.writeTimedOut = true
return time.Time{}, os.ErrDeadlineExceeded
}
return c.writeDeadline, nil
}
func (c *appleTLSConn) markReadTimedOut() {
c.deadlineAccess.Lock()
c.readTimedOut = true
c.deadlineAccess.Unlock()
}
func (c *appleTLSConn) markWriteTimedOut() {
c.deadlineAccess.Lock()
c.writeTimedOut = true
c.deadlineAccess.Unlock()
}
func deadlineTimeoutMs(deadline time.Time) (int, bool) {
if deadline.IsZero() {
return -1, false
}
remaining := time.Until(deadline)
if remaining <= 0 {
return 0, true
}
return timeoutFromDuration(remaining), false
}
func (c *appleTLSConn) isClosed() bool {
select {
case <-c.closed:
return true
default:
return false
}
}
func (c *appleTLSConn) acquireClient() (*C.box_apple_tls_client_t, error) {
c.ioAccess.Lock()
defer c.ioAccess.Unlock()
if c.isClosed() {
return nil, net.ErrClosed
}
client := c.client
if client == nil {
return nil, net.ErrClosed
}
c.ioGroup.Add(1)
return client, nil
}
func (c *appleTLSConn) releaseClient() {
c.ioGroup.Done()
}
func (c *appleTLSConn) errorFromPointer(errorPtr *C.char) error {
if errorPtr != nil {
defer C.free(unsafe.Pointer(errorPtr))
if c.isClosed() {
return net.ErrClosed
}
return E.New(C.GoString(errorPtr))
}
return net.ErrClosed
}
type appleTLSReadWaiter struct {
conn *appleTLSConn
options N.ReadWaitOptions
results chan *C.box_apple_tls_read_result_t
}
var _ N.ReadWaiter = (*appleTLSReadWaiter)(nil)
func (w *appleTLSReadWaiter) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) {
w.options = options
if w.results == nil {
w.results = make(chan *C.box_apple_tls_read_result_t, 1)
}
return false
}
func (w *appleTLSReadWaiter) WaitReadBuffer() (*buf.Buffer, error) {
c := w.conn
c.readAccess.Lock()
defer c.readAccess.Unlock()
if c.readEOF {
return nil, io.EOF
}
maximumLen := readWaitFreeLen(w.options)
if maximumLen <= 0 {
return nil, io.ErrShortBuffer
}
timeoutMs, err := c.prepareReadTimeout()
if err != nil {
return nil, err
}
client, err := c.acquireClient()
if err != nil {
return nil, err
}
defer c.releaseClient()
handle := cgo.NewHandle(w)
defer handle.Delete()
var errorPtr *C.char
if !bool(C.box_apple_tls_client_read_async(client, C.size_t(maximumLen), C.uintptr_t(handle), &errorPtr)) {
return nil, c.errorFromPointer(errorPtr)
}
var result *C.box_apple_tls_read_result_t
if timeoutMs >= 0 {
timer := time.NewTimer(time.Duration(timeoutMs) * time.Millisecond)
defer timer.Stop()
select {
case result = <-w.results:
case <-timer.C:
C.box_apple_tls_client_cancel(client)
result = <-w.results
if result != nil {
C.box_apple_tls_read_result_free(result)
}
c.markReadTimedOut()
return nil, os.ErrDeadlineExceeded
}
} else {
result = <-w.results
}
return c.readWaitResultToBuffer(result, w.options)
}
func (c *appleTLSConn) readWaitResultToBuffer(result *C.box_apple_tls_read_result_t, options N.ReadWaitOptions) (*buf.Buffer, error) {
defer C.box_apple_tls_read_result_free(result)
buffer := options.NewBuffer()
if buffer.IsFull() {
buffer.Release()
return nil, io.ErrShortBuffer
}
startLen := buffer.Len()
var eof C.bool
var errorPtr *C.char
n := C.box_apple_tls_read_result_copy(result, unsafe.Pointer(&buffer.FreeBytes()[0]), C.size_t(buffer.FreeLen()), &eof, &errorPtr)
if n < 0 {
buffer.Release()
return nil, c.errorFromPointer(errorPtr)
}
if bool(eof) {
c.readEOF = true
if n == 0 {
buffer.Release()
return nil, io.EOF
}
}
if n == 0 {
buffer.Release()
return nil, io.ErrNoProgress
}
buffer.Truncate(startLen + int(n))
options.PostReturn(buffer)
return buffer, nil
}
func readWaitFreeLen(options N.ReadWaitOptions) int {
if options.IncreaseBuffer {
return 65535 - options.FrontHeadroom - options.RearHeadroom
}
if options.MTU > 0 {
return options.MTU
}
return buf.BufferSize - options.FrontHeadroom - options.RearHeadroom
}
//export box_apple_tls_read_callback
func box_apple_tls_read_callback(callbackHandle C.uintptr_t, result *C.box_apple_tls_read_result_t) {
handle := cgo.Handle(callbackHandle)
waiter, ok := handle.Value().(*appleTLSReadWaiter)
if !ok {
C.box_apple_tls_read_result_free(result)
return
}
select {
case waiter.results <- result:
default:
C.box_apple_tls_read_result_free(result)
}
}
func (c *appleTLSConn) NetConn() net.Conn {
return c.rawConn
}
func (c *appleTLSConn) HandshakeContext(ctx context.Context) error {
return nil
}
func (c *appleTLSConn) ConnectionState() ConnectionState {
c.stateAccess.RLock()
defer c.stateAccess.RUnlock()
return c.state
}
func parseAppleTLSState(state *C.box_apple_tls_state_t) (tls.ConnectionState, [][]byte, error) {
rawCerts, peerCertificates, err := parseAppleCertChain(state.peer_cert_chain, state.peer_cert_chain_len)
if err != nil {
return tls.ConnectionState{}, nil, err
}
var negotiatedProtocol string
if state.alpn != nil {
negotiatedProtocol = C.GoString(state.alpn)
}
var serverName string
if state.server_name != nil {
serverName = C.GoString(state.server_name)
}
return tls.ConnectionState{
Version: uint16(state.version),
HandshakeComplete: true,
CipherSuite: uint16(state.cipher_suite),
NegotiatedProtocol: negotiatedProtocol,
ServerName: serverName,
PeerCertificates: peerCertificates,
}, rawCerts, nil
}
func parseAppleCertChain(chain *C.uint8_t, chainLen C.size_t) ([][]byte, []*x509.Certificate, error) {
if chain == nil || chainLen == 0 {
return nil, nil, nil
}
chainBytes := C.GoBytes(unsafe.Pointer(chain), C.int(chainLen))
var (
rawCerts [][]byte
peerCertificates []*x509.Certificate
)
for len(chainBytes) >= 4 {
certificateLen := binary.BigEndian.Uint32(chainBytes[:4])
chainBytes = chainBytes[4:]
if len(chainBytes) < int(certificateLen) {
return nil, nil, E.New("apple TLS: invalid certificate chain")
}
certificateData := append([]byte(nil), chainBytes[:certificateLen]...)
certificate, err := x509.ParseCertificate(certificateData)
if err != nil {
return nil, nil, E.Cause(err, "parse peer certificate")
}
rawCerts = append(rawCerts, certificateData)
peerCertificates = append(peerCertificates, certificate)
chainBytes = chainBytes[certificateLen:]
}
if len(chainBytes) != 0 {
return nil, nil, E.New("apple TLS: invalid certificate chain")
}
return rawCerts, peerCertificates, nil
}
func timeoutFromDuration(timeout time.Duration) int {
if timeout <= 0 {
return 0
}
timeoutMilliseconds := int64(timeout / time.Millisecond)
if timeout%time.Millisecond != 0 {
timeoutMilliseconds++
}
if timeoutMilliseconds > math.MaxInt32 {
return math.MaxInt32
}
return int(timeoutMilliseconds)
}
func cStringOrNil(value string) *C.char {
if value == "" {
return nil
}
return C.CString(value)
}
func cFree(pointer *C.char) {
if pointer != nil {
C.free(unsafe.Pointer(pointer))
}
}