mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-14 04:38:28 +10:00
Add NSURLSession http client engine
This commit is contained in:
@@ -1,13 +1,22 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
type HTTPClientManager interface {
|
||||
ResolveTransport(logger logger.ContextLogger, options option.HTTPClientOptions) (http.RoundTripper, error)
|
||||
DefaultTransport() http.RoundTripper
|
||||
type HTTPTransport interface {
|
||||
http.RoundTripper
|
||||
CloseIdleConnections()
|
||||
Clone() HTTPTransport
|
||||
Close() error
|
||||
}
|
||||
|
||||
type HTTPClientManager interface {
|
||||
ResolveTransport(ctx context.Context, logger logger.ContextLogger, options option.HTTPClientOptions) (HTTPTransport, error)
|
||||
DefaultTransport() HTTPTransport
|
||||
ResetNetwork()
|
||||
}
|
||||
|
||||
11
box.go
11
box.go
@@ -173,9 +173,6 @@ func New(options Options) (*Box, error) {
|
||||
|
||||
var internalServices []adapter.LifecycleService
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
httpClientManager := httpclient.NewManager(ctx, logFactory.NewLogger("httpclient"), options.HTTPClients, routeOptions.DefaultHTTPClient)
|
||||
service.MustRegister[adapter.HTTPClientManager](ctx, httpClientManager)
|
||||
httpClientService := adapter.LifecycleService(httpClientManager)
|
||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||
len(certificateOptions.Certificate) > 0 ||
|
||||
@@ -214,6 +211,10 @@ func New(options Options) (*Box, error) {
|
||||
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||
// Must register after ConnectionManager: the Apple HTTP engine's proxy bridge reads it from the context when Manager.Start resolves the default client.
|
||||
httpClientManager := httpclient.NewManager(ctx, logFactory.NewLogger("httpclient"), options.HTTPClients, routeOptions.DefaultHTTPClient)
|
||||
service.MustRegister[adapter.HTTPClientManager](ctx, httpClientManager)
|
||||
httpClientService := adapter.LifecycleService(httpClientManager)
|
||||
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
||||
service.MustRegister[adapter.Router](ctx, router)
|
||||
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
|
||||
@@ -373,11 +374,11 @@ func New(options Options) (*Box, error) {
|
||||
&option.LocalDNSServerOptions{},
|
||||
)
|
||||
})
|
||||
httpClientManager.Initialize(func() (*httpclient.Client, error) {
|
||||
httpClientManager.Initialize(func() (*httpclient.Transport, error) {
|
||||
deprecated.Report(ctx, deprecated.OptionImplicitDefaultHTTPClient)
|
||||
var httpClientOptions option.HTTPClientOptions
|
||||
httpClientOptions.DefaultOutbound = true
|
||||
return httpclient.NewClient(ctx, logFactory.NewLogger("httpclient"), "", httpClientOptions)
|
||||
return httpclient.NewTransport(ctx, logFactory.NewLogger("httpclient"), "", httpClientOptions)
|
||||
})
|
||||
if platformInterface != nil {
|
||||
err = platformInterface.Initialize(networkManager)
|
||||
|
||||
@@ -204,6 +204,9 @@ func buildApple() {
|
||||
"-target", bindTarget,
|
||||
"-libname=box",
|
||||
"-tags-not-macos=with_low_memory",
|
||||
"-iosversion=15.0",
|
||||
"-macosversion=13.0",
|
||||
"-tvosversion=17.0",
|
||||
}
|
||||
//if !withTailscale {
|
||||
// args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
|
||||
|
||||
442
common/httpclient/apple_transport_darwin.go
Normal file
442
common/httpclient/apple_transport_darwin.go
Normal file
@@ -0,0 +1,442 @@
|
||||
//go:build darwin && cgo
|
||||
|
||||
package httpclient
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c -fobjc-arc
|
||||
#cgo LDFLAGS: -framework Foundation -framework Security
|
||||
|
||||
#include <stdlib.h>
|
||||
#include "apple_transport_darwin.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/proxybridge"
|
||||
boxTLS "github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
const applePinnedHashSize = sha256.Size
|
||||
|
||||
func verifyApplePinnedPublicKeySHA256(flatHashes []byte, leafCertificate []byte) error {
|
||||
if len(flatHashes)%applePinnedHashSize != 0 {
|
||||
return E.New("invalid pinned public key list")
|
||||
}
|
||||
knownHashes := make([][]byte, 0, len(flatHashes)/applePinnedHashSize)
|
||||
for offset := 0; offset < len(flatHashes); offset += applePinnedHashSize {
|
||||
knownHashes = append(knownHashes, append([]byte(nil), flatHashes[offset:offset+applePinnedHashSize]...))
|
||||
}
|
||||
return boxTLS.VerifyPublicKeySHA256(knownHashes, [][]byte{leafCertificate})
|
||||
}
|
||||
|
||||
//export box_apple_http_verify_public_key_sha256
|
||||
func box_apple_http_verify_public_key_sha256(knownHashValues *C.uint8_t, knownHashValuesLen C.size_t, leafCert *C.uint8_t, leafCertLen C.size_t) *C.char {
|
||||
flatHashes := C.GoBytes(unsafe.Pointer(knownHashValues), C.int(knownHashValuesLen))
|
||||
leafCertificate := C.GoBytes(unsafe.Pointer(leafCert), C.int(leafCertLen))
|
||||
err := verifyApplePinnedPublicKeySHA256(flatHashes, leafCertificate)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return C.CString(err.Error())
|
||||
}
|
||||
|
||||
type appleSessionConfig struct {
|
||||
serverName string
|
||||
minVersion uint16
|
||||
maxVersion uint16
|
||||
insecure bool
|
||||
anchorPEM string
|
||||
anchorOnly bool
|
||||
pinnedPublicKeySHA256s []byte
|
||||
}
|
||||
|
||||
type appleTransportShared struct {
|
||||
logger logger.ContextLogger
|
||||
bridge *proxybridge.Bridge
|
||||
config appleSessionConfig
|
||||
refs atomic.Int32
|
||||
}
|
||||
|
||||
type appleTransport struct {
|
||||
shared *appleTransportShared
|
||||
access sync.Mutex
|
||||
session *C.box_apple_http_session_t
|
||||
closed bool
|
||||
}
|
||||
|
||||
type errorTransport struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func newAppleTransport(ctx context.Context, logger logger.ContextLogger, rawDialer N.Dialer, options option.HTTPClientOptions) (adapter.HTTPTransport, error) {
|
||||
sessionConfig, err := newAppleSessionConfig(ctx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bridge, err := proxybridge.New(ctx, logger, "apple http proxy", rawDialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shared := &appleTransportShared{
|
||||
logger: logger,
|
||||
bridge: bridge,
|
||||
config: sessionConfig,
|
||||
}
|
||||
shared.refs.Store(1)
|
||||
session, err := shared.newSession()
|
||||
if err != nil {
|
||||
bridge.Close()
|
||||
return nil, err
|
||||
}
|
||||
return &appleTransport{
|
||||
shared: shared,
|
||||
session: session,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newAppleSessionConfig(ctx context.Context, options option.HTTPClientOptions) (appleSessionConfig, error) {
|
||||
version := options.Version
|
||||
if version == 0 {
|
||||
version = 2
|
||||
}
|
||||
switch version {
|
||||
case 2:
|
||||
case 1:
|
||||
return appleSessionConfig{}, E.New("HTTP/1.1 is unsupported in Apple HTTP engine")
|
||||
case 3:
|
||||
return appleSessionConfig{}, E.New("HTTP/3 is unsupported in Apple HTTP engine")
|
||||
default:
|
||||
return appleSessionConfig{}, E.New("unknown HTTP version: ", version)
|
||||
}
|
||||
if options.DisableVersionFallback {
|
||||
return appleSessionConfig{}, E.New("disable_version_fallback is unsupported in Apple HTTP engine")
|
||||
}
|
||||
if options.HTTP2Options != (option.HTTP2Options{}) {
|
||||
return appleSessionConfig{}, E.New("HTTP/2 options are unsupported in Apple HTTP engine")
|
||||
}
|
||||
if options.HTTP3Options != (option.QUICOptions{}) {
|
||||
return appleSessionConfig{}, E.New("QUIC options are unsupported in Apple HTTP engine")
|
||||
}
|
||||
|
||||
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
||||
if tlsOptions.Engine != "" {
|
||||
return appleSessionConfig{}, E.New("tls.engine is unsupported in Apple HTTP engine")
|
||||
}
|
||||
if len(tlsOptions.ALPN) > 0 {
|
||||
return appleSessionConfig{}, E.New("tls.alpn is unsupported in Apple HTTP engine")
|
||||
}
|
||||
validated, err := boxTLS.ValidateAppleTLSOptions(ctx, tlsOptions, "Apple HTTP engine")
|
||||
if err != nil {
|
||||
return appleSessionConfig{}, err
|
||||
}
|
||||
|
||||
config := appleSessionConfig{
|
||||
serverName: tlsOptions.ServerName,
|
||||
minVersion: validated.MinVersion,
|
||||
maxVersion: validated.MaxVersion,
|
||||
insecure: tlsOptions.Insecure || len(tlsOptions.CertificatePublicKeySHA256) > 0,
|
||||
anchorPEM: validated.AnchorPEM,
|
||||
anchorOnly: validated.AnchorOnly,
|
||||
}
|
||||
if len(tlsOptions.CertificatePublicKeySHA256) > 0 {
|
||||
config.pinnedPublicKeySHA256s = make([]byte, 0, len(tlsOptions.CertificatePublicKeySHA256)*applePinnedHashSize)
|
||||
for _, hashValue := range tlsOptions.CertificatePublicKeySHA256 {
|
||||
if len(hashValue) != applePinnedHashSize {
|
||||
return appleSessionConfig{}, E.New("invalid certificate_public_key_sha256 length: ", len(hashValue))
|
||||
}
|
||||
config.pinnedPublicKeySHA256s = append(config.pinnedPublicKeySHA256s, hashValue...)
|
||||
}
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (s *appleTransportShared) retain() {
|
||||
s.refs.Add(1)
|
||||
}
|
||||
|
||||
func (s *appleTransportShared) release() error {
|
||||
if s.refs.Add(-1) == 0 {
|
||||
return s.bridge.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *appleTransportShared) newSession() (*C.box_apple_http_session_t, error) {
|
||||
cProxyHost := C.CString("127.0.0.1")
|
||||
defer C.free(unsafe.Pointer(cProxyHost))
|
||||
cProxyUsername := C.CString(s.bridge.Username())
|
||||
defer C.free(unsafe.Pointer(cProxyUsername))
|
||||
cProxyPassword := C.CString(s.bridge.Password())
|
||||
defer C.free(unsafe.Pointer(cProxyPassword))
|
||||
var cAnchorPEM *C.char
|
||||
if s.config.anchorPEM != "" {
|
||||
cAnchorPEM = C.CString(s.config.anchorPEM)
|
||||
defer C.free(unsafe.Pointer(cAnchorPEM))
|
||||
}
|
||||
var pinnedPointer *C.uint8_t
|
||||
if len(s.config.pinnedPublicKeySHA256s) > 0 {
|
||||
pinnedPointer = (*C.uint8_t)(C.CBytes(s.config.pinnedPublicKeySHA256s))
|
||||
defer C.free(unsafe.Pointer(pinnedPointer))
|
||||
}
|
||||
cConfig := C.box_apple_http_session_config_t{
|
||||
proxy_host: cProxyHost,
|
||||
proxy_port: C.int(s.bridge.Port()),
|
||||
proxy_username: cProxyUsername,
|
||||
proxy_password: cProxyPassword,
|
||||
min_tls_version: C.uint16_t(s.config.minVersion),
|
||||
max_tls_version: C.uint16_t(s.config.maxVersion),
|
||||
insecure: C.bool(s.config.insecure),
|
||||
anchor_pem: cAnchorPEM,
|
||||
anchor_pem_len: C.size_t(len(s.config.anchorPEM)),
|
||||
anchor_only: C.bool(s.config.anchorOnly),
|
||||
pinned_public_key_sha256: pinnedPointer,
|
||||
pinned_public_key_sha256_len: C.size_t(len(s.config.pinnedPublicKeySHA256s)),
|
||||
}
|
||||
var cErr *C.char
|
||||
session := C.box_apple_http_session_create(&cConfig, &cErr)
|
||||
if session != nil {
|
||||
return session, nil
|
||||
}
|
||||
return nil, appleCStringError(cErr, "create Apple HTTP session")
|
||||
}
|
||||
|
||||
func (t *appleTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
if requestRequiresHTTP1(request) {
|
||||
return nil, E.New("HTTP upgrade requests are unsupported in Apple HTTP engine")
|
||||
}
|
||||
if request.URL == nil {
|
||||
return nil, E.New("missing request URL")
|
||||
}
|
||||
switch request.URL.Scheme {
|
||||
case "http", "https":
|
||||
default:
|
||||
return nil, E.New("unsupported URL scheme: ", request.URL.Scheme)
|
||||
}
|
||||
if request.URL.Scheme == "https" && t.shared.config.serverName != "" && !strings.EqualFold(t.shared.config.serverName, request.URL.Hostname()) {
|
||||
return nil, E.New("tls.server_name is unsupported in Apple HTTP engine unless it matches request host")
|
||||
}
|
||||
var body []byte
|
||||
if request.Body != nil && request.Body != http.NoBody {
|
||||
defer request.Body.Close()
|
||||
var err error
|
||||
body, err = io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
headerKeys, headerValues := flattenRequestHeaders(request)
|
||||
cMethod := C.CString(request.Method)
|
||||
defer C.free(unsafe.Pointer(cMethod))
|
||||
cURL := C.CString(request.URL.String())
|
||||
defer C.free(unsafe.Pointer(cURL))
|
||||
cHeaderKeys := make([]*C.char, len(headerKeys))
|
||||
cHeaderValues := make([]*C.char, len(headerValues))
|
||||
defer func() {
|
||||
for _, ptr := range cHeaderKeys {
|
||||
C.free(unsafe.Pointer(ptr))
|
||||
}
|
||||
for _, ptr := range cHeaderValues {
|
||||
C.free(unsafe.Pointer(ptr))
|
||||
}
|
||||
}()
|
||||
for index, value := range headerKeys {
|
||||
cHeaderKeys[index] = C.CString(value)
|
||||
}
|
||||
for index, value := range headerValues {
|
||||
cHeaderValues[index] = C.CString(value)
|
||||
}
|
||||
var headerKeysPointer **C.char
|
||||
var headerValuesPointer **C.char
|
||||
if len(cHeaderKeys) > 0 {
|
||||
pointerArraySize := C.size_t(len(cHeaderKeys)) * C.size_t(unsafe.Sizeof((*C.char)(nil)))
|
||||
headerKeysPointer = (**C.char)(C.malloc(pointerArraySize))
|
||||
defer C.free(unsafe.Pointer(headerKeysPointer))
|
||||
headerValuesPointer = (**C.char)(C.malloc(pointerArraySize))
|
||||
defer C.free(unsafe.Pointer(headerValuesPointer))
|
||||
copy(unsafe.Slice(headerKeysPointer, len(cHeaderKeys)), cHeaderKeys)
|
||||
copy(unsafe.Slice(headerValuesPointer, len(cHeaderValues)), cHeaderValues)
|
||||
}
|
||||
var bodyPointer *C.uint8_t
|
||||
if len(body) > 0 {
|
||||
bodyPointer = (*C.uint8_t)(C.CBytes(body))
|
||||
defer C.free(unsafe.Pointer(bodyPointer))
|
||||
}
|
||||
cRequest := C.box_apple_http_request_t{
|
||||
method: cMethod,
|
||||
url: cURL,
|
||||
header_keys: (**C.char)(headerKeysPointer),
|
||||
header_values: (**C.char)(headerValuesPointer),
|
||||
header_count: C.size_t(len(cHeaderKeys)),
|
||||
body: bodyPointer,
|
||||
body_len: C.size_t(len(body)),
|
||||
}
|
||||
var cErr *C.char
|
||||
var task *C.box_apple_http_task_t
|
||||
t.access.Lock()
|
||||
if t.session == nil {
|
||||
t.access.Unlock()
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
// Keep the session attached until NSURLSession has created the task.
|
||||
task = C.box_apple_http_session_send_async(t.session, &cRequest, &cErr)
|
||||
t.access.Unlock()
|
||||
if task == nil {
|
||||
return nil, appleCStringError(cErr, "create Apple HTTP request")
|
||||
}
|
||||
cancelDone := make(chan struct{})
|
||||
cancelExit := make(chan struct{})
|
||||
go func() {
|
||||
defer close(cancelExit)
|
||||
select {
|
||||
case <-request.Context().Done():
|
||||
C.box_apple_http_task_cancel(task)
|
||||
case <-cancelDone:
|
||||
}
|
||||
}()
|
||||
cResponse := C.box_apple_http_task_wait(task, &cErr)
|
||||
close(cancelDone)
|
||||
<-cancelExit
|
||||
C.box_apple_http_task_close(task)
|
||||
if cResponse == nil {
|
||||
err := appleCStringError(cErr, "Apple HTTP request failed")
|
||||
if request.Context().Err() != nil {
|
||||
return nil, request.Context().Err()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer C.box_apple_http_response_free(cResponse)
|
||||
return parseAppleHTTPResponse(request, cResponse), nil
|
||||
}
|
||||
|
||||
func (t *appleTransport) CloseIdleConnections() {
|
||||
t.access.Lock()
|
||||
if t.closed {
|
||||
t.access.Unlock()
|
||||
return
|
||||
}
|
||||
t.access.Unlock()
|
||||
newSession, err := t.shared.newSession()
|
||||
if err != nil {
|
||||
t.shared.logger.Error(E.Cause(err, "reset Apple HTTP session"))
|
||||
return
|
||||
}
|
||||
t.access.Lock()
|
||||
if t.closed {
|
||||
t.access.Unlock()
|
||||
C.box_apple_http_session_close(newSession)
|
||||
return
|
||||
}
|
||||
oldSession := t.session
|
||||
t.session = newSession
|
||||
t.access.Unlock()
|
||||
C.box_apple_http_session_retire(oldSession)
|
||||
}
|
||||
|
||||
func (t *appleTransport) Clone() adapter.HTTPTransport {
|
||||
t.shared.retain()
|
||||
session, err := t.shared.newSession()
|
||||
if err != nil {
|
||||
_ = t.shared.release()
|
||||
return &errorTransport{err: err}
|
||||
}
|
||||
return &appleTransport{
|
||||
shared: t.shared,
|
||||
session: session,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *appleTransport) Close() error {
|
||||
t.access.Lock()
|
||||
if t.closed {
|
||||
t.access.Unlock()
|
||||
return nil
|
||||
}
|
||||
t.closed = true
|
||||
session := t.session
|
||||
t.session = nil
|
||||
t.access.Unlock()
|
||||
C.box_apple_http_session_close(session)
|
||||
return t.shared.release()
|
||||
}
|
||||
|
||||
func (t *errorTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return nil, t.err
|
||||
}
|
||||
|
||||
func (t *errorTransport) CloseIdleConnections() {
|
||||
}
|
||||
|
||||
func (t *errorTransport) Clone() adapter.HTTPTransport {
|
||||
return &errorTransport{err: t.err}
|
||||
}
|
||||
|
||||
func (t *errorTransport) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func flattenRequestHeaders(request *http.Request) ([]string, []string) {
|
||||
var (
|
||||
keys []string
|
||||
values []string
|
||||
)
|
||||
for key, headerValues := range request.Header {
|
||||
for _, value := range headerValues {
|
||||
keys = append(keys, key)
|
||||
values = append(values, value)
|
||||
}
|
||||
}
|
||||
if request.Host != "" {
|
||||
keys = append(keys, "Host")
|
||||
values = append(values, request.Host)
|
||||
}
|
||||
return keys, values
|
||||
}
|
||||
|
||||
func parseAppleHTTPResponse(request *http.Request, response *C.box_apple_http_response_t) *http.Response {
|
||||
headers := make(http.Header)
|
||||
headerKeys := unsafe.Slice(response.header_keys, int(response.header_count))
|
||||
headerValues := unsafe.Slice(response.header_values, int(response.header_count))
|
||||
for index := range headerKeys {
|
||||
headers.Add(C.GoString(headerKeys[index]), C.GoString(headerValues[index]))
|
||||
}
|
||||
body := bytes.NewReader(C.GoBytes(unsafe.Pointer(response.body), C.int(response.body_len)))
|
||||
// NSURLSession's completion-handler API does not expose the negotiated protocol;
|
||||
// callers that read Response.Proto will see HTTP/1.1 even when the wire was HTTP/2.
|
||||
return &http.Response{
|
||||
StatusCode: int(response.status_code),
|
||||
Status: fmt.Sprintf("%d %s", int(response.status_code), http.StatusText(int(response.status_code))),
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: headers,
|
||||
Body: io.NopCloser(body),
|
||||
ContentLength: int64(body.Len()),
|
||||
Request: request,
|
||||
}
|
||||
}
|
||||
|
||||
func appleCStringError(cErr *C.char, message string) error {
|
||||
if cErr == nil {
|
||||
return E.New(message)
|
||||
}
|
||||
defer C.free(unsafe.Pointer(cErr))
|
||||
return E.New(message, ": ", C.GoString(cErr))
|
||||
}
|
||||
69
common/httpclient/apple_transport_darwin.h
Normal file
69
common/httpclient/apple_transport_darwin.h
Normal file
@@ -0,0 +1,69 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct box_apple_http_session box_apple_http_session_t;
|
||||
typedef struct box_apple_http_task box_apple_http_task_t;
|
||||
|
||||
typedef struct box_apple_http_session_config {
|
||||
const char *proxy_host;
|
||||
int proxy_port;
|
||||
const char *proxy_username;
|
||||
const char *proxy_password;
|
||||
uint16_t min_tls_version;
|
||||
uint16_t max_tls_version;
|
||||
bool insecure;
|
||||
const char *anchor_pem;
|
||||
size_t anchor_pem_len;
|
||||
bool anchor_only;
|
||||
const uint8_t *pinned_public_key_sha256;
|
||||
size_t pinned_public_key_sha256_len;
|
||||
} box_apple_http_session_config_t;
|
||||
|
||||
typedef struct box_apple_http_request {
|
||||
const char *method;
|
||||
const char *url;
|
||||
const char **header_keys;
|
||||
const char **header_values;
|
||||
size_t header_count;
|
||||
const uint8_t *body;
|
||||
size_t body_len;
|
||||
} box_apple_http_request_t;
|
||||
|
||||
typedef struct box_apple_http_response {
|
||||
int status_code;
|
||||
char **header_keys;
|
||||
char **header_values;
|
||||
size_t header_count;
|
||||
uint8_t *body;
|
||||
size_t body_len;
|
||||
char *error;
|
||||
} box_apple_http_response_t;
|
||||
|
||||
box_apple_http_session_t *box_apple_http_session_create(
|
||||
const box_apple_http_session_config_t *config,
|
||||
char **error_out
|
||||
);
|
||||
void box_apple_http_session_retire(box_apple_http_session_t *session);
|
||||
void box_apple_http_session_close(box_apple_http_session_t *session);
|
||||
|
||||
box_apple_http_task_t *box_apple_http_session_send_async(
|
||||
box_apple_http_session_t *session,
|
||||
const box_apple_http_request_t *request,
|
||||
char **error_out
|
||||
);
|
||||
box_apple_http_response_t *box_apple_http_task_wait(
|
||||
box_apple_http_task_t *task,
|
||||
char **error_out
|
||||
);
|
||||
void box_apple_http_task_cancel(box_apple_http_task_t *task);
|
||||
void box_apple_http_task_close(box_apple_http_task_t *task);
|
||||
|
||||
void box_apple_http_response_free(box_apple_http_response_t *response);
|
||||
|
||||
char *box_apple_http_verify_public_key_sha256(
|
||||
uint8_t *known_hash_values,
|
||||
size_t known_hash_values_len,
|
||||
uint8_t *leaf_cert,
|
||||
size_t leaf_cert_len
|
||||
);
|
||||
386
common/httpclient/apple_transport_darwin.m
Normal file
386
common/httpclient/apple_transport_darwin.m
Normal file
@@ -0,0 +1,386 @@
|
||||
#import "apple_transport_darwin.h"
|
||||
|
||||
#import <CoreFoundation/CFStream.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Security/Security.h>
|
||||
#import <dispatch/dispatch.h>
|
||||
#import <stdlib.h>
|
||||
#import <string.h>
|
||||
|
||||
typedef struct box_apple_http_session {
|
||||
void *handle;
|
||||
} box_apple_http_session_t;
|
||||
|
||||
typedef struct box_apple_http_task {
|
||||
void *task;
|
||||
void *done_semaphore;
|
||||
box_apple_http_response_t *response;
|
||||
char *error;
|
||||
} box_apple_http_task_t;
|
||||
|
||||
static void box_set_error_string(char **error_out, NSString *message) {
|
||||
if (error_out == NULL || *error_out != NULL) {
|
||||
return;
|
||||
}
|
||||
const char *utf8 = [message UTF8String];
|
||||
*error_out = strdup(utf8 != NULL ? utf8 : "unknown error");
|
||||
}
|
||||
|
||||
static void box_set_error_from_nserror(char **error_out, NSError *error) {
|
||||
if (error == nil) {
|
||||
box_set_error_string(error_out, @"unknown error");
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
if (trustRef == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (anchors.count > 0 || anchor_only) {
|
||||
CFMutableArrayRef anchorArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
|
||||
for (id certificate in anchors) {
|
||||
CFArrayAppendValue(anchorArray, (__bridge const void *)certificate);
|
||||
}
|
||||
SecTrustSetAnchorCertificates(trustRef, anchorArray);
|
||||
SecTrustSetAnchorCertificatesOnly(trustRef, anchor_only);
|
||||
CFRelease(anchorArray);
|
||||
}
|
||||
CFErrorRef error = NULL;
|
||||
bool result = SecTrustEvaluateWithError(trustRef, &error);
|
||||
if (error != NULL) {
|
||||
CFRelease(error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static box_apple_http_response_t *box_create_response(NSHTTPURLResponse *httpResponse, NSData *data) {
|
||||
box_apple_http_response_t *response = calloc(1, sizeof(box_apple_http_response_t));
|
||||
response->status_code = (int)httpResponse.statusCode;
|
||||
NSDictionary *headers = httpResponse.allHeaderFields;
|
||||
response->header_count = headers.count;
|
||||
if (response->header_count > 0) {
|
||||
response->header_keys = calloc(response->header_count, sizeof(char *));
|
||||
response->header_values = calloc(response->header_count, sizeof(char *));
|
||||
NSUInteger index = 0;
|
||||
for (id key in headers) {
|
||||
NSString *keyString = [[key description] copy];
|
||||
NSString *valueString = [[headers[key] description] copy];
|
||||
response->header_keys[index] = strdup(keyString.UTF8String ?: "");
|
||||
response->header_values[index] = strdup(valueString.UTF8String ?: "");
|
||||
index++;
|
||||
}
|
||||
}
|
||||
if (data.length > 0) {
|
||||
response->body_len = data.length;
|
||||
response->body = malloc(data.length);
|
||||
memcpy(response->body, data.bytes, data.length);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
@interface BoxAppleHTTPSessionDelegate : NSObject <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
|
||||
@property(nonatomic, assign) BOOL insecure;
|
||||
@property(nonatomic, assign) BOOL anchorOnly;
|
||||
@property(nonatomic, strong) NSArray *anchors;
|
||||
@property(nonatomic, strong) NSData *pinnedPublicKeyHashes;
|
||||
@end
|
||||
|
||||
@implementation BoxAppleHTTPSessionDelegate
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
task:(NSURLSessionTask *)task
|
||||
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
|
||||
newRequest:(NSURLRequest *)request
|
||||
completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
|
||||
completionHandler(nil);
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
task:(NSURLSessionTask *)task
|
||||
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
|
||||
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
|
||||
if (![challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
|
||||
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
|
||||
return;
|
||||
}
|
||||
SecTrustRef trustRef = challenge.protectionSpace.serverTrust;
|
||||
if (trustRef == NULL) {
|
||||
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
|
||||
return;
|
||||
}
|
||||
BOOL needsCustomHandling = self.insecure || self.anchorOnly || self.anchors.count > 0 || self.pinnedPublicKeyHashes.length > 0;
|
||||
if (!needsCustomHandling) {
|
||||
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
|
||||
return;
|
||||
}
|
||||
BOOL ok = YES;
|
||||
if (!self.insecure) {
|
||||
if (self.anchorOnly || self.anchors.count > 0) {
|
||||
ok = box_evaluate_trust(trustRef, self.anchors, self.anchorOnly);
|
||||
} else {
|
||||
CFErrorRef error = NULL;
|
||||
ok = SecTrustEvaluateWithError(trustRef, &error);
|
||||
if (error != NULL) {
|
||||
CFRelease(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ok && self.pinnedPublicKeyHashes.length > 0) {
|
||||
CFArrayRef certificateChain = SecTrustCopyCertificateChain(trustRef);
|
||||
SecCertificateRef leafCertificate = NULL;
|
||||
if (certificateChain != NULL && CFArrayGetCount(certificateChain) > 0) {
|
||||
leafCertificate = (SecCertificateRef)CFArrayGetValueAtIndex(certificateChain, 0);
|
||||
}
|
||||
if (leafCertificate == NULL) {
|
||||
ok = NO;
|
||||
} else {
|
||||
NSData *leafData = CFBridgingRelease(SecCertificateCopyData(leafCertificate));
|
||||
char *pinError = box_apple_http_verify_public_key_sha256(
|
||||
(uint8_t *)self.pinnedPublicKeyHashes.bytes,
|
||||
self.pinnedPublicKeyHashes.length,
|
||||
(uint8_t *)leafData.bytes,
|
||||
leafData.length
|
||||
);
|
||||
if (pinError != NULL) {
|
||||
free(pinError);
|
||||
ok = NO;
|
||||
}
|
||||
}
|
||||
if (certificateChain != NULL) {
|
||||
CFRelease(certificateChain);
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
|
||||
return;
|
||||
}
|
||||
completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:trustRef]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface BoxAppleHTTPSessionHandle : NSObject
|
||||
@property(nonatomic, strong) NSURLSession *session;
|
||||
@property(nonatomic, strong) BoxAppleHTTPSessionDelegate *delegate;
|
||||
@end
|
||||
|
||||
@implementation BoxAppleHTTPSessionHandle
|
||||
@end
|
||||
|
||||
box_apple_http_session_t *box_apple_http_session_create(
|
||||
const box_apple_http_session_config_t *config,
|
||||
char **error_out
|
||||
) {
|
||||
@autoreleasepool {
|
||||
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
||||
sessionConfig.URLCache = nil;
|
||||
sessionConfig.HTTPCookieStorage = nil;
|
||||
sessionConfig.URLCredentialStorage = nil;
|
||||
sessionConfig.HTTPShouldSetCookies = NO;
|
||||
if (config != NULL && config->proxy_host != NULL && config->proxy_port > 0) {
|
||||
NSMutableDictionary *proxyDictionary = [NSMutableDictionary dictionary];
|
||||
proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSProxyHost] = [NSString stringWithUTF8String:config->proxy_host];
|
||||
proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSProxyPort] = @(config->proxy_port);
|
||||
proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSVersion] = (__bridge NSString *)kCFStreamSocketSOCKSVersion5;
|
||||
if (config->proxy_username != NULL) {
|
||||
proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSUser] = [NSString stringWithUTF8String:config->proxy_username];
|
||||
}
|
||||
if (config->proxy_password != NULL) {
|
||||
proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSPassword] = [NSString stringWithUTF8String:config->proxy_password];
|
||||
}
|
||||
sessionConfig.connectionProxyDictionary = proxyDictionary;
|
||||
}
|
||||
if (config != NULL && config->min_tls_version != 0) {
|
||||
sessionConfig.TLSMinimumSupportedProtocolVersion = (tls_protocol_version_t)config->min_tls_version;
|
||||
}
|
||||
if (config != NULL && config->max_tls_version != 0) {
|
||||
sessionConfig.TLSMaximumSupportedProtocolVersion = (tls_protocol_version_t)config->max_tls_version;
|
||||
}
|
||||
BoxAppleHTTPSessionDelegate *delegate = [[BoxAppleHTTPSessionDelegate alloc] init];
|
||||
if (config != NULL) {
|
||||
delegate.insecure = config->insecure;
|
||||
delegate.anchorOnly = config->anchor_only;
|
||||
delegate.anchors = box_parse_certificates_from_pem(config->anchor_pem, config->anchor_pem_len);
|
||||
if (config->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];
|
||||
}
|
||||
}
|
||||
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:delegate delegateQueue:nil];
|
||||
if (session == nil) {
|
||||
box_set_error_string(error_out, @"create URLSession");
|
||||
return NULL;
|
||||
}
|
||||
BoxAppleHTTPSessionHandle *handle = [[BoxAppleHTTPSessionHandle alloc] init];
|
||||
handle.session = session;
|
||||
handle.delegate = delegate;
|
||||
box_apple_http_session_t *sessionHandle = calloc(1, sizeof(box_apple_http_session_t));
|
||||
sessionHandle->handle = (__bridge_retained void *)handle;
|
||||
return sessionHandle;
|
||||
}
|
||||
}
|
||||
|
||||
void box_apple_http_session_retire(box_apple_http_session_t *session) {
|
||||
if (session == NULL || session->handle == NULL) {
|
||||
return;
|
||||
}
|
||||
BoxAppleHTTPSessionHandle *handle = (__bridge_transfer BoxAppleHTTPSessionHandle *)session->handle;
|
||||
[handle.session finishTasksAndInvalidate];
|
||||
free(session);
|
||||
}
|
||||
|
||||
void box_apple_http_session_close(box_apple_http_session_t *session) {
|
||||
if (session == NULL || session->handle == NULL) {
|
||||
return;
|
||||
}
|
||||
BoxAppleHTTPSessionHandle *handle = (__bridge_transfer BoxAppleHTTPSessionHandle *)session->handle;
|
||||
[handle.session invalidateAndCancel];
|
||||
free(session);
|
||||
}
|
||||
|
||||
box_apple_http_task_t *box_apple_http_session_send_async(
|
||||
box_apple_http_session_t *session,
|
||||
const box_apple_http_request_t *request,
|
||||
char **error_out
|
||||
) {
|
||||
@autoreleasepool {
|
||||
if (session == NULL || session->handle == NULL || request == NULL || request->method == NULL || request->url == NULL) {
|
||||
box_set_error_string(error_out, @"invalid apple HTTP request");
|
||||
return NULL;
|
||||
}
|
||||
BoxAppleHTTPSessionHandle *handle = (__bridge BoxAppleHTTPSessionHandle *)session->handle;
|
||||
NSURL *requestURL = [NSURL URLWithString:[NSString stringWithUTF8String:request->url]];
|
||||
if (requestURL == nil) {
|
||||
box_set_error_string(error_out, @"invalid request URL");
|
||||
return NULL;
|
||||
}
|
||||
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:requestURL];
|
||||
urlRequest.HTTPMethod = [NSString stringWithUTF8String:request->method];
|
||||
for (size_t index = 0; index < request->header_count; index++) {
|
||||
const char *key = request->header_keys[index];
|
||||
const char *value = request->header_values[index];
|
||||
if (key == NULL || value == NULL) {
|
||||
continue;
|
||||
}
|
||||
[urlRequest addValue:[NSString stringWithUTF8String:value] forHTTPHeaderField:[NSString stringWithUTF8String:key]];
|
||||
}
|
||||
if (request->body != NULL && request->body_len > 0) {
|
||||
urlRequest.HTTPBody = [NSData dataWithBytes:request->body length:request->body_len];
|
||||
}
|
||||
box_apple_http_task_t *task = calloc(1, sizeof(box_apple_http_task_t));
|
||||
dispatch_semaphore_t doneSemaphore = dispatch_semaphore_create(0);
|
||||
task->done_semaphore = (__bridge_retained void *)doneSemaphore;
|
||||
NSURLSessionDataTask *dataTask = [handle.session dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||
if (error != nil) {
|
||||
box_set_error_from_nserror(&task->error, error);
|
||||
} else if (![response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||
box_set_error_string(&task->error, @"unexpected HTTP response type");
|
||||
} else {
|
||||
task->response = box_create_response((NSHTTPURLResponse *)response, data ?: [NSData data]);
|
||||
}
|
||||
dispatch_semaphore_signal((__bridge dispatch_semaphore_t)task->done_semaphore);
|
||||
}];
|
||||
if (dataTask == nil) {
|
||||
box_set_error_string(error_out, @"create data task");
|
||||
box_apple_http_task_close(task);
|
||||
return NULL;
|
||||
}
|
||||
task->task = (__bridge_retained void *)dataTask;
|
||||
[dataTask resume];
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
box_apple_http_response_t *box_apple_http_task_wait(
|
||||
box_apple_http_task_t *task,
|
||||
char **error_out
|
||||
) {
|
||||
if (task == NULL || task->done_semaphore == NULL) {
|
||||
box_set_error_string(error_out, @"invalid apple HTTP task");
|
||||
return NULL;
|
||||
}
|
||||
dispatch_semaphore_wait((__bridge dispatch_semaphore_t)task->done_semaphore, DISPATCH_TIME_FOREVER);
|
||||
if (task->error != NULL) {
|
||||
box_set_error_string(error_out, [NSString stringWithUTF8String:task->error]);
|
||||
return NULL;
|
||||
}
|
||||
return task->response;
|
||||
}
|
||||
|
||||
void box_apple_http_task_cancel(box_apple_http_task_t *task) {
|
||||
if (task == NULL || task->task == NULL) {
|
||||
return;
|
||||
}
|
||||
NSURLSessionTask *nsTask = (__bridge NSURLSessionTask *)task->task;
|
||||
[nsTask cancel];
|
||||
}
|
||||
|
||||
void box_apple_http_task_close(box_apple_http_task_t *task) {
|
||||
if (task == NULL) {
|
||||
return;
|
||||
}
|
||||
if (task->task != NULL) {
|
||||
__unused NSURLSessionTask *nsTask = (__bridge_transfer NSURLSessionTask *)task->task;
|
||||
task->task = NULL;
|
||||
}
|
||||
if (task->done_semaphore != NULL) {
|
||||
__unused dispatch_semaphore_t doneSemaphore = (__bridge_transfer dispatch_semaphore_t)task->done_semaphore;
|
||||
task->done_semaphore = NULL;
|
||||
}
|
||||
free(task->error);
|
||||
free(task);
|
||||
}
|
||||
|
||||
void box_apple_http_response_free(box_apple_http_response_t *response) {
|
||||
if (response == NULL) {
|
||||
return;
|
||||
}
|
||||
for (size_t index = 0; index < response->header_count; index++) {
|
||||
free(response->header_keys[index]);
|
||||
free(response->header_values[index]);
|
||||
}
|
||||
free(response->header_keys);
|
||||
free(response->header_values);
|
||||
free(response->body);
|
||||
free(response->error);
|
||||
free(response);
|
||||
}
|
||||
876
common/httpclient/apple_transport_darwin_test.go
Normal file
876
common/httpclient/apple_transport_darwin_test.go
Normal file
@@ -0,0 +1,876 @@
|
||||
//go:build darwin && cgo
|
||||
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
stdtls "crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxTLS "github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/route"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
const appleHTTPTestTimeout = 5 * time.Second
|
||||
|
||||
const appleHTTPRecoveryLoops = 5
|
||||
|
||||
type appleHTTPTestDialer struct {
|
||||
dialer net.Dialer
|
||||
listener net.ListenConfig
|
||||
hostMap map[string]string
|
||||
}
|
||||
|
||||
type appleHTTPObservedRequest struct {
|
||||
method string
|
||||
body string
|
||||
host string
|
||||
values []string
|
||||
protoMajor int
|
||||
}
|
||||
|
||||
type appleHTTPTestServer struct {
|
||||
server *httptest.Server
|
||||
baseURL string
|
||||
dialHost string
|
||||
certificate stdtls.Certificate
|
||||
certificatePEM string
|
||||
publicKeyHash []byte
|
||||
}
|
||||
|
||||
func TestNewAppleSessionConfig(t *testing.T) {
|
||||
serverCertificate, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost")
|
||||
serverHash := certificatePublicKeySHA256(t, serverCertificate.Certificate[0])
|
||||
otherHash := bytes.Repeat([]byte{0x7f}, applePinnedHashSize)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
options option.HTTPClientOptions
|
||||
check func(t *testing.T, config appleSessionConfig)
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "success with certificate anchors",
|
||||
options: option.HTTPClientOptions{
|
||||
Version: 2,
|
||||
DialerOptions: option.DialerOptions{
|
||||
ConnectTimeout: badoption.Duration(2 * time.Second),
|
||||
},
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "localhost",
|
||||
MinVersion: "1.2",
|
||||
MaxVersion: "1.3",
|
||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
||||
},
|
||||
},
|
||||
},
|
||||
check: func(t *testing.T, config appleSessionConfig) {
|
||||
t.Helper()
|
||||
if config.serverName != "localhost" {
|
||||
t.Fatalf("unexpected server name: %q", config.serverName)
|
||||
}
|
||||
if config.minVersion != stdtls.VersionTLS12 {
|
||||
t.Fatalf("unexpected min version: %x", config.minVersion)
|
||||
}
|
||||
if config.maxVersion != stdtls.VersionTLS13 {
|
||||
t.Fatalf("unexpected max version: %x", config.maxVersion)
|
||||
}
|
||||
if config.insecure {
|
||||
t.Fatal("unexpected insecure flag")
|
||||
}
|
||||
if !config.anchorOnly {
|
||||
t.Fatal("expected anchor_only")
|
||||
}
|
||||
if !strings.Contains(config.anchorPEM, "BEGIN CERTIFICATE") {
|
||||
t.Fatalf("unexpected anchor pem: %q", config.anchorPEM)
|
||||
}
|
||||
if len(config.pinnedPublicKeySHA256s) != 0 {
|
||||
t.Fatalf("unexpected pinned hashes: %d", len(config.pinnedPublicKeySHA256s))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success with flattened pins",
|
||||
options: option.HTTPClientOptions{
|
||||
Version: 2,
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
Insecure: true,
|
||||
CertificatePublicKeySHA256: badoption.Listable[[]byte]{serverHash, otherHash},
|
||||
},
|
||||
},
|
||||
},
|
||||
check: func(t *testing.T, config appleSessionConfig) {
|
||||
t.Helper()
|
||||
if !config.insecure {
|
||||
t.Fatal("expected insecure flag")
|
||||
}
|
||||
if len(config.pinnedPublicKeySHA256s) != 2*applePinnedHashSize {
|
||||
t.Fatalf("unexpected flattened pin length: %d", len(config.pinnedPublicKeySHA256s))
|
||||
}
|
||||
if !bytes.Equal(config.pinnedPublicKeySHA256s[:applePinnedHashSize], serverHash) {
|
||||
t.Fatal("unexpected first pin")
|
||||
}
|
||||
if !bytes.Equal(config.pinnedPublicKeySHA256s[applePinnedHashSize:], otherHash) {
|
||||
t.Fatal("unexpected second pin")
|
||||
}
|
||||
if config.anchorPEM != "" {
|
||||
t.Fatalf("unexpected anchor pem: %q", config.anchorPEM)
|
||||
}
|
||||
if config.anchorOnly {
|
||||
t.Fatal("unexpected anchor_only")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "http11 unsupported",
|
||||
options: option.HTTPClientOptions{Version: 1},
|
||||
wantErr: "HTTP/1.1 is unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "http3 unsupported",
|
||||
options: option.HTTPClientOptions{Version: 3},
|
||||
wantErr: "HTTP/3 is unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "unknown version",
|
||||
options: option.HTTPClientOptions{Version: 9},
|
||||
wantErr: "unknown HTTP version: 9",
|
||||
},
|
||||
{
|
||||
name: "disable version fallback unsupported",
|
||||
options: option.HTTPClientOptions{
|
||||
DisableVersionFallback: true,
|
||||
},
|
||||
wantErr: "disable_version_fallback is unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "http2 options unsupported",
|
||||
options: option.HTTPClientOptions{
|
||||
HTTP2Options: option.HTTP2Options{
|
||||
IdleTimeout: badoption.Duration(time.Second),
|
||||
},
|
||||
},
|
||||
wantErr: "HTTP/2 options are unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "quic options unsupported",
|
||||
options: option.HTTPClientOptions{
|
||||
HTTP3Options: option.QUICOptions{
|
||||
InitialPacketSize: 1200,
|
||||
},
|
||||
},
|
||||
wantErr: "QUIC options are unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "tls engine unsupported",
|
||||
options: option.HTTPClientOptions{
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{Engine: "go"},
|
||||
},
|
||||
},
|
||||
wantErr: "tls.engine is unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "disable sni unsupported",
|
||||
options: option.HTTPClientOptions{
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{DisableSNI: true},
|
||||
},
|
||||
},
|
||||
wantErr: "disable_sni is unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "alpn unsupported",
|
||||
options: option.HTTPClientOptions{
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
ALPN: badoption.Listable[string]{"h2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "tls.alpn is unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "cipher suites unsupported",
|
||||
options: option.HTTPClientOptions{
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
CipherSuites: badoption.Listable[string]{"TLS_AES_128_GCM_SHA256"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "cipher_suites is unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "curve preferences unsupported",
|
||||
options: option.HTTPClientOptions{
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
CurvePreferences: badoption.Listable[option.CurvePreference]{option.CurvePreference(option.X25519)},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "curve_preferences is unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "client certificate unsupported",
|
||||
options: option.HTTPClientOptions{
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
ClientCertificate: badoption.Listable[string]{"client-certificate"},
|
||||
ClientKey: badoption.Listable[string]{"client-key"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "client certificate is unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "tls fragment unsupported",
|
||||
options: option.HTTPClientOptions{
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{Fragment: true},
|
||||
},
|
||||
},
|
||||
wantErr: "tls fragment is unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "ktls unsupported",
|
||||
options: option.HTTPClientOptions{
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{KernelTx: true},
|
||||
},
|
||||
},
|
||||
wantErr: "ktls is unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "ech unsupported",
|
||||
options: option.HTTPClientOptions{
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
ECH: &option.OutboundECHOptions{Enabled: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "ech is unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "utls unsupported",
|
||||
options: option.HTTPClientOptions{
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
UTLS: &option.OutboundUTLSOptions{Enabled: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "utls is unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "reality unsupported",
|
||||
options: option.HTTPClientOptions{
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
Reality: &option.OutboundRealityOptions{Enabled: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "reality is unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "pin and certificate conflict",
|
||||
options: option.HTTPClientOptions{
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
||||
CertificatePublicKeySHA256: badoption.Listable[[]byte]{serverHash},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "certificate_public_key_sha256 is conflict with certificate or certificate_path",
|
||||
},
|
||||
{
|
||||
name: "invalid min version",
|
||||
options: option.HTTPClientOptions{
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{MinVersion: "bogus"},
|
||||
},
|
||||
},
|
||||
wantErr: "parse min_version",
|
||||
},
|
||||
{
|
||||
name: "invalid max version",
|
||||
options: option.HTTPClientOptions{
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{MaxVersion: "bogus"},
|
||||
},
|
||||
},
|
||||
wantErr: "parse max_version",
|
||||
},
|
||||
{
|
||||
name: "invalid pin length",
|
||||
options: option.HTTPClientOptions{
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
CertificatePublicKeySHA256: badoption.Listable[[]byte]{{0x01, 0x02}},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "invalid certificate_public_key_sha256 length: 2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
config, err := newAppleSessionConfig(context.Background(), testCase.options)
|
||||
if testCase.wantErr != "" {
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), testCase.wantErr) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if testCase.check != nil {
|
||||
testCase.check(t, config)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppleTransportVerifyPublicKeySHA256(t *testing.T) {
|
||||
serverCertificate, _ := newAppleHTTPTestCertificate(t, "localhost")
|
||||
goodHash := certificatePublicKeySHA256(t, serverCertificate.Certificate[0])
|
||||
badHash := append([]byte(nil), goodHash...)
|
||||
badHash[0] ^= 0xff
|
||||
|
||||
err := verifyApplePinnedPublicKeySHA256(goodHash, serverCertificate.Certificate[0])
|
||||
if err != nil {
|
||||
t.Fatalf("expected correct pin to succeed: %v", err)
|
||||
}
|
||||
|
||||
err = verifyApplePinnedPublicKeySHA256(badHash, serverCertificate.Certificate[0])
|
||||
if err == nil {
|
||||
t.Fatal("expected incorrect pin to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unrecognized remote public key") {
|
||||
t.Fatalf("unexpected pin mismatch error: %v", err)
|
||||
}
|
||||
|
||||
err = verifyApplePinnedPublicKeySHA256(goodHash[:applePinnedHashSize-1], serverCertificate.Certificate[0])
|
||||
if err == nil {
|
||||
t.Fatal("expected malformed pin list to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "invalid pinned public key list") {
|
||||
t.Fatalf("unexpected malformed pin error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppleTransportRoundTripHTTPS(t *testing.T) {
|
||||
requests := make(chan appleHTTPObservedRequest, 1)
|
||||
server := startAppleHTTPTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
requests <- appleHTTPObservedRequest{
|
||||
method: r.Method,
|
||||
body: string(body),
|
||||
host: r.Host,
|
||||
values: append([]string(nil), r.Header.Values("X-Test")...),
|
||||
protoMajor: r.ProtoMajor,
|
||||
}
|
||||
w.Header().Set("X-Reply", "apple")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_, _ = w.Write([]byte("response body"))
|
||||
})
|
||||
|
||||
transport := newAppleHTTPTestTransport(t, server, option.HTTPClientOptions{
|
||||
Version: 2,
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: appleHTTPServerTLSOptions(server),
|
||||
},
|
||||
})
|
||||
|
||||
request, err := http.NewRequest(http.MethodPost, server.URL("/roundtrip"), bytes.NewReader([]byte("request body")))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request.Header.Add("X-Test", "one")
|
||||
request.Header.Add("X-Test", "two")
|
||||
request.Host = "custom.example"
|
||||
|
||||
response, err := transport.RoundTrip(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
responseBody := readResponseBody(t, response)
|
||||
if response.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("unexpected status code: %d", response.StatusCode)
|
||||
}
|
||||
if response.Status != "201 Created" {
|
||||
t.Fatalf("unexpected status: %q", response.Status)
|
||||
}
|
||||
if response.Header.Get("X-Reply") != "apple" {
|
||||
t.Fatalf("unexpected response header: %q", response.Header.Get("X-Reply"))
|
||||
}
|
||||
if responseBody != "response body" {
|
||||
t.Fatalf("unexpected response body: %q", responseBody)
|
||||
}
|
||||
if response.ContentLength != int64(len(responseBody)) {
|
||||
t.Fatalf("unexpected content length: %d", response.ContentLength)
|
||||
}
|
||||
|
||||
observed := waitObservedRequest(t, requests)
|
||||
if observed.method != http.MethodPost {
|
||||
t.Fatalf("unexpected method: %q", observed.method)
|
||||
}
|
||||
if observed.body != "request body" {
|
||||
t.Fatalf("unexpected request body: %q", observed.body)
|
||||
}
|
||||
if observed.host != "custom.example" {
|
||||
t.Fatalf("unexpected host: %q", observed.host)
|
||||
}
|
||||
if observed.protoMajor != 2 {
|
||||
t.Fatalf("expected HTTP/2 request, got HTTP/%d", observed.protoMajor)
|
||||
}
|
||||
var normalizedValues []string
|
||||
for _, value := range observed.values {
|
||||
for _, part := range strings.Split(value, ",") {
|
||||
normalizedValues = append(normalizedValues, strings.TrimSpace(part))
|
||||
}
|
||||
}
|
||||
slices.Sort(normalizedValues)
|
||||
if !slices.Equal(normalizedValues, []string{"one", "two"}) {
|
||||
t.Fatalf("unexpected header values: %#v", observed.values)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppleTransportPinnedPublicKey(t *testing.T) {
|
||||
server := startAppleHTTPTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("pinned"))
|
||||
})
|
||||
|
||||
goodTransport := newAppleHTTPTestTransport(t, server, option.HTTPClientOptions{
|
||||
Version: 2,
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "localhost",
|
||||
Insecure: true,
|
||||
CertificatePublicKeySHA256: badoption.Listable[[]byte]{server.publicKeyHash},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
response, err := goodTransport.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, server.URL("/good"), nil))
|
||||
if err != nil {
|
||||
t.Fatalf("expected pinned request to succeed: %v", err)
|
||||
}
|
||||
response.Body.Close()
|
||||
|
||||
badHash := append([]byte(nil), server.publicKeyHash...)
|
||||
badHash[0] ^= 0xff
|
||||
badTransport := newAppleHTTPTestTransport(t, server, option.HTTPClientOptions{
|
||||
Version: 2,
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "localhost",
|
||||
Insecure: true,
|
||||
CertificatePublicKeySHA256: badoption.Listable[[]byte]{badHash},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
response, err = badTransport.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, server.URL("/bad"), nil))
|
||||
if err == nil {
|
||||
response.Body.Close()
|
||||
t.Fatal("expected incorrect pinned public key to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppleTransportGuardrails(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
options option.HTTPClientOptions
|
||||
buildRequest func(t *testing.T) *http.Request
|
||||
wantErrSubstr string
|
||||
}{
|
||||
{
|
||||
name: "websocket upgrade rejected",
|
||||
options: option.HTTPClientOptions{
|
||||
Version: 2,
|
||||
},
|
||||
buildRequest: func(t *testing.T) *http.Request {
|
||||
t.Helper()
|
||||
request := newAppleHTTPRequest(t, http.MethodGet, "https://localhost/socket", nil)
|
||||
request.Header.Set("Connection", "Upgrade")
|
||||
request.Header.Set("Upgrade", "websocket")
|
||||
return request
|
||||
},
|
||||
wantErrSubstr: "HTTP upgrade requests are unsupported in Apple HTTP engine",
|
||||
},
|
||||
{
|
||||
name: "missing url rejected",
|
||||
options: option.HTTPClientOptions{
|
||||
Version: 2,
|
||||
},
|
||||
buildRequest: func(t *testing.T) *http.Request {
|
||||
t.Helper()
|
||||
return &http.Request{Method: http.MethodGet}
|
||||
},
|
||||
wantErrSubstr: "missing request URL",
|
||||
},
|
||||
{
|
||||
name: "unsupported scheme rejected",
|
||||
options: option.HTTPClientOptions{
|
||||
Version: 2,
|
||||
},
|
||||
buildRequest: func(t *testing.T) *http.Request {
|
||||
t.Helper()
|
||||
return newAppleHTTPRequest(t, http.MethodGet, "ftp://localhost/file", nil)
|
||||
},
|
||||
wantErrSubstr: "unsupported URL scheme: ftp",
|
||||
},
|
||||
{
|
||||
name: "server name mismatch rejected",
|
||||
options: option.HTTPClientOptions{
|
||||
Version: 2,
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: &option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
buildRequest: func(t *testing.T) *http.Request {
|
||||
t.Helper()
|
||||
return newAppleHTTPRequest(t, http.MethodGet, "https://localhost/path", nil)
|
||||
},
|
||||
wantErrSubstr: "tls.server_name is unsupported in Apple HTTP engine unless it matches request host",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
transport := newAppleHTTPTestTransport(t, nil, testCase.options)
|
||||
response, err := transport.RoundTrip(testCase.buildRequest(t))
|
||||
if err == nil {
|
||||
response.Body.Close()
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), testCase.wantErrSubstr) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppleTransportCancellationRecovery(t *testing.T) {
|
||||
server := startAppleHTTPTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/block":
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case <-time.After(appleHTTPTestTimeout):
|
||||
http.Error(w, "request was not canceled", http.StatusGatewayTimeout)
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
})
|
||||
|
||||
transport := newAppleHTTPTestTransport(t, server, option.HTTPClientOptions{
|
||||
Version: 2,
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: appleHTTPServerTLSOptions(server),
|
||||
},
|
||||
})
|
||||
|
||||
for index := 0; index < appleHTTPRecoveryLoops; index++ {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
request := newAppleHTTPRequestWithContext(t, ctx, http.MethodGet, server.URL("/block"), nil)
|
||||
response, err := transport.RoundTrip(request)
|
||||
cancel()
|
||||
if err == nil {
|
||||
response.Body.Close()
|
||||
t.Fatalf("iteration %d: expected cancellation error", index)
|
||||
}
|
||||
if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("iteration %d: unexpected cancellation error: %v", index, err)
|
||||
}
|
||||
|
||||
response, err = transport.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, server.URL("/ok"), nil))
|
||||
if err != nil {
|
||||
t.Fatalf("iteration %d: follow-up request failed: %v", index, err)
|
||||
}
|
||||
if body := readResponseBody(t, response); body != "ok" {
|
||||
response.Body.Close()
|
||||
t.Fatalf("iteration %d: unexpected follow-up body: %q", index, body)
|
||||
}
|
||||
response.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppleTransportLifecycle(t *testing.T) {
|
||||
server := startAppleHTTPTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
transport := newAppleHTTPTestTransport(t, server, option.HTTPClientOptions{
|
||||
Version: 2,
|
||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
||||
TLS: appleHTTPServerTLSOptions(server),
|
||||
},
|
||||
})
|
||||
clone := transport.Clone()
|
||||
t.Cleanup(func() {
|
||||
_ = clone.Close()
|
||||
})
|
||||
|
||||
assertAppleHTTPSucceeds(t, transport, server.URL("/original"))
|
||||
assertAppleHTTPSucceeds(t, clone, server.URL("/clone"))
|
||||
|
||||
transport.CloseIdleConnections()
|
||||
assertAppleHTTPSucceeds(t, transport, server.URL("/reset"))
|
||||
|
||||
if err := transport.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
response, err := transport.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, server.URL("/closed"), nil))
|
||||
if err == nil {
|
||||
response.Body.Close()
|
||||
t.Fatal("expected closed transport to fail")
|
||||
}
|
||||
if !errors.Is(err, net.ErrClosed) {
|
||||
t.Fatalf("unexpected closed transport error: %v", err)
|
||||
}
|
||||
|
||||
assertAppleHTTPSucceeds(t, clone, server.URL("/clone-after-original-close"))
|
||||
|
||||
clone.CloseIdleConnections()
|
||||
assertAppleHTTPSucceeds(t, clone, server.URL("/clone-reset"))
|
||||
|
||||
if err := clone.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response, err = clone.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, server.URL("/clone-closed"), nil))
|
||||
if err == nil {
|
||||
response.Body.Close()
|
||||
t.Fatal("expected closed clone to fail")
|
||||
}
|
||||
if !errors.Is(err, net.ErrClosed) {
|
||||
t.Fatalf("unexpected closed clone error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func startAppleHTTPTestServer(t *testing.T, handler http.HandlerFunc) *appleHTTPTestServer {
|
||||
t.Helper()
|
||||
|
||||
serverCertificate, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost")
|
||||
server := httptest.NewUnstartedServer(handler)
|
||||
server.EnableHTTP2 = true
|
||||
server.TLS = &stdtls.Config{
|
||||
Certificates: []stdtls.Certificate{serverCertificate},
|
||||
MinVersion: stdtls.VersionTLS12,
|
||||
}
|
||||
server.StartTLS()
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
parsedURL, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
baseURL := *parsedURL
|
||||
baseURL.Host = net.JoinHostPort("localhost", parsedURL.Port())
|
||||
|
||||
return &appleHTTPTestServer{
|
||||
server: server,
|
||||
baseURL: baseURL.String(),
|
||||
dialHost: parsedURL.Hostname(),
|
||||
certificate: serverCertificate,
|
||||
certificatePEM: serverCertificatePEM,
|
||||
publicKeyHash: certificatePublicKeySHA256(t, serverCertificate.Certificate[0]),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *appleHTTPTestServer) URL(path string) string {
|
||||
if path == "" {
|
||||
return s.baseURL
|
||||
}
|
||||
if strings.HasPrefix(path, "/") {
|
||||
return s.baseURL + path
|
||||
}
|
||||
return s.baseURL + "/" + path
|
||||
}
|
||||
|
||||
func newAppleHTTPTestTransport(t *testing.T, server *appleHTTPTestServer, options option.HTTPClientOptions) adapter.HTTPTransport {
|
||||
t.Helper()
|
||||
|
||||
ctx := service.ContextWith[adapter.ConnectionManager](
|
||||
context.Background(),
|
||||
route.NewConnectionManager(log.NewNOPFactory().NewLogger("connection")),
|
||||
)
|
||||
dialer := &appleHTTPTestDialer{
|
||||
hostMap: make(map[string]string),
|
||||
}
|
||||
if server != nil {
|
||||
dialer.hostMap["localhost"] = server.dialHost
|
||||
}
|
||||
|
||||
transport, err := newAppleTransport(ctx, log.NewNOPFactory().NewLogger("httpclient"), dialer, options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = transport.Close()
|
||||
})
|
||||
return transport
|
||||
}
|
||||
|
||||
func (d *appleHTTPTestDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
host := destination.AddrString()
|
||||
if destination.IsDomain() {
|
||||
host = destination.Fqdn
|
||||
if mappedHost, loaded := d.hostMap[host]; loaded {
|
||||
host = mappedHost
|
||||
}
|
||||
}
|
||||
return d.dialer.DialContext(ctx, network, net.JoinHostPort(host, strconv.Itoa(int(destination.Port))))
|
||||
}
|
||||
|
||||
func (d *appleHTTPTestDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
host := destination.AddrString()
|
||||
if destination.IsDomain() {
|
||||
host = destination.Fqdn
|
||||
if mappedHost, loaded := d.hostMap[host]; loaded {
|
||||
host = mappedHost
|
||||
}
|
||||
}
|
||||
if host == "" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
return d.listener.ListenPacket(ctx, N.NetworkUDP, net.JoinHostPort(host, strconv.Itoa(int(destination.Port))))
|
||||
}
|
||||
|
||||
func newAppleHTTPTestCertificate(t *testing.T, serverName string) (stdtls.Certificate, string) {
|
||||
t.Helper()
|
||||
|
||||
privateKeyPEM, certificatePEM, err := boxTLS.GenerateCertificate(nil, nil, time.Now, serverName, time.Now().Add(time.Hour))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
certificate, err := stdtls.X509KeyPair(certificatePEM, privateKeyPEM)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return certificate, string(certificatePEM)
|
||||
}
|
||||
|
||||
func certificatePublicKeySHA256(t *testing.T, certificateDER []byte) []byte {
|
||||
t.Helper()
|
||||
|
||||
certificate, err := x509.ParseCertificate(certificateDER)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
publicKeyDER, err := x509.MarshalPKIXPublicKey(certificate.PublicKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hashValue := sha256.Sum256(publicKeyDER)
|
||||
return append([]byte(nil), hashValue[:]...)
|
||||
}
|
||||
|
||||
func appleHTTPServerTLSOptions(server *appleHTTPTestServer) *option.OutboundTLSOptions {
|
||||
return &option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: "localhost",
|
||||
Certificate: badoption.Listable[string]{server.certificatePEM},
|
||||
}
|
||||
}
|
||||
|
||||
func newAppleHTTPRequest(t *testing.T, method string, rawURL string, body []byte) *http.Request {
|
||||
t.Helper()
|
||||
return newAppleHTTPRequestWithContext(t, context.Background(), method, rawURL, body)
|
||||
}
|
||||
|
||||
func newAppleHTTPRequestWithContext(t *testing.T, ctx context.Context, method string, rawURL string, body []byte) *http.Request {
|
||||
t.Helper()
|
||||
request, err := http.NewRequestWithContext(ctx, method, rawURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
func waitObservedRequest(t *testing.T, requests <-chan appleHTTPObservedRequest) appleHTTPObservedRequest {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
case request := <-requests:
|
||||
return request
|
||||
case <-time.After(appleHTTPTestTimeout):
|
||||
t.Fatal("timed out waiting for observed request")
|
||||
return appleHTTPObservedRequest{}
|
||||
}
|
||||
}
|
||||
|
||||
func readResponseBody(t *testing.T, response *http.Response) string {
|
||||
t.Helper()
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(body)
|
||||
}
|
||||
|
||||
func assertAppleHTTPSucceeds(t *testing.T, transport adapter.HTTPTransport, rawURL string) {
|
||||
t.Helper()
|
||||
|
||||
response, err := transport.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, rawURL, nil))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if body := readResponseBody(t, response); body != "ok" {
|
||||
t.Fatalf("unexpected response body: %q", body)
|
||||
}
|
||||
}
|
||||
17
common/httpclient/apple_transport_stub.go
Normal file
17
common/httpclient/apple_transport_stub.go
Normal file
@@ -0,0 +1,17 @@
|
||||
//go:build !darwin || !cgo
|
||||
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func newAppleTransport(ctx context.Context, logger logger.ContextLogger, rawDialer N.Dialer, options option.HTTPClientOptions) (adapter.HTTPTransport, error) {
|
||||
return nil, E.New("Apple HTTP engine is not available on non-Apple platforms")
|
||||
}
|
||||
@@ -2,12 +2,13 @@ package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
@@ -15,24 +16,20 @@ import (
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type httpTransport interface {
|
||||
http.RoundTripper
|
||||
CloseIdleConnections()
|
||||
Clone() httpTransport
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
transport httpTransport
|
||||
type Transport struct {
|
||||
transport adapter.HTTPTransport
|
||||
dialer N.Dialer
|
||||
headers http.Header
|
||||
host string
|
||||
tag string
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, logger logger.ContextLogger, tag string, options option.HTTPClientOptions) (*Client, error) {
|
||||
func NewTransport(ctx context.Context, logger logger.ContextLogger, tag string, options option.HTTPClientOptions) (*Transport, error) {
|
||||
rawDialer, err := dialer.NewWithOptions(dialer.Options{
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
RemoteIsDomain: true,
|
||||
DirectResolver: options.DirectResolver,
|
||||
ResolverOnDetour: options.ResolveOnDetour,
|
||||
NewDialer: options.ResolveOnDetour,
|
||||
DefaultOutbound: options.DefaultOutbound,
|
||||
@@ -40,6 +37,26 @@ func NewClient(ctx context.Context, logger logger.ContextLogger, tag string, opt
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch options.Engine {
|
||||
case C.TLSEngineApple:
|
||||
transport, transportErr := newAppleTransport(ctx, logger, rawDialer, options)
|
||||
if transportErr != nil {
|
||||
return nil, transportErr
|
||||
}
|
||||
headers := options.Headers.Build()
|
||||
host := headers.Get("Host")
|
||||
headers.Del("Host")
|
||||
return &Transport{
|
||||
transport: transport,
|
||||
dialer: rawDialer,
|
||||
headers: headers,
|
||||
host: host,
|
||||
tag: tag,
|
||||
}, nil
|
||||
case C.TLSEngineDefault, "go":
|
||||
default:
|
||||
return nil, E.New("unknown HTTP engine: ", options.Engine)
|
||||
}
|
||||
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
||||
tlsOptions.Enabled = true
|
||||
baseTLSConfig, err := tls.NewClientWithOptions(tls.ClientOptions{
|
||||
@@ -51,26 +68,27 @@ func NewClient(ctx context.Context, logger logger.ContextLogger, tag string, opt
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewClientWithDialer(rawDialer, baseTLSConfig, tag, options)
|
||||
return NewTransportWithDialer(rawDialer, baseTLSConfig, tag, options)
|
||||
}
|
||||
|
||||
func NewClientWithDialer(rawDialer N.Dialer, baseTLSConfig tls.Config, tag string, options option.HTTPClientOptions) (*Client, error) {
|
||||
headers := options.Headers.Build()
|
||||
host := headers.Get("Host")
|
||||
headers.Del("Host")
|
||||
func NewTransportWithDialer(rawDialer N.Dialer, baseTLSConfig tls.Config, tag string, options option.HTTPClientOptions) (*Transport, error) {
|
||||
transport, err := newTransport(rawDialer, baseTLSConfig, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{
|
||||
headers := options.Headers.Build()
|
||||
host := headers.Get("Host")
|
||||
headers.Del("Host")
|
||||
return &Transport{
|
||||
transport: transport,
|
||||
dialer: rawDialer,
|
||||
headers: headers,
|
||||
host: host,
|
||||
tag: tag,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTPClientOptions) (httpTransport, error) {
|
||||
func newTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTPClientOptions) (adapter.HTTPTransport, error) {
|
||||
version := options.Version
|
||||
if version == 0 {
|
||||
version = 2
|
||||
@@ -79,7 +97,7 @@ func newTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.H
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = 300 * time.Millisecond
|
||||
}
|
||||
var transport httpTransport
|
||||
var transport adapter.HTTPTransport
|
||||
var err error
|
||||
switch version {
|
||||
case 1:
|
||||
@@ -100,7 +118,7 @@ func newTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.H
|
||||
if options.DisableVersionFallback {
|
||||
transport, err = newHTTP3Transport(rawDialer, baseTLSConfig, options.HTTP3Options)
|
||||
} else {
|
||||
var h2Fallback httpTransport
|
||||
var h2Fallback adapter.HTTPTransport
|
||||
h2Fallback, err = newHTTP2FallbackTransport(rawDialer, baseTLSConfig, options.HTTP2Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -116,7 +134,7 @@ func newTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.H
|
||||
return transport, nil
|
||||
}
|
||||
|
||||
func (c *Client) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
func (c *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
if c.tag == "" && len(c.headers) == 0 && c.host == "" {
|
||||
return c.transport.RoundTrip(request)
|
||||
}
|
||||
@@ -132,23 +150,33 @@ func (c *Client) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return c.transport.RoundTrip(request)
|
||||
}
|
||||
|
||||
func (c *Client) CloseIdleConnections() {
|
||||
func (c *Transport) CloseIdleConnections() {
|
||||
c.transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (c *Client) Clone() *Client {
|
||||
return &Client{
|
||||
func (c *Transport) Clone() adapter.HTTPTransport {
|
||||
return &Transport{
|
||||
transport: c.transport.Clone(),
|
||||
dialer: c.dialer,
|
||||
headers: c.headers.Clone(),
|
||||
host: c.host,
|
||||
tag: c.tag,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
c.CloseIdleConnections()
|
||||
if closer, isCloser := c.transport.(io.Closer); isCloser {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
func (c *Transport) Close() error {
|
||||
return c.transport.Close()
|
||||
}
|
||||
|
||||
// InitializeDetour eagerly resolves the detour dialer backing transport so that
|
||||
// detour misconfigurations surface at startup instead of on the first request.
|
||||
func InitializeDetour(transport adapter.HTTPTransport) error {
|
||||
if shared, isShared := transport.(*sharedTransport); isShared {
|
||||
transport = shared.HTTPTransport
|
||||
}
|
||||
inner, isInner := transport.(*Transport)
|
||||
if !isInner {
|
||||
return nil
|
||||
}
|
||||
return dialer.InitializeDetour(inner.dialer)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
@@ -36,6 +37,11 @@ func (t *http1Transport) CloseIdleConnections() {
|
||||
t.transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (t *http1Transport) Clone() httpTransport {
|
||||
func (t *http1Transport) Clone() adapter.HTTPTransport {
|
||||
return &http1Transport{transport: t.transport.Clone()}
|
||||
}
|
||||
|
||||
func (t *http1Transport) Close() error {
|
||||
t.CloseIdleConnections()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
@@ -78,10 +79,15 @@ func (t *http2FallbackTransport) CloseIdleConnections() {
|
||||
t.h2Transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (t *http2FallbackTransport) Clone() httpTransport {
|
||||
func (t *http2FallbackTransport) Clone() adapter.HTTPTransport {
|
||||
return &http2FallbackTransport{
|
||||
h2Transport: CloneHTTP2Transport(t.h2Transport),
|
||||
h1Transport: t.h1Transport.Clone().(*http1Transport),
|
||||
h2Fallback: t.h2Fallback,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *http2FallbackTransport) Close() error {
|
||||
t.CloseIdleConnections()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
@@ -46,9 +47,14 @@ func (t *http2Transport) CloseIdleConnections() {
|
||||
t.h2Transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (t *http2Transport) Clone() httpTransport {
|
||||
func (t *http2Transport) Clone() adapter.HTTPTransport {
|
||||
return &http2Transport{
|
||||
h2Transport: CloneHTTP2Transport(t.h2Transport),
|
||||
h1Transport: t.h1Transport.Clone().(*http1Transport),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *http2Transport) Close() error {
|
||||
t.CloseIdleConnections()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
@@ -26,7 +27,7 @@ type http3Transport struct {
|
||||
|
||||
type http3FallbackTransport struct {
|
||||
h3Transport *http3.Transport
|
||||
h2Fallback httpTransport
|
||||
h2Fallback adapter.HTTPTransport
|
||||
fallbackDelay time.Duration
|
||||
brokenAccess sync.Mutex
|
||||
brokenUntil time.Time
|
||||
@@ -97,7 +98,7 @@ func newHTTP3Transport(
|
||||
rawDialer N.Dialer,
|
||||
baseTLSConfig tls.Config,
|
||||
options option.QUICOptions,
|
||||
) (httpTransport, error) {
|
||||
) (adapter.HTTPTransport, error) {
|
||||
return &http3Transport{
|
||||
h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options),
|
||||
}, nil
|
||||
@@ -106,10 +107,10 @@ func newHTTP3Transport(
|
||||
func newHTTP3FallbackTransport(
|
||||
rawDialer N.Dialer,
|
||||
baseTLSConfig tls.Config,
|
||||
h2Fallback httpTransport,
|
||||
h2Fallback adapter.HTTPTransport,
|
||||
options option.QUICOptions,
|
||||
fallbackDelay time.Duration,
|
||||
) (httpTransport, error) {
|
||||
) (adapter.HTTPTransport, error) {
|
||||
return &http3FallbackTransport{
|
||||
h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options),
|
||||
h2Fallback: h2Fallback,
|
||||
@@ -130,7 +131,7 @@ func (t *http3Transport) Close() error {
|
||||
return t.h3Transport.Close()
|
||||
}
|
||||
|
||||
func (t *http3Transport) Clone() httpTransport {
|
||||
func (t *http3Transport) Clone() adapter.HTTPTransport {
|
||||
return &http3Transport{
|
||||
h3Transport: t.h3Transport,
|
||||
}
|
||||
@@ -275,7 +276,7 @@ func (t *http3FallbackTransport) Close() error {
|
||||
return t.h3Transport.Close()
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) Clone() httpTransport {
|
||||
func (t *http3FallbackTransport) Clone() adapter.HTTPTransport {
|
||||
return &http3FallbackTransport{
|
||||
h3Transport: t.h3Transport,
|
||||
h2Fallback: t.h2Fallback.Clone(),
|
||||
|
||||
@@ -5,6 +5,7 @@ package httpclient
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
@@ -14,10 +15,10 @@ import (
|
||||
func newHTTP3FallbackTransport(
|
||||
rawDialer N.Dialer,
|
||||
baseTLSConfig tls.Config,
|
||||
h2Fallback httpTransport,
|
||||
h2Fallback adapter.HTTPTransport,
|
||||
options option.QUICOptions,
|
||||
fallbackDelay time.Duration,
|
||||
) (httpTransport, error) {
|
||||
) (adapter.HTTPTransport, error) {
|
||||
return nil, E.New("HTTP/3 requires building with the with_quic tag")
|
||||
}
|
||||
|
||||
@@ -25,6 +26,6 @@ func newHTTP3Transport(
|
||||
rawDialer N.Dialer,
|
||||
baseTLSConfig tls.Config,
|
||||
options option.QUICOptions,
|
||||
) (httpTransport, error) {
|
||||
) (adapter.HTTPTransport, error) {
|
||||
return nil, E.New("HTTP/3 requires building with the with_quic tag")
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -22,11 +21,11 @@ type Manager struct {
|
||||
logger log.ContextLogger
|
||||
access sync.Mutex
|
||||
defines map[string]option.HTTPClient
|
||||
clients map[string]*Client
|
||||
transports map[string]*Transport
|
||||
defaultTag string
|
||||
defaultTransport http.RoundTripper
|
||||
defaultTransportFallback func() (*Client, error)
|
||||
fallbackClient *Client
|
||||
defaultTransport adapter.HTTPTransport
|
||||
defaultTransportFallback func() (*Transport, error)
|
||||
fallbackTransport *Transport
|
||||
}
|
||||
|
||||
func NewManager(ctx context.Context, logger log.ContextLogger, clients []option.HTTPClient, defaultHTTPClient string) *Manager {
|
||||
@@ -42,12 +41,12 @@ func NewManager(ctx context.Context, logger log.ContextLogger, clients []option.
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
defines: defines,
|
||||
clients: make(map[string]*Client),
|
||||
transports: make(map[string]*Transport),
|
||||
defaultTag: defaultTag,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Initialize(defaultTransportFallback func() (*Client, error)) {
|
||||
func (m *Manager) Initialize(defaultTransportFallback func() (*Transport, error)) {
|
||||
m.defaultTransportFallback = defaultTransportFallback
|
||||
}
|
||||
|
||||
@@ -66,21 +65,24 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
}
|
||||
m.defaultTransport = transport
|
||||
} else if m.defaultTransportFallback != nil {
|
||||
client, err := m.defaultTransportFallback()
|
||||
transport, err := m.defaultTransportFallback()
|
||||
if err != nil {
|
||||
return E.Cause(err, "create default http client")
|
||||
}
|
||||
m.defaultTransport = client
|
||||
m.fallbackClient = client
|
||||
m.defaultTransport = transport
|
||||
m.fallbackTransport = transport
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) DefaultTransport() http.RoundTripper {
|
||||
return m.defaultTransport
|
||||
func (m *Manager) DefaultTransport() adapter.HTTPTransport {
|
||||
if m.defaultTransport == nil {
|
||||
return nil
|
||||
}
|
||||
return &sharedTransport{m.defaultTransport}
|
||||
}
|
||||
|
||||
func (m *Manager) ResolveTransport(logger logger.ContextLogger, options option.HTTPClientOptions) (http.RoundTripper, error) {
|
||||
func (m *Manager) ResolveTransport(ctx context.Context, logger logger.ContextLogger, options option.HTTPClientOptions) (adapter.HTTPTransport, error) {
|
||||
if options.Tag != "" {
|
||||
if options.ResolveOnDetour {
|
||||
define, loaded := m.defines[options.Tag]
|
||||
@@ -89,48 +91,74 @@ func (m *Manager) ResolveTransport(logger logger.ContextLogger, options option.H
|
||||
}
|
||||
resolvedOptions := define.Options()
|
||||
resolvedOptions.ResolveOnDetour = true
|
||||
return NewClient(m.ctx, logger, options.Tag, resolvedOptions)
|
||||
return NewTransport(ctx, logger, options.Tag, resolvedOptions)
|
||||
}
|
||||
return m.resolveShared(options.Tag)
|
||||
transport, err := m.resolveShared(options.Tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sharedTransport{transport}, nil
|
||||
}
|
||||
return NewClient(m.ctx, logger, "", options)
|
||||
return NewTransport(ctx, logger, "", options)
|
||||
}
|
||||
|
||||
func (m *Manager) resolveShared(tag string) (http.RoundTripper, error) {
|
||||
func (m *Manager) resolveShared(tag string) (adapter.HTTPTransport, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if client, loaded := m.clients[tag]; loaded {
|
||||
return client, nil
|
||||
if transport, loaded := m.transports[tag]; loaded {
|
||||
return transport, nil
|
||||
}
|
||||
define, loaded := m.defines[tag]
|
||||
if !loaded {
|
||||
return nil, E.New("http_client not found: ", tag)
|
||||
}
|
||||
client, err := NewClient(m.ctx, m.logger, tag, define.Options())
|
||||
transport, err := NewTransport(m.ctx, m.logger, tag, define.Options())
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create shared http_client[", tag, "]")
|
||||
}
|
||||
m.clients[tag] = client
|
||||
return client, nil
|
||||
m.transports[tag] = transport
|
||||
return transport, nil
|
||||
}
|
||||
|
||||
type sharedTransport struct {
|
||||
adapter.HTTPTransport
|
||||
}
|
||||
|
||||
func (t *sharedTransport) CloseIdleConnections() {
|
||||
}
|
||||
|
||||
func (t *sharedTransport) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) ResetNetwork() {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
for _, transport := range m.transports {
|
||||
transport.CloseIdleConnections()
|
||||
}
|
||||
if m.fallbackTransport != nil {
|
||||
m.fallbackTransport.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.clients == nil {
|
||||
if m.transports == nil {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
for _, client := range m.clients {
|
||||
err = E.Append(err, client.Close(), func(err error) error {
|
||||
for _, transport := range m.transports {
|
||||
err = E.Append(err, transport.Close(), func(err error) error {
|
||||
return E.Cause(err, "close http client")
|
||||
})
|
||||
}
|
||||
if m.fallbackClient != nil {
|
||||
err = E.Append(err, m.fallbackClient.Close(), func(err error) error {
|
||||
if m.fallbackTransport != nil {
|
||||
err = E.Append(err, m.fallbackTransport.Close(), func(err error) error {
|
||||
return E.Cause(err, "close default http client")
|
||||
})
|
||||
}
|
||||
m.clients = nil
|
||||
m.transports = nil
|
||||
return err
|
||||
}
|
||||
|
||||
115
common/proxybridge/bridge.go
Normal file
115
common/proxybridge/bridge.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package proxybridge
|
||||
|
||||
import (
|
||||
std_bufio "bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/protocol/socks"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type Bridge struct {
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
tag string
|
||||
dialer N.Dialer
|
||||
connection adapter.ConnectionManager
|
||||
tcpListener *net.TCPListener
|
||||
username string
|
||||
password string
|
||||
authenticator *auth.Authenticator
|
||||
}
|
||||
|
||||
func New(ctx context.Context, logger logger.ContextLogger, tag string, dialer N.Dialer) (*Bridge, error) {
|
||||
username := randomHex(16)
|
||||
password := randomHex(16)
|
||||
tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bridge := &Bridge{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
tag: tag,
|
||||
dialer: dialer,
|
||||
connection: service.FromContext[adapter.ConnectionManager](ctx),
|
||||
tcpListener: tcpListener,
|
||||
username: username,
|
||||
password: password,
|
||||
authenticator: auth.NewAuthenticator([]auth.User{{Username: username, Password: password}}),
|
||||
}
|
||||
go bridge.acceptLoop()
|
||||
return bridge, nil
|
||||
}
|
||||
|
||||
func randomHex(size int) string {
|
||||
raw := make([]byte, size)
|
||||
rand.Read(raw)
|
||||
return hex.EncodeToString(raw)
|
||||
}
|
||||
|
||||
func (b *Bridge) Port() uint16 {
|
||||
return M.SocksaddrFromNet(b.tcpListener.Addr()).Port
|
||||
}
|
||||
|
||||
func (b *Bridge) Username() string {
|
||||
return b.username
|
||||
}
|
||||
|
||||
func (b *Bridge) Password() string {
|
||||
return b.password
|
||||
}
|
||||
|
||||
func (b *Bridge) Close() error {
|
||||
return common.Close(b.tcpListener)
|
||||
}
|
||||
|
||||
func (b *Bridge) acceptLoop() {
|
||||
for {
|
||||
tcpConn, err := b.tcpListener.AcceptTCP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ctx := log.ContextWithNewID(b.ctx)
|
||||
go func() {
|
||||
hErr := socks.HandleConnectionEx(ctx, tcpConn, std_bufio.NewReader(tcpConn), b.authenticator, b, nil, 0, M.SocksaddrFromNet(tcpConn.RemoteAddr()), nil)
|
||||
if hErr == nil {
|
||||
return
|
||||
}
|
||||
if E.IsClosedOrCanceled(hErr) {
|
||||
b.logger.DebugContext(ctx, E.Cause(hErr, b.tag, " connection closed"))
|
||||
return
|
||||
}
|
||||
b.logger.ErrorContext(ctx, E.Cause(hErr, b.tag))
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bridge) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Source = source
|
||||
metadata.Destination = destination
|
||||
metadata.Network = N.NetworkTCP
|
||||
b.logger.InfoContext(ctx, b.tag, " connection to ", metadata.Destination)
|
||||
b.connection.NewConnection(ctx, b.dialer, conn, metadata, onClose)
|
||||
}
|
||||
|
||||
func (b *Bridge) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Source = source
|
||||
metadata.Destination = destination
|
||||
metadata.Network = N.NetworkUDP
|
||||
b.logger.InfoContext(ctx, b.tag, " packet connection to ", metadata.Destination)
|
||||
b.connection.NewPacketConnection(ctx, b.dialer, conn, metadata, onClose)
|
||||
}
|
||||
214
common/tls/apple_client.go
Normal file
214
common/tls/apple_client.go
Normal file
@@ -0,0 +1,214 @@
|
||||
//go:build darwin && cgo
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxConstant "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type appleCertificateStore interface {
|
||||
StoreKind() string
|
||||
CurrentPEM() []string
|
||||
}
|
||||
|
||||
type appleClientConfig struct {
|
||||
serverName string
|
||||
nextProtos []string
|
||||
handshakeTimeout time.Duration
|
||||
minVersion uint16
|
||||
maxVersion uint16
|
||||
insecure bool
|
||||
anchorPEM string
|
||||
anchorOnly bool
|
||||
certificatePublicKeySHA256 [][]byte
|
||||
}
|
||||
|
||||
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 {
|
||||
return &appleClientConfig{
|
||||
serverName: c.serverName,
|
||||
nextProtos: append([]string(nil), c.nextProtos...),
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
minVersion: c.minVersion,
|
||||
maxVersion: c.maxVersion,
|
||||
insecure: c.insecure,
|
||||
anchorPEM: c.anchorPEM,
|
||||
anchorOnly: c.anchorOnly,
|
||||
certificatePublicKeySHA256: append([][]byte(nil), c.certificatePublicKeySHA256...),
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var serverName string
|
||||
if options.ServerName != "" {
|
||||
serverName = options.ServerName
|
||||
} else if serverAddress != "" {
|
||||
serverName = serverAddress
|
||||
}
|
||||
if serverName == "" && !options.Insecure && !allowEmptyServerName {
|
||||
return nil, errMissingServerName
|
||||
}
|
||||
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = boxConstant.TCPTimeout
|
||||
}
|
||||
|
||||
return &appleClientConfig{
|
||||
serverName: serverName,
|
||||
nextProtos: append([]string(nil), options.ALPN...),
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
minVersion: validated.MinVersion,
|
||||
maxVersion: validated.MaxVersion,
|
||||
insecure: options.Insecure || len(options.CertificatePublicKeySHA256) > 0,
|
||||
anchorPEM: validated.AnchorPEM,
|
||||
anchorOnly: validated.AnchorOnly,
|
||||
certificatePublicKeySHA256: append([][]byte(nil), options.CertificatePublicKeySHA256...),
|
||||
}, 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 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())
|
||||
}
|
||||
}
|
||||
@@ -6,515 +6,8 @@ package tls
|
||||
#cgo CFLAGS: -x objective-c -fobjc-arc
|
||||
#cgo LDFLAGS: -framework Foundation -framework Network -framework Security
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
#include <Network/Network.h>
|
||||
#include <Security/Security.h>
|
||||
#include <Security/SecProtocolMetadata.h>
|
||||
#include <Security/SecProtocolOptions.h>
|
||||
#include <Security/SecProtocolTypes.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <dlfcn.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
typedef nw_connection_t _Nullable (*box_nw_connection_create_with_connected_socket_and_parameters_f)(int connected_socket, nw_parameters_t parameters);
|
||||
typedef const char * _Nullable (*box_sec_protocol_metadata_string_accessor_f)(sec_protocol_metadata_t metadata);
|
||||
|
||||
typedef struct box_apple_tls_client {
|
||||
void *connection;
|
||||
void *ready_semaphore;
|
||||
atomic_bool ready;
|
||||
atomic_bool ready_done;
|
||||
char *ready_error;
|
||||
} box_apple_tls_client_t;
|
||||
|
||||
typedef struct box_apple_tls_state {
|
||||
uint16_t version;
|
||||
uint16_t cipher_suite;
|
||||
char *alpn;
|
||||
char *server_name;
|
||||
uint8_t *peer_cert_chain;
|
||||
size_t peer_cert_chain_len;
|
||||
} box_apple_tls_state_t;
|
||||
|
||||
static dispatch_queue_t box_apple_tls_queue(void) {
|
||||
static dispatch_queue_t queue;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
queue = dispatch_queue_create("sing-box.apple-private-tls", DISPATCH_QUEUE_CONCURRENT);
|
||||
});
|
||||
return queue;
|
||||
}
|
||||
|
||||
static nw_connection_t box_apple_tls_connection(box_apple_tls_client_t *client) {
|
||||
if (client == NULL || client->connection == NULL) {
|
||||
return nil;
|
||||
}
|
||||
return (__bridge nw_connection_t)client->connection;
|
||||
}
|
||||
|
||||
static dispatch_semaphore_t box_apple_tls_ready_semaphore(box_apple_tls_client_t *client) {
|
||||
if (client == NULL || client->ready_semaphore == NULL) {
|
||||
return nil;
|
||||
}
|
||||
return (__bridge dispatch_semaphore_t)client->ready_semaphore;
|
||||
}
|
||||
|
||||
static void box_set_error_string(char **error_out, NSString *message) {
|
||||
if (error_out == NULL || *error_out != NULL) {
|
||||
return;
|
||||
}
|
||||
if (message == nil) {
|
||||
*error_out = strdup("unknown error");
|
||||
return;
|
||||
}
|
||||
const char *utf8 = [message UTF8String];
|
||||
*error_out = strdup(utf8 != NULL ? utf8 : "unknown error");
|
||||
}
|
||||
|
||||
static void box_set_error_message(char **error_out, const char *message) {
|
||||
if (error_out == NULL || *error_out != NULL) {
|
||||
return;
|
||||
}
|
||||
*error_out = strdup(message != NULL ? message : "unknown error");
|
||||
}
|
||||
|
||||
static void box_set_error_from_nw_error(char **error_out, nw_error_t error) {
|
||||
if (error == NULL) {
|
||||
box_set_error_message(error_out, "unknown network error");
|
||||
return;
|
||||
}
|
||||
CFErrorRef cfError = nw_error_copy_cf_error(error);
|
||||
if (cfError == NULL) {
|
||||
box_set_error_message(error_out, "unknown network error");
|
||||
return;
|
||||
}
|
||||
NSString *description = [(__bridge NSError *)cfError description];
|
||||
box_set_error_string(error_out, description);
|
||||
CFRelease(cfError);
|
||||
}
|
||||
|
||||
static char *box_apple_tls_metadata_copy_negotiated_protocol(sec_protocol_metadata_t metadata) {
|
||||
static box_sec_protocol_metadata_string_accessor_f copy_fn;
|
||||
static box_sec_protocol_metadata_string_accessor_f get_fn;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
copy_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_copy_negotiated_protocol");
|
||||
get_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_get_negotiated_protocol");
|
||||
});
|
||||
if (copy_fn != NULL) {
|
||||
return (char *)copy_fn(metadata);
|
||||
}
|
||||
if (get_fn != NULL) {
|
||||
const char *protocol = get_fn(metadata);
|
||||
if (protocol != NULL) {
|
||||
return strdup(protocol);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char *box_apple_tls_metadata_copy_server_name(sec_protocol_metadata_t metadata) {
|
||||
static box_sec_protocol_metadata_string_accessor_f copy_fn;
|
||||
static box_sec_protocol_metadata_string_accessor_f get_fn;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
copy_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_copy_server_name");
|
||||
get_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_get_server_name");
|
||||
});
|
||||
if (copy_fn != NULL) {
|
||||
return (char *)copy_fn(metadata);
|
||||
}
|
||||
if (get_fn != NULL) {
|
||||
const char *server_name = get_fn(metadata);
|
||||
if (server_name != NULL) {
|
||||
return strdup(server_name);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static NSArray<NSString *> *box_split_lines(const char *content, size_t content_len) {
|
||||
if (content == NULL || content_len == 0) {
|
||||
return @[];
|
||||
}
|
||||
NSString *string = [[NSString alloc] initWithBytes:content length:content_len encoding:NSUTF8StringEncoding];
|
||||
if (string == nil) {
|
||||
return @[];
|
||||
}
|
||||
NSMutableArray<NSString *> *lines = [NSMutableArray array];
|
||||
[string enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {
|
||||
if (line.length > 0) {
|
||||
[lines addObject:line];
|
||||
}
|
||||
}];
|
||||
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) {
|
||||
bool result = false;
|
||||
SecTrustRef trustRef = sec_trust_copy_ref(trust);
|
||||
if (trustRef == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (anchors.count > 0 || anchor_only) {
|
||||
CFMutableArrayRef anchorArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
|
||||
for (id certificate in anchors) {
|
||||
CFArrayAppendValue(anchorArray, (__bridge const void *)certificate);
|
||||
}
|
||||
SecTrustSetAnchorCertificates(trustRef, anchorArray);
|
||||
SecTrustSetAnchorCertificatesOnly(trustRef, anchor_only);
|
||||
CFRelease(anchorArray);
|
||||
}
|
||||
CFErrorRef error = NULL;
|
||||
result = SecTrustEvaluateWithError(trustRef, &error);
|
||||
if (error != NULL) {
|
||||
CFRelease(error);
|
||||
}
|
||||
CFRelease(trustRef);
|
||||
return result;
|
||||
}
|
||||
|
||||
static nw_connection_t box_apple_tls_create_connection(int connected_socket, nw_parameters_t parameters) {
|
||||
static box_nw_connection_create_with_connected_socket_and_parameters_f create_fn;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
char name[] = "sretemarap_dna_tekcos_detcennoc_htiw_etaerc_noitcennoc_wn";
|
||||
for (size_t i = 0, j = sizeof(name) - 2; i < j; i++, j--) {
|
||||
char t = name[i]; name[i] = name[j]; name[j] = t;
|
||||
}
|
||||
create_fn = (box_nw_connection_create_with_connected_socket_and_parameters_f)dlsym(RTLD_DEFAULT, name);
|
||||
});
|
||||
if (create_fn == NULL) {
|
||||
return nil;
|
||||
}
|
||||
return create_fn(connected_socket, parameters);
|
||||
}
|
||||
|
||||
static box_apple_tls_client_t *box_apple_tls_client_create(
|
||||
int connected_socket,
|
||||
const char *server_name,
|
||||
const char *alpn,
|
||||
size_t alpn_len,
|
||||
uint16_t min_version,
|
||||
uint16_t max_version,
|
||||
bool insecure,
|
||||
const char *anchor_pem,
|
||||
size_t anchor_pem_len,
|
||||
bool anchor_only,
|
||||
char **error_out
|
||||
) {
|
||||
NSArray<NSString *> *alpnList = box_split_lines(alpn, alpn_len);
|
||||
NSArray *anchors = box_parse_certificates_from_pem(anchor_pem, anchor_pem_len);
|
||||
nw_parameters_t parameters = nw_parameters_create_secure_tcp(^(nw_protocol_options_t tls_options) {
|
||||
sec_protocol_options_t sec_options = nw_tls_copy_sec_protocol_options(tls_options);
|
||||
if (min_version != 0) {
|
||||
sec_protocol_options_set_min_tls_protocol_version(sec_options, (tls_protocol_version_t)min_version);
|
||||
}
|
||||
if (max_version != 0) {
|
||||
sec_protocol_options_set_max_tls_protocol_version(sec_options, (tls_protocol_version_t)max_version);
|
||||
}
|
||||
if (server_name != NULL && server_name[0] != '\0') {
|
||||
sec_protocol_options_set_tls_server_name(sec_options, server_name);
|
||||
}
|
||||
for (NSString *protocol in alpnList) {
|
||||
sec_protocol_options_add_tls_application_protocol(sec_options, protocol.UTF8String);
|
||||
}
|
||||
sec_protocol_options_set_peer_authentication_required(sec_options, !insecure);
|
||||
if (insecure) {
|
||||
sec_protocol_options_set_verify_block(sec_options, ^(sec_protocol_metadata_t metadata, sec_trust_t trust, sec_protocol_verify_complete_t complete) {
|
||||
complete(true);
|
||||
}, box_apple_tls_queue());
|
||||
} else if (anchors.count > 0 || anchor_only) {
|
||||
sec_protocol_options_set_verify_block(sec_options, ^(sec_protocol_metadata_t metadata, sec_trust_t trust, sec_protocol_verify_complete_t complete) {
|
||||
complete(box_evaluate_trust(trust, anchors, anchor_only));
|
||||
}, box_apple_tls_queue());
|
||||
}
|
||||
}, NW_PARAMETERS_DEFAULT_CONFIGURATION);
|
||||
|
||||
nw_connection_t connection = box_apple_tls_create_connection(connected_socket, parameters);
|
||||
if (connection == NULL) {
|
||||
close(connected_socket);
|
||||
box_set_error_message(error_out, "apple TLS: failed to create connection");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
box_apple_tls_client_t *client = calloc(1, sizeof(box_apple_tls_client_t));
|
||||
client->connection = (__bridge_retained void *)connection;
|
||||
client->ready_semaphore = (__bridge_retained void *)dispatch_semaphore_create(0);
|
||||
atomic_init(&client->ready, false);
|
||||
atomic_init(&client->ready_done, false);
|
||||
|
||||
nw_connection_set_state_changed_handler(connection, ^(nw_connection_state_t state, nw_error_t error) {
|
||||
if (atomic_load(&client->ready_done)) {
|
||||
return;
|
||||
}
|
||||
switch (state) {
|
||||
case nw_connection_state_ready:
|
||||
atomic_store(&client->ready, true);
|
||||
atomic_store(&client->ready_done, true);
|
||||
dispatch_semaphore_signal(box_apple_tls_ready_semaphore(client));
|
||||
break;
|
||||
case nw_connection_state_failed:
|
||||
case nw_connection_state_cancelled:
|
||||
box_set_error_from_nw_error(&client->ready_error, error);
|
||||
atomic_store(&client->ready_done, true);
|
||||
dispatch_semaphore_signal(box_apple_tls_ready_semaphore(client));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
nw_connection_set_queue(connection, box_apple_tls_queue());
|
||||
nw_connection_start(connection);
|
||||
return client;
|
||||
}
|
||||
|
||||
static int box_apple_tls_client_wait_ready(box_apple_tls_client_t *client, int timeout_msec, char **error_out) {
|
||||
dispatch_semaphore_t ready_semaphore = box_apple_tls_ready_semaphore(client);
|
||||
if (ready_semaphore == nil) {
|
||||
box_set_error_message(error_out, "apple TLS: invalid client");
|
||||
return 0;
|
||||
}
|
||||
if (!atomic_load(&client->ready_done)) {
|
||||
dispatch_time_t timeout = DISPATCH_TIME_FOREVER;
|
||||
if (timeout_msec >= 0) {
|
||||
timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC);
|
||||
}
|
||||
long wait_result = dispatch_semaphore_wait(ready_semaphore, timeout);
|
||||
if (wait_result != 0) {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
if (atomic_load(&client->ready)) {
|
||||
return 1;
|
||||
}
|
||||
if (client->ready_error != NULL) {
|
||||
if (error_out != NULL) {
|
||||
*error_out = client->ready_error;
|
||||
client->ready_error = NULL;
|
||||
} else {
|
||||
free(client->ready_error);
|
||||
client->ready_error = NULL;
|
||||
}
|
||||
} else {
|
||||
box_set_error_message(error_out, "apple TLS: handshake failed");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void box_apple_tls_client_cancel(box_apple_tls_client_t *client) {
|
||||
if (client == NULL) {
|
||||
return;
|
||||
}
|
||||
nw_connection_t connection = box_apple_tls_connection(client);
|
||||
if (connection != nil) {
|
||||
nw_connection_cancel(connection);
|
||||
}
|
||||
}
|
||||
|
||||
static void box_apple_tls_client_free(box_apple_tls_client_t *client) {
|
||||
if (client == NULL) {
|
||||
return;
|
||||
}
|
||||
free(client->ready_error);
|
||||
if (client->ready_semaphore != NULL) {
|
||||
CFBridgingRelease(client->ready_semaphore);
|
||||
}
|
||||
if (client->connection != NULL) {
|
||||
CFBridgingRelease(client->connection);
|
||||
}
|
||||
free(client);
|
||||
}
|
||||
|
||||
static ssize_t box_apple_tls_client_read(box_apple_tls_client_t *client, void *buffer, size_t buffer_len, bool *eof_out, char **error_out) {
|
||||
nw_connection_t connection = box_apple_tls_connection(client);
|
||||
if (connection == nil) {
|
||||
box_set_error_message(error_out, "apple TLS: invalid client");
|
||||
return -1;
|
||||
}
|
||||
|
||||
dispatch_semaphore_t read_semaphore = dispatch_semaphore_create(0);
|
||||
__block NSData *content_data = nil;
|
||||
__block bool read_eof = false;
|
||||
__block char *local_error = NULL;
|
||||
|
||||
nw_connection_receive(connection, 1, (uint32_t)buffer_len, ^(dispatch_data_t content, nw_content_context_t context, bool is_complete, nw_error_t error) {
|
||||
if (content != NULL) {
|
||||
const void *mapped = NULL;
|
||||
size_t mapped_len = 0;
|
||||
dispatch_data_t mapped_data = dispatch_data_create_map(content, &mapped, &mapped_len);
|
||||
if (mapped != NULL && mapped_len > 0) {
|
||||
content_data = [NSData dataWithBytes:mapped length:mapped_len];
|
||||
}
|
||||
(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_semaphore_wait(read_semaphore, DISPATCH_TIME_FOREVER);
|
||||
if (local_error != NULL) {
|
||||
if (error_out != NULL) {
|
||||
*error_out = local_error;
|
||||
} else {
|
||||
free(local_error);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
static ssize_t box_apple_tls_client_write(box_apple_tls_client_t *client, const void *buffer, size_t buffer_len, char **error_out) {
|
||||
nw_connection_t connection = box_apple_tls_connection(client);
|
||||
if (connection == nil) {
|
||||
box_set_error_message(error_out, "apple TLS: invalid client");
|
||||
return -1;
|
||||
}
|
||||
if (buffer_len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void *content_copy = malloc(buffer_len);
|
||||
memcpy(content_copy, buffer, buffer_len);
|
||||
dispatch_data_t content = dispatch_data_create(content_copy, buffer_len, box_apple_tls_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_semaphore_wait(write_semaphore, DISPATCH_TIME_FOREVER);
|
||||
if (local_error != NULL) {
|
||||
if (error_out != NULL) {
|
||||
*error_out = local_error;
|
||||
} else {
|
||||
free(local_error);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return (ssize_t)buffer_len;
|
||||
}
|
||||
|
||||
static bool box_apple_tls_client_copy_state(box_apple_tls_client_t *client, box_apple_tls_state_t *state, char **error_out) {
|
||||
nw_connection_t connection = box_apple_tls_connection(client);
|
||||
if (connection == nil) {
|
||||
box_set_error_message(error_out, "apple TLS: invalid client");
|
||||
return false;
|
||||
}
|
||||
memset(state, 0, sizeof(box_apple_tls_state_t));
|
||||
|
||||
nw_protocol_definition_t tls_definition = nw_protocol_copy_tls_definition();
|
||||
nw_protocol_metadata_t metadata = nw_connection_copy_protocol_metadata(connection, tls_definition);
|
||||
if (metadata == NULL || !nw_protocol_metadata_is_tls(metadata)) {
|
||||
box_set_error_message(error_out, "apple TLS: metadata unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
sec_protocol_metadata_t sec_metadata = nw_tls_copy_sec_protocol_metadata(metadata);
|
||||
state->version = (uint16_t)sec_protocol_metadata_get_negotiated_tls_protocol_version(sec_metadata);
|
||||
state->cipher_suite = (uint16_t)sec_protocol_metadata_get_negotiated_tls_ciphersuite(sec_metadata);
|
||||
|
||||
state->alpn = box_apple_tls_metadata_copy_negotiated_protocol(sec_metadata);
|
||||
state->server_name = box_apple_tls_metadata_copy_server_name(sec_metadata);
|
||||
|
||||
NSMutableData *chain_data = [NSMutableData data];
|
||||
sec_protocol_metadata_access_peer_certificate_chain(sec_metadata, ^(sec_certificate_t certificate) {
|
||||
SecCertificateRef certificate_ref = sec_certificate_copy_ref(certificate);
|
||||
if (certificate_ref == NULL) {
|
||||
return;
|
||||
}
|
||||
CFDataRef certificate_data = SecCertificateCopyData(certificate_ref);
|
||||
CFRelease(certificate_ref);
|
||||
if (certificate_data == NULL) {
|
||||
return;
|
||||
}
|
||||
uint32_t certificate_len = (uint32_t)CFDataGetLength(certificate_data);
|
||||
uint32_t network_len = htonl(certificate_len);
|
||||
[chain_data appendBytes:&network_len length:sizeof(network_len)];
|
||||
[chain_data appendBytes:CFDataGetBytePtr(certificate_data) length:certificate_len];
|
||||
CFRelease(certificate_data);
|
||||
});
|
||||
if (chain_data.length > 0) {
|
||||
state->peer_cert_chain = malloc(chain_data.length);
|
||||
memcpy(state->peer_cert_chain, chain_data.bytes, chain_data.length);
|
||||
state->peer_cert_chain_len = chain_data.length;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void box_apple_tls_state_free(box_apple_tls_state_t *state) {
|
||||
if (state == NULL) {
|
||||
return;
|
||||
}
|
||||
free(state->alpn);
|
||||
free(state->server_name);
|
||||
free(state->peer_cert_chain);
|
||||
}
|
||||
|
||||
#include "apple_client_platform.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
@@ -533,80 +26,12 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxConstant "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type appleCertificateStore interface {
|
||||
StoreKind() string
|
||||
CurrentPEM() []string
|
||||
}
|
||||
|
||||
type appleClientConfig struct {
|
||||
serverName string
|
||||
nextProtos []string
|
||||
handshakeTimeout time.Duration
|
||||
minVersion uint16
|
||||
maxVersion uint16
|
||||
insecure bool
|
||||
anchorPEM string
|
||||
anchorOnly bool
|
||||
certificatePublicKeySHA256 [][]byte
|
||||
}
|
||||
|
||||
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 {
|
||||
return &appleClientConfig{
|
||||
serverName: c.serverName,
|
||||
nextProtos: append([]string(nil), c.nextProtos...),
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
minVersion: c.minVersion,
|
||||
maxVersion: c.maxVersion,
|
||||
insecure: c.insecure,
|
||||
anchorPEM: c.anchorPEM,
|
||||
anchorOnly: c.anchorOnly,
|
||||
certificatePublicKeySHA256: append([][]byte(nil), c.certificatePublicKeySHA256...),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (Conn, error) {
|
||||
rawSyscallConn, ok := common.Cast[syscall.Conn](conn)
|
||||
if !ok {
|
||||
@@ -686,7 +111,7 @@ func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn)
|
||||
return nil, err
|
||||
}
|
||||
if len(c.certificatePublicKeySHA256) > 0 {
|
||||
err = verifyPublicKeySHA256(c.certificatePublicKeySHA256, rawCerts, nil)
|
||||
err = VerifyPublicKeySHA256(c.certificatePublicKeySHA256, rawCerts)
|
||||
if err != nil {
|
||||
C.box_apple_tls_client_cancel(client)
|
||||
C.box_apple_tls_client_free(client)
|
||||
@@ -908,120 +333,6 @@ func (c *appleTLSConn) ConnectionState() ConnectionState {
|
||||
return c.state
|
||||
}
|
||||
|
||||
func newAppleClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
if options.Reality != nil && options.Reality.Enabled {
|
||||
return nil, E.New("reality is unsupported in Apple TLS engine")
|
||||
}
|
||||
if options.UTLS != nil && options.UTLS.Enabled {
|
||||
return nil, E.New("utls is unsupported in Apple TLS engine")
|
||||
}
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
return nil, E.New("ech is unsupported in Apple TLS engine")
|
||||
}
|
||||
if options.DisableSNI {
|
||||
return nil, E.New("disable_sni is unsupported in Apple TLS engine")
|
||||
}
|
||||
if len(options.CipherSuites) > 0 {
|
||||
return nil, E.New("cipher_suites is unsupported in Apple TLS engine")
|
||||
}
|
||||
if len(options.CurvePreferences) > 0 {
|
||||
return nil, E.New("curve_preferences is unsupported in Apple TLS engine")
|
||||
}
|
||||
if len(options.ClientCertificate) > 0 || options.ClientCertificatePath != "" || len(options.ClientKey) > 0 || options.ClientKeyPath != "" {
|
||||
return nil, E.New("client certificate is unsupported in Apple TLS engine")
|
||||
}
|
||||
if options.Fragment || options.RecordFragment {
|
||||
return nil, E.New("tls fragment is unsupported in Apple TLS engine")
|
||||
}
|
||||
if options.KernelTx || options.KernelRx {
|
||||
return nil, E.New("ktls is unsupported in Apple TLS engine")
|
||||
}
|
||||
|
||||
var serverName string
|
||||
if options.ServerName != "" {
|
||||
serverName = options.ServerName
|
||||
} else if serverAddress != "" {
|
||||
serverName = serverAddress
|
||||
}
|
||||
if serverName == "" && !options.Insecure && !allowEmptyServerName {
|
||||
return nil, errMissingServerName
|
||||
}
|
||||
if len(options.CertificatePublicKeySHA256) > 0 && (len(options.Certificate) > 0 || options.CertificatePath != "") {
|
||||
return nil, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path")
|
||||
}
|
||||
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = boxConstant.TCPTimeout
|
||||
}
|
||||
|
||||
var minVersion uint16
|
||||
if options.MinVersion != "" {
|
||||
var err error
|
||||
minVersion, err = ParseTLSVersion(options.MinVersion)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse min_version")
|
||||
}
|
||||
}
|
||||
var maxVersion uint16
|
||||
if options.MaxVersion != "" {
|
||||
var err error
|
||||
maxVersion, err = ParseTLSVersion(options.MaxVersion)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse max_version")
|
||||
}
|
||||
}
|
||||
|
||||
anchorPEM, anchorOnly, err := appleAnchorPEM(ctx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &appleClientConfig{
|
||||
serverName: serverName,
|
||||
nextProtos: append([]string(nil), options.ALPN...),
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
minVersion: minVersion,
|
||||
maxVersion: maxVersion,
|
||||
insecure: options.Insecure || len(options.CertificatePublicKeySHA256) > 0,
|
||||
anchorPEM: anchorPEM,
|
||||
anchorOnly: anchorOnly,
|
||||
certificatePublicKeySHA256: append([][]byte(nil), options.CertificatePublicKeySHA256...),
|
||||
}, 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())
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
37
common/tls/apple_client_platform.h
Normal file
37
common/tls/apple_client_platform.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
|
||||
typedef struct box_apple_tls_client box_apple_tls_client_t;
|
||||
|
||||
typedef struct box_apple_tls_state {
|
||||
uint16_t version;
|
||||
uint16_t cipher_suite;
|
||||
char *alpn;
|
||||
char *server_name;
|
||||
uint8_t *peer_cert_chain;
|
||||
size_t peer_cert_chain_len;
|
||||
} box_apple_tls_state_t;
|
||||
|
||||
box_apple_tls_client_t *box_apple_tls_client_create(
|
||||
int connected_socket,
|
||||
const char *server_name,
|
||||
const char *alpn,
|
||||
size_t alpn_len,
|
||||
uint16_t min_version,
|
||||
uint16_t max_version,
|
||||
bool insecure,
|
||||
const char *anchor_pem,
|
||||
size_t anchor_pem_len,
|
||||
bool anchor_only,
|
||||
char **error_out
|
||||
);
|
||||
|
||||
int box_apple_tls_client_wait_ready(box_apple_tls_client_t *client, int timeout_msec, char **error_out);
|
||||
void box_apple_tls_client_cancel(box_apple_tls_client_t *client);
|
||||
void box_apple_tls_client_free(box_apple_tls_client_t *client);
|
||||
ssize_t box_apple_tls_client_read(box_apple_tls_client_t *client, void *buffer, size_t buffer_len, 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, 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);
|
||||
631
common/tls/apple_client_platform.m
Normal file
631
common/tls/apple_client_platform.m
Normal file
@@ -0,0 +1,631 @@
|
||||
#import "apple_client_platform.h"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Network/Network.h>
|
||||
#import <Security/Security.h>
|
||||
#import <Security/SecProtocolMetadata.h>
|
||||
#import <Security/SecProtocolOptions.h>
|
||||
#import <Security/SecProtocolTypes.h>
|
||||
#import <arpa/inet.h>
|
||||
#import <dlfcn.h>
|
||||
#import <dispatch/dispatch.h>
|
||||
#import <stdatomic.h>
|
||||
#import <stdlib.h>
|
||||
#import <string.h>
|
||||
#import <unistd.h>
|
||||
|
||||
typedef nw_connection_t _Nullable (*box_nw_connection_create_with_connected_socket_and_parameters_f)(int connected_socket, nw_parameters_t parameters);
|
||||
typedef const char * _Nullable (*box_sec_protocol_metadata_string_accessor_f)(sec_protocol_metadata_t metadata);
|
||||
|
||||
typedef struct box_apple_tls_client {
|
||||
void *connection;
|
||||
void *queue;
|
||||
void *ready_semaphore;
|
||||
atomic_int ref_count;
|
||||
atomic_bool ready;
|
||||
atomic_bool ready_done;
|
||||
char *ready_error;
|
||||
box_apple_tls_state_t state;
|
||||
} box_apple_tls_client_t;
|
||||
|
||||
static nw_connection_t box_apple_tls_connection(box_apple_tls_client_t *client) {
|
||||
if (client == NULL || client->connection == NULL) {
|
||||
return nil;
|
||||
}
|
||||
return (__bridge nw_connection_t)client->connection;
|
||||
}
|
||||
|
||||
static dispatch_queue_t box_apple_tls_client_queue(box_apple_tls_client_t *client) {
|
||||
if (client == NULL || client->queue == NULL) {
|
||||
return nil;
|
||||
}
|
||||
return (__bridge dispatch_queue_t)client->queue;
|
||||
}
|
||||
|
||||
static dispatch_semaphore_t box_apple_tls_ready_semaphore(box_apple_tls_client_t *client) {
|
||||
if (client == NULL || client->ready_semaphore == NULL) {
|
||||
return nil;
|
||||
}
|
||||
return (__bridge dispatch_semaphore_t)client->ready_semaphore;
|
||||
}
|
||||
|
||||
static void box_apple_tls_state_reset(box_apple_tls_state_t *state) {
|
||||
if (state == NULL) {
|
||||
return;
|
||||
}
|
||||
free(state->alpn);
|
||||
free(state->server_name);
|
||||
free(state->peer_cert_chain);
|
||||
memset(state, 0, sizeof(box_apple_tls_state_t));
|
||||
}
|
||||
|
||||
static void box_apple_tls_client_destroy(box_apple_tls_client_t *client) {
|
||||
free(client->ready_error);
|
||||
box_apple_tls_state_reset(&client->state);
|
||||
if (client->ready_semaphore != NULL) {
|
||||
CFBridgingRelease(client->ready_semaphore);
|
||||
}
|
||||
if (client->connection != NULL) {
|
||||
CFBridgingRelease(client->connection);
|
||||
}
|
||||
if (client->queue != NULL) {
|
||||
CFBridgingRelease(client->queue);
|
||||
}
|
||||
free(client);
|
||||
}
|
||||
|
||||
static void box_apple_tls_client_release(box_apple_tls_client_t *client) {
|
||||
if (client == NULL) {
|
||||
return;
|
||||
}
|
||||
if (atomic_fetch_sub(&client->ref_count, 1) == 1) {
|
||||
box_apple_tls_client_destroy(client);
|
||||
}
|
||||
}
|
||||
|
||||
static void box_set_error_string(char **error_out, NSString *message) {
|
||||
if (error_out == NULL || *error_out != NULL) {
|
||||
return;
|
||||
}
|
||||
const char *utf8 = [message UTF8String];
|
||||
*error_out = strdup(utf8 != NULL ? utf8 : "unknown error");
|
||||
}
|
||||
|
||||
static void box_set_error_message(char **error_out, const char *message) {
|
||||
if (error_out == NULL || *error_out != NULL) {
|
||||
return;
|
||||
}
|
||||
*error_out = strdup(message != NULL ? message : "unknown error");
|
||||
}
|
||||
|
||||
static void box_set_error_from_nw_error(char **error_out, nw_error_t error) {
|
||||
if (error == NULL) {
|
||||
box_set_error_message(error_out, "unknown network error");
|
||||
return;
|
||||
}
|
||||
CFErrorRef cfError = nw_error_copy_cf_error(error);
|
||||
if (cfError == NULL) {
|
||||
box_set_error_message(error_out, "unknown network error");
|
||||
return;
|
||||
}
|
||||
NSString *description = [(__bridge NSError *)cfError description];
|
||||
box_set_error_string(error_out, description);
|
||||
CFRelease(cfError);
|
||||
}
|
||||
|
||||
static char *box_apple_tls_metadata_copy_negotiated_protocol(sec_protocol_metadata_t metadata) {
|
||||
static box_sec_protocol_metadata_string_accessor_f copy_fn;
|
||||
static box_sec_protocol_metadata_string_accessor_f get_fn;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
copy_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_copy_negotiated_protocol");
|
||||
get_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_get_negotiated_protocol");
|
||||
});
|
||||
if (copy_fn != NULL) {
|
||||
return (char *)copy_fn(metadata);
|
||||
}
|
||||
if (get_fn != NULL) {
|
||||
const char *protocol = get_fn(metadata);
|
||||
if (protocol != NULL) {
|
||||
return strdup(protocol);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char *box_apple_tls_metadata_copy_server_name(sec_protocol_metadata_t metadata) {
|
||||
static box_sec_protocol_metadata_string_accessor_f copy_fn;
|
||||
static box_sec_protocol_metadata_string_accessor_f get_fn;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
copy_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_copy_server_name");
|
||||
get_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_get_server_name");
|
||||
});
|
||||
if (copy_fn != NULL) {
|
||||
return (char *)copy_fn(metadata);
|
||||
}
|
||||
if (get_fn != NULL) {
|
||||
const char *server_name = get_fn(metadata);
|
||||
if (server_name != NULL) {
|
||||
return strdup(server_name);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static NSArray<NSString *> *box_split_lines(const char *content, size_t content_len) {
|
||||
if (content == NULL || content_len == 0) {
|
||||
return @[];
|
||||
}
|
||||
NSString *string = [[NSString alloc] initWithBytes:content length:content_len encoding:NSUTF8StringEncoding];
|
||||
if (string == nil) {
|
||||
return @[];
|
||||
}
|
||||
NSMutableArray<NSString *> *lines = [NSMutableArray array];
|
||||
[string enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {
|
||||
if (line.length > 0) {
|
||||
[lines addObject:line];
|
||||
}
|
||||
}];
|
||||
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) {
|
||||
bool result = false;
|
||||
SecTrustRef trustRef = sec_trust_copy_ref(trust);
|
||||
if (trustRef == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (anchors.count > 0 || anchor_only) {
|
||||
CFMutableArrayRef anchorArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
|
||||
for (id certificate in anchors) {
|
||||
CFArrayAppendValue(anchorArray, (__bridge const void *)certificate);
|
||||
}
|
||||
SecTrustSetAnchorCertificates(trustRef, anchorArray);
|
||||
SecTrustSetAnchorCertificatesOnly(trustRef, anchor_only);
|
||||
CFRelease(anchorArray);
|
||||
}
|
||||
CFErrorRef error = NULL;
|
||||
result = SecTrustEvaluateWithError(trustRef, &error);
|
||||
if (error != NULL) {
|
||||
CFRelease(error);
|
||||
}
|
||||
CFRelease(trustRef);
|
||||
return result;
|
||||
}
|
||||
|
||||
static nw_connection_t box_apple_tls_create_connection(int connected_socket, nw_parameters_t parameters) {
|
||||
static box_nw_connection_create_with_connected_socket_and_parameters_f create_fn;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
char name[] = "sretemarap_dna_tekcos_detcennoc_htiw_etaerc_noitcennoc_wn";
|
||||
for (size_t i = 0, j = sizeof(name) - 2; i < j; i++, j--) {
|
||||
char t = name[i];
|
||||
name[i] = name[j];
|
||||
name[j] = t;
|
||||
}
|
||||
create_fn = (box_nw_connection_create_with_connected_socket_and_parameters_f)dlsym(RTLD_DEFAULT, name);
|
||||
});
|
||||
if (create_fn == NULL) {
|
||||
return nil;
|
||||
}
|
||||
return create_fn(connected_socket, parameters);
|
||||
}
|
||||
|
||||
static bool box_apple_tls_state_copy(const box_apple_tls_state_t *source, box_apple_tls_state_t *destination) {
|
||||
memset(destination, 0, sizeof(box_apple_tls_state_t));
|
||||
destination->version = source->version;
|
||||
destination->cipher_suite = source->cipher_suite;
|
||||
if (source->alpn != NULL) {
|
||||
destination->alpn = strdup(source->alpn);
|
||||
if (destination->alpn == NULL) {
|
||||
goto oom;
|
||||
}
|
||||
}
|
||||
if (source->server_name != NULL) {
|
||||
destination->server_name = strdup(source->server_name);
|
||||
if (destination->server_name == NULL) {
|
||||
goto oom;
|
||||
}
|
||||
}
|
||||
if (source->peer_cert_chain_len > 0) {
|
||||
destination->peer_cert_chain = malloc(source->peer_cert_chain_len);
|
||||
if (destination->peer_cert_chain == NULL) {
|
||||
goto oom;
|
||||
}
|
||||
memcpy(destination->peer_cert_chain, source->peer_cert_chain, source->peer_cert_chain_len);
|
||||
destination->peer_cert_chain_len = source->peer_cert_chain_len;
|
||||
}
|
||||
return true;
|
||||
|
||||
oom:
|
||||
box_apple_tls_state_reset(destination);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool box_apple_tls_state_load(nw_connection_t connection, box_apple_tls_state_t *state, char **error_out) {
|
||||
box_apple_tls_state_reset(state);
|
||||
if (connection == nil) {
|
||||
box_set_error_message(error_out, "apple TLS: invalid client");
|
||||
return false;
|
||||
}
|
||||
|
||||
nw_protocol_definition_t tls_definition = nw_protocol_copy_tls_definition();
|
||||
nw_protocol_metadata_t metadata = nw_connection_copy_protocol_metadata(connection, tls_definition);
|
||||
if (metadata == NULL || !nw_protocol_metadata_is_tls(metadata)) {
|
||||
box_set_error_message(error_out, "apple TLS: metadata unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
sec_protocol_metadata_t sec_metadata = nw_tls_copy_sec_protocol_metadata(metadata);
|
||||
if (sec_metadata == NULL) {
|
||||
box_set_error_message(error_out, "apple TLS: metadata unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
state->version = (uint16_t)sec_protocol_metadata_get_negotiated_tls_protocol_version(sec_metadata);
|
||||
state->cipher_suite = (uint16_t)sec_protocol_metadata_get_negotiated_tls_ciphersuite(sec_metadata);
|
||||
state->alpn = box_apple_tls_metadata_copy_negotiated_protocol(sec_metadata);
|
||||
state->server_name = box_apple_tls_metadata_copy_server_name(sec_metadata);
|
||||
|
||||
NSMutableData *chain_data = [NSMutableData data];
|
||||
sec_protocol_metadata_access_peer_certificate_chain(sec_metadata, ^(sec_certificate_t certificate) {
|
||||
SecCertificateRef certificate_ref = sec_certificate_copy_ref(certificate);
|
||||
if (certificate_ref == NULL) {
|
||||
return;
|
||||
}
|
||||
CFDataRef certificate_data = SecCertificateCopyData(certificate_ref);
|
||||
CFRelease(certificate_ref);
|
||||
if (certificate_data == NULL) {
|
||||
return;
|
||||
}
|
||||
uint32_t certificate_len = (uint32_t)CFDataGetLength(certificate_data);
|
||||
uint32_t network_len = htonl(certificate_len);
|
||||
[chain_data appendBytes:&network_len length:sizeof(network_len)];
|
||||
[chain_data appendBytes:CFDataGetBytePtr(certificate_data) length:certificate_len];
|
||||
CFRelease(certificate_data);
|
||||
});
|
||||
if (chain_data.length > 0) {
|
||||
state->peer_cert_chain = malloc(chain_data.length);
|
||||
if (state->peer_cert_chain == NULL) {
|
||||
box_set_error_message(error_out, "apple TLS: out of memory");
|
||||
box_apple_tls_state_reset(state);
|
||||
return false;
|
||||
}
|
||||
memcpy(state->peer_cert_chain, chain_data.bytes, chain_data.length);
|
||||
state->peer_cert_chain_len = chain_data.length;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
box_apple_tls_client_t *box_apple_tls_client_create(
|
||||
int connected_socket,
|
||||
const char *server_name,
|
||||
const char *alpn,
|
||||
size_t alpn_len,
|
||||
uint16_t min_version,
|
||||
uint16_t max_version,
|
||||
bool insecure,
|
||||
const char *anchor_pem,
|
||||
size_t anchor_pem_len,
|
||||
bool anchor_only,
|
||||
char **error_out
|
||||
) {
|
||||
box_apple_tls_client_t *client = calloc(1, sizeof(box_apple_tls_client_t));
|
||||
if (client == NULL) {
|
||||
close(connected_socket);
|
||||
box_set_error_message(error_out, "apple TLS: out of memory");
|
||||
return NULL;
|
||||
}
|
||||
client->queue = (__bridge_retained void *)dispatch_queue_create("sing-box.apple-private-tls", DISPATCH_QUEUE_SERIAL);
|
||||
client->ready_semaphore = (__bridge_retained void *)dispatch_semaphore_create(0);
|
||||
atomic_init(&client->ref_count, 1);
|
||||
atomic_init(&client->ready, false);
|
||||
atomic_init(&client->ready_done, false);
|
||||
|
||||
NSArray<NSString *> *alpnList = box_split_lines(alpn, alpn_len);
|
||||
NSArray *anchors = box_parse_certificates_from_pem(anchor_pem, anchor_pem_len);
|
||||
nw_parameters_t parameters = nw_parameters_create_secure_tcp(^(nw_protocol_options_t tls_options) {
|
||||
sec_protocol_options_t sec_options = nw_tls_copy_sec_protocol_options(tls_options);
|
||||
if (min_version != 0) {
|
||||
sec_protocol_options_set_min_tls_protocol_version(sec_options, (tls_protocol_version_t)min_version);
|
||||
}
|
||||
if (max_version != 0) {
|
||||
sec_protocol_options_set_max_tls_protocol_version(sec_options, (tls_protocol_version_t)max_version);
|
||||
}
|
||||
if (server_name != NULL && server_name[0] != '\0') {
|
||||
sec_protocol_options_set_tls_server_name(sec_options, server_name);
|
||||
}
|
||||
for (NSString *protocol in alpnList) {
|
||||
sec_protocol_options_add_tls_application_protocol(sec_options, protocol.UTF8String);
|
||||
}
|
||||
sec_protocol_options_set_peer_authentication_required(sec_options, !insecure);
|
||||
if (insecure) {
|
||||
sec_protocol_options_set_verify_block(sec_options, ^(sec_protocol_metadata_t metadata, sec_trust_t trust, sec_protocol_verify_complete_t complete) {
|
||||
complete(true);
|
||||
}, box_apple_tls_client_queue(client));
|
||||
} else if (anchors.count > 0 || anchor_only) {
|
||||
sec_protocol_options_set_verify_block(sec_options, ^(sec_protocol_metadata_t metadata, sec_trust_t trust, sec_protocol_verify_complete_t complete) {
|
||||
complete(box_evaluate_trust(trust, anchors, anchor_only));
|
||||
}, box_apple_tls_client_queue(client));
|
||||
}
|
||||
}, NW_PARAMETERS_DEFAULT_CONFIGURATION);
|
||||
|
||||
nw_connection_t connection = box_apple_tls_create_connection(connected_socket, parameters);
|
||||
if (connection == NULL) {
|
||||
close(connected_socket);
|
||||
if (client->ready_semaphore != NULL) {
|
||||
CFBridgingRelease(client->ready_semaphore);
|
||||
}
|
||||
if (client->queue != NULL) {
|
||||
CFBridgingRelease(client->queue);
|
||||
}
|
||||
free(client);
|
||||
box_set_error_message(error_out, "apple TLS: failed to create connection");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
client->connection = (__bridge_retained void *)connection;
|
||||
atomic_fetch_add(&client->ref_count, 1);
|
||||
|
||||
nw_connection_set_state_changed_handler(connection, ^(nw_connection_state_t state, nw_error_t error) {
|
||||
switch (state) {
|
||||
case nw_connection_state_ready:
|
||||
if (!atomic_load(&client->ready_done)) {
|
||||
atomic_store(&client->ready, box_apple_tls_state_load(connection, &client->state, &client->ready_error));
|
||||
atomic_store(&client->ready_done, true);
|
||||
dispatch_semaphore_signal(box_apple_tls_ready_semaphore(client));
|
||||
}
|
||||
break;
|
||||
case nw_connection_state_failed:
|
||||
if (!atomic_load(&client->ready_done)) {
|
||||
box_set_error_from_nw_error(&client->ready_error, error);
|
||||
atomic_store(&client->ready_done, true);
|
||||
dispatch_semaphore_signal(box_apple_tls_ready_semaphore(client));
|
||||
}
|
||||
break;
|
||||
case nw_connection_state_cancelled:
|
||||
if (!atomic_load(&client->ready_done)) {
|
||||
box_set_error_from_nw_error(&client->ready_error, error);
|
||||
atomic_store(&client->ready_done, true);
|
||||
dispatch_semaphore_signal(box_apple_tls_ready_semaphore(client));
|
||||
}
|
||||
box_apple_tls_client_release(client);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
nw_connection_set_queue(connection, box_apple_tls_client_queue(client));
|
||||
nw_connection_start(connection);
|
||||
return client;
|
||||
}
|
||||
|
||||
int box_apple_tls_client_wait_ready(box_apple_tls_client_t *client, int timeout_msec, char **error_out) {
|
||||
dispatch_semaphore_t ready_semaphore = box_apple_tls_ready_semaphore(client);
|
||||
if (ready_semaphore == nil) {
|
||||
box_set_error_message(error_out, "apple TLS: invalid client");
|
||||
return 0;
|
||||
}
|
||||
if (!atomic_load(&client->ready_done)) {
|
||||
dispatch_time_t timeout = DISPATCH_TIME_FOREVER;
|
||||
if (timeout_msec >= 0) {
|
||||
timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC);
|
||||
}
|
||||
long wait_result = dispatch_semaphore_wait(ready_semaphore, timeout);
|
||||
if (wait_result != 0) {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
if (atomic_load(&client->ready)) {
|
||||
return 1;
|
||||
}
|
||||
if (client->ready_error != NULL) {
|
||||
if (error_out != NULL) {
|
||||
*error_out = client->ready_error;
|
||||
client->ready_error = NULL;
|
||||
} else {
|
||||
free(client->ready_error);
|
||||
client->ready_error = NULL;
|
||||
}
|
||||
} else {
|
||||
box_set_error_message(error_out, "apple TLS: handshake failed");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void box_apple_tls_client_cancel(box_apple_tls_client_t *client) {
|
||||
if (client == NULL) {
|
||||
return;
|
||||
}
|
||||
nw_connection_t connection = box_apple_tls_connection(client);
|
||||
if (connection != nil) {
|
||||
nw_connection_cancel(connection);
|
||||
}
|
||||
}
|
||||
|
||||
void box_apple_tls_client_free(box_apple_tls_client_t *client) {
|
||||
if (client == NULL) {
|
||||
return;
|
||||
}
|
||||
nw_connection_t connection = box_apple_tls_connection(client);
|
||||
if (connection != nil) {
|
||||
nw_connection_cancel(connection);
|
||||
}
|
||||
box_apple_tls_client_release(client);
|
||||
}
|
||||
|
||||
ssize_t box_apple_tls_client_read(box_apple_tls_client_t *client, void *buffer, size_t buffer_len, bool *eof_out, char **error_out) {
|
||||
nw_connection_t connection = box_apple_tls_connection(client);
|
||||
if (connection == nil) {
|
||||
box_set_error_message(error_out, "apple TLS: invalid client");
|
||||
return -1;
|
||||
}
|
||||
|
||||
dispatch_semaphore_t read_semaphore = dispatch_semaphore_create(0);
|
||||
__block NSData *content_data = nil;
|
||||
__block bool read_eof = false;
|
||||
__block char *local_error = NULL;
|
||||
|
||||
nw_connection_receive(connection, 1, (uint32_t)buffer_len, ^(dispatch_data_t content, nw_content_context_t context, bool is_complete, nw_error_t error) {
|
||||
if (content != NULL) {
|
||||
const void *mapped = NULL;
|
||||
size_t mapped_len = 0;
|
||||
dispatch_data_t mapped_data = dispatch_data_create_map(content, &mapped, &mapped_len);
|
||||
if (mapped != NULL && mapped_len > 0) {
|
||||
content_data = [NSData dataWithBytes:mapped length:mapped_len];
|
||||
}
|
||||
(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_semaphore_wait(read_semaphore, DISPATCH_TIME_FOREVER);
|
||||
if (local_error != NULL) {
|
||||
if (error_out != NULL) {
|
||||
*error_out = local_error;
|
||||
} else {
|
||||
free(local_error);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
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, char **error_out) {
|
||||
nw_connection_t connection = box_apple_tls_connection(client);
|
||||
if (connection == nil) {
|
||||
box_set_error_message(error_out, "apple TLS: invalid client");
|
||||
return -1;
|
||||
}
|
||||
if (buffer_len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void *content_copy = malloc(buffer_len);
|
||||
dispatch_queue_t queue = box_apple_tls_client_queue(client);
|
||||
if (content_copy == NULL) {
|
||||
free(content_copy);
|
||||
box_set_error_message(error_out, "apple TLS: out of memory");
|
||||
return -1;
|
||||
}
|
||||
if (queue == nil) {
|
||||
free(content_copy);
|
||||
box_set_error_message(error_out, "apple TLS: invalid client");
|
||||
return -1;
|
||||
}
|
||||
memcpy(content_copy, buffer, buffer_len);
|
||||
dispatch_data_t content = dispatch_data_create(content_copy, buffer_len, queue, ^{
|
||||
free(content_copy);
|
||||
});
|
||||
|
||||
dispatch_semaphore_t write_semaphore = dispatch_semaphore_create(0);
|
||||
__block char *local_error = NULL;
|
||||
|
||||
nw_connection_send(connection, content, NW_CONNECTION_DEFAULT_STREAM_CONTEXT, false, ^(nw_error_t error) {
|
||||
if (error != NULL) {
|
||||
box_set_error_from_nw_error(&local_error, error);
|
||||
}
|
||||
dispatch_semaphore_signal(write_semaphore);
|
||||
});
|
||||
|
||||
dispatch_semaphore_wait(write_semaphore, DISPATCH_TIME_FOREVER);
|
||||
if (local_error != NULL) {
|
||||
if (error_out != NULL) {
|
||||
*error_out = local_error;
|
||||
} else {
|
||||
free(local_error);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return (ssize_t)buffer_len;
|
||||
}
|
||||
|
||||
bool box_apple_tls_client_copy_state(box_apple_tls_client_t *client, box_apple_tls_state_t *state, char **error_out) {
|
||||
dispatch_queue_t queue = box_apple_tls_client_queue(client);
|
||||
if (queue == nil || state == NULL) {
|
||||
box_set_error_message(error_out, "apple TLS: invalid client");
|
||||
return false;
|
||||
}
|
||||
memset(state, 0, sizeof(box_apple_tls_state_t));
|
||||
__block bool copied = false;
|
||||
__block char *local_error = NULL;
|
||||
dispatch_sync(queue, ^{
|
||||
if (!atomic_load(&client->ready)) {
|
||||
box_set_error_message(&local_error, "apple TLS: metadata unavailable");
|
||||
return;
|
||||
}
|
||||
if (!box_apple_tls_state_copy(&client->state, state)) {
|
||||
box_set_error_message(&local_error, "apple TLS: out of memory");
|
||||
return;
|
||||
}
|
||||
copied = true;
|
||||
});
|
||||
if (copied) {
|
||||
return true;
|
||||
}
|
||||
if (local_error != NULL) {
|
||||
if (error_out != NULL) {
|
||||
*error_out = local_error;
|
||||
} else {
|
||||
free(local_error);
|
||||
}
|
||||
}
|
||||
box_apple_tls_state_reset(state);
|
||||
return false;
|
||||
}
|
||||
|
||||
void box_apple_tls_state_free(box_apple_tls_state_t *state) {
|
||||
box_apple_tls_state_reset(state);
|
||||
}
|
||||
@@ -16,6 +16,11 @@ import (
|
||||
|
||||
const appleTLSTestTimeout = 5 * time.Second
|
||||
|
||||
const (
|
||||
appleTLSSuccessHandshakeLoops = 20
|
||||
appleTLSFailureRecoveryLoops = 10
|
||||
)
|
||||
|
||||
type appleTLSServerResult struct {
|
||||
state stdtls.ConnectionState
|
||||
err error
|
||||
@@ -23,44 +28,48 @@ type appleTLSServerResult struct {
|
||||
|
||||
func TestAppleClientHandshakeAppliesALPNAndVersion(t *testing.T) {
|
||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
||||
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
||||
Certificates: []stdtls.Certificate{serverCertificate},
|
||||
MinVersion: stdtls.VersionTLS12,
|
||||
MaxVersion: stdtls.VersionTLS12,
|
||||
NextProtos: []string{"h2"},
|
||||
})
|
||||
for index := 0; index < appleTLSSuccessHandshakeLoops; index++ {
|
||||
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
||||
Certificates: []stdtls.Certificate{serverCertificate},
|
||||
MinVersion: stdtls.VersionTLS12,
|
||||
MaxVersion: stdtls.VersionTLS12,
|
||||
NextProtos: []string{"h2"},
|
||||
})
|
||||
|
||||
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
Engine: "apple",
|
||||
ServerName: "localhost",
|
||||
MinVersion: "1.2",
|
||||
MaxVersion: "1.2",
|
||||
ALPN: badoption.Listable[string]{"h2"},
|
||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer clientConn.Close()
|
||||
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
Engine: "apple",
|
||||
ServerName: "localhost",
|
||||
MinVersion: "1.2",
|
||||
MaxVersion: "1.2",
|
||||
ALPN: badoption.Listable[string]{"h2"},
|
||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("iteration %d: %v", index, err)
|
||||
}
|
||||
|
||||
clientState := clientConn.ConnectionState()
|
||||
if clientState.Version != stdtls.VersionTLS12 {
|
||||
t.Fatalf("unexpected negotiated version: %x", clientState.Version)
|
||||
}
|
||||
if clientState.NegotiatedProtocol != "h2" {
|
||||
t.Fatalf("unexpected negotiated protocol: %q", clientState.NegotiatedProtocol)
|
||||
}
|
||||
clientState := clientConn.ConnectionState()
|
||||
if clientState.Version != stdtls.VersionTLS12 {
|
||||
_ = clientConn.Close()
|
||||
t.Fatalf("iteration %d: unexpected negotiated version: %x", index, clientState.Version)
|
||||
}
|
||||
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.Fatal(result.err)
|
||||
}
|
||||
if result.state.Version != stdtls.VersionTLS12 {
|
||||
t.Fatalf("server negotiated unexpected version: %x", result.state.Version)
|
||||
}
|
||||
if result.state.NegotiatedProtocol != "h2" {
|
||||
t.Fatalf("server negotiated unexpected protocol: %q", result.state.NegotiatedProtocol)
|
||||
result := <-serverResult
|
||||
if result.err != nil {
|
||||
t.Fatalf("iteration %d: %v", index, result.err)
|
||||
}
|
||||
if result.state.Version != stdtls.VersionTLS12 {
|
||||
t.Fatalf("iteration %d: server negotiated unexpected version: %x", index, result.state.Version)
|
||||
}
|
||||
if result.state.NegotiatedProtocol != "h2" {
|
||||
t.Fatalf("iteration %d: server negotiated unexpected protocol: %q", index, result.state.NegotiatedProtocol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +120,93 @@ func TestAppleClientHandshakeRejectsServerNameMismatch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppleClientHandshakeRecoversAfterFailure(t *testing.T) {
|
||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
||||
testCases := []struct {
|
||||
name string
|
||||
serverConfig *stdtls.Config
|
||||
clientOptions option.OutboundTLSOptions
|
||||
}{
|
||||
{
|
||||
name: "version mismatch",
|
||||
serverConfig: &stdtls.Config{
|
||||
Certificates: []stdtls.Certificate{serverCertificate},
|
||||
MinVersion: stdtls.VersionTLS13,
|
||||
MaxVersion: stdtls.VersionTLS13,
|
||||
},
|
||||
clientOptions: option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
Engine: "apple",
|
||||
ServerName: "localhost",
|
||||
MaxVersion: "1.2",
|
||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "server name mismatch",
|
||||
serverConfig: &stdtls.Config{
|
||||
Certificates: []stdtls.Certificate{serverCertificate},
|
||||
},
|
||||
clientOptions: option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
Engine: "apple",
|
||||
ServerName: "example.com",
|
||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
||||
},
|
||||
},
|
||||
}
|
||||
successClientOptions := option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
Engine: "apple",
|
||||
ServerName: "localhost",
|
||||
MinVersion: "1.2",
|
||||
MaxVersion: "1.2",
|
||||
ALPN: badoption.Listable[string]{"h2"},
|
||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
for index := 0; index < appleTLSFailureRecoveryLoops; index++ {
|
||||
failedResult, failedAddress := startAppleTLSTestServer(t, testCase.serverConfig)
|
||||
failedConn, err := newAppleTestClientConn(t, failedAddress, testCase.clientOptions)
|
||||
if err == nil {
|
||||
_ = failedConn.Close()
|
||||
t.Fatalf("iteration %d: expected handshake failure", index)
|
||||
}
|
||||
if result := <-failedResult; result.err == nil {
|
||||
t.Fatalf("iteration %d: expected server handshake failure", index)
|
||||
}
|
||||
|
||||
successResult, successAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
||||
Certificates: []stdtls.Certificate{serverCertificate},
|
||||
MinVersion: stdtls.VersionTLS12,
|
||||
MaxVersion: stdtls.VersionTLS12,
|
||||
NextProtos: []string{"h2"},
|
||||
})
|
||||
successConn, err := newAppleTestClientConn(t, successAddress, successClientOptions)
|
||||
if err != nil {
|
||||
t.Fatalf("iteration %d: follow-up handshake failed: %v", index, err)
|
||||
}
|
||||
clientState := successConn.ConnectionState()
|
||||
if clientState.NegotiatedProtocol != "h2" {
|
||||
_ = successConn.Close()
|
||||
t.Fatalf("iteration %d: unexpected negotiated protocol after failure: %q", index, clientState.NegotiatedProtocol)
|
||||
}
|
||||
_ = successConn.Close()
|
||||
|
||||
result := <-successResult
|
||||
if result.err != nil {
|
||||
t.Fatalf("iteration %d: follow-up server handshake failed: %v", index, result.err)
|
||||
}
|
||||
if result.state.NegotiatedProtocol != "h2" {
|
||||
t.Fatalf("iteration %d: follow-up server negotiated unexpected protocol: %q", index, result.state.NegotiatedProtocol)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newAppleTestCertificate(t *testing.T, serverName string) (stdtls.Certificate, string) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/common/badtls"
|
||||
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/logger"
|
||||
@@ -65,8 +66,8 @@ func NewClientWithOptions(options ClientOptions) (Config, error) {
|
||||
options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx")
|
||||
}
|
||||
switch options.Options.Engine {
|
||||
case "", "go":
|
||||
case "apple":
|
||||
case C.TLSEngineDefault, "go":
|
||||
case C.TLSEngineApple:
|
||||
return newAppleClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
||||
default:
|
||||
return nil, E.New("unknown tls engine: ", options.Options.Engine)
|
||||
|
||||
@@ -131,7 +131,7 @@ func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
|
||||
}
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||
return verifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts, tlsConfig.Time)
|
||||
return VerifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts)
|
||||
}
|
||||
}
|
||||
if len(options.ALPN) > 0 {
|
||||
@@ -272,7 +272,7 @@ func verifyConnection(rootCAs *x509.CertPool, timeFunc func() time.Time, serverN
|
||||
}
|
||||
}
|
||||
|
||||
func verifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte, timeFunc func() time.Time) error {
|
||||
func VerifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte) error {
|
||||
leafCertificate, err := x509.ParseCertificate(rawCerts[0])
|
||||
if err != nil {
|
||||
return E.Cause(err, "failed to parse leaf certificate")
|
||||
|
||||
@@ -472,7 +472,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
||||
tlsConfig.ClientAuth = tls.RequestClientCert
|
||||
}
|
||||
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||
return verifyPublicKeySHA256(options.ClientCertificatePublicKeySHA256, rawCerts, tlsConfig.Time)
|
||||
return VerifyPublicKeySHA256(options.ClientCertificatePublicKeySHA256, rawCerts)
|
||||
}
|
||||
} else {
|
||||
return nil, E.New("missing client_certificate, client_certificate_path or client_certificate_public_key_sha256 for client authentication")
|
||||
|
||||
@@ -206,7 +206,7 @@ func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre
|
||||
}
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||
return verifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts, tlsConfig.Time)
|
||||
return VerifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts)
|
||||
}
|
||||
}
|
||||
if len(options.ALPN) > 0 {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
package constant
|
||||
|
||||
const ACMETLS1Protocol = "acme-tls/1"
|
||||
|
||||
const (
|
||||
TLSEngineDefault = ""
|
||||
TLSEngineApple = "apple"
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -13,7 +14,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/httpclient"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
@@ -27,9 +27,9 @@ import (
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const MimeType = "application/dns-message"
|
||||
@@ -43,37 +43,20 @@ func RegisterHTTPS(registry *dns.TransportRegistry) {
|
||||
type HTTPSTransport struct {
|
||||
dns.TransportAdapter
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
method string
|
||||
host string
|
||||
queryHeaders http.Header
|
||||
transportAccess sync.Mutex
|
||||
transport *httpclient.Client
|
||||
transport adapter.HTTPTransport
|
||||
transportResetAt time.Time
|
||||
}
|
||||
|
||||
func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
remoteOptions := option.RemoteDNSServerOptions{
|
||||
DNSServerAddressOptions: options.DNSServerAddressOptions,
|
||||
}
|
||||
remoteOptions.DialerOptions = options.DialerOptions
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, remoteOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
||||
tlsOptions.Enabled = true
|
||||
tlsConfig, err := tls.NewClient(ctx, logger, options.Server, tlsOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tlsConfig.NextProtos()) == 0 {
|
||||
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS})
|
||||
} else if !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
||||
tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, tlsConfig.NextProtos()...))
|
||||
}
|
||||
headers := options.Headers.Build()
|
||||
host := headers.Get("Host")
|
||||
headers.Del("Host")
|
||||
headers.Set("Accept", MimeType)
|
||||
serverAddr := options.DNSServerAddressOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 443
|
||||
@@ -89,7 +72,7 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
if path == "" {
|
||||
path = "/dns-query"
|
||||
}
|
||||
err = sHTTP.URLSetPath(&destinationURL, path)
|
||||
err := sHTTP.URLSetPath(&destinationURL, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -102,17 +85,38 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
default:
|
||||
return nil, E.New("unsupported HTTPS DNS method: ", options.Method)
|
||||
}
|
||||
if method == http.MethodPost {
|
||||
headers.Set("Content-Type", MimeType)
|
||||
}
|
||||
httpClientOptions := options.HTTPClientOptions
|
||||
return NewHTTPRaw(
|
||||
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, remoteOptions),
|
||||
logger,
|
||||
transportDialer,
|
||||
&destinationURL,
|
||||
headers,
|
||||
tlsConfig,
|
||||
httpClientOptions,
|
||||
method,
|
||||
)
|
||||
tlsOptions := common.PtrValueOrDefault(httpClientOptions.TLS)
|
||||
tlsOptions.Enabled = true
|
||||
httpClientOptions.TLS = &tlsOptions
|
||||
httpClientOptions.Tag = ""
|
||||
httpClientOptions.Headers = nil
|
||||
if options.ServerIsDomain() {
|
||||
httpClientOptions.DirectResolver = true
|
||||
}
|
||||
httpClientManager := service.FromContext[adapter.HTTPClientManager](ctx)
|
||||
transport, err := httpClientManager.ResolveTransport(ctx, logger, httpClientOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remoteOptions := option.RemoteDNSServerOptions{
|
||||
RawLocalDNSServerOptions: option.RawLocalDNSServerOptions{
|
||||
DialerOptions: options.DialerOptions,
|
||||
},
|
||||
DNSServerAddressOptions: options.DNSServerAddressOptions,
|
||||
}
|
||||
return &HTTPSTransport{
|
||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, remoteOptions),
|
||||
logger: logger,
|
||||
destination: &destinationURL,
|
||||
method: method,
|
||||
host: host,
|
||||
queryHeaders: headers,
|
||||
transport: transport,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewHTTPRaw(
|
||||
@@ -122,32 +126,25 @@ func NewHTTPRaw(
|
||||
destination *url.URL,
|
||||
headers http.Header,
|
||||
tlsConfig tls.Config,
|
||||
httpClientOptions option.HTTPClientOptions,
|
||||
method string,
|
||||
) (*HTTPSTransport, error) {
|
||||
if destination.Scheme == "https" && tlsConfig == nil {
|
||||
return nil, E.New("TLS transport unavailable")
|
||||
}
|
||||
queryHeaders := headers.Clone()
|
||||
if queryHeaders == nil {
|
||||
queryHeaders = make(http.Header)
|
||||
}
|
||||
host := queryHeaders.Get("Host")
|
||||
queryHeaders.Del("Host")
|
||||
queryHeaders.Set("Accept", MimeType)
|
||||
if method == http.MethodPost {
|
||||
queryHeaders.Set("Content-Type", MimeType)
|
||||
}
|
||||
httpClientOptions.Tag = ""
|
||||
httpClientOptions.Headers = nil
|
||||
currentTransport, err := httpclient.NewClientWithDialer(dialer, tlsConfig, "", httpClientOptions)
|
||||
currentTransport, err := httpclient.NewTransportWithDialer(dialer, tlsConfig, "", option.HTTPClientOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &HTTPSTransport{
|
||||
TransportAdapter: adapter,
|
||||
logger: logger,
|
||||
dialer: dialer,
|
||||
destination: destination,
|
||||
method: method,
|
||||
host: host,
|
||||
@@ -160,22 +157,30 @@ func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
return dialer.InitializeDetour(t.dialer)
|
||||
return httpclient.InitializeDetour(t.transport)
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) Close() error {
|
||||
t.transportAccess.Lock()
|
||||
defer t.transportAccess.Unlock()
|
||||
t.transport.CloseIdleConnections()
|
||||
t.transport = t.transport.Clone()
|
||||
return nil
|
||||
if t.transport == nil {
|
||||
return nil
|
||||
}
|
||||
err := t.transport.Close()
|
||||
t.transport = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) Reset() {
|
||||
t.transportAccess.Lock()
|
||||
defer t.transportAccess.Unlock()
|
||||
t.transport.CloseIdleConnections()
|
||||
t.transport = t.transport.Clone()
|
||||
if t.transport == nil {
|
||||
return
|
||||
}
|
||||
oldTransport := t.transport
|
||||
oldTransport.CloseIdleConnections()
|
||||
// Close is intentionally avoided here because some Clone implementations share transport state.
|
||||
t.transport = oldTransport.Clone()
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
@@ -185,11 +190,12 @@ func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
t.transportAccess.Lock()
|
||||
defer t.transportAccess.Unlock()
|
||||
if t.transportResetAt.After(startAt) {
|
||||
if t.transport == nil || t.transportResetAt.After(startAt) {
|
||||
return nil, err
|
||||
}
|
||||
t.transport.CloseIdleConnections()
|
||||
t.transport = t.transport.Clone()
|
||||
oldTransport := t.transport
|
||||
oldTransport.CloseIdleConnections()
|
||||
t.transport = oldTransport.Clone()
|
||||
t.transportResetAt = time.Now()
|
||||
}
|
||||
return nil, err
|
||||
@@ -229,6 +235,10 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
|
||||
t.transportAccess.Lock()
|
||||
currentTransport := t.transport
|
||||
t.transportAccess.Unlock()
|
||||
if currentTransport == nil {
|
||||
requestBuffer.Release()
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
response, err := currentTransport.RoundTrip(request)
|
||||
requestBuffer.Release()
|
||||
if err != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ When object:
|
||||
|
||||
```json
|
||||
{
|
||||
"engine": "",
|
||||
"version": 0,
|
||||
"disable_version_fallback": false,
|
||||
"headers": {},
|
||||
@@ -28,6 +29,50 @@ When object:
|
||||
|
||||
### Fields
|
||||
|
||||
#### engine
|
||||
|
||||
HTTP engine to use.
|
||||
|
||||
Values:
|
||||
|
||||
* `go` (default)
|
||||
* `apple`
|
||||
|
||||
`apple` uses NSURLSession, only available on Apple platforms.
|
||||
|
||||
!!! warning ""
|
||||
|
||||
Experimental only: due to the high memory overhead of both CGO and Network.framework,
|
||||
do not use in hot paths on iOS and tvOS.
|
||||
|
||||
Supported fields:
|
||||
|
||||
* `headers`
|
||||
* `tls.server_name` (must match request host)
|
||||
* `tls.insecure`
|
||||
* `tls.min_version` / `tls.max_version`
|
||||
* `tls.certificate` / `tls.certificate_path`
|
||||
* `tls.certificate_public_key_sha256`
|
||||
* Dial Fields
|
||||
|
||||
Unsupported fields:
|
||||
|
||||
* `version`
|
||||
* `disable_version_fallback`
|
||||
* HTTP2 Fields
|
||||
* QUIC Fields
|
||||
* `tls.engine`
|
||||
* `tls.alpn`
|
||||
* `tls.disable_sni`
|
||||
* `tls.cipher_suites`
|
||||
* `tls.curve_preferences`
|
||||
* `tls.client_certificate` / `tls.client_certificate_path` / `tls.client_key` / `tls.client_key_path`
|
||||
* `tls.fragment` / `tls.record_fragment`
|
||||
* `tls.kernel_tx` / `tls.kernel_rx`
|
||||
* `tls.ech`
|
||||
* `tls.utls`
|
||||
* `tls.reality`
|
||||
|
||||
#### version
|
||||
|
||||
HTTP version.
|
||||
|
||||
@@ -14,6 +14,7 @@ icon: material/new-box
|
||||
|
||||
```json
|
||||
{
|
||||
"engine": "",
|
||||
"version": 0,
|
||||
"disable_version_fallback": false,
|
||||
"headers": {},
|
||||
@@ -28,6 +29,50 @@ icon: material/new-box
|
||||
|
||||
### 字段
|
||||
|
||||
#### engine
|
||||
|
||||
要使用的 HTTP 引擎。
|
||||
|
||||
可用值:
|
||||
|
||||
* `go`(默认)
|
||||
* `apple`
|
||||
|
||||
`apple` 使用 NSURLSession,仅在 Apple 平台可用。
|
||||
|
||||
!!! warning ""
|
||||
|
||||
仅供实验用途:由于 CGO 和 Network.framework 占用的内存都很多,
|
||||
不应在 iOS 和 tvOS 的热路径中使用。
|
||||
|
||||
支持的字段:
|
||||
|
||||
* `headers`
|
||||
* `tls.server_name`(必须与请求主机匹配)
|
||||
* `tls.insecure`
|
||||
* `tls.min_version` / `tls.max_version`
|
||||
* `tls.certificate` / `tls.certificate_path`
|
||||
* `tls.certificate_public_key_sha256`
|
||||
* 拨号字段
|
||||
|
||||
不支持的字段:
|
||||
|
||||
* `version`
|
||||
* `disable_version_fallback`
|
||||
* HTTP2 字段
|
||||
* QUIC 字段
|
||||
* `tls.engine`
|
||||
* `tls.alpn`
|
||||
* `tls.disable_sni`
|
||||
* `tls.cipher_suites`
|
||||
* `tls.curve_preferences`
|
||||
* `tls.client_certificate` / `tls.client_certificate_path` / `tls.client_key` / `tls.client_key_path`
|
||||
* `tls.fragment` / `tls.record_fragment`
|
||||
* `tls.kernel_tx` / `tls.kernel_rx`
|
||||
* `tls.ech`
|
||||
* `tls.utls`
|
||||
* `tls.reality`
|
||||
|
||||
#### version
|
||||
|
||||
HTTP 版本。
|
||||
|
||||
@@ -197,7 +197,7 @@ TLS engine to use.
|
||||
|
||||
Values:
|
||||
|
||||
* `go`
|
||||
* `go` (default)
|
||||
* `apple`
|
||||
|
||||
`apple` uses Network.framework, only available on Apple platforms and only supports **direct** TCP TLS client connections.
|
||||
@@ -205,7 +205,7 @@ Values:
|
||||
!!! warning ""
|
||||
|
||||
Experimental only: due to the high memory overhead of both CGO and Network.framework,
|
||||
do not use in proxy paths on iOS and tvOS.
|
||||
do not use in hot paths on iOS and tvOS.
|
||||
If you want to circumvent TLS fingerprint-based proxy censorship,
|
||||
use [NaiveProxy](/configuration/outbound/naive/) instead.
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ TLS 版本值:
|
||||
|
||||
可用值:
|
||||
|
||||
* `go`
|
||||
* `go`(默认)
|
||||
* `apple`
|
||||
|
||||
`apple` 使用 Network.framework,仅在 Apple 平台可用,且仅支持 **直接** TCP TLS 客户端连接。
|
||||
@@ -205,7 +205,7 @@ TLS 版本值:
|
||||
!!! warning ""
|
||||
|
||||
仅供实验用途:由于 CGO 和 Network.framework 占用的内存都很多,
|
||||
不应在 iOS 和 tvOS 的代理路径中使用。
|
||||
不应在 iOS 和 tvOS 的热路径中使用。
|
||||
如果您想规避基于 TLS 指纹的代理审查,应使用 [NaiveProxy](/zh/configuration/outbound/naive/)。
|
||||
|
||||
支持的字段:
|
||||
|
||||
@@ -26,6 +26,7 @@ type QUICOptions struct {
|
||||
|
||||
type _HTTPClientOptions struct {
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Engine string `json:"engine,omitempty"`
|
||||
Version int `json:"version,omitempty"`
|
||||
DisableVersionFallback bool `json:"disable_version_fallback,omitempty"`
|
||||
Headers badoption.HTTPHeader `json:"headers,omitempty"`
|
||||
@@ -33,6 +34,7 @@ type _HTTPClientOptions struct {
|
||||
HTTP3Options QUICOptions `json:"-"`
|
||||
DefaultOutbound bool `json:"-"`
|
||||
ResolveOnDetour bool `json:"-"`
|
||||
DirectResolver bool `json:"-"`
|
||||
OutboundTLSOptionsContainer
|
||||
DialerOptions
|
||||
}
|
||||
@@ -54,6 +56,7 @@ func (o HTTPClientOptions) IsEmpty() bool {
|
||||
}
|
||||
o.DefaultOutbound = false
|
||||
o.ResolveOnDetour = false
|
||||
o.DirectResolver = false
|
||||
return reflect.ValueOf(_HTTPClientOptions(o)).IsZero()
|
||||
}
|
||||
|
||||
|
||||
@@ -39,9 +39,6 @@ func NewCertificateProvider(ctx context.Context, _ log.ContextLogger, tag string
|
||||
return nil, E.New("missing tailscale endpoint tag")
|
||||
}
|
||||
endpointManager := service.FromContext[adapter.EndpointManager](ctx)
|
||||
if endpointManager == nil {
|
||||
return nil, E.New("missing endpoint manager in context")
|
||||
}
|
||||
rawEndpoint, loaded := endpointManager.Get(options.Endpoint)
|
||||
if !loaded {
|
||||
return nil, E.New("endpoint not found: ", options.Endpoint)
|
||||
|
||||
@@ -171,9 +171,9 @@ func (t *DNSTransport) createResolver(directDialer func() N.Dialer, resolver *dn
|
||||
tlsConfig := common.Must1(tls.NewClient(t.ctx, t.logger, serverAddr.AddrString(), option.OutboundTLSOptions{
|
||||
ALPN: []string{http2.NextProtoTLS, "http/1.1"},
|
||||
}))
|
||||
return transport.NewHTTPRaw(t.TransportAdapter, t.logger, myDialer, serverURL, http.Header{}, tlsConfig, option.HTTPClientOptions{}, http.MethodPost)
|
||||
return transport.NewHTTPRaw(t.TransportAdapter, t.logger, myDialer, serverURL, http.Header{}, tlsConfig, http.MethodPost)
|
||||
case "http":
|
||||
return transport.NewHTTPRaw(t.TransportAdapter, t.logger, myDialer, serverURL, http.Header{}, nil, option.HTTPClientOptions{}, http.MethodPost)
|
||||
return transport.NewHTTPRaw(t.TransportAdapter, t.logger, myDialer, serverURL, http.Header{}, nil, http.MethodPost)
|
||||
// case "tls":
|
||||
default:
|
||||
return nil, E.New("unknown resolver scheme: ", serverURL.Scheme)
|
||||
|
||||
@@ -219,10 +219,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
}
|
||||
dnsRouter := service.FromContext[adapter.DNSRouter](ctx)
|
||||
httpClientManager := service.FromContext[adapter.HTTPClientManager](ctx)
|
||||
if httpClientManager == nil {
|
||||
return nil, E.New("missing HTTP client manager")
|
||||
}
|
||||
controlTransport, err := httpClientManager.ResolveTransport(logger, controlHTTPClientOptions)
|
||||
controlTransport, err := httpClientManager.ResolveTransport(ctx, logger, controlHTTPClientOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create control HTTP client")
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/proxybridge"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -34,7 +35,7 @@ type Outbound struct {
|
||||
outbound.Adapter
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
proxy *ProxyListener
|
||||
proxy *proxybridge.Bridge
|
||||
startConf *tor.StartConf
|
||||
options map[string]string
|
||||
events chan control.Event
|
||||
@@ -79,11 +80,15 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proxy, err := proxybridge.New(ctx, logger, "proxy", outboundDialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Outbound{
|
||||
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTor, tag, []string{N.NetworkTCP}, options.DialerOptions),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
proxy: NewProxyListener(ctx, logger, outboundDialer),
|
||||
proxy: proxy,
|
||||
startConf: &startConf,
|
||||
options: options.Options,
|
||||
}, nil
|
||||
@@ -117,10 +122,6 @@ func (t *Outbound) start() error {
|
||||
return err
|
||||
}
|
||||
go t.recvLoop()
|
||||
err = t.proxy.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proxyPort := "127.0.0.1:" + F.ToString(t.proxy.Port())
|
||||
proxyUsername := t.proxy.Username()
|
||||
proxyPassword := t.proxy.Password()
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
package tor
|
||||
|
||||
import (
|
||||
std_bufio "bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/protocol/socks"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type ProxyListener struct {
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
dialer N.Dialer
|
||||
connection adapter.ConnectionManager
|
||||
tcpListener *net.TCPListener
|
||||
username string
|
||||
password string
|
||||
authenticator *auth.Authenticator
|
||||
}
|
||||
|
||||
func NewProxyListener(ctx context.Context, logger log.ContextLogger, dialer N.Dialer) *ProxyListener {
|
||||
var usernameB [64]byte
|
||||
var passwordB [64]byte
|
||||
rand.Read(usernameB[:])
|
||||
rand.Read(passwordB[:])
|
||||
username := hex.EncodeToString(usernameB[:])
|
||||
password := hex.EncodeToString(passwordB[:])
|
||||
return &ProxyListener{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
dialer: dialer,
|
||||
connection: service.FromContext[adapter.ConnectionManager](ctx),
|
||||
authenticator: auth.NewAuthenticator([]auth.User{{Username: username, Password: password}}),
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ProxyListener) Start() error {
|
||||
tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{
|
||||
IP: net.IPv4(127, 0, 0, 1),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.tcpListener = tcpListener
|
||||
go l.acceptLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *ProxyListener) Port() uint16 {
|
||||
if l.tcpListener == nil {
|
||||
panic("start listener first")
|
||||
}
|
||||
return M.SocksaddrFromNet(l.tcpListener.Addr()).Port
|
||||
}
|
||||
|
||||
func (l *ProxyListener) Username() string {
|
||||
return l.username
|
||||
}
|
||||
|
||||
func (l *ProxyListener) Password() string {
|
||||
return l.password
|
||||
}
|
||||
|
||||
func (l *ProxyListener) Close() error {
|
||||
return common.Close(l.tcpListener)
|
||||
}
|
||||
|
||||
func (l *ProxyListener) acceptLoop() {
|
||||
for {
|
||||
tcpConn, err := l.tcpListener.AcceptTCP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ctx := log.ContextWithNewID(l.ctx)
|
||||
go func() {
|
||||
hErr := l.accept(ctx, tcpConn)
|
||||
if hErr != nil {
|
||||
if E.IsClosedOrCanceled(hErr) {
|
||||
l.logger.DebugContext(ctx, E.Cause(hErr, "proxy connection closed"))
|
||||
return
|
||||
}
|
||||
l.logger.ErrorContext(ctx, E.Cause(hErr, "proxy"))
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ProxyListener) accept(ctx context.Context, conn *net.TCPConn) error {
|
||||
return socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), l.authenticator, l, nil, 0, M.SocksaddrFromNet(conn.RemoteAddr()), nil)
|
||||
}
|
||||
|
||||
func (l *ProxyListener) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Source = source
|
||||
metadata.Destination = destination
|
||||
metadata.Network = N.NetworkTCP
|
||||
l.logger.InfoContext(ctx, "proxy connection to ", metadata.Destination)
|
||||
l.connection.NewConnection(ctx, l.dialer, conn, metadata, onClose)
|
||||
}
|
||||
|
||||
func (l *ProxyListener) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Source = source
|
||||
metadata.Destination = destination
|
||||
metadata.Network = N.NetworkUDP
|
||||
l.logger.InfoContext(ctx, "proxy packet connection to ", metadata.Destination)
|
||||
l.connection.NewPacketConnection(ctx, l.dialer, conn, metadata, onClose)
|
||||
}
|
||||
@@ -43,6 +43,7 @@ type Router struct {
|
||||
processCache freelru.Cache[processCacheKey, processCacheEntry]
|
||||
neighborResolver adapter.NeighborResolver
|
||||
pauseManager pause.Manager
|
||||
httpClientManager adapter.HTTPClientManager
|
||||
trackers []adapter.ConnectionTracker
|
||||
platformInterface adapter.PlatformInterface
|
||||
started bool
|
||||
@@ -97,6 +98,8 @@ func (r *Router) Initialize(rules []option.Rule, ruleSets []option.RuleSet) erro
|
||||
func (r *Router) Start(stage adapter.StartStage) error {
|
||||
monitor := taskmonitor.New(r.logger, C.StartTimeout)
|
||||
switch stage {
|
||||
case adapter.StartStateInitialize:
|
||||
r.httpClientManager = service.FromContext[adapter.HTTPClientManager](r.ctx)
|
||||
case adapter.StartStateStart:
|
||||
if len(r.ruleSets) > 0 {
|
||||
monitor.Start("initialize rule-set")
|
||||
@@ -275,5 +278,6 @@ func (r *Router) NeighborResolver() adapter.NeighborResolver {
|
||||
|
||||
func (r *Router) ResetNetwork() {
|
||||
r.network.ResetNetwork()
|
||||
r.httpClientManager.ResetNetwork()
|
||||
r.dns.ResetNetwork()
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ type RemoteRuleSet struct {
|
||||
options option.RuleSet
|
||||
updateInterval time.Duration
|
||||
httpClient *http.Client
|
||||
httpTransport adapter.HTTPTransport
|
||||
access sync.RWMutex
|
||||
rules []adapter.HeadlessRule
|
||||
metadata adapter.RuleSetMetadata
|
||||
@@ -80,13 +81,11 @@ func (s *RemoteRuleSet) String() string {
|
||||
func (s *RemoteRuleSet) StartContext(ctx context.Context) error {
|
||||
s.cacheFile = service.FromContext[adapter.CacheFile](s.ctx)
|
||||
httpClientManager := service.FromContext[adapter.HTTPClientManager](s.ctx)
|
||||
if httpClientManager == nil {
|
||||
return E.New("missing http client manager in context")
|
||||
}
|
||||
transport, err := s.resolveTransport(httpClientManager)
|
||||
if err != nil {
|
||||
return E.Cause(err, "create rule-set http client")
|
||||
}
|
||||
s.httpTransport = transport
|
||||
s.httpClient = &http.Client{Transport: transport}
|
||||
if s.cacheFile != nil {
|
||||
if savedSet := s.cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil {
|
||||
@@ -291,12 +290,12 @@ func (s *RemoteRuleSet) fetch(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) resolveTransport(manager adapter.HTTPClientManager) (http.RoundTripper, error) {
|
||||
func (s *RemoteRuleSet) resolveTransport(manager adapter.HTTPClientManager) (adapter.HTTPTransport, error) {
|
||||
if s.options.RemoteOptions.HTTPClient != nil && !s.options.RemoteOptions.HTTPClient.IsEmpty() {
|
||||
if s.options.RemoteOptions.DownloadDetour != "" { //nolint:staticcheck
|
||||
return nil, E.New("http_client is conflict with deprecated download_detour field")
|
||||
}
|
||||
return manager.ResolveTransport(s.logger, *s.options.RemoteOptions.HTTPClient)
|
||||
return manager.ResolveTransport(s.ctx, s.logger, *s.options.RemoteOptions.HTTPClient)
|
||||
}
|
||||
if s.options.RemoteOptions.DownloadDetour != "" { //nolint:staticcheck
|
||||
deprecated.Report(s.ctx, deprecated.OptionLegacyRuleSetDownloadDetour)
|
||||
@@ -304,7 +303,7 @@ func (s *RemoteRuleSet) resolveTransport(manager adapter.HTTPClientManager) (htt
|
||||
httpClientOptions.DialerOptions = option.DialerOptions{
|
||||
Detour: s.options.RemoteOptions.DownloadDetour, //nolint:staticcheck
|
||||
}
|
||||
return manager.ResolveTransport(s.logger, httpClientOptions)
|
||||
return manager.ResolveTransport(s.ctx, s.logger, httpClientOptions)
|
||||
}
|
||||
defaultTransport := manager.DefaultTransport()
|
||||
if defaultTransport == nil {
|
||||
@@ -319,8 +318,8 @@ func (s *RemoteRuleSet) Close() error {
|
||||
if s.updateTicker != nil {
|
||||
s.updateTicker.Stop()
|
||||
}
|
||||
if s.httpClient != nil {
|
||||
s.httpClient.CloseIdleConnections()
|
||||
if s.httpTransport != nil {
|
||||
return s.httpTransport.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -45,11 +45,12 @@ var (
|
||||
|
||||
type Service struct {
|
||||
certificate.Adapter
|
||||
ctx context.Context
|
||||
config *certmagic.Config
|
||||
cache *certmagic.Cache
|
||||
domain []string
|
||||
nextProtos []string
|
||||
ctx context.Context
|
||||
config *certmagic.Config
|
||||
cache *certmagic.Cache
|
||||
domain []string
|
||||
nextProtos []string
|
||||
httpTransport adapter.HTTPTransport
|
||||
}
|
||||
|
||||
func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag string, options option.ACMECertificateProviderOptions) (adapter.CertificateProviderService, error) {
|
||||
@@ -123,7 +124,7 @@ func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag s
|
||||
AltTLSALPNPort: int(options.AlternativeTLSPort),
|
||||
Logger: zapLogger,
|
||||
}
|
||||
acmeHTTPClient, err := newACMEHTTPClient(ctx, logger, options)
|
||||
acmeHTTPClient, httpTransport, err := newACMEHTTPClient(ctx, logger, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -168,12 +169,13 @@ func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag s
|
||||
nextProtos = []string{C.ACMETLS1Protocol}
|
||||
}
|
||||
return &Service{
|
||||
Adapter: certificate.NewAdapter(C.TypeACME, tag),
|
||||
ctx: ctx,
|
||||
config: config,
|
||||
cache: cache,
|
||||
domain: options.Domain,
|
||||
nextProtos: nextProtos,
|
||||
Adapter: certificate.NewAdapter(C.TypeACME, tag),
|
||||
ctx: ctx,
|
||||
config: config,
|
||||
cache: cache,
|
||||
domain: options.Domain,
|
||||
nextProtos: nextProtos,
|
||||
httpTransport: httpTransport,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -188,7 +190,7 @@ func (s *Service) Close() error {
|
||||
if s.cache != nil {
|
||||
s.cache.Stop()
|
||||
}
|
||||
return nil
|
||||
return s.httpTransport.Close()
|
||||
}
|
||||
|
||||
func (s *Service) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
@@ -308,20 +310,17 @@ func createZeroSSLExternalAccountBinding(ctx context.Context, acmeIssuer *certma
|
||||
}, account, nil
|
||||
}
|
||||
|
||||
func newACMEHTTPClient(ctx context.Context, logger log.ContextLogger, options option.ACMECertificateProviderOptions) (*http.Client, error) {
|
||||
func newACMEHTTPClient(ctx context.Context, logger log.ContextLogger, options option.ACMECertificateProviderOptions) (*http.Client, adapter.HTTPTransport, error) {
|
||||
httpClientOptions := common.PtrValueOrDefault(options.HTTPClient)
|
||||
httpClientManager := service.FromContext[adapter.HTTPClientManager](ctx)
|
||||
if httpClientManager == nil {
|
||||
return nil, E.New("missing http client manager in context")
|
||||
}
|
||||
transport, err := httpClientManager.ResolveTransport(logger, httpClientOptions)
|
||||
transport, err := httpClientManager.ResolveTransport(ctx, logger, httpClientOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create ACME provider http client")
|
||||
return nil, nil, E.Cause(err, "create ACME provider http client")
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: certmagic.HTTPTimeout,
|
||||
}, nil
|
||||
}, transport, nil
|
||||
}
|
||||
|
||||
type acmeDNSProvider struct {
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/httpclient"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
@@ -59,19 +58,20 @@ func Register(registry *boxService.Registry) {
|
||||
|
||||
type Service struct {
|
||||
boxService.Adapter
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
stunListener *listener.Listener
|
||||
tlsConfig tls.ServerConfig
|
||||
server *derpserver.Server
|
||||
configPath string
|
||||
verifyClientEndpoint []string
|
||||
verifyClientURL []*option.DERPVerifyClientURLOptions
|
||||
home string
|
||||
meshKey string
|
||||
meshKeyPath string
|
||||
meshWith []*option.DERPMeshOptions
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
stunListener *listener.Listener
|
||||
tlsConfig tls.ServerConfig
|
||||
server *derpserver.Server
|
||||
configPath string
|
||||
verifyClientEndpoint []string
|
||||
verifyClientURL []*option.DERPVerifyClientURLOptions
|
||||
home string
|
||||
meshKey string
|
||||
meshKeyPath string
|
||||
meshWith []*option.DERPMeshOptions
|
||||
verifyClientTransports []adapter.HTTPTransport
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) {
|
||||
@@ -150,12 +150,16 @@ func (d *Service) Start(stage adapter.StartStage) error {
|
||||
if len(d.verifyClientURL) > 0 {
|
||||
var httpClients []*http.Client
|
||||
var urls []string
|
||||
httpClientManager := service.FromContext[adapter.HTTPClientManager](d.ctx)
|
||||
for index, verifyOptions := range d.verifyClientURL {
|
||||
client, createErr := httpclient.NewClient(d.ctx, d.logger, "", verifyOptions.HTTPClientOptions)
|
||||
httpClientOptions := verifyOptions.HTTPClientOptions
|
||||
httpClientOptions.Tag = ""
|
||||
transport, createErr := httpClientManager.ResolveTransport(d.ctx, d.logger, httpClientOptions)
|
||||
if createErr != nil {
|
||||
return E.Cause(createErr, "verify_client_url[", index, "]")
|
||||
}
|
||||
httpClients = append(httpClients, &http.Client{Transport: client})
|
||||
d.verifyClientTransports = append(d.verifyClientTransports, transport)
|
||||
httpClients = append(httpClients, &http.Client{Transport: transport})
|
||||
urls = append(urls, verifyOptions.URL)
|
||||
}
|
||||
server.SetVerifyClientHTTPClient(httpClients)
|
||||
@@ -335,10 +339,16 @@ func (d *Service) startMeshWithHost(derpServer *derpserver.Server, server *optio
|
||||
}
|
||||
|
||||
func (d *Service) Close() error {
|
||||
return common.Close(
|
||||
err := common.Close(
|
||||
common.PtrOrNil(d.listener),
|
||||
d.tlsConfig,
|
||||
)
|
||||
for _, transport := range d.verifyClientTransports {
|
||||
err = E.Append(err, transport.Close(), func(err error) error {
|
||||
return E.Cause(err, "close verify client transport")
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var homePage = `
|
||||
|
||||
@@ -62,6 +62,7 @@ type Service struct {
|
||||
done chan struct{}
|
||||
timeFunc func() time.Time
|
||||
httpClient *http.Client
|
||||
httpTransport adapter.HTTPTransport
|
||||
storage certmagic.Storage
|
||||
storageIssuerKey string
|
||||
storageNamesKey string
|
||||
@@ -102,7 +103,7 @@ func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag s
|
||||
requestedValidity = defaultRequestedValidity
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
httpClient, err := originCAHTTPClient(ctx, logger, options)
|
||||
httpClient, httpTransport, err := originCAHTTPClient(ctx, logger, options)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
@@ -131,6 +132,7 @@ func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag s
|
||||
cancel: cancel,
|
||||
timeFunc: timeFunc,
|
||||
httpClient: httpClient,
|
||||
httpTransport: httpTransport,
|
||||
storage: storage,
|
||||
storageIssuerKey: storageIssuerKey,
|
||||
storageNamesKey: storageNamesKey,
|
||||
@@ -143,17 +145,14 @@ func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag s
|
||||
}, nil
|
||||
}
|
||||
|
||||
func originCAHTTPClient(ctx context.Context, logger log.ContextLogger, options option.CloudflareOriginCACertificateProviderOptions) (*http.Client, error) {
|
||||
func originCAHTTPClient(ctx context.Context, logger log.ContextLogger, options option.CloudflareOriginCACertificateProviderOptions) (*http.Client, adapter.HTTPTransport, error) {
|
||||
httpClientOptions := common.PtrValueOrDefault(options.HTTPClient)
|
||||
httpClientManager := service.FromContext[adapter.HTTPClientManager](ctx)
|
||||
if httpClientManager == nil {
|
||||
return nil, E.New("missing http client manager in context")
|
||||
}
|
||||
transport, err := httpClientManager.ResolveTransport(logger, httpClientOptions)
|
||||
transport, err := httpClientManager.ResolveTransport(ctx, logger, httpClientOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create Cloudflare Origin CA http client")
|
||||
return nil, nil, E.Cause(err, "create Cloudflare Origin CA http client")
|
||||
}
|
||||
return &http.Client{Transport: transport}, nil
|
||||
return &http.Client{Transport: transport}, transport, nil
|
||||
}
|
||||
|
||||
func (s *Service) Start(stage adapter.StartStage) error {
|
||||
@@ -187,10 +186,7 @@ func (s *Service) Close() error {
|
||||
if done := s.done; done != nil {
|
||||
<-done
|
||||
}
|
||||
if transport, loaded := s.httpClient.Transport.(*http.Transport); loaded {
|
||||
transport.CloseIdleConnections()
|
||||
}
|
||||
return nil
|
||||
return s.httpTransport.Close()
|
||||
}
|
||||
|
||||
func (s *Service) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
|
||||
Reference in New Issue
Block a user