Compare commits

..

28 Commits

Author SHA1 Message Date
世界
0cc4baae12 documentation: Fix missing update for ip_version and query_type 2026-04-10 15:04:12 +08:00
世界
7f1d02492b Add cloudflared inbound 2026-04-10 14:54:28 +08:00
世界
c40b2bac3d Fix lint errors 2026-04-10 14:54:28 +08:00
世界
b586655e7a platform: Wrap command RPC error returns with E.Cause 2026-04-10 14:54:28 +08:00
世界
e59c9dc3d4 Add package_name_regex route, DNS and headless rule item 2026-04-10 14:54:28 +08:00
世界
5181736a30 documentation: Fixes 2026-04-10 14:17:03 +08:00
世界
87d3436825 Un-deprecate ip_accept_any DNS rule item 2026-04-10 14:17:03 +08:00
世界
773e768a09 tools: Tailscale status 2026-04-10 14:17:03 +08:00
世界
7465a75796 Fix darwin local DNS transport 2026-04-10 14:17:03 +08:00
世界
0486b28e46 Fix rules lock 2026-04-10 14:17:03 +08:00
世界
80ea102043 Revert "Also enable certificate store by default on Apple platforms"
This reverts commit 62cb06c02f.
2026-04-10 14:17:03 +08:00
世界
9fb7e5f709 tools: Tailscale status 2026-04-10 14:17:03 +08:00
世界
08f07f5f77 platform: Fix darwin signal handler 2026-04-10 14:17:03 +08:00
世界
bb24c1d70a tools: Network Quality & STUN 2026-04-10 14:17:03 +08:00
世界
3dd9c777b6 oom-killer: Free memory on pressure notification and use gradual interval backoff 2026-04-10 14:17:03 +08:00
世界
6f3a759939 Fix deprecated warning double-formatting on localized clients 2026-04-10 14:17:03 +08:00
世界
1a7ff1abd2 platform: Fix set local 2026-04-10 14:17:03 +08:00
nekohasekai
f9fb4dd659 Add evaluate DNS rule action and related rule items 2026-04-10 14:17:03 +08:00
世界
8d67adbd75 Also enable certificate store by default on Apple platforms
`SecTrustEvaluateWithError` is serial
2026-04-10 14:16:46 +08:00
世界
3af9a5f684 platform: Add OOM Report & Crash Rerport 2026-04-10 14:15:55 +08:00
世界
d647a9e7b9 Add BBR profile and hop interval randomization for Hysteria2 2026-04-10 14:15:55 +08:00
nekohasekai
cc2098c0da Refactor ACME support to certificate provider 2026-04-10 14:15:55 +08:00
世界
733361c567 cronet-go: Update chromium to 145.0.7632.159 2026-04-10 14:15:55 +08:00
世界
dcc41a17cc documentation: Update descriptions for neighbor rules 2026-04-10 14:15:55 +08:00
世界
1092643667 Add macOS support for MAC and hostname rule items 2026-04-10 14:15:55 +08:00
世界
2dbe252d34 Add Android support for MAC and hostname rule items 2026-04-10 14:15:55 +08:00
世界
4ca79a465a Add MAC and hostname rule items 2026-04-10 14:15:49 +08:00
世界
b960cb087f Bump version 2026-04-10 14:13:36 +08:00
153 changed files with 8824 additions and 15058 deletions

View File

@@ -1 +1 @@
335e5bef5d88fc4474c9a70b865561f45a67de83
ea7cd33752aed62603775af3df946c1b83f4b0b3

View File

@@ -3,7 +3,6 @@ package adapter
import (
"context"
"net/netip"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
@@ -32,13 +31,12 @@ type DNSClient interface {
}
type DNSQueryOptions struct {
Transport DNSTransport
Strategy C.DomainStrategy
LookupStrategy C.DomainStrategy
DisableCache bool
DisableOptimisticCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
Transport DNSTransport
Strategy C.DomainStrategy
LookupStrategy C.DomainStrategy
DisableCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
}
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
@@ -51,12 +49,11 @@ func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptio
return nil, E.New("domain resolver not found: " + options.Server)
}
return &DNSQueryOptions{
Transport: transport,
Strategy: C.DomainStrategy(options.Strategy),
DisableCache: options.DisableCache,
DisableOptimisticCache: options.DisableOptimisticCache,
RewriteTTL: options.RewriteTTL,
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
Transport: transport,
Strategy: C.DomainStrategy(options.Strategy),
DisableCache: options.DisableCache,
RewriteTTL: options.RewriteTTL,
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
}, nil
}
@@ -66,13 +63,6 @@ type RDRCStore interface {
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
}
type DNSCacheStore interface {
LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool)
SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error
SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger)
ClearDNSCache() error
}
type DNSTransport interface {
Lifecycle
Type() string

View File

@@ -47,12 +47,6 @@ type CacheFile interface {
StoreRDRC() bool
RDRCStore
StoreDNS() bool
DNSCacheStore
SetDisableExpire(disableExpire bool)
SetOptimisticTimeout(timeout time.Duration)
LoadMode() string
StoreMode(mode string) error
LoadSelected(group string) string

View File

@@ -1,22 +0,0 @@
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()
}

View File

@@ -2,11 +2,17 @@ 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"
@@ -45,7 +51,7 @@ type ConnectionRouterEx interface {
type RuleSet interface {
Name() string
StartContext(ctx context.Context) error
StartContext(ctx context.Context, startContext *HTTPStartContext) error
PostStart() error
Metadata() RuleSetMetadata
ExtractIPSet() []*netipx.IPSet
@@ -71,3 +77,46 @@ 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()
}
}

42
box.go
View File

@@ -16,14 +16,12 @@ 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"
@@ -52,7 +50,6 @@ type Box struct {
dnsRouter *dns.Router
connection *route.ConnectionManager
router *route.Router
httpClientService adapter.LifecycleService
internalService []adapter.LifecycleService
done chan struct{}
}
@@ -172,7 +169,6 @@ 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 ||
@@ -185,6 +181,8 @@ 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)
@@ -198,10 +196,7 @@ func New(options Options) (*Box, error) {
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
dnsRouter, err := dns.NewRouter(ctx, logFactory, dnsOptions)
if err != nil {
return nil, E.Cause(err, "initialize DNS router")
}
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
@@ -211,10 +206,6 @@ 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)
@@ -374,12 +365,6 @@ 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 {
@@ -387,7 +372,7 @@ func New(options Options) (*Box, error) {
}
}
if needCacheFile {
cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile))
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
internalServices = append(internalServices, cacheFile)
}
@@ -440,7 +425,6 @@ func New(options Options) (*Box, error) {
dnsRouter: dnsRouter,
connection: connectionManager,
router: router,
httpClientService: httpClientService,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
@@ -503,15 +487,7 @@ 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)
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)
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection, s.router, s.dnsRouter)
if err != nil {
return err
}
@@ -588,14 +564,6 @@ 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()

View File

@@ -204,9 +204,6 @@ 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, ","))

View File

@@ -5,7 +5,6 @@ import (
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/sagernet/sing-box/log"
@@ -36,9 +35,21 @@ 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")
pemBundle := strings.Builder{}
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()
`)
for {
record, err := reader.Read()
if err == io.EOF {
@@ -49,12 +60,18 @@ func updateMozillaIncludedRootCAs() error {
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]
pemBundle.WriteString(cert)
pemBundle.WriteString("\n")
generated.WriteString(cert)
generated.WriteString("`))\n")
}
return writeGeneratedCertificateBundle("mozilla", "mozillaIncluded", pemBundle.String())
generated.WriteString("}\n")
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
}
func fetchChinaFingerprints() (map[string]bool, error) {
@@ -102,11 +119,23 @@ 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")
pemBundle := strings.Builder{}
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()
`)
for {
record, err := reader.Read()
if err == io.EOF {
@@ -120,39 +149,18 @@ func updateChromeIncludedRootCAs() error {
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]
}
pemBundle.WriteString(cert)
pemBundle.WriteString("\n")
generated.WriteString(cert)
generated.WriteString("`))\n")
}
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)
generated.WriteString("}\n")
return os.WriteFile("common/certificate/chrome.go", []byte(generated.String()), 0o644)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -22,10 +22,8 @@ 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
@@ -63,7 +61,6 @@ 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,
@@ -126,37 +123,19 @@ 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)
@@ -166,7 +145,6 @@ 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 {
@@ -179,8 +157,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) {
currentPEM = append(currentPEM, string(pemContent))
if err == nil {
currentPool.AppendCertsFromPEM(pemContent)
}
}
}
@@ -188,7 +166,6 @@ func (s *Store) update() error {
return firstErr
}
s.currentPool = currentPool
s.currentPEM = currentPEM
return nil
}

View File

@@ -19,7 +19,6 @@ type DirectDialer interface {
type DetourDialer struct {
outboundManager adapter.OutboundManager
detour string
defaultOutbound bool
legacyDNSDialer bool
dialer N.Dialer
initOnce sync.Once
@@ -34,13 +33,6 @@ 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 {
@@ -55,18 +47,12 @@ func (d *DetourDialer) Dialer() (N.Dialer, error) {
}
func (d *DetourDialer) init() {
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()
dialer, loaded := d.outboundManager.Outbound(d.detour)
if !loaded {
d.initErr = E.New("outbound detour not found: ", d.detour)
return
}
if !d.defaultOutbound && !d.legacyDNSDialer {
if !d.legacyDNSDialer {
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
if directDialer.IsEmpty() {
d.initErr = E.New("detour to an empty direct outbound makes no sense")

View File

@@ -25,7 +25,6 @@ type Options struct {
NewDialer bool
LegacyDNSDialer bool
DirectOutbound bool
DefaultOutbound bool
}
// TODO: merge with NewWithOptions
@@ -43,26 +42,19 @@ 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 && (!hasDetour || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
if options.RemoteIsDomain && (dialOptions.Detour == "" || 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
@@ -95,12 +87,11 @@ func NewWithOptions(options Options) (N.Dialer, error) {
}
server = dialOptions.DomainResolver.Server
dnsQueryOptions = adapter.DNSQueryOptions{
Transport: transport,
Strategy: strategy,
DisableCache: dialOptions.DomainResolver.DisableCache,
DisableOptimisticCache: dialOptions.DomainResolver.DisableOptimisticCache,
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
Transport: transport,
Strategy: strategy,
DisableCache: dialOptions.DomainResolver.DisableCache,
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
}
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else if options.DirectResolver {

View File

@@ -1,442 +0,0 @@
//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))
}

View File

@@ -1,69 +0,0 @@
#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
);

View File

@@ -1,386 +0,0 @@
#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);
}

View File

@@ -1,876 +0,0 @@
//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)
}
}

View File

@@ -1,17 +0,0 @@
//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")
}

View File

@@ -1,182 +0,0 @@
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)
}

View File

@@ -1,14 +0,0 @@
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
}

View File

@@ -1,86 +0,0 @@
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()
}

