mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-14 04:38:28 +10:00
Compare commits
5 Commits
85291651f9
...
testing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
334dd6e5c0 | ||
|
|
ccfdbf2d57 | ||
|
|
9b75d28ca4 | ||
|
|
2e64545db4 | ||
|
|
9675b0902a |
22
adapter/http.go
Normal file
22
adapter/http.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -2,17 +2,11 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-tun"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
|
||||
"go4.org/netipx"
|
||||
@@ -51,7 +45,7 @@ type ConnectionRouterEx interface {
|
||||
|
||||
type RuleSet interface {
|
||||
Name() string
|
||||
StartContext(ctx context.Context, startContext *HTTPStartContext) error
|
||||
StartContext(ctx context.Context) error
|
||||
PostStart() error
|
||||
Metadata() RuleSetMetadata
|
||||
ExtractIPSet() []*netipx.IPSet
|
||||
@@ -77,46 +71,3 @@ type RuleSetMetadata struct {
|
||||
ContainsIPCIDRRule bool
|
||||
ContainsDNSQueryTypeRule bool
|
||||
}
|
||||
type HTTPStartContext struct {
|
||||
ctx context.Context
|
||||
access sync.Mutex
|
||||
httpClientCache map[string]*http.Client
|
||||
}
|
||||
|
||||
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
|
||||
return &HTTPStartContext{
|
||||
ctx: ctx,
|
||||
httpClientCache: make(map[string]*http.Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
if httpClient, loaded := c.httpClientCache[detour]; loaded {
|
||||
return httpClient
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
TLSHandshakeTimeout: C.TCPTimeout,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
TLSClientConfig: &tls.Config{
|
||||
Time: ntp.TimeFuncFromContext(c.ctx),
|
||||
RootCAs: RootPoolFromContext(c.ctx),
|
||||
},
|
||||
},
|
||||
}
|
||||
c.httpClientCache[detour] = httpClient
|
||||
return httpClient
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) Close() {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
for _, client := range c.httpClientCache {
|
||||
client.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
35
box.go
35
box.go
@@ -16,12 +16,14 @@ import (
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/certificate"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/httpclient"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/direct"
|
||||
@@ -50,6 +52,7 @@ type Box struct {
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
httpClientService adapter.LifecycleService
|
||||
internalService []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
}
|
||||
@@ -169,6 +172,7 @@ func New(options Options) (*Box, error) {
|
||||
}
|
||||
|
||||
var internalServices []adapter.LifecycleService
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||
len(certificateOptions.Certificate) > 0 ||
|
||||
@@ -181,8 +185,6 @@ func New(options Options) (*Box, error) {
|
||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||
internalServices = append(internalServices, certificateStore)
|
||||
}
|
||||
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||
@@ -209,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)
|
||||
@@ -368,6 +374,12 @@ func New(options Options) (*Box, error) {
|
||||
&option.LocalDNSServerOptions{},
|
||||
)
|
||||
})
|
||||
httpClientManager.Initialize(func() (*httpclient.Transport, error) {
|
||||
deprecated.Report(ctx, deprecated.OptionImplicitDefaultHTTPClient)
|
||||
var httpClientOptions option.HTTPClientOptions
|
||||
httpClientOptions.DefaultOutbound = true
|
||||
return httpclient.NewTransport(ctx, logFactory.NewLogger("httpclient"), "", httpClientOptions)
|
||||
})
|
||||
if platformInterface != nil {
|
||||
err = platformInterface.Initialize(networkManager)
|
||||
if err != nil {
|
||||
@@ -428,6 +440,7 @@ func New(options Options) (*Box, error) {
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
httpClientService: httpClientService,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
@@ -490,7 +503,15 @@ func (s *Box) preStart() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection, s.router, s.dnsRouter)
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.logger, adapter.StartStateStart, []adapter.LifecycleService{s.httpClientService})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.router, s.dnsRouter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -567,6 +588,14 @@ func (s *Box) Close() error {
|
||||
})
|
||||
s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
if s.httpClientService != nil {
|
||||
s.logger.Trace("close ", s.httpClientService.Name())
|
||||
startTime := time.Now()
|
||||
err = E.Append(err, s.httpClientService.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", s.httpClientService.Name())
|
||||
})
|
||||
s.logger.Trace("close ", s.httpClientService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
for _, lifecycleService := range s.internalService {
|
||||
s.logger.Trace("close ", lifecycleService.Name())
|
||||
startTime := time.Now()
|
||||
|
||||
@@ -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, ","))
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -35,21 +36,9 @@ func updateMozillaIncludedRootCAs() error {
|
||||
return err
|
||||
}
|
||||
geoIndex := slices.Index(header, "Geographic Focus")
|
||||
nameIndex := slices.Index(header, "Common Name or Certificate Name")
|
||||
certIndex := slices.Index(header, "PEM Info")
|
||||
|
||||
generated := strings.Builder{}
|
||||
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||
|
||||
package certificate
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
var mozillaIncluded *x509.CertPool
|
||||
|
||||
func init() {
|
||||
mozillaIncluded = x509.NewCertPool()
|
||||
`)
|
||||
pemBundle := strings.Builder{}
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
@@ -60,18 +49,12 @@ func init() {
|
||||
if record[geoIndex] == "China" {
|
||||
continue
|
||||
}
|
||||
generated.WriteString("\n // ")
|
||||
generated.WriteString(record[nameIndex])
|
||||
generated.WriteString("\n")
|
||||
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
|
||||
cert := record[certIndex]
|
||||
// Remove single quotes
|
||||
cert = cert[1 : len(cert)-1]
|
||||
generated.WriteString(cert)
|
||||
generated.WriteString("`))\n")
|
||||
pemBundle.WriteString(cert)
|
||||
pemBundle.WriteString("\n")
|
||||
}
|
||||
generated.WriteString("}\n")
|
||||
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
|
||||
return writeGeneratedCertificateBundle("mozilla", "mozillaIncluded", pemBundle.String())
|
||||
}
|
||||
|
||||
func fetchChinaFingerprints() (map[string]bool, error) {
|
||||
@@ -119,23 +102,11 @@ func updateChromeIncludedRootCAs() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
subjectIndex := slices.Index(header, "Subject")
|
||||
statusIndex := slices.Index(header, "Google Chrome Status")
|
||||
certIndex := slices.Index(header, "X.509 Certificate (PEM)")
|
||||
fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint")
|
||||
|
||||
generated := strings.Builder{}
|
||||
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||
|
||||
package certificate
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
var chromeIncluded *x509.CertPool
|
||||
|
||||
func init() {
|
||||
chromeIncluded = x509.NewCertPool()
|
||||
`)
|
||||
pemBundle := strings.Builder{}
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
@@ -149,18 +120,39 @@ func init() {
|
||||
if chinaFingerprints[record[fingerprintIndex]] {
|
||||
continue
|
||||
}
|
||||
generated.WriteString("\n // ")
|
||||
generated.WriteString(record[subjectIndex])
|
||||
generated.WriteString("\n")
|
||||
generated.WriteString(" chromeIncluded.AppendCertsFromPEM([]byte(`")
|
||||
cert := record[certIndex]
|
||||
// Remove single quotes if present
|
||||
if len(cert) > 0 && cert[0] == '\'' {
|
||||
cert = cert[1 : len(cert)-1]
|
||||
}
|
||||
generated.WriteString(cert)
|
||||
generated.WriteString("`))\n")
|
||||
pemBundle.WriteString(cert)
|
||||
pemBundle.WriteString("\n")
|
||||
}
|
||||
generated.WriteString("}\n")
|
||||
return os.WriteFile("common/certificate/chrome.go", []byte(generated.String()), 0o644)
|
||||
return writeGeneratedCertificateBundle("chrome", "chromeIncluded", pemBundle.String())
|
||||
}
|
||||
|
||||
func writeGeneratedCertificateBundle(name string, variableName string, pemBundle string) error {
|
||||
goSource := `// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed ` + name + `.pem
|
||||
var ` + variableName + `PEM string
|
||||
|
||||
var ` + variableName + ` *x509.CertPool
|
||||
|
||||
func init() {
|
||||
` + variableName + ` = x509.NewCertPool()
|
||||
` + variableName + `.AppendCertsFromPEM([]byte(` + variableName + `PEM))
|
||||
}
|
||||
`
|
||||
err := os.WriteFile(filepath.Join("common/certificate", name+".pem"), []byte(pemBundle), 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filepath.Join("common/certificate", name+".go"), []byte(goSource), 0o644)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
2650
common/certificate/chrome.pem
Normal file
2650
common/certificate/chrome.pem
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
4256
common/certificate/mozilla.pem
Normal file
4256
common/certificate/mozilla.pem
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,8 +22,10 @@ var _ adapter.CertificateStore = (*Store)(nil)
|
||||
|
||||
type Store struct {
|
||||
access sync.RWMutex
|
||||
store string
|
||||
systemPool *x509.CertPool
|
||||
currentPool *x509.CertPool
|
||||
currentPEM []string
|
||||
certificate string
|
||||
certificatePaths []string
|
||||
certificateDirectoryPaths []string
|
||||
@@ -61,6 +63,7 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
|
||||
return nil, E.New("unknown certificate store: ", options.Store)
|
||||
}
|
||||
store := &Store{
|
||||
store: options.Store,
|
||||
systemPool: systemPool,
|
||||
certificate: strings.Join(options.Certificate, "\n"),
|
||||
certificatePaths: options.CertificatePath,
|
||||
@@ -123,19 +126,37 @@ func (s *Store) Pool() *x509.CertPool {
|
||||
return s.currentPool
|
||||
}
|
||||
|
||||
func (s *Store) StoreKind() string {
|
||||
return s.store
|
||||
}
|
||||
|
||||
func (s *Store) CurrentPEM() []string {
|
||||
s.access.RLock()
|
||||
defer s.access.RUnlock()
|
||||
return append([]string(nil), s.currentPEM...)
|
||||
}
|
||||
|
||||
func (s *Store) update() error {
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
var currentPool *x509.CertPool
|
||||
var currentPEM []string
|
||||
if s.systemPool == nil {
|
||||
currentPool = x509.NewCertPool()
|
||||
} else {
|
||||
currentPool = s.systemPool.Clone()
|
||||
}
|
||||
switch s.store {
|
||||
case C.CertificateStoreMozilla:
|
||||
currentPEM = append(currentPEM, mozillaIncludedPEM)
|
||||
case C.CertificateStoreChrome:
|
||||
currentPEM = append(currentPEM, chromeIncludedPEM)
|
||||
}
|
||||
if s.certificate != "" {
|
||||
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
||||
return E.New("invalid certificate PEM strings")
|
||||
}
|
||||
currentPEM = append(currentPEM, s.certificate)
|
||||
}
|
||||
for _, path := range s.certificatePaths {
|
||||
pemContent, err := os.ReadFile(path)
|
||||
@@ -145,6 +166,7 @@ func (s *Store) update() error {
|
||||
if !currentPool.AppendCertsFromPEM(pemContent) {
|
||||
return E.New("invalid certificate PEM file: ", path)
|
||||
}
|
||||
currentPEM = append(currentPEM, string(pemContent))
|
||||
}
|
||||
var firstErr error
|
||||
for _, directoryPath := range s.certificateDirectoryPaths {
|
||||
@@ -157,8 +179,8 @@ func (s *Store) update() error {
|
||||
}
|
||||
for _, directoryEntry := range directoryEntries {
|
||||
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
|
||||
if err == nil {
|
||||
currentPool.AppendCertsFromPEM(pemContent)
|
||||
if err == nil && currentPool.AppendCertsFromPEM(pemContent) {
|
||||
currentPEM = append(currentPEM, string(pemContent))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,6 +188,7 @@ func (s *Store) update() error {
|
||||
return firstErr
|
||||
}
|
||||
s.currentPool = currentPool
|
||||
s.currentPEM = currentPEM
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ type DirectDialer interface {
|
||||
type DetourDialer struct {
|
||||
outboundManager adapter.OutboundManager
|
||||
detour string
|
||||
defaultOutbound bool
|
||||
legacyDNSDialer bool
|
||||
dialer N.Dialer
|
||||
initOnce sync.Once
|
||||
@@ -33,6 +34,13 @@ func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNS
|
||||
}
|
||||
}
|
||||
|
||||
func NewDefaultOutboundDetour(outboundManager adapter.OutboundManager) N.Dialer {
|
||||
return &DetourDialer{
|
||||
outboundManager: outboundManager,
|
||||
defaultOutbound: true,
|
||||
}
|
||||
}
|
||||
|
||||
func InitializeDetour(dialer N.Dialer) error {
|
||||
detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
|
||||
if !isDetour {
|
||||
@@ -47,12 +55,18 @@ func (d *DetourDialer) Dialer() (N.Dialer, error) {
|
||||
}
|
||||
|
||||
func (d *DetourDialer) init() {
|
||||
dialer, loaded := d.outboundManager.Outbound(d.detour)
|
||||
if !loaded {
|
||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||
return
|
||||
var dialer adapter.Outbound
|
||||
if d.detour != "" {
|
||||
var loaded bool
|
||||
dialer, loaded = d.outboundManager.Outbound(d.detour)
|
||||
if !loaded {
|
||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
dialer = d.outboundManager.Default()
|
||||
}
|
||||
if !d.legacyDNSDialer {
|
||||
if !d.defaultOutbound && !d.legacyDNSDialer {
|
||||
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
|
||||
if directDialer.IsEmpty() {
|
||||
d.initErr = E.New("detour to an empty direct outbound makes no sense")
|
||||
|
||||
@@ -25,6 +25,7 @@ type Options struct {
|
||||
NewDialer bool
|
||||
LegacyDNSDialer bool
|
||||
DirectOutbound bool
|
||||
DefaultOutbound bool
|
||||
}
|
||||
|
||||
// TODO: merge with NewWithOptions
|
||||
@@ -42,19 +43,26 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
dialer N.Dialer
|
||||
err error
|
||||
)
|
||||
hasDetour := dialOptions.Detour != "" || options.DefaultOutbound
|
||||
if dialOptions.Detour != "" {
|
||||
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
||||
if outboundManager == nil {
|
||||
return nil, E.New("missing outbound manager")
|
||||
}
|
||||
dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
|
||||
} else if options.DefaultOutbound {
|
||||
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
||||
if outboundManager == nil {
|
||||
return nil, E.New("missing outbound manager")
|
||||
}
|
||||
dialer = NewDefaultOutboundDetour(outboundManager)
|
||||
} else {
|
||||
dialer, err = NewDefault(options.Context, dialOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
||||
if options.RemoteIsDomain && (!hasDetour || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
||||
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
|
||||
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
|
||||
var defaultOptions adapter.NetworkOptions
|
||||
|
||||
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")
|
||||
}
|
||||
182
common/httpclient/client.go
Normal file
182
common/httpclient/client.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type Transport struct {
|
||||
transport adapter.HTTPTransport
|
||||
dialer N.Dialer
|
||||
headers http.Header
|
||||
host string
|
||||
tag string
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
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{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Options: tlsOptions,
|
||||
AllowEmptyServerName: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewTransportWithDialer(rawDialer, baseTLSConfig, tag, options)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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) (adapter.HTTPTransport, error) {
|
||||
version := options.Version
|
||||
if version == 0 {
|
||||
version = 2
|
||||
}
|
||||
fallbackDelay := time.Duration(options.DialerOptions.FallbackDelay)
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = 300 * time.Millisecond
|
||||
}
|
||||
var transport adapter.HTTPTransport
|
||||
var err error
|
||||
switch version {
|
||||
case 1:
|
||||
transport = newHTTP1Transport(rawDialer, baseTLSConfig)
|
||||
case 2:
|
||||
if options.DisableVersionFallback {
|
||||
transport, err = newHTTP2Transport(rawDialer, baseTLSConfig, options.HTTP2Options)
|
||||
} else {
|
||||
transport, err = newHTTP2FallbackTransport(rawDialer, baseTLSConfig, options.HTTP2Options)
|
||||
}
|
||||
case 3:
|
||||
if baseTLSConfig != nil {
|
||||
_, err = baseTLSConfig.STDConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if options.DisableVersionFallback {
|
||||
transport, err = newHTTP3Transport(rawDialer, baseTLSConfig, options.HTTP3Options)
|
||||
} else {
|
||||
var h2Fallback adapter.HTTPTransport
|
||||
h2Fallback, err = newHTTP2FallbackTransport(rawDialer, baseTLSConfig, options.HTTP2Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transport, err = newHTTP3FallbackTransport(rawDialer, baseTLSConfig, h2Fallback, options.HTTP3Options, fallbackDelay)
|
||||
}
|
||||
default:
|
||||
return nil, E.New("unknown HTTP version: ", version)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transport, nil
|
||||
}
|
||||
|
||||
func (c *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
if c.tag == "" && len(c.headers) == 0 && c.host == "" {
|
||||
return c.transport.RoundTrip(request)
|
||||
}
|
||||
if c.tag != "" {
|
||||
if transportTag, loaded := transportTagFromContext(request.Context()); loaded && transportTag == c.tag {
|
||||
return nil, E.New("HTTP request loopback in transport[", c.tag, "]")
|
||||
}
|
||||
request = request.Clone(contextWithTransportTag(request.Context(), c.tag))
|
||||
} else {
|
||||
request = request.Clone(request.Context())
|
||||
}
|
||||
applyHeaders(request, c.headers, c.host)
|
||||
return c.transport.RoundTrip(request)
|
||||
}
|
||||
|
||||
func (c *Transport) CloseIdleConnections() {
|
||||
c.transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
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 *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)
|
||||
}
|
||||
14
common/httpclient/context.go
Normal file
14
common/httpclient/context.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package httpclient
|
||||
|
||||
import "context"
|
||||
|
||||
type transportKey struct{}
|
||||
|
||||
func contextWithTransportTag(ctx context.Context, transportTag string) context.Context {
|
||||
return context.WithValue(ctx, transportKey{}, transportTag)
|
||||
}
|
||||
|
||||
func transportTagFromContext(ctx context.Context) (string, bool) {
|
||||
value, loaded := ctx.Value(transportKey{}).(string)
|
||||
return value, loaded
|
||||
}
|
||||
86
common/httpclient/helpers.go
Normal file
86
common/httpclient/helpers.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdTLS "crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func dialTLS(ctx context.Context, rawDialer N.Dialer, baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string, expectProto string) (net.Conn, error) {
|
||||
if baseTLSConfig == nil {
|
||||
return nil, E.New("TLS transport unavailable")
|
||||
}
|
||||
tlsConfig := baseTLSConfig.Clone()
|
||||
if tlsConfig.ServerName() == "" && destination.IsValid() {
|
||||
tlsConfig.SetServerName(destination.AddrString())
|
||||
}
|
||||
tlsConfig.SetNextProtos(nextProtos)
|
||||
conn, err := rawDialer.DialContext(ctx, N.NetworkTCP, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConn, err := tls.ClientHandshake(ctx, conn, tlsConfig)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if expectProto != "" && tlsConn.ConnectionState().NegotiatedProtocol != expectProto {
|
||||
tlsConn.Close()
|
||||
return nil, errHTTP2Fallback
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
func applyHeaders(request *http.Request, headers http.Header, host string) {
|
||||
for header, values := range headers {
|
||||
request.Header[header] = append([]string(nil), values...)
|
||||
}
|
||||
if host != "" {
|
||||
request.Host = host
|
||||
}
|
||||
}
|
||||
|
||||
func requestRequiresHTTP1(request *http.Request) bool {
|
||||
return strings.Contains(strings.ToLower(request.Header.Get("Connection")), "upgrade") &&
|
||||
strings.EqualFold(request.Header.Get("Upgrade"), "websocket")
|
||||
}
|
||||
|
||||
func requestReplayable(request *http.Request) bool {
|
||||
return request.Body == nil || request.Body == http.NoBody || request.GetBody != nil
|
||||
}
|
||||
|
||||
func cloneRequestForRetry(request *http.Request) *http.Request {
|
||||
cloned := request.Clone(request.Context())
|
||||
if request.Body != nil && request.Body != http.NoBody && request.GetBody != nil {
|
||||
cloned.Body = mustGetBody(request)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func mustGetBody(request *http.Request) io.ReadCloser {
|
||||
body, err := request.GetBody()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func buildSTDTLSConfig(baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string) (*stdTLS.Config, error) {
|
||||
if baseTLSConfig == nil {
|
||||
return nil, nil
|
||||
}
|
||||
tlsConfig := baseTLSConfig.Clone()
|
||||
if tlsConfig.ServerName() == "" && destination.IsValid() {
|
||||
tlsConfig.SetServerName(destination.AddrString())
|
||||
}
|
||||
tlsConfig.SetNextProtos(nextProtos)
|
||||
return tlsConfig.STDConfig()
|
||||
}
|
||||
47
common/httpclient/http1_transport.go
Normal file
47
common/httpclient/http1_transport.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
)
|
||||
|
||||
type http1Transport struct {
|
||||
transport *http.Transport
|
||||
}
|
||||
|
||||
func newHTTP1Transport(rawDialer N.Dialer, baseTLSConfig tls.Config) *http1Transport {
|
||||
transport := &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return rawDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
}
|
||||
if baseTLSConfig != nil {
|
||||
transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{"http/1.1"}, "")
|
||||
}
|
||||
}
|
||||
return &http1Transport{transport: transport}
|
||||
}
|
||||
|
||||
func (t *http1Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return t.transport.RoundTrip(request)
|
||||
}
|
||||
|
||||
func (t *http1Transport) CloseIdleConnections() {
|
||||
t.transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (t *http1Transport) Clone() adapter.HTTPTransport {
|
||||
return &http1Transport{transport: t.transport.Clone()}
|
||||
}
|
||||
|
||||
func (t *http1Transport) Close() error {
|
||||
t.CloseIdleConnections()
|
||||
return nil
|
||||
}
|
||||
42
common/httpclient/http2_config.go
Normal file
42
common/httpclient/http2_config.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
stdTLS "crypto/tls"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
func CloneHTTP2Transport(transport *http2.Transport) *http2.Transport {
|
||||
return &http2.Transport{
|
||||
ReadIdleTimeout: transport.ReadIdleTimeout,
|
||||
PingTimeout: transport.PingTimeout,
|
||||
DialTLSContext: transport.DialTLSContext,
|
||||
}
|
||||
}
|
||||
|
||||
func ConfigureHTTP2Transport(options option.HTTP2Options) (*http2.Transport, error) {
|
||||
stdTransport := &http.Transport{
|
||||
TLSClientConfig: &stdTLS.Config{},
|
||||
HTTP2: &http.HTTP2Config{
|
||||
MaxReceiveBufferPerStream: int(options.StreamReceiveWindow.Value()),
|
||||
MaxReceiveBufferPerConnection: int(options.ConnectionReceiveWindow.Value()),
|
||||
MaxConcurrentStreams: options.MaxConcurrentStreams,
|
||||
SendPingTimeout: time.Duration(options.KeepAlivePeriod),
|
||||
PingTimeout: time.Duration(options.IdleTimeout),
|
||||
},
|
||||
}
|
||||
h2Transport, err := http2.ConfigureTransports(stdTransport)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "configure HTTP/2 transport")
|
||||
}
|
||||
// ConfigureTransports binds ConnPool to the throwaway http.Transport; sever it so DialTLSContext is used directly.
|
||||
h2Transport.ConnPool = nil
|
||||
h2Transport.ReadIdleTimeout = time.Duration(options.KeepAlivePeriod)
|
||||
h2Transport.PingTimeout = time.Duration(options.IdleTimeout)
|
||||
return h2Transport, nil
|
||||
}
|
||||
93
common/httpclient/http2_fallback_transport.go
Normal file
93
common/httpclient/http2_fallback_transport.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdTLS "crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
"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"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var errHTTP2Fallback = E.New("fallback to HTTP/1.1")
|
||||
|
||||
type http2FallbackTransport struct {
|
||||
h2Transport *http2.Transport
|
||||
h1Transport *http1Transport
|
||||
h2Fallback *atomic.Bool
|
||||
}
|
||||
|
||||
func newHTTP2FallbackTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTP2Options) (*http2FallbackTransport, error) {
|
||||
h1 := newHTTP1Transport(rawDialer, baseTLSConfig)
|
||||
var fallback atomic.Bool
|
||||
h2Transport, err := ConfigureHTTP2Transport(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h2Transport.DialTLSContext = func(ctx context.Context, network, addr string, _ *stdTLS.Config) (net.Conn, error) {
|
||||
conn, dialErr := dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{http2.NextProtoTLS, "http/1.1"}, http2.NextProtoTLS)
|
||||
if dialErr != nil {
|
||||
if errors.Is(dialErr, errHTTP2Fallback) {
|
||||
fallback.Store(true)
|
||||
}
|
||||
return nil, dialErr
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
return &http2FallbackTransport{
|
||||
h2Transport: h2Transport,
|
||||
h1Transport: h1,
|
||||
h2Fallback: &fallback,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *http2FallbackTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return t.roundTrip(request, true)
|
||||
}
|
||||
|
||||
func (t *http2FallbackTransport) roundTrip(request *http.Request, allowHTTP1Fallback bool) (*http.Response, error) {
|
||||
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
|
||||
return t.h1Transport.RoundTrip(request)
|
||||
}
|
||||
if t.h2Fallback.Load() {
|
||||
if !allowHTTP1Fallback {
|
||||
return nil, errHTTP2Fallback
|
||||
}
|
||||
return t.h1Transport.RoundTrip(request)
|
||||
}
|
||||
response, err := t.h2Transport.RoundTrip(request)
|
||||
if err == nil {
|
||||
return response, nil
|
||||
}
|
||||
if !errors.Is(err, errHTTP2Fallback) || !allowHTTP1Fallback {
|
||||
return nil, err
|
||||
}
|
||||
return t.h1Transport.RoundTrip(cloneRequestForRetry(request))
|
||||
}
|
||||
|
||||
func (t *http2FallbackTransport) CloseIdleConnections() {
|
||||
t.h1Transport.CloseIdleConnections()
|
||||
t.h2Transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
60
common/httpclient/http2_transport.go
Normal file
60
common/httpclient/http2_transport.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdTLS "crypto/tls"
|
||||
"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"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
type http2Transport struct {
|
||||
h2Transport *http2.Transport
|
||||
h1Transport *http1Transport
|
||||
}
|
||||
|
||||
func newHTTP2Transport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTP2Options) (*http2Transport, error) {
|
||||
h1 := newHTTP1Transport(rawDialer, baseTLSConfig)
|
||||
h2Transport, err := ConfigureHTTP2Transport(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h2Transport.DialTLSContext = func(ctx context.Context, network, addr string, _ *stdTLS.Config) (net.Conn, error) {
|
||||
return dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{http2.NextProtoTLS}, http2.NextProtoTLS)
|
||||
}
|
||||
return &http2Transport{
|
||||
h2Transport: h2Transport,
|
||||
h1Transport: h1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *http2Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
|
||||
return t.h1Transport.RoundTrip(request)
|
||||
}
|
||||
return t.h2Transport.RoundTrip(request)
|
||||
}
|
||||
|
||||
func (t *http2Transport) CloseIdleConnections() {
|
||||
t.h1Transport.CloseIdleConnections()
|
||||
t.h2Transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
312
common/httpclient/http3_transport.go
Normal file
312
common/httpclient/http3_transport.go
Normal file
@@ -0,0 +1,312 @@
|
||||
//go:build with_quic
|
||||
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdTLS "crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type http3Transport struct {
|
||||
h3Transport *http3.Transport
|
||||
}
|
||||
|
||||
type http3FallbackTransport struct {
|
||||
h3Transport *http3.Transport
|
||||
h2Fallback adapter.HTTPTransport
|
||||
fallbackDelay time.Duration
|
||||
brokenAccess sync.Mutex
|
||||
brokenUntil time.Time
|
||||
brokenBackoff time.Duration
|
||||
}
|
||||
|
||||
func newHTTP3RoundTripper(
|
||||
rawDialer N.Dialer,
|
||||
baseTLSConfig tls.Config,
|
||||
options option.QUICOptions,
|
||||
) *http3.Transport {
|
||||
var handshakeTimeout time.Duration
|
||||
if baseTLSConfig != nil {
|
||||
handshakeTimeout = baseTLSConfig.HandshakeTimeout()
|
||||
}
|
||||
quicConfig := &quic.Config{
|
||||
InitialStreamReceiveWindow: options.StreamReceiveWindow.Value(),
|
||||
MaxStreamReceiveWindow: options.StreamReceiveWindow.Value(),
|
||||
InitialConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
|
||||
MaxConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
|
||||
KeepAlivePeriod: time.Duration(options.KeepAlivePeriod),
|
||||
MaxIdleTimeout: time.Duration(options.IdleTimeout),
|
||||
DisablePathMTUDiscovery: options.DisablePathMTUDiscovery,
|
||||
}
|
||||
if options.InitialPacketSize > 0 {
|
||||
quicConfig.InitialPacketSize = uint16(options.InitialPacketSize)
|
||||
}
|
||||
if options.MaxConcurrentStreams > 0 {
|
||||
quicConfig.MaxIncomingStreams = int64(options.MaxConcurrentStreams)
|
||||
}
|
||||
if handshakeTimeout > 0 {
|
||||
quicConfig.HandshakeIdleTimeout = handshakeTimeout
|
||||
}
|
||||
h3Transport := &http3.Transport{
|
||||
TLSClientConfig: &stdTLS.Config{},
|
||||
QUICConfig: quicConfig,
|
||||
Dial: func(ctx context.Context, addr string, tlsConfig *stdTLS.Config, quicConfig *quic.Config) (*quic.Conn, error) {
|
||||
if handshakeTimeout > 0 && quicConfig.HandshakeIdleTimeout == 0 {
|
||||
quicConfig = quicConfig.Clone()
|
||||
quicConfig.HandshakeIdleTimeout = handshakeTimeout
|
||||
}
|
||||
if baseTLSConfig != nil {
|
||||
var err error
|
||||
tlsConfig, err = buildSTDTLSConfig(baseTLSConfig, M.ParseSocksaddr(addr), []string{http3.NextProtoH3})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
tlsConfig = tlsConfig.Clone()
|
||||
tlsConfig.NextProtos = []string{http3.NextProtoH3}
|
||||
}
|
||||
conn, err := rawDialer.DialContext(ctx, N.NetworkUDP, M.ParseSocksaddr(addr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quicConn, err := quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsConfig, quicConfig)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return quicConn, nil
|
||||
},
|
||||
}
|
||||
return h3Transport
|
||||
}
|
||||
|
||||
func newHTTP3Transport(
|
||||
rawDialer N.Dialer,
|
||||
baseTLSConfig tls.Config,
|
||||
options option.QUICOptions,
|
||||
) (adapter.HTTPTransport, error) {
|
||||
return &http3Transport{
|
||||
h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newHTTP3FallbackTransport(
|
||||
rawDialer N.Dialer,
|
||||
baseTLSConfig tls.Config,
|
||||
h2Fallback adapter.HTTPTransport,
|
||||
options option.QUICOptions,
|
||||
fallbackDelay time.Duration,
|
||||
) (adapter.HTTPTransport, error) {
|
||||
return &http3FallbackTransport{
|
||||
h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options),
|
||||
h2Fallback: h2Fallback,
|
||||
fallbackDelay: fallbackDelay,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *http3Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return t.h3Transport.RoundTrip(request)
|
||||
}
|
||||
|
||||
func (t *http3Transport) CloseIdleConnections() {
|
||||
t.h3Transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (t *http3Transport) Close() error {
|
||||
t.CloseIdleConnections()
|
||||
return t.h3Transport.Close()
|
||||
}
|
||||
|
||||
func (t *http3Transport) Clone() adapter.HTTPTransport {
|
||||
return &http3Transport{
|
||||
h3Transport: t.h3Transport,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
|
||||
return t.h2Fallback.RoundTrip(request)
|
||||
}
|
||||
return t.roundTripHTTP3(request)
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) roundTripHTTP3(request *http.Request) (*http.Response, error) {
|
||||
if t.h3Broken() {
|
||||
return t.h2FallbackRoundTrip(request)
|
||||
}
|
||||
response, err := t.h3Transport.RoundTripOpt(request, http3.RoundTripOpt{OnlyCachedConn: true})
|
||||
if err == nil {
|
||||
t.clearH3Broken()
|
||||
return response, nil
|
||||
}
|
||||
if !errors.Is(err, http3.ErrNoCachedConn) {
|
||||
t.markH3Broken()
|
||||
return t.h2FallbackRoundTrip(cloneRequestForRetry(request))
|
||||
}
|
||||
if !requestReplayable(request) {
|
||||
response, err = t.h3Transport.RoundTrip(request)
|
||||
if err == nil {
|
||||
t.clearH3Broken()
|
||||
return response, nil
|
||||
}
|
||||
t.markH3Broken()
|
||||
return nil, err
|
||||
}
|
||||
return t.roundTripHTTP3Race(request)
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request) (*http.Response, error) {
|
||||
ctx, cancel := context.WithCancel(request.Context())
|
||||
defer cancel()
|
||||
type result struct {
|
||||
response *http.Response
|
||||
err error
|
||||
h3 bool
|
||||
}
|
||||
results := make(chan result, 2)
|
||||
startRoundTrip := func(request *http.Request, useH3 bool) {
|
||||
request = request.WithContext(ctx)
|
||||
var (
|
||||
response *http.Response
|
||||
err error
|
||||
)
|
||||
if useH3 {
|
||||
response, err = t.h3Transport.RoundTrip(request)
|
||||
} else {
|
||||
response, err = t.h2FallbackRoundTrip(request)
|
||||
}
|
||||
results <- result{response: response, err: err, h3: useH3}
|
||||
}
|
||||
goroutines := 1
|
||||
received := 0
|
||||
drainRemaining := func() {
|
||||
cancel()
|
||||
for range goroutines - received {
|
||||
go func() {
|
||||
loser := <-results
|
||||
if loser.response != nil && loser.response.Body != nil {
|
||||
loser.response.Body.Close()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
go startRoundTrip(cloneRequestForRetry(request), true)
|
||||
timer := time.NewTimer(t.fallbackDelay)
|
||||
defer timer.Stop()
|
||||
var (
|
||||
h3Err error
|
||||
fallbackErr error
|
||||
)
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
if goroutines == 1 {
|
||||
goroutines++
|
||||
go startRoundTrip(cloneRequestForRetry(request), false)
|
||||
}
|
||||
case raceResult := <-results:
|
||||
received++
|
||||
if raceResult.err == nil {
|
||||
if raceResult.h3 {
|
||||
t.clearH3Broken()
|
||||
}
|
||||
drainRemaining()
|
||||
return raceResult.response, nil
|
||||
}
|
||||
if raceResult.h3 {
|
||||
t.markH3Broken()
|
||||
h3Err = raceResult.err
|
||||
if goroutines == 1 {
|
||||
goroutines++
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
go startRoundTrip(cloneRequestForRetry(request), false)
|
||||
}
|
||||
} else {
|
||||
fallbackErr = raceResult.err
|
||||
}
|
||||
if received < goroutines {
|
||||
continue
|
||||
}
|
||||
drainRemaining()
|
||||
switch {
|
||||
case h3Err != nil && fallbackErr != nil:
|
||||
return nil, E.Errors(h3Err, fallbackErr)
|
||||
case fallbackErr != nil:
|
||||
return nil, fallbackErr
|
||||
default:
|
||||
return nil, h3Err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) h2FallbackRoundTrip(request *http.Request) (*http.Response, error) {
|
||||
if fallback, isFallback := t.h2Fallback.(*http2FallbackTransport); isFallback {
|
||||
return fallback.roundTrip(request, true)
|
||||
}
|
||||
return t.h2Fallback.RoundTrip(request)
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) CloseIdleConnections() {
|
||||
t.h3Transport.CloseIdleConnections()
|
||||
t.h2Fallback.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) Close() error {
|
||||
t.CloseIdleConnections()
|
||||
return t.h3Transport.Close()
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) Clone() adapter.HTTPTransport {
|
||||
return &http3FallbackTransport{
|
||||
h3Transport: t.h3Transport,
|
||||
h2Fallback: t.h2Fallback.Clone(),
|
||||
fallbackDelay: t.fallbackDelay,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) h3Broken() bool {
|
||||
t.brokenAccess.Lock()
|
||||
defer t.brokenAccess.Unlock()
|
||||
return !t.brokenUntil.IsZero() && time.Now().Before(t.brokenUntil)
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) clearH3Broken() {
|
||||
t.brokenAccess.Lock()
|
||||
t.brokenUntil = time.Time{}
|
||||
t.brokenBackoff = 0
|
||||
t.brokenAccess.Unlock()
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) markH3Broken() {
|
||||
t.brokenAccess.Lock()
|
||||
defer t.brokenAccess.Unlock()
|
||||
if t.brokenBackoff == 0 {
|
||||
t.brokenBackoff = 5 * time.Minute
|
||||
} else {
|
||||
t.brokenBackoff *= 2
|
||||
if t.brokenBackoff > 48*time.Hour {
|
||||
t.brokenBackoff = 48 * time.Hour
|
||||
}
|
||||
}
|
||||
t.brokenUntil = time.Now().Add(t.brokenBackoff)
|
||||
}
|
||||
31
common/httpclient/http3_transport_stub.go
Normal file
31
common/httpclient/http3_transport_stub.go
Normal file
@@ -0,0 +1,31 @@
|
||||
//go:build !with_quic
|
||||
|
||||
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"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func newHTTP3FallbackTransport(
|
||||
rawDialer N.Dialer,
|
||||
baseTLSConfig tls.Config,
|
||||
h2Fallback adapter.HTTPTransport,
|
||||
options option.QUICOptions,
|
||||
fallbackDelay time.Duration,
|
||||
) (adapter.HTTPTransport, error) {
|
||||
return nil, E.New("HTTP/3 requires building with the with_quic tag")
|
||||
}
|
||||
|
||||
func newHTTP3Transport(
|
||||
rawDialer N.Dialer,
|
||||
baseTLSConfig tls.Config,
|
||||
options option.QUICOptions,
|
||||
) (adapter.HTTPTransport, error) {
|
||||
return nil, E.New("HTTP/3 requires building with the with_quic tag")
|
||||
}
|
||||
164
common/httpclient/manager.go
Normal file
164
common/httpclient/manager.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
_ adapter.HTTPClientManager = (*Manager)(nil)
|
||||
_ adapter.LifecycleService = (*Manager)(nil)
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
access sync.Mutex
|
||||
defines map[string]option.HTTPClient
|
||||
transports map[string]*Transport
|
||||
defaultTag string
|
||||
defaultTransport adapter.HTTPTransport
|
||||
defaultTransportFallback func() (*Transport, error)
|
||||
fallbackTransport *Transport
|
||||
}
|
||||
|
||||
func NewManager(ctx context.Context, logger log.ContextLogger, clients []option.HTTPClient, defaultHTTPClient string) *Manager {
|
||||
defines := make(map[string]option.HTTPClient, len(clients))
|
||||
for _, client := range clients {
|
||||
defines[client.Tag] = client
|
||||
}
|
||||
defaultTag := defaultHTTPClient
|
||||
if defaultTag == "" && len(clients) > 0 {
|
||||
defaultTag = clients[0].Tag
|
||||
}
|
||||
return &Manager{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
defines: defines,
|
||||
transports: make(map[string]*Transport),
|
||||
defaultTag: defaultTag,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Initialize(defaultTransportFallback func() (*Transport, error)) {
|
||||
m.defaultTransportFallback = defaultTransportFallback
|
||||
}
|
||||
|
||||
func (m *Manager) Name() string {
|
||||
return "http-client"
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
if m.defaultTag != "" {
|
||||
transport, err := m.resolveShared(m.defaultTag)
|
||||
if err != nil {
|
||||
return E.Cause(err, "resolve default http client")
|
||||
}
|
||||
m.defaultTransport = transport
|
||||
} else if m.defaultTransportFallback != nil {
|
||||
transport, err := m.defaultTransportFallback()
|
||||
if err != nil {
|
||||
return E.Cause(err, "create default http client")
|
||||
}
|
||||
m.defaultTransport = transport
|
||||
m.fallbackTransport = transport
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) DefaultTransport() adapter.HTTPTransport {
|
||||
if m.defaultTransport == nil {
|
||||
return nil
|
||||
}
|
||||
return &sharedTransport{m.defaultTransport}
|
||||
}
|
||||
|
||||
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]
|
||||
if !loaded {
|
||||
return nil, E.New("http_client not found: ", options.Tag)
|
||||
}
|
||||
resolvedOptions := define.Options()
|
||||
resolvedOptions.ResolveOnDetour = true
|
||||
return NewTransport(ctx, logger, options.Tag, resolvedOptions)
|
||||
}
|
||||
transport, err := m.resolveShared(options.Tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sharedTransport{transport}, nil
|
||||
}
|
||||
return NewTransport(ctx, logger, "", options)
|
||||
}
|
||||
|
||||
func (m *Manager) resolveShared(tag string) (adapter.HTTPTransport, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
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)
|
||||
}
|
||||
transport, err := NewTransport(m.ctx, m.logger, tag, define.Options())
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create shared http_client[", tag, "]")
|
||||
}
|
||||
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.transports == nil {
|
||||
return nil
|
||||
}
|
||||
var err 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.fallbackTransport != nil {
|
||||
err = E.Append(err, m.fallbackTransport.Close(), func(err error) error {
|
||||
return E.Cause(err, "close default http client")
|
||||
})
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
414
common/tls/apple_client_platform.go
Normal file
414
common/tls/apple_client_platform.go
Normal file
@@ -0,0 +1,414 @@
|
||||
//go:build darwin && cgo
|
||||
|
||||
package tls
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c -fobjc-arc
|
||||
#cgo LDFLAGS: -framework Foundation -framework Network -framework Security
|
||||
|
||||
#include <stdlib.h>
|
||||
#include "apple_client_platform.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (Conn, error) {
|
||||
rawSyscallConn, ok := common.Cast[syscall.Conn](conn)
|
||||
if !ok {
|
||||
return nil, E.New("apple TLS: requires fd-backed TCP connection")
|
||||
}
|
||||
syscallConn, err := rawSyscallConn.SyscallConn()
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "access raw connection")
|
||||
}
|
||||
|
||||
var dupFD int
|
||||
controlErr := syscallConn.Control(func(fd uintptr) {
|
||||
dupFD, err = unix.Dup(int(fd))
|
||||
})
|
||||
if controlErr != nil {
|
||||
return nil, E.Cause(controlErr, "access raw connection")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "duplicate raw connection")
|
||||
}
|
||||
|
||||
serverName := c.serverName
|
||||
serverNamePtr := cStringOrNil(serverName)
|
||||
defer cFree(serverNamePtr)
|
||||
|
||||
alpn := strings.Join(c.nextProtos, "\n")
|
||||
alpnPtr := cStringOrNil(alpn)
|
||||
defer cFree(alpnPtr)
|
||||
|
||||
anchorPEMPtr := cStringOrNil(c.anchorPEM)
|
||||
defer cFree(anchorPEMPtr)
|
||||
|
||||
var errorPtr *C.char
|
||||
client := C.box_apple_tls_client_create(
|
||||
C.int(dupFD),
|
||||
serverNamePtr,
|
||||
alpnPtr,
|
||||
C.size_t(len(alpn)),
|
||||
C.uint16_t(c.minVersion),
|
||||
C.uint16_t(c.maxVersion),
|
||||
C.bool(c.insecure),
|
||||
anchorPEMPtr,
|
||||
C.size_t(len(c.anchorPEM)),
|
||||
C.bool(c.anchorOnly),
|
||||
&errorPtr,
|
||||
)
|
||||
if client == nil {
|
||||
if errorPtr != nil {
|
||||
defer C.free(unsafe.Pointer(errorPtr))
|
||||
return nil, E.New(C.GoString(errorPtr))
|
||||
}
|
||||
return nil, E.New("apple TLS: create connection")
|
||||
}
|
||||
if err = waitAppleTLSClientReady(ctx, client); err != nil {
|
||||
C.box_apple_tls_client_cancel(client)
|
||||
C.box_apple_tls_client_free(client)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var state C.box_apple_tls_state_t
|
||||
stateOK := C.box_apple_tls_client_copy_state(client, &state, &errorPtr)
|
||||
if !bool(stateOK) {
|
||||
C.box_apple_tls_client_cancel(client)
|
||||
C.box_apple_tls_client_free(client)
|
||||
if errorPtr != nil {
|
||||
defer C.free(unsafe.Pointer(errorPtr))
|
||||
return nil, E.New(C.GoString(errorPtr))
|
||||
}
|
||||
return nil, E.New("apple TLS: read metadata")
|
||||
}
|
||||
defer C.box_apple_tls_state_free(&state)
|
||||
|
||||
connectionState, rawCerts, err := parseAppleTLSState(&state)
|
||||
if err != nil {
|
||||
C.box_apple_tls_client_cancel(client)
|
||||
C.box_apple_tls_client_free(client)
|
||||
return nil, err
|
||||
}
|
||||
if len(c.certificatePublicKeySHA256) > 0 {
|
||||
err = VerifyPublicKeySHA256(c.certificatePublicKeySHA256, rawCerts)
|
||||
if err != nil {
|
||||
C.box_apple_tls_client_cancel(client)
|
||||
C.box_apple_tls_client_free(client)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &appleTLSConn{
|
||||
rawConn: conn,
|
||||
client: client,
|
||||
state: connectionState,
|
||||
closed: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
const appleTLSHandshakePollInterval = 100 * time.Millisecond
|
||||
|
||||
func waitAppleTLSClientReady(ctx context.Context, client *C.box_apple_tls_client_t) error {
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
C.box_apple_tls_client_cancel(client)
|
||||
return err
|
||||
}
|
||||
|
||||
waitTimeout := appleTLSHandshakePollInterval
|
||||
if deadline, loaded := ctx.Deadline(); loaded {
|
||||
remaining := time.Until(deadline)
|
||||
if remaining <= 0 {
|
||||
C.box_apple_tls_client_cancel(client)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return context.DeadlineExceeded
|
||||
}
|
||||
if remaining < waitTimeout {
|
||||
waitTimeout = remaining
|
||||
}
|
||||
}
|
||||
|
||||
var errorPtr *C.char
|
||||
waitResult := C.box_apple_tls_client_wait_ready(client, C.int(timeoutFromDuration(waitTimeout)), &errorPtr)
|
||||
switch waitResult {
|
||||
case 1:
|
||||
return nil
|
||||
case -2:
|
||||
continue
|
||||
case 0:
|
||||
if errorPtr != nil {
|
||||
defer C.free(unsafe.Pointer(errorPtr))
|
||||
return E.New(C.GoString(errorPtr))
|
||||
}
|
||||
return E.New("apple TLS: handshake failed")
|
||||
default:
|
||||
return E.New("apple TLS: invalid handshake state")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type appleTLSConn struct {
|
||||
rawConn net.Conn
|
||||
client *C.box_apple_tls_client_t
|
||||
state tls.ConnectionState
|
||||
|
||||
readAccess sync.Mutex
|
||||
writeAccess sync.Mutex
|
||||
stateAccess sync.RWMutex
|
||||
closeOnce sync.Once
|
||||
ioAccess sync.Mutex
|
||||
ioGroup sync.WaitGroup
|
||||
closed chan struct{}
|
||||
readEOF bool
|
||||
}
|
||||
|
||||
func (c *appleTLSConn) Read(p []byte) (int, error) {
|
||||
c.readAccess.Lock()
|
||||
defer c.readAccess.Unlock()
|
||||
if c.readEOF {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
client, err := c.acquireClient()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer c.releaseClient()
|
||||
|
||||
var eof C.bool
|
||||
var errorPtr *C.char
|
||||
n := C.box_apple_tls_client_read(client, unsafe.Pointer(&p[0]), C.size_t(len(p)), &eof, &errorPtr)
|
||||
switch {
|
||||
case n >= 0:
|
||||
if bool(eof) {
|
||||
c.readEOF = true
|
||||
if n == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
}
|
||||
return int(n), nil
|
||||
default:
|
||||
if errorPtr != nil {
|
||||
defer C.free(unsafe.Pointer(errorPtr))
|
||||
if c.isClosed() {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
return 0, E.New(C.GoString(errorPtr))
|
||||
}
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
func (c *appleTLSConn) Write(p []byte) (int, error) {
|
||||
c.writeAccess.Lock()
|
||||
defer c.writeAccess.Unlock()
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
client, err := c.acquireClient()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer c.releaseClient()
|
||||
|
||||
var errorPtr *C.char
|
||||
n := C.box_apple_tls_client_write(client, unsafe.Pointer(&p[0]), C.size_t(len(p)), &errorPtr)
|
||||
if n >= 0 {
|
||||
return int(n), nil
|
||||
}
|
||||
if errorPtr != nil {
|
||||
defer C.free(unsafe.Pointer(errorPtr))
|
||||
if c.isClosed() {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
return 0, E.New(C.GoString(errorPtr))
|
||||
}
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
|
||||
func (c *appleTLSConn) Close() error {
|
||||
var closeErr error
|
||||
c.closeOnce.Do(func() {
|
||||
close(c.closed)
|
||||
C.box_apple_tls_client_cancel(c.client)
|
||||
closeErr = c.rawConn.Close()
|
||||
c.ioAccess.Lock()
|
||||
c.ioGroup.Wait()
|
||||
C.box_apple_tls_client_free(c.client)
|
||||
c.client = nil
|
||||
c.ioAccess.Unlock()
|
||||
})
|
||||
return closeErr
|
||||
}
|
||||
|
||||
func (c *appleTLSConn) LocalAddr() net.Addr {
|
||||
return c.rawConn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *appleTLSConn) RemoteAddr() net.Addr {
|
||||
return c.rawConn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *appleTLSConn) SetDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (c *appleTLSConn) SetReadDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (c *appleTLSConn) SetWriteDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (c *appleTLSConn) NeedAdditionalReadDeadline() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *appleTLSConn) isClosed() bool {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (c *appleTLSConn) acquireClient() (*C.box_apple_tls_client_t, error) {
|
||||
c.ioAccess.Lock()
|
||||
defer c.ioAccess.Unlock()
|
||||
if c.isClosed() {
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
client := c.client
|
||||
if client == nil {
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
c.ioGroup.Add(1)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *appleTLSConn) releaseClient() {
|
||||
c.ioGroup.Done()
|
||||
}
|
||||
|
||||
func (c *appleTLSConn) NetConn() net.Conn {
|
||||
return c.rawConn
|
||||
}
|
||||
|
||||
func (c *appleTLSConn) HandshakeContext(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *appleTLSConn) ConnectionState() ConnectionState {
|
||||
c.stateAccess.RLock()
|
||||
defer c.stateAccess.RUnlock()
|
||||
return c.state
|
||||
}
|
||||
|
||||
func parseAppleTLSState(state *C.box_apple_tls_state_t) (tls.ConnectionState, [][]byte, error) {
|
||||
rawCerts, peerCertificates, err := parseAppleCertChain(state.peer_cert_chain, state.peer_cert_chain_len)
|
||||
if err != nil {
|
||||
return tls.ConnectionState{}, nil, err
|
||||
}
|
||||
var negotiatedProtocol string
|
||||
if state.alpn != nil {
|
||||
negotiatedProtocol = C.GoString(state.alpn)
|
||||
}
|
||||
var serverName string
|
||||
if state.server_name != nil {
|
||||
serverName = C.GoString(state.server_name)
|
||||
}
|
||||
return tls.ConnectionState{
|
||||
Version: uint16(state.version),
|
||||
HandshakeComplete: true,
|
||||
CipherSuite: uint16(state.cipher_suite),
|
||||
NegotiatedProtocol: negotiatedProtocol,
|
||||
ServerName: serverName,
|
||||
PeerCertificates: peerCertificates,
|
||||
}, rawCerts, nil
|
||||
}
|
||||
|
||||
func parseAppleCertChain(chain *C.uint8_t, chainLen C.size_t) ([][]byte, []*x509.Certificate, error) {
|
||||
if chain == nil || chainLen == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
chainBytes := C.GoBytes(unsafe.Pointer(chain), C.int(chainLen))
|
||||
var (
|
||||
rawCerts [][]byte
|
||||
peerCertificates []*x509.Certificate
|
||||
)
|
||||
for len(chainBytes) >= 4 {
|
||||
certificateLen := binary.BigEndian.Uint32(chainBytes[:4])
|
||||
chainBytes = chainBytes[4:]
|
||||
if len(chainBytes) < int(certificateLen) {
|
||||
return nil, nil, E.New("apple TLS: invalid certificate chain")
|
||||
}
|
||||
certificateData := append([]byte(nil), chainBytes[:certificateLen]...)
|
||||
certificate, err := x509.ParseCertificate(certificateData)
|
||||
if err != nil {
|
||||
return nil, nil, E.Cause(err, "parse peer certificate")
|
||||
}
|
||||
rawCerts = append(rawCerts, certificateData)
|
||||
peerCertificates = append(peerCertificates, certificate)
|
||||
chainBytes = chainBytes[certificateLen:]
|
||||
}
|
||||
if len(chainBytes) != 0 {
|
||||
return nil, nil, E.New("apple TLS: invalid certificate chain")
|
||||
}
|
||||
return rawCerts, peerCertificates, nil
|
||||
}
|
||||
|
||||
func timeoutFromDuration(timeout time.Duration) int {
|
||||
if timeout <= 0 {
|
||||
return 0
|
||||
}
|
||||
timeoutMilliseconds := int64(timeout / time.Millisecond)
|
||||
if timeout%time.Millisecond != 0 {
|
||||
timeoutMilliseconds++
|
||||
}
|
||||
if timeoutMilliseconds > math.MaxInt32 {
|
||||
return math.MaxInt32
|
||||
}
|
||||
return int(timeoutMilliseconds)
|
||||
}
|
||||
|
||||
func cStringOrNil(value string) *C.char {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return C.CString(value)
|
||||
}
|
||||
|
||||
func cFree(pointer *C.char) {
|
||||
if pointer != nil {
|
||||
C.free(unsafe.Pointer(pointer))
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
301
common/tls/apple_client_platform_test.go
Normal file
301
common/tls/apple_client_platform_test.go
Normal file
@@ -0,0 +1,301 @@
|
||||
//go:build darwin && cgo
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdtls "crypto/tls"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
const appleTLSTestTimeout = 5 * time.Second
|
||||
|
||||
const (
|
||||
appleTLSSuccessHandshakeLoops = 20
|
||||
appleTLSFailureRecoveryLoops = 10
|
||||
)
|
||||
|
||||
type appleTLSServerResult struct {
|
||||
state stdtls.ConnectionState
|
||||
err error
|
||||
}
|
||||
|
||||
func TestAppleClientHandshakeAppliesALPNAndVersion(t *testing.T) {
|
||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
||||
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.Fatalf("iteration %d: %v", index, err)
|
||||
}
|
||||
|
||||
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.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppleClientHandshakeRejectsVersionMismatch(t *testing.T) {
|
||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
||||
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
||||
Certificates: []stdtls.Certificate{serverCertificate},
|
||||
MinVersion: stdtls.VersionTLS13,
|
||||
MaxVersion: stdtls.VersionTLS13,
|
||||
})
|
||||
|
||||
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
Engine: "apple",
|
||||
ServerName: "localhost",
|
||||
MaxVersion: "1.2",
|
||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
||||
})
|
||||
if err == nil {
|
||||
clientConn.Close()
|
||||
t.Fatal("expected version mismatch handshake to fail")
|
||||
}
|
||||
|
||||
if result := <-serverResult; result.err == nil {
|
||||
t.Fatal("expected server handshake to fail on version mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppleClientHandshakeRejectsServerNameMismatch(t *testing.T) {
|
||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
||||
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
||||
Certificates: []stdtls.Certificate{serverCertificate},
|
||||
})
|
||||
|
||||
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
Engine: "apple",
|
||||
ServerName: "example.com",
|
||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
||||
})
|
||||
if err == nil {
|
||||
clientConn.Close()
|
||||
t.Fatal("expected server name mismatch handshake to fail")
|
||||
}
|
||||
|
||||
if result := <-serverResult; result.err == nil {
|
||||
t.Fatal("expected server handshake to fail on server name mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
privateKeyPEM, certificatePEM, err := 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 startAppleTLSTestServer(t *testing.T, tlsConfig *stdtls.Config) (<-chan appleTLSServerResult, string) {
|
||||
t.Helper()
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
listener.Close()
|
||||
})
|
||||
|
||||
if tcpListener, isTCP := listener.(*net.TCPListener); isTCP {
|
||||
err = tcpListener.SetDeadline(time.Now().Add(appleTLSTestTimeout))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
result := make(chan appleTLSServerResult, 1)
|
||||
go func() {
|
||||
defer close(result)
|
||||
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
result <- appleTLSServerResult{err: err}
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
err = conn.SetDeadline(time.Now().Add(appleTLSTestTimeout))
|
||||
if err != nil {
|
||||
result <- appleTLSServerResult{err: err}
|
||||
return
|
||||
}
|
||||
|
||||
tlsConn := stdtls.Server(conn, tlsConfig)
|
||||
defer tlsConn.Close()
|
||||
|
||||
err = tlsConn.Handshake()
|
||||
if err != nil {
|
||||
result <- appleTLSServerResult{err: err}
|
||||
return
|
||||
}
|
||||
|
||||
result <- appleTLSServerResult{state: tlsConn.ConnectionState()}
|
||||
}()
|
||||
|
||||
return result, listener.Addr().String()
|
||||
}
|
||||
|
||||
func newAppleTestClientConn(t *testing.T, serverAddress string, options option.OutboundTLSOptions) (Conn, error) {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), appleTLSTestTimeout)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clientConfig, err := NewClientWithOptions(ClientOptions{
|
||||
Context: ctx,
|
||||
Logger: logger.NOP(),
|
||||
ServerAddress: "",
|
||||
Options: options,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", serverAddress, appleTLSTestTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tlsConn, err := ClientHandshake(ctx, conn, clientConfig)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
15
common/tls/apple_client_stub.go
Normal file
15
common/tls/apple_client_stub.go
Normal file
@@ -0,0 +1,15 @@
|
||||
//go:build !darwin || !cgo
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
func newAppleClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
return nil, E.New("Apple TLS engine is not available on non-Apple platforms")
|
||||
}
|
||||
@@ -10,12 +10,15 @@ import (
|
||||
"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"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
|
||||
var errMissingServerName = E.New("missing server_name or insecure=true")
|
||||
|
||||
func NewDialerFromOptions(ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
|
||||
if !options.Enabled {
|
||||
return dialer, nil
|
||||
@@ -42,11 +45,12 @@ func NewClient(ctx context.Context, logger logger.ContextLogger, serverAddress s
|
||||
}
|
||||
|
||||
type ClientOptions struct {
|
||||
Context context.Context
|
||||
Logger logger.ContextLogger
|
||||
ServerAddress string
|
||||
Options option.OutboundTLSOptions
|
||||
KTLSCompatible bool
|
||||
Context context.Context
|
||||
Logger logger.ContextLogger
|
||||
ServerAddress string
|
||||
Options option.OutboundTLSOptions
|
||||
AllowEmptyServerName bool
|
||||
KTLSCompatible bool
|
||||
}
|
||||
|
||||
func NewClientWithOptions(options ClientOptions) (Config, error) {
|
||||
@@ -61,17 +65,22 @@ func NewClientWithOptions(options ClientOptions) (Config, error) {
|
||||
if options.Options.KernelRx {
|
||||
options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx")
|
||||
}
|
||||
if options.Options.Reality != nil && options.Options.Reality.Enabled {
|
||||
return NewRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||
} else if options.Options.UTLS != nil && options.Options.UTLS.Enabled {
|
||||
return NewUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||
switch options.Options.Engine {
|
||||
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)
|
||||
}
|
||||
return NewSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||
if options.Options.Reality != nil && options.Options.Reality.Enabled {
|
||||
return newRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
||||
} else if options.Options.UTLS != nil && options.Options.UTLS.Enabled {
|
||||
return newUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
||||
}
|
||||
return newSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
||||
}
|
||||
|
||||
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||
defer cancel()
|
||||
tlsConn, err := aTLS.ClientHandshake(ctx, conn, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -52,11 +52,15 @@ type RealityClientConfig struct {
|
||||
}
|
||||
|
||||
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newRealityClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
if options.UTLS == nil || !options.UTLS.Enabled {
|
||||
return nil, E.New("uTLS is required by reality client")
|
||||
}
|
||||
|
||||
uClient, err := NewUTLSClient(ctx, logger, serverAddress, options)
|
||||
uClient, err := newUTLSClient(ctx, logger, serverAddress, options, allowEmptyServerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -108,6 +112,14 @@ func (e *RealityClientConfig) SetNextProtos(nextProto []string) {
|
||||
e.uClient.SetNextProtos(nextProto)
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) HandshakeTimeout() time.Duration {
|
||||
return e.uClient.HandshakeTimeout()
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
e.uClient.SetHandshakeTimeout(timeout)
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) STDConfig() (*STDConfig, error) {
|
||||
return nil, E.New("unsupported usage for reality")
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ import (
|
||||
var _ ServerConfigCompat = (*RealityServerConfig)(nil)
|
||||
|
||||
type RealityServerConfig struct {
|
||||
config *utls.RealityConfig
|
||||
config *utls.RealityConfig
|
||||
handshakeTimeout time.Duration
|
||||
}
|
||||
|
||||
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||
@@ -130,7 +131,16 @@ func NewRealityServer(ctx context.Context, logger log.ContextLogger, options opt
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
return nil, E.New("Reality is conflict with ECH")
|
||||
}
|
||||
var config ServerConfig = &RealityServerConfig{&tlsConfig}
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = C.TCPTimeout
|
||||
}
|
||||
var config ServerConfig = &RealityServerConfig{
|
||||
config: &tlsConfig,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
}
|
||||
if options.KernelTx || options.KernelRx {
|
||||
if !C.IsLinux {
|
||||
return nil, E.New("kTLS is only supported on Linux")
|
||||
@@ -161,6 +171,14 @@ func (c *RealityServerConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *RealityServerConfig) HandshakeTimeout() time.Duration {
|
||||
return c.handshakeTimeout
|
||||
}
|
||||
|
||||
func (c *RealityServerConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
c.handshakeTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *RealityServerConfig) STDConfig() (*tls.Config, error) {
|
||||
return nil, E.New("unsupported usage for reality")
|
||||
}
|
||||
@@ -191,7 +209,8 @@ func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn
|
||||
|
||||
func (c *RealityServerConfig) Clone() Config {
|
||||
return &RealityServerConfig{
|
||||
config: c.config.Clone(),
|
||||
config: c.config.Clone(),
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,8 +46,11 @@ func NewServerWithOptions(options ServerOptions) (ServerConfig, error) {
|
||||
}
|
||||
|
||||
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||
defer cancel()
|
||||
if config.HandshakeTimeout() == 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, C.TCPTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
tlsConn, err := aTLS.ServerHandshake(ctx, conn, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -24,16 +24,30 @@ import (
|
||||
type STDClientConfig struct {
|
||||
ctx context.Context
|
||||
config *tls.Config
|
||||
serverName string
|
||||
disableSNI bool
|
||||
verifyServerName bool
|
||||
handshakeTimeout time.Duration
|
||||
fragment bool
|
||||
fragmentFallbackDelay time.Duration
|
||||
recordFragment bool
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) ServerName() string {
|
||||
return c.config.ServerName
|
||||
return c.serverName
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) SetServerName(serverName string) {
|
||||
c.serverName = serverName
|
||||
if c.disableSNI {
|
||||
c.config.ServerName = ""
|
||||
if c.verifyServerName {
|
||||
c.config.VerifyConnection = verifyConnection(c.config.RootCAs, c.config.Time, serverName)
|
||||
} else {
|
||||
c.config.VerifyConnection = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
c.config.ServerName = serverName
|
||||
}
|
||||
|
||||
@@ -45,6 +59,14 @@ func (c *STDClientConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) HandshakeTimeout() time.Duration {
|
||||
return c.handshakeTimeout
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
c.handshakeTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) STDConfig() (*STDConfig, error) {
|
||||
return c.config, nil
|
||||
}
|
||||
@@ -57,13 +79,19 @@ func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) {
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) Clone() Config {
|
||||
return &STDClientConfig{
|
||||
cloned := &STDClientConfig{
|
||||
ctx: c.ctx,
|
||||
config: c.config.Clone(),
|
||||
serverName: c.serverName,
|
||||
disableSNI: c.disableSNI,
|
||||
verifyServerName: c.verifyServerName,
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
fragment: c.fragment,
|
||||
fragmentFallbackDelay: c.fragmentFallbackDelay,
|
||||
recordFragment: c.recordFragment,
|
||||
}
|
||||
cloned.SetServerName(cloned.serverName)
|
||||
return cloned
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) ECHConfigList() []byte {
|
||||
@@ -75,41 +103,27 @@ func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte
|
||||
}
|
||||
|
||||
func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newSTDClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
var serverName string
|
||||
if options.ServerName != "" {
|
||||
serverName = options.ServerName
|
||||
} else if serverAddress != "" {
|
||||
serverName = serverAddress
|
||||
}
|
||||
if serverName == "" && !options.Insecure {
|
||||
return nil, E.New("missing server_name or insecure=true")
|
||||
if serverName == "" && !options.Insecure && !allowEmptyServerName {
|
||||
return nil, errMissingServerName
|
||||
}
|
||||
|
||||
var tlsConfig tls.Config
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||
if !options.DisableSNI {
|
||||
tlsConfig.ServerName = serverName
|
||||
}
|
||||
if options.Insecure {
|
||||
tlsConfig.InsecureSkipVerify = options.Insecure
|
||||
} else if options.DisableSNI {
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
|
||||
verifyOptions := x509.VerifyOptions{
|
||||
Roots: tlsConfig.RootCAs,
|
||||
DNSName: serverName,
|
||||
Intermediates: x509.NewCertPool(),
|
||||
}
|
||||
for _, cert := range state.PeerCertificates[1:] {
|
||||
verifyOptions.Intermediates.AddCert(cert)
|
||||
}
|
||||
if tlsConfig.Time != nil {
|
||||
verifyOptions.CurrentTime = tlsConfig.Time()
|
||||
}
|
||||
_, err := state.PeerCertificates[0].Verify(verifyOptions)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(options.CertificatePublicKeySHA256) > 0 {
|
||||
if len(options.Certificate) > 0 || options.CertificatePath != "" {
|
||||
@@ -117,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 {
|
||||
@@ -198,7 +212,24 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
|
||||
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
|
||||
return nil, E.New("client certificate and client key must be provided together")
|
||||
}
|
||||
var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = C.TCPTimeout
|
||||
}
|
||||
var config Config = &STDClientConfig{
|
||||
ctx: ctx,
|
||||
config: &tlsConfig,
|
||||
serverName: serverName,
|
||||
disableSNI: options.DisableSNI,
|
||||
verifyServerName: options.DisableSNI && !options.Insecure,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
fragment: options.Fragment,
|
||||
fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay),
|
||||
recordFragment: options.RecordFragment,
|
||||
}
|
||||
config.SetServerName(serverName)
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
var err error
|
||||
config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options)
|
||||
@@ -220,7 +251,28 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func verifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte, timeFunc func() time.Time) error {
|
||||
func verifyConnection(rootCAs *x509.CertPool, timeFunc func() time.Time, serverName string) func(state tls.ConnectionState) error {
|
||||
return func(state tls.ConnectionState) error {
|
||||
if serverName == "" {
|
||||
return errMissingServerName
|
||||
}
|
||||
verifyOptions := x509.VerifyOptions{
|
||||
Roots: rootCAs,
|
||||
DNSName: serverName,
|
||||
Intermediates: x509.NewCertPool(),
|
||||
}
|
||||
for _, cert := range state.PeerCertificates[1:] {
|
||||
verifyOptions.Intermediates.AddCert(cert)
|
||||
}
|
||||
if timeFunc != nil {
|
||||
verifyOptions.CurrentTime = timeFunc()
|
||||
}
|
||||
_, err := state.PeerCertificates[0].Verify(verifyOptions)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
@@ -92,6 +92,7 @@ func getACMENextProtos(provider adapter.CertificateProvider) []string {
|
||||
type STDServerConfig struct {
|
||||
access sync.RWMutex
|
||||
config *tls.Config
|
||||
handshakeTimeout time.Duration
|
||||
logger log.Logger
|
||||
certificateProvider managedCertificateProvider
|
||||
acmeService adapter.SimpleLifecycle
|
||||
@@ -139,6 +140,18 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
||||
c.config = config
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) HandshakeTimeout() time.Duration {
|
||||
c.access.RLock()
|
||||
defer c.access.RUnlock()
|
||||
return c.handshakeTimeout
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
c.handshakeTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) hasACMEALPN() bool {
|
||||
if c.acmeService != nil {
|
||||
return true
|
||||
@@ -165,7 +178,8 @@ func (c *STDServerConfig) Server(conn net.Conn) (Conn, error) {
|
||||
|
||||
func (c *STDServerConfig) Clone() Config {
|
||||
return &STDServerConfig{
|
||||
config: c.config.Clone(),
|
||||
config: c.config.Clone(),
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,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")
|
||||
@@ -471,8 +485,15 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = C.TCPTimeout
|
||||
}
|
||||
serverConfig := &STDServerConfig{
|
||||
config: tlsConfig,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
logger: logger,
|
||||
certificateProvider: certificateProvider,
|
||||
acmeService: acmeService,
|
||||
|
||||
@@ -28,6 +28,10 @@ import (
|
||||
type UTLSClientConfig struct {
|
||||
ctx context.Context
|
||||
config *utls.Config
|
||||
serverName string
|
||||
disableSNI bool
|
||||
verifyServerName bool
|
||||
handshakeTimeout time.Duration
|
||||
id utls.ClientHelloID
|
||||
fragment bool
|
||||
fragmentFallbackDelay time.Duration
|
||||
@@ -35,10 +39,20 @@ type UTLSClientConfig struct {
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) ServerName() string {
|
||||
return c.config.ServerName
|
||||
return c.serverName
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) SetServerName(serverName string) {
|
||||
c.serverName = serverName
|
||||
if c.disableSNI {
|
||||
c.config.ServerName = ""
|
||||
if c.verifyServerName {
|
||||
c.config.InsecureServerNameToVerify = serverName
|
||||
} else {
|
||||
c.config.InsecureServerNameToVerify = ""
|
||||
}
|
||||
return
|
||||
}
|
||||
c.config.ServerName = serverName
|
||||
}
|
||||
|
||||
@@ -53,6 +67,14 @@ func (c *UTLSClientConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) HandshakeTimeout() time.Duration {
|
||||
return c.handshakeTimeout
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
c.handshakeTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) STDConfig() (*STDConfig, error) {
|
||||
return nil, E.New("unsupported usage for uTLS")
|
||||
}
|
||||
@@ -69,9 +91,20 @@ func (c *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []by
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) Clone() Config {
|
||||
return &UTLSClientConfig{
|
||||
c.ctx, c.config.Clone(), c.id, c.fragment, c.fragmentFallbackDelay, c.recordFragment,
|
||||
cloned := &UTLSClientConfig{
|
||||
ctx: c.ctx,
|
||||
config: c.config.Clone(),
|
||||
serverName: c.serverName,
|
||||
disableSNI: c.disableSNI,
|
||||
verifyServerName: c.verifyServerName,
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
id: c.id,
|
||||
fragment: c.fragment,
|
||||
fragmentFallbackDelay: c.fragmentFallbackDelay,
|
||||
recordFragment: c.recordFragment,
|
||||
}
|
||||
cloned.SetServerName(cloned.serverName)
|
||||
return cloned
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) ECHConfigList() []byte {
|
||||
@@ -143,29 +176,29 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newUTLSClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
var serverName string
|
||||
if options.ServerName != "" {
|
||||
serverName = options.ServerName
|
||||
} else if serverAddress != "" {
|
||||
serverName = serverAddress
|
||||
}
|
||||
if serverName == "" && !options.Insecure {
|
||||
return nil, E.New("missing server_name or insecure=true")
|
||||
if serverName == "" && !options.Insecure && !allowEmptyServerName {
|
||||
return nil, errMissingServerName
|
||||
}
|
||||
|
||||
var tlsConfig utls.Config
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||
if !options.DisableSNI {
|
||||
tlsConfig.ServerName = serverName
|
||||
}
|
||||
if options.Insecure {
|
||||
tlsConfig.InsecureSkipVerify = options.Insecure
|
||||
} else if options.DisableSNI {
|
||||
if options.Reality != nil && options.Reality.Enabled {
|
||||
return nil, E.New("disable_sni is unsupported in reality")
|
||||
}
|
||||
tlsConfig.InsecureServerNameToVerify = serverName
|
||||
}
|
||||
if len(options.CertificatePublicKeySHA256) > 0 {
|
||||
if len(options.Certificate) > 0 || options.CertificatePath != "" {
|
||||
@@ -173,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 {
|
||||
@@ -251,11 +284,29 @@ func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre
|
||||
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
|
||||
return nil, E.New("client certificate and client key must be provided together")
|
||||
}
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = C.TCPTimeout
|
||||
}
|
||||
id, err := uTLSClientHelloID(options.UTLS.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var config Config = &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||
var config Config = &UTLSClientConfig{
|
||||
ctx: ctx,
|
||||
config: &tlsConfig,
|
||||
serverName: serverName,
|
||||
disableSNI: options.DisableSNI,
|
||||
verifyServerName: options.DisableSNI && !options.Insecure,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
id: id,
|
||||
fragment: options.Fragment,
|
||||
fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay),
|
||||
recordFragment: options.RecordFragment,
|
||||
}
|
||||
config.SetServerName(serverName)
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
if options.Reality != nil && options.Reality.Enabled {
|
||||
return nil, E.New("Reality is conflict with ECH")
|
||||
|
||||
@@ -12,10 +12,18 @@ import (
|
||||
)
|
||||
|
||||
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newUTLSClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`)
|
||||
}
|
||||
|
||||
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newRealityClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
package constant
|
||||
|
||||
const ACMETLS1Protocol = "acme-tls/1"
|
||||
|
||||
const (
|
||||
TLSEngineDefault = ""
|
||||
TLSEngineApple = "apple"
|
||||
)
|
||||
|
||||
@@ -139,9 +139,9 @@ type fakeRuleSet struct {
|
||||
beforeDecrementReference func()
|
||||
}
|
||||
|
||||
func (s *fakeRuleSet) Name() string { return "fake-rule-set" }
|
||||
func (s *fakeRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error { return nil }
|
||||
func (s *fakeRuleSet) PostStart() error { return nil }
|
||||
func (s *fakeRuleSet) Name() string { return "fake-rule-set" }
|
||||
func (s *fakeRuleSet) StartContext(context.Context) error { return nil }
|
||||
func (s *fakeRuleSet) PostStart() error { return nil }
|
||||
func (s *fakeRuleSet) Metadata() adapter.RuleSetMetadata {
|
||||
s.access.Lock()
|
||||
metadata := s.metadata
|
||||
|
||||
@@ -3,17 +3,18 @@ package transport
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"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"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
@@ -26,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"
|
||||
@@ -42,57 +43,20 @@ func RegisterHTTPS(registry *dns.TransportRegistry) {
|
||||
type HTTPSTransport struct {
|
||||
dns.TransportAdapter
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
headers http.Header
|
||||
method string
|
||||
host string
|
||||
queryHeaders http.Header
|
||||
transportAccess sync.Mutex
|
||||
transport *HTTPSTransportWrapper
|
||||
transport adapter.HTTPTransport
|
||||
transportResetAt time.Time
|
||||
}
|
||||
|
||||
func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
||||
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, "http/1.1"})
|
||||
}
|
||||
headers := options.Headers.Build()
|
||||
host := headers.Get("Host")
|
||||
if host != "" {
|
||||
headers.Del("Host")
|
||||
} else {
|
||||
if tlsConfig.ServerName() != "" {
|
||||
host = tlsConfig.ServerName()
|
||||
} else {
|
||||
host = options.Server
|
||||
}
|
||||
}
|
||||
destinationURL := url.URL{
|
||||
Scheme: "https",
|
||||
Host: host,
|
||||
}
|
||||
if destinationURL.Host == "" {
|
||||
destinationURL.Host = options.Server
|
||||
}
|
||||
if options.ServerPort != 0 && options.ServerPort != 443 {
|
||||
destinationURL.Host = net.JoinHostPort(destinationURL.Host, strconv.Itoa(int(options.ServerPort)))
|
||||
}
|
||||
path := options.Path
|
||||
if path == "" {
|
||||
path = "/dns-query"
|
||||
}
|
||||
err = sHTTP.URLSetPath(&destinationURL, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headers.Del("Host")
|
||||
headers.Set("Accept", MimeType)
|
||||
serverAddr := options.DNSServerAddressOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 443
|
||||
@@ -100,56 +64,123 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
if !serverAddr.IsValid() {
|
||||
return nil, E.New("invalid server address: ", serverAddr)
|
||||
}
|
||||
return NewHTTPSRaw(
|
||||
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions),
|
||||
logger,
|
||||
transportDialer,
|
||||
&destinationURL,
|
||||
headers,
|
||||
serverAddr,
|
||||
tlsConfig,
|
||||
), nil
|
||||
destinationURL := url.URL{
|
||||
Scheme: "https",
|
||||
Host: doHURLHost(serverAddr, 443),
|
||||
}
|
||||
path := options.Path
|
||||
if path == "" {
|
||||
path = "/dns-query"
|
||||
}
|
||||
err := sHTTP.URLSetPath(&destinationURL, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
method := strings.ToUpper(options.Method)
|
||||
if method == "" {
|
||||
method = http.MethodPost
|
||||
}
|
||||
switch method {
|
||||
case http.MethodGet, http.MethodPost:
|
||||
default:
|
||||
return nil, E.New("unsupported HTTPS DNS method: ", options.Method)
|
||||
}
|
||||
if method == http.MethodPost {
|
||||
headers.Set("Content-Type", MimeType)
|
||||
}
|
||||
httpClientOptions := options.HTTPClientOptions
|
||||
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 NewHTTPSRaw(
|
||||
func NewHTTPRaw(
|
||||
adapter dns.TransportAdapter,
|
||||
logger log.ContextLogger,
|
||||
logger logger.ContextLogger,
|
||||
dialer N.Dialer,
|
||||
destination *url.URL,
|
||||
headers http.Header,
|
||||
serverAddr M.Socksaddr,
|
||||
tlsConfig tls.Config,
|
||||
) *HTTPSTransport {
|
||||
method string,
|
||||
) (*HTTPSTransport, error) {
|
||||
if destination.Scheme == "https" && tlsConfig == nil {
|
||||
return nil, E.New("TLS transport unavailable")
|
||||
}
|
||||
queryHeaders := headers.Clone()
|
||||
host := queryHeaders.Get("Host")
|
||||
queryHeaders.Del("Host")
|
||||
queryHeaders.Set("Accept", MimeType)
|
||||
if method == http.MethodPost {
|
||||
queryHeaders.Set("Content-Type", MimeType)
|
||||
}
|
||||
currentTransport, err := httpclient.NewTransportWithDialer(dialer, tlsConfig, "", option.HTTPClientOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &HTTPSTransport{
|
||||
TransportAdapter: adapter,
|
||||
logger: logger,
|
||||
dialer: dialer,
|
||||
destination: destination,
|
||||
headers: headers,
|
||||
transport: NewHTTPSTransportWrapper(tls.NewDialer(dialer, tlsConfig), serverAddr),
|
||||
}
|
||||
method: method,
|
||||
host: host,
|
||||
queryHeaders: queryHeaders,
|
||||
transport: currentTransport,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -159,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
|
||||
@@ -181,17 +213,32 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
|
||||
requestBuffer.Release()
|
||||
return nil, err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage))
|
||||
requestURL := *t.destination
|
||||
var request *http.Request
|
||||
switch t.method {
|
||||
case http.MethodGet:
|
||||
query := requestURL.Query()
|
||||
query.Set("dns", base64.RawURLEncoding.EncodeToString(rawMessage))
|
||||
requestURL.RawQuery = query.Encode()
|
||||
request, err = http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), nil)
|
||||
default:
|
||||
request, err = http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), bytes.NewReader(rawMessage))
|
||||
}
|
||||
if err != nil {
|
||||
requestBuffer.Release()
|
||||
return nil, err
|
||||
}
|
||||
request.Header = t.headers.Clone()
|
||||
request.Header.Set("Content-Type", MimeType)
|
||||
request.Header.Set("Accept", MimeType)
|
||||
request.Header = t.queryHeaders.Clone()
|
||||
if t.host != "" {
|
||||
request.Host = t.host
|
||||
}
|
||||
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 {
|
||||
@@ -222,3 +269,13 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
|
||||
}
|
||||
return &responseMessage, nil
|
||||
}
|
||||
|
||||
func doHURLHost(serverAddr M.Socksaddr, defaultPort uint16) string {
|
||||
if serverAddr.Port != defaultPort {
|
||||
return serverAddr.String()
|
||||
}
|
||||
if serverAddr.IsIPv6() {
|
||||
return "[" + serverAddr.AddrString() + "]"
|
||||
}
|
||||
return serverAddr.AddrString()
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var errFallback = E.New("fallback to HTTP/1.1")
|
||||
|
||||
type HTTPSTransportWrapper struct {
|
||||
http2Transport *http2.Transport
|
||||
httpTransport *http.Transport
|
||||
fallback *atomic.Bool
|
||||
}
|
||||
|
||||
func NewHTTPSTransportWrapper(dialer tls.Dialer, serverAddr M.Socksaddr) *HTTPSTransportWrapper {
|
||||
var fallback atomic.Bool
|
||||
return &HTTPSTransportWrapper{
|
||||
http2Transport: &http2.Transport{
|
||||
DialTLSContext: func(ctx context.Context, _, _ string, _ *tls.STDConfig) (net.Conn, error) {
|
||||
tlsConn, err := dialer.DialTLSContext(ctx, serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state := tlsConn.ConnectionState()
|
||||
if state.NegotiatedProtocol == http2.NextProtoTLS {
|
||||
return tlsConn, nil
|
||||
}
|
||||
tlsConn.Close()
|
||||
fallback.Store(true)
|
||||
return nil, errFallback
|
||||
},
|
||||
},
|
||||
httpTransport: &http.Transport{
|
||||
DialTLSContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return dialer.DialTLSContext(ctx, serverAddr)
|
||||
},
|
||||
},
|
||||
fallback: &fallback,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTTPSTransportWrapper) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
if h.fallback.Load() {
|
||||
return h.httpTransport.RoundTrip(request)
|
||||
} else {
|
||||
response, err := h.http2Transport.RoundTrip(request)
|
||||
if err != nil {
|
||||
if errors.Is(err, errFallback) {
|
||||
return h.httpTransport.RoundTrip(request)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTTPSTransportWrapper) CloseIdleConnections() {
|
||||
h.http2Transport.CloseIdleConnections()
|
||||
h.httpTransport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (h *HTTPSTransportWrapper) Clone() *HTTPSTransportWrapper {
|
||||
return &HTTPSTransportWrapper{
|
||||
httpTransport: h.httpTransport,
|
||||
http2Transport: &http2.Transport{
|
||||
DialTLSContext: h.http2Transport.DialTLSContext,
|
||||
},
|
||||
fallback: h.fallback,
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
@@ -40,18 +41,23 @@ func RegisterHTTP3Transport(registry *dns.TransportRegistry) {
|
||||
|
||||
type HTTP3Transport struct {
|
||||
dns.TransportAdapter
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
headers http.Header
|
||||
serverAddr M.Socksaddr
|
||||
tlsConfig *tls.STDConfig
|
||||
transportAccess sync.Mutex
|
||||
transport *http3.Transport
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
headers http.Header
|
||||
handshakeTimeout time.Duration
|
||||
serverAddr M.Socksaddr
|
||||
tlsConfig *tls.STDConfig
|
||||
transportAccess sync.Mutex
|
||||
transport *http3.Transport
|
||||
}
|
||||
|
||||
func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
||||
remoteOptions := option.RemoteDNSServerOptions{
|
||||
DNSServerAddressOptions: options.DNSServerAddressOptions,
|
||||
}
|
||||
remoteOptions.DialerOptions = options.DialerOptions
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, remoteOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -61,6 +67,7 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handshakeTimeout := tlsConfig.HandshakeTimeout()
|
||||
stdConfig, err := tlsConfig.STDConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -102,11 +109,12 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
return nil, E.New("invalid server address: ", serverAddr)
|
||||
}
|
||||
t := &HTTP3Transport{
|
||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
|
||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, remoteOptions),
|
||||
logger: logger,
|
||||
dialer: transportDialer,
|
||||
destination: &destinationURL,
|
||||
headers: headers,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
serverAddr: serverAddr,
|
||||
tlsConfig: stdConfig,
|
||||
}
|
||||
@@ -115,8 +123,17 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
}
|
||||
|
||||
func (t *HTTP3Transport) newTransport() *http3.Transport {
|
||||
quicConfig := &quic.Config{}
|
||||
if t.handshakeTimeout > 0 {
|
||||
quicConfig.HandshakeIdleTimeout = t.handshakeTimeout
|
||||
}
|
||||
return &http3.Transport{
|
||||
QUICConfig: quicConfig,
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (*quic.Conn, error) {
|
||||
if t.handshakeTimeout > 0 && cfg.HandshakeIdleTimeout == 0 {
|
||||
cfg = cfg.Clone()
|
||||
cfg.HandshakeIdleTimeout = t.handshakeTimeout
|
||||
}
|
||||
conn, dialErr := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
|
||||
if dialErr != nil {
|
||||
return nil, dialErr
|
||||
|
||||
@@ -2,6 +2,29 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.14.0-alpha.11
|
||||
|
||||
* Add optimistic DNS cache **1**
|
||||
* Update NaiveProxy to 147.0.7727.49
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Optimistic DNS cache returns an expired cached response immediately while
|
||||
refreshing it in the background, reducing tail latency for repeated
|
||||
queries. Enabled via [`optimistic`](/configuration/dns/#optimistic)
|
||||
in DNS options, and can be persisted across restarts with the new
|
||||
[`store_dns`](/configuration/experimental/cache-file/#store_dns) cache
|
||||
file option. A per-query
|
||||
[`disable_optimistic_cache`](/configuration/dns/rule_action/#disable_optimistic_cache)
|
||||
field is also available on DNS rule actions and the `resolve` route rule
|
||||
action.
|
||||
|
||||
This deprecates the `independent_cache` DNS option (the DNS cache now
|
||||
always keys by transport) and the `store_rdrc` cache file option
|
||||
(replaced by `store_dns`); both will be removed in sing-box 1.16.0.
|
||||
See [Migration](/migration/#migrate-independent-dns-cache).
|
||||
|
||||
#### 1.14.0-alpha.10
|
||||
|
||||
* Add `evaluate` DNS rule action and Response Match Fields **1**
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-alert: `headers`, `tls`, Dial Fields moved to [HTTP Client Fields](#http-client-fields)
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
# DNS over HTTP3 (DoH3)
|
||||
@@ -15,27 +19,20 @@ icon: material/new-box
|
||||
{
|
||||
"type": "h3",
|
||||
"tag": "",
|
||||
|
||||
|
||||
"server": "",
|
||||
"server_port": 443,
|
||||
|
||||
"server_port": 0,
|
||||
|
||||
"path": "",
|
||||
"headers": {},
|
||||
|
||||
"tls": {},
|
||||
|
||||
// Dial Fields
|
||||
"method": "",
|
||||
|
||||
... // HTTP Client Fields
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "Difference from legacy H3 server"
|
||||
|
||||
* The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default.
|
||||
* The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead.
|
||||
|
||||
### Fields
|
||||
|
||||
#### server
|
||||
@@ -58,14 +55,14 @@ The path of the DNS server.
|
||||
|
||||
`/dns-query` will be used by default.
|
||||
|
||||
#### headers
|
||||
#### method
|
||||
|
||||
Additional headers to be sent to the DNS server.
|
||||
HTTP request method.
|
||||
|
||||
#### tls
|
||||
Available values: `GET`, `POST`.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
`POST` will be used by default.
|
||||
|
||||
### Dial Fields
|
||||
### HTTP Client Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-alert: `headers`、`tls`、拨号字段已移至 [HTTP 客户端字段](#http-客户端字段)
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
# DNS over HTTP3 (DoH3)
|
||||
@@ -17,25 +21,18 @@ icon: material/new-box
|
||||
"tag": "",
|
||||
|
||||
"server": "",
|
||||
"server_port": 443,
|
||||
"server_port": 0,
|
||||
|
||||
"path": "",
|
||||
"headers": {},
|
||||
"method": "",
|
||||
|
||||
"tls": {},
|
||||
|
||||
// 拨号字段
|
||||
... // HTTP 客户端字段
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "与旧版 H3 服务器的区别"
|
||||
|
||||
* 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。
|
||||
* 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。
|
||||
|
||||
### 字段
|
||||
|
||||
#### server
|
||||
@@ -58,14 +55,14 @@ DNS 服务器的路径。
|
||||
|
||||
默认使用 `/dns-query`。
|
||||
|
||||
#### headers
|
||||
#### method
|
||||
|
||||
发送到 DNS 服务器的额外标头。
|
||||
HTTP 请求方法。
|
||||
|
||||
#### tls
|
||||
可用值:`GET`、`POST`。
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
默认使用 `POST`。
|
||||
|
||||
### 拨号字段
|
||||
### HTTP 客户端字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-alert: `headers`, `tls`, Dial Fields moved to [HTTP Client Fields](#http-client-fields)
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
# DNS over HTTPS (DoH)
|
||||
@@ -15,27 +19,20 @@ icon: material/new-box
|
||||
{
|
||||
"type": "https",
|
||||
"tag": "",
|
||||
|
||||
|
||||
"server": "",
|
||||
"server_port": 443,
|
||||
|
||||
"server_port": 0,
|
||||
|
||||
"path": "",
|
||||
"headers": {},
|
||||
|
||||
"tls": {},
|
||||
|
||||
// Dial Fields
|
||||
"method": "",
|
||||
|
||||
... // HTTP Client Fields
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "Difference from legacy HTTPS server"
|
||||
|
||||
* The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default.
|
||||
* The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead.
|
||||
|
||||
### Fields
|
||||
|
||||
#### server
|
||||
@@ -58,14 +55,14 @@ The path of the DNS server.
|
||||
|
||||
`/dns-query` will be used by default.
|
||||
|
||||
#### headers
|
||||
#### method
|
||||
|
||||
Additional headers to be sent to the DNS server.
|
||||
HTTP request method.
|
||||
|
||||
#### tls
|
||||
Available values: `GET`, `POST`.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
`POST` will be used by default.
|
||||
|
||||
### Dial Fields
|
||||
### HTTP Client Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-alert: `headers`、`tls`、拨号字段已移至 [HTTP 客户端字段](#http-客户端字段)
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
# DNS over HTTPS (DoH)
|
||||
@@ -17,25 +21,18 @@ icon: material/new-box
|
||||
"tag": "",
|
||||
|
||||
"server": "",
|
||||
"server_port": 443,
|
||||
"server_port": 0,
|
||||
|
||||
"path": "",
|
||||
"headers": {},
|
||||
"method": "",
|
||||
|
||||
"tls": {},
|
||||
|
||||
// 拨号字段
|
||||
... // HTTP 客户端字段
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "与旧版 HTTPS 服务器的区别"
|
||||
|
||||
* 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。
|
||||
* 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。
|
||||
|
||||
### 字段
|
||||
|
||||
#### server
|
||||
@@ -58,14 +55,14 @@ DNS 服务器的路径。
|
||||
|
||||
默认使用 `/dns-query`。
|
||||
|
||||
#### headers
|
||||
#### method
|
||||
|
||||
发送到 DNS 服务器的额外标头。
|
||||
HTTP 请求方法。
|
||||
|
||||
#### tls
|
||||
可用值:`GET`、`POST`。
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
默认使用 `POST`。
|
||||
|
||||
### 拨号字段
|
||||
### HTTP 客户端字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [control_http_client](#control_http_client)
|
||||
:material-delete-clock: [Dial Fields](#dial-fields)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [relay_server_port](#relay_server_port)
|
||||
@@ -22,6 +27,7 @@ icon: material/new-box
|
||||
"state_directory": "",
|
||||
"auth_key": "",
|
||||
"control_url": "",
|
||||
"control_http_client": {}, // or ""
|
||||
"ephemeral": false,
|
||||
"hostname": "",
|
||||
"accept_routes": false,
|
||||
@@ -148,10 +154,18 @@ UDP NAT expiration time.
|
||||
|
||||
`5m` will be used by default.
|
||||
|
||||
#### control_http_client
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
HTTP Client for connecting to the Tailscale control plane.
|
||||
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
!!! note
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Dial Fields in Tailscale endpoints only control how it connects to the control plane and have nothing to do with actual connections.
|
||||
Dial Fields in Tailscale endpoints are deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0, use `control_http_client` instead.
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [control_http_client](#control_http_client)
|
||||
:material-delete-clock: [拨号字段](#拨号字段)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [relay_server_port](#relay_server_port)
|
||||
@@ -22,6 +27,7 @@ icon: material/new-box
|
||||
"state_directory": "",
|
||||
"auth_key": "",
|
||||
"control_url": "",
|
||||
"control_http_client": {}, // 或 ""
|
||||
"ephemeral": false,
|
||||
"hostname": "",
|
||||
"accept_routes": false,
|
||||
@@ -147,10 +153,18 @@ UDP NAT 过期时间。
|
||||
|
||||
默认使用 `5m`。
|
||||
|
||||
#### control_http_client
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
用于连接 Tailscale 控制平面的 HTTP 客户端。
|
||||
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
!!! note
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
Tailscale 端点中的拨号字段仅控制它如何连接到控制平面,与实际连接无关。
|
||||
Tailscale 端点中的拨号字段已在 sing-box 1.14.0 废弃且将在 sing-box 1.16.0 中被移除,请使用 `control_http_client` 代替。
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||
|
||||
@@ -21,11 +21,16 @@
|
||||
}
|
||||
],
|
||||
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
// Deprecated
|
||||
|
||||
"recv_window_conn": 0,
|
||||
"recv_window_client": 0,
|
||||
"max_conn_client": 0,
|
||||
"disable_mtu_discovery": false,
|
||||
"tls": {}
|
||||
"disable_mtu_discovery": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -76,32 +81,38 @@ Authentication password, in base64.
|
||||
|
||||
Authentication password.
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
The QUIC stream-level flow control window for receiving data.
|
||||
|
||||
`15728640 (15 MB/s)` will be used if empty.
|
||||
|
||||
#### recv_window_client
|
||||
|
||||
The QUIC connection-level flow control window for receiving data.
|
||||
|
||||
`67108864 (64 MB/s)` will be used if empty.
|
||||
|
||||
#### max_conn_client
|
||||
|
||||
The maximum number of QUIC concurrent bidirectional streams that a peer is allowed to open.
|
||||
|
||||
`1024` will be used if empty.
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
|
||||
|
||||
Force enabled on for systems other than Linux and Windows (according to upstream).
|
||||
|
||||
#### tls
|
||||
|
||||
==Required==
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
### Deprecated Fields
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `stream_receive_window` instead.
|
||||
|
||||
#### recv_window_client
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `connection_receive_window` instead.
|
||||
|
||||
#### max_conn_client
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `max_concurrent_streams` instead.
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `disable_path_mtu_discovery` instead.
|
||||
@@ -21,11 +21,16 @@
|
||||
}
|
||||
],
|
||||
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
// 废弃的
|
||||
|
||||
"recv_window_conn": 0,
|
||||
"recv_window_client": 0,
|
||||
"max_conn_client": 0,
|
||||
"disable_mtu_discovery": false,
|
||||
"tls": {}
|
||||
"disable_mtu_discovery": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -76,32 +81,38 @@ base64 编码的认证密码。
|
||||
|
||||
认证密码。
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
用于接收数据的 QUIC 流级流控制窗口。
|
||||
|
||||
默认 `15728640 (15 MB/s)`。
|
||||
|
||||
#### recv_window_client
|
||||
|
||||
用于接收数据的 QUIC 连接级流控制窗口。
|
||||
|
||||
默认 `67108864 (64 MB/s)`。
|
||||
|
||||
#### max_conn_client
|
||||
|
||||
允许对等点打开的 QUIC 并发双向流的最大数量。
|
||||
|
||||
默认 `1024`。
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
禁用路径 MTU 发现 (RFC 8899)。 数据包的大小最多为 1252 (IPv4) / 1232 (IPv6) 字节。
|
||||
|
||||
强制为 Linux 和 Windows 以外的系统启用(根据上游)。
|
||||
|
||||
#### tls
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
### 废弃字段
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `stream_receive_window` 代替。
|
||||
|
||||
#### recv_window_client
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `connection_receive_window` 代替。
|
||||
|
||||
#### max_conn_client
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `max_concurrent_streams` 代替。
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `disable_path_mtu_discovery` 代替。
|
||||
@@ -34,6 +34,9 @@ icon: material/alert-decagram
|
||||
],
|
||||
"ignore_client_bandwidth": false,
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
"masquerade": "", // or {}
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false
|
||||
@@ -95,6 +98,10 @@ Deny clients to use the BBR CC.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
#### masquerade
|
||||
|
||||
HTTP3 server behavior (URL string configuration) when authentication fails.
|
||||
|
||||
@@ -34,6 +34,9 @@ icon: material/alert-decagram
|
||||
],
|
||||
"ignore_client_bandwidth": false,
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
"masquerade": "", // 或 {}
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false
|
||||
@@ -92,6 +95,10 @@ Hysteria 用户
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
#### masquerade
|
||||
|
||||
HTTP3 服务器认证失败时的行为 (URL 字符串配置)。
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
"auth_timeout": "3s",
|
||||
"zero_rtt_handshake": false,
|
||||
"heartbeat": "10s",
|
||||
"tls": {}
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,4 +77,8 @@ Interval for sending heartbeat packets for keeping the connection alive
|
||||
|
||||
==Required==
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
@@ -18,7 +18,9 @@
|
||||
"auth_timeout": "3s",
|
||||
"zero_rtt_handshake": false,
|
||||
"heartbeat": "10s",
|
||||
"tls": {}
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,4 +77,8 @@ QUIC 拥塞控制算法
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
@@ -10,6 +10,7 @@ sing-box uses JSON for configuration files.
|
||||
"ntp": {},
|
||||
"certificate": {},
|
||||
"certificate_providers": [],
|
||||
"http_clients": [],
|
||||
"endpoints": [],
|
||||
"inbounds": [],
|
||||
"outbounds": [],
|
||||
@@ -28,6 +29,7 @@ sing-box uses JSON for configuration files.
|
||||
| `ntp` | [NTP](./ntp/) |
|
||||
| `certificate` | [Certificate](./certificate/) |
|
||||
| `certificate_providers` | [Certificate Provider](./shared/certificate-provider/) |
|
||||
| `http_clients` | [HTTP Client](./shared/http-client/) |
|
||||
| `endpoints` | [Endpoint](./endpoint/) |
|
||||
| `inbounds` | [Inbound](./inbound/) |
|
||||
| `outbounds` | [Outbound](./outbound/) |
|
||||
|
||||
@@ -10,6 +10,7 @@ sing-box 使用 JSON 作为配置文件格式。
|
||||
"ntp": {},
|
||||
"certificate": {},
|
||||
"certificate_providers": [],
|
||||
"http_clients": [],
|
||||
"endpoints": [],
|
||||
"inbounds": [],
|
||||
"outbounds": [],
|
||||
@@ -28,6 +29,7 @@ sing-box 使用 JSON 作为配置文件格式。
|
||||
| `ntp` | [NTP](./ntp/) |
|
||||
| `certificate` | [证书](./certificate/) |
|
||||
| `certificate_providers` | [证书提供者](./shared/certificate-provider/) |
|
||||
| `http_clients` | [HTTP 客户端](./shared/http-client/) |
|
||||
| `endpoints` | [端点](./endpoint/) |
|
||||
| `inbounds` | [入站](./inbound/) |
|
||||
| `outbounds` | [出站](./outbound/) |
|
||||
|
||||
@@ -27,13 +27,18 @@ icon: material/new-box
|
||||
"obfs": "fuck me till the daylight",
|
||||
"auth": "",
|
||||
"auth_str": "password",
|
||||
"network": "",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
... // Dial Fields
|
||||
|
||||
// Deprecated
|
||||
|
||||
"recv_window_conn": 0,
|
||||
"recv_window": 0,
|
||||
"disable_mtu_discovery": false,
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // Dial Fields
|
||||
"disable_mtu_discovery": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -104,24 +109,6 @@ Authentication password, in base64.
|
||||
|
||||
Authentication password.
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
The QUIC stream-level flow control window for receiving data.
|
||||
|
||||
`15728640 (15 MB/s)` will be used if empty.
|
||||
|
||||
#### recv_window
|
||||
|
||||
The QUIC connection-level flow control window for receiving data.
|
||||
|
||||
`67108864 (64 MB/s)` will be used if empty.
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
|
||||
|
||||
Force enabled on for systems other than Linux and Windows (according to upstream).
|
||||
|
||||
#### network
|
||||
|
||||
Enabled network
|
||||
@@ -136,6 +123,30 @@ Both is enabled by default.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
### Deprecated Fields
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `stream_receive_window` instead.
|
||||
|
||||
#### recv_window
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `connection_receive_window` instead.
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `disable_path_mtu_discovery` instead.
|
||||
|
||||
@@ -27,13 +27,18 @@ icon: material/new-box
|
||||
"obfs": "fuck me till the daylight",
|
||||
"auth": "",
|
||||
"auth_str": "password",
|
||||
"network": "",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
... // 拨号字段
|
||||
|
||||
// 废弃的
|
||||
|
||||
"recv_window_conn": 0,
|
||||
"recv_window": 0,
|
||||
"disable_mtu_discovery": false,
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // 拨号字段
|
||||
"disable_mtu_discovery": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -104,24 +109,6 @@ base64 编码的认证密码。
|
||||
|
||||
认证密码。
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
用于接收数据的 QUIC 流级流控制窗口。
|
||||
|
||||
默认 `15728640 (15 MB/s)`。
|
||||
|
||||
#### recv_window
|
||||
|
||||
用于接收数据的 QUIC 连接级流控制窗口。
|
||||
|
||||
默认 `67108864 (64 MB/s)`。
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
禁用路径 MTU 发现 (RFC 8899)。 数据包的大小最多为 1252 (IPv4) / 1232 (IPv6) 字节。
|
||||
|
||||
强制为 Linux 和 Windows 以外的系统启用(根据上游)。
|
||||
|
||||
#### network
|
||||
|
||||
启用的网络协议。
|
||||
@@ -136,7 +123,30 @@ base64 编码的认证密码。
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||
|
||||
### 废弃字段
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `stream_receive_window` 代替。
|
||||
|
||||
#### recv_window
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `connection_receive_window` 代替。
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `disable_path_mtu_discovery` 代替。
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
"password": "goofy_ahh_password",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false,
|
||||
|
||||
@@ -124,6 +127,10 @@ Both is enabled by default.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
#### bbr_profile
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
"password": "goofy_ahh_password",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false,
|
||||
|
||||
@@ -122,6 +125,10 @@ QUIC 流量混淆器密码.
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
#### bbr_profile
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
"heartbeat": "10s",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
```
|
||||
@@ -91,6 +93,10 @@ Both is enabled by default.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
"heartbeat": "10s",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
```
|
||||
@@ -99,6 +101,10 @@ UDP 包中继模式
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||
|
||||
@@ -6,6 +6,7 @@ icon: material/alert-decagram
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [default_http_client](#default_http_client)
|
||||
:material-plus: [find_neighbor](#find_neighbor)
|
||||
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
|
||||
|
||||
@@ -43,6 +44,7 @@ icon: material/alert-decagram
|
||||
"find_process": false,
|
||||
"find_neighbor": false,
|
||||
"dhcp_lease_files": [],
|
||||
"default_http_client": "",
|
||||
"default_domain_resolver": "", // or {}
|
||||
"default_network_strategy": "",
|
||||
"default_network_type": [],
|
||||
@@ -147,6 +149,14 @@ Custom DHCP lease file paths for hostname and MAC address resolution.
|
||||
|
||||
Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea) if empty.
|
||||
|
||||
#### default_http_client
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Tag of the default [HTTP Client](/configuration/shared/http-client/) used by remote rule-sets.
|
||||
|
||||
If empty and `http_clients` is defined, the first HTTP client is used.
|
||||
|
||||
#### default_domain_resolver
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
@@ -6,6 +6,7 @@ icon: material/alert-decagram
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [default_http_client](#default_http_client)
|
||||
:material-plus: [find_neighbor](#find_neighbor)
|
||||
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
|
||||
|
||||
@@ -45,6 +46,7 @@ icon: material/alert-decagram
|
||||
"find_process": false,
|
||||
"find_neighbor": false,
|
||||
"dhcp_lease_files": [],
|
||||
"default_http_client": "",
|
||||
"default_network_strategy": "",
|
||||
"default_fallback_delay": ""
|
||||
}
|
||||
@@ -146,6 +148,14 @@ icon: material/alert-decagram
|
||||
|
||||
为空时自动从常见 DHCP 服务器(dnsmasq、odhcpd、ISC dhcpd、Kea)检测。
|
||||
|
||||
#### default_http_client
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
远程规则集使用的默认 [HTTP 客户端](/zh/configuration/shared/http-client/) 的标签。
|
||||
|
||||
如果为空且 `http_clients` 已定义,将使用第一个 HTTP 客户端。
|
||||
|
||||
#### default_domain_resolver
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [http_client](#http_client)
|
||||
:material-delete-clock: [download_detour](#download_detour)
|
||||
|
||||
!!! quote "Changes in sing-box 1.10.0"
|
||||
|
||||
:material-plus: `type: inline`
|
||||
@@ -43,8 +48,12 @@
|
||||
"tag": "",
|
||||
"format": "source", // or binary
|
||||
"url": "",
|
||||
"download_detour": "", // optional
|
||||
"update_interval": "" // optional
|
||||
"http_client": "", // or {}
|
||||
"update_interval": "",
|
||||
|
||||
// Deprecated
|
||||
|
||||
"download_detour": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -102,14 +111,26 @@ File path of rule-set.
|
||||
|
||||
Download URL of rule-set.
|
||||
|
||||
#### download_detour
|
||||
#### http_client
|
||||
|
||||
Tag of the outbound to download rule-set.
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Default outbound will be used if empty.
|
||||
HTTP Client for downloading rule-set.
|
||||
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
Default transport will be used if empty.
|
||||
|
||||
#### update_interval
|
||||
|
||||
Update interval of rule-set.
|
||||
|
||||
`1d` will be used if empty.
|
||||
|
||||
#### download_detour
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
`download_detour` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0, use `http_client` instead.
|
||||
|
||||
Tag of the outbound to download rule-set.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [http_client](#http_client)
|
||||
:material-delete-clock: [download_detour](#download_detour)
|
||||
|
||||
!!! quote "sing-box 1.10.0 中的更改"
|
||||
|
||||
:material-plus: `type: inline`
|
||||
@@ -43,8 +48,12 @@
|
||||
"tag": "",
|
||||
"format": "source", // or binary
|
||||
"url": "",
|
||||
"download_detour": "", // 可选
|
||||
"update_interval": "" // 可选
|
||||
"http_client": "", // 或 {}
|
||||
"update_interval": "",
|
||||
|
||||
// 废弃的
|
||||
|
||||
"download_detour": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -102,14 +111,26 @@
|
||||
|
||||
规则集的下载 URL。
|
||||
|
||||
#### download_detour
|
||||
#### http_client
|
||||
|
||||
用于下载规则集的出站的标签。
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
如果为空,将使用默认出站。
|
||||
用于下载规则集的 HTTP 客户端。
|
||||
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
如果为空,将使用默认传输。
|
||||
|
||||
#### update_interval
|
||||
|
||||
规则集的更新间隔。
|
||||
|
||||
默认使用 `1d`。
|
||||
|
||||
#### download_detour
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
`download_detour` 已在 sing-box 1.14.0 废弃且将在 sing-box 1.16.0 中被移除,请使用 `http_client` 代替。
|
||||
|
||||
用于下载规则集的出站的标签。
|
||||
|
||||
@@ -58,9 +58,9 @@ Object format:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://my-headscale.com/verify",
|
||||
|
||||
... // Dial Fields
|
||||
"url": "",
|
||||
|
||||
... // HTTP Client Fields
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -58,9 +58,9 @@ Derper 配置文件路径。
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://my-headscale.com/verify",
|
||||
"url": "",
|
||||
|
||||
... // 拨号字段
|
||||
... // HTTP 客户端字段
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ icon: material/new-box
|
||||
|
||||
:material-plus: [account_key](#account_key)
|
||||
:material-plus: [key_type](#key_type)
|
||||
:material-plus: [detour](#detour)
|
||||
:material-plus: [http_client](#http_client)
|
||||
|
||||
# ACME
|
||||
|
||||
@@ -37,7 +37,7 @@ icon: material/new-box
|
||||
},
|
||||
"dns01_challenge": {},
|
||||
"key_type": "",
|
||||
"detour": ""
|
||||
"http_client": "" // or {}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -141,10 +141,10 @@ The private key type to generate for new certificates.
|
||||
| `rsa2048` | RSA |
|
||||
| `rsa4096` | RSA |
|
||||
|
||||
#### detour
|
||||
#### http_client
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
The tag of the upstream outbound.
|
||||
HTTP Client for all provider HTTP requests.
|
||||
|
||||
All provider HTTP requests will use this outbound.
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
@@ -6,7 +6,7 @@ icon: material/new-box
|
||||
|
||||
:material-plus: [account_key](#account_key)
|
||||
:material-plus: [key_type](#key_type)
|
||||
:material-plus: [detour](#detour)
|
||||
:material-plus: [http_client](#http_client)
|
||||
|
||||
# ACME
|
||||
|
||||
@@ -37,7 +37,7 @@ icon: material/new-box
|
||||
},
|
||||
"dns01_challenge": {},
|
||||
"key_type": "",
|
||||
"detour": ""
|
||||
"http_client": "" // 或 {}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -136,10 +136,12 @@ ACME DNS01 质询字段。如果配置,将禁用其他质询方法。
|
||||
| `rsa2048` | RSA |
|
||||
| `rsa4096` | RSA |
|
||||
|
||||
#### detour
|
||||
#### http_client
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
上游出站的标签。
|
||||
用于所有提供者 HTTP 请求的 HTTP 客户端。
|
||||
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
所有提供者 HTTP 请求将使用此出站。
|
||||
|
||||
@@ -19,7 +19,7 @@ icon: material/new-box
|
||||
"origin_ca_key": "",
|
||||
"request_type": "",
|
||||
"requested_validity": 0,
|
||||
"detour": ""
|
||||
"http_client": "" // or {}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,8 +75,8 @@ Available values: `7`, `30`, `90`, `365`, `730`, `1095`, `5475`.
|
||||
|
||||
`5475` days (15 years) is used if empty.
|
||||
|
||||
#### detour
|
||||
#### http_client
|
||||
|
||||
The tag of the upstream outbound.
|
||||
HTTP Client for all provider HTTP requests.
|
||||
|
||||
All provider HTTP requests will use this outbound.
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
@@ -19,7 +19,7 @@ icon: material/new-box
|
||||
"origin_ca_key": "",
|
||||
"request_type": "",
|
||||
"requested_validity": 0,
|
||||
"detour": ""
|
||||
"http_client": "" // 或 {}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,8 +75,8 @@ Cloudflare Origin CA Key。
|
||||
|
||||
如果为空,使用 `5475` 天(15 年)。
|
||||
|
||||
#### detour
|
||||
#### http_client
|
||||
|
||||
上游出站的标签。
|
||||
用于所有提供者 HTTP 请求的 HTTP 客户端。
|
||||
|
||||
所有提供者 HTTP 请求将使用此出站。
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
114
docs/configuration/shared/http-client.md
Normal file
114
docs/configuration/shared/http-client.md
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
### Structure
|
||||
|
||||
A string or an object.
|
||||
|
||||
When string, the tag of a shared [HTTP Client](/configuration/shared/http-client/) defined in top-level `http_clients`.
|
||||
|
||||
When object:
|
||||
|
||||
```json
|
||||
{
|
||||
"engine": "",
|
||||
"version": 0,
|
||||
"disable_version_fallback": false,
|
||||
"headers": {},
|
||||
|
||||
... // HTTP2 Fields
|
||||
|
||||
"tls": {},
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
Available values: `1`, `2`, `3`.
|
||||
|
||||
`2` is used by default.
|
||||
|
||||
When `3`, [HTTP2 Fields](#http2-fields) are replaced by [QUIC Fields](#quic-fields).
|
||||
|
||||
#### disable_version_fallback
|
||||
|
||||
Disable automatic fallback to lower HTTP version.
|
||||
|
||||
#### headers
|
||||
|
||||
Custom HTTP headers.
|
||||
|
||||
`Host` header is used as request host.
|
||||
|
||||
### HTTP2 Fields
|
||||
|
||||
When `version` is `2` (default).
|
||||
|
||||
See [HTTP2 Fields](/configuration/shared/http2/) for details.
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
When `version` is `3`.
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
### TLS Fields
|
||||
|
||||
See [TLS](/configuration/shared/tls/#outbound) for details.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
114
docs/configuration/shared/http-client.zh.md
Normal file
114
docs/configuration/shared/http-client.zh.md
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
### 结构
|
||||
|
||||
字符串或对象。
|
||||
|
||||
当为字符串时,为顶层 `http_clients` 中定义的共享 [HTTP 客户端](/zh/configuration/shared/http-client/) 的标签。
|
||||
|
||||
当为对象时:
|
||||
|
||||
```json
|
||||
{
|
||||
"engine": "",
|
||||
"version": 0,
|
||||
"disable_version_fallback": false,
|
||||
"headers": {},
|
||||
|
||||
... // HTTP2 字段
|
||||
|
||||
"tls": {},
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### 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 版本。
|
||||
|
||||
可用值:`1`、`2`、`3`。
|
||||
|
||||
默认使用 `2`。
|
||||
|
||||
当为 `3` 时,[HTTP2 字段](#http2-字段) 替换为 [QUIC 字段](#quic-字段)。
|
||||
|
||||
#### disable_version_fallback
|
||||
|
||||
禁用自动回退到更低的 HTTP 版本。
|
||||
|
||||
#### headers
|
||||
|
||||
自定义 HTTP 标头。
|
||||
|
||||
`Host` 标头用作请求主机。
|
||||
|
||||
### HTTP2 字段
|
||||
|
||||
当 `version` 为 `2`(默认)时。
|
||||
|
||||
参阅 [HTTP2 字段](/zh/configuration/shared/http2/) 了解详情。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
当 `version` 为 `3` 时。
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
### TLS 字段
|
||||
|
||||
参阅 [TLS](/zh/configuration/shared/tls/#出站) 了解详情。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||
43
docs/configuration/shared/http2.md
Normal file
43
docs/configuration/shared/http2.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"idle_timeout": "",
|
||||
"keep_alive_period": "",
|
||||
"stream_receive_window": "",
|
||||
"connection_receive_window": "",
|
||||
"max_concurrent_streams": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### idle_timeout
|
||||
|
||||
Idle connection timeout, in golang's Duration format.
|
||||
|
||||
#### keep_alive_period
|
||||
|
||||
Keep alive period, in golang's Duration format.
|
||||
|
||||
#### stream_receive_window
|
||||
|
||||
HTTP2 stream-level flow-control receive window size.
|
||||
|
||||
Accepts memory size format, e.g. `"64 MB"`.
|
||||
|
||||
#### connection_receive_window
|
||||
|
||||
HTTP2 connection-level flow-control receive window size.
|
||||
|
||||
Accepts memory size format, e.g. `"64 MB"`.
|
||||
|
||||
#### max_concurrent_streams
|
||||
|
||||
Maximum concurrent streams per connection.
|
||||
43
docs/configuration/shared/http2.zh.md
Normal file
43
docs/configuration/shared/http2.zh.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"idle_timeout": "",
|
||||
"keep_alive_period": "",
|
||||
"stream_receive_window": "",
|
||||
"connection_receive_window": "",
|
||||
"max_concurrent_streams": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### idle_timeout
|
||||
|
||||
空闲连接超时,采用 golang 的 Duration 格式。
|
||||
|
||||
#### keep_alive_period
|
||||
|
||||
Keep alive 周期,采用 golang 的 Duration 格式。
|
||||
|
||||
#### stream_receive_window
|
||||
|
||||
HTTP2 流级别流控接收窗口大小。
|
||||
|
||||
接受内存大小格式,例如 `"64 MB"`。
|
||||
|
||||
#### connection_receive_window
|
||||
|
||||
HTTP2 连接级别流控接收窗口大小。
|
||||
|
||||
接受内存大小格式,例如 `"64 MB"`。
|
||||
|
||||
#### max_concurrent_streams
|
||||
|
||||
每个连接的最大并发流数。
|
||||
30
docs/configuration/shared/quic.md
Normal file
30
docs/configuration/shared/quic.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"initial_packet_size": 0,
|
||||
"disable_path_mtu_discovery": false,
|
||||
|
||||
... // HTTP2 Fields
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### initial_packet_size
|
||||
|
||||
Initial QUIC packet size.
|
||||
|
||||
#### disable_path_mtu_discovery
|
||||
|
||||
Disable QUIC path MTU discovery.
|
||||
|
||||
### HTTP2 Fields
|
||||
|
||||
See [HTTP2 Fields](/configuration/shared/http2/) for details.
|
||||
30
docs/configuration/shared/quic.zh.md
Normal file
30
docs/configuration/shared/quic.zh.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"initial_packet_size": 0,
|
||||
"disable_path_mtu_discovery": false,
|
||||
|
||||
... // HTTP2 字段
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### initial_packet_size
|
||||
|
||||
初始 QUIC 数据包大小。
|
||||
|
||||
#### disable_path_mtu_discovery
|
||||
|
||||
禁用 QUIC 路径 MTU 发现。
|
||||
|
||||
### HTTP2 字段
|
||||
|
||||
参阅 [HTTP2 字段](/zh/configuration/shared/http2/) 了解详情。
|
||||
@@ -5,6 +5,7 @@ icon: material/new-box
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [certificate_provider](#certificate_provider)
|
||||
:material-plus: [handshake_timeout](#handshake_timeout)
|
||||
:material-delete-clock: [acme](#acme-fields)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
@@ -54,6 +55,7 @@ icon: material/new-box
|
||||
"key_path": "",
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false,
|
||||
"handshake_timeout": "",
|
||||
"certificate_provider": "",
|
||||
|
||||
// Deprecated
|
||||
@@ -106,6 +108,7 @@ icon: material/new-box
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"engine": "",
|
||||
"disable_sni": false,
|
||||
"server_name": "",
|
||||
"insecure": false,
|
||||
@@ -124,6 +127,9 @@ icon: material/new-box
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "",
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false,
|
||||
"handshake_timeout": "",
|
||||
"ech": {
|
||||
"enabled": false,
|
||||
"config": [],
|
||||
@@ -183,6 +189,49 @@ Cipher suite values:
|
||||
|
||||
Enable TLS.
|
||||
|
||||
#### engine
|
||||
|
||||
==Client only==
|
||||
|
||||
TLS engine to use.
|
||||
|
||||
Values:
|
||||
|
||||
* `go` (default)
|
||||
* `apple`
|
||||
|
||||
`apple` uses Network.framework, only available on Apple platforms and only supports **direct** TCP TLS client connections.
|
||||
|
||||
!!! 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.
|
||||
If you want to circumvent TLS fingerprint-based proxy censorship,
|
||||
use [NaiveProxy](/configuration/outbound/naive/) instead.
|
||||
|
||||
Supported fields:
|
||||
|
||||
* `server_name`
|
||||
* `insecure`
|
||||
* `alpn`
|
||||
* `min_version`
|
||||
* `max_version`
|
||||
* `certificate` / `certificate_path`
|
||||
* `certificate_public_key_sha256`
|
||||
* `handshake_timeout`
|
||||
|
||||
Unsupported fields:
|
||||
|
||||
* `disable_sni`
|
||||
* `cipher_suites`
|
||||
* `curve_preferences`
|
||||
* `client_certificate` / `client_certificate_path` / `client_key` / `client_key_path`
|
||||
* `fragment` / `record_fragment`
|
||||
* `kernel_tx` / `kernel_rx`
|
||||
* `ech`
|
||||
* `utls`
|
||||
* `reality`
|
||||
|
||||
#### disable_sni
|
||||
|
||||
==Client only==
|
||||
@@ -417,6 +466,14 @@ Enable kernel TLS transmit support.
|
||||
|
||||
Enable kernel TLS receive support.
|
||||
|
||||
#### handshake_timeout
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
TLS handshake timeout, in golang's Duration format.
|
||||
|
||||
`15s` is used by default.
|
||||
|
||||
#### certificate_provider
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
@@ -5,6 +5,7 @@ icon: material/new-box
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [certificate_provider](#certificate_provider)
|
||||
:material-plus: [handshake_timeout](#handshake_timeout)
|
||||
:material-delete-clock: [acme](#acme-字段)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
@@ -54,6 +55,7 @@ icon: material/new-box
|
||||
"key_path": "",
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false,
|
||||
"handshake_timeout": "",
|
||||
"certificate_provider": "",
|
||||
|
||||
// 废弃的
|
||||
@@ -106,6 +108,7 @@ icon: material/new-box
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"engine": "",
|
||||
"disable_sni": false,
|
||||
"server_name": "",
|
||||
"insecure": false,
|
||||
@@ -124,6 +127,9 @@ icon: material/new-box
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "",
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false,
|
||||
"handshake_timeout": "",
|
||||
"ech": {
|
||||
"enabled": false,
|
||||
"config": [],
|
||||
@@ -183,6 +189,48 @@ TLS 版本值:
|
||||
|
||||
启用 TLS
|
||||
|
||||
#### engine
|
||||
|
||||
==仅客户端==
|
||||
|
||||
要使用的 TLS 引擎。
|
||||
|
||||
可用值:
|
||||
|
||||
* `go`(默认)
|
||||
* `apple`
|
||||
|
||||
`apple` 使用 Network.framework,仅在 Apple 平台可用,且仅支持 **直接** TCP TLS 客户端连接。
|
||||
|
||||
!!! warning ""
|
||||
|
||||
仅供实验用途:由于 CGO 和 Network.framework 占用的内存都很多,
|
||||
不应在 iOS 和 tvOS 的热路径中使用。
|
||||
如果您想规避基于 TLS 指纹的代理审查,应使用 [NaiveProxy](/zh/configuration/outbound/naive/)。
|
||||
|
||||
支持的字段:
|
||||
|
||||
* `server_name`
|
||||
* `insecure`
|
||||
* `alpn`
|
||||
* `min_version`
|
||||
* `max_version`
|
||||
* `certificate` / `certificate_path`
|
||||
* `certificate_public_key_sha256`
|
||||
* `handshake_timeout`
|
||||
|
||||
不支持的字段:
|
||||
|
||||
* `disable_sni`
|
||||
* `cipher_suites`
|
||||
* `curve_preferences`
|
||||
* `client_certificate` / `client_certificate_path` / `client_key` / `client_key_path`
|
||||
* `fragment` / `record_fragment`
|
||||
* `kernel_tx` / `kernel_rx`
|
||||
* `ech`
|
||||
* `utls`
|
||||
* `reality`
|
||||
|
||||
#### disable_sni
|
||||
|
||||
==仅客户端==
|
||||
@@ -416,6 +464,14 @@ echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/
|
||||
|
||||
启用内核 TLS 接收支持。
|
||||
|
||||
#### handshake_timeout
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
TLS 握手超时,采用 golang 的 Duration 格式。
|
||||
|
||||
默认使用 `15s`。
|
||||
|
||||
#### certificate_provider
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
@@ -6,6 +6,27 @@ icon: material/delete-alert
|
||||
|
||||
## 1.14.0
|
||||
|
||||
#### Legacy `download_detour` remote rule-set option
|
||||
|
||||
Legacy `download_detour` remote rule-set option is deprecated,
|
||||
use `http_client` instead.
|
||||
|
||||
Old field will be removed in sing-box 1.16.0.
|
||||
|
||||
#### Implicit default HTTP client
|
||||
|
||||
Implicit default HTTP client using the default outbound for remote rule-sets is deprecated.
|
||||
Configure `http_clients` and `route.default_http_client` explicitly.
|
||||
|
||||
Old behavior will be removed in sing-box 1.16.0.
|
||||
|
||||
#### Legacy dialer options in Tailscale endpoint
|
||||
|
||||
Legacy dialer options in Tailscale endpoints are deprecated,
|
||||
use `control_http_client` instead.
|
||||
|
||||
Old fields will be removed in sing-box 1.16.0.
|
||||
|
||||
#### Inline ACME options in TLS
|
||||
|
||||
Inline ACME options (`tls.acme`) are deprecated
|
||||
|
||||
@@ -6,6 +6,27 @@ icon: material/delete-alert
|
||||
|
||||
## 1.14.0
|
||||
|
||||
#### 旧版远程规则集 `download_detour` 选项
|
||||
|
||||
旧版远程规则集 `download_detour` 选项已废弃,
|
||||
请使用 `http_client` 代替。
|
||||
|
||||
旧字段将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
#### 隐式默认 HTTP 客户端
|
||||
|
||||
使用默认出站为远程规则集隐式创建默认 HTTP 客户端的行为已废弃。
|
||||
请显式配置 `http_clients` 和 `route.default_http_client`。
|
||||
|
||||
旧行为将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
#### Tailscale 端点中的旧版拨号选项
|
||||
|
||||
Tailscale 端点中的旧版拨号选项已废弃,
|
||||
请使用 `control_http_client` 代替。
|
||||
|
||||
旧字段将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
#### TLS 中的内联 ACME 选项
|
||||
|
||||
TLS 中的内联 ACME 选项(`tls.acme`)已废弃,
|
||||
|
||||
@@ -93,6 +93,22 @@ var OptionInlineACME = Note{
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-inline-acme-to-certificate-provider",
|
||||
}
|
||||
|
||||
var OptionLegacyRuleSetDownloadDetour = Note{
|
||||
Name: "legacy-rule-set-download-detour",
|
||||
Description: "legacy `download_detour` remote rule-set option",
|
||||
DeprecatedVersion: "1.14.0",
|
||||
ScheduledVersion: "1.16.0",
|
||||
EnvName: "LEGACY_RULE_SET_DOWNLOAD_DETOUR",
|
||||
}
|
||||
|
||||
var OptionLegacyTailscaleEndpointDialer = Note{
|
||||
Name: "legacy-tailscale-endpoint-dialer",
|
||||
Description: "legacy dialer options in Tailscale endpoint",
|
||||
DeprecatedVersion: "1.14.0",
|
||||
ScheduledVersion: "1.16.0",
|
||||
EnvName: "LEGACY_TAILSCALE_ENDPOINT_DIALER",
|
||||
}
|
||||
|
||||
var OptionRuleSetIPCIDRAcceptEmpty = Note{
|
||||
Name: "dns-rule-rule-set-ip-cidr-accept-empty",
|
||||
Description: "Legacy `rule_set_ip_cidr_accept_empty` DNS rule item",
|
||||
@@ -138,14 +154,25 @@ var OptionStoreRDRC = Note{
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-store-rdrc",
|
||||
}
|
||||
|
||||
var OptionImplicitDefaultHTTPClient = Note{
|
||||
Name: "implicit-default-http-client",
|
||||
Description: "implicit default HTTP client using default outbound for remote rule-sets",
|
||||
DeprecatedVersion: "1.14.0",
|
||||
ScheduledVersion: "1.16.0",
|
||||
EnvName: "IMPLICIT_DEFAULT_HTTP_CLIENT",
|
||||
}
|
||||
|
||||
var Options = []Note{
|
||||
OptionOutboundDNSRuleItem,
|
||||
OptionMissingDomainResolver,
|
||||
OptionLegacyDomainStrategyOptions,
|
||||
OptionInlineACME,
|
||||
OptionLegacyRuleSetDownloadDetour,
|
||||
OptionLegacyTailscaleEndpointDialer,
|
||||
OptionRuleSetIPCIDRAcceptEmpty,
|
||||
OptionLegacyDNSAddressFilter,
|
||||
OptionLegacyDNSRuleStrategy,
|
||||
OptionIndependentDNSCache,
|
||||
OptionStoreRDRC,
|
||||
OptionImplicitDefaultHTTPClient,
|
||||
}
|
||||
|
||||
4
go.mod
4
go.mod
@@ -37,10 +37,10 @@ require (
|
||||
github.com/sagernet/gomobile v0.1.12
|
||||
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
|
||||
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
|
||||
github.com/sagernet/sing v0.8.5-0.20260404181712-947827ec3849
|
||||
github.com/sagernet/sing v0.8.5-0.20260413075835-f518f5326bd0
|
||||
github.com/sagernet/sing-cloudflared v0.0.0-20260407120610-7715dc2523fa
|
||||
github.com/sagernet/sing-mux v0.3.4
|
||||
github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212
|
||||
github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
|
||||
|
||||
8
go.sum
8
go.sum
@@ -242,14 +242,14 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen
|
||||
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
||||
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
|
||||
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
|
||||
github.com/sagernet/sing v0.8.5-0.20260404181712-947827ec3849 h1:P8jaGN561IbHBxjlU8IGrFK65n1vDOrHo8FOMgHfn14=
|
||||
github.com/sagernet/sing v0.8.5-0.20260404181712-947827ec3849/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing v0.8.5-0.20260413075835-f518f5326bd0 h1:UY13Df9qLR5ZQfWP3kpUlkz12UXp+oc0yxUva7gM6rM=
|
||||
github.com/sagernet/sing v0.8.5-0.20260413075835-f518f5326bd0/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-cloudflared v0.0.0-20260407120610-7715dc2523fa h1:165HiOfgfofJIirEp1NGSmsoJAi+++WhR29IhtAu4A4=
|
||||
github.com/sagernet/sing-cloudflared v0.0.0-20260407120610-7715dc2523fa/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y=
|
||||
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
|
||||
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
|
||||
github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212 h1:7mFOUqy+DyOj7qKGd1X54UMXbnbJiiMileK/tn17xYc=
|
||||
github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8=
|
||||
github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6 h1:j3ISQRDyY5rs27NzUS/le+DHR0iOO0K0x+mWDLzu4Ok=
|
||||
github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6/go.mod h1:r5Adw0EMUyhGBCjPI2JEupDtC040DrrvreXtua7Ifdc=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
|
||||
|
||||
@@ -122,6 +122,9 @@ nav:
|
||||
- Listen Fields: configuration/shared/listen.md
|
||||
- Dial Fields: configuration/shared/dial.md
|
||||
- TLS: configuration/shared/tls.md
|
||||
- HTTP Client: configuration/shared/http-client.md
|
||||
- HTTP2 Fields: configuration/shared/http2.md
|
||||
- QUIC Fields: configuration/shared/quic.md
|
||||
- Certificate Provider:
|
||||
- configuration/shared/certificate-provider/index.md
|
||||
- ACME: configuration/shared/certificate-provider/acme.md
|
||||
|
||||
@@ -24,7 +24,7 @@ type ACMECertificateProviderOptions struct {
|
||||
ExternalAccount *ACMEExternalAccountOptions `json:"external_account,omitempty"`
|
||||
DNS01Challenge *ACMEProviderDNS01ChallengeOptions `json:"dns01_challenge,omitempty"`
|
||||
KeyType ACMEKeyType `json:"key_type,omitempty"`
|
||||
Detour string `json:"detour,omitempty"`
|
||||
HTTPClient *HTTPClientOptions `json:"http_client,omitempty"`
|
||||
}
|
||||
|
||||
type _ACMEProviderDNS01ChallengeOptions struct {
|
||||
|
||||
@@ -167,10 +167,10 @@ type RemoteTLSDNSServerOptions struct {
|
||||
}
|
||||
|
||||
type RemoteHTTPSDNSServerOptions struct {
|
||||
RemoteTLSDNSServerOptions
|
||||
Path string `json:"path,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Headers badoption.HTTPHeader `json:"headers,omitempty"`
|
||||
DNSServerAddressOptions
|
||||
Path string `json:"path,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
HTTPClientOptions
|
||||
}
|
||||
|
||||
type FakeIPDNSServerOptions struct {
|
||||
|
||||
126
option/http.go
Normal file
126
option/http.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
type HTTP2Options struct {
|
||||
IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"`
|
||||
KeepAlivePeriod badoption.Duration `json:"keep_alive_period,omitempty"`
|
||||
StreamReceiveWindow byteformats.MemoryBytes `json:"stream_receive_window,omitempty"`
|
||||
ConnectionReceiveWindow byteformats.MemoryBytes `json:"connection_receive_window,omitempty"`
|
||||
MaxConcurrentStreams int `json:"max_concurrent_streams,omitempty"`
|
||||
}
|
||||
|
||||
type QUICOptions struct {
|
||||
HTTP2Options
|
||||
InitialPacketSize int `json:"initial_packet_size,omitempty"`
|
||||
DisablePathMTUDiscovery bool `json:"disable_path_mtu_discovery,omitempty"`
|
||||
}
|
||||
|
||||
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"`
|
||||
HTTP2Options HTTP2Options `json:"-"`
|
||||
HTTP3Options QUICOptions `json:"-"`
|
||||
DefaultOutbound bool `json:"-"`
|
||||
ResolveOnDetour bool `json:"-"`
|
||||
DirectResolver bool `json:"-"`
|
||||
OutboundTLSOptionsContainer
|
||||
DialerOptions
|
||||
}
|
||||
|
||||
type (
|
||||
HTTPClient _HTTPClientOptions
|
||||
HTTPClientOptions _HTTPClientOptions
|
||||
)
|
||||
|
||||
func (h HTTPClient) Options() HTTPClientOptions {
|
||||
options := HTTPClientOptions(h)
|
||||
options.Tag = ""
|
||||
return options
|
||||
}
|
||||
|
||||
func (o HTTPClientOptions) IsEmpty() bool {
|
||||
if o.Tag != "" {
|
||||
return false
|
||||
}
|
||||
o.DefaultOutbound = false
|
||||
o.ResolveOnDetour = false
|
||||
o.DirectResolver = false
|
||||
return reflect.ValueOf(_HTTPClientOptions(o)).IsZero()
|
||||
}
|
||||
|
||||
func (o HTTPClientOptions) MarshalJSON() ([]byte, error) {
|
||||
if o.Tag != "" {
|
||||
return json.Marshal(o.Tag)
|
||||
}
|
||||
return badjson.MarshallObjects(_HTTPClientOptions(o), httpClientVariant(_HTTPClientOptions(o)))
|
||||
}
|
||||
|
||||
func (o *HTTPClientOptions) UnmarshalJSON(content []byte) error {
|
||||
if len(content) > 0 && content[0] == '"' {
|
||||
*o = HTTPClientOptions{}
|
||||
return json.Unmarshal(content, &o.Tag)
|
||||
}
|
||||
var options _HTTPClientOptions
|
||||
err := json.Unmarshal(content, &options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = unmarshalHTTPClientVersionOptions(content, &options, &options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.Tag = ""
|
||||
*o = HTTPClientOptions(options)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h HTTPClient) MarshalJSON() ([]byte, error) {
|
||||
return badjson.MarshallObjects(_HTTPClientOptions(h), httpClientVariant(_HTTPClientOptions(h)))
|
||||
}
|
||||
|
||||
func (h *HTTPClient) UnmarshalJSON(content []byte) error {
|
||||
err := json.Unmarshal(content, (*_HTTPClientOptions)(h))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmarshalHTTPClientVersionOptions(content, (*_HTTPClientOptions)(h), (*_HTTPClientOptions)(h))
|
||||
}
|
||||
|
||||
func unmarshalHTTPClientVersionOptions(content []byte, baseStruct any, options *_HTTPClientOptions) error {
|
||||
switch options.Version {
|
||||
case 1:
|
||||
return json.UnmarshalDisallowUnknownFields(content, baseStruct)
|
||||
case 0, 2:
|
||||
options.Version = 2
|
||||
return badjson.UnmarshallExcluded(content, baseStruct, &options.HTTP2Options)
|
||||
case 3:
|
||||
return badjson.UnmarshallExcluded(content, baseStruct, &options.HTTP3Options)
|
||||
default:
|
||||
return E.New("unknown HTTP version: ", options.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func httpClientVariant(options _HTTPClientOptions) any {
|
||||
switch options.Version {
|
||||
case 1:
|
||||
return nil
|
||||
case 0, 2:
|
||||
return options.HTTP2Options
|
||||
case 3:
|
||||
return options.HTTP3Options
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -7,17 +7,22 @@ import (
|
||||
|
||||
type HysteriaInboundOptions struct {
|
||||
ListenOptions
|
||||
Up *byteformats.NetworkBytesCompat `json:"up,omitempty"`
|
||||
UpMbps int `json:"up_mbps,omitempty"`
|
||||
Down *byteformats.NetworkBytesCompat `json:"down,omitempty"`
|
||||
DownMbps int `json:"down_mbps,omitempty"`
|
||||
Obfs string `json:"obfs,omitempty"`
|
||||
Users []HysteriaUser `json:"users,omitempty"`
|
||||
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
|
||||
ReceiveWindowClient uint64 `json:"recv_window_client,omitempty"`
|
||||
MaxConnClient int `json:"max_conn_client,omitempty"`
|
||||
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
|
||||
Up *byteformats.NetworkBytesCompat `json:"up,omitempty"`
|
||||
UpMbps int `json:"up_mbps,omitempty"`
|
||||
Down *byteformats.NetworkBytesCompat `json:"down,omitempty"`
|
||||
DownMbps int `json:"down_mbps,omitempty"`
|
||||
Obfs string `json:"obfs,omitempty"`
|
||||
Users []HysteriaUser `json:"users,omitempty"`
|
||||
// Deprecated: use QUIC fields instead
|
||||
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
|
||||
// Deprecated: use QUIC fields instead
|
||||
ReceiveWindowClient uint64 `json:"recv_window_client,omitempty"`
|
||||
// Deprecated: use QUIC fields instead
|
||||
MaxConnClient int `json:"max_conn_client,omitempty"`
|
||||
// Deprecated: use QUIC fields instead
|
||||
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
|
||||
InboundTLSOptionsContainer
|
||||
QUICOptions
|
||||
}
|
||||
|
||||
type HysteriaUser struct {
|
||||
@@ -29,18 +34,22 @@ type HysteriaUser struct {
|
||||
type HysteriaOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"`
|
||||
HopInterval badoption.Duration `json:"hop_interval,omitempty"`
|
||||
Up *byteformats.NetworkBytesCompat `json:"up,omitempty"`
|
||||
UpMbps int `json:"up_mbps,omitempty"`
|
||||
Down *byteformats.NetworkBytesCompat `json:"down,omitempty"`
|
||||
DownMbps int `json:"down_mbps,omitempty"`
|
||||
Obfs string `json:"obfs,omitempty"`
|
||||
Auth []byte `json:"auth,omitempty"`
|
||||
AuthString string `json:"auth_str,omitempty"`
|
||||
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
|
||||
ReceiveWindow uint64 `json:"recv_window,omitempty"`
|
||||
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
|
||||
Network NetworkList `json:"network,omitempty"`
|
||||
ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"`
|
||||
HopInterval badoption.Duration `json:"hop_interval,omitempty"`
|
||||
Up *byteformats.NetworkBytesCompat `json:"up,omitempty"`
|
||||
UpMbps int `json:"up_mbps,omitempty"`
|
||||
Down *byteformats.NetworkBytesCompat `json:"down,omitempty"`
|
||||
DownMbps int `json:"down_mbps,omitempty"`
|
||||
Obfs string `json:"obfs,omitempty"`
|
||||
Auth []byte `json:"auth,omitempty"`
|
||||
AuthString string `json:"auth_str,omitempty"`
|
||||
// Deprecated: use QUIC fields instead
|
||||
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
|
||||
// Deprecated: use QUIC fields instead
|
||||
ReceiveWindow uint64 `json:"recv_window,omitempty"`
|
||||
// Deprecated: use QUIC fields instead
|
||||
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
|
||||
Network NetworkList `json:"network,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
QUICOptions
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ type Hysteria2InboundOptions struct {
|
||||
Users []Hysteria2User `json:"users,omitempty"`
|
||||
IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"`
|
||||
InboundTLSOptionsContainer
|
||||
QUICOptions
|
||||
Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"`
|
||||
BBRProfile string `json:"bbr_profile,omitempty"`
|
||||
BrutalDebug bool `json:"brutal_debug,omitempty"`
|
||||
@@ -122,6 +123,7 @@ type Hysteria2OutboundOptions struct {
|
||||
Password string `json:"password,omitempty"`
|
||||
Network NetworkList `json:"network,omitempty"`
|
||||
OutboundTLSOptionsContainer
|
||||
QUICOptions
|
||||
BBRProfile string `json:"bbr_profile,omitempty"`
|
||||
BrutalDebug bool `json:"brutal_debug,omitempty"`
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ type _Options struct {
|
||||
NTP *NTPOptions `json:"ntp,omitempty"`
|
||||
Certificate *CertificateOptions `json:"certificate,omitempty"`
|
||||
CertificateProviders []CertificateProvider `json:"certificate_providers,omitempty"`
|
||||
HTTPClients []HTTPClient `json:"http_clients,omitempty"`
|
||||
Endpoints []Endpoint `json:"endpoints,omitempty"`
|
||||
Inbounds []Inbound `json:"inbounds,omitempty"`
|
||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||
@@ -61,6 +62,10 @@ func checkOptions(options *Options) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = checkHTTPClients(options.HTTPClients)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -79,6 +84,20 @@ func checkCertificateProviders(providers []CertificateProvider) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkHTTPClients(clients []HTTPClient) error {
|
||||
seen := make(map[string]bool)
|
||||
for _, client := range clients {
|
||||
if client.Tag == "" {
|
||||
return E.New("missing http client tag")
|
||||
}
|
||||
if seen[client.Tag] {
|
||||
return E.New("duplicate http client tag: ", client.Tag)
|
||||
}
|
||||
seen[client.Tag] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkInbounds(inbounds []Inbound) error {
|
||||
seen := make(map[string]bool)
|
||||
for i, inbound := range inbounds {
|
||||
|
||||
@@ -15,7 +15,7 @@ type CloudflareOriginCACertificateProviderOptions struct {
|
||||
OriginCAKey string `json:"origin_ca_key,omitempty"`
|
||||
RequestType CloudflareOriginCARequestType `json:"request_type,omitempty"`
|
||||
RequestedValidity CloudflareOriginCARequestValidity `json:"requested_validity,omitempty"`
|
||||
Detour string `json:"detour,omitempty"`
|
||||
HTTPClient *HTTPClientOptions `json:"http_client,omitempty"`
|
||||
}
|
||||
|
||||
type CloudflareOriginCARequestType string
|
||||
|
||||
@@ -20,6 +20,7 @@ type RouteOptions struct {
|
||||
DefaultNetworkType badoption.Listable[InterfaceType] `json:"default_network_type,omitempty"`
|
||||
DefaultFallbackNetworkType badoption.Listable[InterfaceType] `json:"default_fallback_network_type,omitempty"`
|
||||
DefaultFallbackDelay badoption.Duration `json:"default_fallback_delay,omitempty"`
|
||||
DefaultHTTPClient string `json:"default_http_client,omitempty"`
|
||||
}
|
||||
|
||||
type GeoIPOptions struct {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user