mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-13 20:28:32 +10:00
Compare commits
37 Commits
0cc4baae12
...
testing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccfdbf2d57 | ||
|
|
9b75d28ca4 | ||
|
|
2e64545db4 | ||
|
|
9675b0902a | ||
|
|
ebd31ca363 | ||
|
|
6ba7a6f001 | ||
|
|
b7e1a14974 | ||
|
|
a5c0112f0c | ||
|
|
e6427e8244 | ||
|
|
c0d9551bcf | ||
|
|
5cdf1aa000 | ||
|
|
6da0aa0c82 | ||
|
|
97f4723467 | ||
|
|
6c7fb1dad1 | ||
|
|
e0696f5e94 | ||
|
|
ddcaf040e2 | ||
|
|
57039ac11d | ||
|
|
abd6baf3cb | ||
|
|
a48fd106c3 | ||
|
|
6dfab9225f | ||
|
|
5e7e58f5e9 | ||
|
|
cfcc766d74 | ||
|
|
a24170638e | ||
|
|
ac9c0e7a81 | ||
|
|
51166f4601 | ||
|
|
5d254d9015 | ||
|
|
d3fc58ceb8 | ||
|
|
58d22df1be | ||
|
|
574852bdc1 | ||
|
|
ddc181f65a | ||
|
|
e2727d9556 | ||
|
|
f8b05790d1 | ||
|
|
c1203821f9 | ||
|
|
9805db343c | ||
|
|
b28083b131 | ||
|
|
0d1ce7957d | ||
|
|
025b947a24 |
2
.github/CRONET_GO_VERSION
vendored
2
.github/CRONET_GO_VERSION
vendored
@@ -1 +1 @@
|
||||
ea7cd33752aed62603775af3df946c1b83f4b0b3
|
||||
335e5bef5d88fc4474c9a70b865561f45a67de83
|
||||
|
||||
@@ -3,6 +3,7 @@ package adapter
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -31,12 +32,13 @@ type DNSClient interface {
|
||||
}
|
||||
|
||||
type DNSQueryOptions struct {
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
LookupStrategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
RewriteTTL *uint32
|
||||
ClientSubnet netip.Prefix
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
LookupStrategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
DisableOptimisticCache bool
|
||||
RewriteTTL *uint32
|
||||
ClientSubnet netip.Prefix
|
||||
}
|
||||
|
||||
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
||||
@@ -49,11 +51,12 @@ 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,
|
||||
RewriteTTL: options.RewriteTTL,
|
||||
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
Transport: transport,
|
||||
Strategy: C.DomainStrategy(options.Strategy),
|
||||
DisableCache: options.DisableCache,
|
||||
DisableOptimisticCache: options.DisableOptimisticCache,
|
||||
RewriteTTL: options.RewriteTTL,
|
||||
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -63,6 +66,13 @@ 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
|
||||
|
||||
@@ -47,6 +47,12 @@ 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
|
||||
|
||||
13
adapter/http.go
Normal file
13
adapter/http.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
type HTTPClientManager interface {
|
||||
ResolveTransport(logger logger.ContextLogger, options option.HTTPClientOptions) (http.RoundTripper, error)
|
||||
DefaultTransport() http.RoundTripper
|
||||
}
|
||||
@@ -2,17 +2,11 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-tun"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
|
||||
"go4.org/netipx"
|
||||
@@ -51,7 +45,7 @@ type ConnectionRouterEx interface {
|
||||
|
||||
type RuleSet interface {
|
||||
Name() string
|
||||
StartContext(ctx context.Context, startContext *HTTPStartContext) error
|
||||
StartContext(ctx context.Context) error
|
||||
PostStart() error
|
||||
Metadata() RuleSetMetadata
|
||||
ExtractIPSet() []*netipx.IPSet
|
||||
@@ -77,46 +71,3 @@ type RuleSetMetadata struct {
|
||||
ContainsIPCIDRRule bool
|
||||
ContainsDNSQueryTypeRule bool
|
||||
}
|
||||
type HTTPStartContext struct {
|
||||
ctx context.Context
|
||||
access sync.Mutex
|
||||
httpClientCache map[string]*http.Client
|
||||
}
|
||||
|
||||
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
|
||||
return &HTTPStartContext{
|
||||
ctx: ctx,
|
||||
httpClientCache: make(map[string]*http.Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
if httpClient, loaded := c.httpClientCache[detour]; loaded {
|
||||
return httpClient
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
TLSHandshakeTimeout: C.TCPTimeout,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
TLSClientConfig: &tls.Config{
|
||||
Time: ntp.TimeFuncFromContext(c.ctx),
|
||||
RootCAs: RootPoolFromContext(c.ctx),
|
||||
},
|
||||
},
|
||||
}
|
||||
c.httpClientCache[detour] = httpClient
|
||||
return httpClient
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) Close() {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
for _, client := range c.httpClientCache {
|
||||
client.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
41
box.go
41
box.go
@@ -16,12 +16,14 @@ import (
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/certificate"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/httpclient"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/direct"
|
||||
@@ -50,6 +52,7 @@ type Box struct {
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
httpClientService adapter.LifecycleService
|
||||
internalService []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
}
|
||||
@@ -169,6 +172,10 @@ func New(options Options) (*Box, error) {
|
||||
}
|
||||
|
||||
var internalServices []adapter.LifecycleService
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
httpClientManager := httpclient.NewManager(ctx, logFactory.NewLogger("httpclient"), options.HTTPClients, routeOptions.DefaultHTTPClient)
|
||||
service.MustRegister[adapter.HTTPClientManager](ctx, httpClientManager)
|
||||
httpClientService := adapter.LifecycleService(httpClientManager)
|
||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||
len(certificateOptions.Certificate) > 0 ||
|
||||
@@ -181,8 +188,6 @@ func New(options Options) (*Box, error) {
|
||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||
internalServices = append(internalServices, certificateStore)
|
||||
}
|
||||
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||
@@ -196,7 +201,10 @@ 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 := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
dnsRouter, err := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize DNS router")
|
||||
}
|
||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
|
||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
||||
@@ -365,6 +373,12 @@ func New(options Options) (*Box, error) {
|
||||
&option.LocalDNSServerOptions{},
|
||||
)
|
||||
})
|
||||
httpClientManager.Initialize(func() (*httpclient.Client, error) {
|
||||
deprecated.Report(ctx, deprecated.OptionImplicitDefaultHTTPClient)
|
||||
var httpClientOptions option.HTTPClientOptions
|
||||
httpClientOptions.DefaultOutbound = true
|
||||
return httpclient.NewClient(ctx, logFactory.NewLogger("httpclient"), "", httpClientOptions)
|
||||
})
|
||||
if platformInterface != nil {
|
||||
err = platformInterface.Initialize(networkManager)
|
||||
if err != nil {
|
||||
@@ -372,7 +386,7 @@ func New(options Options) (*Box, error) {
|
||||
}
|
||||
}
|
||||
if needCacheFile {
|
||||
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||
internalServices = append(internalServices, cacheFile)
|
||||
}
|
||||
@@ -425,6 +439,7 @@ func New(options Options) (*Box, error) {
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
httpClientService: httpClientService,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
@@ -487,7 +502,15 @@ func (s *Box) preStart() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection, s.router, s.dnsRouter)
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.logger, adapter.StartStateStart, []adapter.LifecycleService{s.httpClientService})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.router, s.dnsRouter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -564,6 +587,14 @@ func (s *Box) Close() error {
|
||||
})
|
||||
s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
if s.httpClientService != nil {
|
||||
s.logger.Trace("close ", s.httpClientService.Name())
|
||||
startTime := time.Now()
|
||||
err = E.Append(err, s.httpClientService.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", s.httpClientService.Name())
|
||||
})
|
||||
s.logger.Trace("close ", s.httpClientService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
for _, lifecycleService := range s.internalService {
|
||||
s.logger.Trace("close ", lifecycleService.Name())
|
||||
startTime := time.Now()
|
||||
|
||||
Submodule clients/android updated: 4f0826b94d...fea0f3a7ba
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -35,21 +36,9 @@ func updateMozillaIncludedRootCAs() error {
|
||||
return err
|
||||
}
|
||||
geoIndex := slices.Index(header, "Geographic Focus")
|
||||
nameIndex := slices.Index(header, "Common Name or Certificate Name")
|
||||
certIndex := slices.Index(header, "PEM Info")
|
||||
|
||||
generated := strings.Builder{}
|
||||
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||
|
||||
package certificate
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
var mozillaIncluded *x509.CertPool
|
||||
|
||||
func init() {
|
||||
mozillaIncluded = x509.NewCertPool()
|
||||
`)
|
||||
pemBundle := strings.Builder{}
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
@@ -60,18 +49,12 @@ func init() {
|
||||
if record[geoIndex] == "China" {
|
||||
continue
|
||||
}
|
||||
generated.WriteString("\n // ")
|
||||
generated.WriteString(record[nameIndex])
|
||||
generated.WriteString("\n")
|
||||
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
|
||||
cert := record[certIndex]
|
||||
// Remove single quotes
|
||||
cert = cert[1 : len(cert)-1]
|
||||
generated.WriteString(cert)
|
||||
generated.WriteString("`))\n")
|
||||
pemBundle.WriteString(cert)
|
||||
pemBundle.WriteString("\n")
|
||||
}
|
||||
generated.WriteString("}\n")
|
||||
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
|
||||
return writeGeneratedCertificateBundle("mozilla", "mozillaIncluded", pemBundle.String())
|
||||
}
|
||||
|
||||
func fetchChinaFingerprints() (map[string]bool, error) {
|
||||
@@ -119,23 +102,11 @@ func updateChromeIncludedRootCAs() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
subjectIndex := slices.Index(header, "Subject")
|
||||
statusIndex := slices.Index(header, "Google Chrome Status")
|
||||
certIndex := slices.Index(header, "X.509 Certificate (PEM)")
|
||||
fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint")
|
||||
|
||||
generated := strings.Builder{}
|
||||
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||
|
||||
package certificate
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
var chromeIncluded *x509.CertPool
|
||||
|
||||
func init() {
|
||||
chromeIncluded = x509.NewCertPool()
|
||||
`)
|
||||
pemBundle := strings.Builder{}
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
@@ -149,18 +120,39 @@ func init() {
|
||||
if chinaFingerprints[record[fingerprintIndex]] {
|
||||
continue
|
||||
}
|
||||
generated.WriteString("\n // ")
|
||||
generated.WriteString(record[subjectIndex])
|
||||
generated.WriteString("\n")
|
||||
generated.WriteString(" chromeIncluded.AppendCertsFromPEM([]byte(`")
|
||||
cert := record[certIndex]
|
||||
// Remove single quotes if present
|
||||
if len(cert) > 0 && cert[0] == '\'' {
|
||||
cert = cert[1 : len(cert)-1]
|
||||
}
|
||||
generated.WriteString(cert)
|
||||
generated.WriteString("`))\n")
|
||||
pemBundle.WriteString(cert)
|
||||
pemBundle.WriteString("\n")
|
||||
}
|
||||
generated.WriteString("}\n")
|
||||
return os.WriteFile("common/certificate/chrome.go", []byte(generated.String()), 0o644)
|
||||
return writeGeneratedCertificateBundle("chrome", "chromeIncluded", pemBundle.String())
|
||||
}
|
||||
|
||||
func writeGeneratedCertificateBundle(name string, variableName string, pemBundle string) error {
|
||||
goSource := `// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed ` + name + `.pem
|
||||
var ` + variableName + `PEM string
|
||||
|
||||
var ` + variableName + ` *x509.CertPool
|
||||
|
||||
func init() {
|
||||
` + variableName + ` = x509.NewCertPool()
|
||||
` + variableName + `.AppendCertsFromPEM([]byte(` + variableName + `PEM))
|
||||
}
|
||||
`
|
||||
err := os.WriteFile(filepath.Join("common/certificate", name+".pem"), []byte(pemBundle), 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filepath.Join("common/certificate", name+".go"), []byte(goSource), 0o644)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
2650
common/certificate/chrome.pem
Normal file
2650
common/certificate/chrome.pem
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
4256
common/certificate/mozilla.pem
Normal file
4256
common/certificate/mozilla.pem
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,8 +22,10 @@ var _ adapter.CertificateStore = (*Store)(nil)
|
||||
|
||||
type Store struct {
|
||||
access sync.RWMutex
|
||||
store string
|
||||
systemPool *x509.CertPool
|
||||
currentPool *x509.CertPool
|
||||
currentPEM []string
|
||||
certificate string
|
||||
certificatePaths []string
|
||||
certificateDirectoryPaths []string
|
||||
@@ -61,6 +63,7 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
|
||||
return nil, E.New("unknown certificate store: ", options.Store)
|
||||
}
|
||||
store := &Store{
|
||||
store: options.Store,
|
||||
systemPool: systemPool,
|
||||
certificate: strings.Join(options.Certificate, "\n"),
|
||||
certificatePaths: options.CertificatePath,
|
||||
@@ -123,19 +126,37 @@ func (s *Store) Pool() *x509.CertPool {
|
||||
return s.currentPool
|
||||
}
|
||||
|
||||
func (s *Store) StoreKind() string {
|
||||
return s.store
|
||||
}
|
||||
|
||||
func (s *Store) CurrentPEM() []string {
|
||||
s.access.RLock()
|
||||
defer s.access.RUnlock()
|
||||
return append([]string(nil), s.currentPEM...)
|
||||
}
|
||||
|
||||
func (s *Store) update() error {
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
var currentPool *x509.CertPool
|
||||
var currentPEM []string
|
||||
if s.systemPool == nil {
|
||||
currentPool = x509.NewCertPool()
|
||||
} else {
|
||||
currentPool = s.systemPool.Clone()
|
||||
}
|
||||
switch s.store {
|
||||
case C.CertificateStoreMozilla:
|
||||
currentPEM = append(currentPEM, mozillaIncludedPEM)
|
||||
case C.CertificateStoreChrome:
|
||||
currentPEM = append(currentPEM, chromeIncludedPEM)
|
||||
}
|
||||
if s.certificate != "" {
|
||||
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
||||
return E.New("invalid certificate PEM strings")
|
||||
}
|
||||
currentPEM = append(currentPEM, s.certificate)
|
||||
}
|
||||
for _, path := range s.certificatePaths {
|
||||
pemContent, err := os.ReadFile(path)
|
||||
@@ -145,6 +166,7 @@ func (s *Store) update() error {
|
||||
if !currentPool.AppendCertsFromPEM(pemContent) {
|
||||
return E.New("invalid certificate PEM file: ", path)
|
||||
}
|
||||
currentPEM = append(currentPEM, string(pemContent))
|
||||
}
|
||||
var firstErr error
|
||||
for _, directoryPath := range s.certificateDirectoryPaths {
|
||||
@@ -157,8 +179,8 @@ func (s *Store) update() error {
|
||||
}
|
||||
for _, directoryEntry := range directoryEntries {
|
||||
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
|
||||
if err == nil {
|
||||
currentPool.AppendCertsFromPEM(pemContent)
|
||||
if err == nil && currentPool.AppendCertsFromPEM(pemContent) {
|
||||
currentPEM = append(currentPEM, string(pemContent))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,6 +188,7 @@ func (s *Store) update() error {
|
||||
return firstErr
|
||||
}
|
||||
s.currentPool = currentPool
|
||||
s.currentPEM = currentPEM
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ type DirectDialer interface {
|
||||
type DetourDialer struct {
|
||||
outboundManager adapter.OutboundManager
|
||||
detour string
|
||||
defaultOutbound bool
|
||||
legacyDNSDialer bool
|
||||
dialer N.Dialer
|
||||
initOnce sync.Once
|
||||
@@ -33,6 +34,13 @@ func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNS
|
||||
}
|
||||
}
|
||||
|
||||
func NewDefaultOutboundDetour(outboundManager adapter.OutboundManager) N.Dialer {
|
||||
return &DetourDialer{
|
||||
outboundManager: outboundManager,
|
||||
defaultOutbound: true,
|
||||
}
|
||||
}
|
||||
|
||||
func InitializeDetour(dialer N.Dialer) error {
|
||||
detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
|
||||
if !isDetour {
|
||||
@@ -47,12 +55,18 @@ func (d *DetourDialer) Dialer() (N.Dialer, error) {
|
||||
}
|
||||
|
||||
func (d *DetourDialer) init() {
|
||||
dialer, loaded := d.outboundManager.Outbound(d.detour)
|
||||
if !loaded {
|
||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||
return
|
||||
var dialer adapter.Outbound
|
||||
if d.detour != "" {
|
||||
var loaded bool
|
||||
dialer, loaded = d.outboundManager.Outbound(d.detour)
|
||||
if !loaded {
|
||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
dialer = d.outboundManager.Default()
|
||||
}
|
||||
if !d.legacyDNSDialer {
|
||||
if !d.defaultOutbound && !d.legacyDNSDialer {
|
||||
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
|
||||
if directDialer.IsEmpty() {
|
||||
d.initErr = E.New("detour to an empty direct outbound makes no sense")
|
||||
|
||||
@@ -25,6 +25,7 @@ type Options struct {
|
||||
NewDialer bool
|
||||
LegacyDNSDialer bool
|
||||
DirectOutbound bool
|
||||
DefaultOutbound bool
|
||||
}
|
||||
|
||||
// TODO: merge with NewWithOptions
|
||||
@@ -42,19 +43,26 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
dialer N.Dialer
|
||||
err error
|
||||
)
|
||||
hasDetour := dialOptions.Detour != "" || options.DefaultOutbound
|
||||
if dialOptions.Detour != "" {
|
||||
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
||||
if outboundManager == nil {
|
||||
return nil, E.New("missing outbound manager")
|
||||
}
|
||||
dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
|
||||
} else if options.DefaultOutbound {
|
||||
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
||||
if outboundManager == nil {
|
||||
return nil, E.New("missing outbound manager")
|
||||
}
|
||||
dialer = NewDefaultOutboundDetour(outboundManager)
|
||||
} else {
|
||||
dialer, err = NewDefault(options.Context, dialOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
||||
if options.RemoteIsDomain && (!hasDetour || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
||||
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
|
||||
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
|
||||
var defaultOptions adapter.NetworkOptions
|
||||
@@ -87,11 +95,12 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
}
|
||||
server = dialOptions.DomainResolver.Server
|
||||
dnsQueryOptions = adapter.DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: strategy,
|
||||
DisableCache: dialOptions.DomainResolver.DisableCache,
|
||||
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
||||
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||
Transport: transport,
|
||||
Strategy: strategy,
|
||||
DisableCache: dialOptions.DomainResolver.DisableCache,
|
||||
DisableOptimisticCache: dialOptions.DomainResolver.DisableOptimisticCache,
|
||||
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
||||
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||
}
|
||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||
} else if options.DirectResolver {
|
||||
|
||||
154
common/httpclient/client.go
Normal file
154
common/httpclient/client.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"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"
|
||||
)
|
||||
|
||||
type httpTransport interface {
|
||||
http.RoundTripper
|
||||
CloseIdleConnections()
|
||||
Clone() httpTransport
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
transport httpTransport
|
||||
headers http.Header
|
||||
host string
|
||||
tag string
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, logger logger.ContextLogger, tag string, options option.HTTPClientOptions) (*Client, error) {
|
||||
rawDialer, err := dialer.NewWithOptions(dialer.Options{
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
RemoteIsDomain: true,
|
||||
ResolverOnDetour: options.ResolveOnDetour,
|
||||
NewDialer: options.ResolveOnDetour,
|
||||
DefaultOutbound: options.DefaultOutbound,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 NewClientWithDialer(rawDialer, baseTLSConfig, tag, options)
|
||||
}
|
||||
|
||||
func NewClientWithDialer(rawDialer N.Dialer, baseTLSConfig tls.Config, tag string, options option.HTTPClientOptions) (*Client, error) {
|
||||
headers := options.Headers.Build()
|
||||
host := headers.Get("Host")
|
||||
headers.Del("Host")
|
||||
transport, err := newTransport(rawDialer, baseTLSConfig, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{
|
||||
transport: transport,
|
||||
headers: headers,
|
||||
host: host,
|
||||
tag: tag,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTPClientOptions) (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 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 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 *Client) 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 *Client) CloseIdleConnections() {
|
||||
c.transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (c *Client) Clone() *Client {
|
||||
return &Client{
|
||||
transport: c.transport.Clone(),
|
||||
headers: c.headers.Clone(),
|
||||
host: c.host,
|
||||
tag: c.tag,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
c.CloseIdleConnections()
|
||||
if closer, isCloser := c.transport.(io.Closer); isCloser {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
14
common/httpclient/context.go
Normal file
14
common/httpclient/context.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package httpclient
|
||||
|
||||
import "context"
|
||||
|
||||
type transportKey struct{}
|
||||
|
||||
func contextWithTransportTag(ctx context.Context, transportTag string) context.Context {
|
||||
return context.WithValue(ctx, transportKey{}, transportTag)
|
||||
}
|
||||
|
||||
func transportTagFromContext(ctx context.Context) (string, bool) {
|
||||
value, loaded := ctx.Value(transportKey{}).(string)
|
||||
return value, loaded
|
||||
}
|
||||
86
common/httpclient/helpers.go
Normal file
86
common/httpclient/helpers.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdTLS "crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func dialTLS(ctx context.Context, rawDialer N.Dialer, baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string, expectProto string) (net.Conn, error) {
|
||||
if baseTLSConfig == nil {
|
||||
return nil, E.New("TLS transport unavailable")
|
||||
}
|
||||
tlsConfig := baseTLSConfig.Clone()
|
||||
if tlsConfig.ServerName() == "" && destination.IsValid() {
|
||||
tlsConfig.SetServerName(destination.AddrString())
|
||||
}
|
||||
tlsConfig.SetNextProtos(nextProtos)
|
||||
conn, err := rawDialer.DialContext(ctx, N.NetworkTCP, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConn, err := tls.ClientHandshake(ctx, conn, tlsConfig)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if expectProto != "" && tlsConn.ConnectionState().NegotiatedProtocol != expectProto {
|
||||
tlsConn.Close()
|
||||
return nil, errHTTP2Fallback
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
func applyHeaders(request *http.Request, headers http.Header, host string) {
|
||||
for header, values := range headers {
|
||||
request.Header[header] = append([]string(nil), values...)
|
||||
}
|
||||
if host != "" {
|
||||
request.Host = host
|
||||
}
|
||||
}
|
||||
|
||||
func requestRequiresHTTP1(request *http.Request) bool {
|
||||
return strings.Contains(strings.ToLower(request.Header.Get("Connection")), "upgrade") &&
|
||||
strings.EqualFold(request.Header.Get("Upgrade"), "websocket")
|
||||
}
|
||||
|
||||
func requestReplayable(request *http.Request) bool {
|
||||
return request.Body == nil || request.Body == http.NoBody || request.GetBody != nil
|
||||
}
|
||||
|
||||
func cloneRequestForRetry(request *http.Request) *http.Request {
|
||||
cloned := request.Clone(request.Context())
|
||||
if request.Body != nil && request.Body != http.NoBody && request.GetBody != nil {
|
||||
cloned.Body = mustGetBody(request)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func mustGetBody(request *http.Request) io.ReadCloser {
|
||||
body, err := request.GetBody()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func buildSTDTLSConfig(baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string) (*stdTLS.Config, error) {
|
||||
if baseTLSConfig == nil {
|
||||
return nil, nil
|
||||
}
|
||||
tlsConfig := baseTLSConfig.Clone()
|
||||
if tlsConfig.ServerName() == "" && destination.IsValid() {
|
||||
tlsConfig.SetServerName(destination.AddrString())
|
||||
}
|
||||
tlsConfig.SetNextProtos(nextProtos)
|
||||
return tlsConfig.STDConfig()
|
||||
}
|
||||
41
common/httpclient/http1_transport.go
Normal file
41
common/httpclient/http1_transport.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"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() httpTransport {
|
||||
return &http1Transport{transport: t.transport.Clone()}
|
||||
}
|
||||
42
common/httpclient/http2_config.go
Normal file
42
common/httpclient/http2_config.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
stdTLS "crypto/tls"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
func CloneHTTP2Transport(transport *http2.Transport) *http2.Transport {
|
||||
return &http2.Transport{
|
||||
ReadIdleTimeout: transport.ReadIdleTimeout,
|
||||
PingTimeout: transport.PingTimeout,
|
||||
DialTLSContext: transport.DialTLSContext,
|
||||
}
|
||||
}
|
||||
|
||||
func ConfigureHTTP2Transport(options option.HTTP2Options) (*http2.Transport, error) {
|
||||
stdTransport := &http.Transport{
|
||||
TLSClientConfig: &stdTLS.Config{},
|
||||
HTTP2: &http.HTTP2Config{
|
||||
MaxReceiveBufferPerStream: int(options.StreamReceiveWindow.Value()),
|
||||
MaxReceiveBufferPerConnection: int(options.ConnectionReceiveWindow.Value()),
|
||||
MaxConcurrentStreams: options.MaxConcurrentStreams,
|
||||
SendPingTimeout: time.Duration(options.KeepAlivePeriod),
|
||||
PingTimeout: time.Duration(options.IdleTimeout),
|
||||
},
|
||||
}
|
||||
h2Transport, err := http2.ConfigureTransports(stdTransport)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "configure HTTP/2 transport")
|
||||
}
|
||||
// ConfigureTransports binds ConnPool to the throwaway http.Transport; sever it so DialTLSContext is used directly.
|
||||
h2Transport.ConnPool = nil
|
||||
h2Transport.ReadIdleTimeout = time.Duration(options.KeepAlivePeriod)
|
||||
h2Transport.PingTimeout = time.Duration(options.IdleTimeout)
|
||||
return h2Transport, nil
|
||||
}
|
||||
87
common/httpclient/http2_fallback_transport.go
Normal file
87
common/httpclient/http2_fallback_transport.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdTLS "crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
"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() httpTransport {
|
||||
return &http2FallbackTransport{
|
||||
h2Transport: CloneHTTP2Transport(t.h2Transport),
|
||||
h1Transport: t.h1Transport.Clone().(*http1Transport),
|
||||
h2Fallback: t.h2Fallback,
|
||||
}
|
||||
}
|
||||
54
common/httpclient/http2_transport.go
Normal file
54
common/httpclient/http2_transport.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdTLS "crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"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() httpTransport {
|
||||
return &http2Transport{
|
||||
h2Transport: CloneHTTP2Transport(t.h2Transport),
|
||||
h1Transport: t.h1Transport.Clone().(*http1Transport),
|
||||
}
|
||||
}
|
||||
311
common/httpclient/http3_transport.go
Normal file
311
common/httpclient/http3_transport.go
Normal file
@@ -0,0 +1,311 @@
|
||||
//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/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 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,
|
||||
) (httpTransport, error) {
|
||||
return &http3Transport{
|
||||
h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newHTTP3FallbackTransport(
|
||||
rawDialer N.Dialer,
|
||||
baseTLSConfig tls.Config,
|
||||
h2Fallback httpTransport,
|
||||
options option.QUICOptions,
|
||||
fallbackDelay time.Duration,
|
||||
) (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() 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() 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)
|
||||
}
|
||||
30
common/httpclient/http3_transport_stub.go
Normal file
30
common/httpclient/http3_transport_stub.go
Normal file
@@ -0,0 +1,30 @@
|
||||
//go:build !with_quic
|
||||
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"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 httpTransport,
|
||||
options option.QUICOptions,
|
||||
fallbackDelay time.Duration,
|
||||
) (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,
|
||||
) (httpTransport, error) {
|
||||
return nil, E.New("HTTP/3 requires building with the with_quic tag")
|
||||
}
|
||||
136
common/httpclient/manager.go
Normal file
136
common/httpclient/manager.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"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
|
||||
clients map[string]*Client
|
||||
defaultTag string
|
||||
defaultTransport http.RoundTripper
|
||||
defaultTransportFallback func() (*Client, error)
|
||||
fallbackClient *Client
|
||||
}
|
||||
|
||||
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,
|
||||
clients: make(map[string]*Client),
|
||||
defaultTag: defaultTag,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Initialize(defaultTransportFallback func() (*Client, 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 {
|
||||
client, err := m.defaultTransportFallback()
|
||||
if err != nil {
|
||||
return E.Cause(err, "create default http client")
|
||||
}
|
||||
m.defaultTransport = client
|
||||
m.fallbackClient = client
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) DefaultTransport() http.RoundTripper {
|
||||
return m.defaultTransport
|
||||
}
|
||||
|
||||
func (m *Manager) ResolveTransport(logger logger.ContextLogger, options option.HTTPClientOptions) (http.RoundTripper, 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 NewClient(m.ctx, logger, options.Tag, resolvedOptions)
|
||||
}
|
||||
return m.resolveShared(options.Tag)
|
||||
}
|
||||
return NewClient(m.ctx, logger, "", options)
|
||||
}
|
||||
|
||||
func (m *Manager) resolveShared(tag string) (http.RoundTripper, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if client, loaded := m.clients[tag]; loaded {
|
||||
return client, nil
|
||||
}
|
||||
define, loaded := m.defines[tag]
|
||||
if !loaded {
|
||||
return nil, E.New("http_client not found: ", tag)
|
||||
}
|
||||
client, err := NewClient(m.ctx, m.logger, tag, define.Options())
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create shared http_client[", tag, "]")
|
||||
}
|
||||
m.clients[tag] = client
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.clients == nil {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
for _, client := range m.clients {
|
||||
err = E.Append(err, client.Close(), func(err error) error {
|
||||
return E.Cause(err, "close http client")
|
||||
})
|
||||
}
|
||||
if m.fallbackClient != nil {
|
||||
err = E.Append(err, m.fallbackClient.Close(), func(err error) error {
|
||||
return E.Cause(err, "close default http client")
|
||||
})
|
||||
}
|
||||
m.clients = nil
|
||||
return err
|
||||
}
|
||||
@@ -9,6 +9,8 @@ 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"
|
||||
@@ -431,6 +433,9 @@ func Run(options Options) (*Result, error) {
|
||||
defer func() {
|
||||
_ = packetConn.Close()
|
||||
}()
|
||||
if deadline.NeedAdditionalReadDeadline(packetConn) {
|
||||
packetConn = deadline.NewPacketConn(bufio.NewPacketConn(packetConn))
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
1103
common/tls/apple_client_platform.go
Normal file
1103
common/tls/apple_client_platform.go
Normal file
File diff suppressed because it is too large
Load Diff
205
common/tls/apple_client_platform_test.go
Normal file
205
common/tls/apple_client_platform_test.go
Normal file
@@ -0,0 +1,205 @@
|
||||
//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
|
||||
|
||||
type appleTLSServerResult struct {
|
||||
state stdtls.ConnectionState
|
||||
err error
|
||||
}
|
||||
|
||||
func TestAppleClientHandshakeAppliesALPNAndVersion(t *testing.T) {
|
||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
||||
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
||||
Certificates: []stdtls.Certificate{serverCertificate},
|
||||
MinVersion: stdtls.VersionTLS12,
|
||||
MaxVersion: stdtls.VersionTLS12,
|
||||
NextProtos: []string{"h2"},
|
||||
})
|
||||
|
||||
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
Engine: "apple",
|
||||
ServerName: "localhost",
|
||||
MinVersion: "1.2",
|
||||
MaxVersion: "1.2",
|
||||
ALPN: badoption.Listable[string]{"h2"},
|
||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer clientConn.Close()
|
||||
|
||||
clientState := clientConn.ConnectionState()
|
||||
if clientState.Version != stdtls.VersionTLS12 {
|
||||
t.Fatalf("unexpected negotiated version: %x", clientState.Version)
|
||||
}
|
||||
if clientState.NegotiatedProtocol != "h2" {
|
||||
t.Fatalf("unexpected negotiated protocol: %q", clientState.NegotiatedProtocol)
|
||||
}
|
||||
|
||||
result := <-serverResult
|
||||
if result.err != nil {
|
||||
t.Fatal(result.err)
|
||||
}
|
||||
if result.state.Version != stdtls.VersionTLS12 {
|
||||
t.Fatalf("server negotiated unexpected version: %x", result.state.Version)
|
||||
}
|
||||
if result.state.NegotiatedProtocol != "h2" {
|
||||
t.Fatalf("server negotiated unexpected protocol: %q", result.state.NegotiatedProtocol)
|
||||
}
|
||||
}
|
||||
|
||||
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 newAppleTestCertificate(t *testing.T, serverName string) (stdtls.Certificate, string) {
|
||||
t.Helper()
|
||||
|
||||
privateKeyPEM, certificatePEM, err := GenerateCertificate(nil, nil, time.Now, serverName, time.Now().Add(time.Hour))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
certificate, err := stdtls.X509KeyPair(certificatePEM, privateKeyPEM)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return certificate, string(certificatePEM)
|
||||
}
|
||||
|
||||
func startAppleTLSTestServer(t *testing.T, tlsConfig *stdtls.Config) (<-chan appleTLSServerResult, string) {
|
||||
t.Helper()
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
listener.Close()
|
||||
})
|
||||
|
||||
if tcpListener, isTCP := listener.(*net.TCPListener); isTCP {
|
||||
err = tcpListener.SetDeadline(time.Now().Add(appleTLSTestTimeout))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
result := make(chan appleTLSServerResult, 1)
|
||||
go func() {
|
||||
defer close(result)
|
||||
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
result <- appleTLSServerResult{err: err}
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
err = conn.SetDeadline(time.Now().Add(appleTLSTestTimeout))
|
||||
if err != nil {
|
||||
result <- appleTLSServerResult{err: err}
|
||||
return
|
||||
}
|
||||
|
||||
tlsConn := stdtls.Server(conn, tlsConfig)
|
||||
defer tlsConn.Close()
|
||||
|
||||
err = tlsConn.Handshake()
|
||||
if err != nil {
|
||||
result <- appleTLSServerResult{err: err}
|
||||
return
|
||||
}
|
||||
|
||||
result <- appleTLSServerResult{state: tlsConn.ConnectionState()}
|
||||
}()
|
||||
|
||||
return result, listener.Addr().String()
|
||||
}
|
||||
|
||||
func newAppleTestClientConn(t *testing.T, serverAddress string, options option.OutboundTLSOptions) (Conn, error) {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), appleTLSTestTimeout)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clientConfig, err := NewClientWithOptions(ClientOptions{
|
||||
Context: ctx,
|
||||
Logger: logger.NOP(),
|
||||
ServerAddress: "",
|
||||
Options: options,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", serverAddress, appleTLSTestTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tlsConn, err := ClientHandshake(ctx, conn, clientConfig)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
15
common/tls/apple_client_stub.go
Normal file
15
common/tls/apple_client_stub.go
Normal file
@@ -0,0 +1,15 @@
|
||||
//go:build !darwin || !cgo
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
func newAppleClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
return nil, E.New("Apple TLS engine is not available on non-Apple platforms")
|
||||
}
|
||||
@@ -8,14 +8,16 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/common/badtls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
|
||||
var errMissingServerName = E.New("missing server_name or insecure=true")
|
||||
|
||||
func NewDialerFromOptions(ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
|
||||
if !options.Enabled {
|
||||
return dialer, nil
|
||||
@@ -42,11 +44,12 @@ func NewClient(ctx context.Context, logger logger.ContextLogger, serverAddress s
|
||||
}
|
||||
|
||||
type ClientOptions struct {
|
||||
Context context.Context
|
||||
Logger logger.ContextLogger
|
||||
ServerAddress string
|
||||
Options option.OutboundTLSOptions
|
||||
KTLSCompatible bool
|
||||
Context context.Context
|
||||
Logger logger.ContextLogger
|
||||
ServerAddress string
|
||||
Options option.OutboundTLSOptions
|
||||
AllowEmptyServerName bool
|
||||
KTLSCompatible bool
|
||||
}
|
||||
|
||||
func NewClientWithOptions(options ClientOptions) (Config, error) {
|
||||
@@ -61,17 +64,22 @@ func NewClientWithOptions(options ClientOptions) (Config, error) {
|
||||
if options.Options.KernelRx {
|
||||
options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx")
|
||||
}
|
||||
if options.Options.Reality != nil && options.Options.Reality.Enabled {
|
||||
return NewRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||
} else if options.Options.UTLS != nil && options.Options.UTLS.Enabled {
|
||||
return NewUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||
switch options.Options.Engine {
|
||||
case "", "go":
|
||||
case "apple":
|
||||
return newAppleClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
||||
default:
|
||||
return nil, E.New("unknown tls engine: ", options.Options.Engine)
|
||||
}
|
||||
return NewSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||
if options.Options.Reality != nil && options.Options.Reality.Enabled {
|
||||
return newRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
||||
} else if options.Options.UTLS != nil && options.Options.UTLS.Enabled {
|
||||
return newUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
||||
}
|
||||
return newSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
||||
}
|
||||
|
||||
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||
defer cancel()
|
||||
tlsConn, err := aTLS.ClientHandshake(ctx, conn, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -52,11 +52,15 @@ type RealityClientConfig struct {
|
||||
}
|
||||
|
||||
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newRealityClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
if options.UTLS == nil || !options.UTLS.Enabled {
|
||||
return nil, E.New("uTLS is required by reality client")
|
||||
}
|
||||
|
||||
uClient, err := NewUTLSClient(ctx, logger, serverAddress, options)
|
||||
uClient, err := newUTLSClient(ctx, logger, serverAddress, options, allowEmptyServerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -108,6 +112,14 @@ func (e *RealityClientConfig) SetNextProtos(nextProto []string) {
|
||||
e.uClient.SetNextProtos(nextProto)
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) HandshakeTimeout() time.Duration {
|
||||
return e.uClient.HandshakeTimeout()
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
e.uClient.SetHandshakeTimeout(timeout)
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) STDConfig() (*STDConfig, error) {
|
||||
return nil, E.New("unsupported usage for reality")
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ import (
|
||||
var _ ServerConfigCompat = (*RealityServerConfig)(nil)
|
||||
|
||||
type RealityServerConfig struct {
|
||||
config *utls.RealityConfig
|
||||
config *utls.RealityConfig
|
||||
handshakeTimeout time.Duration
|
||||
}
|
||||
|
||||
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||
@@ -130,7 +131,16 @@ func NewRealityServer(ctx context.Context, logger log.ContextLogger, options opt
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
return nil, E.New("Reality is conflict with ECH")
|
||||
}
|
||||
var config ServerConfig = &RealityServerConfig{&tlsConfig}
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = C.TCPTimeout
|
||||
}
|
||||
var config ServerConfig = &RealityServerConfig{
|
||||
config: &tlsConfig,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
}
|
||||
if options.KernelTx || options.KernelRx {
|
||||
if !C.IsLinux {
|
||||
return nil, E.New("kTLS is only supported on Linux")
|
||||
@@ -161,6 +171,14 @@ func (c *RealityServerConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *RealityServerConfig) HandshakeTimeout() time.Duration {
|
||||
return c.handshakeTimeout
|
||||
}
|
||||
|
||||
func (c *RealityServerConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
c.handshakeTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *RealityServerConfig) STDConfig() (*tls.Config, error) {
|
||||
return nil, E.New("unsupported usage for reality")
|
||||
}
|
||||
@@ -191,7 +209,8 @@ func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn
|
||||
|
||||
func (c *RealityServerConfig) Clone() Config {
|
||||
return &RealityServerConfig{
|
||||
config: c.config.Clone(),
|
||||
config: c.config.Clone(),
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,8 +46,11 @@ func NewServerWithOptions(options ServerOptions) (ServerConfig, error) {
|
||||
}
|
||||
|
||||
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||
defer cancel()
|
||||
if config.HandshakeTimeout() == 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, C.TCPTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
tlsConn, err := aTLS.ServerHandshake(ctx, conn, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -24,16 +24,30 @@ import (
|
||||
type STDClientConfig struct {
|
||||
ctx context.Context
|
||||
config *tls.Config
|
||||
serverName string
|
||||
disableSNI bool
|
||||
verifyServerName bool
|
||||
handshakeTimeout time.Duration
|
||||
fragment bool
|
||||
fragmentFallbackDelay time.Duration
|
||||
recordFragment bool
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) ServerName() string {
|
||||
return c.config.ServerName
|
||||
return c.serverName
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) SetServerName(serverName string) {
|
||||
c.serverName = serverName
|
||||
if c.disableSNI {
|
||||
c.config.ServerName = ""
|
||||
if c.verifyServerName {
|
||||
c.config.VerifyConnection = verifyConnection(c.config.RootCAs, c.config.Time, serverName)
|
||||
} else {
|
||||
c.config.VerifyConnection = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
c.config.ServerName = serverName
|
||||
}
|
||||
|
||||
@@ -45,6 +59,14 @@ func (c *STDClientConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) HandshakeTimeout() time.Duration {
|
||||
return c.handshakeTimeout
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
c.handshakeTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) STDConfig() (*STDConfig, error) {
|
||||
return c.config, nil
|
||||
}
|
||||
@@ -57,13 +79,19 @@ func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) {
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) Clone() Config {
|
||||
return &STDClientConfig{
|
||||
cloned := &STDClientConfig{
|
||||
ctx: c.ctx,
|
||||
config: c.config.Clone(),
|
||||
serverName: c.serverName,
|
||||
disableSNI: c.disableSNI,
|
||||
verifyServerName: c.verifyServerName,
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
fragment: c.fragment,
|
||||
fragmentFallbackDelay: c.fragmentFallbackDelay,
|
||||
recordFragment: c.recordFragment,
|
||||
}
|
||||
cloned.SetServerName(cloned.serverName)
|
||||
return cloned
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) ECHConfigList() []byte {
|
||||
@@ -75,41 +103,27 @@ func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte
|
||||
}
|
||||
|
||||
func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newSTDClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
var serverName string
|
||||
if options.ServerName != "" {
|
||||
serverName = options.ServerName
|
||||
} else if serverAddress != "" {
|
||||
serverName = serverAddress
|
||||
}
|
||||
if serverName == "" && !options.Insecure {
|
||||
return nil, E.New("missing server_name or insecure=true")
|
||||
if serverName == "" && !options.Insecure && !allowEmptyServerName {
|
||||
return nil, errMissingServerName
|
||||
}
|
||||
|
||||
var tlsConfig tls.Config
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||
if !options.DisableSNI {
|
||||
tlsConfig.ServerName = serverName
|
||||
}
|
||||
if options.Insecure {
|
||||
tlsConfig.InsecureSkipVerify = options.Insecure
|
||||
} else if options.DisableSNI {
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
|
||||
verifyOptions := x509.VerifyOptions{
|
||||
Roots: tlsConfig.RootCAs,
|
||||
DNSName: serverName,
|
||||
Intermediates: x509.NewCertPool(),
|
||||
}
|
||||
for _, cert := range state.PeerCertificates[1:] {
|
||||
verifyOptions.Intermediates.AddCert(cert)
|
||||
}
|
||||
if tlsConfig.Time != nil {
|
||||
verifyOptions.CurrentTime = tlsConfig.Time()
|
||||
}
|
||||
_, err := state.PeerCertificates[0].Verify(verifyOptions)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(options.CertificatePublicKeySHA256) > 0 {
|
||||
if len(options.Certificate) > 0 || options.CertificatePath != "" {
|
||||
@@ -198,7 +212,24 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
|
||||
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
|
||||
return nil, E.New("client certificate and client key must be provided together")
|
||||
}
|
||||
var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = C.TCPTimeout
|
||||
}
|
||||
var config Config = &STDClientConfig{
|
||||
ctx: ctx,
|
||||
config: &tlsConfig,
|
||||
serverName: serverName,
|
||||
disableSNI: options.DisableSNI,
|
||||
verifyServerName: options.DisableSNI && !options.Insecure,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
fragment: options.Fragment,
|
||||
fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay),
|
||||
recordFragment: options.RecordFragment,
|
||||
}
|
||||
config.SetServerName(serverName)
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
var err error
|
||||
config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options)
|
||||
@@ -220,6 +251,27 @@ 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, timeFunc func() time.Time) error {
|
||||
leafCertificate, err := x509.ParseCertificate(rawCerts[0])
|
||||
if err != nil {
|
||||
|
||||
@@ -92,6 +92,7 @@ func getACMENextProtos(provider adapter.CertificateProvider) []string {
|
||||
type STDServerConfig struct {
|
||||
access sync.RWMutex
|
||||
config *tls.Config
|
||||
handshakeTimeout time.Duration
|
||||
logger log.Logger
|
||||
certificateProvider managedCertificateProvider
|
||||
acmeService adapter.SimpleLifecycle
|
||||
@@ -139,6 +140,18 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
||||
c.config = config
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) HandshakeTimeout() time.Duration {
|
||||
c.access.RLock()
|
||||
defer c.access.RUnlock()
|
||||
return c.handshakeTimeout
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
c.handshakeTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) hasACMEALPN() bool {
|
||||
if c.acmeService != nil {
|
||||
return true
|
||||
@@ -165,7 +178,8 @@ func (c *STDServerConfig) Server(conn net.Conn) (Conn, error) {
|
||||
|
||||
func (c *STDServerConfig) Clone() Config {
|
||||
return &STDServerConfig{
|
||||
config: c.config.Clone(),
|
||||
config: c.config.Clone(),
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,8 +485,15 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = C.TCPTimeout
|
||||
}
|
||||
serverConfig := &STDServerConfig{
|
||||
config: tlsConfig,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
logger: logger,
|
||||
certificateProvider: certificateProvider,
|
||||
acmeService: acmeService,
|
||||
|
||||
@@ -28,6 +28,10 @@ import (
|
||||
type UTLSClientConfig struct {
|
||||
ctx context.Context
|
||||
config *utls.Config
|
||||
serverName string
|
||||
disableSNI bool
|
||||
verifyServerName bool
|
||||
handshakeTimeout time.Duration
|
||||
id utls.ClientHelloID
|
||||
fragment bool
|
||||
fragmentFallbackDelay time.Duration
|
||||
@@ -35,10 +39,20 @@ type UTLSClientConfig struct {
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) ServerName() string {
|
||||
return c.config.ServerName
|
||||
return c.serverName
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) SetServerName(serverName string) {
|
||||
c.serverName = serverName
|
||||
if c.disableSNI {
|
||||
c.config.ServerName = ""
|
||||
if c.verifyServerName {
|
||||
c.config.InsecureServerNameToVerify = serverName
|
||||
} else {
|
||||
c.config.InsecureServerNameToVerify = ""
|
||||
}
|
||||
return
|
||||
}
|
||||
c.config.ServerName = serverName
|
||||
}
|
||||
|
||||
@@ -53,6 +67,14 @@ func (c *UTLSClientConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) HandshakeTimeout() time.Duration {
|
||||
return c.handshakeTimeout
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
c.handshakeTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) STDConfig() (*STDConfig, error) {
|
||||
return nil, E.New("unsupported usage for uTLS")
|
||||
}
|
||||
@@ -69,9 +91,20 @@ func (c *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []by
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) Clone() Config {
|
||||
return &UTLSClientConfig{
|
||||
c.ctx, c.config.Clone(), c.id, c.fragment, c.fragmentFallbackDelay, c.recordFragment,
|
||||
cloned := &UTLSClientConfig{
|
||||
ctx: c.ctx,
|
||||
config: c.config.Clone(),
|
||||
serverName: c.serverName,
|
||||
disableSNI: c.disableSNI,
|
||||
verifyServerName: c.verifyServerName,
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
id: c.id,
|
||||
fragment: c.fragment,
|
||||
fragmentFallbackDelay: c.fragmentFallbackDelay,
|
||||
recordFragment: c.recordFragment,
|
||||
}
|
||||
cloned.SetServerName(cloned.serverName)
|
||||
return cloned
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) ECHConfigList() []byte {
|
||||
@@ -143,29 +176,29 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newUTLSClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
var serverName string
|
||||
if options.ServerName != "" {
|
||||
serverName = options.ServerName
|
||||
} else if serverAddress != "" {
|
||||
serverName = serverAddress
|
||||
}
|
||||
if serverName == "" && !options.Insecure {
|
||||
return nil, E.New("missing server_name or insecure=true")
|
||||
if serverName == "" && !options.Insecure && !allowEmptyServerName {
|
||||
return nil, errMissingServerName
|
||||
}
|
||||
|
||||
var tlsConfig utls.Config
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||
if !options.DisableSNI {
|
||||
tlsConfig.ServerName = serverName
|
||||
}
|
||||
if options.Insecure {
|
||||
tlsConfig.InsecureSkipVerify = options.Insecure
|
||||
} else if options.DisableSNI {
|
||||
if options.Reality != nil && options.Reality.Enabled {
|
||||
return nil, E.New("disable_sni is unsupported in reality")
|
||||
}
|
||||
tlsConfig.InsecureServerNameToVerify = serverName
|
||||
}
|
||||
if len(options.CertificatePublicKeySHA256) > 0 {
|
||||
if len(options.Certificate) > 0 || options.CertificatePath != "" {
|
||||
@@ -251,11 +284,29 @@ func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre
|
||||
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
|
||||
return nil, E.New("client certificate and client key must be provided together")
|
||||
}
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = C.TCPTimeout
|
||||
}
|
||||
id, err := uTLSClientHelloID(options.UTLS.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var config Config = &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||
var config Config = &UTLSClientConfig{
|
||||
ctx: ctx,
|
||||
config: &tlsConfig,
|
||||
serverName: serverName,
|
||||
disableSNI: options.DisableSNI,
|
||||
verifyServerName: options.DisableSNI && !options.Insecure,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
id: id,
|
||||
fragment: options.Fragment,
|
||||
fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay),
|
||||
recordFragment: options.RecordFragment,
|
||||
}
|
||||
config.SetServerName(serverName)
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
if options.Reality != nil && options.Reality.Enabled {
|
||||
return nil, E.New("Reality is conflict with ECH")
|
||||
|
||||
@@ -12,10 +12,18 @@ import (
|
||||
)
|
||||
|
||||
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newUTLSClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`)
|
||||
}
|
||||
|
||||
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newRealityClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`)
|
||||
}
|
||||
|
||||
|
||||
578
dns/client.go
578
dns/client.go
@@ -30,59 +30,63 @@ var (
|
||||
var _ adapter.DNSClient = (*Client)(nil)
|
||||
|
||||
type Client 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{}]
|
||||
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{}]
|
||||
}
|
||||
|
||||
type ClientOptions struct {
|
||||
Timeout time.Duration
|
||||
DisableCache bool
|
||||
DisableExpire bool
|
||||
IndependentCache bool
|
||||
CacheCapacity uint32
|
||||
ClientSubnet netip.Prefix
|
||||
RDRC func() adapter.RDRCStore
|
||||
Logger logger.ContextLogger
|
||||
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
|
||||
}
|
||||
|
||||
func NewClient(options ClientOptions) *Client {
|
||||
client := &Client{
|
||||
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
|
||||
}
|
||||
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))
|
||||
}
|
||||
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,
|
||||
}
|
||||
if client.timeout == 0 {
|
||||
client.timeout = C.DNSTimeout
|
||||
}
|
||||
if !client.disableCache && client.initDNSCacheFunc == nil {
|
||||
client.initializeMemoryCache()
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
type transportCacheKey struct {
|
||||
type dnsCacheKey struct {
|
||||
dns.Question
|
||||
transportTag string
|
||||
}
|
||||
@@ -91,6 +95,19 @@ 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) {
|
||||
@@ -107,6 +124,37 @@ 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 {
|
||||
@@ -121,13 +169,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
}
|
||||
return FixedResponseStatus(message, dns.RcodeSuccess), nil
|
||||
}
|
||||
clientSubnet := options.ClientSubnet
|
||||
if !clientSubnet.IsValid() {
|
||||
clientSubnet = c.clientSubnet
|
||||
}
|
||||
if clientSubnet.IsValid() {
|
||||
message = SetClientSubnet(message, clientSubnet)
|
||||
}
|
||||
message = c.prepareExchangeMessage(message, options)
|
||||
|
||||
isSimpleRequest := len(message.Question) == 1 &&
|
||||
len(message.Ns) == 0 &&
|
||||
@@ -139,40 +181,32 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
!options.ClientSubnet.IsValid()
|
||||
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
|
||||
if !disableCache {
|
||||
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)
|
||||
}()
|
||||
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()
|
||||
}
|
||||
} else {
|
||||
defer func() {
|
||||
c.cacheLock.Delete(cacheKey)
|
||||
close(cond)
|
||||
}()
|
||||
}
|
||||
response, ttl := c.loadResponse(question, transport)
|
||||
response, ttl, isStale := c.loadResponse(question, transport)
|
||||
if response != nil {
|
||||
logCachedResponse(c.logger, ctx, response, ttl)
|
||||
response.Id = message.Id
|
||||
return 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,52 +222,10 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
return nil, ErrResponseRejectedCached
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
response, err := transport.Exchange(ctx, message)
|
||||
cancel()
|
||||
response, err := c.exchangeToTransport(ctx, transport, message)
|
||||
if err != nil {
|
||||
var rcodeError RcodeError
|
||||
if errors.As(err, &rcodeError) {
|
||||
response = FixedResponseStatus(message, int(rcodeError))
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
@@ -250,54 +242,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
return response, ErrResponseRejected
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
timeToLive := applyResponseOptions(question, response, options)
|
||||
if !disableCache {
|
||||
c.storeCache(transport, question, response, timeToLive)
|
||||
}
|
||||
@@ -363,8 +308,12 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
|
||||
func (c *Client) ClearCache() {
|
||||
if c.cache != nil {
|
||||
c.cache.Purge()
|
||||
} else if c.transportCache != nil {
|
||||
c.transportCache.Purge()
|
||||
}
|
||||
if c.dnsCache != nil {
|
||||
err := c.dnsCache.ClearDNSCache()
|
||||
if err != nil && c.logger != nil {
|
||||
c.logger.Warn("clear DNS cache: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,24 +329,22 @@ 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 {
|
||||
if !c.independentCache {
|
||||
c.cache.Add(question, message.Copy())
|
||||
} else {
|
||||
c.transportCache.Add(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
}, message.Copy())
|
||||
}
|
||||
c.cache.Add(key, message.Copy())
|
||||
} else {
|
||||
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))
|
||||
}
|
||||
c.cache.AddWithLifetime(key, message.Copy(), time.Second*time.Duration(timeToLive))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,19 +354,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
|
||||
@@ -430,98 +377,177 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran
|
||||
return MessageToAddresses(response), nil
|
||||
}
|
||||
|
||||
func (c *Client) questionCache(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) {
|
||||
response, _ := c.loadResponse(question, transport)
|
||||
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)
|
||||
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) {
|
||||
var (
|
||||
response *dns.Msg
|
||||
loaded bool
|
||||
)
|
||||
if c.disableExpire {
|
||||
if !c.independentCache {
|
||||
response, loaded = c.cache.Get(question)
|
||||
} else {
|
||||
response, loaded = c.transportCache.Get(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
})
|
||||
}
|
||||
if !loaded {
|
||||
return nil, 0
|
||||
}
|
||||
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 {
|
||||
c.transportCache.Remove(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
})
|
||||
}
|
||||
return nil, 0
|
||||
}
|
||||
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
|
||||
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()}
|
||||
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
|
||||
})
|
||||
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 responseChecker != nil {
|
||||
var rejected bool
|
||||
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
||||
rejected = true
|
||||
} else {
|
||||
rejected = !responseChecker(response)
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -22,6 +22,19 @@ 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
|
||||
|
||||
@@ -51,7 +51,7 @@ type Router struct {
|
||||
closing bool
|
||||
}
|
||||
|
||||
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {
|
||||
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) (*Router, error) {
|
||||
router := &Router{
|
||||
ctx: ctx,
|
||||
logger: logFactory.NewLogger("dns"),
|
||||
@@ -61,12 +61,30 @@ 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{
|
||||
DisableCache: options.DNSClientOptions.DisableCache,
|
||||
DisableExpire: options.DNSClientOptions.DisableExpire,
|
||||
IndependentCache: options.DNSClientOptions.IndependentCache,
|
||||
CacheCapacity: options.DNSClientOptions.CacheCapacity,
|
||||
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}),
|
||||
Context: ctx,
|
||||
DisableCache: options.DNSClientOptions.DisableCache,
|
||||
DisableExpire: options.DNSClientOptions.DisableExpire,
|
||||
OptimisticTimeout: optimisticTimeout,
|
||||
CacheCapacity: options.DNSClientOptions.CacheCapacity,
|
||||
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}),
|
||||
RDRC: func() adapter.RDRCStore {
|
||||
cacheFile := service.FromContext[adapter.CacheFile](ctx)
|
||||
if cacheFile == nil {
|
||||
@@ -77,12 +95,24 @@ 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
|
||||
return router, nil
|
||||
}
|
||||
|
||||
func (r *Router) Initialize(rules []option.DNSRule) error {
|
||||
@@ -319,6 +349,9 @@ 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
|
||||
}
|
||||
@@ -907,7 +940,9 @@ 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,
|
||||
disabled: dnsRuleActionType(rule) == C.RuleActionTypeEvaluate ||
|
||||
dnsRuleActionType(rule) == C.RuleActionTypeRespond ||
|
||||
dnsRuleActionDisablesLegacyDNSMode(rule.LogicalOptions.DNSRuleAction),
|
||||
neededFromStrategy: dnsRuleActionHasStrategy(rule.LogicalOptions.DNSRuleAction),
|
||||
}
|
||||
flags.needed = flags.neededFromStrategy
|
||||
@@ -926,7 +961,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),
|
||||
disabled: defaultRuleDisablesLegacyDNSMode(rule) || dnsRuleActionDisablesLegacyDNSMode(rule.DNSRuleAction),
|
||||
neededFromStrategy: dnsRuleActionHasStrategy(rule.DNSRuleAction),
|
||||
}
|
||||
flags.needed = defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule) || flags.neededFromStrategy
|
||||
@@ -1063,6 +1098,17 @@ 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:
|
||||
|
||||
@@ -139,9 +139,9 @@ type fakeRuleSet struct {
|
||||
beforeDecrementReference func()
|
||||
}
|
||||
|
||||
func (s *fakeRuleSet) Name() string { return "fake-rule-set" }
|
||||
func (s *fakeRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error { return nil }
|
||||
func (s *fakeRuleSet) PostStart() error { return nil }
|
||||
func (s *fakeRuleSet) Name() string { return "fake-rule-set" }
|
||||
func (s *fakeRuleSet) StartContext(context.Context) error { return nil }
|
||||
func (s *fakeRuleSet) PostStart() error { return nil }
|
||||
func (s *fakeRuleSet) Metadata() adapter.RuleSetMetadata {
|
||||
s.access.Lock()
|
||||
metadata := s.metadata
|
||||
|
||||
@@ -3,17 +3,18 @@ package transport
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/httpclient"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
@@ -44,14 +45,20 @@ type HTTPSTransport struct {
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
headers http.Header
|
||||
method string
|
||||
host string
|
||||
queryHeaders http.Header
|
||||
transportAccess sync.Mutex
|
||||
transport *HTTPSTransportWrapper
|
||||
transport *httpclient.Client
|
||||
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)
|
||||
remoteOptions := option.RemoteDNSServerOptions{
|
||||
DNSServerAddressOptions: options.DNSServerAddressOptions,
|
||||
}
|
||||
remoteOptions.DialerOptions = options.DialerOptions
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, remoteOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -62,28 +69,21 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
return nil, err
|
||||
}
|
||||
if len(tlsConfig.NextProtos()) == 0 {
|
||||
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
|
||||
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS})
|
||||
} else if !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
||||
tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, tlsConfig.NextProtos()...))
|
||||
}
|
||||
headers := options.Headers.Build()
|
||||
host := headers.Get("Host")
|
||||
if host != "" {
|
||||
headers.Del("Host")
|
||||
} else {
|
||||
if tlsConfig.ServerName() != "" {
|
||||
host = tlsConfig.ServerName()
|
||||
} else {
|
||||
host = options.Server
|
||||
}
|
||||
serverAddr := options.DNSServerAddressOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 443
|
||||
}
|
||||
if !serverAddr.IsValid() {
|
||||
return nil, E.New("invalid server address: ", serverAddr)
|
||||
}
|
||||
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)))
|
||||
Host: doHURLHost(serverAddr, 443),
|
||||
}
|
||||
path := options.Path
|
||||
if path == "" {
|
||||
@@ -93,41 +93,67 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverAddr := options.DNSServerAddressOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 443
|
||||
method := strings.ToUpper(options.Method)
|
||||
if method == "" {
|
||||
method = http.MethodPost
|
||||
}
|
||||
if !serverAddr.IsValid() {
|
||||
return nil, E.New("invalid server address: ", serverAddr)
|
||||
switch method {
|
||||
case http.MethodGet, http.MethodPost:
|
||||
default:
|
||||
return nil, E.New("unsupported HTTPS DNS method: ", options.Method)
|
||||
}
|
||||
return NewHTTPSRaw(
|
||||
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions),
|
||||
httpClientOptions := options.HTTPClientOptions
|
||||
return NewHTTPRaw(
|
||||
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, remoteOptions),
|
||||
logger,
|
||||
transportDialer,
|
||||
&destinationURL,
|
||||
headers,
|
||||
serverAddr,
|
||||
tlsConfig,
|
||||
), nil
|
||||
httpClientOptions,
|
||||
method,
|
||||
)
|
||||
}
|
||||
|
||||
func NewHTTPSRaw(
|
||||
func NewHTTPRaw(
|
||||
adapter dns.TransportAdapter,
|
||||
logger log.ContextLogger,
|
||||
logger logger.ContextLogger,
|
||||
dialer N.Dialer,
|
||||
destination *url.URL,
|
||||
headers http.Header,
|
||||
serverAddr M.Socksaddr,
|
||||
tlsConfig tls.Config,
|
||||
) *HTTPSTransport {
|
||||
httpClientOptions option.HTTPClientOptions,
|
||||
method string,
|
||||
) (*HTTPSTransport, error) {
|
||||
if destination.Scheme == "https" && tlsConfig == nil {
|
||||
return nil, E.New("TLS transport unavailable")
|
||||
}
|
||||
queryHeaders := headers.Clone()
|
||||
if queryHeaders == nil {
|
||||
queryHeaders = make(http.Header)
|
||||
}
|
||||
host := queryHeaders.Get("Host")
|
||||
queryHeaders.Del("Host")
|
||||
queryHeaders.Set("Accept", MimeType)
|
||||
if method == http.MethodPost {
|
||||
queryHeaders.Set("Content-Type", MimeType)
|
||||
}
|
||||
httpClientOptions.Tag = ""
|
||||
httpClientOptions.Headers = nil
|
||||
currentTransport, err := httpclient.NewClientWithDialer(dialer, tlsConfig, "", httpClientOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &HTTPSTransport{
|
||||
TransportAdapter: adapter,
|
||||
logger: logger,
|
||||
dialer: dialer,
|
||||
destination: destination,
|
||||
headers: headers,
|
||||
transport: NewHTTPSTransportWrapper(tls.NewDialer(dialer, tlsConfig), serverAddr),
|
||||
}
|
||||
method: method,
|
||||
host: host,
|
||||
queryHeaders: queryHeaders,
|
||||
transport: currentTransport,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
|
||||
@@ -181,14 +207,25 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
|
||||
requestBuffer.Release()
|
||||
return nil, err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage))
|
||||
requestURL := *t.destination
|
||||
var request *http.Request
|
||||
switch t.method {
|
||||
case http.MethodGet:
|
||||
query := requestURL.Query()
|
||||
query.Set("dns", base64.RawURLEncoding.EncodeToString(rawMessage))
|
||||
requestURL.RawQuery = query.Encode()
|
||||
request, err = http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), nil)
|
||||
default:
|
||||
request, err = http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), bytes.NewReader(rawMessage))
|
||||
}
|
||||
if err != nil {
|
||||
requestBuffer.Release()
|
||||
return nil, err
|
||||
}
|
||||
request.Header = t.headers.Clone()
|
||||
request.Header.Set("Content-Type", MimeType)
|
||||
request.Header.Set("Accept", MimeType)
|
||||
request.Header = t.queryHeaders.Clone()
|
||||
if t.host != "" {
|
||||
request.Host = t.host
|
||||
}
|
||||
t.transportAccess.Lock()
|
||||
currentTransport := t.transport
|
||||
t.transportAccess.Unlock()
|
||||
@@ -222,3 +259,13 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
|
||||
}
|
||||
return &responseMessage, nil
|
||||
}
|
||||
|
||||
func doHURLHost(serverAddr M.Socksaddr, defaultPort uint16) string {
|
||||
if serverAddr.Port != defaultPort {
|
||||
return serverAddr.String()
|
||||
}
|
||||
if serverAddr.IsIPv6() {
|
||||
return "[" + serverAddr.AddrString() + "]"
|
||||
}
|
||||
return serverAddr.AddrString()
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var errFallback = E.New("fallback to HTTP/1.1")
|
||||
|
||||
type HTTPSTransportWrapper struct {
|
||||
http2Transport *http2.Transport
|
||||
httpTransport *http.Transport
|
||||
fallback *atomic.Bool
|
||||
}
|
||||
|
||||
func NewHTTPSTransportWrapper(dialer tls.Dialer, serverAddr M.Socksaddr) *HTTPSTransportWrapper {
|
||||
var fallback atomic.Bool
|
||||
return &HTTPSTransportWrapper{
|
||||
http2Transport: &http2.Transport{
|
||||
DialTLSContext: func(ctx context.Context, _, _ string, _ *tls.STDConfig) (net.Conn, error) {
|
||||
tlsConn, err := dialer.DialTLSContext(ctx, serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state := tlsConn.ConnectionState()
|
||||
if state.NegotiatedProtocol == http2.NextProtoTLS {
|
||||
return tlsConn, nil
|
||||
}
|
||||
tlsConn.Close()
|
||||
fallback.Store(true)
|
||||
return nil, errFallback
|
||||
},
|
||||
},
|
||||
httpTransport: &http.Transport{
|
||||
DialTLSContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return dialer.DialTLSContext(ctx, serverAddr)
|
||||
},
|
||||
},
|
||||
fallback: &fallback,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTTPSTransportWrapper) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
if h.fallback.Load() {
|
||||
return h.httpTransport.RoundTrip(request)
|
||||
} else {
|
||||
response, err := h.http2Transport.RoundTrip(request)
|
||||
if err != nil {
|
||||
if errors.Is(err, errFallback) {
|
||||
return h.httpTransport.RoundTrip(request)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTTPSTransportWrapper) CloseIdleConnections() {
|
||||
h.http2Transport.CloseIdleConnections()
|
||||
h.httpTransport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (h *HTTPSTransportWrapper) Clone() *HTTPSTransportWrapper {
|
||||
return &HTTPSTransportWrapper{
|
||||
httpTransport: h.httpTransport,
|
||||
http2Transport: &http2.Transport{
|
||||
DialTLSContext: h.http2Transport.DialTLSContext,
|
||||
},
|
||||
fallback: h.fallback,
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,24 @@ package local
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
#include <dns.h>
|
||||
#include <resolv.h>
|
||||
#include <netdb.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);
|
||||
}
|
||||
|
||||
static void *cgo_res_init() {
|
||||
res_state state = calloc(1, sizeof(struct __res_state));
|
||||
@@ -52,7 +68,59 @@ import (
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func resolvSearch(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) {
|
||||
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) {
|
||||
state := C.cgo_res_init()
|
||||
if state == nil {
|
||||
return nil, E.New("res_ninit failed")
|
||||
@@ -61,6 +129,7 @@ func resolvSearch(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg,
|
||||
|
||||
cName := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cName))
|
||||
|
||||
bufSize := 1232
|
||||
for {
|
||||
answer := make([]byte, bufSize)
|
||||
@@ -74,37 +143,55 @@ func resolvSearch(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg,
|
||||
bufSize = int(n)
|
||||
continue
|
||||
}
|
||||
var response mDNS.Msg
|
||||
err := response.Unpack(answer[:int(n)])
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "unpack res_nsearch response")
|
||||
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
|
||||
}
|
||||
return &response, nil
|
||||
continue
|
||||
}
|
||||
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 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
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +228,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
|
||||
}
|
||||
resultCh := make(chan resolvResult, 1)
|
||||
go func() {
|
||||
response, err := resolvSearch(name, int(question.Qclass), int(question.Qtype), timeoutSeconds)
|
||||
response, err := darwinLookupSystemDNS(name, int(question.Qclass), int(question.Qtype), timeoutSeconds)
|
||||
resultCh <- resolvResult{response, err}
|
||||
}()
|
||||
var result resolvResult
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
@@ -40,18 +41,23 @@ func RegisterHTTP3Transport(registry *dns.TransportRegistry) {
|
||||
|
||||
type HTTP3Transport struct {
|
||||
dns.TransportAdapter
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
headers http.Header
|
||||
serverAddr M.Socksaddr
|
||||
tlsConfig *tls.STDConfig
|
||||
transportAccess sync.Mutex
|
||||
transport *http3.Transport
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
headers http.Header
|
||||
handshakeTimeout time.Duration
|
||||
serverAddr M.Socksaddr
|
||||
tlsConfig *tls.STDConfig
|
||||
transportAccess sync.Mutex
|
||||
transport *http3.Transport
|
||||
}
|
||||
|
||||
func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
||||
remoteOptions := option.RemoteDNSServerOptions{
|
||||
DNSServerAddressOptions: options.DNSServerAddressOptions,
|
||||
}
|
||||
remoteOptions.DialerOptions = options.DialerOptions
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, remoteOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -61,6 +67,7 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handshakeTimeout := tlsConfig.HandshakeTimeout()
|
||||
stdConfig, err := tlsConfig.STDConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -102,11 +109,12 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
return nil, E.New("invalid server address: ", serverAddr)
|
||||
}
|
||||
t := &HTTP3Transport{
|
||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
|
||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, remoteOptions),
|
||||
logger: logger,
|
||||
dialer: transportDialer,
|
||||
destination: &destinationURL,
|
||||
headers: headers,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
serverAddr: serverAddr,
|
||||
tlsConfig: stdConfig,
|
||||
}
|
||||
@@ -115,8 +123,17 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
}
|
||||
|
||||
func (t *HTTP3Transport) newTransport() *http3.Transport {
|
||||
quicConfig := &quic.Config{}
|
||||
if t.handshakeTimeout > 0 {
|
||||
quicConfig.HandshakeIdleTimeout = t.handshakeTimeout
|
||||
}
|
||||
return &http3.Transport{
|
||||
QUICConfig: quicConfig,
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (*quic.Conn, error) {
|
||||
if t.handshakeTimeout > 0 && cfg.HandshakeIdleTimeout == 0 {
|
||||
cfg = cfg.Clone()
|
||||
cfg.HandshakeIdleTimeout = t.handshakeTimeout
|
||||
}
|
||||
conn, dialErr := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
|
||||
if dialErr != nil {
|
||||
return nil, dialErr
|
||||
|
||||
@@ -2,11 +2,98 @@
|
||||
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
|
||||
|
||||
#### 1.13.6
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.6
|
||||
#### 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
|
||||
|
||||
@@ -14,10 +101,33 @@ icon: material/alert-decagram
|
||||
|
||||
* 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**
|
||||
@@ -42,6 +152,59 @@ 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
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
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)
|
||||
@@ -25,6 +30,7 @@ icon: material/alert-decagram
|
||||
"disable_expire": false,
|
||||
"independent_cache": false,
|
||||
"cache_capacity": 0,
|
||||
"optimistic": false, // or {}
|
||||
"reverse_mapping": false,
|
||||
"client_subnet": "",
|
||||
"fakeip": {}
|
||||
@@ -57,12 +63,20 @@ 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
|
||||
@@ -73,6 +87,34 @@ 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.
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
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)
|
||||
@@ -25,6 +30,7 @@ icon: material/alert-decagram
|
||||
"disable_expire": false,
|
||||
"independent_cache": false,
|
||||
"cache_capacity": 0,
|
||||
"optimistic": false, // or {}
|
||||
"reverse_mapping": false,
|
||||
"client_subnet": "",
|
||||
"fakeip": {}
|
||||
@@ -56,12 +62,20 @@ 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
|
||||
@@ -72,6 +86,34 @@ 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 地址的反向映射以为路由目的提供域名。
|
||||
|
||||
@@ -6,7 +6,8 @@ icon: material/new-box
|
||||
|
||||
:material-delete-clock: [strategy](#strategy)
|
||||
:material-plus: [evaluate](#evaluate)
|
||||
:material-plus: [respond](#respond)
|
||||
:material-plus: [respond](#respond)
|
||||
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
@@ -23,6 +24,7 @@ icon: material/new-box
|
||||
"server": "",
|
||||
"strategy": "",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
@@ -52,6 +54,12 @@ 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.
|
||||
@@ -73,6 +81,7 @@ Will override `dns.client_subnet`.
|
||||
"action": "evaluate",
|
||||
"server": "",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
@@ -97,6 +106,12 @@ 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.
|
||||
@@ -131,6 +146,7 @@ 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
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ icon: material/new-box
|
||||
|
||||
:material-delete-clock: [strategy](#strategy)
|
||||
:material-plus: [evaluate](#evaluate)
|
||||
:material-plus: [respond](#respond)
|
||||
:material-plus: [respond](#respond)
|
||||
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
@@ -23,6 +24,7 @@ icon: material/new-box
|
||||
"server": "",
|
||||
"strategy": "",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
@@ -52,6 +54,12 @@ icon: material/new-box
|
||||
|
||||
在此查询中禁用缓存。
|
||||
|
||||
#### disable_optimistic_cache
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
在此查询中禁用乐观 DNS 缓存。
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
重写 DNS 回应中的 TTL。
|
||||
@@ -73,6 +81,7 @@ icon: material/new-box
|
||||
"action": "evaluate",
|
||||
"server": "",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
@@ -95,6 +104,12 @@ icon: material/new-box
|
||||
|
||||
在此查询中禁用缓存。
|
||||
|
||||
#### disable_optimistic_cache
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
在此查询中禁用乐观 DNS 缓存。
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
重写 DNS 回应中的 TTL。
|
||||
@@ -129,6 +144,7 @@ icon: material/new-box
|
||||
{
|
||||
"action": "route-options",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-alert: `headers`, `tls`, Dial Fields moved to [HTTP Client Fields](#http-client-fields)
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
# DNS over HTTP3 (DoH3)
|
||||
@@ -15,27 +19,20 @@ icon: material/new-box
|
||||
{
|
||||
"type": "h3",
|
||||
"tag": "",
|
||||
|
||||
|
||||
"server": "",
|
||||
"server_port": 443,
|
||||
|
||||
"server_port": 0,
|
||||
|
||||
"path": "",
|
||||
"headers": {},
|
||||
|
||||
"tls": {},
|
||||
|
||||
// Dial Fields
|
||||
"method": "",
|
||||
|
||||
... // HTTP Client Fields
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "Difference from legacy H3 server"
|
||||
|
||||
* The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default.
|
||||
* The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead.
|
||||
|
||||
### Fields
|
||||
|
||||
#### server
|
||||
@@ -58,14 +55,14 @@ The path of the DNS server.
|
||||
|
||||
`/dns-query` will be used by default.
|
||||
|
||||
#### headers
|
||||
#### method
|
||||
|
||||
Additional headers to be sent to the DNS server.
|
||||
HTTP request method.
|
||||
|
||||
#### tls
|
||||
Available values: `GET`, `POST`.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
`POST` will be used by default.
|
||||
|
||||
### Dial Fields
|
||||
### HTTP Client Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-alert: `headers`、`tls`、拨号字段已移至 [HTTP 客户端字段](#http-客户端字段)
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
# DNS over HTTP3 (DoH3)
|
||||
@@ -17,25 +21,18 @@ icon: material/new-box
|
||||
"tag": "",
|
||||
|
||||
"server": "",
|
||||
"server_port": 443,
|
||||
"server_port": 0,
|
||||
|
||||
"path": "",
|
||||
"headers": {},
|
||||
"method": "",
|
||||
|
||||
"tls": {},
|
||||
|
||||
// 拨号字段
|
||||
... // HTTP 客户端字段
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "与旧版 H3 服务器的区别"
|
||||
|
||||
* 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。
|
||||
* 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。
|
||||
|
||||
### 字段
|
||||
|
||||
#### server
|
||||
@@ -58,14 +55,14 @@ DNS 服务器的路径。
|
||||
|
||||
默认使用 `/dns-query`。
|
||||
|
||||
#### headers
|
||||
#### method
|
||||
|
||||
发送到 DNS 服务器的额外标头。
|
||||
HTTP 请求方法。
|
||||
|
||||
#### tls
|
||||
可用值:`GET`、`POST`。
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
默认使用 `POST`。
|
||||
|
||||
### 拨号字段
|
||||
### HTTP 客户端字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-alert: `headers`, `tls`, Dial Fields moved to [HTTP Client Fields](#http-client-fields)
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
# DNS over HTTPS (DoH)
|
||||
@@ -15,27 +19,20 @@ icon: material/new-box
|
||||
{
|
||||
"type": "https",
|
||||
"tag": "",
|
||||
|
||||
|
||||
"server": "",
|
||||
"server_port": 443,
|
||||
|
||||
"server_port": 0,
|
||||
|
||||
"path": "",
|
||||
"headers": {},
|
||||
|
||||
"tls": {},
|
||||
|
||||
// Dial Fields
|
||||
"method": "",
|
||||
|
||||
... // HTTP Client Fields
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "Difference from legacy HTTPS server"
|
||||
|
||||
* The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default.
|
||||
* The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead.
|
||||
|
||||
### Fields
|
||||
|
||||
#### server
|
||||
@@ -58,14 +55,14 @@ The path of the DNS server.
|
||||
|
||||
`/dns-query` will be used by default.
|
||||
|
||||
#### headers
|
||||
#### method
|
||||
|
||||
Additional headers to be sent to the DNS server.
|
||||
HTTP request method.
|
||||
|
||||
#### tls
|
||||
Available values: `GET`, `POST`.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
`POST` will be used by default.
|
||||
|
||||
### Dial Fields
|
||||
### HTTP Client Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-alert: `headers`、`tls`、拨号字段已移至 [HTTP 客户端字段](#http-客户端字段)
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
# DNS over HTTPS (DoH)
|
||||
@@ -17,25 +21,18 @@ icon: material/new-box
|
||||
"tag": "",
|
||||
|
||||
"server": "",
|
||||
"server_port": 443,
|
||||
"server_port": 0,
|
||||
|
||||
"path": "",
|
||||
"headers": {},
|
||||
"method": "",
|
||||
|
||||
"tls": {},
|
||||
|
||||
// 拨号字段
|
||||
... // HTTP 客户端字段
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "与旧版 HTTPS 服务器的区别"
|
||||
|
||||
* 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。
|
||||
* 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。
|
||||
|
||||
### 字段
|
||||
|
||||
#### server
|
||||
@@ -58,14 +55,14 @@ DNS 服务器的路径。
|
||||
|
||||
默认使用 `/dns-query`。
|
||||
|
||||
#### headers
|
||||
#### method
|
||||
|
||||
发送到 DNS 服务器的额外标头。
|
||||
HTTP 请求方法。
|
||||
|
||||
#### tls
|
||||
可用值:`GET`、`POST`。
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
默认使用 `POST`。
|
||||
|
||||
### 拨号字段
|
||||
### HTTP 客户端字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [control_http_client](#control_http_client)
|
||||
:material-delete-clock: [Dial Fields](#dial-fields)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [relay_server_port](#relay_server_port)
|
||||
@@ -22,6 +27,7 @@ icon: material/new-box
|
||||
"state_directory": "",
|
||||
"auth_key": "",
|
||||
"control_url": "",
|
||||
"control_http_client": {}, // or ""
|
||||
"ephemeral": false,
|
||||
"hostname": "",
|
||||
"accept_routes": false,
|
||||
@@ -148,10 +154,18 @@ UDP NAT expiration time.
|
||||
|
||||
`5m` will be used by default.
|
||||
|
||||
#### control_http_client
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
HTTP Client for connecting to the Tailscale control plane.
|
||||
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
!!! note
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Dial Fields in Tailscale endpoints only control how it connects to the control plane and have nothing to do with actual connections.
|
||||
Dial Fields in Tailscale endpoints are deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0, use `control_http_client` instead.
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [control_http_client](#control_http_client)
|
||||
:material-delete-clock: [拨号字段](#拨号字段)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [relay_server_port](#relay_server_port)
|
||||
@@ -22,6 +27,7 @@ icon: material/new-box
|
||||
"state_directory": "",
|
||||
"auth_key": "",
|
||||
"control_url": "",
|
||||
"control_http_client": {}, // 或 ""
|
||||
"ephemeral": false,
|
||||
"hostname": "",
|
||||
"accept_routes": false,
|
||||
@@ -147,10 +153,18 @@ UDP NAT 过期时间。
|
||||
|
||||
默认使用 `5m`。
|
||||
|
||||
#### control_http_client
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
用于连接 Tailscale 控制平面的 HTTP 客户端。
|
||||
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
!!! note
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
Tailscale 端点中的拨号字段仅控制它如何连接到控制平面,与实际连接无关。
|
||||
Tailscale 端点中的拨号字段已在 sing-box 1.14.0 废弃且将在 sing-box 1.16.0 中被移除,请使用 `control_http_client` 代替。
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
!!! 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)
|
||||
@@ -14,7 +19,8 @@
|
||||
"cache_id": "",
|
||||
"store_fakeip": false,
|
||||
"store_rdrc": false,
|
||||
"rdrc_timeout": ""
|
||||
"rdrc_timeout": "",
|
||||
"store_dns": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -42,6 +48,10 @@ 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)
|
||||
@@ -52,3 +62,9 @@ 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.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
!!! 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)
|
||||
@@ -14,7 +19,8 @@
|
||||
"cache_id": "",
|
||||
"store_fakeip": false,
|
||||
"store_rdrc": false,
|
||||
"rdrc_timeout": ""
|
||||
"rdrc_timeout": "",
|
||||
"store_dns": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -40,6 +46,10 @@
|
||||
|
||||
#### 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/#旧版地址筛选字段) 的检查结果将被缓存至过期。
|
||||
@@ -49,3 +59,9 @@
|
||||
拒绝的 DNS 响应缓存超时。
|
||||
|
||||
默认使用 `7d`。
|
||||
|
||||
#### store_dns
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
将 DNS 缓存存储在缓存文件中。
|
||||
|
||||
@@ -21,11 +21,16 @@
|
||||
}
|
||||
],
|
||||
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
// Deprecated
|
||||
|
||||
"recv_window_conn": 0,
|
||||
"recv_window_client": 0,
|
||||
"max_conn_client": 0,
|
||||
"disable_mtu_discovery": false,
|
||||
"tls": {}
|
||||
"disable_mtu_discovery": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -76,32 +81,38 @@ Authentication password, in base64.
|
||||
|
||||
Authentication password.
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
The QUIC stream-level flow control window for receiving data.
|
||||
|
||||
`15728640 (15 MB/s)` will be used if empty.
|
||||
|
||||
#### recv_window_client
|
||||
|
||||
The QUIC connection-level flow control window for receiving data.
|
||||
|
||||
`67108864 (64 MB/s)` will be used if empty.
|
||||
|
||||
#### max_conn_client
|
||||
|
||||
The maximum number of QUIC concurrent bidirectional streams that a peer is allowed to open.
|
||||
|
||||
`1024` will be used if empty.
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
|
||||
|
||||
Force enabled on for systems other than Linux and Windows (according to upstream).
|
||||
|
||||
#### tls
|
||||
|
||||
==Required==
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
### Deprecated Fields
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `stream_receive_window` instead.
|
||||
|
||||
#### recv_window_client
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `connection_receive_window` instead.
|
||||
|
||||
#### max_conn_client
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `max_concurrent_streams` instead.
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `disable_path_mtu_discovery` instead.
|
||||
@@ -21,11 +21,16 @@
|
||||
}
|
||||
],
|
||||
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
// 废弃的
|
||||
|
||||
"recv_window_conn": 0,
|
||||
"recv_window_client": 0,
|
||||
"max_conn_client": 0,
|
||||
"disable_mtu_discovery": false,
|
||||
"tls": {}
|
||||
"disable_mtu_discovery": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -76,32 +81,38 @@ base64 编码的认证密码。
|
||||
|
||||
认证密码。
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
用于接收数据的 QUIC 流级流控制窗口。
|
||||
|
||||
默认 `15728640 (15 MB/s)`。
|
||||
|
||||
#### recv_window_client
|
||||
|
||||
用于接收数据的 QUIC 连接级流控制窗口。
|
||||
|
||||
默认 `67108864 (64 MB/s)`。
|
||||
|
||||
#### max_conn_client
|
||||
|
||||
允许对等点打开的 QUIC 并发双向流的最大数量。
|
||||
|
||||
默认 `1024`。
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
禁用路径 MTU 发现 (RFC 8899)。 数据包的大小最多为 1252 (IPv4) / 1232 (IPv6) 字节。
|
||||
|
||||
强制为 Linux 和 Windows 以外的系统启用(根据上游)。
|
||||
|
||||
#### tls
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
### 废弃字段
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `stream_receive_window` 代替。
|
||||
|
||||
#### recv_window_client
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `connection_receive_window` 代替。
|
||||
|
||||
#### max_conn_client
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `max_concurrent_streams` 代替。
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `disable_path_mtu_discovery` 代替。
|
||||
@@ -34,6 +34,9 @@ icon: material/alert-decagram
|
||||
],
|
||||
"ignore_client_bandwidth": false,
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
"masquerade": "", // or {}
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false
|
||||
@@ -95,6 +98,10 @@ Deny clients to use the BBR CC.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
#### masquerade
|
||||
|
||||
HTTP3 server behavior (URL string configuration) when authentication fails.
|
||||
|
||||
@@ -34,6 +34,9 @@ icon: material/alert-decagram
|
||||
],
|
||||
"ignore_client_bandwidth": false,
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
"masquerade": "", // 或 {}
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false
|
||||
@@ -92,6 +95,10 @@ Hysteria 用户
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
#### masquerade
|
||||
|
||||
HTTP3 服务器认证失败时的行为 (URL 字符串配置)。
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
"auth_timeout": "3s",
|
||||
"zero_rtt_handshake": false,
|
||||
"heartbeat": "10s",
|
||||
"tls": {}
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,4 +77,8 @@ Interval for sending heartbeat packets for keeping the connection alive
|
||||
|
||||
==Required==
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
@@ -18,7 +18,9 @@
|
||||
"auth_timeout": "3s",
|
||||
"zero_rtt_handshake": false,
|
||||
"heartbeat": "10s",
|
||||
"tls": {}
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,4 +77,8 @@ QUIC 拥塞控制算法
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
@@ -10,6 +10,7 @@ sing-box uses JSON for configuration files.
|
||||
"ntp": {},
|
||||
"certificate": {},
|
||||
"certificate_providers": [],
|
||||
"http_clients": [],
|
||||
"endpoints": [],
|
||||
"inbounds": [],
|
||||
"outbounds": [],
|
||||
@@ -28,6 +29,7 @@ sing-box uses JSON for configuration files.
|
||||
| `ntp` | [NTP](./ntp/) |
|
||||
| `certificate` | [Certificate](./certificate/) |
|
||||
| `certificate_providers` | [Certificate Provider](./shared/certificate-provider/) |
|
||||
| `http_clients` | [HTTP Client](./shared/http-client/) |
|
||||
| `endpoints` | [Endpoint](./endpoint/) |
|
||||
| `inbounds` | [Inbound](./inbound/) |
|
||||
| `outbounds` | [Outbound](./outbound/) |
|
||||
|
||||
@@ -10,6 +10,7 @@ sing-box 使用 JSON 作为配置文件格式。
|
||||
"ntp": {},
|
||||
"certificate": {},
|
||||
"certificate_providers": [],
|
||||
"http_clients": [],
|
||||
"endpoints": [],
|
||||
"inbounds": [],
|
||||
"outbounds": [],
|
||||
@@ -28,6 +29,7 @@ sing-box 使用 JSON 作为配置文件格式。
|
||||
| `ntp` | [NTP](./ntp/) |
|
||||
| `certificate` | [证书](./certificate/) |
|
||||
| `certificate_providers` | [证书提供者](./shared/certificate-provider/) |
|
||||
| `http_clients` | [HTTP 客户端](./shared/http-client/) |
|
||||
| `endpoints` | [端点](./endpoint/) |
|
||||
| `inbounds` | [入站](./inbound/) |
|
||||
| `outbounds` | [出站](./outbound/) |
|
||||
|
||||
@@ -27,13 +27,18 @@ icon: material/new-box
|
||||
"obfs": "fuck me till the daylight",
|
||||
"auth": "",
|
||||
"auth_str": "password",
|
||||
"network": "",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
... // Dial Fields
|
||||
|
||||
// Deprecated
|
||||
|
||||
"recv_window_conn": 0,
|
||||
"recv_window": 0,
|
||||
"disable_mtu_discovery": false,
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // Dial Fields
|
||||
"disable_mtu_discovery": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -104,24 +109,6 @@ Authentication password, in base64.
|
||||
|
||||
Authentication password.
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
The QUIC stream-level flow control window for receiving data.
|
||||
|
||||
`15728640 (15 MB/s)` will be used if empty.
|
||||
|
||||
#### recv_window
|
||||
|
||||
The QUIC connection-level flow control window for receiving data.
|
||||
|
||||
`67108864 (64 MB/s)` will be used if empty.
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
|
||||
|
||||
Force enabled on for systems other than Linux and Windows (according to upstream).
|
||||
|
||||
#### network
|
||||
|
||||
Enabled network
|
||||
@@ -136,6 +123,30 @@ Both is enabled by default.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
### Deprecated Fields
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `stream_receive_window` instead.
|
||||
|
||||
#### recv_window
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `connection_receive_window` instead.
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `disable_path_mtu_discovery` instead.
|
||||
|
||||
@@ -27,13 +27,18 @@ icon: material/new-box
|
||||
"obfs": "fuck me till the daylight",
|
||||
"auth": "",
|
||||
"auth_str": "password",
|
||||
"network": "",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
... // 拨号字段
|
||||
|
||||
// 废弃的
|
||||
|
||||
"recv_window_conn": 0,
|
||||
"recv_window": 0,
|
||||
"disable_mtu_discovery": false,
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // 拨号字段
|
||||
"disable_mtu_discovery": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -104,24 +109,6 @@ base64 编码的认证密码。
|
||||
|
||||
认证密码。
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
用于接收数据的 QUIC 流级流控制窗口。
|
||||
|
||||
默认 `15728640 (15 MB/s)`。
|
||||
|
||||
#### recv_window
|
||||
|
||||
用于接收数据的 QUIC 连接级流控制窗口。
|
||||
|
||||
默认 `67108864 (64 MB/s)`。
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
禁用路径 MTU 发现 (RFC 8899)。 数据包的大小最多为 1252 (IPv4) / 1232 (IPv6) 字节。
|
||||
|
||||
强制为 Linux 和 Windows 以外的系统启用(根据上游)。
|
||||
|
||||
#### network
|
||||
|
||||
启用的网络协议。
|
||||
@@ -136,7 +123,30 @@ base64 编码的认证密码。
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||
|
||||
### 废弃字段
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `stream_receive_window` 代替。
|
||||
|
||||
#### recv_window
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `connection_receive_window` 代替。
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `disable_path_mtu_discovery` 代替。
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
"password": "goofy_ahh_password",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false,
|
||||
|
||||
@@ -124,6 +127,10 @@ Both is enabled by default.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
#### bbr_profile
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
"password": "goofy_ahh_password",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false,
|
||||
|
||||
@@ -122,6 +125,10 @@ QUIC 流量混淆器密码.
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
#### bbr_profile
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
"heartbeat": "10s",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
```
|
||||
@@ -91,6 +93,10 @@ Both is enabled by default.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
"heartbeat": "10s",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
```
|
||||
@@ -99,6 +101,10 @@ UDP 包中继模式
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||
|
||||
@@ -6,6 +6,7 @@ icon: material/alert-decagram
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [default_http_client](#default_http_client)
|
||||
:material-plus: [find_neighbor](#find_neighbor)
|
||||
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
|
||||
|
||||
@@ -43,6 +44,7 @@ icon: material/alert-decagram
|
||||
"find_process": false,
|
||||
"find_neighbor": false,
|
||||
"dhcp_lease_files": [],
|
||||
"default_http_client": "",
|
||||
"default_domain_resolver": "", // or {}
|
||||
"default_network_strategy": "",
|
||||
"default_network_type": [],
|
||||
@@ -147,6 +149,14 @@ Custom DHCP lease file paths for hostname and MAC address resolution.
|
||||
|
||||
Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea) if empty.
|
||||
|
||||
#### default_http_client
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Tag of the default [HTTP Client](/configuration/shared/http-client/) used by remote rule-sets.
|
||||
|
||||
If empty and `http_clients` is defined, the first HTTP client is used.
|
||||
|
||||
#### default_domain_resolver
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
@@ -6,6 +6,7 @@ icon: material/alert-decagram
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [default_http_client](#default_http_client)
|
||||
:material-plus: [find_neighbor](#find_neighbor)
|
||||
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
|
||||
|
||||
@@ -45,6 +46,7 @@ icon: material/alert-decagram
|
||||
"find_process": false,
|
||||
"find_neighbor": false,
|
||||
"dhcp_lease_files": [],
|
||||
"default_http_client": "",
|
||||
"default_network_strategy": "",
|
||||
"default_fallback_delay": ""
|
||||
}
|
||||
@@ -146,6 +148,14 @@ icon: material/alert-decagram
|
||||
|
||||
为空时自动从常见 DHCP 服务器(dnsmasq、odhcpd、ISC dhcpd、Kea)检测。
|
||||
|
||||
#### default_http_client
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
远程规则集使用的默认 [HTTP 客户端](/zh/configuration/shared/http-client/) 的标签。
|
||||
|
||||
如果为空且 `http_clients` 已定义,将使用第一个 HTTP 客户端。
|
||||
|
||||
#### default_domain_resolver
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
@@ -7,6 +7,10 @@ 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)
|
||||
@@ -279,6 +283,7 @@ Timeout for sniffing.
|
||||
"server": "",
|
||||
"strategy": "",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
@@ -302,6 +307,12 @@ 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"
|
||||
|
||||
@@ -7,6 +7,10 @@ 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)
|
||||
@@ -268,6 +272,7 @@ UDP 连接超时时间。
|
||||
"server": "",
|
||||
"strategy": "",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
@@ -291,6 +296,12 @@ 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 起"
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [http_client](#http_client)
|
||||
:material-delete-clock: [download_detour](#download_detour)
|
||||
|
||||
!!! quote "Changes in sing-box 1.10.0"
|
||||
|
||||
:material-plus: `type: inline`
|
||||
@@ -43,8 +48,12 @@
|
||||
"tag": "",
|
||||
"format": "source", // or binary
|
||||
"url": "",
|
||||
"download_detour": "", // optional
|
||||
"update_interval": "" // optional
|
||||
"http_client": "", // or {}
|
||||
"update_interval": "",
|
||||
|
||||
// Deprecated
|
||||
|
||||
"download_detour": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -102,14 +111,26 @@ File path of rule-set.
|
||||
|
||||
Download URL of rule-set.
|
||||
|
||||
#### download_detour
|
||||
#### http_client
|
||||
|
||||
Tag of the outbound to download rule-set.
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Default outbound will be used if empty.
|
||||
HTTP Client for downloading rule-set.
|
||||
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
Default transport will be used if empty.
|
||||
|
||||
#### update_interval
|
||||
|
||||
Update interval of rule-set.
|
||||
|
||||
`1d` will be used if empty.
|
||||
|
||||
#### download_detour
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
`download_detour` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0, use `http_client` instead.
|
||||
|
||||
Tag of the outbound to download rule-set.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [http_client](#http_client)
|
||||
:material-delete-clock: [download_detour](#download_detour)
|
||||
|
||||
!!! quote "sing-box 1.10.0 中的更改"
|
||||
|
||||
:material-plus: `type: inline`
|
||||
@@ -43,8 +48,12 @@
|
||||
"tag": "",
|
||||
"format": "source", // or binary
|
||||
"url": "",
|
||||
"download_detour": "", // 可选
|
||||
"update_interval": "" // 可选
|
||||
"http_client": "", // 或 {}
|
||||
"update_interval": "",
|
||||
|
||||
// 废弃的
|
||||
|
||||
"download_detour": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -102,14 +111,26 @@
|
||||
|
||||
规则集的下载 URL。
|
||||
|
||||
#### download_detour
|
||||
#### http_client
|
||||
|
||||
用于下载规则集的出站的标签。
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
如果为空,将使用默认出站。
|
||||
用于下载规则集的 HTTP 客户端。
|
||||
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
如果为空,将使用默认传输。
|
||||
|
||||
#### update_interval
|
||||
|
||||
规则集的更新间隔。
|
||||
|
||||
默认使用 `1d`。
|
||||
|
||||
#### download_detour
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
`download_detour` 已在 sing-box 1.14.0 废弃且将在 sing-box 1.16.0 中被移除,请使用 `http_client` 代替。
|
||||
|
||||
用于下载规则集的出站的标签。
|
||||
|
||||
@@ -58,9 +58,9 @@ Object format:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://my-headscale.com/verify",
|
||||
|
||||
... // Dial Fields
|
||||
"url": "",
|
||||
|
||||
... // HTTP Client Fields
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -58,9 +58,9 @@ Derper 配置文件路径。
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://my-headscale.com/verify",
|
||||
"url": "",
|
||||
|
||||
... // 拨号字段
|
||||
... // HTTP 客户端字段
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ icon: material/new-box
|
||||
|
||||
:material-plus: [account_key](#account_key)
|
||||
:material-plus: [key_type](#key_type)
|
||||
:material-plus: [detour](#detour)
|
||||
:material-plus: [http_client](#http_client)
|
||||
|
||||
# ACME
|
||||
|
||||
@@ -37,7 +37,7 @@ icon: material/new-box
|
||||
},
|
||||
"dns01_challenge": {},
|
||||
"key_type": "",
|
||||
"detour": ""
|
||||
"http_client": "" // or {}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -141,10 +141,10 @@ The private key type to generate for new certificates.
|
||||
| `rsa2048` | RSA |
|
||||
| `rsa4096` | RSA |
|
||||
|
||||
#### detour
|
||||
#### http_client
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
The tag of the upstream outbound.
|
||||
HTTP Client for all provider HTTP requests.
|
||||
|
||||
All provider HTTP requests will use this outbound.
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
@@ -6,7 +6,7 @@ icon: material/new-box
|
||||
|
||||
:material-plus: [account_key](#account_key)
|
||||
:material-plus: [key_type](#key_type)
|
||||
:material-plus: [detour](#detour)
|
||||
:material-plus: [http_client](#http_client)
|
||||
|
||||
# ACME
|
||||
|
||||
@@ -37,7 +37,7 @@ icon: material/new-box
|
||||
},
|
||||
"dns01_challenge": {},
|
||||
"key_type": "",
|
||||
"detour": ""
|
||||
"http_client": "" // 或 {}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -136,10 +136,12 @@ ACME DNS01 质询字段。如果配置,将禁用其他质询方法。
|
||||
| `rsa2048` | RSA |
|
||||
| `rsa4096` | RSA |
|
||||
|
||||
#### detour
|
||||
#### http_client
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
上游出站的标签。
|
||||
用于所有提供者 HTTP 请求的 HTTP 客户端。
|
||||
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
所有提供者 HTTP 请求将使用此出站。
|
||||
|
||||
@@ -19,7 +19,7 @@ icon: material/new-box
|
||||
"origin_ca_key": "",
|
||||
"request_type": "",
|
||||
"requested_validity": 0,
|
||||
"detour": ""
|
||||
"http_client": "" // or {}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,8 +75,8 @@ Available values: `7`, `30`, `90`, `365`, `730`, `1095`, `5475`.
|
||||
|
||||
`5475` days (15 years) is used if empty.
|
||||
|
||||
#### detour
|
||||
#### http_client
|
||||
|
||||
The tag of the upstream outbound.
|
||||
HTTP Client for all provider HTTP requests.
|
||||
|
||||
All provider HTTP requests will use this outbound.
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
@@ -19,7 +19,7 @@ icon: material/new-box
|
||||
"origin_ca_key": "",
|
||||
"request_type": "",
|
||||
"requested_validity": 0,
|
||||
"detour": ""
|
||||
"http_client": "" // 或 {}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,8 +75,8 @@ Cloudflare Origin CA Key。
|
||||
|
||||
如果为空,使用 `5475` 天(15 年)。
|
||||
|
||||
#### detour
|
||||
#### http_client
|
||||
|
||||
上游出站的标签。
|
||||
用于所有提供者 HTTP 请求的 HTTP 客户端。
|
||||
|
||||
所有提供者 HTTP 请求将使用此出站。
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
69
docs/configuration/shared/http-client.md
Normal file
69
docs/configuration/shared/http-client.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
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
|
||||
{
|
||||
"version": 0,
|
||||
"disable_version_fallback": false,
|
||||
"headers": {},
|
||||
|
||||
... // HTTP2 Fields
|
||||
|
||||
"tls": {},
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### 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.
|
||||
69
docs/configuration/shared/http-client.zh.md
Normal file
69
docs/configuration/shared/http-client.zh.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
### 结构
|
||||
|
||||
字符串或对象。
|
||||
|
||||
当为字符串时,为顶层 `http_clients` 中定义的共享 [HTTP 客户端](/zh/configuration/shared/http-client/) 的标签。
|
||||
|
||||
当为对象时:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 0,
|
||||
"disable_version_fallback": false,
|
||||
"headers": {},
|
||||
|
||||
... // HTTP2 字段
|
||||
|
||||
"tls": {},
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### version
|
||||
|
||||
HTTP 版本。
|
||||
|
||||
可用值:`1`、`2`、`3`。
|
||||
|
||||
默认使用 `2`。
|
||||
|
||||
当为 `3` 时,[HTTP2 字段](#http2-字段) 替换为 [QUIC 字段](#quic-字段)。
|
||||
|
||||
#### disable_version_fallback
|
||||
|
||||
禁用自动回退到更低的 HTTP 版本。
|
||||
|
||||
#### headers
|
||||
|
||||
自定义 HTTP 标头。
|
||||
|
||||
`Host` 标头用作请求主机。
|
||||
|
||||
### HTTP2 字段
|
||||
|
||||
当 `version` 为 `2`(默认)时。
|
||||
|
||||
参阅 [HTTP2 字段](/zh/configuration/shared/http2/) 了解详情。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
当 `version` 为 `3` 时。
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
### TLS 字段
|
||||
|
||||
参阅 [TLS](/zh/configuration/shared/tls/#出站) 了解详情。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||
43
docs/configuration/shared/http2.md
Normal file
43
docs/configuration/shared/http2.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"idle_timeout": "",
|
||||
"keep_alive_period": "",
|
||||
"stream_receive_window": "",
|
||||
"connection_receive_window": "",
|
||||
"max_concurrent_streams": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### idle_timeout
|
||||
|
||||
Idle connection timeout, in golang's Duration format.
|
||||
|
||||
#### keep_alive_period
|
||||
|
||||
Keep alive period, in golang's Duration format.
|
||||
|
||||
#### stream_receive_window
|
||||
|
||||
HTTP2 stream-level flow-control receive window size.
|
||||
|
||||
Accepts memory size format, e.g. `"64 MB"`.
|
||||
|
||||
#### connection_receive_window
|
||||
|
||||
HTTP2 connection-level flow-control receive window size.
|
||||
|
||||
Accepts memory size format, e.g. `"64 MB"`.
|
||||
|
||||
#### max_concurrent_streams
|
||||
|
||||
Maximum concurrent streams per connection.
|
||||
43
docs/configuration/shared/http2.zh.md
Normal file
43
docs/configuration/shared/http2.zh.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"idle_timeout": "",
|
||||
"keep_alive_period": "",
|
||||
"stream_receive_window": "",
|
||||
"connection_receive_window": "",
|
||||
"max_concurrent_streams": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### idle_timeout
|
||||
|
||||
空闲连接超时,采用 golang 的 Duration 格式。
|
||||
|
||||
#### keep_alive_period
|
||||
|
||||
Keep alive 周期,采用 golang 的 Duration 格式。
|
||||
|
||||
#### stream_receive_window
|
||||
|
||||
HTTP2 流级别流控接收窗口大小。
|
||||
|
||||
接受内存大小格式,例如 `"64 MB"`。
|
||||
|
||||
#### connection_receive_window
|
||||
|
||||
HTTP2 连接级别流控接收窗口大小。
|
||||
|
||||
接受内存大小格式,例如 `"64 MB"`。
|
||||
|
||||
#### max_concurrent_streams
|
||||
|
||||
每个连接的最大并发流数。
|
||||
30
docs/configuration/shared/quic.md
Normal file
30
docs/configuration/shared/quic.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"initial_packet_size": 0,
|
||||
"disable_path_mtu_discovery": false,
|
||||
|
||||
... // HTTP2 Fields
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### initial_packet_size
|
||||
|
||||
Initial QUIC packet size.
|
||||
|
||||
#### disable_path_mtu_discovery
|
||||
|
||||
Disable QUIC path MTU discovery.
|
||||
|
||||
### HTTP2 Fields
|
||||
|
||||
See [HTTP2 Fields](/configuration/shared/http2/) for details.
|
||||
30
docs/configuration/shared/quic.zh.md
Normal file
30
docs/configuration/shared/quic.zh.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"initial_packet_size": 0,
|
||||
"disable_path_mtu_discovery": false,
|
||||
|
||||
... // HTTP2 字段
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### initial_packet_size
|
||||
|
||||
初始 QUIC 数据包大小。
|
||||
|
||||
#### disable_path_mtu_discovery
|
||||
|
||||
禁用 QUIC 路径 MTU 发现。
|
||||
|
||||
### HTTP2 字段
|
||||
|
||||
参阅 [HTTP2 字段](/zh/configuration/shared/http2/) 了解详情。
|
||||
@@ -5,6 +5,7 @@ icon: material/new-box
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [certificate_provider](#certificate_provider)
|
||||
:material-plus: [handshake_timeout](#handshake_timeout)
|
||||
:material-delete-clock: [acme](#acme-fields)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
@@ -54,6 +55,7 @@ icon: material/new-box
|
||||
"key_path": "",
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false,
|
||||
"handshake_timeout": "",
|
||||
"certificate_provider": "",
|
||||
|
||||
// Deprecated
|
||||
@@ -106,6 +108,7 @@ icon: material/new-box
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"engine": "",
|
||||
"disable_sni": false,
|
||||
"server_name": "",
|
||||
"insecure": false,
|
||||
@@ -124,6 +127,9 @@ icon: material/new-box
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "",
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false,
|
||||
"handshake_timeout": "",
|
||||
"ech": {
|
||||
"enabled": false,
|
||||
"config": [],
|
||||
@@ -183,6 +189,49 @@ Cipher suite values:
|
||||
|
||||
Enable TLS.
|
||||
|
||||
#### engine
|
||||
|
||||
==Client only==
|
||||
|
||||
TLS engine to use.
|
||||
|
||||
Values:
|
||||
|
||||
* `go`
|
||||
* `apple`
|
||||
|
||||
`apple` uses Network.framework, only available on Apple platforms and only supports **direct** TCP TLS client connections.
|
||||
|
||||
!!! warning ""
|
||||
|
||||
Experimental only: due to the high memory overhead of both CGO and Network.framework,
|
||||
do not use in proxy paths on iOS and tvOS.
|
||||
If you want to circumvent TLS fingerprint-based proxy censorship,
|
||||
use [NaiveProxy](/configuration/outbound/naive/) instead.
|
||||
|
||||
Supported fields:
|
||||
|
||||
* `server_name`
|
||||
* `insecure`
|
||||
* `alpn`
|
||||
* `min_version`
|
||||
* `max_version`
|
||||
* `certificate` / `certificate_path`
|
||||
* `certificate_public_key_sha256`
|
||||
* `handshake_timeout`
|
||||
|
||||
Unsupported fields:
|
||||
|
||||
* `disable_sni`
|
||||
* `cipher_suites`
|
||||
* `curve_preferences`
|
||||
* `client_certificate` / `client_certificate_path` / `client_key` / `client_key_path`
|
||||
* `fragment` / `record_fragment`
|
||||
* `kernel_tx` / `kernel_rx`
|
||||
* `ech`
|
||||
* `utls`
|
||||
* `reality`
|
||||
|
||||
#### disable_sni
|
||||
|
||||
==Client only==
|
||||
@@ -417,6 +466,14 @@ Enable kernel TLS transmit support.
|
||||
|
||||
Enable kernel TLS receive support.
|
||||
|
||||
#### handshake_timeout
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
TLS handshake timeout, in golang's Duration format.
|
||||
|
||||
`15s` is used by default.
|
||||
|
||||
#### certificate_provider
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
@@ -5,6 +5,7 @@ icon: material/new-box
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [certificate_provider](#certificate_provider)
|
||||
:material-plus: [handshake_timeout](#handshake_timeout)
|
||||
:material-delete-clock: [acme](#acme-字段)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
@@ -54,6 +55,7 @@ icon: material/new-box
|
||||
"key_path": "",
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false,
|
||||
"handshake_timeout": "",
|
||||
"certificate_provider": "",
|
||||
|
||||
// 废弃的
|
||||
@@ -106,6 +108,7 @@ icon: material/new-box
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"engine": "",
|
||||
"disable_sni": false,
|
||||
"server_name": "",
|
||||
"insecure": false,
|
||||
@@ -124,6 +127,9 @@ icon: material/new-box
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "",
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false,
|
||||
"handshake_timeout": "",
|
||||
"ech": {
|
||||
"enabled": false,
|
||||
"config": [],
|
||||
@@ -183,6 +189,48 @@ TLS 版本值:
|
||||
|
||||
启用 TLS
|
||||
|
||||
#### engine
|
||||
|
||||
==仅客户端==
|
||||
|
||||
要使用的 TLS 引擎。
|
||||
|
||||
可用值:
|
||||
|
||||
* `go`
|
||||
* `apple`
|
||||
|
||||
`apple` 使用 Network.framework,仅在 Apple 平台可用,且仅支持 **直接** TCP TLS 客户端连接。
|
||||
|
||||
!!! warning ""
|
||||
|
||||
仅供实验用途:由于 CGO 和 Network.framework 占用的内存都很多,
|
||||
不应在 iOS 和 tvOS 的代理路径中使用。
|
||||
如果您想规避基于 TLS 指纹的代理审查,应使用 [NaiveProxy](/zh/configuration/outbound/naive/)。
|
||||
|
||||
支持的字段:
|
||||
|
||||
* `server_name`
|
||||
* `insecure`
|
||||
* `alpn`
|
||||
* `min_version`
|
||||
* `max_version`
|
||||
* `certificate` / `certificate_path`
|
||||
* `certificate_public_key_sha256`
|
||||
* `handshake_timeout`
|
||||
|
||||
不支持的字段:
|
||||
|
||||
* `disable_sni`
|
||||
* `cipher_suites`
|
||||
* `curve_preferences`
|
||||
* `client_certificate` / `client_certificate_path` / `client_key` / `client_key_path`
|
||||
* `fragment` / `record_fragment`
|
||||
* `kernel_tx` / `kernel_rx`
|
||||
* `ech`
|
||||
* `utls`
|
||||
* `reality`
|
||||
|
||||
#### disable_sni
|
||||
|
||||
==仅客户端==
|
||||
@@ -416,6 +464,14 @@ echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/
|
||||
|
||||
启用内核 TLS 接收支持。
|
||||
|
||||
#### handshake_timeout
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
TLS 握手超时,采用 golang 的 Duration 格式。
|
||||
|
||||
默认使用 `15s`。
|
||||
|
||||
#### certificate_provider
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
@@ -6,6 +6,27 @@ icon: material/delete-alert
|
||||
|
||||
## 1.14.0
|
||||
|
||||
#### Legacy `download_detour` remote rule-set option
|
||||
|
||||
Legacy `download_detour` remote rule-set option is deprecated,
|
||||
use `http_client` instead.
|
||||
|
||||
Old field will be removed in sing-box 1.16.0.
|
||||
|
||||
#### Implicit default HTTP client
|
||||
|
||||
Implicit default HTTP client using the default outbound for remote rule-sets is deprecated.
|
||||
Configure `http_clients` and `route.default_http_client` explicitly.
|
||||
|
||||
Old behavior will be removed in sing-box 1.16.0.
|
||||
|
||||
#### Legacy dialer options in Tailscale endpoint
|
||||
|
||||
Legacy dialer options in Tailscale endpoints are deprecated,
|
||||
use `control_http_client` instead.
|
||||
|
||||
Old fields will be removed in sing-box 1.16.0.
|
||||
|
||||
#### Inline ACME options in TLS
|
||||
|
||||
Inline ACME options (`tls.acme`) are deprecated
|
||||
@@ -27,6 +48,21 @@ check [Migration](../migration/#migrate-address-filter-fields-to-response-matchi
|
||||
|
||||
Old fields will be removed in sing-box 1.16.0.
|
||||
|
||||
#### `independent_cache` DNS option
|
||||
|
||||
`independent_cache` DNS option is deprecated.
|
||||
The DNS cache now always keys by transport, making this option unnecessary,
|
||||
check [Migration](../migration/#migrate-independent-dns-cache).
|
||||
|
||||
Old fields will be removed in sing-box 1.16.0.
|
||||
|
||||
#### `store_rdrc` cache file option
|
||||
|
||||
`store_rdrc` cache file option is deprecated,
|
||||
check [Migration](../migration/#migrate-store-rdrc).
|
||||
|
||||
Old fields will be removed in sing-box 1.16.0.
|
||||
|
||||
#### Legacy Address Filter Fields in DNS rules
|
||||
|
||||
Legacy Address Filter Fields (`ip_cidr`, `ip_is_private` without `match_response`)
|
||||
|
||||
@@ -6,6 +6,27 @@ icon: material/delete-alert
|
||||
|
||||
## 1.14.0
|
||||
|
||||
#### 旧版远程规则集 `download_detour` 选项
|
||||
|
||||
旧版远程规则集 `download_detour` 选项已废弃,
|
||||
请使用 `http_client` 代替。
|
||||
|
||||
旧字段将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
#### 隐式默认 HTTP 客户端
|
||||
|
||||
使用默认出站为远程规则集隐式创建默认 HTTP 客户端的行为已废弃。
|
||||
请显式配置 `http_clients` 和 `route.default_http_client`。
|
||||
|
||||
旧行为将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
#### Tailscale 端点中的旧版拨号选项
|
||||
|
||||
Tailscale 端点中的旧版拨号选项已废弃,
|
||||
请使用 `control_http_client` 代替。
|
||||
|
||||
旧字段将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
#### TLS 中的内联 ACME 选项
|
||||
|
||||
TLS 中的内联 ACME 选项(`tls.acme`)已废弃,
|
||||
@@ -27,6 +48,21 @@ TLS 中的内联 ACME 选项(`tls.acme`)已废弃,
|
||||
|
||||
旧字段将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
#### `independent_cache` DNS 选项
|
||||
|
||||
`independent_cache` DNS 选项已废弃。
|
||||
DNS 缓存现在始终按传输分离,使此选项不再需要,
|
||||
参阅[迁移指南](/zh/migration/#迁移-independent-dns-cache)。
|
||||
|
||||
旧字段将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
#### `store_rdrc` 缓存文件选项
|
||||
|
||||
`store_rdrc` 缓存文件选项已废弃,
|
||||
参阅[迁移指南](/zh/migration/#迁移-store_rdrc)。
|
||||
|
||||
旧字段将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
#### 旧版地址筛选字段 (DNS 规则)
|
||||
|
||||
旧版地址筛选字段(不使用 `match_response` 的 `ip_cidr`、`ip_is_private`)已废弃,
|
||||
|
||||
@@ -137,6 +137,68 @@ to fetch a DNS response, then match against it explicitly with `match_response`.
|
||||
}
|
||||
```
|
||||
|
||||
### Migrate independent DNS cache
|
||||
|
||||
The DNS cache now always keys by transport name, making `independent_cache` unnecessary.
|
||||
Simply remove the field.
|
||||
|
||||
!!! info "References"
|
||||
|
||||
[DNS](/configuration/dns/)
|
||||
|
||||
=== ":material-card-remove: Deprecated"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"independent_cache": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-multiple: New"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Migrate store_rdrc
|
||||
|
||||
`store_rdrc` is deprecated and can be replaced by `store_dns`,
|
||||
which persists the full DNS cache to the cache file.
|
||||
|
||||
!!! info "References"
|
||||
|
||||
[Cache File](/configuration/experimental/cache-file/)
|
||||
|
||||
=== ":material-card-remove: Deprecated"
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"cache_file": {
|
||||
"enabled": true,
|
||||
"store_rdrc": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-multiple: New"
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"cache_file": {
|
||||
"enabled": true,
|
||||
"store_dns": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ip_version and query_type behavior changes in DNS rules
|
||||
|
||||
In sing-box 1.14.0, the behavior of
|
||||
|
||||
@@ -137,6 +137,68 @@ sing-box 1.14.0 新增字段参阅 [ACME](/zh/configuration/shared/certificate-p
|
||||
}
|
||||
```
|
||||
|
||||
### 迁移 independent DNS cache
|
||||
|
||||
DNS 缓存现在始终按传输名称分离,使 `independent_cache` 不再需要。
|
||||
直接移除该字段即可。
|
||||
|
||||
!!! info "参考"
|
||||
|
||||
[DNS](/zh/configuration/dns/)
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"independent_cache": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {}
|
||||
}
|
||||
```
|
||||
|
||||
### 迁移 store_rdrc
|
||||
|
||||
`store_rdrc` 已废弃,且可以被 `store_dns` 替代,
|
||||
后者将完整的 DNS 缓存持久化到缓存文件中。
|
||||
|
||||
!!! info "参考"
|
||||
|
||||
[缓存文件](/zh/configuration/experimental/cache-file/)
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"cache_file": {
|
||||
"enabled": true,
|
||||
"store_rdrc": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"cache_file": {
|
||||
"enabled": true,
|
||||
"store_dns": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### DNS 规则中的 ip_version 和 query_type 行为更改
|
||||
|
||||
在 sing-box 1.14.0 中,DNS 规则中的
|
||||
|
||||
@@ -12,9 +12,11 @@ import (
|
||||
"github.com/sagernet/bbolt"
|
||||
bboltErrors "github.com/sagernet/bbolt/errors"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
)
|
||||
|
||||
@@ -30,6 +32,7 @@ var (
|
||||
string(bucketMode),
|
||||
string(bucketRuleSet),
|
||||
string(bucketRDRC),
|
||||
string(bucketDNSCache),
|
||||
}
|
||||
|
||||
cacheIDDefault = []byte("default")
|
||||
@@ -38,30 +41,43 @@ var (
|
||||
var _ adapter.CacheFile = (*CacheFile)(nil)
|
||||
|
||||
type CacheFile struct {
|
||||
ctx context.Context
|
||||
path string
|
||||
cacheID []byte
|
||||
storeFakeIP bool
|
||||
storeRDRC bool
|
||||
rdrcTimeout time.Duration
|
||||
DB *bbolt.DB
|
||||
resetAccess sync.Mutex
|
||||
saveMetadataTimer *time.Timer
|
||||
saveFakeIPAccess sync.RWMutex
|
||||
saveDomain map[netip.Addr]string
|
||||
saveAddress4 map[string]netip.Addr
|
||||
saveAddress6 map[string]netip.Addr
|
||||
saveRDRCAccess sync.RWMutex
|
||||
saveRDRC map[saveRDRCCacheKey]bool
|
||||
ctx context.Context
|
||||
logger logger.Logger
|
||||
path string
|
||||
cacheID []byte
|
||||
storeFakeIP bool
|
||||
storeRDRC bool
|
||||
storeDNS bool
|
||||
disableExpire bool
|
||||
rdrcTimeout time.Duration
|
||||
optimisticTimeout time.Duration
|
||||
DB *bbolt.DB
|
||||
resetAccess sync.Mutex
|
||||
saveMetadataTimer *time.Timer
|
||||
saveFakeIPAccess sync.RWMutex
|
||||
saveDomain map[netip.Addr]string
|
||||
saveAddress4 map[string]netip.Addr
|
||||
saveAddress6 map[string]netip.Addr
|
||||
saveRDRCAccess sync.RWMutex
|
||||
saveRDRC map[saveCacheKey]bool
|
||||
saveDNSCacheAccess sync.RWMutex
|
||||
saveDNSCache map[saveCacheKey]saveDNSCacheEntry
|
||||
}
|
||||
|
||||
type saveRDRCCacheKey struct {
|
||||
type saveCacheKey struct {
|
||||
TransportName string
|
||||
QuestionName string
|
||||
QType uint16
|
||||
}
|
||||
|
||||
func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
|
||||
type saveDNSCacheEntry struct {
|
||||
rawMessage []byte
|
||||
expireAt time.Time
|
||||
sequence uint64
|
||||
saving bool
|
||||
}
|
||||
|
||||
func New(ctx context.Context, logger logger.Logger, options option.CacheFileOptions) *CacheFile {
|
||||
var path string
|
||||
if options.Path != "" {
|
||||
path = options.Path
|
||||
@@ -72,6 +88,9 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
|
||||
if options.CacheID != "" {
|
||||
cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...)
|
||||
}
|
||||
if options.StoreRDRC {
|
||||
deprecated.Report(ctx, deprecated.OptionStoreRDRC)
|
||||
}
|
||||
var rdrcTimeout time.Duration
|
||||
if options.StoreRDRC {
|
||||
if options.RDRCTimeout > 0 {
|
||||
@@ -82,15 +101,18 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
|
||||
}
|
||||
return &CacheFile{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
path: filemanager.BasePath(ctx, path),
|
||||
cacheID: cacheIDBytes,
|
||||
storeFakeIP: options.StoreFakeIP,
|
||||
storeRDRC: options.StoreRDRC,
|
||||
storeDNS: options.StoreDNS,
|
||||
rdrcTimeout: rdrcTimeout,
|
||||
saveDomain: make(map[netip.Addr]string),
|
||||
saveAddress4: make(map[string]netip.Addr),
|
||||
saveAddress6: make(map[string]netip.Addr),
|
||||
saveRDRC: make(map[saveRDRCCacheKey]bool),
|
||||
saveRDRC: make(map[saveCacheKey]bool),
|
||||
saveDNSCache: make(map[saveCacheKey]saveDNSCacheEntry),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,10 +124,44 @@ func (c *CacheFile) Dependencies() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CacheFile) SetOptimisticTimeout(timeout time.Duration) {
|
||||
c.optimisticTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *CacheFile) SetDisableExpire(disableExpire bool) {
|
||||
c.disableExpire = disableExpire
|
||||
}
|
||||
|
||||
func (c *CacheFile) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateInitialize {
|
||||
return nil
|
||||
switch stage {
|
||||
case adapter.StartStateInitialize:
|
||||
return c.start()
|
||||
case adapter.StartStateStart:
|
||||
c.startCacheCleanup()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CacheFile) startCacheCleanup() {
|
||||
if c.storeDNS {
|
||||
c.clearRDRC()
|
||||
c.cleanupDNSCache()
|
||||
interval := c.optimisticTimeout / 2
|
||||
if interval <= 0 {
|
||||
interval = time.Hour
|
||||
}
|
||||
go c.loopCacheCleanup(interval, c.cleanupDNSCache)
|
||||
} else if c.storeRDRC {
|
||||
c.cleanupRDRC()
|
||||
interval := c.rdrcTimeout / 2
|
||||
if interval <= 0 {
|
||||
interval = time.Hour
|
||||
}
|
||||
go c.loopCacheCleanup(interval, c.cleanupRDRC)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CacheFile) start() error {
|
||||
const fileMode = 0o666
|
||||
options := bbolt.Options{Timeout: time.Second}
|
||||
var (
|
||||
|
||||
299
experimental/cachefile/dns_cache.go
Normal file
299
experimental/cachefile/dns_cache.go
Normal file
@@ -0,0 +1,299 @@
|
||||
package cachefile
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/bbolt"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
var bucketDNSCache = []byte("dns_cache")
|
||||
|
||||
func (c *CacheFile) StoreDNS() bool {
|
||||
return c.storeDNS
|
||||
}
|
||||
|
||||
func (c *CacheFile) LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool) {
|
||||
c.saveDNSCacheAccess.RLock()
|
||||
entry, cached := c.saveDNSCache[saveCacheKey{transportName, qName, qType}]
|
||||
c.saveDNSCacheAccess.RUnlock()
|
||||
if cached {
|
||||
return entry.rawMessage, entry.expireAt, true
|
||||
}
|
||||
key := buf.Get(2 + len(qName))
|
||||
binary.BigEndian.PutUint16(key, qType)
|
||||
copy(key[2:], qName)
|
||||
defer buf.Put(key)
|
||||
err := c.view(func(tx *bbolt.Tx) error {
|
||||
bucket := c.bucket(tx, bucketDNSCache)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
bucket = bucket.Bucket([]byte(transportName))
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
content := bucket.Get(key)
|
||||
if len(content) < 8 {
|
||||
return nil
|
||||
}
|
||||
expireAt = time.Unix(int64(binary.BigEndian.Uint64(content[:8])), 0)
|
||||
rawMessage = make([]byte, len(content)-8)
|
||||
copy(rawMessage, content[8:])
|
||||
loaded = true
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, time.Time{}, false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *CacheFile) SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error {
|
||||
return c.batch(func(tx *bbolt.Tx) error {
|
||||
bucket, err := c.createBucket(tx, bucketDNSCache)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bucket, err = bucket.CreateBucketIfNotExists([]byte(transportName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := buf.Get(2 + len(qName))
|
||||
binary.BigEndian.PutUint16(key, qType)
|
||||
copy(key[2:], qName)
|
||||
defer buf.Put(key)
|
||||
value := buf.Get(8 + len(rawMessage))
|
||||
defer buf.Put(value)
|
||||
binary.BigEndian.PutUint64(value[:8], uint64(expireAt.Unix()))
|
||||
copy(value[8:], rawMessage)
|
||||
return bucket.Put(key, value)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CacheFile) SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger) {
|
||||
saveKey := saveCacheKey{transportName, qName, qType}
|
||||
if !c.queueDNSCacheSave(saveKey, rawMessage, expireAt) {
|
||||
return
|
||||
}
|
||||
go c.flushPendingDNSCache(saveKey, logger)
|
||||
}
|
||||
|
||||
func (c *CacheFile) queueDNSCacheSave(saveKey saveCacheKey, rawMessage []byte, expireAt time.Time) bool {
|
||||
c.saveDNSCacheAccess.Lock()
|
||||
defer c.saveDNSCacheAccess.Unlock()
|
||||
entry := c.saveDNSCache[saveKey]
|
||||
entry.rawMessage = append([]byte(nil), rawMessage...)
|
||||
entry.expireAt = expireAt
|
||||
entry.sequence++
|
||||
startFlush := !entry.saving
|
||||
entry.saving = true
|
||||
c.saveDNSCache[saveKey] = entry
|
||||
return startFlush
|
||||
}
|
||||
|
||||
func (c *CacheFile) flushPendingDNSCache(saveKey saveCacheKey, logger logger.Logger) {
|
||||
c.flushPendingDNSCacheWith(saveKey, logger, func(entry saveDNSCacheEntry) error {
|
||||
return c.SaveDNSCache(saveKey.TransportName, saveKey.QuestionName, saveKey.QType, entry.rawMessage, entry.expireAt)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CacheFile) flushPendingDNSCacheWith(saveKey saveCacheKey, logger logger.Logger, save func(saveDNSCacheEntry) error) {
|
||||
for {
|
||||
c.saveDNSCacheAccess.RLock()
|
||||
entry, loaded := c.saveDNSCache[saveKey]
|
||||
c.saveDNSCacheAccess.RUnlock()
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
err := save(entry)
|
||||
if err != nil {
|
||||
logger.Warn("save DNS cache: ", err)
|
||||
}
|
||||
c.saveDNSCacheAccess.Lock()
|
||||
currentEntry, loaded := c.saveDNSCache[saveKey]
|
||||
if !loaded {
|
||||
c.saveDNSCacheAccess.Unlock()
|
||||
return
|
||||
}
|
||||
if currentEntry.sequence != entry.sequence {
|
||||
c.saveDNSCacheAccess.Unlock()
|
||||
continue
|
||||
}
|
||||
delete(c.saveDNSCache, saveKey)
|
||||
c.saveDNSCacheAccess.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CacheFile) ClearDNSCache() error {
|
||||
c.saveDNSCacheAccess.Lock()
|
||||
clear(c.saveDNSCache)
|
||||
c.saveDNSCacheAccess.Unlock()
|
||||
return c.batch(func(tx *bbolt.Tx) error {
|
||||
if c.cacheID == nil {
|
||||
bucket := tx.Bucket(bucketDNSCache)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
return tx.DeleteBucket(bucketDNSCache)
|
||||
}
|
||||
bucket := tx.Bucket(c.cacheID)
|
||||
if bucket == nil || bucket.Bucket(bucketDNSCache) == nil {
|
||||
return nil
|
||||
}
|
||||
return bucket.DeleteBucket(bucketDNSCache)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CacheFile) loopCacheCleanup(interval time.Duration, cleanupFunc func()) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
cleanupFunc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CacheFile) cleanupDNSCache() {
|
||||
now := time.Now()
|
||||
err := c.batch(func(tx *bbolt.Tx) error {
|
||||
bucket := c.bucket(tx, bucketDNSCache)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
var emptyTransports [][]byte
|
||||
err := bucket.ForEachBucket(func(transportName []byte) error {
|
||||
transportBucket := bucket.Bucket(transportName)
|
||||
if transportBucket == nil {
|
||||
return nil
|
||||
}
|
||||
var expiredKeys [][]byte
|
||||
err := transportBucket.ForEach(func(key, value []byte) error {
|
||||
if len(value) < 8 {
|
||||
expiredKeys = append(expiredKeys, append([]byte(nil), key...))
|
||||
return nil
|
||||
}
|
||||
if c.disableExpire {
|
||||
return nil
|
||||
}
|
||||
expireAt := time.Unix(int64(binary.BigEndian.Uint64(value[:8])), 0)
|
||||
if now.After(expireAt.Add(c.optimisticTimeout)) {
|
||||
expiredKeys = append(expiredKeys, append([]byte(nil), key...))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, key := range expiredKeys {
|
||||
err = transportBucket.Delete(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
first, _ := transportBucket.Cursor().First()
|
||||
if first == nil {
|
||||
emptyTransports = append(emptyTransports, append([]byte(nil), transportName...))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, name := range emptyTransports {
|
||||
err = bucket.DeleteBucket(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Warn("cleanup DNS cache: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CacheFile) clearRDRC() {
|
||||
c.saveRDRCAccess.Lock()
|
||||
clear(c.saveRDRC)
|
||||
c.saveRDRCAccess.Unlock()
|
||||
err := c.batch(func(tx *bbolt.Tx) error {
|
||||
if c.cacheID == nil {
|
||||
if tx.Bucket(bucketRDRC) == nil {
|
||||
return nil
|
||||
}
|
||||
return tx.DeleteBucket(bucketRDRC)
|
||||
}
|
||||
bucket := tx.Bucket(c.cacheID)
|
||||
if bucket == nil || bucket.Bucket(bucketRDRC) == nil {
|
||||
return nil
|
||||
}
|
||||
return bucket.DeleteBucket(bucketRDRC)
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Warn("clear RDRC: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CacheFile) cleanupRDRC() {
|
||||
now := time.Now()
|
||||
err := c.batch(func(tx *bbolt.Tx) error {
|
||||
bucket := c.bucket(tx, bucketRDRC)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
var emptyTransports [][]byte
|
||||
err := bucket.ForEachBucket(func(transportName []byte) error {
|
||||
transportBucket := bucket.Bucket(transportName)
|
||||
if transportBucket == nil {
|
||||
return nil
|
||||
}
|
||||
var expiredKeys [][]byte
|
||||
err := transportBucket.ForEach(func(key, value []byte) error {
|
||||
if len(value) < 8 {
|
||||
expiredKeys = append(expiredKeys, append([]byte(nil), key...))
|
||||
return nil
|
||||
}
|
||||
expiresAt := time.Unix(int64(binary.BigEndian.Uint64(value)), 0)
|
||||
if now.After(expiresAt) {
|
||||
expiredKeys = append(expiredKeys, append([]byte(nil), key...))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, key := range expiredKeys {
|
||||
err = transportBucket.Delete(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
first, _ := transportBucket.Cursor().First()
|
||||
if first == nil {
|
||||
emptyTransports = append(emptyTransports, append([]byte(nil), transportName...))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, name := range emptyTransports {
|
||||
err = bucket.DeleteBucket(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Warn("cleanup RDRC: ", err)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ func (c *CacheFile) RDRCTimeout() time.Duration {
|
||||
|
||||
func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (rejected bool) {
|
||||
c.saveRDRCAccess.RLock()
|
||||
rejected, cached := c.saveRDRC[saveRDRCCacheKey{transportName, qName, qType}]
|
||||
rejected, cached := c.saveRDRC[saveCacheKey{transportName, qName, qType}]
|
||||
c.saveRDRCAccess.RUnlock()
|
||||
if cached {
|
||||
return
|
||||
@@ -93,7 +93,7 @@ func (c *CacheFile) SaveRDRC(transportName string, qName string, qType uint16) e
|
||||
}
|
||||
|
||||
func (c *CacheFile) SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger) {
|
||||
saveKey := saveRDRCCacheKey{transportName, qName, qType}
|
||||
saveKey := saveCacheKey{transportName, qName, qType}
|
||||
c.saveRDRCAccess.Lock()
|
||||
c.saveRDRC[saveKey] = true
|
||||
c.saveRDRCAccess.Unlock()
|
||||
|
||||
@@ -93,6 +93,22 @@ var OptionInlineACME = Note{
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-inline-acme-to-certificate-provider",
|
||||
}
|
||||
|
||||
var OptionLegacyRuleSetDownloadDetour = Note{
|
||||
Name: "legacy-rule-set-download-detour",
|
||||
Description: "legacy `download_detour` remote rule-set option",
|
||||
DeprecatedVersion: "1.14.0",
|
||||
ScheduledVersion: "1.16.0",
|
||||
EnvName: "LEGACY_RULE_SET_DOWNLOAD_DETOUR",
|
||||
}
|
||||
|
||||
var OptionLegacyTailscaleEndpointDialer = Note{
|
||||
Name: "legacy-tailscale-endpoint-dialer",
|
||||
Description: "legacy dialer options in Tailscale endpoint",
|
||||
DeprecatedVersion: "1.14.0",
|
||||
ScheduledVersion: "1.16.0",
|
||||
EnvName: "LEGACY_TAILSCALE_ENDPOINT_DIALER",
|
||||
}
|
||||
|
||||
var OptionRuleSetIPCIDRAcceptEmpty = Note{
|
||||
Name: "dns-rule-rule-set-ip-cidr-accept-empty",
|
||||
Description: "Legacy `rule_set_ip_cidr_accept_empty` DNS rule item",
|
||||
@@ -120,12 +136,43 @@ var OptionLegacyDNSRuleStrategy = Note{
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-dns-rule-action-strategy-to-rule-items",
|
||||
}
|
||||
|
||||
var OptionIndependentDNSCache = Note{
|
||||
Name: "independent-dns-cache",
|
||||
Description: "`independent_cache` DNS option",
|
||||
DeprecatedVersion: "1.14.0",
|
||||
ScheduledVersion: "1.16.0",
|
||||
EnvName: "INDEPENDENT_DNS_CACHE",
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-independent-dns-cache",
|
||||
}
|
||||
|
||||
var OptionStoreRDRC = Note{
|
||||
Name: "store-rdrc",
|
||||
Description: "`store_rdrc` cache file option",
|
||||
DeprecatedVersion: "1.14.0",
|
||||
ScheduledVersion: "1.16.0",
|
||||
EnvName: "STORE_RDRC",
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-store-rdrc",
|
||||
}
|
||||
|
||||
var OptionImplicitDefaultHTTPClient = Note{
|
||||
Name: "implicit-default-http-client",
|
||||
Description: "implicit default HTTP client using default outbound for remote rule-sets",
|
||||
DeprecatedVersion: "1.14.0",
|
||||
ScheduledVersion: "1.16.0",
|
||||
EnvName: "IMPLICIT_DEFAULT_HTTP_CLIENT",
|
||||
}
|
||||
|
||||
var Options = []Note{
|
||||
OptionOutboundDNSRuleItem,
|
||||
OptionMissingDomainResolver,
|
||||
OptionLegacyDomainStrategyOptions,
|
||||
OptionInlineACME,
|
||||
OptionLegacyRuleSetDownloadDetour,
|
||||
OptionLegacyTailscaleEndpointDialer,
|
||||
OptionRuleSetIPCIDRAcceptEmpty,
|
||||
OptionLegacyDNSAddressFilter,
|
||||
OptionLegacyDNSRuleStrategy,
|
||||
OptionIndependentDNSCache,
|
||||
OptionStoreRDRC,
|
||||
OptionImplicitDefaultHTTPClient,
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user