View File

@@ -1,47 +0,0 @@
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
}

View File

@@ -1,42 +0,0 @@
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
}

View File

@@ -1,93 +0,0 @@
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
}

View File

@@ -1,60 +0,0 @@
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
}

View File

@@ -1,312 +0,0 @@
//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)
}

View File

@@ -1,31 +0,0 @@
//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")
}

View File

@@ -1,164 +0,0 @@
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
}

View File

@@ -1,115 +0,0 @@
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)
}

View File

@@ -9,8 +9,6 @@ import (
"net/netip"
"time"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/bufio/deadline"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
@@ -433,9 +431,6 @@ func Run(options Options) (*Result, error) {
defer func() {
_ = packetConn.Close()
}()
if deadline.NeedAdditionalReadDeadline(packetConn) {
packetConn = deadline.NewPacketConn(bufio.NewPacketConn(packetConn))
}
select {
case <-ctx.Done():

View File

@@ -1,214 +0,0 @@
//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())
}
}

View File

@@ -1,414 +0,0 @@
//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))
}
}

View File

@@ -1,37 +0,0 @@
#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);

View File

@@ -1,631 +0,0 @@
#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);
}

View File

@@ -1,301 +0,0 @@
//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
}

View File

@@ -1,15 +0,0 @@
//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")
}

View File

@@ -10,15 +10,12 @@ 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
@@ -45,12 +42,11 @@ func NewClient(ctx context.Context, logger logger.ContextLogger, serverAddress s
}
type ClientOptions struct {
Context context.Context
Logger logger.ContextLogger
ServerAddress string
Options option.OutboundTLSOptions
AllowEmptyServerName bool
KTLSCompatible bool
Context context.Context
Logger logger.ContextLogger
ServerAddress string
Options option.OutboundTLSOptions
KTLSCompatible bool
}
func NewClientWithOptions(options ClientOptions) (Config, error) {
@@ -65,22 +61,17 @@ 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")
}
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)
}
if options.Options.Reality != nil && options.Options.Reality.Enabled {
return newRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
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, options.AllowEmptyServerName)
return NewUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options)
}
return newSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
return NewSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options)
}
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

View File

@@ -52,15 +52,11 @@ 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, allowEmptyServerName)
uClient, err := NewUTLSClient(ctx, logger, serverAddress, options)
if err != nil {
return nil, err
}
@@ -112,14 +108,6 @@ 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")
}

View File

