mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-13 20:28:32 +10:00
Compare commits
28 Commits
testing
...
fafe3847ec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fafe3847ec | ||
|
|
7f1d02492b | ||
|
|
c40b2bac3d | ||
|
|
b586655e7a | ||
|
|
e59c9dc3d4 | ||
|
|
5181736a30 | ||
|
|
87d3436825 | ||
|
|
773e768a09 | ||
|
|
7465a75796 | ||
|
|
0486b28e46 | ||
|
|
80ea102043 | ||
|
|
9fb7e5f709 | ||
|
|
08f07f5f77 | ||
|
|
bb24c1d70a | ||
|
|
3dd9c777b6 | ||
|
|
6f3a759939 | ||
|
|
1a7ff1abd2 | ||
|
|
f9fb4dd659 | ||
|
|
8d67adbd75 | ||
|
|
3af9a5f684 | ||
|
|
d647a9e7b9 | ||
|
|
cc2098c0da | ||
|
|
733361c567 | ||
|
|
dcc41a17cc | ||
|
|
1092643667 | ||
|
|
2dbe252d34 | ||
|
|
4ca79a465a | ||
|
|
b960cb087f |
2
.github/CRONET_GO_VERSION
vendored
2
.github/CRONET_GO_VERSION
vendored
@@ -1 +1 @@
|
||||
335e5bef5d88fc4474c9a70b865561f45a67de83
|
||||
ea7cd33752aed62603775af3df946c1b83f4b0b3
|
||||
|
||||
@@ -3,7 +3,6 @@ package adapter
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -32,13 +31,12 @@ type DNSClient interface {
|
||||
}
|
||||
|
||||
type DNSQueryOptions struct {
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
LookupStrategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
DisableOptimisticCache bool
|
||||
RewriteTTL *uint32
|
||||
ClientSubnet netip.Prefix
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
LookupStrategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
RewriteTTL *uint32
|
||||
ClientSubnet netip.Prefix
|
||||
}
|
||||
|
||||
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
||||
@@ -51,12 +49,11 @@ func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptio
|
||||
return nil, E.New("domain resolver not found: " + options.Server)
|
||||
}
|
||||
return &DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: C.DomainStrategy(options.Strategy),
|
||||
DisableCache: options.DisableCache,
|
||||
DisableOptimisticCache: options.DisableOptimisticCache,
|
||||
RewriteTTL: options.RewriteTTL,
|
||||
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
Transport: transport,
|
||||
Strategy: C.DomainStrategy(options.Strategy),
|
||||
DisableCache: options.DisableCache,
|
||||
RewriteTTL: options.RewriteTTL,
|
||||
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -66,13 +63,6 @@ type RDRCStore interface {
|
||||
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
|
||||
}
|
||||
|
||||
type DNSCacheStore interface {
|
||||
LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool)
|
||||
SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error
|
||||
SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger)
|
||||
ClearDNSCache() error
|
||||
}
|
||||
|
||||
type DNSTransport interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
|
||||
@@ -47,12 +47,6 @@ type CacheFile interface {
|
||||
StoreRDRC() bool
|
||||
RDRCStore
|
||||
|
||||
StoreDNS() bool
|
||||
DNSCacheStore
|
||||
|
||||
SetDisableExpire(disableExpire bool)
|
||||
SetOptimisticTimeout(timeout time.Duration)
|
||||
|
||||
LoadMode() string
|
||||
StoreMode(mode string) error
|
||||
LoadSelected(group string) string
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
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,11 +2,17 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-tun"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
|
||||
"go4.org/netipx"
|
||||
@@ -45,7 +51,7 @@ type ConnectionRouterEx interface {
|
||||
|
||||
type RuleSet interface {
|
||||
Name() string
|
||||
StartContext(ctx context.Context) error
|
||||
StartContext(ctx context.Context, startContext *HTTPStartContext) error
|
||||
PostStart() error
|
||||
Metadata() RuleSetMetadata
|
||||
ExtractIPSet() []*netipx.IPSet
|
||||
@@ -71,3 +77,46 @@ type RuleSetMetadata struct {
|
||||
ContainsIPCIDRRule bool
|
||||
ContainsDNSQueryTypeRule bool
|
||||
}
|
||||
type HTTPStartContext struct {
|
||||
ctx context.Context
|
||||
access sync.Mutex
|
||||
httpClientCache map[string]*http.Client
|
||||
}
|
||||
|
||||
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
|
||||
return &HTTPStartContext{
|
||||
ctx: ctx,
|
||||
httpClientCache: make(map[string]*http.Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
if httpClient, loaded := c.httpClientCache[detour]; loaded {
|
||||
return httpClient
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
TLSHandshakeTimeout: C.TCPTimeout,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
TLSClientConfig: &tls.Config{
|
||||
Time: ntp.TimeFuncFromContext(c.ctx),
|
||||
RootCAs: RootPoolFromContext(c.ctx),
|
||||
},
|
||||
},
|
||||
}
|
||||
c.httpClientCache[detour] = httpClient
|
||||
return httpClient
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) Close() {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
for _, client := range c.httpClientCache {
|
||||
client.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
41
box.go
41
box.go
@@ -16,14 +16,12 @@ import (
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/certificate"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/httpclient"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/direct"
|
||||
@@ -52,7 +50,6 @@ type Box struct {
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
httpClientService adapter.LifecycleService
|
||||
internalService []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
}
|
||||
@@ -172,10 +169,6 @@ func New(options Options) (*Box, error) {
|
||||
}
|
||||
|
||||
var internalServices []adapter.LifecycleService
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
httpClientManager := httpclient.NewManager(ctx, logFactory.NewLogger("httpclient"), options.HTTPClients, routeOptions.DefaultHTTPClient)
|
||||
service.MustRegister[adapter.HTTPClientManager](ctx, httpClientManager)
|
||||
httpClientService := adapter.LifecycleService(httpClientManager)
|
||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||
len(certificateOptions.Certificate) > 0 ||
|
||||
@@ -188,6 +181,8 @@ func New(options Options) (*Box, error) {
|
||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||
internalServices = append(internalServices, certificateStore)
|
||||
}
|
||||
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||
@@ -201,10 +196,7 @@ func New(options Options) (*Box, error) {
|
||||
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
|
||||
dnsRouter, err := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize DNS router")
|
||||
}
|
||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
|
||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
||||
@@ -373,12 +365,6 @@ 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 {
|
||||
@@ -386,7 +372,7 @@ func New(options Options) (*Box, error) {
|
||||
}
|
||||
}
|
||||
if needCacheFile {
|
||||
cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||
internalServices = append(internalServices, cacheFile)
|
||||
}
|
||||
@@ -439,7 +425,6 @@ func New(options Options) (*Box, error) {
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
httpClientService: httpClientService,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
@@ -502,15 +487,7 @@ func (s *Box) preStart() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.logger, adapter.StartStateStart, []adapter.LifecycleService{s.httpClientService})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.router, s.dnsRouter)
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection, s.router, s.dnsRouter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -587,14 +564,6 @@ func (s *Box) Close() error {
|
||||
})
|
||||
s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
if s.httpClientService != nil {
|
||||
s.logger.Trace("close ", s.httpClientService.Name())
|
||||
startTime := time.Now()
|
||||
err = E.Append(err, s.httpClientService.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", s.httpClientService.Name())
|
||||
})
|
||||
s.logger.Trace("close ", s.httpClientService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
for _, lifecycleService := range s.internalService {
|
||||
s.logger.Trace("close ", lifecycleService.Name())
|
||||
startTime := time.Now()
|
||||
|
||||
Submodule clients/android updated: fea0f3a7ba...4f0826b94d
@@ -5,7 +5,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -36,9 +35,21 @@ func updateMozillaIncludedRootCAs() error {
|
||||
return err
|
||||
}
|
||||
geoIndex := slices.Index(header, "Geographic Focus")
|
||||
nameIndex := slices.Index(header, "Common Name or Certificate Name")
|
||||
certIndex := slices.Index(header, "PEM Info")
|
||||
|
||||
pemBundle := strings.Builder{}
|
||||
generated := strings.Builder{}
|
||||
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||
|
||||
package certificate
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
var mozillaIncluded *x509.CertPool
|
||||
|
||||
func init() {
|
||||
mozillaIncluded = x509.NewCertPool()
|
||||
`)
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
@@ -49,12 +60,18 @@ func updateMozillaIncludedRootCAs() error {
|
||||
if record[geoIndex] == "China" {
|
||||
continue
|
||||
}
|
||||
generated.WriteString("\n // ")
|
||||
generated.WriteString(record[nameIndex])
|
||||
generated.WriteString("\n")
|
||||
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
|
||||
cert := record[certIndex]
|
||||
// Remove single quotes
|
||||
cert = cert[1 : len(cert)-1]
|
||||
pemBundle.WriteString(cert)
|
||||
pemBundle.WriteString("\n")
|
||||
generated.WriteString(cert)
|
||||
generated.WriteString("`))\n")
|
||||
}
|
||||
return writeGeneratedCertificateBundle("mozilla", "mozillaIncluded", pemBundle.String())
|
||||
generated.WriteString("}\n")
|
||||
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
|
||||
}
|
||||
|
||||
func fetchChinaFingerprints() (map[string]bool, error) {
|
||||
@@ -102,11 +119,23 @@ func updateChromeIncludedRootCAs() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
subjectIndex := slices.Index(header, "Subject")
|
||||
statusIndex := slices.Index(header, "Google Chrome Status")
|
||||
certIndex := slices.Index(header, "X.509 Certificate (PEM)")
|
||||
fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint")
|
||||
|
||||
pemBundle := strings.Builder{}
|
||||
generated := strings.Builder{}
|
||||
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||
|
||||
package certificate
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
var chromeIncluded *x509.CertPool
|
||||
|
||||
func init() {
|
||||
chromeIncluded = x509.NewCertPool()
|
||||
`)
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
@@ -120,39 +149,18 @@ func updateChromeIncludedRootCAs() error {
|
||||
if chinaFingerprints[record[fingerprintIndex]] {
|
||||
continue
|
||||
}
|
||||
generated.WriteString("\n // ")
|
||||
generated.WriteString(record[subjectIndex])
|
||||
generated.WriteString("\n")
|
||||
generated.WriteString(" chromeIncluded.AppendCertsFromPEM([]byte(`")
|
||||
cert := record[certIndex]
|
||||
// Remove single quotes if present
|
||||
if len(cert) > 0 && cert[0] == '\'' {
|
||||
cert = cert[1 : len(cert)-1]
|
||||
}
|
||||
pemBundle.WriteString(cert)
|
||||
pemBundle.WriteString("\n")
|
||||
generated.WriteString(cert)
|
||||
generated.WriteString("`))\n")
|
||||
}
|
||||
return writeGeneratedCertificateBundle("chrome", "chromeIncluded", pemBundle.String())
|
||||
}
|
||||
|
||||
func writeGeneratedCertificateBundle(name string, variableName string, pemBundle string) error {
|
||||
goSource := `// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed ` + name + `.pem
|
||||
var ` + variableName + `PEM string
|
||||
|
||||
var ` + variableName + ` *x509.CertPool
|
||||
|
||||
func init() {
|
||||
` + variableName + ` = x509.NewCertPool()
|
||||
` + variableName + `.AppendCertsFromPEM([]byte(` + variableName + `PEM))
|
||||
}
|
||||
`
|
||||
err := os.WriteFile(filepath.Join("common/certificate", name+".pem"), []byte(pemBundle), 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filepath.Join("common/certificate", name+".go"), []byte(goSource), 0o644)
|
||||
generated.WriteString("}\n")
|
||||
return os.WriteFile("common/certificate/chrome.go", []byte(generated.String()), 0o644)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -22,10 +22,8 @@ var _ adapter.CertificateStore = (*Store)(nil)
|
||||
|
||||
type Store struct {
|
||||
access sync.RWMutex
|
||||
store string
|
||||
systemPool *x509.CertPool
|
||||
currentPool *x509.CertPool
|
||||
currentPEM []string
|
||||
certificate string
|
||||
certificatePaths []string
|
||||
certificateDirectoryPaths []string
|
||||
@@ -63,7 +61,6 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
|
||||
return nil, E.New("unknown certificate store: ", options.Store)
|
||||
}
|
||||
store := &Store{
|
||||
store: options.Store,
|
||||
systemPool: systemPool,
|
||||
certificate: strings.Join(options.Certificate, "\n"),
|
||||
certificatePaths: options.CertificatePath,
|
||||
@@ -126,37 +123,19 @@ func (s *Store) Pool() *x509.CertPool {
|
||||
return s.currentPool
|
||||
}
|
||||
|
||||
func (s *Store) StoreKind() string {
|
||||
return s.store
|
||||
}
|
||||
|
||||
func (s *Store) CurrentPEM() []string {
|
||||
s.access.RLock()
|
||||
defer s.access.RUnlock()
|
||||
return append([]string(nil), s.currentPEM...)
|
||||
}
|
||||
|
||||
func (s *Store) update() error {
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
var currentPool *x509.CertPool
|
||||
var currentPEM []string
|
||||
if s.systemPool == nil {
|
||||
currentPool = x509.NewCertPool()
|
||||
} else {
|
||||
currentPool = s.systemPool.Clone()
|
||||
}
|
||||
switch s.store {
|
||||
case C.CertificateStoreMozilla:
|
||||
currentPEM = append(currentPEM, mozillaIncludedPEM)
|
||||
case C.CertificateStoreChrome:
|
||||
currentPEM = append(currentPEM, chromeIncludedPEM)
|
||||
}
|
||||
if s.certificate != "" {
|
||||
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
||||
return E.New("invalid certificate PEM strings")
|
||||
}
|
||||
currentPEM = append(currentPEM, s.certificate)
|
||||
}
|
||||
for _, path := range s.certificatePaths {
|
||||
pemContent, err := os.ReadFile(path)
|
||||
@@ -166,7 +145,6 @@ func (s *Store) update() error {
|
||||
if !currentPool.AppendCertsFromPEM(pemContent) {
|
||||
return E.New("invalid certificate PEM file: ", path)
|
||||
}
|
||||
currentPEM = append(currentPEM, string(pemContent))
|
||||
}
|
||||
var firstErr error
|
||||
for _, directoryPath := range s.certificateDirectoryPaths {
|
||||
@@ -179,8 +157,8 @@ func (s *Store) update() error {
|
||||
}
|
||||
for _, directoryEntry := range directoryEntries {
|
||||
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
|
||||
if err == nil && currentPool.AppendCertsFromPEM(pemContent) {
|
||||
currentPEM = append(currentPEM, string(pemContent))
|
||||
if err == nil {
|
||||
currentPool.AppendCertsFromPEM(pemContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,7 +166,6 @@ func (s *Store) update() error {
|
||||
return firstErr
|
||||
}
|
||||
s.currentPool = currentPool
|
||||
s.currentPEM = currentPEM
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ type DirectDialer interface {
|
||||
type DetourDialer struct {
|
||||
outboundManager adapter.OutboundManager
|
||||
detour string
|
||||
defaultOutbound bool
|
||||
legacyDNSDialer bool
|
||||
dialer N.Dialer
|
||||
initOnce sync.Once
|
||||
@@ -34,13 +33,6 @@ func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNS
|
||||
}
|
||||
}
|
||||
|
||||
func NewDefaultOutboundDetour(outboundManager adapter.OutboundManager) N.Dialer {
|
||||
return &DetourDialer{
|
||||
outboundManager: outboundManager,
|
||||
defaultOutbound: true,
|
||||
}
|
||||
}
|
||||
|
||||
func InitializeDetour(dialer N.Dialer) error {
|
||||
detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
|
||||
if !isDetour {
|
||||
@@ -55,18 +47,12 @@ func (d *DetourDialer) Dialer() (N.Dialer, error) {
|
||||
}
|
||||
|
||||
func (d *DetourDialer) init() {
|
||||
var dialer adapter.Outbound
|
||||
if d.detour != "" {
|
||||
var loaded bool
|
||||
dialer, loaded = d.outboundManager.Outbound(d.detour)
|
||||
if !loaded {
|
||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
dialer = d.outboundManager.Default()
|
||||
dialer, loaded := d.outboundManager.Outbound(d.detour)
|
||||
if !loaded {
|
||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||
return
|
||||
}
|
||||
if !d.defaultOutbound && !d.legacyDNSDialer {
|
||||
if !d.legacyDNSDialer {
|
||||
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
|
||||
if directDialer.IsEmpty() {
|
||||
d.initErr = E.New("detour to an empty direct outbound makes no sense")
|
||||
|
||||
@@ -25,7 +25,6 @@ type Options struct {
|
||||
NewDialer bool
|
||||
LegacyDNSDialer bool
|
||||
DirectOutbound bool
|
||||
DefaultOutbound bool
|
||||
}
|
||||
|
||||
// TODO: merge with NewWithOptions
|
||||
@@ -43,26 +42,19 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
dialer N.Dialer
|
||||
err error
|
||||
)
|
||||
hasDetour := dialOptions.Detour != "" || options.DefaultOutbound
|
||||
if dialOptions.Detour != "" {
|
||||
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
||||
if outboundManager == nil {
|
||||
return nil, E.New("missing outbound manager")
|
||||
}
|
||||
dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
|
||||
} else if options.DefaultOutbound {
|
||||
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
||||
if outboundManager == nil {
|
||||
return nil, E.New("missing outbound manager")
|
||||
}
|
||||
dialer = NewDefaultOutboundDetour(outboundManager)
|
||||
} else {
|
||||
dialer, err = NewDefault(options.Context, dialOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if options.RemoteIsDomain && (!hasDetour || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
||||
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
||||
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
|
||||
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
|
||||
var defaultOptions adapter.NetworkOptions
|
||||
@@ -95,12 +87,11 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
}
|
||||
server = dialOptions.DomainResolver.Server
|
||||
dnsQueryOptions = adapter.DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: strategy,
|
||||
DisableCache: dialOptions.DomainResolver.DisableCache,
|
||||
DisableOptimisticCache: dialOptions.DomainResolver.DisableOptimisticCache,
|
||||
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
||||
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||
Transport: transport,
|
||||
Strategy: strategy,
|
||||
DisableCache: dialOptions.DomainResolver.DisableCache,
|
||||
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
||||
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||
}
|
||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||
} else if options.DirectResolver {
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package httpclient
|
||||
|
||||
import "context"
|
||||
|
||||
type transportKey struct{}
|
||||
|
||||
func contextWithTransportTag(ctx context.Context, transportTag string) context.Context {
|
||||
return context.WithValue(ctx, transportKey{}, transportTag)
|
||||
}
|
||||
|
||||
func transportTagFromContext(ctx context.Context) (string, bool) {
|
||||
value, loaded := ctx.Value(transportKey{}).(string)
|
||||
return value, loaded
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdTLS "crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func dialTLS(ctx context.Context, rawDialer N.Dialer, baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string, expectProto string) (net.Conn, error) {
|
||||
if baseTLSConfig == nil {
|
||||
return nil, E.New("TLS transport unavailable")
|
||||
}
|
||||
tlsConfig := baseTLSConfig.Clone()
|
||||
if tlsConfig.ServerName() == "" && destination.IsValid() {
|
||||
tlsConfig.SetServerName(destination.AddrString())
|
||||
}
|
||||
tlsConfig.SetNextProtos(nextProtos)
|
||||
conn, err := rawDialer.DialContext(ctx, N.NetworkTCP, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConn, err := tls.ClientHandshake(ctx, conn, tlsConfig)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if expectProto != "" && tlsConn.ConnectionState().NegotiatedProtocol != expectProto {
|
||||
tlsConn.Close()
|
||||
return nil, errHTTP2Fallback
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
func applyHeaders(request *http.Request, headers http.Header, host string) {
|
||||
for header, values := range headers {
|
||||
request.Header[header] = append([]string(nil), values...)
|
||||
}
|
||||
if host != "" {
|
||||
request.Host = host
|
||||
}
|
||||
}
|
||||
|
||||
func requestRequiresHTTP1(request *http.Request) bool {
|
||||
return strings.Contains(strings.ToLower(request.Header.Get("Connection")), "upgrade") &&
|
||||
strings.EqualFold(request.Header.Get("Upgrade"), "websocket")
|
||||
}
|
||||
|
||||
func requestReplayable(request *http.Request) bool {
|
||||
return request.Body == nil || request.Body == http.NoBody || request.GetBody != nil
|
||||
}
|
||||
|
||||
func cloneRequestForRetry(request *http.Request) *http.Request {
|
||||
cloned := request.Clone(request.Context())
|
||||
if request.Body != nil && request.Body != http.NoBody && request.GetBody != nil {
|
||||
cloned.Body = mustGetBody(request)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func mustGetBody(request *http.Request) io.ReadCloser {
|
||||
body, err := request.GetBody()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func buildSTDTLSConfig(baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string) (*stdTLS.Config, error) {
|
||||
if baseTLSConfig == nil {
|
||||
return nil, nil
|
||||
}
|
||||
tlsConfig := baseTLSConfig.Clone()
|
||||
if tlsConfig.ServerName() == "" && destination.IsValid() {
|
||||
tlsConfig.SetServerName(destination.AddrString())
|
||||
}
|
||||
tlsConfig.SetNextProtos(nextProtos)
|
||||
return tlsConfig.STDConfig()
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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()}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
stdTLS "crypto/tls"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
func CloneHTTP2Transport(transport *http2.Transport) *http2.Transport {
|
||||
return &http2.Transport{
|
||||
ReadIdleTimeout: transport.ReadIdleTimeout,
|
||||
PingTimeout: transport.PingTimeout,
|
||||
DialTLSContext: transport.DialTLSContext,
|
||||
}
|
||||
}
|
||||
|
||||
func ConfigureHTTP2Transport(options option.HTTP2Options) (*http2.Transport, error) {
|
||||
stdTransport := &http.Transport{
|
||||
TLSClientConfig: &stdTLS.Config{},
|
||||
HTTP2: &http.HTTP2Config{
|
||||
MaxReceiveBufferPerStream: int(options.StreamReceiveWindow.Value()),
|
||||
MaxReceiveBufferPerConnection: int(options.ConnectionReceiveWindow.Value()),
|
||||
MaxConcurrentStreams: options.MaxConcurrentStreams,
|
||||
SendPingTimeout: time.Duration(options.KeepAlivePeriod),
|
||||
PingTimeout: time.Duration(options.IdleTimeout),
|
||||
},
|
||||
}
|
||||
h2Transport, err := http2.ConfigureTransports(stdTransport)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "configure HTTP/2 transport")
|
||||
}
|
||||
// ConfigureTransports binds ConnPool to the throwaway http.Transport; sever it so DialTLSContext is used directly.
|
||||
h2Transport.ConnPool = nil
|
||||
h2Transport.ReadIdleTimeout = time.Duration(options.KeepAlivePeriod)
|
||||
h2Transport.PingTimeout = time.Duration(options.IdleTimeout)
|
||||
return h2Transport, nil
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
//go:build with_quic
|
||||
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdTLS "crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
"github.com/sagernet/sing-box/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)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
//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")
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
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,8 +9,6 @@ import (
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
"github.com/sagernet/sing/common/bufio/deadline"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
@@ -433,9 +431,6 @@ func Run(options Options) (*Result, error) {
|
||||
defer func() {
|
||||
_ = packetConn.Close()
|
||||
}()
|
||||
if deadline.NeedAdditionalReadDeadline(packetConn) {
|
||||
packetConn = deadline.NewPacketConn(bufio.NewPacketConn(packetConn))
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,205 +0,0 @@
|
||||
//go:build darwin && cgo
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdtls "crypto/tls"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
const appleTLSTestTimeout = 5 * time.Second
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//go:build !darwin || !cgo
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
func newAppleClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
return nil, E.New("Apple TLS engine is not available on non-Apple platforms")
|
||||
}
|
||||
@@ -8,16 +8,14 @@ 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
|
||||
@@ -44,12 +42,11 @@ func NewClient(ctx context.Context, logger logger.ContextLogger, serverAddress s
|
||||
}
|
||||
|
||||
type ClientOptions struct {
|
||||
Context context.Context
|
||||
Logger logger.ContextLogger
|
||||
ServerAddress string
|
||||
Options option.OutboundTLSOptions
|
||||
AllowEmptyServerName bool
|
||||
KTLSCompatible bool
|
||||
Context context.Context
|
||||
Logger logger.ContextLogger
|
||||
ServerAddress string
|
||||
Options option.OutboundTLSOptions
|
||||
KTLSCompatible bool
|
||||
}
|
||||
|
||||
func NewClientWithOptions(options ClientOptions) (Config, error) {
|
||||
@@ -64,22 +61,17 @@ func NewClientWithOptions(options ClientOptions) (Config, error) {
|
||||
if options.Options.KernelRx {
|
||||
options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx")
|
||||
}
|
||||
switch options.Options.Engine {
|
||||
case "", "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)
|
||||
}
|
||||
if options.Options.Reality != nil && options.Options.Reality.Enabled {
|
||||
return newRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
||||
return NewRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||
} else if options.Options.UTLS != nil && options.Options.UTLS.Enabled {
|
||||
return newUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
||||
return NewUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||
}
|
||||
return newSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
||||
return NewSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||
}
|
||||
|
||||
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||
defer cancel()
|
||||
tlsConn, err := aTLS.ClientHandshake(ctx, conn, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -52,15 +52,11 @@ type RealityClientConfig struct {
|
||||
}
|
||||
|
||||
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newRealityClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
if options.UTLS == nil || !options.UTLS.Enabled {
|
||||
return nil, E.New("uTLS is required by reality client")
|
||||
}
|
||||
|
||||
uClient, err := newUTLSClient(ctx, logger, serverAddress, options, allowEmptyServerName)
|
||||
uClient, err := NewUTLSClient(ctx, logger, serverAddress, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -112,14 +108,6 @@ func (e *RealityClientConfig) SetNextProtos(nextProto []string) {
|
||||
e.uClient.SetNextProtos(nextProto)
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) HandshakeTimeout() time.Duration {
|
||||
return e.uClient.HandshakeTimeout()
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
e.uClient.SetHandshakeTimeout(timeout)
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) STDConfig() (*STDConfig, error) {
|
||||
return nil, E.New("unsupported usage for reality")
|
||||
}
|
||||
|
||||
@@ -26,8 +26,7 @@ import (
|
||||
var _ ServerConfigCompat = (*RealityServerConfig)(nil)
|
||||
|
||||
type RealityServerConfig struct {
|
||||
config *utls.RealityConfig
|
||||
handshakeTimeout time.Duration
|
||||
config *utls.RealityConfig
|
||||
}
|
||||
|
||||
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||
@@ -131,16 +130,7 @@ func NewRealityServer(ctx context.Context, logger log.ContextLogger, options opt
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
return nil, E.New("Reality is conflict with ECH")
|
||||
}
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = C.TCPTimeout
|
||||
}
|
||||
var config ServerConfig = &RealityServerConfig{
|
||||
config: &tlsConfig,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
}
|
||||
var config ServerConfig = &RealityServerConfig{&tlsConfig}
|
||||
if options.KernelTx || options.KernelRx {
|
||||
if !C.IsLinux {
|
||||
return nil, E.New("kTLS is only supported on Linux")
|
||||
@@ -171,14 +161,6 @@ func (c *RealityServerConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *RealityServerConfig) HandshakeTimeout() time.Duration {
|
||||
return c.handshakeTimeout
|
||||
}
|
||||
|
||||
func (c *RealityServerConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
c.handshakeTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *RealityServerConfig) STDConfig() (*tls.Config, error) {
|
||||
return nil, E.New("unsupported usage for reality")
|
||||
}
|
||||
@@ -209,8 +191,7 @@ func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn
|
||||
|
||||
func (c *RealityServerConfig) Clone() Config {
|
||||
return &RealityServerConfig{
|
||||
config: c.config.Clone(),
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
config: c.config.Clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,11 +46,8 @@ func NewServerWithOptions(options ServerOptions) (ServerConfig, error) {
|
||||
}
|
||||
|
||||
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
||||
if config.HandshakeTimeout() == 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, C.TCPTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||
defer cancel()
|
||||
tlsConn, err := aTLS.ServerHandshake(ctx, conn, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -24,30 +24,16 @@ import (
|
||||
type STDClientConfig struct {
|
||||
ctx context.Context
|
||||
config *tls.Config
|
||||
serverName string
|
||||
disableSNI bool
|
||||
verifyServerName bool
|
||||
handshakeTimeout time.Duration
|
||||
fragment bool
|
||||
fragmentFallbackDelay time.Duration
|
||||
recordFragment bool
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) ServerName() string {
|
||||
return c.serverName
|
||||
return c.config.ServerName
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) SetServerName(serverName string) {
|
||||
c.serverName = serverName
|
||||
if c.disableSNI {
|
||||
c.config.ServerName = ""
|
||||
if c.verifyServerName {
|
||||
c.config.VerifyConnection = verifyConnection(c.config.RootCAs, c.config.Time, serverName)
|
||||
} else {
|
||||
c.config.VerifyConnection = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
c.config.ServerName = serverName
|
||||
}
|
||||
|
||||
@@ -59,14 +45,6 @@ func (c *STDClientConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) HandshakeTimeout() time.Duration {
|
||||
return c.handshakeTimeout
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
c.handshakeTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) STDConfig() (*STDConfig, error) {
|
||||
return c.config, nil
|
||||
}
|
||||
@@ -79,19 +57,13 @@ func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) {
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) Clone() Config {
|
||||
cloned := &STDClientConfig{
|
||||
return &STDClientConfig{
|
||||
ctx: c.ctx,
|
||||
config: c.config.Clone(),
|
||||
serverName: c.serverName,
|
||||
disableSNI: c.disableSNI,
|
||||
verifyServerName: c.verifyServerName,
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
fragment: c.fragment,
|
||||
fragmentFallbackDelay: c.fragmentFallbackDelay,
|
||||
recordFragment: c.recordFragment,
|
||||
}
|
||||
cloned.SetServerName(cloned.serverName)
|
||||
return cloned
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) ECHConfigList() []byte {
|
||||
@@ -103,27 +75,41 @@ func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte
|
||||
}
|
||||
|
||||
func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newSTDClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
var serverName string
|
||||
if options.ServerName != "" {
|
||||
serverName = options.ServerName
|
||||
} else if serverAddress != "" {
|
||||
serverName = serverAddress
|
||||
}
|
||||
if serverName == "" && !options.Insecure && !allowEmptyServerName {
|
||||
return nil, errMissingServerName
|
||||
if serverName == "" && !options.Insecure {
|
||||
return nil, E.New("missing server_name or insecure=true")
|
||||
}
|
||||
|
||||
var tlsConfig tls.Config
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||
if !options.DisableSNI {
|
||||
tlsConfig.ServerName = serverName
|
||||
}
|
||||
if options.Insecure {
|
||||
tlsConfig.InsecureSkipVerify = options.Insecure
|
||||
} else if options.DisableSNI {
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
|
||||
verifyOptions := x509.VerifyOptions{
|
||||
Roots: tlsConfig.RootCAs,
|
||||
DNSName: serverName,
|
||||
Intermediates: x509.NewCertPool(),
|
||||
}
|
||||
for _, cert := range state.PeerCertificates[1:] {
|
||||
verifyOptions.Intermediates.AddCert(cert)
|
||||
}
|
||||
if tlsConfig.Time != nil {
|
||||
verifyOptions.CurrentTime = tlsConfig.Time()
|
||||
}
|
||||
_, err := state.PeerCertificates[0].Verify(verifyOptions)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(options.CertificatePublicKeySHA256) > 0 {
|
||||
if len(options.Certificate) > 0 || options.CertificatePath != "" {
|
||||
@@ -212,24 +198,7 @@ func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
|
||||
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
|
||||
return nil, E.New("client certificate and client key must be provided together")
|
||||
}
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = C.TCPTimeout
|
||||
}
|
||||
var config Config = &STDClientConfig{
|
||||
ctx: ctx,
|
||||
config: &tlsConfig,
|
||||
serverName: serverName,
|
||||
disableSNI: options.DisableSNI,
|
||||
verifyServerName: options.DisableSNI && !options.Insecure,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
fragment: options.Fragment,
|
||||
fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay),
|
||||
recordFragment: options.RecordFragment,
|
||||
}
|
||||
config.SetServerName(serverName)
|
||||
var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
var err error
|
||||
config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options)
|
||||
@@ -251,27 +220,6 @@ 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,7 +92,6 @@ func getACMENextProtos(provider adapter.CertificateProvider) []string {
|
||||
type STDServerConfig struct {
|
||||
access sync.RWMutex
|
||||
config *tls.Config
|
||||
handshakeTimeout time.Duration
|
||||
logger log.Logger
|
||||
certificateProvider managedCertificateProvider
|
||||
acmeService adapter.SimpleLifecycle
|
||||
@@ -140,18 +139,6 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
||||
c.config = config
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) HandshakeTimeout() time.Duration {
|
||||
c.access.RLock()
|
||||
defer c.access.RUnlock()
|
||||
return c.handshakeTimeout
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
c.handshakeTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) hasACMEALPN() bool {
|
||||
if c.acmeService != nil {
|
||||
return true
|
||||
@@ -178,8 +165,7 @@ func (c *STDServerConfig) Server(conn net.Conn) (Conn, error) {
|
||||
|
||||
func (c *STDServerConfig) Clone() Config {
|
||||
return &STDServerConfig{
|
||||
config: c.config.Clone(),
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
config: c.config.Clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,15 +471,8 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = C.TCPTimeout
|
||||
}
|
||||
serverConfig := &STDServerConfig{
|
||||
config: tlsConfig,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
logger: logger,
|
||||
certificateProvider: certificateProvider,
|
||||
acmeService: acmeService,
|
||||
|
||||
@@ -28,10 +28,6 @@ import (
|
||||
type UTLSClientConfig struct {
|
||||
ctx context.Context
|
||||
config *utls.Config
|
||||
serverName string
|
||||
disableSNI bool
|
||||
verifyServerName bool
|
||||
handshakeTimeout time.Duration
|
||||
id utls.ClientHelloID
|
||||
fragment bool
|
||||
fragmentFallbackDelay time.Duration
|
||||
@@ -39,20 +35,10 @@ type UTLSClientConfig struct {
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) ServerName() string {
|
||||
return c.serverName
|
||||
return c.config.ServerName
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) SetServerName(serverName string) {
|
||||
c.serverName = serverName
|
||||
if c.disableSNI {
|
||||
c.config.ServerName = ""
|
||||
if c.verifyServerName {
|
||||
c.config.InsecureServerNameToVerify = serverName
|
||||
} else {
|
||||
c.config.InsecureServerNameToVerify = ""
|
||||
}
|
||||
return
|
||||
}
|
||||
c.config.ServerName = serverName
|
||||
}
|
||||
|
||||
@@ -67,14 +53,6 @@ func (c *UTLSClientConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) HandshakeTimeout() time.Duration {
|
||||
return c.handshakeTimeout
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
c.handshakeTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) STDConfig() (*STDConfig, error) {
|
||||
return nil, E.New("unsupported usage for uTLS")
|
||||
}
|
||||
@@ -91,20 +69,9 @@ func (c *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []by
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) Clone() Config {
|
||||
cloned := &UTLSClientConfig{
|
||||
ctx: c.ctx,
|
||||
config: c.config.Clone(),
|
||||
serverName: c.serverName,
|
||||
disableSNI: c.disableSNI,
|
||||
verifyServerName: c.verifyServerName,
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
id: c.id,
|
||||
fragment: c.fragment,
|
||||
fragmentFallbackDelay: c.fragmentFallbackDelay,
|
||||
recordFragment: c.recordFragment,
|
||||
return &UTLSClientConfig{
|
||||
c.ctx, c.config.Clone(), c.id, c.fragment, c.fragmentFallbackDelay, c.recordFragment,
|
||||
}
|
||||
cloned.SetServerName(cloned.serverName)
|
||||
return cloned
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) ECHConfigList() []byte {
|
||||
@@ -176,29 +143,29 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newUTLSClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
var serverName string
|
||||
if options.ServerName != "" {
|
||||
serverName = options.ServerName
|
||||
} else if serverAddress != "" {
|
||||
serverName = serverAddress
|
||||
}
|
||||
if serverName == "" && !options.Insecure && !allowEmptyServerName {
|
||||
return nil, errMissingServerName
|
||||
if serverName == "" && !options.Insecure {
|
||||
return nil, E.New("missing server_name or insecure=true")
|
||||
}
|
||||
|
||||
var tlsConfig utls.Config
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||
if !options.DisableSNI {
|
||||
tlsConfig.ServerName = serverName
|
||||
}
|
||||
if options.Insecure {
|
||||
tlsConfig.InsecureSkipVerify = options.Insecure
|
||||
} else if options.DisableSNI {
|
||||
if options.Reality != nil && options.Reality.Enabled {
|
||||
return nil, E.New("disable_sni is unsupported in reality")
|
||||
}
|
||||
tlsConfig.InsecureServerNameToVerify = serverName
|
||||
}
|
||||
if len(options.CertificatePublicKeySHA256) > 0 {
|
||||
if len(options.Certificate) > 0 || options.CertificatePath != "" {
|
||||
@@ -284,29 +251,11 @@ func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre
|
||||
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
|
||||
return nil, E.New("client certificate and client key must be provided together")
|
||||
}
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = C.TCPTimeout
|
||||
}
|
||||
id, err := uTLSClientHelloID(options.UTLS.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var config Config = &UTLSClientConfig{
|
||||
ctx: ctx,
|
||||
config: &tlsConfig,
|
||||
serverName: serverName,
|
||||
disableSNI: options.DisableSNI,
|
||||
verifyServerName: options.DisableSNI && !options.Insecure,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
id: id,
|
||||
fragment: options.Fragment,
|
||||
fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay),
|
||||
recordFragment: options.RecordFragment,
|
||||
}
|
||||
config.SetServerName(serverName)
|
||||
var config Config = &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
if options.Reality != nil && options.Reality.Enabled {
|
||||
return nil, E.New("Reality is conflict with ECH")
|
||||
|
||||
@@ -12,18 +12,10 @@ import (
|
||||
)
|
||||
|
||||
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newUTLSClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`)
|
||||
}
|
||||
|
||||
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newRealityClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`)
|
||||
}
|
||||
|
||||
|
||||
562
dns/client.go
562
dns/client.go
@@ -30,63 +30,59 @@ var (
|
||||
var _ adapter.DNSClient = (*Client)(nil)
|
||||
|
||||
type Client struct {
|
||||
ctx context.Context
|
||||
timeout time.Duration
|
||||
disableCache bool
|
||||
disableExpire bool
|
||||
optimisticTimeout time.Duration
|
||||
cacheCapacity uint32
|
||||
clientSubnet netip.Prefix
|
||||
rdrc adapter.RDRCStore
|
||||
initRDRCFunc func() adapter.RDRCStore
|
||||
dnsCache adapter.DNSCacheStore
|
||||
initDNSCacheFunc func() adapter.DNSCacheStore
|
||||
logger logger.ContextLogger
|
||||
cache freelru.Cache[dnsCacheKey, *dns.Msg]
|
||||
cacheLock compatible.Map[dnsCacheKey, chan struct{}]
|
||||
backgroundRefresh compatible.Map[dnsCacheKey, struct{}]
|
||||
timeout time.Duration
|
||||
disableCache bool
|
||||
disableExpire bool
|
||||
independentCache bool
|
||||
clientSubnet netip.Prefix
|
||||
rdrc adapter.RDRCStore
|
||||
initRDRCFunc func() adapter.RDRCStore
|
||||
logger logger.ContextLogger
|
||||
cache freelru.Cache[dns.Question, *dns.Msg]
|
||||
cacheLock compatible.Map[dns.Question, chan struct{}]
|
||||
transportCache freelru.Cache[transportCacheKey, *dns.Msg]
|
||||
transportCacheLock compatible.Map[dns.Question, chan struct{}]
|
||||
}
|
||||
|
||||
type ClientOptions struct {
|
||||
Context context.Context
|
||||
Timeout time.Duration
|
||||
DisableCache bool
|
||||
DisableExpire bool
|
||||
OptimisticTimeout time.Duration
|
||||
CacheCapacity uint32
|
||||
ClientSubnet netip.Prefix
|
||||
RDRC func() adapter.RDRCStore
|
||||
DNSCache func() adapter.DNSCacheStore
|
||||
Logger logger.ContextLogger
|
||||
Timeout time.Duration
|
||||
DisableCache bool
|
||||
DisableExpire bool
|
||||
IndependentCache bool
|
||||
CacheCapacity uint32
|
||||
ClientSubnet netip.Prefix
|
||||
RDRC func() adapter.RDRCStore
|
||||
Logger logger.ContextLogger
|
||||
}
|
||||
|
||||
func NewClient(options ClientOptions) *Client {
|
||||
cacheCapacity := options.CacheCapacity
|
||||
if cacheCapacity < 1024 {
|
||||
cacheCapacity = 1024
|
||||
}
|
||||
client := &Client{
|
||||
ctx: options.Context,
|
||||
timeout: options.Timeout,
|
||||
disableCache: options.DisableCache,
|
||||
disableExpire: options.DisableExpire,
|
||||
optimisticTimeout: options.OptimisticTimeout,
|
||||
cacheCapacity: cacheCapacity,
|
||||
clientSubnet: options.ClientSubnet,
|
||||
initRDRCFunc: options.RDRC,
|
||||
initDNSCacheFunc: options.DNSCache,
|
||||
logger: options.Logger,
|
||||
timeout: options.Timeout,
|
||||
disableCache: options.DisableCache,
|
||||
disableExpire: options.DisableExpire,
|
||||
independentCache: options.IndependentCache,
|
||||
clientSubnet: options.ClientSubnet,
|
||||
initRDRCFunc: options.RDRC,
|
||||
logger: options.Logger,
|
||||
}
|
||||
if client.timeout == 0 {
|
||||
client.timeout = C.DNSTimeout
|
||||
}
|
||||
if !client.disableCache && client.initDNSCacheFunc == nil {
|
||||
client.initializeMemoryCache()
|
||||
cacheCapacity := options.CacheCapacity
|
||||
if cacheCapacity < 1024 {
|
||||
cacheCapacity = 1024
|
||||
}
|
||||
if !client.disableCache {
|
||||
if !client.independentCache {
|
||||
client.cache = common.Must1(freelru.NewSharded[dns.Question, *dns.Msg](cacheCapacity, maphash.NewHasher[dns.Question]().Hash32))
|
||||
} else {
|
||||
client.transportCache = common.Must1(freelru.NewSharded[transportCacheKey, *dns.Msg](cacheCapacity, maphash.NewHasher[transportCacheKey]().Hash32))
|
||||
}
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
type dnsCacheKey struct {
|
||||
type transportCacheKey struct {
|
||||
dns.Question
|
||||
transportTag string
|
||||
}
|
||||
@@ -95,19 +91,6 @@ func (c *Client) Start() {
|
||||
if c.initRDRCFunc != nil {
|
||||
c.rdrc = c.initRDRCFunc()
|
||||
}
|
||||
if c.initDNSCacheFunc != nil {
|
||||
c.dnsCache = c.initDNSCacheFunc()
|
||||
}
|
||||
if c.dnsCache == nil {
|
||||
c.initializeMemoryCache()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) initializeMemoryCache() {
|
||||
if c.disableCache || c.cache != nil {
|
||||
return
|
||||
}
|
||||
c.cache = common.Must1(freelru.NewSharded[dnsCacheKey, *dns.Msg](c.cacheCapacity, maphash.NewHasher[dnsCacheKey]().Hash32))
|
||||
}
|
||||
|
||||
func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
|
||||
@@ -124,37 +107,6 @@ func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func computeTimeToLive(response *dns.Msg) uint32 {
|
||||
var timeToLive uint32
|
||||
if len(response.Answer) == 0 {
|
||||
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
|
||||
return soaTTL
|
||||
}
|
||||
}
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
||||
timeToLive = record.Header().Ttl
|
||||
}
|
||||
}
|
||||
}
|
||||
return timeToLive
|
||||
}
|
||||
|
||||
func normalizeTTL(response *dns.Msg, timeToLive uint32) {
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
record.Header().Ttl = timeToLive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error) {
|
||||
if len(message.Question) == 0 {
|
||||
if c.logger != nil {
|
||||
@@ -169,7 +121,13 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
}
|
||||
return FixedResponseStatus(message, dns.RcodeSuccess), nil
|
||||
}
|
||||
message = c.prepareExchangeMessage(message, options)
|
||||
clientSubnet := options.ClientSubnet
|
||||
if !clientSubnet.IsValid() {
|
||||
clientSubnet = c.clientSubnet
|
||||
}
|
||||
if clientSubnet.IsValid() {
|
||||
message = SetClientSubnet(message, clientSubnet)
|
||||
}
|
||||
|
||||
isSimpleRequest := len(message.Question) == 1 &&
|
||||
len(message.Ns) == 0 &&
|
||||
@@ -181,32 +139,40 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
!options.ClientSubnet.IsValid()
|
||||
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
|
||||
if !disableCache {
|
||||
cacheKey := dnsCacheKey{Question: question, transportTag: transport.Tag()}
|
||||
cond, loaded := c.cacheLock.LoadOrStore(cacheKey, make(chan struct{}))
|
||||
if loaded {
|
||||
select {
|
||||
case <-cond:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
if c.cache != nil {
|
||||
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
|
||||
if loaded {
|
||||
select {
|
||||
case <-cond:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
} else {
|
||||
defer func() {
|
||||
c.cacheLock.Delete(question)
|
||||
close(cond)
|
||||
}()
|
||||
}
|
||||
} else if c.transportCache != nil {
|
||||
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
|
||||
if loaded {
|
||||
select {
|
||||
case <-cond:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
} else {
|
||||
defer func() {
|
||||
c.transportCacheLock.Delete(question)
|
||||
close(cond)
|
||||
}()
|
||||
}
|
||||
} else {
|
||||
defer func() {
|
||||
c.cacheLock.Delete(cacheKey)
|
||||
close(cond)
|
||||
}()
|
||||
}
|
||||
response, ttl, isStale := c.loadResponse(question, transport)
|
||||
response, ttl := c.loadResponse(question, transport)
|
||||
if response != nil {
|
||||
if isStale && !options.DisableOptimisticCache {
|
||||
c.backgroundRefreshDNS(transport, question, message.Copy(), options, responseChecker)
|
||||
logOptimisticResponse(c.logger, ctx, response)
|
||||
response.Id = message.Id
|
||||
return response, nil
|
||||
} else if !isStale {
|
||||
logCachedResponse(c.logger, ctx, response, ttl)
|
||||
response.Id = message.Id
|
||||
return response, nil
|
||||
}
|
||||
logCachedResponse(c.logger, ctx, response, ttl)
|
||||
response.Id = message.Id
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,10 +188,52 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
return nil, ErrResponseRejectedCached
|
||||
}
|
||||
}
|
||||
response, err := c.exchangeToTransport(ctx, transport, message)
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
response, err := transport.Exchange(ctx, message)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var rcodeError RcodeError
|
||||
if errors.As(err, &rcodeError) {
|
||||
response = FixedResponseStatus(message, int(rcodeError))
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
/*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
|
||||
validResponse := response
|
||||
loop:
|
||||
for {
|
||||
var (
|
||||
addresses int
|
||||
queryCNAME string
|
||||
)
|
||||
for _, rawRR := range validResponse.Answer {
|
||||
switch rr := rawRR.(type) {
|
||||
case *dns.A:
|
||||
break loop
|
||||
case *dns.AAAA:
|
||||
break loop
|
||||
case *dns.CNAME:
|
||||
queryCNAME = rr.Target
|
||||
}
|
||||
}
|
||||
if queryCNAME == "" {
|
||||
break
|
||||
}
|
||||
exMessage := *message
|
||||
exMessage.Question = []dns.Question{{
|
||||
Name: queryCNAME,
|
||||
Qtype: question.Qtype,
|
||||
}}
|
||||
validResponse, err = c.Exchange(ctx, transport, &exMessage, options, responseChecker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if validResponse != response {
|
||||
response.Answer = append(response.Answer, validResponse.Answer...)
|
||||
}
|
||||
}*/
|
||||
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
|
||||
if responseChecker != nil {
|
||||
var rejected bool
|
||||
@@ -242,7 +250,54 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
return response, ErrResponseRejected
|
||||
}
|
||||
}
|
||||
timeToLive := applyResponseOptions(question, response, options)
|
||||
if question.Qtype == dns.TypeHTTPS {
|
||||
if options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only {
|
||||
for _, rr := range response.Answer {
|
||||
https, isHTTPS := rr.(*dns.HTTPS)
|
||||
if !isHTTPS {
|
||||
continue
|
||||
}
|
||||
content := https.SVCB
|
||||
content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool {
|
||||
if options.Strategy == C.DomainStrategyIPv4Only {
|
||||
return it.Key() != dns.SVCB_IPV6HINT
|
||||
} else {
|
||||
return it.Key() != dns.SVCB_IPV4HINT
|
||||
}
|
||||
})
|
||||
https.SVCB = content
|
||||
}
|
||||
}
|
||||
}
|
||||
var timeToLive uint32
|
||||
if len(response.Answer) == 0 {
|
||||
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
|
||||
timeToLive = soaTTL
|
||||
}
|
||||
}
|
||||
if timeToLive == 0 {
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
||||
timeToLive = record.Header().Ttl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if options.RewriteTTL != nil {
|
||||
timeToLive = *options.RewriteTTL
|
||||
}
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
record.Header().Ttl = timeToLive
|
||||
}
|
||||
}
|
||||
if !disableCache {
|
||||
c.storeCache(transport, question, response, timeToLive)
|
||||
}
|
||||
@@ -308,12 +363,8 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
|
||||
func (c *Client) ClearCache() {
|
||||
if c.cache != nil {
|
||||
c.cache.Purge()
|
||||
}
|
||||
if c.dnsCache != nil {
|
||||
err := c.dnsCache.ClearDNSCache()
|
||||
if err != nil && c.logger != nil {
|
||||
c.logger.Warn("clear DNS cache: ", err)
|
||||
}
|
||||
} else if c.transportCache != nil {
|
||||
c.transportCache.Purge()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,22 +380,24 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
|
||||
if timeToLive == 0 {
|
||||
return
|
||||
}
|
||||
if c.dnsCache != nil {
|
||||
packed, err := message.Pack()
|
||||
if err == nil {
|
||||
expireAt := time.Now().Add(time.Second * time.Duration(timeToLive))
|
||||
c.dnsCache.SaveDNSCacheAsync(transport.Tag(), question.Name, question.Qtype, packed, expireAt, c.logger)
|
||||
}
|
||||
return
|
||||
}
|
||||
if c.cache == nil {
|
||||
return
|
||||
}
|
||||
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
|
||||
if c.disableExpire {
|
||||
c.cache.Add(key, message.Copy())
|
||||
if !c.independentCache {
|
||||
c.cache.Add(question, message.Copy())
|
||||
} else {
|
||||
c.transportCache.Add(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
}, message.Copy())
|
||||
}
|
||||
} else {
|
||||
c.cache.AddWithLifetime(key, message.Copy(), time.Second*time.Duration(timeToLive))
|
||||
if !c.independentCache {
|
||||
c.cache.AddWithLifetime(question, message.Copy(), time.Second*time.Duration(timeToLive))
|
||||
} else {
|
||||
c.transportCache.AddWithLifetime(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
}, message.Copy(), time.Second*time.Duration(timeToLive))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,19 +407,19 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran
|
||||
Qtype: qType,
|
||||
Qclass: dns.ClassINET,
|
||||
}
|
||||
disableCache := c.disableCache || options.DisableCache
|
||||
if !disableCache {
|
||||
cachedAddresses, err := c.questionCache(question, transport)
|
||||
if err != ErrNotCached {
|
||||
return cachedAddresses, err
|
||||
}
|
||||
}
|
||||
message := dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
RecursionDesired: true,
|
||||
},
|
||||
Question: []dns.Question{question},
|
||||
}
|
||||
disableCache := c.disableCache || options.DisableCache
|
||||
if !disableCache {
|
||||
cachedAddresses, err := c.questionCache(ctx, transport, &message, options, responseChecker)
|
||||
if err != ErrNotCached {
|
||||
return cachedAddresses, err
|
||||
}
|
||||
}
|
||||
response, err := c.Exchange(ctx, transport, &message, options, responseChecker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -377,177 +430,98 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran
|
||||
return MessageToAddresses(response), nil
|
||||
}
|
||||
|
||||
func (c *Client) questionCache(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
|
||||
question := message.Question[0]
|
||||
response, _, isStale := c.loadResponse(question, transport)
|
||||
func (c *Client) questionCache(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) {
|
||||
response, _ := c.loadResponse(question, transport)
|
||||
if response == nil {
|
||||
return nil, ErrNotCached
|
||||
}
|
||||
if isStale {
|
||||
if options.DisableOptimisticCache {
|
||||
return nil, ErrNotCached
|
||||
}
|
||||
c.backgroundRefreshDNS(transport, question, c.prepareExchangeMessage(message.Copy(), options), options, responseChecker)
|
||||
logOptimisticResponse(c.logger, ctx, response)
|
||||
}
|
||||
if response.Rcode != dns.RcodeSuccess {
|
||||
return nil, RcodeError(response.Rcode)
|
||||
}
|
||||
return MessageToAddresses(response), nil
|
||||
}
|
||||
|
||||
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) {
|
||||
if c.dnsCache != nil {
|
||||
return c.loadPersistentResponse(question, transport)
|
||||
}
|
||||
if c.cache == nil {
|
||||
return nil, 0, false
|
||||
}
|
||||
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
|
||||
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int) {
|
||||
var (
|
||||
response *dns.Msg
|
||||
loaded bool
|
||||
)
|
||||
if c.disableExpire {
|
||||
response, loaded := c.cache.Get(key)
|
||||
if !loaded {
|
||||
return nil, 0, false
|
||||
}
|
||||
return response.Copy(), 0, false
|
||||
}
|
||||
response, expireAt, loaded := c.cache.GetWithLifetimeNoExpire(key)
|
||||
if !loaded {
|
||||
return nil, 0, false
|
||||
}
|
||||
timeNow := time.Now()
|
||||
if timeNow.After(expireAt) {
|
||||
if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) {
|
||||
response = response.Copy()
|
||||
normalizeTTL(response, 1)
|
||||
return response, 0, true
|
||||
}
|
||||
c.cache.Remove(key)
|
||||
return nil, 0, false
|
||||
}
|
||||
nowTTL := int(expireAt.Sub(timeNow).Seconds())
|
||||
if nowTTL < 0 {
|
||||
nowTTL = 0
|
||||
}
|
||||
response = response.Copy()
|
||||
normalizeTTL(response, uint32(nowTTL))
|
||||
return response, nowTTL, false
|
||||
}
|
||||
|
||||
func (c *Client) loadPersistentResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) {
|
||||
rawMessage, expireAt, loaded := c.dnsCache.LoadDNSCache(transport.Tag(), question.Name, question.Qtype)
|
||||
if !loaded {
|
||||
return nil, 0, false
|
||||
}
|
||||
response := new(dns.Msg)
|
||||
err := response.Unpack(rawMessage)
|
||||
if err != nil {
|
||||
return nil, 0, false
|
||||
}
|
||||
if c.disableExpire {
|
||||
return response, 0, false
|
||||
}
|
||||
timeNow := time.Now()
|
||||
if timeNow.After(expireAt) {
|
||||
if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) {
|
||||
normalizeTTL(response, 1)
|
||||
return response, 0, true
|
||||
}
|
||||
return nil, 0, false
|
||||
}
|
||||
nowTTL := int(expireAt.Sub(timeNow).Seconds())
|
||||
if nowTTL < 0 {
|
||||
nowTTL = 0
|
||||
}
|
||||
normalizeTTL(response, uint32(nowTTL))
|
||||
return response, nowTTL, false
|
||||
}
|
||||
|
||||
func applyResponseOptions(question dns.Question, response *dns.Msg, options adapter.DNSQueryOptions) uint32 {
|
||||
if question.Qtype == dns.TypeHTTPS && (options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only) {
|
||||
for _, rr := range response.Answer {
|
||||
https, isHTTPS := rr.(*dns.HTTPS)
|
||||
if !isHTTPS {
|
||||
continue
|
||||
}
|
||||
content := https.SVCB
|
||||
content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool {
|
||||
if options.Strategy == C.DomainStrategyIPv4Only {
|
||||
return it.Key() != dns.SVCB_IPV6HINT
|
||||
}
|
||||
return it.Key() != dns.SVCB_IPV4HINT
|
||||
if !c.independentCache {
|
||||
response, loaded = c.cache.Get(question)
|
||||
} else {
|
||||
response, loaded = c.transportCache.Get(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
})
|
||||
https.SVCB = content
|
||||
}
|
||||
}
|
||||
timeToLive := computeTimeToLive(response)
|
||||
if options.RewriteTTL != nil {
|
||||
timeToLive = *options.RewriteTTL
|
||||
}
|
||||
normalizeTTL(response, timeToLive)
|
||||
return timeToLive
|
||||
}
|
||||
|
||||
func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) {
|
||||
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
|
||||
_, loaded := c.backgroundRefresh.LoadOrStore(key, struct{}{})
|
||||
if loaded {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer c.backgroundRefresh.Delete(key)
|
||||
ctx := contextWithTransportTag(c.ctx, transport.Tag())
|
||||
response, err := c.exchangeToTransport(ctx, transport, message)
|
||||
if err != nil {
|
||||
if c.logger != nil {
|
||||
c.logger.Debug("optimistic refresh failed for ", FqdnToDomain(question.Name), ": ", err)
|
||||
}
|
||||
return
|
||||
if !loaded {
|
||||
return nil, 0
|
||||
}
|
||||
if responseChecker != nil {
|
||||
var rejected bool
|
||||
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
||||
rejected = true
|
||||
return response.Copy(), 0
|
||||
} else {
|
||||
var expireAt time.Time
|
||||
if !c.independentCache {
|
||||
response, expireAt, loaded = c.cache.GetWithLifetime(question)
|
||||
} else {
|
||||
response, expireAt, loaded = c.transportCache.GetWithLifetime(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
})
|
||||
}
|
||||
if !loaded {
|
||||
return nil, 0
|
||||
}
|
||||
timeNow := time.Now()
|
||||
if timeNow.After(expireAt) {
|
||||
if !c.independentCache {
|
||||
c.cache.Remove(question)
|
||||
} else {
|
||||
rejected = !responseChecker(response)
|
||||
c.transportCache.Remove(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
})
|
||||
}
|
||||
if rejected {
|
||||
if c.rdrc != nil {
|
||||
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
|
||||
}
|
||||
return
|
||||
}
|
||||
} else if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
||||
return
|
||||
return nil, 0
|
||||
}
|
||||
timeToLive := applyResponseOptions(question, response, options)
|
||||
c.storeCache(transport, question, response, timeToLive)
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *Client) prepareExchangeMessage(message *dns.Msg, options adapter.DNSQueryOptions) *dns.Msg {
|
||||
clientSubnet := options.ClientSubnet
|
||||
if !clientSubnet.IsValid() {
|
||||
clientSubnet = c.clientSubnet
|
||||
var originTTL int
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
|
||||
originTTL = int(record.Header().Ttl)
|
||||
}
|
||||
}
|
||||
}
|
||||
nowTTL := int(expireAt.Sub(timeNow).Seconds())
|
||||
if nowTTL < 0 {
|
||||
nowTTL = 0
|
||||
}
|
||||
response = response.Copy()
|
||||
if originTTL > 0 {
|
||||
duration := uint32(originTTL - nowTTL)
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
record.Header().Ttl = record.Header().Ttl - duration
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
record.Header().Ttl = uint32(nowTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
return response, nowTTL
|
||||
}
|
||||
if clientSubnet.IsValid() {
|
||||
message = SetClientSubnet(message, clientSubnet)
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg) (*dns.Msg, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
response, err := transport.Exchange(ctx, message)
|
||||
if err == nil {
|
||||
return response, nil
|
||||
}
|
||||
var rcodeError RcodeError
|
||||
if errors.As(err, &rcodeError) {
|
||||
return FixedResponseStatus(message, int(rcodeError)), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func MessageToAddresses(response *dns.Msg) []netip.Addr {
|
||||
|
||||
@@ -22,19 +22,6 @@ func logCachedResponse(logger logger.ContextLogger, ctx context.Context, respons
|
||||
}
|
||||
}
|
||||
|
||||
func logOptimisticResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg) {
|
||||
if logger == nil || len(response.Question) == 0 {
|
||||
return
|
||||
}
|
||||
domain := FqdnToDomain(response.Question[0].Name)
|
||||
logger.DebugContext(ctx, "optimistic ", domain, " ", dns.RcodeToString[response.Rcode])
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
logger.InfoContext(ctx, "optimistic ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) {
|
||||
if logger == nil || len(response.Question) == 0 {
|
||||
return
|
||||
|
||||
@@ -51,7 +51,7 @@ type Router struct {
|
||||
closing bool
|
||||
}
|
||||
|
||||
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) (*Router, error) {
|
||||
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {
|
||||
router := &Router{
|
||||
ctx: ctx,
|
||||
logger: logFactory.NewLogger("dns"),
|
||||
@@ -61,30 +61,12 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
|
||||
rules: make([]adapter.DNSRule, 0, len(options.Rules)),
|
||||
defaultDomainStrategy: C.DomainStrategy(options.Strategy),
|
||||
}
|
||||
if options.DNSClientOptions.IndependentCache {
|
||||
deprecated.Report(ctx, deprecated.OptionIndependentDNSCache)
|
||||
}
|
||||
var optimisticTimeout time.Duration
|
||||
optimisticOptions := common.PtrValueOrDefault(options.DNSClientOptions.Optimistic)
|
||||
if optimisticOptions.Enabled {
|
||||
if options.DNSClientOptions.DisableCache {
|
||||
return nil, E.New("`optimistic` is conflict with `disable_cache`")
|
||||
}
|
||||
if options.DNSClientOptions.DisableExpire {
|
||||
return nil, E.New("`optimistic` is conflict with `disable_expire`")
|
||||
}
|
||||
optimisticTimeout = time.Duration(optimisticOptions.Timeout)
|
||||
if optimisticTimeout == 0 {
|
||||
optimisticTimeout = 3 * 24 * time.Hour
|
||||
}
|
||||
}
|
||||
router.client = NewClient(ClientOptions{
|
||||
Context: ctx,
|
||||
DisableCache: options.DNSClientOptions.DisableCache,
|
||||
DisableExpire: options.DNSClientOptions.DisableExpire,
|
||||
OptimisticTimeout: optimisticTimeout,
|
||||
CacheCapacity: options.DNSClientOptions.CacheCapacity,
|
||||
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}),
|
||||
DisableCache: options.DNSClientOptions.DisableCache,
|
||||
DisableExpire: options.DNSClientOptions.DisableExpire,
|
||||
IndependentCache: options.DNSClientOptions.IndependentCache,
|
||||
CacheCapacity: options.DNSClientOptions.CacheCapacity,
|
||||
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}),
|
||||
RDRC: func() adapter.RDRCStore {
|
||||
cacheFile := service.FromContext[adapter.CacheFile](ctx)
|
||||
if cacheFile == nil {
|
||||
@@ -95,24 +77,12 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
|
||||
}
|
||||
return cacheFile
|
||||
},
|
||||
DNSCache: func() adapter.DNSCacheStore {
|
||||
cacheFile := service.FromContext[adapter.CacheFile](ctx)
|
||||
if cacheFile == nil {
|
||||
return nil
|
||||
}
|
||||
if !cacheFile.StoreDNS() {
|
||||
return nil
|
||||
}
|
||||
cacheFile.SetDisableExpire(options.DNSClientOptions.DisableExpire)
|
||||
cacheFile.SetOptimisticTimeout(optimisticTimeout)
|
||||
return cacheFile
|
||||
},
|
||||
Logger: router.logger,
|
||||
})
|
||||
if options.ReverseMapping {
|
||||
router.dnsReverseMapping = common.Must1(freelru.NewSharded[netip.Addr, string](1024, maphash.NewHasher[netip.Addr]().Hash32))
|
||||
}
|
||||
return router, nil
|
||||
return router
|
||||
}
|
||||
|
||||
func (r *Router) Initialize(rules []option.DNSRule) error {
|
||||
@@ -349,9 +319,6 @@ func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOpt
|
||||
if routeOptions.DisableCache {
|
||||
options.DisableCache = true
|
||||
}
|
||||
if routeOptions.DisableOptimisticCache {
|
||||
options.DisableOptimisticCache = true
|
||||
}
|
||||
if routeOptions.RewriteTTL != nil {
|
||||
options.RewriteTTL = routeOptions.RewriteTTL
|
||||
}
|
||||
@@ -940,9 +907,7 @@ func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule, m
|
||||
return dnsRuleModeRequirementsInDefaultRule(router, rule.DefaultOptions, metadataOverrides)
|
||||
case C.RuleTypeLogical:
|
||||
flags := dnsRuleModeFlags{
|
||||
disabled: dnsRuleActionType(rule) == C.RuleActionTypeEvaluate ||
|
||||
dnsRuleActionType(rule) == C.RuleActionTypeRespond ||
|
||||
dnsRuleActionDisablesLegacyDNSMode(rule.LogicalOptions.DNSRuleAction),
|
||||
disabled: dnsRuleActionType(rule) == C.RuleActionTypeEvaluate || dnsRuleActionType(rule) == C.RuleActionTypeRespond,
|
||||
neededFromStrategy: dnsRuleActionHasStrategy(rule.LogicalOptions.DNSRuleAction),
|
||||
}
|
||||
flags.needed = flags.neededFromStrategy
|
||||
@@ -961,7 +926,7 @@ func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule, m
|
||||
|
||||
func dnsRuleModeRequirementsInDefaultRule(router adapter.Router, rule option.DefaultDNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) {
|
||||
flags := dnsRuleModeFlags{
|
||||
disabled: defaultRuleDisablesLegacyDNSMode(rule) || dnsRuleActionDisablesLegacyDNSMode(rule.DNSRuleAction),
|
||||
disabled: defaultRuleDisablesLegacyDNSMode(rule),
|
||||
neededFromStrategy: dnsRuleActionHasStrategy(rule.DNSRuleAction),
|
||||
}
|
||||
flags.needed = defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule) || flags.neededFromStrategy
|
||||
@@ -1098,17 +1063,6 @@ func validateLegacyDNSModeDisabledDefaultRule(rule option.DefaultDNSRule) (bool,
|
||||
return rule.MatchResponse || rule.Action == C.RuleActionTypeRespond, nil
|
||||
}
|
||||
|
||||
func dnsRuleActionDisablesLegacyDNSMode(action option.DNSRuleAction) bool {
|
||||
switch action.Action {
|
||||
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
|
||||
return action.RouteOptions.DisableOptimisticCache
|
||||
case C.RuleActionTypeRouteOptions:
|
||||
return action.RouteOptionsOptions.DisableOptimisticCache
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func dnsRuleActionHasStrategy(action option.DNSRuleAction) bool {
|
||||
switch action.Action {
|
||||
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
|
||||
|
||||
@@ -139,9 +139,9 @@ type fakeRuleSet struct {
|
||||
beforeDecrementReference func()
|
||||
}
|
||||
|
||||
func (s *fakeRuleSet) Name() string { return "fake-rule-set" }
|
||||
func (s *fakeRuleSet) StartContext(context.Context) error { return nil }
|
||||
func (s *fakeRuleSet) PostStart() error { return nil }
|
||||
func (s *fakeRuleSet) Name() string { return "fake-rule-set" }
|
||||
func (s *fakeRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error { return nil }
|
||||
func (s *fakeRuleSet) PostStart() error { return nil }
|
||||
func (s *fakeRuleSet) Metadata() adapter.RuleSetMetadata {
|
||||
s.access.Lock()
|
||||
metadata := s.metadata
|
||||
|
||||
@@ -3,18 +3,17 @@ package transport
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/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"
|
||||
@@ -45,20 +44,14 @@ type HTTPSTransport struct {
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
method string
|
||||
host string
|
||||
queryHeaders http.Header
|
||||
headers http.Header
|
||||
transportAccess sync.Mutex
|
||||
transport *httpclient.Client
|
||||
transport *HTTPSTransportWrapper
|
||||
transportResetAt time.Time
|
||||
}
|
||||
|
||||
func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
remoteOptions := option.RemoteDNSServerOptions{
|
||||
DNSServerAddressOptions: options.DNSServerAddressOptions,
|
||||
}
|
||||
remoteOptions.DialerOptions = options.DialerOptions
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, remoteOptions)
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -69,21 +62,28 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
return nil, err
|
||||
}
|
||||
if len(tlsConfig.NextProtos()) == 0 {
|
||||
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS})
|
||||
} else if !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
||||
tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, tlsConfig.NextProtos()...))
|
||||
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
|
||||
}
|
||||
headers := options.Headers.Build()
|
||||
serverAddr := options.DNSServerAddressOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 443
|
||||
}
|
||||
if !serverAddr.IsValid() {
|
||||
return nil, E.New("invalid server address: ", serverAddr)
|
||||
host := headers.Get("Host")
|
||||
if host != "" {
|
||||
headers.Del("Host")
|
||||
} else {
|
||||
if tlsConfig.ServerName() != "" {
|
||||
host = tlsConfig.ServerName()
|
||||
} else {
|
||||
host = options.Server
|
||||
}
|
||||
}
|
||||
destinationURL := url.URL{
|
||||
Scheme: "https",
|
||||
Host: doHURLHost(serverAddr, 443),
|
||||
Host: host,
|
||||
}
|
||||
if destinationURL.Host == "" {
|
||||
destinationURL.Host = options.Server
|
||||
}
|
||||
if options.ServerPort != 0 && options.ServerPort != 443 {
|
||||
destinationURL.Host = net.JoinHostPort(destinationURL.Host, strconv.Itoa(int(options.ServerPort)))
|
||||
}
|
||||
path := options.Path
|
||||
if path == "" {
|
||||
@@ -93,67 +93,41 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
method := strings.ToUpper(options.Method)
|
||||
if method == "" {
|
||||
method = http.MethodPost
|
||||
serverAddr := options.DNSServerAddressOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 443
|
||||
}
|
||||
switch method {
|
||||
case http.MethodGet, http.MethodPost:
|
||||
default:
|
||||
return nil, E.New("unsupported HTTPS DNS method: ", options.Method)
|
||||
if !serverAddr.IsValid() {
|
||||
return nil, E.New("invalid server address: ", serverAddr)
|
||||
}
|
||||
httpClientOptions := options.HTTPClientOptions
|
||||
return NewHTTPRaw(
|
||||
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, remoteOptions),
|
||||
return NewHTTPSRaw(
|
||||
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions),
|
||||
logger,
|
||||
transportDialer,
|
||||
&destinationURL,
|
||||
headers,
|
||||
serverAddr,
|
||||
tlsConfig,
|
||||
httpClientOptions,
|
||||
method,
|
||||
)
|
||||
), nil
|
||||
}
|
||||
|
||||
func NewHTTPRaw(
|
||||
func NewHTTPSRaw(
|
||||
adapter dns.TransportAdapter,
|
||||
logger logger.ContextLogger,
|
||||
logger log.ContextLogger,
|
||||
dialer N.Dialer,
|
||||
destination *url.URL,
|
||||
headers http.Header,
|
||||
serverAddr M.Socksaddr,
|
||||
tlsConfig tls.Config,
|
||||
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
|
||||
}
|
||||
) *HTTPSTransport {
|
||||
return &HTTPSTransport{
|
||||
TransportAdapter: adapter,
|
||||
logger: logger,
|
||||
dialer: dialer,
|
||||
destination: destination,
|
||||
method: method,
|
||||
host: host,
|
||||
queryHeaders: queryHeaders,
|
||||
transport: currentTransport,
|
||||
}, nil
|
||||
headers: headers,
|
||||
transport: NewHTTPSTransportWrapper(tls.NewDialer(dialer, tlsConfig), serverAddr),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
|
||||
@@ -207,25 +181,14 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
|
||||
requestBuffer.Release()
|
||||
return nil, err
|
||||
}
|
||||
requestURL := *t.destination
|
||||
var request *http.Request
|
||||
switch t.method {
|
||||
case http.MethodGet:
|
||||
query := requestURL.Query()
|
||||
query.Set("dns", base64.RawURLEncoding.EncodeToString(rawMessage))
|
||||
requestURL.RawQuery = query.Encode()
|
||||
request, err = http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), nil)
|
||||
default:
|
||||
request, err = http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), bytes.NewReader(rawMessage))
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage))
|
||||
if err != nil {
|
||||
requestBuffer.Release()
|
||||
return nil, err
|
||||
}
|
||||
request.Header = t.queryHeaders.Clone()
|
||||
if t.host != "" {
|
||||
request.Host = t.host
|
||||
}
|
||||
request.Header = t.headers.Clone()
|
||||
request.Header.Set("Content-Type", MimeType)
|
||||
request.Header.Set("Accept", MimeType)
|
||||
t.transportAccess.Lock()
|
||||
currentTransport := t.transport
|
||||
t.transportAccess.Unlock()
|
||||
@@ -259,13 +222,3 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
|
||||
}
|
||||
return &responseMessage, nil
|
||||
}
|
||||
|
||||
func doHURLHost(serverAddr M.Socksaddr, defaultPort uint16) string {
|
||||
if serverAddr.Port != defaultPort {
|
||||
return serverAddr.String()
|
||||
}
|
||||
if serverAddr.IsIPv6() {
|
||||
return "[" + serverAddr.AddrString() + "]"
|
||||
}
|
||||
return serverAddr.AddrString()
|
||||
}
|
||||
|
||||
80
dns/transport/https_transport.go
Normal file
80
dns/transport/https_transport.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var errFallback = E.New("fallback to HTTP/1.1")
|
||||
|
||||
type HTTPSTransportWrapper struct {
|
||||
http2Transport *http2.Transport
|
||||
httpTransport *http.Transport
|
||||
fallback *atomic.Bool
|
||||
}
|
||||
|
||||
func NewHTTPSTransportWrapper(dialer tls.Dialer, serverAddr M.Socksaddr) *HTTPSTransportWrapper {
|
||||
var fallback atomic.Bool
|
||||
return &HTTPSTransportWrapper{
|
||||
http2Transport: &http2.Transport{
|
||||
DialTLSContext: func(ctx context.Context, _, _ string, _ *tls.STDConfig) (net.Conn, error) {
|
||||
tlsConn, err := dialer.DialTLSContext(ctx, serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state := tlsConn.ConnectionState()
|
||||
if state.NegotiatedProtocol == http2.NextProtoTLS {
|
||||
return tlsConn, nil
|
||||
}
|
||||
tlsConn.Close()
|
||||
fallback.Store(true)
|
||||
return nil, errFallback
|
||||
},
|
||||
},
|
||||
httpTransport: &http.Transport{
|
||||
DialTLSContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return dialer.DialTLSContext(ctx, serverAddr)
|
||||
},
|
||||
},
|
||||
fallback: &fallback,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTTPSTransportWrapper) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
if h.fallback.Load() {
|
||||
return h.httpTransport.RoundTrip(request)
|
||||
} else {
|
||||
response, err := h.http2Transport.RoundTrip(request)
|
||||
if err != nil {
|
||||
if errors.Is(err, errFallback) {
|
||||
return h.httpTransport.RoundTrip(request)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTTPSTransportWrapper) CloseIdleConnections() {
|
||||
h.http2Transport.CloseIdleConnections()
|
||||
h.httpTransport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (h *HTTPSTransportWrapper) Clone() *HTTPSTransportWrapper {
|
||||
return &HTTPSTransportWrapper{
|
||||
httpTransport: h.httpTransport,
|
||||
http2Transport: &http2.Transport{
|
||||
DialTLSContext: h.http2Transport.DialTLSContext,
|
||||
},
|
||||
fallback: h.fallback,
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,8 @@ package local
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
#include <dns.h>
|
||||
#include <resolv.h>
|
||||
|
||||
static void *cgo_dns_open_super() {
|
||||
return (void *)dns_open(NULL);
|
||||
}
|
||||
|
||||
static void cgo_dns_close(void *opaque) {
|
||||
if (opaque != NULL) dns_free((dns_handle_t)opaque);
|
||||
}
|
||||
|
||||
static int cgo_dns_search(void *opaque, const char *name, int class, int type,
|
||||
unsigned char *answer, int anslen) {
|
||||
dns_handle_t handle = (dns_handle_t)opaque;
|
||||
struct sockaddr_storage from;
|
||||
uint32_t fromlen = sizeof(from);
|
||||
return dns_search(handle, name, class, type, (char *)answer, anslen, (struct sockaddr *)&from, &fromlen);
|
||||
}
|
||||
#include <netdb.h>
|
||||
|
||||
static void *cgo_res_init() {
|
||||
res_state state = calloc(1, sizeof(struct __res_state));
|
||||
@@ -68,59 +52,7 @@ import (
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const (
|
||||
darwinResolverHostNotFound = 1
|
||||
darwinResolverTryAgain = 2
|
||||
darwinResolverNoRecovery = 3
|
||||
darwinResolverNoData = 4
|
||||
|
||||
darwinResolverMaxPacketSize = 65535
|
||||
)
|
||||
|
||||
var errDarwinNeedLargerBuffer = errors.New("darwin resolver response truncated")
|
||||
|
||||
func darwinLookupSystemDNS(name string, class, qtype, timeoutSeconds int) (*mDNS.Msg, error) {
|
||||
response, err := darwinSearchWithSystemRouting(name, class, qtype)
|
||||
if err == nil {
|
||||
return response, nil
|
||||
}
|
||||
fallbackResponse, fallbackErr := darwinSearchWithResolv(name, class, qtype, timeoutSeconds)
|
||||
if fallbackErr == nil || fallbackResponse != nil {
|
||||
return fallbackResponse, fallbackErr
|
||||
}
|
||||
return nil, E.Errors(
|
||||
E.Cause(err, "dns_search"),
|
||||
E.Cause(fallbackErr, "res_nsearch"),
|
||||
)
|
||||
}
|
||||
|
||||
func darwinSearchWithSystemRouting(name string, class, qtype int) (*mDNS.Msg, error) {
|
||||
handle := C.cgo_dns_open_super()
|
||||
if handle == nil {
|
||||
return nil, E.New("dns_open failed")
|
||||
}
|
||||
defer C.cgo_dns_close(handle)
|
||||
|
||||
cName := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cName))
|
||||
|
||||
bufSize := 1232
|
||||
for {
|
||||
answer := make([]byte, bufSize)
|
||||
n := C.cgo_dns_search(handle, cName, C.int(class), C.int(qtype),
|
||||
(*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)))
|
||||
if n <= 0 {
|
||||
return nil, E.New("dns_search failed for ", name)
|
||||
}
|
||||
if int(n) > bufSize {
|
||||
bufSize = int(n)
|
||||
continue
|
||||
}
|
||||
return unpackDarwinResolverMessage(answer[:int(n)], "dns_search")
|
||||
}
|
||||
}
|
||||
|
||||
func darwinSearchWithResolv(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) {
|
||||
func resolvSearch(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) {
|
||||
state := C.cgo_res_init()
|
||||
if state == nil {
|
||||
return nil, E.New("res_ninit failed")
|
||||
@@ -129,7 +61,6 @@ func darwinSearchWithResolv(name string, class, qtype int, timeoutSeconds int) (
|
||||
|
||||
cName := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cName))
|
||||
|
||||
bufSize := 1232
|
||||
for {
|
||||
answer := make([]byte, bufSize)
|
||||
@@ -143,55 +74,37 @@ func darwinSearchWithResolv(name string, class, qtype int, timeoutSeconds int) (
|
||||
bufSize = int(n)
|
||||
continue
|
||||
}
|
||||
return unpackDarwinResolverMessage(answer[:int(n)], "res_nsearch")
|
||||
}
|
||||
response, err := handleDarwinResolvFailure(name, answer, int(hErrno))
|
||||
if err == nil {
|
||||
return response, nil
|
||||
}
|
||||
if errors.Is(err, errDarwinNeedLargerBuffer) && bufSize < darwinResolverMaxPacketSize {
|
||||
bufSize *= 2
|
||||
if bufSize > darwinResolverMaxPacketSize {
|
||||
bufSize = darwinResolverMaxPacketSize
|
||||
var response mDNS.Msg
|
||||
err := response.Unpack(answer[:int(n)])
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "unpack res_nsearch response")
|
||||
}
|
||||
continue
|
||||
return &response, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func unpackDarwinResolverMessage(packet []byte, source string) (*mDNS.Msg, error) {
|
||||
var response mDNS.Msg
|
||||
err := response.Unpack(packet)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "unpack ", source, " response")
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func handleDarwinResolvFailure(name string, answer []byte, hErrno int) (*mDNS.Msg, error) {
|
||||
response, err := unpackDarwinResolverMessage(answer, "res_nsearch failure")
|
||||
if err == nil && response.Response {
|
||||
if response.Truncated && len(answer) < darwinResolverMaxPacketSize {
|
||||
return nil, errDarwinNeedLargerBuffer
|
||||
var response mDNS.Msg
|
||||
_ = response.Unpack(answer[:bufSize])
|
||||
if response.Response {
|
||||
if response.Truncated && bufSize < 65535 {
|
||||
bufSize *= 2
|
||||
if bufSize > 65535 {
|
||||
bufSize = 65535
|
||||
}
|
||||
continue
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
switch hErrno {
|
||||
case C.HOST_NOT_FOUND:
|
||||
return nil, dns.RcodeNameError
|
||||
case C.TRY_AGAIN:
|
||||
return nil, dns.RcodeNameError
|
||||
case C.NO_RECOVERY:
|
||||
return nil, dns.RcodeServerFailure
|
||||
case C.NO_DATA:
|
||||
return nil, dns.RcodeSuccess
|
||||
default:
|
||||
return nil, E.New("res_nsearch: unknown error ", int(hErrno), " for ", name)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
return nil, darwinResolverHErrno(name, hErrno)
|
||||
}
|
||||
|
||||
func darwinResolverHErrno(name string, hErrno int) error {
|
||||
switch hErrno {
|
||||
case darwinResolverHostNotFound:
|
||||
return dns.RcodeNameError
|
||||
case darwinResolverTryAgain:
|
||||
return dns.RcodeServerFailure
|
||||
case darwinResolverNoRecovery:
|
||||
return dns.RcodeServerFailure
|
||||
case darwinResolverNoData:
|
||||
return dns.RcodeSuccess
|
||||
default:
|
||||
return E.New("res_nsearch: unknown error ", hErrno, " for ", name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,7 +141,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
|
||||
}
|
||||
resultCh := make(chan resolvResult, 1)
|
||||
go func() {
|
||||
response, err := darwinLookupSystemDNS(name, int(question.Qclass), int(question.Qtype), timeoutSeconds)
|
||||
response, err := resolvSearch(name, int(question.Qclass), int(question.Qtype), timeoutSeconds)
|
||||
resultCh <- resolvResult{response, err}
|
||||
}()
|
||||
var result resolvResult
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
@@ -41,23 +40,18 @@ func RegisterHTTP3Transport(registry *dns.TransportRegistry) {
|
||||
|
||||
type HTTP3Transport struct {
|
||||
dns.TransportAdapter
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
headers http.Header
|
||||
handshakeTimeout time.Duration
|
||||
serverAddr M.Socksaddr
|
||||
tlsConfig *tls.STDConfig
|
||||
transportAccess sync.Mutex
|
||||
transport *http3.Transport
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
headers http.Header
|
||||
serverAddr M.Socksaddr
|
||||
tlsConfig *tls.STDConfig
|
||||
transportAccess sync.Mutex
|
||||
transport *http3.Transport
|
||||
}
|
||||
|
||||
func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
remoteOptions := option.RemoteDNSServerOptions{
|
||||
DNSServerAddressOptions: options.DNSServerAddressOptions,
|
||||
}
|
||||
remoteOptions.DialerOptions = options.DialerOptions
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, remoteOptions)
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -67,7 +61,6 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handshakeTimeout := tlsConfig.HandshakeTimeout()
|
||||
stdConfig, err := tlsConfig.STDConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -109,12 +102,11 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
return nil, E.New("invalid server address: ", serverAddr)
|
||||
}
|
||||
t := &HTTP3Transport{
|
||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, remoteOptions),
|
||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
|
||||
logger: logger,
|
||||
dialer: transportDialer,
|
||||
destination: &destinationURL,
|
||||
headers: headers,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
serverAddr: serverAddr,
|
||||
tlsConfig: stdConfig,
|
||||
}
|
||||
@@ -123,17 +115,8 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
}
|
||||
|
||||
func (t *HTTP3Transport) newTransport() *http3.Transport {
|
||||
quicConfig := &quic.Config{}
|
||||
if t.handshakeTimeout > 0 {
|
||||
quicConfig.HandshakeIdleTimeout = t.handshakeTimeout
|
||||
}
|
||||
return &http3.Transport{
|
||||
QUICConfig: quicConfig,
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (*quic.Conn, error) {
|
||||
if t.handshakeTimeout > 0 && cfg.HandshakeIdleTimeout == 0 {
|
||||
cfg = cfg.Clone()
|
||||
cfg.HandshakeIdleTimeout = t.handshakeTimeout
|
||||
}
|
||||
conn, dialErr := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
|
||||
if dialErr != nil {
|
||||
return nil, dialErr
|
||||
|
||||
@@ -2,132 +2,22 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.14.0-alpha.11
|
||||
|
||||
* Add optimistic DNS cache **1**
|
||||
* Update NaiveProxy to 147.0.7727.49
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Optimistic DNS cache returns an expired cached response immediately while
|
||||
refreshing it in the background, reducing tail latency for repeated
|
||||
queries. Enabled via [`optimistic`](/configuration/dns/#optimistic)
|
||||
in DNS options, and can be persisted across restarts with the new
|
||||
[`store_dns`](/configuration/experimental/cache-file/#store_dns) cache
|
||||
file option. A per-query
|
||||
[`disable_optimistic_cache`](/configuration/dns/rule_action/#disable_optimistic_cache)
|
||||
field is also available on DNS rule actions and the `resolve` route rule
|
||||
action.
|
||||
|
||||
This deprecates the `independent_cache` DNS option (the DNS cache now
|
||||
always keys by transport) and the `store_rdrc` cache file option
|
||||
(replaced by `store_dns`); both will be removed in sing-box 1.16.0.
|
||||
See [Migration](/migration/#migrate-independent-dns-cache).
|
||||
|
||||
#### 1.14.0-alpha.10
|
||||
|
||||
* Add `evaluate` DNS rule action and Response Match Fields **1**
|
||||
* `ip_version` and `query_type` now also take effect on internal DNS lookups **2**
|
||||
* Add `package_name_regex` route, DNS and headless rule item **3**
|
||||
* Add cloudflared inbound **4**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Response Match Fields
|
||||
([`response_rcode`](/configuration/dns/rule/#response_rcode),
|
||||
[`response_answer`](/configuration/dns/rule/#response_answer),
|
||||
[`response_ns`](/configuration/dns/rule/#response_ns),
|
||||
and [`response_extra`](/configuration/dns/rule/#response_extra))
|
||||
match the evaluated DNS response. They are gated by the new
|
||||
[`match_response`](/configuration/dns/rule/#match_response) field and
|
||||
populated by a preceding
|
||||
[`evaluate`](/configuration/dns/rule_action/#evaluate) DNS rule action;
|
||||
the evaluated response can also be returned directly by a
|
||||
[`respond`](/configuration/dns/rule_action/#respond) action.
|
||||
|
||||
This deprecates the Legacy Address Filter Fields (`ip_cidr`,
|
||||
`ip_is_private` without `match_response`) in DNS rules, the Legacy
|
||||
`strategy` DNS rule action option, and the Legacy
|
||||
`rule_set_ip_cidr_accept_empty` DNS rule item; all three will be removed
|
||||
in sing-box 1.16.0.
|
||||
See [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
||||
|
||||
**2**:
|
||||
|
||||
`ip_version` and `query_type` in DNS rules, together with `query_type` in
|
||||
referenced rule-sets, now take effect on every DNS rule evaluation,
|
||||
including matches from internal domain resolutions that do not target a
|
||||
specific DNS server (for example a `resolve` route rule action without
|
||||
`server` set). In earlier versions they were silently ignored in that
|
||||
path. Combining these fields with any of the legacy DNS fields deprecated
|
||||
in **1** in the same DNS configuration is no longer supported and is
|
||||
rejected at startup.
|
||||
See [Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules).
|
||||
|
||||
**3**:
|
||||
|
||||
See [Route Rule](/configuration/route/rule/#package_name_regex),
|
||||
[DNS Rule](/configuration/dns/rule/#package_name_regex) and
|
||||
[Headless Rule](/configuration/rule-set/headless-rule/#package_name_regex).
|
||||
|
||||
**4**:
|
||||
|
||||
See [Cloudflared](/configuration/inbound/cloudflared/).
|
||||
|
||||
#### 1.13.7
|
||||
|
||||
* Fixes and improvement
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.6
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.14.0-alpha.8
|
||||
|
||||
* Add BBR profile and hop interval randomization for Hysteria2 **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
See [Hysteria2 Inbound](/configuration/inbound/hysteria2/#bbr_profile) and [Hysteria2 Outbound](/configuration/outbound/hysteria2/#bbr_profile).
|
||||
|
||||
#### 1.14.0-alpha.8
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.5
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.14.0-alpha.7
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.4
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.14.0-alpha.4
|
||||
|
||||
* Refactor ACME support to certificate provider system **1**
|
||||
* Add Cloudflare Origin CA certificate provider **2**
|
||||
* Add Tailscale certificate provider **3**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
See [Certificate Provider](/configuration/shared/certificate-provider/) and [Migration](/migration/#migrate-inline-acme-to-certificate-provider).
|
||||
|
||||
**2**:
|
||||
|
||||
See [Cloudflare Origin CA](/configuration/shared/certificate-provider/cloudflare-origin-ca).
|
||||
|
||||
**3**:
|
||||
|
||||
See [Tailscale](/configuration/shared/certificate-provider/tailscale).
|
||||
|
||||
#### 1.13.3
|
||||
|
||||
* Add OpenWrt and Alpine APK packages to release **1**
|
||||
@@ -152,59 +42,6 @@ from [SagerNet/go](https://github.com/SagerNet/go).
|
||||
|
||||
See [OCM](/configuration/service/ocm).
|
||||
|
||||
#### 1.12.24
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.14.0-alpha.2
|
||||
|
||||
* Add OpenWrt and Alpine APK packages to release **1**
|
||||
* Backport to macOS 10.13 High Sierra **2**
|
||||
* OCM service: Add WebSocket support for Responses API **3**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
|
||||
|
||||
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
|
||||
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
|
||||
|
||||
**2**:
|
||||
|
||||
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
|
||||
macOS 10.13 High Sierra, built using Go 1.25 with patches
|
||||
from [SagerNet/go](https://github.com/SagerNet/go).
|
||||
|
||||
**3**:
|
||||
|
||||
See [OCM](/configuration/service/ocm).
|
||||
|
||||
#### 1.14.0-alpha.1
|
||||
|
||||
* Add `source_mac_address` and `source_hostname` rule items **1**
|
||||
* Add `include_mac_address` and `exclude_mac_address` TUN options **2**
|
||||
* Update NaiveProxy to 145.0.7632.159 **3**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
New rule items for matching LAN devices by MAC address and hostname via neighbor resolution.
|
||||
Supported on Linux, macOS, or in graphical clients on Android and macOS.
|
||||
|
||||
See [Route Rule](/configuration/route/rule/#source_mac_address), [DNS Rule](/configuration/dns/rule/#source_mac_address) and [Neighbor Resolution](/configuration/shared/neighbor/).
|
||||
|
||||
**2**:
|
||||
|
||||
Limit or exclude devices from TUN routing by MAC address.
|
||||
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
|
||||
|
||||
See [TUN](/configuration/inbound/tun/#include_mac_address).
|
||||
|
||||
**3**:
|
||||
|
||||
This is not an official update from NaiveProxy. Instead, it's a Chromium codebase update maintained by Project S.
|
||||
|
||||
#### 1.13.2
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-delete-clock: [independent_cache](#independent_cache)
|
||||
:material-plus: [optimistic](#optimistic)
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-decagram: [servers](#servers)
|
||||
@@ -30,7 +25,6 @@ icon: material/alert-decagram
|
||||
"disable_expire": false,
|
||||
"independent_cache": false,
|
||||
"cache_capacity": 0,
|
||||
"optimistic": false, // or {}
|
||||
"reverse_mapping": false,
|
||||
"client_subnet": "",
|
||||
"fakeip": {}
|
||||
@@ -63,20 +57,12 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
||||
|
||||
Disable dns cache.
|
||||
|
||||
Conflict with `optimistic`.
|
||||
|
||||
#### disable_expire
|
||||
|
||||
Disable dns cache expire.
|
||||
|
||||
Conflict with `optimistic`.
|
||||
|
||||
#### independent_cache
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
`independent_cache` is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-independent-dns-cache).
|
||||
|
||||
Make each DNS server's cache independent for special purposes. If enabled, will slightly degrade performance.
|
||||
|
||||
#### cache_capacity
|
||||
@@ -87,34 +73,6 @@ LRU cache capacity.
|
||||
|
||||
Value less than 1024 will be ignored.
|
||||
|
||||
#### optimistic
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Enable optimistic DNS caching. When a cached DNS entry has expired but is still within the timeout window,
|
||||
the stale response is returned immediately while a background refresh is triggered.
|
||||
|
||||
Conflict with `disable_cache` and `disable_expire`.
|
||||
|
||||
Accepts a boolean or an object. When set to `true`, the default timeout of `3d` is used.
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"timeout": "3d"
|
||||
}
|
||||
```
|
||||
|
||||
##### enabled
|
||||
|
||||
Enable optimistic DNS caching.
|
||||
|
||||
##### timeout
|
||||
|
||||
The maximum time an expired cache entry can be served optimistically.
|
||||
|
||||
`3d` is used by default.
|
||||
|
||||
#### reverse_mapping
|
||||
|
||||
Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing.
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-delete-clock: [independent_cache](#independent_cache)
|
||||
:material-plus: [optimistic](#optimistic)
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-decagram: [servers](#servers)
|
||||
@@ -30,7 +25,6 @@ icon: material/alert-decagram
|
||||
"disable_expire": false,
|
||||
"independent_cache": false,
|
||||
"cache_capacity": 0,
|
||||
"optimistic": false, // or {}
|
||||
"reverse_mapping": false,
|
||||
"client_subnet": "",
|
||||
"fakeip": {}
|
||||
@@ -62,20 +56,12 @@ icon: material/alert-decagram
|
||||
|
||||
禁用 DNS 缓存。
|
||||
|
||||
与 `optimistic` 冲突。
|
||||
|
||||
#### disable_expire
|
||||
|
||||
禁用 DNS 缓存过期。
|
||||
|
||||
与 `optimistic` 冲突。
|
||||
|
||||
#### independent_cache
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
`independent_cache` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除,参阅[迁移指南](/zh/migration/#迁移-independent-dns-cache)。
|
||||
|
||||
使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。
|
||||
|
||||
#### cache_capacity
|
||||
@@ -86,34 +72,6 @@ LRU 缓存容量。
|
||||
|
||||
小于 1024 的值将被忽略。
|
||||
|
||||
#### optimistic
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
启用乐观 DNS 缓存。当缓存的 DNS 条目已过期但仍在超时窗口内时,
|
||||
立即返回过期的响应,同时在后台触发刷新。
|
||||
|
||||
与 `disable_cache` 和 `disable_expire` 冲突。
|
||||
|
||||
接受布尔值或对象。当设置为 `true` 时,使用默认超时 `3d`。
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"timeout": "3d"
|
||||
}
|
||||
```
|
||||
|
||||
##### enabled
|
||||
|
||||
启用乐观 DNS 缓存。
|
||||
|
||||
##### timeout
|
||||
|
||||
过期缓存条目可被乐观提供的最长时间。
|
||||
|
||||
默认使用 `3d`。
|
||||
|
||||
#### reverse_mapping
|
||||
|
||||
在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。
|
||||
|
||||
@@ -12,9 +12,7 @@ icon: material/alert-decagram
|
||||
:material-plus: [response_answer](#response_answer)
|
||||
:material-plus: [response_ns](#response_ns)
|
||||
:material-plus: [response_extra](#response_extra)
|
||||
:material-plus: [package_name_regex](#package_name_regex)
|
||||
:material-alert: [ip_version](#ip_version)
|
||||
:material-alert: [query_type](#query_type)
|
||||
:material-plus: [package_name_regex](#package_name_regex)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
@@ -245,46 +243,12 @@ Tags of [Inbound](/configuration/inbound/).
|
||||
|
||||
#### ip_version
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
This field now also applies when a DNS rule is matched from an internal
|
||||
domain resolution that does not target a specific DNS server, such as a
|
||||
[`resolve`](../../route/rule_action/#resolve) route rule action without a
|
||||
`server` set. In earlier versions, only DNS queries received from a
|
||||
client evaluated this field. See
|
||||
[Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules)
|
||||
for the full list.
|
||||
|
||||
Setting this field makes the DNS rule incompatible in the same DNS
|
||||
configuration with Legacy Address Filter Fields in DNS rules, the Legacy
|
||||
`strategy` DNS rule action option, and the Legacy
|
||||
`rule_set_ip_cidr_accept_empty` DNS rule item. To combine with
|
||||
address-based filtering, use the [`evaluate`](../rule_action/#evaluate)
|
||||
action and [`match_response`](#match_response).
|
||||
|
||||
4 (A DNS query) or 6 (AAAA DNS query).
|
||||
|
||||
Not limited if empty.
|
||||
|
||||
#### query_type
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
This field now also applies when a DNS rule is matched from an internal
|
||||
domain resolution that does not target a specific DNS server, such as a
|
||||
[`resolve`](../../route/rule_action/#resolve) route rule action without a
|
||||
`server` set. In earlier versions, only DNS queries received from a
|
||||
client evaluated this field. See
|
||||
[Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules)
|
||||
for the full list.
|
||||
|
||||
Setting this field makes the DNS rule incompatible in the same DNS
|
||||
configuration with Legacy Address Filter Fields in DNS rules, the Legacy
|
||||
`strategy` DNS rule action option, and the Legacy
|
||||
`rule_set_ip_cidr_accept_empty` DNS rule item. To combine with
|
||||
address-based filtering, use the [`evaluate`](../rule_action/#evaluate)
|
||||
action and [`match_response`](#match_response).
|
||||
|
||||
DNS query type. Values can be integers or type name strings.
|
||||
|
||||
#### network
|
||||
|
||||
@@ -12,9 +12,7 @@ icon: material/alert-decagram
|
||||
:material-plus: [response_answer](#response_answer)
|
||||
:material-plus: [response_ns](#response_ns)
|
||||
:material-plus: [response_extra](#response_extra)
|
||||
:material-plus: [package_name_regex](#package_name_regex)
|
||||
:material-alert: [ip_version](#ip_version)
|
||||
:material-alert: [query_type](#query_type)
|
||||
:material-plus: [package_name_regex](#package_name_regex)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
@@ -245,38 +243,12 @@ icon: material/alert-decagram
|
||||
|
||||
#### ip_version
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效,
|
||||
例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。
|
||||
此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅
|
||||
[迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。
|
||||
|
||||
在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与
|
||||
旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项,
|
||||
或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。如需与
|
||||
基于地址的筛选组合,请使用 [`evaluate`](../rule_action/#evaluate) 动作和
|
||||
[`match_response`](#match_response)。
|
||||
|
||||
4 (A DNS 查询) 或 6 (AAAA DNS 查询)。
|
||||
|
||||
默认不限制。
|
||||
|
||||
#### query_type
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效,
|
||||
例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。
|
||||
此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅
|
||||
[迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。
|
||||
|
||||
在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与
|
||||
旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项,
|
||||
或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。如需与
|
||||
基于地址的筛选组合,请使用 [`evaluate`](../rule_action/#evaluate) 动作和
|
||||
[`match_response`](#match_response)。
|
||||
|
||||
DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
#### network
|
||||
|
||||
@@ -6,8 +6,7 @@ icon: material/new-box
|
||||
|
||||
:material-delete-clock: [strategy](#strategy)
|
||||
:material-plus: [evaluate](#evaluate)
|
||||
:material-plus: [respond](#respond)
|
||||
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
|
||||
:material-plus: [respond](#respond)
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
@@ -24,7 +23,6 @@ icon: material/new-box
|
||||
"server": "",
|
||||
"strategy": "",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
@@ -54,12 +52,6 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
||||
|
||||
Disable cache and save cache in this query.
|
||||
|
||||
#### disable_optimistic_cache
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Disable optimistic DNS caching in this query.
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
Rewrite TTL in DNS responses.
|
||||
@@ -81,7 +73,6 @@ Will override `dns.client_subnet`.
|
||||
"action": "evaluate",
|
||||
"server": "",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
@@ -106,12 +97,6 @@ Tag of target server.
|
||||
|
||||
Disable cache and save cache in this query.
|
||||
|
||||
#### disable_optimistic_cache
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Disable optimistic DNS caching in this query.
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
Rewrite TTL in DNS responses.
|
||||
@@ -146,7 +131,6 @@ Only allowed after a preceding top-level `evaluate` rule. If the action is reach
|
||||
{
|
||||
"action": "route-options",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@ icon: material/new-box
|
||||
|
||||
:material-delete-clock: [strategy](#strategy)
|
||||
:material-plus: [evaluate](#evaluate)
|
||||
:material-plus: [respond](#respond)
|
||||
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
|
||||
:material-plus: [respond](#respond)
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
@@ -24,7 +23,6 @@ icon: material/new-box
|
||||
"server": "",
|
||||
"strategy": "",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
@@ -54,12 +52,6 @@ icon: material/new-box
|
||||
|
||||
在此查询中禁用缓存。
|
||||
|
||||
#### disable_optimistic_cache
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
在此查询中禁用乐观 DNS 缓存。
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
重写 DNS 回应中的 TTL。
|
||||
@@ -81,7 +73,6 @@ icon: material/new-box
|
||||
"action": "evaluate",
|
||||
"server": "",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
@@ -104,12 +95,6 @@ icon: material/new-box
|
||||
|
||||
在此查询中禁用缓存。
|
||||
|
||||
#### disable_optimistic_cache
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
在此查询中禁用乐观 DNS 缓存。
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
重写 DNS 回应中的 TTL。
|
||||
@@ -144,7 +129,6 @@ icon: material/new-box
|
||||
{
|
||||
"action": "route-options",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-alert: `headers`, `tls`, Dial Fields moved to [HTTP Client Fields](#http-client-fields)
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
# DNS over HTTP3 (DoH3)
|
||||
@@ -19,20 +15,27 @@ icon: material/new-box
|
||||
{
|
||||
"type": "h3",
|
||||
"tag": "",
|
||||
|
||||
|
||||
"server": "",
|
||||
"server_port": 0,
|
||||
|
||||
"server_port": 443,
|
||||
|
||||
"path": "",
|
||||
"method": "",
|
||||
|
||||
... // HTTP Client Fields
|
||||
"headers": {},
|
||||
|
||||
"tls": {},
|
||||
|
||||
// Dial Fields
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "Difference from legacy H3 server"
|
||||
|
||||
* The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default.
|
||||
* The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead.
|
||||
|
||||
### Fields
|
||||
|
||||
#### server
|
||||
@@ -55,14 +58,14 @@ The path of the DNS server.
|
||||
|
||||
`/dns-query` will be used by default.
|
||||
|
||||
#### method
|
||||
#### headers
|
||||
|
||||
HTTP request method.
|
||||
Additional headers to be sent to the DNS server.
|
||||
|
||||
Available values: `GET`, `POST`.
|
||||
#### tls
|
||||
|
||||
`POST` will be used by default.
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
### HTTP Client Fields
|
||||
### Dial Fields
|
||||
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-alert: `headers`、`tls`、拨号字段已移至 [HTTP 客户端字段](#http-客户端字段)
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
# DNS over HTTP3 (DoH3)
|
||||
@@ -21,18 +17,25 @@ icon: material/new-box
|
||||
"tag": "",
|
||||
|
||||
"server": "",
|
||||
"server_port": 0,
|
||||
"server_port": 443,
|
||||
|
||||
"path": "",
|
||||
"method": "",
|
||||
"headers": {},
|
||||
|
||||
... // HTTP 客户端字段
|
||||
"tls": {},
|
||||
|
||||
// 拨号字段
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "与旧版 H3 服务器的区别"
|
||||
|
||||
* 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。
|
||||
* 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。
|
||||
|
||||
### 字段
|
||||
|
||||
#### server
|
||||
@@ -55,14 +58,14 @@ DNS 服务器的路径。
|
||||
|
||||
默认使用 `/dns-query`。
|
||||
|
||||
#### method
|
||||
#### headers
|
||||
|
||||
HTTP 请求方法。
|
||||
发送到 DNS 服务器的额外标头。
|
||||
|
||||
可用值:`GET`、`POST`。
|
||||
#### tls
|
||||
|
||||
默认使用 `POST`。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### HTTP 客户端字段
|
||||
### 拨号字段
|
||||
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||
@@ -2,10 +2,6 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-alert: `headers`, `tls`, Dial Fields moved to [HTTP Client Fields](#http-client-fields)
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
# DNS over HTTPS (DoH)
|
||||
@@ -19,20 +15,27 @@ icon: material/new-box
|
||||
{
|
||||
"type": "https",
|
||||
"tag": "",
|
||||
|
||||
|
||||
"server": "",
|
||||
"server_port": 0,
|
||||
|
||||
"server_port": 443,
|
||||
|
||||
"path": "",
|
||||
"method": "",
|
||||
|
||||
... // HTTP Client Fields
|
||||
"headers": {},
|
||||
|
||||
"tls": {},
|
||||
|
||||
// Dial Fields
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "Difference from legacy HTTPS server"
|
||||
|
||||
* The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default.
|
||||
* The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead.
|
||||
|
||||
### Fields
|
||||
|
||||
#### server
|
||||
@@ -55,14 +58,14 @@ The path of the DNS server.
|
||||
|
||||
`/dns-query` will be used by default.
|
||||
|
||||
#### method
|
||||
#### headers
|
||||
|
||||
HTTP request method.
|
||||
Additional headers to be sent to the DNS server.
|
||||
|
||||
Available values: `GET`, `POST`.
|
||||
#### tls
|
||||
|
||||
`POST` will be used by default.
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
### HTTP Client Fields
|
||||
### Dial Fields
|
||||
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-alert: `headers`、`tls`、拨号字段已移至 [HTTP 客户端字段](#http-客户端字段)
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
# DNS over HTTPS (DoH)
|
||||
@@ -21,18 +17,25 @@ icon: material/new-box
|
||||
"tag": "",
|
||||
|
||||
"server": "",
|
||||
"server_port": 0,
|
||||
"server_port": 443,
|
||||
|
||||
"path": "",
|
||||
"method": "",
|
||||
"headers": {},
|
||||
|
||||
... // HTTP 客户端字段
|
||||
"tls": {},
|
||||
|
||||
// 拨号字段
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "与旧版 HTTPS 服务器的区别"
|
||||
|
||||
* 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。
|
||||
* 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。
|
||||
|
||||
### 字段
|
||||
|
||||
#### server
|
||||
@@ -55,14 +58,14 @@ DNS 服务器的路径。
|
||||
|
||||
默认使用 `/dns-query`。
|
||||
|
||||
#### method
|
||||
#### headers
|
||||
|
||||
HTTP 请求方法。
|
||||
发送到 DNS 服务器的额外标头。
|
||||
|
||||
可用值:`GET`、`POST`。
|
||||
#### tls
|
||||
|
||||
默认使用 `POST`。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### HTTP 客户端字段
|
||||
### 拨号字段
|
||||
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||
@@ -2,11 +2,6 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [control_http_client](#control_http_client)
|
||||
:material-delete-clock: [Dial Fields](#dial-fields)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [relay_server_port](#relay_server_port)
|
||||
@@ -27,7 +22,6 @@ icon: material/new-box
|
||||
"state_directory": "",
|
||||
"auth_key": "",
|
||||
"control_url": "",
|
||||
"control_http_client": {}, // or ""
|
||||
"ephemeral": false,
|
||||
"hostname": "",
|
||||
"accept_routes": false,
|
||||
@@ -154,18 +148,10 @@ UDP NAT expiration time.
|
||||
|
||||
`5m` will be used by default.
|
||||
|
||||
#### control_http_client
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
HTTP Client for connecting to the Tailscale control plane.
|
||||
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
!!! note
|
||||
|
||||
Dial Fields in Tailscale endpoints are deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0, use `control_http_client` instead.
|
||||
Dial Fields in Tailscale endpoints only control how it connects to the control plane and have nothing to do with actual connections.
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [control_http_client](#control_http_client)
|
||||
:material-delete-clock: [拨号字段](#拨号字段)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [relay_server_port](#relay_server_port)
|
||||
@@ -27,7 +22,6 @@ icon: material/new-box
|
||||
"state_directory": "",
|
||||
"auth_key": "",
|
||||
"control_url": "",
|
||||
"control_http_client": {}, // 或 ""
|
||||
"ephemeral": false,
|
||||
"hostname": "",
|
||||
"accept_routes": false,
|
||||
@@ -153,18 +147,10 @@ UDP NAT 过期时间。
|
||||
|
||||
默认使用 `5m`。
|
||||
|
||||
#### control_http_client
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
用于连接 Tailscale 控制平面的 HTTP 客户端。
|
||||
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
!!! note
|
||||
|
||||
Tailscale 端点中的拨号字段已在 sing-box 1.14.0 废弃且将在 sing-box 1.16.0 中被移除,请使用 `control_http_client` 代替。
|
||||
Tailscale 端点中的拨号字段仅控制它如何连接到控制平面,与实际连接无关。
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-delete-clock: [store_rdrc](#store_rdrc)
|
||||
:material-plus: [store_dns](#store_dns)
|
||||
|
||||
!!! quote "Changes in sing-box 1.9.0"
|
||||
|
||||
:material-plus: [store_rdrc](#store_rdrc)
|
||||
@@ -19,8 +14,7 @@
|
||||
"cache_id": "",
|
||||
"store_fakeip": false,
|
||||
"store_rdrc": false,
|
||||
"rdrc_timeout": "",
|
||||
"store_dns": false
|
||||
"rdrc_timeout": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -48,10 +42,6 @@ Store fakeip in the cache file
|
||||
|
||||
#### store_rdrc
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
`store_rdrc` is deprecated and will be removed in sing-box 1.16.0, check [Migration](/migration/#migrate-store-rdrc).
|
||||
|
||||
Store rejected DNS response cache in the cache file
|
||||
|
||||
The check results of [Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields)
|
||||
@@ -62,9 +52,3 @@ will be cached until expiration.
|
||||
Timeout of rejected DNS response cache.
|
||||
|
||||
`7d` is used by default.
|
||||
|
||||
#### store_dns
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Store DNS cache in the cache file.
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
!!! question "自 sing-box 1.8.0 起"
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-delete-clock: [store_rdrc](#store_rdrc)
|
||||
:material-plus: [store_dns](#store_dns)
|
||||
|
||||
!!! quote "sing-box 1.9.0 中的更改"
|
||||
|
||||
:material-plus: [store_rdrc](#store_rdrc)
|
||||
@@ -19,8 +14,7 @@
|
||||
"cache_id": "",
|
||||
"store_fakeip": false,
|
||||
"store_rdrc": false,
|
||||
"rdrc_timeout": "",
|
||||
"store_dns": false
|
||||
"rdrc_timeout": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -46,10 +40,6 @@
|
||||
|
||||
#### store_rdrc
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
`store_rdrc` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除,参阅[迁移指南](/zh/migration/#迁移-store_rdrc)。
|
||||
|
||||
将拒绝的 DNS 响应缓存存储在缓存文件中。
|
||||
|
||||
[旧版地址筛选字段](/zh/configuration/dns/rule/#旧版地址筛选字段) 的检查结果将被缓存至过期。
|
||||
@@ -59,9 +49,3 @@
|
||||
拒绝的 DNS 响应缓存超时。
|
||||
|
||||
默认使用 `7d`。
|
||||
|
||||
#### store_dns
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
将 DNS 缓存存储在缓存文件中。
|
||||
|
||||
@@ -21,16 +21,11 @@
|
||||
}
|
||||
],
|
||||
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
// Deprecated
|
||||
|
||||
"recv_window_conn": 0,
|
||||
"recv_window_client": 0,
|
||||
"max_conn_client": 0,
|
||||
"disable_mtu_discovery": false
|
||||
"disable_mtu_discovery": false,
|
||||
"tls": {}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -81,38 +76,32 @@ Authentication password, in base64.
|
||||
|
||||
Authentication password.
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
The QUIC stream-level flow control window for receiving data.
|
||||
|
||||
`15728640 (15 MB/s)` will be used if empty.
|
||||
|
||||
#### recv_window_client
|
||||
|
||||
The QUIC connection-level flow control window for receiving data.
|
||||
|
||||
`67108864 (64 MB/s)` will be used if empty.
|
||||
|
||||
#### max_conn_client
|
||||
|
||||
The maximum number of QUIC concurrent bidirectional streams that a peer is allowed to open.
|
||||
|
||||
`1024` will be used if empty.
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
|
||||
|
||||
Force enabled on for systems other than Linux and Windows (according to upstream).
|
||||
|
||||
#### tls
|
||||
|
||||
==Required==
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
### Deprecated Fields
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `stream_receive_window` instead.
|
||||
|
||||
#### recv_window_client
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `connection_receive_window` instead.
|
||||
|
||||
#### max_conn_client
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `max_concurrent_streams` instead.
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `disable_path_mtu_discovery` instead.
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
@@ -21,16 +21,11 @@
|
||||
}
|
||||
],
|
||||
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
// 废弃的
|
||||
|
||||
"recv_window_conn": 0,
|
||||
"recv_window_client": 0,
|
||||
"max_conn_client": 0,
|
||||
"disable_mtu_discovery": false
|
||||
"disable_mtu_discovery": false,
|
||||
"tls": {}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -81,38 +76,32 @@ base64 编码的认证密码。
|
||||
|
||||
认证密码。
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
用于接收数据的 QUIC 流级流控制窗口。
|
||||
|
||||
默认 `15728640 (15 MB/s)`。
|
||||
|
||||
#### recv_window_client
|
||||
|
||||
用于接收数据的 QUIC 连接级流控制窗口。
|
||||
|
||||
默认 `67108864 (64 MB/s)`。
|
||||
|
||||
#### max_conn_client
|
||||
|
||||
允许对等点打开的 QUIC 并发双向流的最大数量。
|
||||
|
||||
默认 `1024`。
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
禁用路径 MTU 发现 (RFC 8899)。 数据包的大小最多为 1252 (IPv4) / 1232 (IPv6) 字节。
|
||||
|
||||
强制为 Linux 和 Windows 以外的系统启用(根据上游)。
|
||||
|
||||
#### tls
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
### 废弃字段
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `stream_receive_window` 代替。
|
||||
|
||||
#### recv_window_client
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `connection_receive_window` 代替。
|
||||
|
||||
#### max_conn_client
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `max_concurrent_streams` 代替。
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `disable_path_mtu_discovery` 代替。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
@@ -34,9 +34,6 @@ icon: material/alert-decagram
|
||||
],
|
||||
"ignore_client_bandwidth": false,
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
"masquerade": "", // or {}
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false
|
||||
@@ -98,10 +95,6 @@ Deny clients to use the BBR CC.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
#### masquerade
|
||||
|
||||
HTTP3 server behavior (URL string configuration) when authentication fails.
|
||||
|
||||
@@ -34,9 +34,6 @@ icon: material/alert-decagram
|
||||
],
|
||||
"ignore_client_bandwidth": false,
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
"masquerade": "", // 或 {}
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false
|
||||
@@ -95,10 +92,6 @@ Hysteria 用户
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
#### masquerade
|
||||
|
||||
HTTP3 服务器认证失败时的行为 (URL 字符串配置)。
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
"auth_timeout": "3s",
|
||||
"zero_rtt_handshake": false,
|
||||
"heartbeat": "10s",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
"tls": {}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -77,8 +75,4 @@ Interval for sending heartbeat packets for keeping the connection alive
|
||||
|
||||
==Required==
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
@@ -18,9 +18,7 @@
|
||||
"auth_timeout": "3s",
|
||||
"zero_rtt_handshake": false,
|
||||
"heartbeat": "10s",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
"tls": {}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -77,8 +75,4 @@ QUIC 拥塞控制算法
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
@@ -10,7 +10,6 @@ sing-box uses JSON for configuration files.
|
||||
"ntp": {},
|
||||
"certificate": {},
|
||||
"certificate_providers": [],
|
||||
"http_clients": [],
|
||||
"endpoints": [],
|
||||
"inbounds": [],
|
||||
"outbounds": [],
|
||||
@@ -29,7 +28,6 @@ sing-box uses JSON for configuration files.
|
||||
| `ntp` | [NTP](./ntp/) |
|
||||
| `certificate` | [Certificate](./certificate/) |
|
||||
| `certificate_providers` | [Certificate Provider](./shared/certificate-provider/) |
|
||||
| `http_clients` | [HTTP Client](./shared/http-client/) |
|
||||
| `endpoints` | [Endpoint](./endpoint/) |
|
||||
| `inbounds` | [Inbound](./inbound/) |
|
||||
| `outbounds` | [Outbound](./outbound/) |
|
||||
|
||||
@@ -10,7 +10,6 @@ sing-box 使用 JSON 作为配置文件格式。
|
||||
"ntp": {},
|
||||
"certificate": {},
|
||||
"certificate_providers": [],
|
||||
"http_clients": [],
|
||||
"endpoints": [],
|
||||
"inbounds": [],
|
||||
"outbounds": [],
|
||||
@@ -29,7 +28,6 @@ sing-box 使用 JSON 作为配置文件格式。
|
||||
| `ntp` | [NTP](./ntp/) |
|
||||
| `certificate` | [证书](./certificate/) |
|
||||
| `certificate_providers` | [证书提供者](./shared/certificate-provider/) |
|
||||
| `http_clients` | [HTTP 客户端](./shared/http-client/) |
|
||||
| `endpoints` | [端点](./endpoint/) |
|
||||
| `inbounds` | [入站](./inbound/) |
|
||||
| `outbounds` | [出站](./outbound/) |
|
||||
|
||||
@@ -27,18 +27,13 @@ icon: material/new-box
|
||||
"obfs": "fuck me till the daylight",
|
||||
"auth": "",
|
||||
"auth_str": "password",
|
||||
"network": "",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
... // Dial Fields
|
||||
|
||||
// Deprecated
|
||||
|
||||
"recv_window_conn": 0,
|
||||
"recv_window": 0,
|
||||
"disable_mtu_discovery": false
|
||||
"disable_mtu_discovery": false,
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
```
|
||||
|
||||
@@ -109,6 +104,24 @@ Authentication password, in base64.
|
||||
|
||||
Authentication password.
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
The QUIC stream-level flow control window for receiving data.
|
||||
|
||||
`15728640 (15 MB/s)` will be used if empty.
|
||||
|
||||
#### recv_window
|
||||
|
||||
The QUIC connection-level flow control window for receiving data.
|
||||
|
||||
`67108864 (64 MB/s)` will be used if empty.
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
|
||||
|
||||
Force enabled on for systems other than Linux and Windows (according to upstream).
|
||||
|
||||
#### network
|
||||
|
||||
Enabled network
|
||||
@@ -123,30 +136,6 @@ Both is enabled by default.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
### Deprecated Fields
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `stream_receive_window` instead.
|
||||
|
||||
#### recv_window
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `connection_receive_window` instead.
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `disable_path_mtu_discovery` instead.
|
||||
|
||||
@@ -27,18 +27,13 @@ icon: material/new-box
|
||||
"obfs": "fuck me till the daylight",
|
||||
"auth": "",
|
||||
"auth_str": "password",
|
||||
"network": "",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
... // 拨号字段
|
||||
|
||||
// 废弃的
|
||||
|
||||
"recv_window_conn": 0,
|
||||
"recv_window": 0,
|
||||
"disable_mtu_discovery": false
|
||||
"disable_mtu_discovery": false,
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
```
|
||||
|
||||
@@ -109,6 +104,24 @@ base64 编码的认证密码。
|
||||
|
||||
认证密码。
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
用于接收数据的 QUIC 流级流控制窗口。
|
||||
|
||||
默认 `15728640 (15 MB/s)`。
|
||||
|
||||
#### recv_window
|
||||
|
||||
用于接收数据的 QUIC 连接级流控制窗口。
|
||||
|
||||
默认 `67108864 (64 MB/s)`。
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
禁用路径 MTU 发现 (RFC 8899)。 数据包的大小最多为 1252 (IPv4) / 1232 (IPv6) 字节。
|
||||
|
||||
强制为 Linux 和 Windows 以外的系统启用(根据上游)。
|
||||
|
||||
#### network
|
||||
|
||||
启用的网络协议。
|
||||
@@ -123,30 +136,7 @@ base64 编码的认证密码。
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||
|
||||
### 废弃字段
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `stream_receive_window` 代替。
|
||||
|
||||
#### recv_window
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `connection_receive_window` 代替。
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `disable_path_mtu_discovery` 代替。
|
||||
|
||||
@@ -31,9 +31,6 @@
|
||||
"password": "goofy_ahh_password",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false,
|
||||
|
||||
@@ -127,10 +124,6 @@ Both is enabled by default.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
#### bbr_profile
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
@@ -31,9 +31,6 @@
|
||||
"password": "goofy_ahh_password",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false,
|
||||
|
||||
@@ -125,10 +122,6 @@ QUIC 流量混淆器密码.
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
#### bbr_profile
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
"heartbeat": "10s",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
```
|
||||
@@ -93,10 +91,6 @@ Both is enabled by default.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
"heartbeat": "10s",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
```
|
||||
@@ -101,10 +99,6 @@ UDP 包中继模式
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||
|
||||
@@ -6,7 +6,6 @@ icon: material/alert-decagram
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [default_http_client](#default_http_client)
|
||||
:material-plus: [find_neighbor](#find_neighbor)
|
||||
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
|
||||
|
||||
@@ -44,7 +43,6 @@ icon: material/alert-decagram
|
||||
"find_process": false,
|
||||
"find_neighbor": false,
|
||||
"dhcp_lease_files": [],
|
||||
"default_http_client": "",
|
||||
"default_domain_resolver": "", // or {}
|
||||
"default_network_strategy": "",
|
||||
"default_network_type": [],
|
||||
@@ -149,14 +147,6 @@ Custom DHCP lease file paths for hostname and MAC address resolution.
|
||||
|
||||
Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea) if empty.
|
||||
|
||||
#### default_http_client
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Tag of the default [HTTP Client](/configuration/shared/http-client/) used by remote rule-sets.
|
||||
|
||||
If empty and `http_clients` is defined, the first HTTP client is used.
|
||||
|
||||
#### default_domain_resolver
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
@@ -6,7 +6,6 @@ icon: material/alert-decagram
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [default_http_client](#default_http_client)
|
||||
:material-plus: [find_neighbor](#find_neighbor)
|
||||
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
|
||||
|
||||
@@ -46,7 +45,6 @@ icon: material/alert-decagram
|
||||
"find_process": false,
|
||||
"find_neighbor": false,
|
||||
"dhcp_lease_files": [],
|
||||
"default_http_client": "",
|
||||
"default_network_strategy": "",
|
||||
"default_fallback_delay": ""
|
||||
}
|
||||
@@ -148,14 +146,6 @@ icon: material/alert-decagram
|
||||
|
||||
为空时自动从常见 DHCP 服务器(dnsmasq、odhcpd、ISC dhcpd、Kea)检测。
|
||||
|
||||
#### default_http_client
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
远程规则集使用的默认 [HTTP 客户端](/zh/configuration/shared/http-client/) 的标签。
|
||||
|
||||
如果为空且 `http_clients` 已定义,将使用第一个 HTTP 客户端。
|
||||
|
||||
#### default_domain_resolver
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
@@ -7,10 +7,6 @@ icon: material/new-box
|
||||
:material-plus: [bypass](#bypass)
|
||||
:material-alert: [reject](#reject)
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache)
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [tls_fragment](#tls_fragment)
|
||||
@@ -283,7 +279,6 @@ Timeout for sniffing.
|
||||
"server": "",
|
||||
"strategy": "",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
@@ -307,12 +302,6 @@ DNS resolution strategy, available values are: `prefer_ipv4`, `prefer_ipv6`, `ip
|
||||
|
||||
Disable cache and save cache in this query.
|
||||
|
||||
#### disable_optimistic_cache
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Disable optimistic DNS caching in this query.
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
@@ -7,10 +7,6 @@ icon: material/new-box
|
||||
:material-plus: [bypass](#bypass)
|
||||
:material-alert: [reject](#reject)
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache)
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [tls_fragment](#tls_fragment)
|
||||
@@ -272,7 +268,6 @@ UDP 连接超时时间。
|
||||
"server": "",
|
||||
"strategy": "",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
@@ -296,12 +291,6 @@ DNS 解析策略,可用值有:`prefer_ipv4`、`prefer_ipv6`、`ipv4_only`、
|
||||
|
||||
在此查询中禁用缓存。
|
||||
|
||||
#### disable_optimistic_cache
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
在此查询中禁用乐观 DNS 缓存。
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
@@ -4,8 +4,7 @@ icon: material/new-box
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [package_name_regex](#package_name_regex)
|
||||
:material-alert: [query_type](#query_type)
|
||||
:material-plus: [package_name_regex](#package_name_regex)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
@@ -133,20 +132,6 @@ icon: material/new-box
|
||||
|
||||
#### query_type
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
When a DNS rule references this rule-set, this field now also applies
|
||||
when the DNS rule is matched from an internal domain resolution that
|
||||
does not target a specific DNS server. In earlier versions, only DNS
|
||||
queries received from a client evaluated this field. See
|
||||
[Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules)
|
||||
for the full list.
|
||||
|
||||
When a DNS rule references a rule-set containing this field, the DNS
|
||||
rule is incompatible in the same DNS configuration with Legacy Address
|
||||
Filter Fields in DNS rules, the Legacy `strategy` DNS rule action
|
||||
option, and the Legacy `rule_set_ip_cidr_accept_empty` DNS rule item.
|
||||
|
||||
DNS query type. Values can be integers or type name strings.
|
||||
|
||||
#### network
|
||||
|
||||
@@ -4,8 +4,7 @@ icon: material/new-box
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [package_name_regex](#package_name_regex)
|
||||
:material-alert: [query_type](#query_type)
|
||||
:material-plus: [package_name_regex](#package_name_regex)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
@@ -133,17 +132,6 @@ icon: material/new-box
|
||||
|
||||
#### query_type
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
当 DNS 规则引用此规则集时,此字段现在也会在 DNS 规则被未指定具体
|
||||
DNS 服务器的内部域名解析匹配时生效。此前只有来自客户端的 DNS 查询
|
||||
才会评估此字段。完整列表参阅
|
||||
[迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。
|
||||
|
||||
当 DNS 规则引用了包含此字段的规则集时,该 DNS 规则在同一 DNS 配置中
|
||||
不能与旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项,
|
||||
或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。
|
||||
|
||||
DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
#### network
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [http_client](#http_client)
|
||||
:material-delete-clock: [download_detour](#download_detour)
|
||||
|
||||
!!! quote "Changes in sing-box 1.10.0"
|
||||
|
||||
:material-plus: `type: inline`
|
||||
@@ -48,12 +43,8 @@
|
||||
"tag": "",
|
||||
"format": "source", // or binary
|
||||
"url": "",
|
||||
"http_client": "", // or {}
|
||||
"update_interval": "",
|
||||
|
||||
// Deprecated
|
||||
|
||||
"download_detour": ""
|
||||
"download_detour": "", // optional
|
||||
"update_interval": "" // optional
|
||||
}
|
||||
```
|
||||
|
||||
@@ -111,26 +102,14 @@ File path of rule-set.
|
||||
|
||||
Download URL of rule-set.
|
||||
|
||||
#### http_client
|
||||
#### download_detour
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
Tag of the outbound to download rule-set.
|
||||
|
||||
HTTP Client for downloading rule-set.
|
||||
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
Default transport will be used if empty.
|
||||
Default outbound will be used if empty.
|
||||
|
||||
#### update_interval
|
||||
|
||||
Update interval of rule-set.
|
||||
|
||||
`1d` will be used if empty.
|
||||
|
||||
#### download_detour
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
`download_detour` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0, use `http_client` instead.
|
||||
|
||||
Tag of the outbound to download rule-set.
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [http_client](#http_client)
|
||||
:material-delete-clock: [download_detour](#download_detour)
|
||||
|
||||
!!! quote "sing-box 1.10.0 中的更改"
|
||||
|
||||
:material-plus: `type: inline`
|
||||
@@ -48,12 +43,8 @@
|
||||
"tag": "",
|
||||
"format": "source", // or binary
|
||||
"url": "",
|
||||
"http_client": "", // 或 {}
|
||||
"update_interval": "",
|
||||
|
||||
// 废弃的
|
||||
|
||||
"download_detour": ""
|
||||
"download_detour": "", // 可选
|
||||
"update_interval": "" // 可选
|
||||
}
|
||||
```
|
||||
|
||||
@@ -111,26 +102,14 @@
|
||||
|
||||
规则集的下载 URL。
|
||||
|
||||
#### http_client
|
||||
#### download_detour
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
用于下载规则集的出站的标签。
|
||||
|
||||
用于下载规则集的 HTTP 客户端。
|
||||
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
如果为空,将使用默认传输。
|
||||
如果为空,将使用默认出站。
|
||||
|
||||
#### update_interval
|
||||
|
||||
规则集的更新间隔。
|
||||
|
||||
默认使用 `1d`。
|
||||
|
||||
#### download_detour
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
`download_detour` 已在 sing-box 1.14.0 废弃且将在 sing-box 1.16.0 中被移除,请使用 `http_client` 代替。
|
||||
|
||||
用于下载规则集的出站的标签。
|
||||
|
||||
@@ -58,9 +58,9 @@ Object format:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "",
|
||||
|
||||
... // HTTP Client Fields
|
||||
"url": "https://my-headscale.com/verify",
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -58,9 +58,9 @@ Derper 配置文件路径。
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "",
|
||||
"url": "https://my-headscale.com/verify",
|
||||
|
||||
... // HTTP 客户端字段
|
||||
... // 拨号字段
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ icon: material/new-box
|
||||
|
||||
:material-plus: [account_key](#account_key)
|
||||
:material-plus: [key_type](#key_type)
|
||||
:material-plus: [http_client](#http_client)
|
||||
:material-plus: [detour](#detour)
|
||||
|
||||
# ACME
|
||||
|
||||
@@ -37,7 +37,7 @@ icon: material/new-box
|
||||
},
|
||||
"dns01_challenge": {},
|
||||
"key_type": "",
|
||||
"http_client": "" // or {}
|
||||
"detour": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -141,10 +141,10 @@ The private key type to generate for new certificates.
|
||||
| `rsa2048` | RSA |
|
||||
| `rsa4096` | RSA |
|
||||
|
||||
#### http_client
|
||||
#### detour
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
HTTP Client for all provider HTTP requests.
|
||||
The tag of the upstream outbound.
|
||||
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
All provider HTTP requests will use this outbound.
|
||||
|
||||
@@ -6,7 +6,7 @@ icon: material/new-box
|
||||
|
||||
:material-plus: [account_key](#account_key)
|
||||
:material-plus: [key_type](#key_type)
|
||||
:material-plus: [http_client](#http_client)
|
||||
:material-plus: [detour](#detour)
|
||||
|
||||
# ACME
|
||||
|
||||
@@ -37,7 +37,7 @@ icon: material/new-box
|
||||
},
|
||||
"dns01_challenge": {},
|
||||
"key_type": "",
|
||||
"http_client": "" // 或 {}
|
||||
"detour": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -136,12 +136,10 @@ ACME DNS01 质询字段。如果配置,将禁用其他质询方法。
|
||||
| `rsa2048` | RSA |
|
||||
| `rsa4096` | RSA |
|
||||
|
||||
#### http_client
|
||||
#### detour
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
用于所有提供者 HTTP 请求的 HTTP 客户端。
|
||||
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
上游出站的标签。
|
||||
|
||||
所有提供者 HTTP 请求将使用此出站。
|
||||
|
||||
@@ -19,7 +19,7 @@ icon: material/new-box
|
||||
"origin_ca_key": "",
|
||||
"request_type": "",
|
||||
"requested_validity": 0,
|
||||
"http_client": "" // or {}
|
||||
"detour": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,8 +75,8 @@ Available values: `7`, `30`, `90`, `365`, `730`, `1095`, `5475`.
|
||||
|
||||
`5475` days (15 years) is used if empty.
|
||||
|
||||
#### http_client
|
||||
#### detour
|
||||
|
||||
HTTP Client for all provider HTTP requests.
|
||||
The tag of the upstream outbound.
|
||||
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
All provider HTTP requests will use this outbound.
|
||||
|
||||
@@ -19,7 +19,7 @@ icon: material/new-box
|
||||
"origin_ca_key": "",
|
||||
"request_type": "",
|
||||
"requested_validity": 0,
|
||||
"http_client": "" // 或 {}
|
||||
"detour": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,8 +75,8 @@ Cloudflare Origin CA Key。
|
||||
|
||||
如果为空,使用 `5475` 天(15 年)。
|
||||
|
||||
#### http_client
|
||||
#### detour
|
||||
|
||||
用于所有提供者 HTTP 请求的 HTTP 客户端。
|
||||
上游出站的标签。
|
||||
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
所有提供者 HTTP 请求将使用此出站。
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
### Structure
|
||||
|
||||
A string or an object.
|
||||
|
||||
When string, the tag of a shared [HTTP Client](/configuration/shared/http-client/) defined in top-level `http_clients`.
|
||||
|
||||
When object:
|
||||
|
||||
```json
|
||||
{
|
||||
"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.
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
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/) 了解详情。
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"idle_timeout": "",
|
||||
"keep_alive_period": "",
|
||||
"stream_receive_window": "",
|
||||
"connection_receive_window": "",
|
||||
"max_concurrent_streams": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### idle_timeout
|
||||
|
||||
Idle connection timeout, in golang's Duration format.
|
||||
|
||||
#### keep_alive_period
|
||||
|
||||
Keep alive period, in golang's Duration format.
|
||||
|
||||
#### stream_receive_window
|
||||
|
||||
HTTP2 stream-level flow-control receive window size.
|
||||
|
||||
Accepts memory size format, e.g. `"64 MB"`.
|
||||
|
||||
#### connection_receive_window
|
||||
|
||||
HTTP2 connection-level flow-control receive window size.
|
||||
|
||||
Accepts memory size format, e.g. `"64 MB"`.
|
||||
|
||||
#### max_concurrent_streams
|
||||
|
||||
Maximum concurrent streams per connection.
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"idle_timeout": "",
|
||||
"keep_alive_period": "",
|
||||
"stream_receive_window": "",
|
||||
"connection_receive_window": "",
|
||||
"max_concurrent_streams": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### idle_timeout
|
||||
|
||||
空闲连接超时,采用 golang 的 Duration 格式。
|
||||
|
||||
#### keep_alive_period
|
||||
|
||||
Keep alive 周期,采用 golang 的 Duration 格式。
|
||||
|
||||
#### stream_receive_window
|
||||
|
||||
HTTP2 流级别流控接收窗口大小。
|
||||
|
||||
接受内存大小格式,例如 `"64 MB"`。
|
||||
|
||||
#### connection_receive_window
|
||||
|
||||
HTTP2 连接级别流控接收窗口大小。
|
||||
|
||||
接受内存大小格式,例如 `"64 MB"`。
|
||||
|
||||
#### max_concurrent_streams
|
||||
|
||||
每个连接的最大并发流数。
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"initial_packet_size": 0,
|
||||
"disable_path_mtu_discovery": false,
|
||||
|
||||
... // HTTP2 Fields
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### initial_packet_size
|
||||
|
||||
Initial QUIC packet size.
|
||||
|
||||
#### disable_path_mtu_discovery
|
||||
|
||||
Disable QUIC path MTU discovery.
|
||||
|
||||
### HTTP2 Fields
|
||||
|
||||
See [HTTP2 Fields](/configuration/shared/http2/) for details.
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
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,7 +5,6 @@ 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"
|
||||
@@ -55,7 +54,6 @@ icon: material/new-box
|
||||
"key_path": "",
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false,
|
||||
"handshake_timeout": "",
|
||||
"certificate_provider": "",
|
||||
|
||||
// Deprecated
|
||||
@@ -108,7 +106,6 @@ icon: material/new-box
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"engine": "",
|
||||
"disable_sni": false,
|
||||
"server_name": "",
|
||||
"insecure": false,
|
||||
@@ -127,9 +124,6 @@ icon: material/new-box
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "",
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false,
|
||||
"handshake_timeout": "",
|
||||
"ech": {
|
||||
"enabled": false,
|
||||
"config": [],
|
||||
@@ -189,49 +183,6 @@ 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==
|
||||
@@ -466,14 +417,6 @@ 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,7 +5,6 @@ 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 中的更改"
|
||||
@@ -55,7 +54,6 @@ icon: material/new-box
|
||||
"key_path": "",
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false,
|
||||
"handshake_timeout": "",
|
||||
"certificate_provider": "",
|
||||
|
||||
// 废弃的
|
||||
@@ -108,7 +106,6 @@ icon: material/new-box
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"engine": "",
|
||||
"disable_sni": false,
|
||||
"server_name": "",
|
||||
"insecure": false,
|
||||
@@ -127,9 +124,6 @@ icon: material/new-box
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "",
|
||||
"record_fragment": false,
|
||||
"kernel_tx": false,
|
||||
"kernel_rx": false,
|
||||
"handshake_timeout": "",
|
||||
"ech": {
|
||||
"enabled": false,
|
||||
"config": [],
|
||||
@@ -189,48 +183,6 @@ 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
|
||||
|
||||
==仅客户端==
|
||||
@@ -464,14 +416,6 @@ 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,27 +6,6 @@ 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
|
||||
@@ -48,21 +27,6 @@ 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,27 +6,6 @@ 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`)已废弃,
|
||||
@@ -48,21 +27,6 @@ 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,118 +137,6 @@ 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
|
||||
[`ip_version`](/configuration/dns/rule/#ip_version) and
|
||||
[`query_type`](/configuration/dns/rule/#query_type) in DNS rules, together with
|
||||
[`query_type`](/configuration/rule-set/headless-rule/#query_type) in referenced
|
||||
rule-sets, changes in two ways.
|
||||
|
||||
First, these fields now take effect on every DNS rule evaluation. In earlier
|
||||
versions they were evaluated only for DNS queries received from a client
|
||||
(for example, from a DNS inbound or intercepted by `tun`), and were silently
|
||||
ignored when a DNS rule was matched from an internal domain resolution that
|
||||
did not target a specific DNS server. Such internal resolutions include:
|
||||
|
||||
- The [`resolve`](/configuration/route/rule_action/#resolve) route rule
|
||||
action without a `server` set.
|
||||
- ICMP traffic routed to a domain destination through a `direct` outbound.
|
||||
- A [WireGuard](/configuration/endpoint/wireguard/) or
|
||||
[Tailscale](/configuration/endpoint/tailscale/) endpoint used as an
|
||||
outbound, when resolving its own destination address.
|
||||
- A [SOCKS4](/configuration/outbound/socks/) outbound, which must resolve
|
||||
the destination locally because the protocol has no in-protocol domain
|
||||
support.
|
||||
- The [DERP](/configuration/service/derp/) `bootstrap-dns` endpoint and the
|
||||
[`resolved`](/configuration/service/resolved/) service (when resolving a
|
||||
hostname or an SRV target).
|
||||
|
||||
Resolutions that target a specific DNS server — via
|
||||
[`domain_resolver`](/configuration/shared/dial/#domain_resolver) on a dial
|
||||
field, [`default_domain_resolver`](/configuration/route/#default_domain_resolver)
|
||||
in route options, or an explicit `server` on a DNS rule action or the
|
||||
`resolve` route rule action — do not go through DNS rule matching and are
|
||||
unaffected.
|
||||
|
||||
Second, setting `ip_version` or `query_type` in a DNS rule, or referencing a
|
||||
rule-set containing `query_type`, is no longer compatible in the same DNS
|
||||
configuration with Legacy Address Filter Fields in DNS rules, the Legacy
|
||||
`strategy` DNS rule action option, or the Legacy `rule_set_ip_cidr_accept_empty`
|
||||
DNS rule item. Such a configuration will be rejected at startup. To combine
|
||||
these fields with address-based filtering, migrate to response matching via
|
||||
the [`evaluate`](/configuration/dns/rule_action/#evaluate) action and
|
||||
[`match_response`](/configuration/dns/rule/#match_response), see
|
||||
[Migrate address filter fields to response matching](#migrate-address-filter-fields-to-response-matching).
|
||||
|
||||
!!! info "References"
|
||||
|
||||
[DNS Rule](/configuration/dns/rule/) /
|
||||
[Headless Rule](/configuration/rule-set/headless-rule/) /
|
||||
[Route Rule Action](/configuration/route/rule_action/#resolve)
|
||||
|
||||
## 1.12.0
|
||||
|
||||
### Migrate to new DNS server formats
|
||||
|
||||
@@ -137,109 +137,6 @@ 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 规则中的
|
||||
[`ip_version`](/zh/configuration/dns/rule/#ip_version) 和
|
||||
[`query_type`](/zh/configuration/dns/rule/#query_type),以及被引用规则集中的
|
||||
[`query_type`](/zh/configuration/rule-set/headless-rule/#query_type),
|
||||
行为有两项更改。
|
||||
|
||||
其一,这些字段现在对每一次 DNS 规则评估都会生效。此前它们仅对来自客户端的 DNS 查询
|
||||
(例如来自 DNS 入站或被 `tun` 截获的查询)生效,当 DNS 规则被未指定具体 DNS 服务器的
|
||||
内部域名解析匹配时,会被静默忽略。此类内部解析包括:
|
||||
|
||||
- 未设置 `server` 的 [`resolve`](/zh/configuration/route/rule_action/#resolve) 路由规则动作。
|
||||
- 通过 `direct` 出站路由到域名目标的 ICMP 流量。
|
||||
- 作为出站使用的 [WireGuard](/zh/configuration/endpoint/wireguard/) 或
|
||||
[Tailscale](/zh/configuration/endpoint/tailscale/) 端点在解析自身目标地址时。
|
||||
- [SOCKS4](/zh/configuration/outbound/socks/) 出站,因为协议本身不支持域名,
|
||||
必须在本地解析目标。
|
||||
- [DERP](/zh/configuration/service/derp/) 的 `bootstrap-dns` 端点,以及
|
||||
[`resolved`](/zh/configuration/service/resolved/) 服务在解析主机名或 SRV 目标时。
|
||||
|
||||
通过拨号字段中的
|
||||
[`domain_resolver`](/zh/configuration/shared/dial/#domain_resolver)、
|
||||
路由选项中的 [`default_domain_resolver`](/zh/configuration/route/#default_domain_resolver),
|
||||
或 DNS 规则动作与 `resolve` 路由规则动作上显式的 `server` 指定具体 DNS 服务器的
|
||||
解析,不会经过 DNS 规则匹配,不受此次更改影响。
|
||||
|
||||
其二,设置了 `ip_version` 或 `query_type` 的 DNS 规则,或引用了包含 `query_type` 的
|
||||
规则集的 DNS 规则,在同一 DNS 配置中不再能与旧版地址筛选字段 (DNS 规则)、旧版
|
||||
DNS 规则动作 `strategy` 选项,或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。
|
||||
此类配置将在启动时被拒绝。如需将这些字段与基于地址的筛选组合,请通过
|
||||
[`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作和
|
||||
[`match_response`](/zh/configuration/dns/rule/#match_response) 迁移到响应匹配,
|
||||
参阅 [迁移地址筛选字段到响应匹配](#迁移地址筛选字段到响应匹配)。
|
||||
|
||||
!!! info "参考"
|
||||
|
||||
[DNS 规则](/zh/configuration/dns/rule/) /
|
||||
[Headless 规则](/zh/configuration/rule-set/headless-rule/) /
|
||||
[路由规则动作](/zh/configuration/route/rule_action/#resolve)
|
||||
|
||||
## 1.12.0
|
||||
|
||||
### 迁移到新的 DNS 服务器格式
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user