@@ -26,8 +26,7 @@ import (
var _ ServerConfigCompat = (*RealityServerConfig)(nil)
type RealityServerConfig struct {
config *utls.RealityConfig
handshakeTimeout time.Duration
config *utls.RealityConfig
}
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
@@ -131,16 +130,7 @@ 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 handshakeTimeout time.Duration
if options.HandshakeTimeout > 0 {
handshakeTimeout = options.HandshakeTimeout.Build()
} else {
handshakeTimeout = C.TCPTimeout
}
var config ServerConfig = &RealityServerConfig{
config: &tlsConfig,
handshakeTimeout: handshakeTimeout,
}
var config ServerConfig = &RealityServerConfig{&tlsConfig}
if options.KernelTx || options.KernelRx {
if !C.IsLinux {
return nil, E.New("kTLS is only supported on Linux")
@@ -171,14 +161,6 @@ 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")
}
@@ -209,8 +191,7 @@ func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn
func (c *RealityServerConfig) Clone() Config {
return &RealityServerConfig{
config: c.config.Clone(),
handshakeTimeout: c.handshakeTimeout,
config: c.config.Clone(),
}
}

View File

@@ -46,11 +46,8 @@ func NewServerWithOptions(options ServerOptions) (ServerConfig, error) {
}
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
if config.HandshakeTimeout() == 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, C.TCPTimeout)
defer cancel()
}
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
defer cancel()
tlsConn, err := aTLS.ServerHandshake(ctx, conn, config)
if err != nil {
return nil, err

View File

@@ -24,30 +24,16 @@ 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.serverName
return c.config.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
}
@@ -59,14 +45,6 @@ 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
}
@@ -79,19 +57,13 @@ func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) {
}
func (c *STDClientConfig) Clone() Config {
cloned := &STDClientConfig{
return &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 {
@@ -103,27 +75,41 @@ 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 && !allowEmptyServerName {
return nil, errMissingServerName
if serverName == "" && !options.Insecure {
return nil, E.New("missing server_name or insecure=true")
}
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 != "" {
@@ -131,7 +117,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)
return verifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts, tlsConfig.Time)
}
}
if len(options.ALPN) > 0 {
@@ -212,24 +198,7 @@ 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 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)
var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
if options.ECH != nil && options.ECH.Enabled {
var err error
config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options)
@@ -251,28 +220,7 @@ func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
return config, nil
}
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 {
func verifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte, timeFunc func() time.Time) error {
leafCertificate, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return E.Cause(err, "failed to parse leaf certificate")

View File

@@ -92,7 +92,6 @@ 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
@@ -140,18 +139,6 @@ 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
@@ -178,8 +165,7 @@ func (c *STDServerConfig) Server(conn net.Conn) (Conn, error) {
func (c *STDServerConfig) Clone() Config {
return &STDServerConfig{
config: c.config.Clone(),
handshakeTimeout: c.handshakeTimeout,
config: c.config.Clone(),
}
}
@@ -472,7 +458,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)
return verifyPublicKeySHA256(options.ClientCertificatePublicKeySHA256, rawCerts, tlsConfig.Time)
}
} else {
return nil, E.New("missing client_certificate, client_certificate_path or client_certificate_public_key_sha256 for client authentication")
@@ -485,15 +471,8 @@ 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,

View File

@@ -28,10 +28,6 @@ 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
@@ -39,20 +35,10 @@ type UTLSClientConfig struct {
}
func (c *UTLSClientConfig) ServerName() string {
return c.serverName
return c.config.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
}
@@ -67,14 +53,6 @@ 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")
}
@@ -91,20 +69,9 @@ func (c *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []by
}
func (c *UTLSClientConfig) Clone() Config {
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,
return &UTLSClientConfig{
c.ctx, c.config.Clone(), c.id, c.fragment, c.fragmentFallbackDelay, c.recordFragment,
}
cloned.SetServerName(cloned.serverName)
return cloned
}
func (c *UTLSClientConfig) ECHConfigList() []byte {
@@ -176,29 +143,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 && !allowEmptyServerName {
return nil, errMissingServerName
if serverName == "" && !options.Insecure {
return nil, E.New("missing server_name or insecure=true")
}
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 != "" {
@@ -206,7 +173,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)
return verifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts, tlsConfig.Time)
}
}
if len(options.ALPN) > 0 {
@@ -284,29 +251,11 @@ 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: 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)
var config Config = &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
if options.ECH != nil && options.ECH.Enabled {
if options.Reality != nil && options.Reality.Enabled {
return nil, E.New("Reality is conflict with ECH")

View File

@@ -12,18 +12,10 @@ 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`)
}

View File

@@ -1,8 +1,3 @@
package constant
const ACMETLS1Protocol = "acme-tls/1"
const (
TLSEngineDefault = ""
TLSEngineApple = "apple"
)

View File

@@ -30,63 +30,59 @@ var (
var _ adapter.DNSClient = (*Client)(nil)
type Client struct {
ctx context.Context
timeout time.Duration
disableCache bool
disableExpire bool
optimisticTimeout time.Duration
cacheCapacity uint32
clientSubnet netip.Prefix
rdrc adapter.RDRCStore
initRDRCFunc func() adapter.RDRCStore
dnsCache adapter.DNSCacheStore
initDNSCacheFunc func() adapter.DNSCacheStore
logger logger.ContextLogger
cache freelru.Cache[dnsCacheKey, *dns.Msg]
cacheLock compatible.Map[dnsCacheKey, chan struct{}]
backgroundRefresh compatible.Map[dnsCacheKey, struct{}]
timeout time.Duration
disableCache bool
disableExpire bool
independentCache bool
clientSubnet netip.Prefix
rdrc adapter.RDRCStore
initRDRCFunc func() adapter.RDRCStore
logger logger.ContextLogger
cache freelru.Cache[dns.Question, *dns.Msg]
cacheLock compatible.Map[dns.Question, chan struct{}]
transportCache freelru.Cache[transportCacheKey, *dns.Msg]
transportCacheLock compatible.Map[dns.Question, chan struct{}]
}
type ClientOptions struct {
Context context.Context
Timeout time.Duration
DisableCache bool
DisableExpire bool
OptimisticTimeout time.Duration
CacheCapacity uint32
ClientSubnet netip.Prefix
RDRC func() adapter.RDRCStore
DNSCache func() adapter.DNSCacheStore
Logger logger.ContextLogger
Timeout time.Duration
DisableCache bool
DisableExpire bool
IndependentCache bool
CacheCapacity uint32
ClientSubnet netip.Prefix
RDRC func() adapter.RDRCStore
Logger logger.ContextLogger
}
func NewClient(options ClientOptions) *Client {
cacheCapacity := options.CacheCapacity
if cacheCapacity < 1024 {
cacheCapacity = 1024
}
client := &Client{
ctx: options.Context,
timeout: options.Timeout,
disableCache: options.DisableCache,
disableExpire: options.DisableExpire,
optimisticTimeout: options.OptimisticTimeout,
cacheCapacity: cacheCapacity,
clientSubnet: options.ClientSubnet,
initRDRCFunc: options.RDRC,
initDNSCacheFunc: options.DNSCache,
logger: options.Logger,
timeout: options.Timeout,
disableCache: options.DisableCache,
disableExpire: options.DisableExpire,
independentCache: options.IndependentCache,
clientSubnet: options.ClientSubnet,
initRDRCFunc: options.RDRC,
logger: options.Logger,
}
if client.timeout == 0 {
client.timeout = C.DNSTimeout
}
if !client.disableCache && client.initDNSCacheFunc == nil {
client.initializeMemoryCache()
cacheCapacity := options.CacheCapacity
if cacheCapacity < 1024 {
cacheCapacity = 1024
}
if !client.disableCache {
if !client.independentCache {
client.cache = common.Must1(freelru.NewSharded[dns.Question, *dns.Msg](cacheCapacity, maphash.NewHasher[dns.Question]().Hash32))
} else {
client.transportCache = common.Must1(freelru.NewSharded[transportCacheKey, *dns.Msg](cacheCapacity, maphash.NewHasher[transportCacheKey]().Hash32))
}
}
return client
}
type dnsCacheKey struct {
type transportCacheKey struct {
dns.Question
transportTag string
}
@@ -95,19 +91,6 @@ func (c *Client) Start() {
if c.initRDRCFunc != nil {
c.rdrc = c.initRDRCFunc()
}
if c.initDNSCacheFunc != nil {
c.dnsCache = c.initDNSCacheFunc()
}
if c.dnsCache == nil {
c.initializeMemoryCache()
}
}
func (c *Client) initializeMemoryCache() {
if c.disableCache || c.cache != nil {
return
}
c.cache = common.Must1(freelru.NewSharded[dnsCacheKey, *dns.Msg](c.cacheCapacity, maphash.NewHasher[dnsCacheKey]().Hash32))
}
func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
@@ -124,37 +107,6 @@ func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
return 0, false
}
func computeTimeToLive(response *dns.Msg) uint32 {
var timeToLive uint32
if len(response.Answer) == 0 {
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
return soaTTL
}
}
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
timeToLive = record.Header().Ttl
}
}
}
return timeToLive
}
func normalizeTTL(response *dns.Msg, timeToLive uint32) {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = timeToLive
}
}
}
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error) {
if len(message.Question) == 0 {
if c.logger != nil {
@@ -169,7 +121,13 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
}
return FixedResponseStatus(message, dns.RcodeSuccess), nil
}
message = c.prepareExchangeMessage(message, options)
clientSubnet := options.ClientSubnet
if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet
}
if clientSubnet.IsValid() {
message = SetClientSubnet(message, clientSubnet)
}
isSimpleRequest := len(message.Question) == 1 &&
len(message.Ns) == 0 &&
@@ -181,32 +139,40 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
!options.ClientSubnet.IsValid()
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
if !disableCache {
cacheKey := dnsCacheKey{Question: question, transportTag: transport.Tag()}
cond, loaded := c.cacheLock.LoadOrStore(cacheKey, make(chan struct{}))
if loaded {
select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
if c.cache != nil {
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
if loaded {
select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
} else {
defer func() {
c.cacheLock.Delete(question)
close(cond)
}()
}
} else if c.transportCache != nil {
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
if loaded {
select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
} else {
defer func() {
c.transportCacheLock.Delete(question)
close(cond)
}()
}
} else {
defer func() {
c.cacheLock.Delete(cacheKey)
close(cond)
}()
}
response, ttl, isStale := c.loadResponse(question, transport)
response, ttl := c.loadResponse(question, transport)
if response != nil {
if isStale && !options.DisableOptimisticCache {
c.backgroundRefreshDNS(transport, question, message.Copy(), options, responseChecker)
logOptimisticResponse(c.logger, ctx, response)
response.Id = message.Id
return response, nil
} else if !isStale {
logCachedResponse(c.logger, ctx, response, ttl)
response.Id = message.Id
return response, nil
}
logCachedResponse(c.logger, ctx, response, ttl)
response.Id = message.Id
return response, nil
}
}
@@ -222,10 +188,52 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return nil, ErrResponseRejectedCached
}
}
response, err := c.exchangeToTransport(ctx, transport, message)
ctx, cancel := context.WithTimeout(ctx, c.timeout)
response, err := transport.Exchange(ctx, message)
cancel()
if err != nil {
return nil, err
var rcodeError RcodeError
if errors.As(err, &rcodeError) {
response = FixedResponseStatus(message, int(rcodeError))
} else {
return nil, err
}
}
/*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
validResponse := response
loop:
for {
var (
addresses int
queryCNAME string
)
for _, rawRR := range validResponse.Answer {
switch rr := rawRR.(type) {
case *dns.A:
break loop
case *dns.AAAA:
break loop
case *dns.CNAME:
queryCNAME = rr.Target
}
}
if queryCNAME == "" {
break
}
exMessage := *message
exMessage.Question = []dns.Question{{
Name: queryCNAME,
Qtype: question.Qtype,
}}
validResponse, err = c.Exchange(ctx, transport, &exMessage, options, responseChecker)
if err != nil {
return nil, err
}
}
if validResponse != response {
response.Answer = append(response.Answer, validResponse.Answer...)
}
}*/
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
if responseChecker != nil {
var rejected bool
@@ -242,7 +250,54 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return response, ErrResponseRejected
}
}
timeToLive := applyResponseOptions(question, response, options)
if question.Qtype == dns.TypeHTTPS {
if options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only {
for _, rr := range response.Answer {
https, isHTTPS := rr.(*dns.HTTPS)
if !isHTTPS {
continue
}
content := https.SVCB
content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool {
if options.Strategy == C.DomainStrategyIPv4Only {
return it.Key() != dns.SVCB_IPV6HINT
} else {
return it.Key() != dns.SVCB_IPV4HINT
}
})
https.SVCB = content
}
}
}
var timeToLive uint32
if len(response.Answer) == 0 {
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
timeToLive = soaTTL
}
}
if timeToLive == 0 {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
timeToLive = record.Header().Ttl
}
}
}
}
if options.RewriteTTL != nil {
timeToLive = *options.RewriteTTL
}
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = timeToLive
}
}
if !disableCache {
c.storeCache(transport, question, response, timeToLive)
}
@@ -308,12 +363,8 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
func (c *Client) ClearCache() {
if c.cache != nil {
c.cache.Purge()
}
if c.dnsCache != nil {
err := c.dnsCache.ClearDNSCache()
if err != nil && c.logger != nil {
c.logger.Warn("clear DNS cache: ", err)
}
} else if c.transportCache != nil {
c.transportCache.Purge()
}
}
@@ -329,22 +380,24 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
if timeToLive == 0 {
return
}
if c.dnsCache != nil {
packed, err := message.Pack()
if err == nil {
expireAt := time.Now().Add(time.Second * time.Duration(timeToLive))
c.dnsCache.SaveDNSCacheAsync(transport.Tag(), question.Name, question.Qtype, packed, expireAt, c.logger)
}
return
}
if c.cache == nil {
return
}
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
if c.disableExpire {
c.cache.Add(key, message.Copy())
if !c.independentCache {
c.cache.Add(question, message.Copy())
} else {
c.transportCache.Add(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
}, message.Copy())
}
} else {
c.cache.AddWithLifetime(key, message.Copy(), time.Second*time.Duration(timeToLive))
if !c.independentCache {
c.cache.AddWithLifetime(question, message.Copy(), time.Second*time.Duration(timeToLive))
} else {
c.transportCache.AddWithLifetime(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
}, message.Copy(), time.Second*time.Duration(timeToLive))
}
}
}
@@ -354,19 +407,19 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran
Qtype: qType,
Qclass: dns.ClassINET,
}
disableCache := c.disableCache || options.DisableCache
if !disableCache {
cachedAddresses, err := c.questionCache(question, transport)
if err != ErrNotCached {
return cachedAddresses, err
}
}
message := dns.Msg{
MsgHdr: dns.MsgHdr{
RecursionDesired: true,
},
Question: []dns.Question{question},
}
disableCache := c.disableCache || options.DisableCache
if !disableCache {
cachedAddresses, err := c.questionCache(ctx, transport, &message, options, responseChecker)
if err != ErrNotCached {
return cachedAddresses, err
}
}
response, err := c.Exchange(ctx, transport, &message, options, responseChecker)
if err != nil {
return nil, err
@@ -377,177 +430,98 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran
return MessageToAddresses(response), nil
}
func (c *Client) questionCache(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
question := message.Question[0]
response, _, isStale := c.loadResponse(question, transport)
func (c *Client) questionCache(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) {
response, _ := c.loadResponse(question, transport)
if response == nil {
return nil, ErrNotCached
}
if isStale {
if options.DisableOptimisticCache {
return nil, ErrNotCached
}
c.backgroundRefreshDNS(transport, question, c.prepareExchangeMessage(message.Copy(), options), options, responseChecker)
logOptimisticResponse(c.logger, ctx, response)
}
if response.Rcode != dns.RcodeSuccess {
return nil, RcodeError(response.Rcode)
}
return MessageToAddresses(response), nil
}
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) {
if c.dnsCache != nil {
return c.loadPersistentResponse(question, transport)
}
if c.cache == nil {
return nil, 0, false
}
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int) {
var (
response *dns.Msg
loaded bool
)
if c.disableExpire {
response, loaded := c.cache.Get(key)
if !loaded {
return nil, 0, false
}
return response.Copy(), 0, false
}
response, expireAt, loaded := c.cache.GetWithLifetimeNoExpire(key)
if !loaded {
return nil, 0, false
}
timeNow := time.Now()
if timeNow.After(expireAt) {
if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) {
response = response.Copy()
normalizeTTL(response, 1)
return response, 0, true
}
c.cache.Remove(key)
return nil, 0, false
}
nowTTL := int(expireAt.Sub(timeNow).Seconds())
if nowTTL < 0 {
nowTTL = 0
}
response = response.Copy()
normalizeTTL(response, uint32(nowTTL))
return response, nowTTL, false
}
func (c *Client) loadPersistentResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) {
rawMessage, expireAt, loaded := c.dnsCache.LoadDNSCache(transport.Tag(), question.Name, question.Qtype)
if !loaded {
return nil, 0, false
}
response := new(dns.Msg)
err := response.Unpack(rawMessage)
if err != nil {
return nil, 0, false
}
if c.disableExpire {
return response, 0, false
}
timeNow := time.Now()
if timeNow.After(expireAt) {
if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) {
normalizeTTL(response, 1)
return response, 0, true
}
return nil, 0, false
}
nowTTL := int(expireAt.Sub(timeNow).Seconds())
if nowTTL < 0 {
nowTTL = 0
}
normalizeTTL(response, uint32(nowTTL))
return response, nowTTL, false
}
func applyResponseOptions(question dns.Question, response *dns.Msg, options adapter.DNSQueryOptions) uint32 {
if question.Qtype == dns.TypeHTTPS && (options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only) {
for _, rr := range response.Answer {
https, isHTTPS := rr.(*dns.HTTPS)
if !isHTTPS {
continue
}
content := https.SVCB
content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool {
if options.Strategy == C.DomainStrategyIPv4Only {
return it.Key() != dns.SVCB_IPV6HINT
}
return it.Key() != dns.SVCB_IPV4HINT
if !c.independentCache {
response, loaded = c.cache.Get(question)
} else {
response, loaded = c.transportCache.Get(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
})
https.SVCB = content
}
}
timeToLive := computeTimeToLive(response)
if options.RewriteTTL != nil {
timeToLive = *options.RewriteTTL
}
normalizeTTL(response, timeToLive)
return timeToLive
}
func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) {
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
_, loaded := c.backgroundRefresh.LoadOrStore(key, struct{}{})
if loaded {
return
}
go func() {
defer c.backgroundRefresh.Delete(key)
ctx := contextWithTransportTag(c.ctx, transport.Tag())
response, err := c.exchangeToTransport(ctx, transport, message)
if err != nil {
if c.logger != nil {
c.logger.Debug("optimistic refresh failed for ", FqdnToDomain(question.Name), ": ", err)
}
return
if !loaded {
return nil, 0
}
if responseChecker != nil {
var rejected bool
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
rejected = true
return response.Copy(), 0
} else {
var expireAt time.Time
if !c.independentCache {
response, expireAt, loaded = c.cache.GetWithLifetime(question)
} else {
response, expireAt, loaded = c.transportCache.GetWithLifetime(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
})
}
if !loaded {
return nil, 0
}
timeNow := time.Now()
if timeNow.After(expireAt) {
if !c.independentCache {
c.cache.Remove(question)
} else {
rejected = !responseChecker(response)
c.transportCache.Remove(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
})
}
if rejected {
if c.rdrc != nil {
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
}
return
}
} else if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
return
return nil, 0
}
timeToLive := applyResponseOptions(question, response, options)
c.storeCache(transport, question, response, timeToLive)
}()
}
func (c *Client) prepareExchangeMessage(message *dns.Msg, options adapter.DNSQueryOptions) *dns.Msg {
clientSubnet := options.ClientSubnet
if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet
var originTTL int
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
originTTL = int(record.Header().Ttl)
}
}
}
nowTTL := int(expireAt.Sub(timeNow).Seconds())
if nowTTL < 0 {
nowTTL = 0
}
response = response.Copy()
if originTTL > 0 {
duration := uint32(originTTL - nowTTL)
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = record.Header().Ttl - duration
}
}
} else {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = uint32(nowTTL)
}
}
}
return response, nowTTL
}
if clientSubnet.IsValid() {
message = SetClientSubnet(message, clientSubnet)
}
return message
}
func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg) (*dns.Msg, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
response, err := transport.Exchange(ctx, message)
if err == nil {
return response, nil
}
var rcodeError RcodeError
if errors.As(err, &rcodeError) {
return FixedResponseStatus(message, int(rcodeError)), nil
}
return nil, err
}
func MessageToAddresses(response *dns.Msg) []netip.Addr {

View File

@@ -22,19 +22,6 @@ func logCachedResponse(logger logger.ContextLogger, ctx context.Context, respons
}
}
func logOptimisticResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg) {
if logger == nil || len(response.Question) == 0 {
return
}
domain := FqdnToDomain(response.Question[0].Name)
logger.DebugContext(ctx, "optimistic ", domain, " ", dns.RcodeToString[response.Rcode])
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
logger.InfoContext(ctx, "optimistic ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
}
}
}
func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) {
if logger == nil || len(response.Question) == 0 {
return

View File

@@ -51,7 +51,7 @@ type Router struct {
closing bool
}
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) (*Router, error) {
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {
router := &Router{
ctx: ctx,
logger: logFactory.NewLogger("dns"),
@@ -61,30 +61,12 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
rules: make([]adapter.DNSRule, 0, len(options.Rules)),
defaultDomainStrategy: C.DomainStrategy(options.Strategy),
}
if options.DNSClientOptions.IndependentCache {
deprecated.Report(ctx, deprecated.OptionIndependentDNSCache)
}
var optimisticTimeout time.Duration
optimisticOptions := common.PtrValueOrDefault(options.DNSClientOptions.Optimistic)
if optimisticOptions.Enabled {
if options.DNSClientOptions.DisableCache {
return nil, E.New("`optimistic` is conflict with `disable_cache`")
}
if options.DNSClientOptions.DisableExpire {
return nil, E.New("`optimistic` is conflict with `disable_expire`")
}
optimisticTimeout = time.Duration(optimisticOptions.Timeout)
if optimisticTimeout == 0 {
optimisticTimeout = 3 * 24 * time.Hour
}
}
router.client = NewClient(ClientOptions{
Context: ctx,
DisableCache: options.DNSClientOptions.DisableCache,
DisableExpire: options.DNSClientOptions.DisableExpire,
OptimisticTimeout: optimisticTimeout,
CacheCapacity: options.DNSClientOptions.CacheCapacity,
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}),
DisableCache: options.DNSClientOptions.DisableCache,
DisableExpire: options.DNSClientOptions.DisableExpire,
IndependentCache: options.DNSClientOptions.IndependentCache,
CacheCapacity: options.DNSClientOptions.CacheCapacity,
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}),
RDRC: func() adapter.RDRCStore {
cacheFile := service.FromContext[adapter.CacheFile](ctx)
if cacheFile == nil {
@@ -95,24 +77,12 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
}
return cacheFile
},
DNSCache: func() adapter.DNSCacheStore {
cacheFile := service.FromContext[adapter.CacheFile](ctx)
if cacheFile == nil {
return nil
}
if !cacheFile.StoreDNS() {
return nil
}
cacheFile.SetDisableExpire(options.DNSClientOptions.DisableExpire)
cacheFile.SetOptimisticTimeout(optimisticTimeout)
return cacheFile
},
Logger: router.logger,
})
if options.ReverseMapping {
router.dnsReverseMapping = common.Must1(freelru.NewSharded[netip.Addr, string](1024, maphash.NewHasher[netip.Addr]().Hash32))
}
return router, nil
return router
}
func (r *Router) Initialize(rules []option.DNSRule) error {
@@ -349,9 +319,6 @@ func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOpt
if routeOptions.DisableCache {
options.DisableCache = true
}
if routeOptions.DisableOptimisticCache {
options.DisableOptimisticCache = true
}
if routeOptions.RewriteTTL != nil {
options.RewriteTTL = routeOptions.RewriteTTL
}
@@ -940,9 +907,7 @@ func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule, m
return dnsRuleModeRequirementsInDefaultRule(router, rule.DefaultOptions, metadataOverrides)
case C.RuleTypeLogical:
flags := dnsRuleModeFlags{
disabled: dnsRuleActionType(rule) == C.RuleActionTypeEvaluate ||
dnsRuleActionType(rule) == C.RuleActionTypeRespond ||
dnsRuleActionDisablesLegacyDNSMode(rule.LogicalOptions.DNSRuleAction),
disabled: dnsRuleActionType(rule) == C.RuleActionTypeEvaluate || dnsRuleActionType(rule) == C.RuleActionTypeRespond,
neededFromStrategy: dnsRuleActionHasStrategy(rule.LogicalOptions.DNSRuleAction),
}
flags.needed = flags.neededFromStrategy
@@ -961,7 +926,7 @@ func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule, m
func dnsRuleModeRequirementsInDefaultRule(router adapter.Router, rule option.DefaultDNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) {
flags := dnsRuleModeFlags{
disabled: defaultRuleDisablesLegacyDNSMode(rule) || dnsRuleActionDisablesLegacyDNSMode(rule.DNSRuleAction),
disabled: defaultRuleDisablesLegacyDNSMode(rule),
neededFromStrategy: dnsRuleActionHasStrategy(rule.DNSRuleAction),
}
flags.needed = defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule) || flags.neededFromStrategy
@@ -1098,17 +1063,6 @@ func validateLegacyDNSModeDisabledDefaultRule(rule option.DefaultDNSRule) (bool,
return rule.MatchResponse || rule.Action == C.RuleActionTypeRespond, nil
}
func dnsRuleActionDisablesLegacyDNSMode(action option.DNSRuleAction) bool {
switch action.Action {
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
return action.RouteOptions.DisableOptimisticCache
case C.RuleActionTypeRouteOptions:
return action.RouteOptionsOptions.DisableOptimisticCache
default:
return false
}
}
func dnsRuleActionHasStrategy(action option.DNSRuleAction) bool {
switch action.Action {
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:

View File

@@ -139,9 +139,9 @@ type fakeRuleSet struct {
beforeDecrementReference func()
}
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) 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) Metadata() adapter.RuleSetMetadata {
s.access.Lock()
metadata := s.metadata

View File

@@ -3,18 +3,17 @@ package transport
import (
"bytes"
"context"
"encoding/base64"
"errors"
"io"
"net"
"net/http"
"net/url"
"strings"
"strconv"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/httpclient"
"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/dns"
@@ -27,9 +26,9 @@ import (
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
sHTTP "github.com/sagernet/sing/protocol/http"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns"
"golang.org/x/net/http2"
)
const MimeType = "application/dns-message"
@@ -43,20 +42,57 @@ func RegisterHTTPS(registry *dns.TransportRegistry) {
type HTTPSTransport struct {
dns.TransportAdapter
logger logger.ContextLogger
dialer N.Dialer
destination *url.URL
method string
host string
queryHeaders http.Header
headers http.Header
transportAccess sync.Mutex
transport adapter.HTTPTransport
transport *HTTPSTransportWrapper
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")
headers.Del("Host")
headers.Set("Accept", MimeType)
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
}
serverAddr := options.DNSServerAddressOptions.Build()
if serverAddr.Port == 0 {
serverAddr.Port = 443
@@ -64,123 +100,56 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
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
return NewHTTPSRaw(
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions),
logger,
transportDialer,
&destinationURL,
headers,
serverAddr,
tlsConfig,
), nil
}
func NewHTTPRaw(
func NewHTTPSRaw(
adapter dns.TransportAdapter,
logger logger.ContextLogger,
logger log.ContextLogger,
dialer N.Dialer,
destination *url.URL,
headers http.Header,
serverAddr M.Socksaddr,
tlsConfig tls.Config,
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
}
) *HTTPSTransport {
return &HTTPSTransport{
TransportAdapter: adapter,
logger: logger,
dialer: dialer,
destination: destination,
method: method,
host: host,
queryHeaders: queryHeaders,
transport: currentTransport,
}, nil
headers: headers,
transport: NewHTTPSTransportWrapper(tls.NewDialer(dialer, tlsConfig), serverAddr),
}
}
func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
return httpclient.InitializeDetour(t.transport)
return dialer.InitializeDetour(t.dialer)
}
func (t *HTTPSTransport) Close() error {
t.transportAccess.Lock()
defer t.transportAccess.Unlock()
if t.transport == nil {
return nil
}
err := t.transport.Close()
t.transport = nil
return err
t.transport.CloseIdleConnections()
t.transport = t.transport.Clone()
return nil
}
func (t *HTTPSTransport) Reset() {
t.transportAccess.Lock()
defer t.transportAccess.Unlock()
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()
t.transport.CloseIdleConnections()
t.transport = t.transport.Clone()
}
func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
@@ -190,12 +159,11 @@ 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.transport == nil || t.transportResetAt.After(startAt) {
if t.transportResetAt.After(startAt) {
return nil, err
}
oldTransport := t.transport
oldTransport.CloseIdleConnections()
t.transport = oldTransport.Clone()
t.transport.CloseIdleConnections()
t.transport = t.transport.Clone()
t.transportResetAt = time.Now()
}
return nil, err
@@ -213,32 +181,17 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
requestBuffer.Release()
return nil, err
}
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))
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage))
if err != nil {
requestBuffer.Release()
return nil, err
}
request.Header = t.queryHeaders.Clone()
if t.host != "" {
request.Host = t.host
}
request.Header = t.headers.Clone()
request.Header.Set("Content-Type", MimeType)
request.Header.Set("Accept", MimeType)
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 {
@@ -269,13 +222,3 @@ 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()
}

View File

@@ -0,0 +1,80 @@
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,
}
}

View File

@@ -4,24 +4,8 @@ package local
/*
#include <stdlib.h>
#include <dns.h>
#include <resolv.h>
static void *cgo_dns_open_super() {
return (void *)dns_open(NULL);
}
static void cgo_dns_close(void *opaque) {
if (opaque != NULL) dns_free((dns_handle_t)opaque);
}
static int cgo_dns_search(void *opaque, const char *name, int class, int type,
unsigned char *answer, int anslen) {
dns_handle_t handle = (dns_handle_t)opaque;
struct sockaddr_storage from;
uint32_t fromlen = sizeof(from);
return dns_search(handle, name, class, type, (char *)answer, anslen, (struct sockaddr *)&from, &fromlen);
}
#include <netdb.h>
static void *cgo_res_init() {
res_state state = calloc(1, sizeof(struct __res_state));
@@ -68,59 +52,7 @@ import (
mDNS "github.com/miekg/dns"
)
const (
darwinResolverHostNotFound = 1
darwinResolverTryAgain = 2
darwinResolverNoRecovery = 3
darwinResolverNoData = 4
darwinResolverMaxPacketSize = 65535
)
var errDarwinNeedLargerBuffer = errors.New("darwin resolver response truncated")
func darwinLookupSystemDNS(name string, class, qtype, timeoutSeconds int) (*mDNS.Msg, error) {
response, err := darwinSearchWithSystemRouting(name, class, qtype)
if err == nil {
return response, nil
}
fallbackResponse, fallbackErr := darwinSearchWithResolv(name, class, qtype, timeoutSeconds)
if fallbackErr == nil || fallbackResponse != nil {
return fallbackResponse, fallbackErr
}
return nil, E.Errors(
E.Cause(err, "dns_search"),
E.Cause(fallbackErr, "res_nsearch"),
)
}
func darwinSearchWithSystemRouting(name string, class, qtype int) (*mDNS.Msg, error) {
handle := C.cgo_dns_open_super()
if handle == nil {
return nil, E.New("dns_open failed")
}
defer C.cgo_dns_close(handle)
cName := C.CString(name)
defer C.free(unsafe.Pointer(cName))
bufSize := 1232
for {
answer := make([]byte, bufSize)
n := C.cgo_dns_search(handle, cName, C.int(class), C.int(qtype),
(*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)))
if n <= 0 {
return nil, E.New("dns_search failed for ", name)
}
if int(n) > bufSize {
bufSize = int(n)
continue
}
return unpackDarwinResolverMessage(answer[:int(n)], "dns_search")
}
}
func darwinSearchWithResolv(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) {
func resolvSearch(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) {
state := C.cgo_res_init()
if state == nil {
return nil, E.New("res_ninit failed")
@@ -129,7 +61,6 @@ func darwinSearchWithResolv(name string, class, qtype int, timeoutSeconds int) (
cName := C.CString(name)
defer C.free(unsafe.Pointer(cName))
bufSize := 1232
for {
answer := make([]byte, bufSize)
@@ -143,55 +74,37 @@ func darwinSearchWithResolv(name string, class, qtype int, timeoutSeconds int) (
bufSize = int(n)
continue
}
return unpackDarwinResolverMessage(answer[:int(n)], "res_nsearch")
}
response, err := handleDarwinResolvFailure(name, answer, int(hErrno))
if err == nil {
return response, nil
}
if errors.Is(err, errDarwinNeedLargerBuffer) && bufSize < darwinResolverMaxPacketSize {
bufSize *= 2
if bufSize > darwinResolverMaxPacketSize {
bufSize = darwinResolverMaxPacketSize
var response mDNS.Msg
err := response.Unpack(answer[:int(n)])
if err != nil {
return nil, E.Cause(err, "unpack res_nsearch response")
}
continue
return &response, nil
}
return nil, err
}
}
func unpackDarwinResolverMessage(packet []byte, source string) (*mDNS.Msg, error) {
var response mDNS.Msg
err := response.Unpack(packet)
if err != nil {
return nil, E.Cause(err, "unpack ", source, " response")
}
return &response, nil
}
func handleDarwinResolvFailure(name string, answer []byte, hErrno int) (*mDNS.Msg, error) {
response, err := unpackDarwinResolverMessage(answer, "res_nsearch failure")
if err == nil && response.Response {
if response.Truncated && len(answer) < darwinResolverMaxPacketSize {
return nil, errDarwinNeedLargerBuffer
var response mDNS.Msg
_ = response.Unpack(answer[:bufSize])
if response.Response {
if response.Truncated && bufSize < 65535 {
bufSize *= 2
if bufSize > 65535 {
bufSize = 65535
}
continue
}
return &response, nil
}
switch hErrno {
case C.HOST_NOT_FOUND:
return nil, dns.RcodeNameError
case C.TRY_AGAIN:
return nil, dns.RcodeNameError
case C.NO_RECOVERY:
return nil, dns.RcodeServerFailure
case C.NO_DATA:
return nil, dns.RcodeSuccess
default:
return nil, E.New("res_nsearch: unknown error ", int(hErrno), " for ", name)
}
return response, nil
}
return nil, darwinResolverHErrno(name, hErrno)
}
func darwinResolverHErrno(name string, hErrno int) error {
switch hErrno {
case darwinResolverHostNotFound:
return dns.RcodeNameError
case darwinResolverTryAgain:
return dns.RcodeServerFailure
case darwinResolverNoRecovery:
return dns.RcodeServerFailure
case darwinResolverNoData:
return dns.RcodeSuccess
default:
return E.New("res_nsearch: unknown error ", hErrno, " for ", name)
}
}
@@ -228,7 +141,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
}
resultCh := make(chan resolvResult, 1)
go func() {
response, err := darwinLookupSystemDNS(name, int(question.Qclass), int(question.Qtype), timeoutSeconds)
response, err := resolvSearch(name, int(question.Qclass), int(question.Qtype), timeoutSeconds)
resultCh <- resolvResult{response, err}
}()
var result resolvResult

View File

@@ -9,7 +9,6 @@ import (
"net/url"
"strconv"
"sync"
"time"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
@@ -41,23 +40,18 @@ func RegisterHTTP3Transport(registry *dns.TransportRegistry) {
type HTTP3Transport struct {
dns.TransportAdapter
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
logger logger.ContextLogger
dialer N.Dialer
destination *url.URL
headers http.Header
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) {
remoteOptions := option.RemoteDNSServerOptions{
DNSServerAddressOptions: options.DNSServerAddressOptions,
}
remoteOptions.DialerOptions = options.DialerOptions
transportDialer, err := dns.NewRemoteDialer(ctx, remoteOptions)
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
if err != nil {
return nil, err
}
@@ -67,7 +61,6 @@ 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
@@ -109,12 +102,11 @@ 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, remoteOptions),
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
logger: logger,
dialer: transportDialer,
destination: &destinationURL,
headers: headers,
handshakeTimeout: handshakeTimeout,
serverAddr: serverAddr,
tlsConfig: stdConfig,
}
@@ -123,17 +115,8 @@ 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

View File

@@ -2,132 +2,22 @@
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**
* `ip_version` and `query_type` now also take effect on internal DNS lookups **2**
* Add `package_name_regex` route, DNS and headless rule item **3**
* Add cloudflared inbound **4**
* Fixes and improvements
**1**:
Response Match Fields
([`response_rcode`](/configuration/dns/rule/#response_rcode),
[`response_answer`](/configuration/dns/rule/#response_answer),
[`response_ns`](/configuration/dns/rule/#response_ns),
and [`response_extra`](/configuration/dns/rule/#response_extra))
match the evaluated DNS response. They are gated by the new
[`match_response`](/configuration/dns/rule/#match_response) field and
populated by a preceding
[`evaluate`](/configuration/dns/rule_action/#evaluate) DNS rule action;
the evaluated response can also be returned directly by a
[`respond`](/configuration/dns/rule_action/#respond) action.
This deprecates the Legacy Address Filter Fields (`ip_cidr`,
`ip_is_private` without `match_response`) in DNS rules, the Legacy
`strategy` DNS rule action option, and the Legacy
`rule_set_ip_cidr_accept_empty` DNS rule item; all three will be removed
in sing-box 1.16.0.
See [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
**2**:
`ip_version` and `query_type` in DNS rules, together with `query_type` in
referenced rule-sets, now take effect on every DNS rule evaluation,
including matches from internal domain resolutions that do not target a
specific DNS server (for example a `resolve` route rule action without
`server` set). In earlier versions they were silently ignored in that
path. Combining these fields with any of the legacy DNS fields deprecated
in **1** in the same DNS configuration is no longer supported and is
rejected at startup.
See [Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules).
**3**:
See [Route Rule](/configuration/route/rule/#package_name_regex),
[DNS Rule](/configuration/dns/rule/#package_name_regex) and
[Headless Rule](/configuration/rule-set/headless-rule/#package_name_regex).
**4**:
See [Cloudflared](/configuration/inbound/cloudflared/).
#### 1.13.7
* Fixes and improvement
* Fixes and improvements
#### 1.13.6
* Fixes and improvements
#### 1.14.0-alpha.8
* Add BBR profile and hop interval randomization for Hysteria2 **1**
* Fixes and improvements
**1**:
See [Hysteria2 Inbound](/configuration/inbound/hysteria2/#bbr_profile) and [Hysteria2 Outbound](/configuration/outbound/hysteria2/#bbr_profile).
#### 1.14.0-alpha.8
* Fixes and improvements
#### 1.13.5
* Fixes and improvements
#### 1.14.0-alpha.7
* Fixes and improvements
#### 1.13.4
* Fixes and improvements
#### 1.14.0-alpha.4
* Refactor ACME support to certificate provider system **1**
* Add Cloudflare Origin CA certificate provider **2**
* Add Tailscale certificate provider **3**
* Fixes and improvements
**1**:
See [Certificate Provider](/configuration/shared/certificate-provider/) and [Migration](/migration/#migrate-inline-acme-to-certificate-provider).
**2**:
See [Cloudflare Origin CA](/configuration/shared/certificate-provider/cloudflare-origin-ca).
**3**:
See [Tailscale](/configuration/shared/certificate-provider/tailscale).
#### 1.13.3
* Add OpenWrt and Alpine APK packages to release **1**
@@ -152,59 +42,6 @@ from [SagerNet/go](https://github.com/SagerNet/go).
See [OCM](/configuration/service/ocm).
#### 1.12.24
* Fixes and improvements
#### 1.14.0-alpha.2
* Add OpenWrt and Alpine APK packages to release **1**
* Backport to macOS 10.13 High Sierra **2**
* OCM service: Add WebSocket support for Responses API **3**
* Fixes and improvements
**1**:
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
**2**:
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
macOS 10.13 High Sierra, built using Go 1.25 with patches
from [SagerNet/go](https://github.com/SagerNet/go).
**3**:
See [OCM](/configuration/service/ocm).
#### 1.14.0-alpha.1
* Add `source_mac_address` and `source_hostname` rule items **1**
* Add `include_mac_address` and `exclude_mac_address` TUN options **2**
* Update NaiveProxy to 145.0.7632.159 **3**
* Fixes and improvements
**1**:
New rule items for matching LAN devices by MAC address and hostname via neighbor resolution.
Supported on Linux, macOS, or in graphical clients on Android and macOS.
See [Route Rule](/configuration/route/rule/#source_mac_address), [DNS Rule](/configuration/dns/rule/#source_mac_address) and [Neighbor Resolution](/configuration/shared/neighbor/).
**2**:
Limit or exclude devices from TUN routing by MAC address.
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
See [TUN](/configuration/inbound/tun/#include_mac_address).
**3**:
This is not an official update from NaiveProxy. Instead, it's a Chromium codebase update maintained by Project S.
#### 1.13.2
* Fixes and improvements

View File

@@ -2,11 +2,6 @@
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.14.0"
:material-delete-clock: [independent_cache](#independent_cache)
:material-plus: [optimistic](#optimistic)
!!! quote "Changes in sing-box 1.12.0"
:material-decagram: [servers](#servers)
@@ -30,7 +25,6 @@ icon: material/alert-decagram
"disable_expire": false,
"independent_cache": false,
"cache_capacity": 0,
"optimistic": false, // or {}
"reverse_mapping": false,
"client_subnet": "",
"fakeip": {}
@@ -63,20 +57,12 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
Disable dns cache.
Conflict with `optimistic`.
#### disable_expire
Disable dns cache expire.
Conflict with `optimistic`.
#### independent_cache
!!! failure "Deprecated in sing-box 1.14.0"
`independent_cache` is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-independent-dns-cache).
Make each DNS server's cache independent for special purposes. If enabled, will slightly degrade performance.
#### cache_capacity
@@ -87,34 +73,6 @@ LRU cache capacity.
Value less than 1024 will be ignored.
#### optimistic
!!! question "Since sing-box 1.14.0"
Enable optimistic DNS caching. When a cached DNS entry has expired but is still within the timeout window,
the stale response is returned immediately while a background refresh is triggered.
Conflict with `disable_cache` and `disable_expire`.
Accepts a boolean or an object. When set to `true`, the default timeout of `3d` is used.
```json
{
"enabled": true,
"timeout": "3d"
}
```
##### enabled
Enable optimistic DNS caching.
##### timeout
The maximum time an expired cache entry can be served optimistically.
`3d` is used by default.
#### reverse_mapping
Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing.

View File

@@ -2,11 +2,6 @@
icon: material/alert-decagram
---
!!! quote "sing-box 1.14.0 中的更改"
:material-delete-clock: [independent_cache](#independent_cache)
:material-plus: [optimistic](#optimistic)
!!! quote "sing-box 1.12.0 中的更改"
:material-decagram: [servers](#servers)
@@ -30,7 +25,6 @@ icon: material/alert-decagram
"disable_expire": false,
"independent_cache": false,
"cache_capacity": 0,
"optimistic": false, // or {}
"reverse_mapping": false,
"client_subnet": "",
"fakeip": {}
@@ -62,20 +56,12 @@ icon: material/alert-decagram
禁用 DNS 缓存。
`optimistic` 冲突。
#### disable_expire
禁用 DNS 缓存过期。
`optimistic` 冲突。
#### independent_cache
!!! failure "已在 sing-box 1.14.0 废弃"
`independent_cache` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除,参阅[迁移指南](/zh/migration/#迁移-independent-dns-cache)。
使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。
#### cache_capacity
@@ -86,34 +72,6 @@ LRU 缓存容量。
小于 1024 的值将被忽略。
#### optimistic
!!! question "自 sing-box 1.14.0 起"
启用乐观 DNS 缓存。当缓存的 DNS 条目已过期但仍在超时窗口内时,
立即返回过期的响应,同时在后台触发刷新。
`disable_cache``disable_expire` 冲突。
接受布尔值或对象。当设置为 `true` 时,使用默认超时 `3d`
```json
{
"enabled": true,
"timeout": "3d"
}
```
##### enabled
启用乐观 DNS 缓存。
##### timeout
过期缓存条目可被乐观提供的最长时间。
默认使用 `3d`
#### reverse_mapping
在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。

View File

@@ -6,8 +6,7 @@ icon: material/new-box
:material-delete-clock: [strategy](#strategy)
:material-plus: [evaluate](#evaluate)
:material-plus: [respond](#respond)
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
:material-plus: [respond](#respond)
!!! quote "Changes in sing-box 1.12.0"
@@ -24,7 +23,6 @@ icon: material/new-box
"server": "",
"strategy": "",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
@@ -54,12 +52,6 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
Disable cache and save cache in this query.
#### disable_optimistic_cache
!!! question "Since sing-box 1.14.0"
Disable optimistic DNS caching in this query.
#### rewrite_ttl
Rewrite TTL in DNS responses.
@@ -81,7 +73,6 @@ Will override `dns.client_subnet`.
"action": "evaluate",
"server": "",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
@@ -106,12 +97,6 @@ Tag of target server.
Disable cache and save cache in this query.
#### disable_optimistic_cache
!!! question "Since sing-box 1.14.0"
Disable optimistic DNS caching in this query.
#### rewrite_ttl
Rewrite TTL in DNS responses.
@@ -146,7 +131,6 @@ Only allowed after a preceding top-level `evaluate` rule. If the action is reach
{
"action": "route-options",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}

View File

@@ -6,8 +6,7 @@ icon: material/new-box
:material-delete-clock: [strategy](#strategy)
:material-plus: [evaluate](#evaluate)
:material-plus: [respond](#respond)
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
:material-plus: [respond](#respond)
!!! quote "sing-box 1.12.0 中的更改"
@@ -24,7 +23,6 @@ icon: material/new-box
"server": "",
"strategy": "",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
@@ -54,12 +52,6 @@ icon: material/new-box
在此查询中禁用缓存。
#### disable_optimistic_cache
!!! question "自 sing-box 1.14.0 起"
在此查询中禁用乐观 DNS 缓存。
#### rewrite_ttl
重写 DNS 回应中的 TTL。
@@ -81,7 +73,6 @@ icon: material/new-box
"action": "evaluate",
"server": "",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
@@ -104,12 +95,6 @@ icon: material/new-box
在此查询中禁用缓存。
#### disable_optimistic_cache
!!! question "自 sing-box 1.14.0 起"
在此查询中禁用乐观 DNS 缓存。
#### rewrite_ttl
重写 DNS 回应中的 TTL。
@@ -144,7 +129,6 @@ icon: material/new-box
{
"action": "route-options",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}

View File

@@ -2,10 +2,6 @@
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)
@@ -19,20 +15,27 @@ icon: material/new-box
{
"type": "h3",
"tag": "",
"server": "",
"server_port": 0,
"server_port": 443,
"path": "",
"method": "",
... // HTTP Client Fields
"headers": {},
"tls": {},
// Dial 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
@@ -55,14 +58,14 @@ The path of the DNS server.
`/dns-query` will be used by default.
#### method
#### headers
HTTP request method.
Additional headers to be sent to the DNS server.
Available values: `GET`, `POST`.
#### tls
`POST` will be used by default.
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
### HTTP Client Fields
### Dial Fields
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
See [Dial Fields](/configuration/shared/dial/) for details.

View File

@@ -2,10 +2,6 @@
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)
@@ -21,18 +17,25 @@ icon: material/new-box
"tag": "",
"server": "",
"server_port": 0,
"server_port": 443,
"path": "",
"method": "",
"headers": {},
... // HTTP 客户端字段
"tls": {},
// 拨号字段
}
]
}
}
```
!!! info "与旧版 H3 服务器的区别"
* 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。
* 旧服务器使用 `address_resolver``address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver``domain_strategy`
### 字段
#### server
@@ -55,14 +58,14 @@ DNS 服务器的路径。
默认使用 `/dns-query`
#### method
#### headers
HTTP 请求方法
发送到 DNS 服务器的额外标头
可用值:`GET``POST`
#### tls
默认使用 `POST`
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)
### HTTP 客户端字段
### 拨号字段
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。

View File

@@ -2,10 +2,6 @@
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)
@@ -19,20 +15,27 @@ icon: material/new-box
{
"type": "https",
"tag": "",
"server": "",
"server_port": 0,
"server_port": 443,
"path": "",
"method": "",
... // HTTP Client Fields
"headers": {},
"tls": {},
// Dial 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
@@ -55,14 +58,14 @@ The path of the DNS server.
`/dns-query` will be used by default.
#### method
#### headers
HTTP request method.
Additional headers to be sent to the DNS server.
Available values: `GET`, `POST`.
#### tls
`POST` will be used by default.
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
### HTTP Client Fields
### Dial Fields
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
See [Dial Fields](/configuration/shared/dial/) for details.

View File

@@ -2,10 +2,6 @@
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)
@@ -21,18 +17,25 @@ icon: material/new-box
"tag": "",
"server": "",
"server_port": 0,
"server_port": 443,
"path": "",
"method": "",
"headers": {},
... // HTTP 客户端字段
"tls": {},
// 拨号字段
}
]
}
}
```
!!! info "与旧版 HTTPS 服务器的区别"
* 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。
* 旧服务器使用 `address_resolver``address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver``domain_strategy`
### 字段
#### server
@@ -55,14 +58,14 @@ DNS 服务器的路径。
默认使用 `/dns-query`
#### method
#### headers
HTTP 请求方法
发送到 DNS 服务器的额外标头
可用值:`GET``POST`
#### tls
默认使用 `POST`
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)
### HTTP 客户端字段
### 拨号字段
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。

View File

@@ -2,11 +2,6 @@
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)
@@ -27,7 +22,6 @@ icon: material/new-box
"state_directory": "",
"auth_key": "",
"control_url": "",
"control_http_client": {}, // or ""
"ephemeral": false,
"hostname": "",
"accept_routes": false,
@@ -154,18 +148,10 @@ 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
!!! failure "Deprecated in sing-box 1.14.0"
!!! note
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.
Dial Fields in Tailscale endpoints only control how it connects to the control plane and have nothing to do with actual connections.
See [Dial Fields](/configuration/shared/dial/) for details.

View File

@@ -2,11 +2,6 @@
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)
@@ -27,7 +22,6 @@ icon: material/new-box
"state_directory": "",
"auth_key": "",
"control_url": "",
"control_http_client": {}, // 或 ""
"ephemeral": false,
"hostname": "",
"accept_routes": false,
@@ -153,18 +147,10 @@ UDP NAT 过期时间。
默认使用 `5m`
#### control_http_client
!!! question "自 sing-box 1.14.0 起"
用于连接 Tailscale 控制平面的 HTTP 客户端。
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
### 拨号字段
!!! failure "已在 sing-box 1.14.0 废弃"
!!! note
Tailscale 端点中的拨号字段已在 sing-box 1.14.0 废弃且将在 sing-box 1.16.0 中被移除,请使用 `control_http_client` 代替
Tailscale 端点中的拨号字段仅控制它如何连接到控制平面,与实际连接无关
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。

View File

@@ -1,10 +1,5 @@
!!! question "Since sing-box 1.8.0"
!!! quote "Changes in sing-box 1.14.0"
:material-delete-clock: [store_rdrc](#store_rdrc)
:material-plus: [store_dns](#store_dns)
!!! quote "Changes in sing-box 1.9.0"
:material-plus: [store_rdrc](#store_rdrc)
@@ -19,8 +14,7 @@
"cache_id": "",
"store_fakeip": false,
"store_rdrc": false,
"rdrc_timeout": "",
"store_dns": false
"rdrc_timeout": ""
}
```
@@ -48,10 +42,6 @@ Store fakeip in the cache file
#### store_rdrc
!!! failure "Deprecated in sing-box 1.14.0"
`store_rdrc` is deprecated and will be removed in sing-box 1.16.0, check [Migration](/migration/#migrate-store-rdrc).
Store rejected DNS response cache in the cache file
The check results of [Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields)
@@ -62,9 +52,3 @@ will be cached until expiration.
Timeout of rejected DNS response cache.
`7d` is used by default.
#### store_dns
!!! question "Since sing-box 1.14.0"
Store DNS cache in the cache file.

View File

@@ -1,10 +1,5 @@
!!! question "自 sing-box 1.8.0 起"
!!! quote "sing-box 1.14.0 中的更改"
:material-delete-clock: [store_rdrc](#store_rdrc)
:material-plus: [store_dns](#store_dns)
!!! quote "sing-box 1.9.0 中的更改"
:material-plus: [store_rdrc](#store_rdrc)
@@ -19,8 +14,7 @@
"cache_id": "",
"store_fakeip": false,
"store_rdrc": false,
"rdrc_timeout": "",
"store_dns": false
"rdrc_timeout": ""
}
```
@@ -46,10 +40,6 @@
#### store_rdrc
!!! failure "已在 sing-box 1.14.0 废弃"
`store_rdrc` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除,参阅[迁移指南](/zh/migration/#迁移-store_rdrc)。
将拒绝的 DNS 响应缓存存储在缓存文件中。
[旧版地址筛选字段](/zh/configuration/dns/rule/#旧版地址筛选字段) 的检查结果将被缓存至过期。
@@ -59,9 +49,3 @@
拒绝的 DNS 响应缓存超时。
默认使用 `7d`
#### store_dns
!!! question "自 sing-box 1.14.0 起"
将 DNS 缓存存储在缓存文件中。

View File

@@ -21,16 +21,11 @@
}
],
"tls": {},
... // QUIC Fields
// Deprecated
"recv_window_conn": 0,
"recv_window_client": 0,
"max_conn_client": 0,
"disable_mtu_discovery": false
"disable_mtu_discovery": false,
"tls": {}
}
```
@@ -81,38 +76,32 @@ 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).
### 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.
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).

View File

@@ -21,16 +21,11 @@
}
],
"tls": {},
... // QUIC 字段
// 废弃的
"recv_window_conn": 0,
"recv_window_client": 0,
"max_conn_client": 0,
"disable_mtu_discovery": false
"disable_mtu_discovery": false,
"tls": {}
}
```
@@ -81,38 +76,32 @@ 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/#入站)。
### 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` 代替。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。

View File

@@ -34,9 +34,6 @@ icon: material/alert-decagram
],
"ignore_client_bandwidth": false,
"tls": {},
... // QUIC Fields
"masquerade": "", // or {}
"bbr_profile": "",
"brutal_debug": false
@@ -98,10 +95,6 @@ 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.

View File

@@ -34,9 +34,6 @@ icon: material/alert-decagram
],
"ignore_client_bandwidth": false,
"tls": {},
... // QUIC 字段
"masquerade": "", // 或 {}
"bbr_profile": "",
"brutal_debug": false
@@ -95,10 +92,6 @@ Hysteria 用户
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
### QUIC 字段
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
#### masquerade
HTTP3 服务器认证失败时的行为 URL 字符串配置)。

View File

@@ -18,9 +18,7 @@
"auth_timeout": "3s",
"zero_rtt_handshake": false,
"heartbeat": "10s",
"tls": {},
... // QUIC Fields
"tls": {}
}
```
@@ -77,8 +75,4 @@ Interval for sending heartbeat packets for keeping the connection alive
==Required==
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
### QUIC Fields
See [QUIC Fields](/configuration/shared/quic/) for details.
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).

View File

@@ -18,9 +18,7 @@
"auth_timeout": "3s",
"zero_rtt_handshake": false,
"heartbeat": "10s",
"tls": {},
... // QUIC 字段
"tls": {}
}
```
@@ -77,8 +75,4 @@ QUIC 拥塞控制算法
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
### QUIC 字段
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。

View File

@@ -10,7 +10,6 @@ sing-box uses JSON for configuration files.
"ntp": {},
"certificate": {},
"certificate_providers": [],
"http_clients": [],
"endpoints": [],
"inbounds": [],
"outbounds": [],
@@ -29,7 +28,6 @@ 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/) |

View File

@@ -10,7 +10,6 @@ sing-box 使用 JSON 作为配置文件格式。
"ntp": {},
"certificate": {},
"certificate_providers": [],
"http_clients": [],
"endpoints": [],
"inbounds": [],
"outbounds": [],
@@ -29,7 +28,6 @@ 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/) |

View File

@@ -27,18 +27,13 @@ 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
"disable_mtu_discovery": false,
"network": "tcp",
"tls": {},
... // Dial Fields
}
```
@@ -109,6 +104,24 @@ 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
@@ -123,30 +136,6 @@ 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.

View File

@@ -27,18 +27,13 @@ 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
"disable_mtu_discovery": false,
"network": "tcp",
"tls": {},
... // 拨号字段
}
```
@@ -109,6 +104,24 @@ 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
启用的网络协议。
@@ -123,30 +136,7 @@ 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` 代替。

View File

@@ -31,9 +31,6 @@
"password": "goofy_ahh_password",
"network": "tcp",
"tls": {},
... // QUIC Fields
"bbr_profile": "",
"brutal_debug": false,
@@ -127,10 +124,6 @@ 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"

View File

@@ -31,9 +31,6 @@
"password": "goofy_ahh_password",
"network": "tcp",
"tls": {},
... // QUIC 字段
"bbr_profile": "",
"brutal_debug": false,
@@ -125,10 +122,6 @@ QUIC 流量混淆器密码.
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
### QUIC 字段
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
#### bbr_profile
!!! question "自 sing-box 1.14.0 起"

View File

@@ -16,9 +16,7 @@
"heartbeat": "10s",
"network": "tcp",
"tls": {},
... // QUIC Fields
... // Dial Fields
}
```
@@ -93,10 +91,6 @@ 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.

View File

@@ -16,9 +16,7 @@
"heartbeat": "10s",
"network": "tcp",
"tls": {},
... // QUIC 字段
... // 拨号字段
}
```
@@ -101,10 +99,6 @@ UDP 包中继模式
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
### QUIC 字段
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
### 拨号字段
参阅 [拨号字段](/zh/configuration/shared/dial/)。

View File

@@ -6,7 +6,6 @@ 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)
@@ -44,7 +43,6 @@ 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": [],
@@ -149,14 +147,6 @@ 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"

View File

@@ -6,7 +6,6 @@ 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)
@@ -46,7 +45,6 @@ icon: material/alert-decagram
"find_process": false,
"find_neighbor": false,
"dhcp_lease_files": [],
"default_http_client": "",
"default_network_strategy": "",
"default_fallback_delay": ""
}
@@ -148,14 +146,6 @@ 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 起"

View File

@@ -7,10 +7,6 @@ icon: material/new-box
:material-plus: [bypass](#bypass)
:material-alert: [reject](#reject)
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache)
!!! quote "Changes in sing-box 1.12.0"
:material-plus: [tls_fragment](#tls_fragment)
@@ -283,7 +279,6 @@ Timeout for sniffing.
"server": "",
"strategy": "",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
@@ -307,12 +302,6 @@ DNS resolution strategy, available values are: `prefer_ipv4`, `prefer_ipv6`, `ip
Disable cache and save cache in this query.
#### disable_optimistic_cache
!!! question "Since sing-box 1.14.0"
Disable optimistic DNS caching in this query.
#### rewrite_ttl
!!! question "Since sing-box 1.12.0"

View File

@@ -7,10 +7,6 @@ icon: material/new-box
:material-plus: [bypass](#bypass)
:material-alert: [reject](#reject)
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache)
!!! quote "sing-box 1.12.0 中的更改"
:material-plus: [tls_fragment](#tls_fragment)
@@ -272,7 +268,6 @@ UDP 连接超时时间。
"server": "",
"strategy": "",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
@@ -296,12 +291,6 @@ DNS 解析策略,可用值有:`prefer_ipv4`、`prefer_ipv6`、`ipv4_only`、
在此查询中禁用缓存。
#### disable_optimistic_cache
!!! question "自 sing-box 1.14.0 起"
在此查询中禁用乐观 DNS 缓存。
#### rewrite_ttl
!!! question "自 sing-box 1.12.0 起"

View File

@@ -1,8 +1,3 @@
!!! 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`
@@ -48,12 +43,8 @@
"tag": "",
"format": "source", // or binary
"url": "",
"http_client": "", // or {}
"update_interval": "",
// Deprecated
"download_detour": ""
"download_detour": "", // optional
"update_interval": "" // optional
}
```
@@ -111,26 +102,14 @@ File path of rule-set.
Download URL of rule-set.
#### http_client
#### download_detour
!!! question "Since sing-box 1.14.0"
Tag of the outbound to download rule-set.
HTTP Client for downloading rule-set.
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
Default transport will be used if empty.
Default outbound 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.

View File

@@ -1,8 +1,3 @@
!!! 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`
@@ -48,12 +43,8 @@
"tag": "",
"format": "source", // or binary
"url": "",
"http_client": "", // 或 {}
"update_interval": "",
// 废弃的
"download_detour": ""
"download_detour": "", // 可选
"update_interval": "" // 可选
}
```
@@ -111,26 +102,14 @@
规则集的下载 URL。
#### http_client
#### download_detour
!!! 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` 代替。
用于下载规则集的出站的标签。

View File

@@ -58,9 +58,9 @@ Object format:
```json
{
"url": "",
... // HTTP Client Fields
"url": "https://my-headscale.com/verify",
... // Dial Fields
}
```

View File

@@ -58,9 +58,9 @@ Derper 配置文件路径。
```json
{
"url": "",
"url": "https://my-headscale.com/verify",
... // HTTP 客户端字段
... // 拨号字段
}
```

View File

@@ -6,7 +6,7 @@ icon: material/new-box
:material-plus: [account_key](#account_key)
:material-plus: [key_type](#key_type)
:material-plus: [http_client](#http_client)
:material-plus: [detour](#detour)
# ACME
@@ -37,7 +37,7 @@ icon: material/new-box
},
"dns01_challenge": {},
"key_type": "",
"http_client": "" // or {}
"detour": ""
}
```
@@ -141,10 +141,10 @@ The private key type to generate for new certificates.
| `rsa2048` | RSA |
| `rsa4096` | RSA |
#### http_client
#### detour
!!! question "Since sing-box 1.14.0"
HTTP Client for all provider HTTP requests.
The tag of the upstream outbound.
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
All provider HTTP requests will use this outbound.

View File

@@ -6,7 +6,7 @@ icon: material/new-box
:material-plus: [account_key](#account_key)
:material-plus: [key_type](#key_type)
:material-plus: [http_client](#http_client)
:material-plus: [detour](#detour)
# ACME
@@ -37,7 +37,7 @@ icon: material/new-box
},
"dns01_challenge": {},
"key_type": "",
"http_client": "" // 或 {}
"detour": ""
}
```
@@ -136,12 +136,10 @@ ACME DNS01 质询字段。如果配置,将禁用其他质询方法。
| `rsa2048` | RSA |
| `rsa4096` | RSA |
#### http_client
#### detour
!!! question "自 sing-box 1.14.0 起"
用于所有提供者 HTTP 请求的 HTTP 客户端
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
上游出站的标签
所有提供者 HTTP 请求将使用此出站。

View File

@@ -19,7 +19,7 @@ icon: material/new-box
"origin_ca_key": "",
"request_type": "",
"requested_validity": 0,
"http_client": "" // or {}
"detour": ""
}
```
@@ -75,8 +75,8 @@ Available values: `7`, `30`, `90`, `365`, `730`, `1095`, `5475`.
`5475` days (15 years) is used if empty.
#### http_client
#### detour
HTTP Client for all provider HTTP requests.
The tag of the upstream outbound.
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
All provider HTTP requests will use this outbound.

View File

@@ -19,7 +19,7 @@ icon: material/new-box
"origin_ca_key": "",
"request_type": "",
"requested_validity": 0,
"http_client": "" // 或 {}
"detour": ""
}
```
@@ -75,8 +75,8 @@ Cloudflare Origin CA Key。
如果为空,使用 `5475`15 年)。
#### http_client
#### detour
用于所有提供者 HTTP 请求的 HTTP 客户端
上游出站的标签
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情
所有提供者 HTTP 请求将使用此出站

View File

@@ -1,114 +0,0 @@
---
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.

View File

@@ -1,114 +0,0 @@
---
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/) 了解详情。

View File

@@ -1,43 +0,0 @@
---
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.

View File

@@ -1,43 +0,0 @@
---
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
每个连接的最大并发流数。

View File

@@ -1,30 +0,0 @@
---
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.

Some files were not shown because too many files have changed in this diff Show More