Compare commits

..

4 Commits

Author SHA1 Message Date
世界
ccfdbf2d57 sing: Fix freelru GetWithLifetimeNoExpire 2026-04-13 15:59:30 +08:00
世界
9b75d28ca4 Add Network.framework TLS engine 2026-04-13 01:41:01 +08:00
世界
2e64545db4 Refactor: HTTP clients, unified HTTP2/QUIC options 2026-04-12 22:46:34 +08:00
世界
9675b0902a Bump version 2026-04-11 12:32:54 +08:00
110 changed files with 11006 additions and 8127 deletions

13
adapter/http.go Normal file
View File

@@ -0,0 +1,13 @@
package adapter
import (
"net/http"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/logger"
)
type HTTPClientManager interface {
ResolveTransport(logger logger.ContextLogger, options option.HTTPClientOptions) (http.RoundTripper, error)
DefaultTransport() http.RoundTripper
}

View File

@@ -2,17 +2,11 @@ package adapter
import (
"context"
"crypto/tls"
"net"
"net/http"
"sync"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-tun"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/common/x/list"
"go4.org/netipx"
@@ -51,7 +45,7 @@ type ConnectionRouterEx interface {
type RuleSet interface {
Name() string
StartContext(ctx context.Context, startContext *HTTPStartContext) error
StartContext(ctx context.Context) error
PostStart() error
Metadata() RuleSetMetadata
ExtractIPSet() []*netipx.IPSet
@@ -77,46 +71,3 @@ type RuleSetMetadata struct {
ContainsIPCIDRRule bool
ContainsDNSQueryTypeRule bool
}
type HTTPStartContext struct {
ctx context.Context
access sync.Mutex
httpClientCache map[string]*http.Client
}
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
return &HTTPStartContext{
ctx: ctx,
httpClientCache: make(map[string]*http.Client),
}
}
func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
c.access.Lock()
defer c.access.Unlock()
if httpClient, loaded := c.httpClientCache[detour]; loaded {
return httpClient
}
httpClient := &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: C.TCPTimeout,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
TLSClientConfig: &tls.Config{
Time: ntp.TimeFuncFromContext(c.ctx),
RootCAs: RootPoolFromContext(c.ctx),
},
},
}
c.httpClientCache[detour] = httpClient
return httpClient
}
func (c *HTTPStartContext) Close() {
c.access.Lock()
defer c.access.Unlock()
for _, client := range c.httpClientCache {
client.CloseIdleConnections()
}
}

34
box.go
View File

@@ -16,12 +16,14 @@ import (
boxService "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/common/certificate"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/httpclient"
"github.com/sagernet/sing-box/common/taskmonitor"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/cachefile"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/direct"
@@ -50,6 +52,7 @@ type Box struct {
dnsRouter *dns.Router
connection *route.ConnectionManager
router *route.Router
httpClientService adapter.LifecycleService
internalService []adapter.LifecycleService
done chan struct{}
}
@@ -169,6 +172,10 @@ func New(options Options) (*Box, error) {
}
var internalServices []adapter.LifecycleService
routeOptions := common.PtrValueOrDefault(options.Route)
httpClientManager := httpclient.NewManager(ctx, logFactory.NewLogger("httpclient"), options.HTTPClients, routeOptions.DefaultHTTPClient)
service.MustRegister[adapter.HTTPClientManager](ctx, httpClientManager)
httpClientService := adapter.LifecycleService(httpClientManager)
certificateOptions := common.PtrValueOrDefault(options.Certificate)
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
len(certificateOptions.Certificate) > 0 ||
@@ -181,8 +188,6 @@ func New(options Options) (*Box, error) {
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
internalServices = append(internalServices, certificateStore)
}
routeOptions := common.PtrValueOrDefault(options.Route)
dnsOptions := common.PtrValueOrDefault(options.DNS)
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
@@ -368,6 +373,12 @@ func New(options Options) (*Box, error) {
&option.LocalDNSServerOptions{},
)
})
httpClientManager.Initialize(func() (*httpclient.Client, error) {
deprecated.Report(ctx, deprecated.OptionImplicitDefaultHTTPClient)
var httpClientOptions option.HTTPClientOptions
httpClientOptions.DefaultOutbound = true
return httpclient.NewClient(ctx, logFactory.NewLogger("httpclient"), "", httpClientOptions)
})
if platformInterface != nil {
err = platformInterface.Initialize(networkManager)
if err != nil {
@@ -428,6 +439,7 @@ func New(options Options) (*Box, error) {
dnsRouter: dnsRouter,
connection: connectionManager,
router: router,
httpClientService: httpClientService,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
@@ -490,7 +502,15 @@ func (s *Box) preStart() error {
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection, s.router, s.dnsRouter)
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection)
if err != nil {
return err
}
err = adapter.StartNamed(s.logger, adapter.StartStateStart, []adapter.LifecycleService{s.httpClientService})
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.router, s.dnsRouter)
if err != nil {
return err
}
@@ -567,6 +587,14 @@ func (s *Box) Close() error {
})
s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
if s.httpClientService != nil {
s.logger.Trace("close ", s.httpClientService.Name())
startTime := time.Now()
err = E.Append(err, s.httpClientService.Close(), func(err error) error {
return E.Cause(err, "close ", s.httpClientService.Name())
})
s.logger.Trace("close ", s.httpClientService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
for _, lifecycleService := range s.internalService {
s.logger.Trace("close ", lifecycleService.Name())
startTime := time.Now()

View File

@@ -5,6 +5,7 @@ import (
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/sagernet/sing-box/log"
@@ -35,21 +36,9 @@ func updateMozillaIncludedRootCAs() error {
return err
}
geoIndex := slices.Index(header, "Geographic Focus")
nameIndex := slices.Index(header, "Common Name or Certificate Name")
certIndex := slices.Index(header, "PEM Info")
generated := strings.Builder{}
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
package certificate
import "crypto/x509"
var mozillaIncluded *x509.CertPool
func init() {
mozillaIncluded = x509.NewCertPool()
`)
pemBundle := strings.Builder{}
for {
record, err := reader.Read()
if err == io.EOF {
@@ -60,18 +49,12 @@ func init() {
if record[geoIndex] == "China" {
continue
}
generated.WriteString("\n // ")
generated.WriteString(record[nameIndex])
generated.WriteString("\n")
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
cert := record[certIndex]
// Remove single quotes
cert = cert[1 : len(cert)-1]
generated.WriteString(cert)
generated.WriteString("`))\n")
pemBundle.WriteString(cert)
pemBundle.WriteString("\n")
}
generated.WriteString("}\n")
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
return writeGeneratedCertificateBundle("mozilla", "mozillaIncluded", pemBundle.String())
}
func fetchChinaFingerprints() (map[string]bool, error) {
@@ -119,23 +102,11 @@ func updateChromeIncludedRootCAs() error {
if err != nil {
return err
}
subjectIndex := slices.Index(header, "Subject")
statusIndex := slices.Index(header, "Google Chrome Status")
certIndex := slices.Index(header, "X.509 Certificate (PEM)")
fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint")
generated := strings.Builder{}
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
package certificate
import "crypto/x509"
var chromeIncluded *x509.CertPool
func init() {
chromeIncluded = x509.NewCertPool()
`)
pemBundle := strings.Builder{}
for {
record, err := reader.Read()
if err == io.EOF {
@@ -149,18 +120,39 @@ func init() {
if chinaFingerprints[record[fingerprintIndex]] {
continue
}
generated.WriteString("\n // ")
generated.WriteString(record[subjectIndex])
generated.WriteString("\n")
generated.WriteString(" chromeIncluded.AppendCertsFromPEM([]byte(`")
cert := record[certIndex]
// Remove single quotes if present
if len(cert) > 0 && cert[0] == '\'' {
cert = cert[1 : len(cert)-1]
}
generated.WriteString(cert)
generated.WriteString("`))\n")
pemBundle.WriteString(cert)
pemBundle.WriteString("\n")
}
generated.WriteString("}\n")
return os.WriteFile("common/certificate/chrome.go", []byte(generated.String()), 0o644)
return writeGeneratedCertificateBundle("chrome", "chromeIncluded", pemBundle.String())
}
func writeGeneratedCertificateBundle(name string, variableName string, pemBundle string) error {
goSource := `// Code generated by 'make update_certificates'. DO NOT EDIT.
package certificate
import (
"crypto/x509"
_ "embed"
)
//go:embed ` + name + `.pem
var ` + variableName + `PEM string
var ` + variableName + ` *x509.CertPool
func init() {
` + variableName + ` = x509.NewCertPool()
` + variableName + `.AppendCertsFromPEM([]byte(` + variableName + `PEM))
}
`
err := os.WriteFile(filepath.Join("common/certificate", name+".pem"), []byte(pemBundle), 0o644)
if err != nil {
return err
}
return os.WriteFile(filepath.Join("common/certificate", name+".go"), []byte(goSource), 0o644)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -22,8 +22,10 @@ var _ adapter.CertificateStore = (*Store)(nil)
type Store struct {
access sync.RWMutex
store string
systemPool *x509.CertPool
currentPool *x509.CertPool
currentPEM []string
certificate string
certificatePaths []string
certificateDirectoryPaths []string
@@ -61,6 +63,7 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
return nil, E.New("unknown certificate store: ", options.Store)
}
store := &Store{
store: options.Store,
systemPool: systemPool,
certificate: strings.Join(options.Certificate, "\n"),
certificatePaths: options.CertificatePath,
@@ -123,19 +126,37 @@ func (s *Store) Pool() *x509.CertPool {
return s.currentPool
}
func (s *Store) StoreKind() string {
return s.store
}
func (s *Store) CurrentPEM() []string {
s.access.RLock()
defer s.access.RUnlock()
return append([]string(nil), s.currentPEM...)
}
func (s *Store) update() error {
s.access.Lock()
defer s.access.Unlock()
var currentPool *x509.CertPool
var currentPEM []string
if s.systemPool == nil {
currentPool = x509.NewCertPool()
} else {
currentPool = s.systemPool.Clone()
}
switch s.store {
case C.CertificateStoreMozilla:
currentPEM = append(currentPEM, mozillaIncludedPEM)
case C.CertificateStoreChrome:
currentPEM = append(currentPEM, chromeIncludedPEM)
}
if s.certificate != "" {
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
return E.New("invalid certificate PEM strings")
}
currentPEM = append(currentPEM, s.certificate)
}
for _, path := range s.certificatePaths {
pemContent, err := os.ReadFile(path)
@@ -145,6 +166,7 @@ func (s *Store) update() error {
if !currentPool.AppendCertsFromPEM(pemContent) {
return E.New("invalid certificate PEM file: ", path)
}
currentPEM = append(currentPEM, string(pemContent))
}
var firstErr error
for _, directoryPath := range s.certificateDirectoryPaths {
@@ -157,8 +179,8 @@ func (s *Store) update() error {
}
for _, directoryEntry := range directoryEntries {
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
if err == nil {
currentPool.AppendCertsFromPEM(pemContent)
if err == nil && currentPool.AppendCertsFromPEM(pemContent) {
currentPEM = append(currentPEM, string(pemContent))
}
}
}
@@ -166,6 +188,7 @@ func (s *Store) update() error {
return firstErr
}
s.currentPool = currentPool
s.currentPEM = currentPEM
return nil
}

View File

@@ -19,6 +19,7 @@ type DirectDialer interface {
type DetourDialer struct {
outboundManager adapter.OutboundManager
detour string
defaultOutbound bool
legacyDNSDialer bool
dialer N.Dialer
initOnce sync.Once
@@ -33,6 +34,13 @@ func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNS
}
}
func NewDefaultOutboundDetour(outboundManager adapter.OutboundManager) N.Dialer {
return &DetourDialer{
outboundManager: outboundManager,
defaultOutbound: true,
}
}
func InitializeDetour(dialer N.Dialer) error {
detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
if !isDetour {
@@ -47,12 +55,18 @@ func (d *DetourDialer) Dialer() (N.Dialer, error) {
}
func (d *DetourDialer) init() {
dialer, loaded := d.outboundManager.Outbound(d.detour)
if !loaded {
d.initErr = E.New("outbound detour not found: ", d.detour)
return
var dialer adapter.Outbound
if d.detour != "" {
var loaded bool
dialer, loaded = d.outboundManager.Outbound(d.detour)
if !loaded {
d.initErr = E.New("outbound detour not found: ", d.detour)
return
}
} else {
dialer = d.outboundManager.Default()
}
if !d.legacyDNSDialer {
if !d.defaultOutbound && !d.legacyDNSDialer {
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
if directDialer.IsEmpty() {
d.initErr = E.New("detour to an empty direct outbound makes no sense")

View File

@@ -25,6 +25,7 @@ type Options struct {
NewDialer bool
LegacyDNSDialer bool
DirectOutbound bool
DefaultOutbound bool
}
// TODO: merge with NewWithOptions
@@ -42,19 +43,26 @@ func NewWithOptions(options Options) (N.Dialer, error) {
dialer N.Dialer
err error
)
hasDetour := dialOptions.Detour != "" || options.DefaultOutbound
if dialOptions.Detour != "" {
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
if outboundManager == nil {
return nil, E.New("missing outbound manager")
}
dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
} else if options.DefaultOutbound {
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
if outboundManager == nil {
return nil, E.New("missing outbound manager")
}
dialer = NewDefaultOutboundDetour(outboundManager)
} else {
dialer, err = NewDefault(options.Context, dialOptions)
if err != nil {
return nil, err
}
}
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
if options.RemoteIsDomain && (!hasDetour || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
var defaultOptions adapter.NetworkOptions

154
common/httpclient/client.go Normal file
View File

@@ -0,0 +1,154 @@
package httpclient
import (
"context"
"io"
"net/http"
"time"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
)
type httpTransport interface {
http.RoundTripper
CloseIdleConnections()
Clone() httpTransport
}
type Client struct {
transport httpTransport
headers http.Header
host string
tag string
}
func NewClient(ctx context.Context, logger logger.ContextLogger, tag string, options option.HTTPClientOptions) (*Client, error) {
rawDialer, err := dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
RemoteIsDomain: true,
ResolverOnDetour: options.ResolveOnDetour,
NewDialer: options.ResolveOnDetour,
DefaultOutbound: options.DefaultOutbound,
})
if err != nil {
return nil, err
}
tlsOptions := common.PtrValueOrDefault(options.TLS)
tlsOptions.Enabled = true
baseTLSConfig, err := tls.NewClientWithOptions(tls.ClientOptions{
Context: ctx,
Logger: logger,
Options: tlsOptions,
AllowEmptyServerName: true,
})
if err != nil {
return nil, err
}
return NewClientWithDialer(rawDialer, baseTLSConfig, tag, options)
}
func NewClientWithDialer(rawDialer N.Dialer, baseTLSConfig tls.Config, tag string, options option.HTTPClientOptions) (*Client, error) {
headers := options.Headers.Build()
host := headers.Get("Host")
headers.Del("Host")
transport, err := newTransport(rawDialer, baseTLSConfig, options)
if err != nil {
return nil, err
}
return &Client{
transport: transport,
headers: headers,
host: host,
tag: tag,
}, nil
}
func newTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTPClientOptions) (httpTransport, error) {
version := options.Version
if version == 0 {
version = 2
}
fallbackDelay := time.Duration(options.DialerOptions.FallbackDelay)
if fallbackDelay == 0 {
fallbackDelay = 300 * time.Millisecond
}
var transport httpTransport
var err error
switch version {
case 1:
transport = newHTTP1Transport(rawDialer, baseTLSConfig)
case 2:
if options.DisableVersionFallback {
transport, err = newHTTP2Transport(rawDialer, baseTLSConfig, options.HTTP2Options)
} else {
transport, err = newHTTP2FallbackTransport(rawDialer, baseTLSConfig, options.HTTP2Options)
}
case 3:
if baseTLSConfig != nil {
_, err = baseTLSConfig.STDConfig()
if err != nil {
return nil, err
}
}
if options.DisableVersionFallback {
transport, err = newHTTP3Transport(rawDialer, baseTLSConfig, options.HTTP3Options)
} else {
var h2Fallback httpTransport
h2Fallback, err = newHTTP2FallbackTransport(rawDialer, baseTLSConfig, options.HTTP2Options)
if err != nil {
return nil, err
}
transport, err = newHTTP3FallbackTransport(rawDialer, baseTLSConfig, h2Fallback, options.HTTP3Options, fallbackDelay)
}
default:
return nil, E.New("unknown HTTP version: ", version)
}
if err != nil {
return nil, err
}
return transport, nil
}
func (c *Client) RoundTrip(request *http.Request) (*http.Response, error) {
if c.tag == "" && len(c.headers) == 0 && c.host == "" {
return c.transport.RoundTrip(request)
}
if c.tag != "" {
if transportTag, loaded := transportTagFromContext(request.Context()); loaded && transportTag == c.tag {
return nil, E.New("HTTP request loopback in transport[", c.tag, "]")
}
request = request.Clone(contextWithTransportTag(request.Context(), c.tag))
} else {
request = request.Clone(request.Context())
}
applyHeaders(request, c.headers, c.host)
return c.transport.RoundTrip(request)
}
func (c *Client) CloseIdleConnections() {
c.transport.CloseIdleConnections()
}
func (c *Client) Clone() *Client {
return &Client{
transport: c.transport.Clone(),
headers: c.headers.Clone(),
host: c.host,
tag: c.tag,
}
}
func (c *Client) Close() error {
c.CloseIdleConnections()
if closer, isCloser := c.transport.(io.Closer); isCloser {
return closer.Close()
}
return nil
}

View File

@@ -0,0 +1,14 @@
package httpclient
import "context"
type transportKey struct{}
func contextWithTransportTag(ctx context.Context, transportTag string) context.Context {
return context.WithValue(ctx, transportKey{}, transportTag)
}
func transportTagFromContext(ctx context.Context) (string, bool) {
value, loaded := ctx.Value(transportKey{}).(string)
return value, loaded
}

View File

@@ -0,0 +1,86 @@
package httpclient
import (
"context"
stdTLS "crypto/tls"
"io"
"net"
"net/http"
"strings"
"github.com/sagernet/sing-box/common/tls"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func dialTLS(ctx context.Context, rawDialer N.Dialer, baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string, expectProto string) (net.Conn, error) {
if baseTLSConfig == nil {
return nil, E.New("TLS transport unavailable")
}
tlsConfig := baseTLSConfig.Clone()
if tlsConfig.ServerName() == "" && destination.IsValid() {
tlsConfig.SetServerName(destination.AddrString())
}
tlsConfig.SetNextProtos(nextProtos)
conn, err := rawDialer.DialContext(ctx, N.NetworkTCP, destination)
if err != nil {
return nil, err
}
tlsConn, err := tls.ClientHandshake(ctx, conn, tlsConfig)
if err != nil {
conn.Close()
return nil, err
}
if expectProto != "" && tlsConn.ConnectionState().NegotiatedProtocol != expectProto {
tlsConn.Close()
return nil, errHTTP2Fallback
}
return tlsConn, nil
}
func applyHeaders(request *http.Request, headers http.Header, host string) {
for header, values := range headers {
request.Header[header] = append([]string(nil), values...)
}
if host != "" {
request.Host = host
}
}
func requestRequiresHTTP1(request *http.Request) bool {
return strings.Contains(strings.ToLower(request.Header.Get("Connection")), "upgrade") &&
strings.EqualFold(request.Header.Get("Upgrade"), "websocket")
}
func requestReplayable(request *http.Request) bool {
return request.Body == nil || request.Body == http.NoBody || request.GetBody != nil
}
func cloneRequestForRetry(request *http.Request) *http.Request {
cloned := request.Clone(request.Context())
if request.Body != nil && request.Body != http.NoBody && request.GetBody != nil {
cloned.Body = mustGetBody(request)
}
return cloned
}
func mustGetBody(request *http.Request) io.ReadCloser {
body, err := request.GetBody()
if err != nil {
panic(err)
}
return body
}
func buildSTDTLSConfig(baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string) (*stdTLS.Config, error) {
if baseTLSConfig == nil {
return nil, nil
}
tlsConfig := baseTLSConfig.Clone()
if tlsConfig.ServerName() == "" && destination.IsValid() {
tlsConfig.SetServerName(destination.AddrString())
}
tlsConfig.SetNextProtos(nextProtos)
return tlsConfig.STDConfig()
}

View File

@@ -0,0 +1,41 @@
package httpclient
import (
"context"
"net"
"net/http"
"github.com/sagernet/sing-box/common/tls"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type http1Transport struct {
transport *http.Transport
}
func newHTTP1Transport(rawDialer N.Dialer, baseTLSConfig tls.Config) *http1Transport {
transport := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return rawDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
}
if baseTLSConfig != nil {
transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{"http/1.1"}, "")
}
}
return &http1Transport{transport: transport}
}
func (t *http1Transport) RoundTrip(request *http.Request) (*http.Response, error) {
return t.transport.RoundTrip(request)
}
func (t *http1Transport) CloseIdleConnections() {
t.transport.CloseIdleConnections()
}
func (t *http1Transport) Clone() httpTransport {
return &http1Transport{transport: t.transport.Clone()}
}

View File

@@ -0,0 +1,42 @@
package httpclient
import (
stdTLS "crypto/tls"
"net/http"
"time"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/net/http2"
)
func CloneHTTP2Transport(transport *http2.Transport) *http2.Transport {
return &http2.Transport{
ReadIdleTimeout: transport.ReadIdleTimeout,
PingTimeout: transport.PingTimeout,
DialTLSContext: transport.DialTLSContext,
}
}
func ConfigureHTTP2Transport(options option.HTTP2Options) (*http2.Transport, error) {
stdTransport := &http.Transport{
TLSClientConfig: &stdTLS.Config{},
HTTP2: &http.HTTP2Config{
MaxReceiveBufferPerStream: int(options.StreamReceiveWindow.Value()),
MaxReceiveBufferPerConnection: int(options.ConnectionReceiveWindow.Value()),
MaxConcurrentStreams: options.MaxConcurrentStreams,
SendPingTimeout: time.Duration(options.KeepAlivePeriod),
PingTimeout: time.Duration(options.IdleTimeout),
},
}
h2Transport, err := http2.ConfigureTransports(stdTransport)
if err != nil {
return nil, E.Cause(err, "configure HTTP/2 transport")
}
// ConfigureTransports binds ConnPool to the throwaway http.Transport; sever it so DialTLSContext is used directly.
h2Transport.ConnPool = nil
h2Transport.ReadIdleTimeout = time.Duration(options.KeepAlivePeriod)
h2Transport.PingTimeout = time.Duration(options.IdleTimeout)
return h2Transport, nil
}

View File

@@ -0,0 +1,87 @@
package httpclient
import (
"context"
stdTLS "crypto/tls"
"errors"
"net"
"net/http"
"sync/atomic"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"golang.org/x/net/http2"
)
var errHTTP2Fallback = E.New("fallback to HTTP/1.1")
type http2FallbackTransport struct {
h2Transport *http2.Transport
h1Transport *http1Transport
h2Fallback *atomic.Bool
}
func newHTTP2FallbackTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTP2Options) (*http2FallbackTransport, error) {
h1 := newHTTP1Transport(rawDialer, baseTLSConfig)
var fallback atomic.Bool
h2Transport, err := ConfigureHTTP2Transport(options)
if err != nil {
return nil, err
}
h2Transport.DialTLSContext = func(ctx context.Context, network, addr string, _ *stdTLS.Config) (net.Conn, error) {
conn, dialErr := dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{http2.NextProtoTLS, "http/1.1"}, http2.NextProtoTLS)
if dialErr != nil {
if errors.Is(dialErr, errHTTP2Fallback) {
fallback.Store(true)
}
return nil, dialErr
}
return conn, nil
}
return &http2FallbackTransport{
h2Transport: h2Transport,
h1Transport: h1,
h2Fallback: &fallback,
}, nil
}
func (t *http2FallbackTransport) RoundTrip(request *http.Request) (*http.Response, error) {
return t.roundTrip(request, true)
}
func (t *http2FallbackTransport) roundTrip(request *http.Request, allowHTTP1Fallback bool) (*http.Response, error) {
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
return t.h1Transport.RoundTrip(request)
}
if t.h2Fallback.Load() {
if !allowHTTP1Fallback {
return nil, errHTTP2Fallback
}
return t.h1Transport.RoundTrip(request)
}
response, err := t.h2Transport.RoundTrip(request)
if err == nil {
return response, nil
}
if !errors.Is(err, errHTTP2Fallback) || !allowHTTP1Fallback {
return nil, err
}
return t.h1Transport.RoundTrip(cloneRequestForRetry(request))
}
func (t *http2FallbackTransport) CloseIdleConnections() {
t.h1Transport.CloseIdleConnections()
t.h2Transport.CloseIdleConnections()
}
func (t *http2FallbackTransport) Clone() httpTransport {
return &http2FallbackTransport{
h2Transport: CloneHTTP2Transport(t.h2Transport),
h1Transport: t.h1Transport.Clone().(*http1Transport),
h2Fallback: t.h2Fallback,
}
}

View File

@@ -0,0 +1,54 @@
package httpclient
import (
"context"
stdTLS "crypto/tls"
"net"
"net/http"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/option"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"golang.org/x/net/http2"
)
type http2Transport struct {
h2Transport *http2.Transport
h1Transport *http1Transport
}
func newHTTP2Transport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTP2Options) (*http2Transport, error) {
h1 := newHTTP1Transport(rawDialer, baseTLSConfig)
h2Transport, err := ConfigureHTTP2Transport(options)
if err != nil {
return nil, err
}
h2Transport.DialTLSContext = func(ctx context.Context, network, addr string, _ *stdTLS.Config) (net.Conn, error) {
return dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{http2.NextProtoTLS}, http2.NextProtoTLS)
}
return &http2Transport{
h2Transport: h2Transport,
h1Transport: h1,
}, nil
}
func (t *http2Transport) RoundTrip(request *http.Request) (*http.Response, error) {
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
return t.h1Transport.RoundTrip(request)
}
return t.h2Transport.RoundTrip(request)
}
func (t *http2Transport) CloseIdleConnections() {
t.h1Transport.CloseIdleConnections()
t.h2Transport.CloseIdleConnections()
}
func (t *http2Transport) Clone() httpTransport {
return &http2Transport{
h2Transport: CloneHTTP2Transport(t.h2Transport),
h1Transport: t.h1Transport.Clone().(*http1Transport),
}
}

View File

@@ -0,0 +1,311 @@
//go:build with_quic
package httpclient
import (
"context"
stdTLS "crypto/tls"
"errors"
"net/http"
"sync"
"time"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type http3Transport struct {
h3Transport *http3.Transport
}
type http3FallbackTransport struct {
h3Transport *http3.Transport
h2Fallback httpTransport
fallbackDelay time.Duration
brokenAccess sync.Mutex
brokenUntil time.Time
brokenBackoff time.Duration
}
func newHTTP3RoundTripper(
rawDialer N.Dialer,
baseTLSConfig tls.Config,
options option.QUICOptions,
) *http3.Transport {
var handshakeTimeout time.Duration
if baseTLSConfig != nil {
handshakeTimeout = baseTLSConfig.HandshakeTimeout()
}
quicConfig := &quic.Config{
InitialStreamReceiveWindow: options.StreamReceiveWindow.Value(),
MaxStreamReceiveWindow: options.StreamReceiveWindow.Value(),
InitialConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
MaxConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
KeepAlivePeriod: time.Duration(options.KeepAlivePeriod),
MaxIdleTimeout: time.Duration(options.IdleTimeout),
DisablePathMTUDiscovery: options.DisablePathMTUDiscovery,
}
if options.InitialPacketSize > 0 {
quicConfig.InitialPacketSize = uint16(options.InitialPacketSize)
}
if options.MaxConcurrentStreams > 0 {
quicConfig.MaxIncomingStreams = int64(options.MaxConcurrentStreams)
}
if handshakeTimeout > 0 {
quicConfig.HandshakeIdleTimeout = handshakeTimeout
}
h3Transport := &http3.Transport{
TLSClientConfig: &stdTLS.Config{},
QUICConfig: quicConfig,
Dial: func(ctx context.Context, addr string, tlsConfig *stdTLS.Config, quicConfig *quic.Config) (*quic.Conn, error) {
if handshakeTimeout > 0 && quicConfig.HandshakeIdleTimeout == 0 {
quicConfig = quicConfig.Clone()
quicConfig.HandshakeIdleTimeout = handshakeTimeout
}
if baseTLSConfig != nil {
var err error
tlsConfig, err = buildSTDTLSConfig(baseTLSConfig, M.ParseSocksaddr(addr), []string{http3.NextProtoH3})
if err != nil {
return nil, err
}
} else {
tlsConfig = tlsConfig.Clone()
tlsConfig.NextProtos = []string{http3.NextProtoH3}
}
conn, err := rawDialer.DialContext(ctx, N.NetworkUDP, M.ParseSocksaddr(addr))
if err != nil {
return nil, err
}
quicConn, err := quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsConfig, quicConfig)
if err != nil {
conn.Close()
return nil, err
}
return quicConn, nil
},
}
return h3Transport
}
func newHTTP3Transport(
rawDialer N.Dialer,
baseTLSConfig tls.Config,
options option.QUICOptions,
) (httpTransport, error) {
return &http3Transport{
h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options),
}, nil
}
func newHTTP3FallbackTransport(
rawDialer N.Dialer,
baseTLSConfig tls.Config,
h2Fallback httpTransport,
options option.QUICOptions,
fallbackDelay time.Duration,
) (httpTransport, error) {
return &http3FallbackTransport{
h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options),
h2Fallback: h2Fallback,
fallbackDelay: fallbackDelay,
}, nil
}
func (t *http3Transport) RoundTrip(request *http.Request) (*http.Response, error) {
return t.h3Transport.RoundTrip(request)
}
func (t *http3Transport) CloseIdleConnections() {
t.h3Transport.CloseIdleConnections()
}
func (t *http3Transport) Close() error {
t.CloseIdleConnections()
return t.h3Transport.Close()
}
func (t *http3Transport) Clone() httpTransport {
return &http3Transport{
h3Transport: t.h3Transport,
}
}
func (t *http3FallbackTransport) RoundTrip(request *http.Request) (*http.Response, error) {
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
return t.h2Fallback.RoundTrip(request)
}
return t.roundTripHTTP3(request)
}
func (t *http3FallbackTransport) roundTripHTTP3(request *http.Request) (*http.Response, error) {
if t.h3Broken() {
return t.h2FallbackRoundTrip(request)
}
response, err := t.h3Transport.RoundTripOpt(request, http3.RoundTripOpt{OnlyCachedConn: true})
if err == nil {
t.clearH3Broken()
return response, nil
}
if !errors.Is(err, http3.ErrNoCachedConn) {
t.markH3Broken()
return t.h2FallbackRoundTrip(cloneRequestForRetry(request))
}
if !requestReplayable(request) {
response, err = t.h3Transport.RoundTrip(request)
if err == nil {
t.clearH3Broken()
return response, nil
}
t.markH3Broken()
return nil, err
}
return t.roundTripHTTP3Race(request)
}
func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request) (*http.Response, error) {
ctx, cancel := context.WithCancel(request.Context())
defer cancel()
type result struct {
response *http.Response
err error
h3 bool
}
results := make(chan result, 2)
startRoundTrip := func(request *http.Request, useH3 bool) {
request = request.WithContext(ctx)
var (
response *http.Response
err error
)
if useH3 {
response, err = t.h3Transport.RoundTrip(request)
} else {
response, err = t.h2FallbackRoundTrip(request)
}
results <- result{response: response, err: err, h3: useH3}
}
goroutines := 1
received := 0
drainRemaining := func() {
cancel()
for range goroutines - received {
go func() {
loser := <-results
if loser.response != nil && loser.response.Body != nil {
loser.response.Body.Close()
}
}()
}
}
go startRoundTrip(cloneRequestForRetry(request), true)
timer := time.NewTimer(t.fallbackDelay)
defer timer.Stop()
var (
h3Err error
fallbackErr error
)
for {
select {
case <-timer.C:
if goroutines == 1 {
goroutines++
go startRoundTrip(cloneRequestForRetry(request), false)
}
case raceResult := <-results:
received++
if raceResult.err == nil {
if raceResult.h3 {
t.clearH3Broken()
}
drainRemaining()
return raceResult.response, nil
}
if raceResult.h3 {
t.markH3Broken()
h3Err = raceResult.err
if goroutines == 1 {
goroutines++
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
go startRoundTrip(cloneRequestForRetry(request), false)
}
} else {
fallbackErr = raceResult.err
}
if received < goroutines {
continue
}
drainRemaining()
switch {
case h3Err != nil && fallbackErr != nil:
return nil, E.Errors(h3Err, fallbackErr)
case fallbackErr != nil:
return nil, fallbackErr
default:
return nil, h3Err
}
}
}
}
func (t *http3FallbackTransport) h2FallbackRoundTrip(request *http.Request) (*http.Response, error) {
if fallback, isFallback := t.h2Fallback.(*http2FallbackTransport); isFallback {
return fallback.roundTrip(request, true)
}
return t.h2Fallback.RoundTrip(request)
}
func (t *http3FallbackTransport) CloseIdleConnections() {
t.h3Transport.CloseIdleConnections()
t.h2Fallback.CloseIdleConnections()
}
func (t *http3FallbackTransport) Close() error {
t.CloseIdleConnections()
return t.h3Transport.Close()
}
func (t *http3FallbackTransport) Clone() httpTransport {
return &http3FallbackTransport{
h3Transport: t.h3Transport,
h2Fallback: t.h2Fallback.Clone(),
fallbackDelay: t.fallbackDelay,
}
}
func (t *http3FallbackTransport) h3Broken() bool {
t.brokenAccess.Lock()
defer t.brokenAccess.Unlock()
return !t.brokenUntil.IsZero() && time.Now().Before(t.brokenUntil)
}
func (t *http3FallbackTransport) clearH3Broken() {
t.brokenAccess.Lock()
t.brokenUntil = time.Time{}
t.brokenBackoff = 0
t.brokenAccess.Unlock()
}
func (t *http3FallbackTransport) markH3Broken() {
t.brokenAccess.Lock()
defer t.brokenAccess.Unlock()
if t.brokenBackoff == 0 {
t.brokenBackoff = 5 * time.Minute
} else {
t.brokenBackoff *= 2
if t.brokenBackoff > 48*time.Hour {
t.brokenBackoff = 48 * time.Hour
}
}
t.brokenUntil = time.Now().Add(t.brokenBackoff)
}

View File

@@ -0,0 +1,30 @@
//go:build !with_quic
package httpclient
import (
"time"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
)
func newHTTP3FallbackTransport(
rawDialer N.Dialer,
baseTLSConfig tls.Config,
h2Fallback httpTransport,
options option.QUICOptions,
fallbackDelay time.Duration,
) (httpTransport, error) {
return nil, E.New("HTTP/3 requires building with the with_quic tag")
}
func newHTTP3Transport(
rawDialer N.Dialer,
baseTLSConfig tls.Config,
options option.QUICOptions,
) (httpTransport, error) {
return nil, E.New("HTTP/3 requires building with the with_quic tag")
}

View File

@@ -0,0 +1,136 @@
package httpclient
import (
"context"
"net/http"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
var (
_ adapter.HTTPClientManager = (*Manager)(nil)
_ adapter.LifecycleService = (*Manager)(nil)
)
type Manager struct {
ctx context.Context
logger log.ContextLogger
access sync.Mutex
defines map[string]option.HTTPClient
clients map[string]*Client
defaultTag string
defaultTransport http.RoundTripper
defaultTransportFallback func() (*Client, error)
fallbackClient *Client
}
func NewManager(ctx context.Context, logger log.ContextLogger, clients []option.HTTPClient, defaultHTTPClient string) *Manager {
defines := make(map[string]option.HTTPClient, len(clients))
for _, client := range clients {
defines[client.Tag] = client
}
defaultTag := defaultHTTPClient
if defaultTag == "" && len(clients) > 0 {
defaultTag = clients[0].Tag
}
return &Manager{
ctx: ctx,
logger: logger,
defines: defines,
clients: make(map[string]*Client),
defaultTag: defaultTag,
}
}
func (m *Manager) Initialize(defaultTransportFallback func() (*Client, error)) {
m.defaultTransportFallback = defaultTransportFallback
}
func (m *Manager) Name() string {
return "http-client"
}
func (m *Manager) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
if m.defaultTag != "" {
transport, err := m.resolveShared(m.defaultTag)
if err != nil {
return E.Cause(err, "resolve default http client")
}
m.defaultTransport = transport
} else if m.defaultTransportFallback != nil {
client, err := m.defaultTransportFallback()
if err != nil {
return E.Cause(err, "create default http client")
}
m.defaultTransport = client
m.fallbackClient = client
}
return nil
}
func (m *Manager) DefaultTransport() http.RoundTripper {
return m.defaultTransport
}
func (m *Manager) ResolveTransport(logger logger.ContextLogger, options option.HTTPClientOptions) (http.RoundTripper, error) {
if options.Tag != "" {
if options.ResolveOnDetour {
define, loaded := m.defines[options.Tag]
if !loaded {
return nil, E.New("http_client not found: ", options.Tag)
}
resolvedOptions := define.Options()
resolvedOptions.ResolveOnDetour = true
return NewClient(m.ctx, logger, options.Tag, resolvedOptions)
}
return m.resolveShared(options.Tag)
}
return NewClient(m.ctx, logger, "", options)
}
func (m *Manager) resolveShared(tag string) (http.RoundTripper, error) {
m.access.Lock()
defer m.access.Unlock()
if client, loaded := m.clients[tag]; loaded {
return client, nil
}
define, loaded := m.defines[tag]
if !loaded {
return nil, E.New("http_client not found: ", tag)
}
client, err := NewClient(m.ctx, m.logger, tag, define.Options())
if err != nil {
return nil, E.Cause(err, "create shared http_client[", tag, "]")
}
m.clients[tag] = client
return client, nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if m.clients == nil {
return nil
}
var err error
for _, client := range m.clients {
err = E.Append(err, client.Close(), func(err error) error {
return E.Cause(err, "close http client")
})
}
if m.fallbackClient != nil {
err = E.Append(err, m.fallbackClient.Close(), func(err error) error {
return E.Cause(err, "close default http client")
})
}
m.clients = nil
return err
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,205 @@
//go:build darwin && cgo
package tls
import (
"context"
stdtls "crypto/tls"
"net"
"testing"
"time"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json/badoption"
"github.com/sagernet/sing/common/logger"
)
const appleTLSTestTimeout = 5 * time.Second
type appleTLSServerResult struct {
state stdtls.ConnectionState
err error
}
func TestAppleClientHandshakeAppliesALPNAndVersion(t *testing.T) {
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
Certificates: []stdtls.Certificate{serverCertificate},
MinVersion: stdtls.VersionTLS12,
MaxVersion: stdtls.VersionTLS12,
NextProtos: []string{"h2"},
})
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
Enabled: true,
Engine: "apple",
ServerName: "localhost",
MinVersion: "1.2",
MaxVersion: "1.2",
ALPN: badoption.Listable[string]{"h2"},
Certificate: badoption.Listable[string]{serverCertificatePEM},
})
if err != nil {
t.Fatal(err)
}
defer clientConn.Close()
clientState := clientConn.ConnectionState()
if clientState.Version != stdtls.VersionTLS12 {
t.Fatalf("unexpected negotiated version: %x", clientState.Version)
}
if clientState.NegotiatedProtocol != "h2" {
t.Fatalf("unexpected negotiated protocol: %q", clientState.NegotiatedProtocol)
}
result := <-serverResult
if result.err != nil {
t.Fatal(result.err)
}
if result.state.Version != stdtls.VersionTLS12 {
t.Fatalf("server negotiated unexpected version: %x", result.state.Version)
}
if result.state.NegotiatedProtocol != "h2" {
t.Fatalf("server negotiated unexpected protocol: %q", result.state.NegotiatedProtocol)
}
}
func TestAppleClientHandshakeRejectsVersionMismatch(t *testing.T) {
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
Certificates: []stdtls.Certificate{serverCertificate},
MinVersion: stdtls.VersionTLS13,
MaxVersion: stdtls.VersionTLS13,
})
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
Enabled: true,
Engine: "apple",
ServerName: "localhost",
MaxVersion: "1.2",
Certificate: badoption.Listable[string]{serverCertificatePEM},
})
if err == nil {
clientConn.Close()
t.Fatal("expected version mismatch handshake to fail")
}
if result := <-serverResult; result.err == nil {
t.Fatal("expected server handshake to fail on version mismatch")
}
}
func TestAppleClientHandshakeRejectsServerNameMismatch(t *testing.T) {
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
Certificates: []stdtls.Certificate{serverCertificate},
})
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
Enabled: true,
Engine: "apple",
ServerName: "example.com",
Certificate: badoption.Listable[string]{serverCertificatePEM},
})
if err == nil {
clientConn.Close()
t.Fatal("expected server name mismatch handshake to fail")
}
if result := <-serverResult; result.err == nil {
t.Fatal("expected server handshake to fail on server name mismatch")
}
}
func newAppleTestCertificate(t *testing.T, serverName string) (stdtls.Certificate, string) {
t.Helper()
privateKeyPEM, certificatePEM, err := GenerateCertificate(nil, nil, time.Now, serverName, time.Now().Add(time.Hour))
if err != nil {
t.Fatal(err)
}
certificate, err := stdtls.X509KeyPair(certificatePEM, privateKeyPEM)
if err != nil {
t.Fatal(err)
}
return certificate, string(certificatePEM)
}
func startAppleTLSTestServer(t *testing.T, tlsConfig *stdtls.Config) (<-chan appleTLSServerResult, string) {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
listener.Close()
})
if tcpListener, isTCP := listener.(*net.TCPListener); isTCP {
err = tcpListener.SetDeadline(time.Now().Add(appleTLSTestTimeout))
if err != nil {
t.Fatal(err)
}
}
result := make(chan appleTLSServerResult, 1)
go func() {
defer close(result)
conn, err := listener.Accept()
if err != nil {
result <- appleTLSServerResult{err: err}
return
}
defer conn.Close()
err = conn.SetDeadline(time.Now().Add(appleTLSTestTimeout))
if err != nil {
result <- appleTLSServerResult{err: err}
return
}
tlsConn := stdtls.Server(conn, tlsConfig)
defer tlsConn.Close()
err = tlsConn.Handshake()
if err != nil {
result <- appleTLSServerResult{err: err}
return
}
result <- appleTLSServerResult{state: tlsConn.ConnectionState()}
}()
return result, listener.Addr().String()
}
func newAppleTestClientConn(t *testing.T, serverAddress string, options option.OutboundTLSOptions) (Conn, error) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), appleTLSTestTimeout)
t.Cleanup(cancel)
clientConfig, err := NewClientWithOptions(ClientOptions{
Context: ctx,
Logger: logger.NOP(),
ServerAddress: "",
Options: options,
})
if err != nil {
return nil, err
}
conn, err := net.DialTimeout("tcp", serverAddress, appleTLSTestTimeout)
if err != nil {
return nil, err
}
tlsConn, err := ClientHandshake(ctx, conn, clientConfig)
if err != nil {
conn.Close()
return nil, err
}
return tlsConn, nil
}

View File

@@ -0,0 +1,15 @@
//go:build !darwin || !cgo
package tls
import (
"context"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
func newAppleClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
return nil, E.New("Apple TLS engine is not available on non-Apple platforms")
}

View File

@@ -8,14 +8,16 @@ import (
"os"
"github.com/sagernet/sing-box/common/badtls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls"
)
var errMissingServerName = E.New("missing server_name or insecure=true")
func NewDialerFromOptions(ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
if !options.Enabled {
return dialer, nil
@@ -42,11 +44,12 @@ func NewClient(ctx context.Context, logger logger.ContextLogger, serverAddress s
}
type ClientOptions struct {
Context context.Context
Logger logger.ContextLogger
ServerAddress string
Options option.OutboundTLSOptions
KTLSCompatible bool
Context context.Context
Logger logger.ContextLogger
ServerAddress string
Options option.OutboundTLSOptions
AllowEmptyServerName bool
KTLSCompatible bool
}
func NewClientWithOptions(options ClientOptions) (Config, error) {
@@ -61,17 +64,22 @@ func NewClientWithOptions(options ClientOptions) (Config, error) {
if options.Options.KernelRx {
options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx")
}
if options.Options.Reality != nil && options.Options.Reality.Enabled {
return NewRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options)
} else if options.Options.UTLS != nil && options.Options.UTLS.Enabled {
return NewUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options)
switch options.Options.Engine {
case "", "go":
case "apple":
return newAppleClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
default:
return nil, E.New("unknown tls engine: ", options.Options.Engine)
}
return NewSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options)
if options.Options.Reality != nil && options.Options.Reality.Enabled {
return newRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
} else if options.Options.UTLS != nil && options.Options.UTLS.Enabled {
return newUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
}
return newSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
}
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
defer cancel()
tlsConn, err := aTLS.ClientHandshake(ctx, conn, config)
if err != nil {
return nil, err

View File

@@ -52,11 +52,15 @@ type RealityClientConfig struct {
}
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return newRealityClient(ctx, logger, serverAddress, options, false)
}
func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
if options.UTLS == nil || !options.UTLS.Enabled {
return nil, E.New("uTLS is required by reality client")
}
uClient, err := NewUTLSClient(ctx, logger, serverAddress, options)
uClient, err := newUTLSClient(ctx, logger, serverAddress, options, allowEmptyServerName)
if err != nil {
return nil, err
}
@@ -108,6 +112,14 @@ func (e *RealityClientConfig) SetNextProtos(nextProto []string) {
e.uClient.SetNextProtos(nextProto)
}
func (e *RealityClientConfig) HandshakeTimeout() time.Duration {
return e.uClient.HandshakeTimeout()
}
func (e *RealityClientConfig) SetHandshakeTimeout(timeout time.Duration) {
e.uClient.SetHandshakeTimeout(timeout)
}
func (e *RealityClientConfig) STDConfig() (*STDConfig, error) {
return nil, E.New("unsupported usage for reality")
}

View File

@@ -26,7 +26,8 @@ import (
var _ ServerConfigCompat = (*RealityServerConfig)(nil)
type RealityServerConfig struct {
config *utls.RealityConfig
config *utls.RealityConfig
handshakeTimeout time.Duration
}
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
@@ -130,7 +131,16 @@ func NewRealityServer(ctx context.Context, logger log.ContextLogger, options opt
if options.ECH != nil && options.ECH.Enabled {
return nil, E.New("Reality is conflict with ECH")
}
var config ServerConfig = &RealityServerConfig{&tlsConfig}
var handshakeTimeout time.Duration
if options.HandshakeTimeout > 0 {
handshakeTimeout = options.HandshakeTimeout.Build()
} else {
handshakeTimeout = C.TCPTimeout
}
var config ServerConfig = &RealityServerConfig{
config: &tlsConfig,
handshakeTimeout: handshakeTimeout,
}
if options.KernelTx || options.KernelRx {
if !C.IsLinux {
return nil, E.New("kTLS is only supported on Linux")
@@ -161,6 +171,14 @@ func (c *RealityServerConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto
}
func (c *RealityServerConfig) HandshakeTimeout() time.Duration {
return c.handshakeTimeout
}
func (c *RealityServerConfig) SetHandshakeTimeout(timeout time.Duration) {
c.handshakeTimeout = timeout
}
func (c *RealityServerConfig) STDConfig() (*tls.Config, error) {
return nil, E.New("unsupported usage for reality")
}
@@ -191,7 +209,8 @@ func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn
func (c *RealityServerConfig) Clone() Config {
return &RealityServerConfig{
config: c.config.Clone(),
config: c.config.Clone(),
handshakeTimeout: c.handshakeTimeout,
}
}

View File

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

View File

@@ -24,16 +24,30 @@ import (
type STDClientConfig struct {
ctx context.Context
config *tls.Config
serverName string
disableSNI bool
verifyServerName bool
handshakeTimeout time.Duration
fragment bool
fragmentFallbackDelay time.Duration
recordFragment bool
}
func (c *STDClientConfig) ServerName() string {
return c.config.ServerName
return c.serverName
}
func (c *STDClientConfig) SetServerName(serverName string) {
c.serverName = serverName
if c.disableSNI {
c.config.ServerName = ""
if c.verifyServerName {
c.config.VerifyConnection = verifyConnection(c.config.RootCAs, c.config.Time, serverName)
} else {
c.config.VerifyConnection = nil
}
return
}
c.config.ServerName = serverName
}
@@ -45,6 +59,14 @@ func (c *STDClientConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto
}
func (c *STDClientConfig) HandshakeTimeout() time.Duration {
return c.handshakeTimeout
}
func (c *STDClientConfig) SetHandshakeTimeout(timeout time.Duration) {
c.handshakeTimeout = timeout
}
func (c *STDClientConfig) STDConfig() (*STDConfig, error) {
return c.config, nil
}
@@ -57,13 +79,19 @@ func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) {
}
func (c *STDClientConfig) Clone() Config {
return &STDClientConfig{
cloned := &STDClientConfig{
ctx: c.ctx,
config: c.config.Clone(),
serverName: c.serverName,
disableSNI: c.disableSNI,
verifyServerName: c.verifyServerName,
handshakeTimeout: c.handshakeTimeout,
fragment: c.fragment,
fragmentFallbackDelay: c.fragmentFallbackDelay,
recordFragment: c.recordFragment,
}
cloned.SetServerName(cloned.serverName)
return cloned
}
func (c *STDClientConfig) ECHConfigList() []byte {
@@ -75,41 +103,27 @@ func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte
}
func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return newSTDClient(ctx, logger, serverAddress, options, false)
}
func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
var serverName string
if options.ServerName != "" {
serverName = options.ServerName
} else if serverAddress != "" {
serverName = serverAddress
}
if serverName == "" && !options.Insecure {
return nil, E.New("missing server_name or insecure=true")
if serverName == "" && !options.Insecure && !allowEmptyServerName {
return nil, errMissingServerName
}
var tlsConfig tls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if !options.DisableSNI {
tlsConfig.ServerName = serverName
}
if options.Insecure {
tlsConfig.InsecureSkipVerify = options.Insecure
} else if options.DisableSNI {
tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
verifyOptions := x509.VerifyOptions{
Roots: tlsConfig.RootCAs,
DNSName: serverName,
Intermediates: x509.NewCertPool(),
}
for _, cert := range state.PeerCertificates[1:] {
verifyOptions.Intermediates.AddCert(cert)
}
if tlsConfig.Time != nil {
verifyOptions.CurrentTime = tlsConfig.Time()
}
_, err := state.PeerCertificates[0].Verify(verifyOptions)
return err
}
}
if len(options.CertificatePublicKeySHA256) > 0 {
if len(options.Certificate) > 0 || options.CertificatePath != "" {
@@ -198,7 +212,24 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
return nil, E.New("client certificate and client key must be provided together")
}
var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
var handshakeTimeout time.Duration
if options.HandshakeTimeout > 0 {
handshakeTimeout = options.HandshakeTimeout.Build()
} else {
handshakeTimeout = C.TCPTimeout
}
var config Config = &STDClientConfig{
ctx: ctx,
config: &tlsConfig,
serverName: serverName,
disableSNI: options.DisableSNI,
verifyServerName: options.DisableSNI && !options.Insecure,
handshakeTimeout: handshakeTimeout,
fragment: options.Fragment,
fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay),
recordFragment: options.RecordFragment,
}
config.SetServerName(serverName)
if options.ECH != nil && options.ECH.Enabled {
var err error
config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options)
@@ -220,6 +251,27 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
return config, nil
}
func verifyConnection(rootCAs *x509.CertPool, timeFunc func() time.Time, serverName string) func(state tls.ConnectionState) error {
return func(state tls.ConnectionState) error {
if serverName == "" {
return errMissingServerName
}
verifyOptions := x509.VerifyOptions{
Roots: rootCAs,
DNSName: serverName,
Intermediates: x509.NewCertPool(),
}
for _, cert := range state.PeerCertificates[1:] {
verifyOptions.Intermediates.AddCert(cert)
}
if timeFunc != nil {
verifyOptions.CurrentTime = timeFunc()
}
_, err := state.PeerCertificates[0].Verify(verifyOptions)
return err
}
}
func verifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte, timeFunc func() time.Time) error {
leafCertificate, err := x509.ParseCertificate(rawCerts[0])
if err != nil {

View File

@@ -92,6 +92,7 @@ func getACMENextProtos(provider adapter.CertificateProvider) []string {
type STDServerConfig struct {
access sync.RWMutex
config *tls.Config
handshakeTimeout time.Duration
logger log.Logger
certificateProvider managedCertificateProvider
acmeService adapter.SimpleLifecycle
@@ -139,6 +140,18 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) {
c.config = config
}
func (c *STDServerConfig) HandshakeTimeout() time.Duration {
c.access.RLock()
defer c.access.RUnlock()
return c.handshakeTimeout
}
func (c *STDServerConfig) SetHandshakeTimeout(timeout time.Duration) {
c.access.Lock()
defer c.access.Unlock()
c.handshakeTimeout = timeout
}
func (c *STDServerConfig) hasACMEALPN() bool {
if c.acmeService != nil {
return true
@@ -165,7 +178,8 @@ func (c *STDServerConfig) Server(conn net.Conn) (Conn, error) {
func (c *STDServerConfig) Clone() Config {
return &STDServerConfig{
config: c.config.Clone(),
config: c.config.Clone(),
handshakeTimeout: c.handshakeTimeout,
}
}
@@ -471,8 +485,15 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
return nil, err
}
}
var handshakeTimeout time.Duration
if options.HandshakeTimeout > 0 {
handshakeTimeout = options.HandshakeTimeout.Build()
} else {
handshakeTimeout = C.TCPTimeout
}
serverConfig := &STDServerConfig{
config: tlsConfig,
handshakeTimeout: handshakeTimeout,
logger: logger,
certificateProvider: certificateProvider,
acmeService: acmeService,

View File

@@ -28,6 +28,10 @@ import (
type UTLSClientConfig struct {
ctx context.Context
config *utls.Config
serverName string
disableSNI bool
verifyServerName bool
handshakeTimeout time.Duration
id utls.ClientHelloID
fragment bool
fragmentFallbackDelay time.Duration
@@ -35,10 +39,20 @@ type UTLSClientConfig struct {
}
func (c *UTLSClientConfig) ServerName() string {
return c.config.ServerName
return c.serverName
}
func (c *UTLSClientConfig) SetServerName(serverName string) {
c.serverName = serverName
if c.disableSNI {
c.config.ServerName = ""
if c.verifyServerName {
c.config.InsecureServerNameToVerify = serverName
} else {
c.config.InsecureServerNameToVerify = ""
}
return
}
c.config.ServerName = serverName
}
@@ -53,6 +67,14 @@ func (c *UTLSClientConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto
}
func (c *UTLSClientConfig) HandshakeTimeout() time.Duration {
return c.handshakeTimeout
}
func (c *UTLSClientConfig) SetHandshakeTimeout(timeout time.Duration) {
c.handshakeTimeout = timeout
}
func (c *UTLSClientConfig) STDConfig() (*STDConfig, error) {
return nil, E.New("unsupported usage for uTLS")
}
@@ -69,9 +91,20 @@ func (c *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []by
}
func (c *UTLSClientConfig) Clone() Config {
return &UTLSClientConfig{
c.ctx, c.config.Clone(), c.id, c.fragment, c.fragmentFallbackDelay, c.recordFragment,
cloned := &UTLSClientConfig{
ctx: c.ctx,
config: c.config.Clone(),
serverName: c.serverName,
disableSNI: c.disableSNI,
verifyServerName: c.verifyServerName,
handshakeTimeout: c.handshakeTimeout,
id: c.id,
fragment: c.fragment,
fragmentFallbackDelay: c.fragmentFallbackDelay,
recordFragment: c.recordFragment,
}
cloned.SetServerName(cloned.serverName)
return cloned
}
func (c *UTLSClientConfig) ECHConfigList() []byte {
@@ -143,29 +176,29 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
}
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return newUTLSClient(ctx, logger, serverAddress, options, false)
}
func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
var serverName string
if options.ServerName != "" {
serverName = options.ServerName
} else if serverAddress != "" {
serverName = serverAddress
}
if serverName == "" && !options.Insecure {
return nil, E.New("missing server_name or insecure=true")
if serverName == "" && !options.Insecure && !allowEmptyServerName {
return nil, errMissingServerName
}
var tlsConfig utls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if !options.DisableSNI {
tlsConfig.ServerName = serverName
}
if options.Insecure {
tlsConfig.InsecureSkipVerify = options.Insecure
} else if options.DisableSNI {
if options.Reality != nil && options.Reality.Enabled {
return nil, E.New("disable_sni is unsupported in reality")
}
tlsConfig.InsecureServerNameToVerify = serverName
}
if len(options.CertificatePublicKeySHA256) > 0 {
if len(options.Certificate) > 0 || options.CertificatePath != "" {
@@ -251,11 +284,29 @@ func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
return nil, E.New("client certificate and client key must be provided together")
}
var handshakeTimeout time.Duration
if options.HandshakeTimeout > 0 {
handshakeTimeout = options.HandshakeTimeout.Build()
} else {
handshakeTimeout = C.TCPTimeout
}
id, err := uTLSClientHelloID(options.UTLS.Fingerprint)
if err != nil {
return nil, err
}
var config Config = &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
var config Config = &UTLSClientConfig{
ctx: ctx,
config: &tlsConfig,
serverName: serverName,
disableSNI: options.DisableSNI,
verifyServerName: options.DisableSNI && !options.Insecure,
handshakeTimeout: handshakeTimeout,
id: id,
fragment: options.Fragment,
fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay),
recordFragment: options.RecordFragment,
}
config.SetServerName(serverName)
if options.ECH != nil && options.ECH.Enabled {
if options.Reality != nil && options.Reality.Enabled {
return nil, E.New("Reality is conflict with ECH")

View File

@@ -12,10 +12,18 @@ import (
)
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return newUTLSClient(ctx, logger, serverAddress, options, false)
}
func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`)
}
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return newRealityClient(ctx, logger, serverAddress, options, false)
}
func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`)
}

View File

@@ -139,9 +139,9 @@ type fakeRuleSet struct {
beforeDecrementReference func()
}
func (s *fakeRuleSet) Name() string { return "fake-rule-set" }
func (s *fakeRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error { return nil }
func (s *fakeRuleSet) PostStart() error { return nil }
func (s *fakeRuleSet) Name() string { return "fake-rule-set" }
func (s *fakeRuleSet) StartContext(context.Context) error { return nil }
func (s *fakeRuleSet) PostStart() error { return nil }
func (s *fakeRuleSet) Metadata() adapter.RuleSetMetadata {
s.access.Lock()
metadata := s.metadata

View File

@@ -3,17 +3,18 @@ package transport
import (
"bytes"
"context"
"encoding/base64"
"errors"
"io"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/httpclient"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
@@ -44,14 +45,20 @@ type HTTPSTransport struct {
logger logger.ContextLogger
dialer N.Dialer
destination *url.URL
headers http.Header
method string
host string
queryHeaders http.Header
transportAccess sync.Mutex
transport *HTTPSTransportWrapper
transport *httpclient.Client
transportResetAt time.Time
}
func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
remoteOptions := option.RemoteDNSServerOptions{
DNSServerAddressOptions: options.DNSServerAddressOptions,
}
remoteOptions.DialerOptions = options.DialerOptions
transportDialer, err := dns.NewRemoteDialer(ctx, remoteOptions)
if err != nil {
return nil, err
}
@@ -62,28 +69,21 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
return nil, err
}
if len(tlsConfig.NextProtos()) == 0 {
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS})
} else if !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) {
tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, tlsConfig.NextProtos()...))
}
headers := options.Headers.Build()
host := headers.Get("Host")
if host != "" {
headers.Del("Host")
} else {
if tlsConfig.ServerName() != "" {
host = tlsConfig.ServerName()
} else {
host = options.Server
}
serverAddr := options.DNSServerAddressOptions.Build()
if serverAddr.Port == 0 {
serverAddr.Port = 443
}
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
destinationURL := url.URL{
Scheme: "https",
Host: host,
}
if destinationURL.Host == "" {
destinationURL.Host = options.Server
}
if options.ServerPort != 0 && options.ServerPort != 443 {
destinationURL.Host = net.JoinHostPort(destinationURL.Host, strconv.Itoa(int(options.ServerPort)))
Host: doHURLHost(serverAddr, 443),
}
path := options.Path
if path == "" {
@@ -93,41 +93,67 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
if err != nil {
return nil, err
}
serverAddr := options.DNSServerAddressOptions.Build()
if serverAddr.Port == 0 {
serverAddr.Port = 443
method := strings.ToUpper(options.Method)
if method == "" {
method = http.MethodPost
}
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
switch method {
case http.MethodGet, http.MethodPost:
default:
return nil, E.New("unsupported HTTPS DNS method: ", options.Method)
}
return NewHTTPSRaw(
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions),
httpClientOptions := options.HTTPClientOptions
return NewHTTPRaw(
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, remoteOptions),
logger,
transportDialer,
&destinationURL,
headers,
serverAddr,
tlsConfig,
), nil
httpClientOptions,
method,
)
}
func NewHTTPSRaw(
func NewHTTPRaw(
adapter dns.TransportAdapter,
logger log.ContextLogger,
logger logger.ContextLogger,
dialer N.Dialer,
destination *url.URL,
headers http.Header,
serverAddr M.Socksaddr,
tlsConfig tls.Config,
) *HTTPSTransport {
httpClientOptions option.HTTPClientOptions,
method string,
) (*HTTPSTransport, error) {
if destination.Scheme == "https" && tlsConfig == nil {
return nil, E.New("TLS transport unavailable")
}
queryHeaders := headers.Clone()
if queryHeaders == nil {
queryHeaders = make(http.Header)
}
host := queryHeaders.Get("Host")
queryHeaders.Del("Host")
queryHeaders.Set("Accept", MimeType)
if method == http.MethodPost {
queryHeaders.Set("Content-Type", MimeType)
}
httpClientOptions.Tag = ""
httpClientOptions.Headers = nil
currentTransport, err := httpclient.NewClientWithDialer(dialer, tlsConfig, "", httpClientOptions)
if err != nil {
return nil, err
}
return &HTTPSTransport{
TransportAdapter: adapter,
logger: logger,
dialer: dialer,
destination: destination,
headers: headers,
transport: NewHTTPSTransportWrapper(tls.NewDialer(dialer, tlsConfig), serverAddr),
}
method: method,
host: host,
queryHeaders: queryHeaders,
transport: currentTransport,
}, nil
}
func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
@@ -181,14 +207,25 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
requestBuffer.Release()
return nil, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage))
requestURL := *t.destination
var request *http.Request
switch t.method {
case http.MethodGet:
query := requestURL.Query()
query.Set("dns", base64.RawURLEncoding.EncodeToString(rawMessage))
requestURL.RawQuery = query.Encode()
request, err = http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), nil)
default:
request, err = http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), bytes.NewReader(rawMessage))
}
if err != nil {
requestBuffer.Release()
return nil, err
}
request.Header = t.headers.Clone()
request.Header.Set("Content-Type", MimeType)
request.Header.Set("Accept", MimeType)
request.Header = t.queryHeaders.Clone()
if t.host != "" {
request.Host = t.host
}
t.transportAccess.Lock()
currentTransport := t.transport
t.transportAccess.Unlock()
@@ -222,3 +259,13 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
}
return &responseMessage, nil
}
func doHURLHost(serverAddr M.Socksaddr, defaultPort uint16) string {
if serverAddr.Port != defaultPort {
return serverAddr.String()
}
if serverAddr.IsIPv6() {
return "[" + serverAddr.AddrString() + "]"
}
return serverAddr.AddrString()
}

View File

@@ -1,80 +0,0 @@
package transport
import (
"context"
"errors"
"net"
"net/http"
"sync/atomic"
"github.com/sagernet/sing-box/common/tls"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"golang.org/x/net/http2"
)
var errFallback = E.New("fallback to HTTP/1.1")
type HTTPSTransportWrapper struct {
http2Transport *http2.Transport
httpTransport *http.Transport
fallback *atomic.Bool
}
func NewHTTPSTransportWrapper(dialer tls.Dialer, serverAddr M.Socksaddr) *HTTPSTransportWrapper {
var fallback atomic.Bool
return &HTTPSTransportWrapper{
http2Transport: &http2.Transport{
DialTLSContext: func(ctx context.Context, _, _ string, _ *tls.STDConfig) (net.Conn, error) {
tlsConn, err := dialer.DialTLSContext(ctx, serverAddr)
if err != nil {
return nil, err
}
state := tlsConn.ConnectionState()
if state.NegotiatedProtocol == http2.NextProtoTLS {
return tlsConn, nil
}
tlsConn.Close()
fallback.Store(true)
return nil, errFallback
},
},
httpTransport: &http.Transport{
DialTLSContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialer.DialTLSContext(ctx, serverAddr)
},
},
fallback: &fallback,
}
}
func (h *HTTPSTransportWrapper) RoundTrip(request *http.Request) (*http.Response, error) {
if h.fallback.Load() {
return h.httpTransport.RoundTrip(request)
} else {
response, err := h.http2Transport.RoundTrip(request)
if err != nil {
if errors.Is(err, errFallback) {
return h.httpTransport.RoundTrip(request)
}
return nil, err
}
return response, nil
}
}
func (h *HTTPSTransportWrapper) CloseIdleConnections() {
h.http2Transport.CloseIdleConnections()
h.httpTransport.CloseIdleConnections()
}
func (h *HTTPSTransportWrapper) Clone() *HTTPSTransportWrapper {
return &HTTPSTransportWrapper{
httpTransport: h.httpTransport,
http2Transport: &http2.Transport{
DialTLSContext: h.http2Transport.DialTLSContext,
},
fallback: h.fallback,
}
}

View File

@@ -9,6 +9,7 @@ import (
"net/url"
"strconv"
"sync"
"time"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
@@ -40,18 +41,23 @@ func RegisterHTTP3Transport(registry *dns.TransportRegistry) {
type HTTP3Transport struct {
dns.TransportAdapter
logger logger.ContextLogger
dialer N.Dialer
destination *url.URL
headers http.Header
serverAddr M.Socksaddr
tlsConfig *tls.STDConfig
transportAccess sync.Mutex
transport *http3.Transport
logger logger.ContextLogger
dialer N.Dialer
destination *url.URL
headers http.Header
handshakeTimeout time.Duration
serverAddr M.Socksaddr
tlsConfig *tls.STDConfig
transportAccess sync.Mutex
transport *http3.Transport
}
func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
remoteOptions := option.RemoteDNSServerOptions{
DNSServerAddressOptions: options.DNSServerAddressOptions,
}
remoteOptions.DialerOptions = options.DialerOptions
transportDialer, err := dns.NewRemoteDialer(ctx, remoteOptions)
if err != nil {
return nil, err
}
@@ -61,6 +67,7 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
if err != nil {
return nil, err
}
handshakeTimeout := tlsConfig.HandshakeTimeout()
stdConfig, err := tlsConfig.STDConfig()
if err != nil {
return nil, err
@@ -102,11 +109,12 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
return nil, E.New("invalid server address: ", serverAddr)
}
t := &HTTP3Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, remoteOptions),
logger: logger,
dialer: transportDialer,
destination: &destinationURL,
headers: headers,
handshakeTimeout: handshakeTimeout,
serverAddr: serverAddr,
tlsConfig: stdConfig,
}
@@ -115,8 +123,17 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
}
func (t *HTTP3Transport) newTransport() *http3.Transport {
quicConfig := &quic.Config{}
if t.handshakeTimeout > 0 {
quicConfig.HandshakeIdleTimeout = t.handshakeTimeout
}
return &http3.Transport{
QUICConfig: quicConfig,
Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (*quic.Conn, error) {
if t.handshakeTimeout > 0 && cfg.HandshakeIdleTimeout == 0 {
cfg = cfg.Clone()
cfg.HandshakeIdleTimeout = t.handshakeTimeout
}
conn, dialErr := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if dialErr != nil {
return nil, dialErr

View File

@@ -2,6 +2,29 @@
icon: material/alert-decagram
---
#### 1.14.0-alpha.11
* Add optimistic DNS cache **1**
* Update NaiveProxy to 147.0.7727.49
* Fixes and improvements
**1**:
Optimistic DNS cache returns an expired cached response immediately while
refreshing it in the background, reducing tail latency for repeated
queries. Enabled via [`optimistic`](/configuration/dns/#optimistic)
in DNS options, and can be persisted across restarts with the new
[`store_dns`](/configuration/experimental/cache-file/#store_dns) cache
file option. A per-query
[`disable_optimistic_cache`](/configuration/dns/rule_action/#disable_optimistic_cache)
field is also available on DNS rule actions and the `resolve` route rule
action.
This deprecates the `independent_cache` DNS option (the DNS cache now
always keys by transport) and the `store_rdrc` cache file option
(replaced by `store_dns`); both will be removed in sing-box 1.16.0.
See [Migration](/migration/#migrate-independent-dns-cache).
#### 1.14.0-alpha.10
* Add `evaluate` DNS rule action and Response Match Fields **1**

View File

@@ -2,6 +2,10 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-alert: `headers`, `tls`, Dial Fields moved to [HTTP Client Fields](#http-client-fields)
!!! question "Since sing-box 1.12.0"
# DNS over HTTP3 (DoH3)
@@ -15,27 +19,20 @@ icon: material/new-box
{
"type": "h3",
"tag": "",
"server": "",
"server_port": 443,
"server_port": 0,
"path": "",
"headers": {},
"tls": {},
// Dial Fields
"method": "",
... // HTTP Client Fields
}
]
}
}
```
!!! info "Difference from legacy H3 server"
* The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default.
* The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead.
### Fields
#### server
@@ -58,14 +55,14 @@ The path of the DNS server.
`/dns-query` will be used by default.
#### headers
#### method
Additional headers to be sent to the DNS server.
HTTP request method.
#### tls
Available values: `GET`, `POST`.
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
`POST` will be used by default.
### Dial Fields
### HTTP Client Fields
See [Dial Fields](/configuration/shared/dial/) for details.
See [HTTP Client Fields](/configuration/shared/http-client/) for details.

View File

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

View File

@@ -2,6 +2,10 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-alert: `headers`, `tls`, Dial Fields moved to [HTTP Client Fields](#http-client-fields)
!!! question "Since sing-box 1.12.0"
# DNS over HTTPS (DoH)
@@ -15,27 +19,20 @@ icon: material/new-box
{
"type": "https",
"tag": "",
"server": "",
"server_port": 443,
"server_port": 0,
"path": "",
"headers": {},
"tls": {},
// Dial Fields
"method": "",
... // HTTP Client Fields
}
]
}
}
```
!!! info "Difference from legacy HTTPS server"
* The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default.
* The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead.
### Fields
#### server
@@ -58,14 +55,14 @@ The path of the DNS server.
`/dns-query` will be used by default.
#### headers
#### method
Additional headers to be sent to the DNS server.
HTTP request method.
#### tls
Available values: `GET`, `POST`.
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
`POST` will be used by default.
### Dial Fields
### HTTP Client Fields
See [Dial Fields](/configuration/shared/dial/) for details.
See [HTTP Client Fields](/configuration/shared/http-client/) for details.

View File

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

View File

@@ -2,6 +2,11 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [control_http_client](#control_http_client)
:material-delete-clock: [Dial Fields](#dial-fields)
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [relay_server_port](#relay_server_port)
@@ -22,6 +27,7 @@ icon: material/new-box
"state_directory": "",
"auth_key": "",
"control_url": "",
"control_http_client": {}, // or ""
"ephemeral": false,
"hostname": "",
"accept_routes": false,
@@ -148,10 +154,18 @@ UDP NAT expiration time.
`5m` will be used by default.
#### control_http_client
!!! question "Since sing-box 1.14.0"
HTTP Client for connecting to the Tailscale control plane.
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
### Dial Fields
!!! note
!!! failure "Deprecated in sing-box 1.14.0"
Dial Fields in Tailscale endpoints only control how it connects to the control plane and have nothing to do with actual connections.
Dial Fields in Tailscale endpoints are deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0, use `control_http_client` instead.
See [Dial Fields](/configuration/shared/dial/) for details.

View File

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

View File

@@ -21,11 +21,16 @@
}
],
"tls": {},
... // QUIC Fields
// Deprecated
"recv_window_conn": 0,
"recv_window_client": 0,
"max_conn_client": 0,
"disable_mtu_discovery": false,
"tls": {}
"disable_mtu_discovery": false
}
```
@@ -76,32 +81,38 @@ Authentication password, in base64.
Authentication password.
#### recv_window_conn
The QUIC stream-level flow control window for receiving data.
`15728640 (15 MB/s)` will be used if empty.
#### recv_window_client
The QUIC connection-level flow control window for receiving data.
`67108864 (64 MB/s)` will be used if empty.
#### max_conn_client
The maximum number of QUIC concurrent bidirectional streams that a peer is allowed to open.
`1024` will be used if empty.
#### disable_mtu_discovery
Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
Force enabled on for systems other than Linux and Windows (according to upstream).
#### tls
==Required==
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
### QUIC Fields
See [QUIC Fields](/configuration/shared/quic/) for details.
### Deprecated Fields
#### recv_window_conn
!!! failure "Deprecated in sing-box 1.14.0"
Use QUIC fields `stream_receive_window` instead.
#### recv_window_client
!!! failure "Deprecated in sing-box 1.14.0"
Use QUIC fields `connection_receive_window` instead.
#### max_conn_client
!!! failure "Deprecated in sing-box 1.14.0"
Use QUIC fields `max_concurrent_streams` instead.
#### disable_mtu_discovery
!!! failure "Deprecated in sing-box 1.14.0"
Use QUIC fields `disable_path_mtu_discovery` instead.

View File

@@ -21,11 +21,16 @@
}
],
"tls": {},
... // QUIC 字段
// 废弃的
"recv_window_conn": 0,
"recv_window_client": 0,
"max_conn_client": 0,
"disable_mtu_discovery": false,
"tls": {}
"disable_mtu_discovery": false
}
```
@@ -76,32 +81,38 @@ base64 编码的认证密码。
认证密码。
#### recv_window_conn
用于接收数据的 QUIC 流级流控制窗口。
默认 `15728640 (15 MB/s)`
#### recv_window_client
用于接收数据的 QUIC 连接级流控制窗口。
默认 `67108864 (64 MB/s)`
#### max_conn_client
允许对等点打开的 QUIC 并发双向流的最大数量。
默认 `1024`
#### disable_mtu_discovery
禁用路径 MTU 发现 (RFC 8899)。 数据包的大小最多为 1252 (IPv4) / 1232 (IPv6) 字节。
强制为 Linux 和 Windows 以外的系统启用(根据上游)。
#### tls
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
### QUIC 字段
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
### 废弃字段
#### recv_window_conn
!!! failure "已在 sing-box 1.14.0 废弃"
请使用 QUIC 字段 `stream_receive_window` 代替。
#### recv_window_client
!!! failure "已在 sing-box 1.14.0 废弃"
请使用 QUIC 字段 `connection_receive_window` 代替。
#### max_conn_client
!!! failure "已在 sing-box 1.14.0 废弃"
请使用 QUIC 字段 `max_concurrent_streams` 代替。
#### disable_mtu_discovery
!!! failure "已在 sing-box 1.14.0 废弃"
请使用 QUIC 字段 `disable_path_mtu_discovery` 代替。

View File

@@ -34,6 +34,9 @@ icon: material/alert-decagram
],
"ignore_client_bandwidth": false,
"tls": {},
... // QUIC Fields
"masquerade": "", // or {}
"bbr_profile": "",
"brutal_debug": false
@@ -95,6 +98,10 @@ Deny clients to use the BBR CC.
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
### QUIC Fields
See [QUIC Fields](/configuration/shared/quic/) for details.
#### masquerade
HTTP3 server behavior (URL string configuration) when authentication fails.

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ sing-box uses JSON for configuration files.
"ntp": {},
"certificate": {},
"certificate_providers": [],
"http_clients": [],
"endpoints": [],
"inbounds": [],
"outbounds": [],
@@ -28,6 +29,7 @@ sing-box uses JSON for configuration files.
| `ntp` | [NTP](./ntp/) |
| `certificate` | [Certificate](./certificate/) |
| `certificate_providers` | [Certificate Provider](./shared/certificate-provider/) |
| `http_clients` | [HTTP Client](./shared/http-client/) |
| `endpoints` | [Endpoint](./endpoint/) |
| `inbounds` | [Inbound](./inbound/) |
| `outbounds` | [Outbound](./outbound/) |

View File

@@ -10,6 +10,7 @@ sing-box 使用 JSON 作为配置文件格式。
"ntp": {},
"certificate": {},
"certificate_providers": [],
"http_clients": [],
"endpoints": [],
"inbounds": [],
"outbounds": [],
@@ -28,6 +29,7 @@ sing-box 使用 JSON 作为配置文件格式。
| `ntp` | [NTP](./ntp/) |
| `certificate` | [证书](./certificate/) |
| `certificate_providers` | [证书提供者](./shared/certificate-provider/) |
| `http_clients` | [HTTP 客户端](./shared/http-client/) |
| `endpoints` | [端点](./endpoint/) |
| `inbounds` | [入站](./inbound/) |
| `outbounds` | [出站](./outbound/) |

View File

@@ -27,13 +27,18 @@ icon: material/new-box
"obfs": "fuck me till the daylight",
"auth": "",
"auth_str": "password",
"network": "",
"tls": {},
... // QUIC Fields
... // Dial Fields
// Deprecated
"recv_window_conn": 0,
"recv_window": 0,
"disable_mtu_discovery": false,
"network": "tcp",
"tls": {},
... // Dial Fields
"disable_mtu_discovery": false
}
```
@@ -104,24 +109,6 @@ Authentication password, in base64.
Authentication password.
#### recv_window_conn
The QUIC stream-level flow control window for receiving data.
`15728640 (15 MB/s)` will be used if empty.
#### recv_window
The QUIC connection-level flow control window for receiving data.
`67108864 (64 MB/s)` will be used if empty.
#### disable_mtu_discovery
Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
Force enabled on for systems other than Linux and Windows (according to upstream).
#### network
Enabled network
@@ -136,6 +123,30 @@ Both is enabled by default.
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
### QUIC Fields
See [QUIC Fields](/configuration/shared/quic/) for details.
### Dial Fields
See [Dial Fields](/configuration/shared/dial/) for details.
### Deprecated Fields
#### recv_window_conn
!!! failure "Deprecated in sing-box 1.14.0"
Use QUIC fields `stream_receive_window` instead.
#### recv_window
!!! failure "Deprecated in sing-box 1.14.0"
Use QUIC fields `connection_receive_window` instead.
#### disable_mtu_discovery
!!! failure "Deprecated in sing-box 1.14.0"
Use QUIC fields `disable_path_mtu_discovery` instead.

View File

@@ -27,13 +27,18 @@ icon: material/new-box
"obfs": "fuck me till the daylight",
"auth": "",
"auth_str": "password",
"network": "",
"tls": {},
... // QUIC 字段
... // 拨号字段
// 废弃的
"recv_window_conn": 0,
"recv_window": 0,
"disable_mtu_discovery": false,
"network": "tcp",
"tls": {},
... // 拨号字段
"disable_mtu_discovery": false
}
```
@@ -104,24 +109,6 @@ base64 编码的认证密码。
认证密码。
#### recv_window_conn
用于接收数据的 QUIC 流级流控制窗口。
默认 `15728640 (15 MB/s)`
#### recv_window
用于接收数据的 QUIC 连接级流控制窗口。
默认 `67108864 (64 MB/s)`
#### disable_mtu_discovery
禁用路径 MTU 发现 (RFC 8899)。 数据包的大小最多为 1252 (IPv4) / 1232 (IPv6) 字节。
强制为 Linux 和 Windows 以外的系统启用(根据上游)。
#### network
启用的网络协议。
@@ -136,7 +123,30 @@ base64 编码的认证密码。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
### QUIC 字段
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
### 拨号字段
参阅 [拨号字段](/zh/configuration/shared/dial/)。
### 废弃字段
#### recv_window_conn
!!! failure "已在 sing-box 1.14.0 废弃"
请使用 QUIC 字段 `stream_receive_window` 代替。
#### recv_window
!!! failure "已在 sing-box 1.14.0 废弃"
请使用 QUIC 字段 `connection_receive_window` 代替。
#### disable_mtu_discovery
!!! failure "已在 sing-box 1.14.0 废弃"
请使用 QUIC 字段 `disable_path_mtu_discovery` 代替。

View File

@@ -31,6 +31,9 @@
"password": "goofy_ahh_password",
"network": "tcp",
"tls": {},
... // QUIC Fields
"bbr_profile": "",
"brutal_debug": false,
@@ -124,6 +127,10 @@ Both is enabled by default.
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
### QUIC Fields
See [QUIC Fields](/configuration/shared/quic/) for details.
#### bbr_profile
!!! question "Since sing-box 1.14.0"

View File

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

View File

@@ -16,7 +16,9 @@
"heartbeat": "10s",
"network": "tcp",
"tls": {},
... // QUIC Fields
... // Dial Fields
}
```
@@ -91,6 +93,10 @@ Both is enabled by default.
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
### QUIC Fields
See [QUIC Fields](/configuration/shared/quic/) for details.
### Dial Fields
See [Dial Fields](/configuration/shared/dial/) for details.

View File

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

View File

@@ -6,6 +6,7 @@ icon: material/alert-decagram
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [default_http_client](#default_http_client)
:material-plus: [find_neighbor](#find_neighbor)
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
@@ -43,6 +44,7 @@ icon: material/alert-decagram
"find_process": false,
"find_neighbor": false,
"dhcp_lease_files": [],
"default_http_client": "",
"default_domain_resolver": "", // or {}
"default_network_strategy": "",
"default_network_type": [],
@@ -147,6 +149,14 @@ Custom DHCP lease file paths for hostname and MAC address resolution.
Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea) if empty.
#### default_http_client
!!! question "Since sing-box 1.14.0"
Tag of the default [HTTP Client](/configuration/shared/http-client/) used by remote rule-sets.
If empty and `http_clients` is defined, the first HTTP client is used.
#### default_domain_resolver
!!! question "Since sing-box 1.12.0"

View File

@@ -6,6 +6,7 @@ icon: material/alert-decagram
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [default_http_client](#default_http_client)
:material-plus: [find_neighbor](#find_neighbor)
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
@@ -45,6 +46,7 @@ icon: material/alert-decagram
"find_process": false,
"find_neighbor": false,
"dhcp_lease_files": [],
"default_http_client": "",
"default_network_strategy": "",
"default_fallback_delay": ""
}
@@ -146,6 +148,14 @@ icon: material/alert-decagram
为空时自动从常见 DHCP 服务器dnsmasq、odhcpd、ISC dhcpd、Kea检测。
#### default_http_client
!!! question "自 sing-box 1.14.0 起"
远程规则集使用的默认 [HTTP 客户端](/zh/configuration/shared/http-client/) 的标签。
如果为空且 `http_clients` 已定义,将使用第一个 HTTP 客户端。
#### default_domain_resolver
!!! question "自 sing-box 1.12.0 起"

View File

@@ -1,3 +1,8 @@
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [http_client](#http_client)
:material-delete-clock: [download_detour](#download_detour)
!!! quote "Changes in sing-box 1.10.0"
:material-plus: `type: inline`
@@ -43,8 +48,12 @@
"tag": "",
"format": "source", // or binary
"url": "",
"download_detour": "", // optional
"update_interval": "" // optional
"http_client": "", // or {}
"update_interval": "",
// Deprecated
"download_detour": ""
}
```
@@ -102,14 +111,26 @@ File path of rule-set.
Download URL of rule-set.
#### download_detour
#### http_client
Tag of the outbound to download rule-set.
!!! question "Since sing-box 1.14.0"
Default outbound will be used if empty.
HTTP Client for downloading rule-set.
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
Default transport will be used if empty.
#### update_interval
Update interval of rule-set.
`1d` will be used if empty.
#### download_detour
!!! failure "Deprecated in sing-box 1.14.0"
`download_detour` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0, use `http_client` instead.
Tag of the outbound to download rule-set.

View File

@@ -1,3 +1,8 @@
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [http_client](#http_client)
:material-delete-clock: [download_detour](#download_detour)
!!! quote "sing-box 1.10.0 中的更改"
:material-plus: `type: inline`
@@ -43,8 +48,12 @@
"tag": "",
"format": "source", // or binary
"url": "",
"download_detour": "", // 可选
"update_interval": "" // 可选
"http_client": "", // 或 {}
"update_interval": "",
// 废弃的
"download_detour": ""
}
```
@@ -102,14 +111,26 @@
规则集的下载 URL。
#### download_detour
#### http_client
用于下载规则集的出站的标签。
!!! question "自 sing-box 1.14.0 起"
如果为空,将使用默认出站
用于下载规则集的 HTTP 客户端
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
如果为空,将使用默认传输。
#### update_interval
规则集的更新间隔。
默认使用 `1d`。
#### download_detour
!!! failure "已在 sing-box 1.14.0 废弃"
`download_detour` 已在 sing-box 1.14.0 废弃且将在 sing-box 1.16.0 中被移除,请使用 `http_client` 代替。
用于下载规则集的出站的标签。

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,69 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.14.0"
### Structure
A string or an object.
When string, the tag of a shared [HTTP Client](/configuration/shared/http-client/) defined in top-level `http_clients`.
When object:
```json
{
"version": 0,
"disable_version_fallback": false,
"headers": {},
... // HTTP2 Fields
"tls": {},
... // Dial Fields
}
```
### Fields
#### version
HTTP version.
Available values: `1`, `2`, `3`.
`2` is used by default.
When `3`, [HTTP2 Fields](#http2-fields) are replaced by [QUIC Fields](#quic-fields).
#### disable_version_fallback
Disable automatic fallback to lower HTTP version.
#### headers
Custom HTTP headers.
`Host` header is used as request host.
### HTTP2 Fields
When `version` is `2` (default).
See [HTTP2 Fields](/configuration/shared/http2/) for details.
### QUIC Fields
When `version` is `3`.
See [QUIC Fields](/configuration/shared/quic/) for details.
### TLS Fields
See [TLS](/configuration/shared/tls/#outbound) for details.
### Dial Fields
See [Dial Fields](/configuration/shared/dial/) for details.

View File

@@ -0,0 +1,69 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.14.0 起"
### 结构
字符串或对象。
当为字符串时,为顶层 `http_clients` 中定义的共享 [HTTP 客户端](/zh/configuration/shared/http-client/) 的标签。
当为对象时:
```json
{
"version": 0,
"disable_version_fallback": false,
"headers": {},
... // HTTP2 字段
"tls": {},
... // 拨号字段
}
```
### 字段
#### version
HTTP 版本。
可用值:`1``2``3`
默认使用 `2`
当为 `3` 时,[HTTP2 字段](#http2-字段) 替换为 [QUIC 字段](#quic-字段)。
#### disable_version_fallback
禁用自动回退到更低的 HTTP 版本。
#### headers
自定义 HTTP 标头。
`Host` 标头用作请求主机。
### HTTP2 字段
`version``2`(默认)时。
参阅 [HTTP2 字段](/zh/configuration/shared/http2/) 了解详情。
### QUIC 字段
`version``3` 时。
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
### TLS 字段
参阅 [TLS](/zh/configuration/shared/tls/#出站) 了解详情。
### 拨号字段
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。

View File

@@ -0,0 +1,43 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.14.0"
### Structure
```json
{
"idle_timeout": "",
"keep_alive_period": "",
"stream_receive_window": "",
"connection_receive_window": "",
"max_concurrent_streams": 0
}
```
### Fields
#### idle_timeout
Idle connection timeout, in golang's Duration format.
#### keep_alive_period
Keep alive period, in golang's Duration format.
#### stream_receive_window
HTTP2 stream-level flow-control receive window size.
Accepts memory size format, e.g. `"64 MB"`.
#### connection_receive_window
HTTP2 connection-level flow-control receive window size.
Accepts memory size format, e.g. `"64 MB"`.
#### max_concurrent_streams
Maximum concurrent streams per connection.

View File

@@ -0,0 +1,43 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.14.0 起"
### 结构
```json
{
"idle_timeout": "",
"keep_alive_period": "",
"stream_receive_window": "",
"connection_receive_window": "",
"max_concurrent_streams": 0
}
```
### 字段
#### idle_timeout
空闲连接超时,采用 golang 的 Duration 格式。
#### keep_alive_period
Keep alive 周期,采用 golang 的 Duration 格式。
#### stream_receive_window
HTTP2 流级别流控接收窗口大小。
接受内存大小格式,例如 `"64 MB"`
#### connection_receive_window
HTTP2 连接级别流控接收窗口大小。
接受内存大小格式,例如 `"64 MB"`
#### max_concurrent_streams
每个连接的最大并发流数。

View File

@@ -0,0 +1,30 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.14.0"
### Structure
```json
{
"initial_packet_size": 0,
"disable_path_mtu_discovery": false,
... // HTTP2 Fields
}
```
### Fields
#### initial_packet_size
Initial QUIC packet size.
#### disable_path_mtu_discovery
Disable QUIC path MTU discovery.
### HTTP2 Fields
See [HTTP2 Fields](/configuration/shared/http2/) for details.

View File

@@ -0,0 +1,30 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.14.0 起"
### 结构
```json
{
"initial_packet_size": 0,
"disable_path_mtu_discovery": false,
... // HTTP2 字段
}
```
### 字段
#### initial_packet_size
初始 QUIC 数据包大小。
#### disable_path_mtu_discovery
禁用 QUIC 路径 MTU 发现。
### HTTP2 字段
参阅 [HTTP2 字段](/zh/configuration/shared/http2/) 了解详情。

View File

@@ -5,6 +5,7 @@ icon: material/new-box
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [certificate_provider](#certificate_provider)
:material-plus: [handshake_timeout](#handshake_timeout)
:material-delete-clock: [acme](#acme-fields)
!!! quote "Changes in sing-box 1.13.0"
@@ -54,6 +55,7 @@ icon: material/new-box
"key_path": "",
"kernel_tx": false,
"kernel_rx": false,
"handshake_timeout": "",
"certificate_provider": "",
// Deprecated
@@ -106,6 +108,7 @@ icon: material/new-box
```json
{
"enabled": true,
"engine": "",
"disable_sni": false,
"server_name": "",
"insecure": false,
@@ -124,6 +127,9 @@ icon: material/new-box
"fragment": false,
"fragment_fallback_delay": "",
"record_fragment": false,
"kernel_tx": false,
"kernel_rx": false,
"handshake_timeout": "",
"ech": {
"enabled": false,
"config": [],
@@ -183,6 +189,49 @@ Cipher suite values:
Enable TLS.
#### engine
==Client only==
TLS engine to use.
Values:
* `go`
* `apple`
`apple` uses Network.framework, only available on Apple platforms and only supports **direct** TCP TLS client connections.
!!! warning ""
Experimental only: due to the high memory overhead of both CGO and Network.framework,
do not use in proxy paths on iOS and tvOS.
If you want to circumvent TLS fingerprint-based proxy censorship,
use [NaiveProxy](/configuration/outbound/naive/) instead.
Supported fields:
* `server_name`
* `insecure`
* `alpn`
* `min_version`
* `max_version`
* `certificate` / `certificate_path`
* `certificate_public_key_sha256`
* `handshake_timeout`
Unsupported fields:
* `disable_sni`
* `cipher_suites`
* `curve_preferences`
* `client_certificate` / `client_certificate_path` / `client_key` / `client_key_path`
* `fragment` / `record_fragment`
* `kernel_tx` / `kernel_rx`
* `ech`
* `utls`
* `reality`
#### disable_sni
==Client only==
@@ -417,6 +466,14 @@ Enable kernel TLS transmit support.
Enable kernel TLS receive support.
#### handshake_timeout
!!! question "Since sing-box 1.14.0"
TLS handshake timeout, in golang's Duration format.
`15s` is used by default.
#### certificate_provider
!!! question "Since sing-box 1.14.0"

View File

@@ -5,6 +5,7 @@ icon: material/new-box
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [certificate_provider](#certificate_provider)
:material-plus: [handshake_timeout](#handshake_timeout)
:material-delete-clock: [acme](#acme-字段)
!!! quote "sing-box 1.13.0 中的更改"
@@ -54,6 +55,7 @@ icon: material/new-box
"key_path": "",
"kernel_tx": false,
"kernel_rx": false,
"handshake_timeout": "",
"certificate_provider": "",
// 废弃的
@@ -106,6 +108,7 @@ icon: material/new-box
```json
{
"enabled": true,
"engine": "",
"disable_sni": false,
"server_name": "",
"insecure": false,
@@ -124,6 +127,9 @@ icon: material/new-box
"fragment": false,
"fragment_fallback_delay": "",
"record_fragment": false,
"kernel_tx": false,
"kernel_rx": false,
"handshake_timeout": "",
"ech": {
"enabled": false,
"config": [],
@@ -183,6 +189,48 @@ TLS 版本值:
启用 TLS
#### engine
==仅客户端==
要使用的 TLS 引擎。
可用值:
* `go`
* `apple`
`apple` 使用 Network.framework仅在 Apple 平台可用,且仅支持 **直接** TCP TLS 客户端连接。
!!! warning ""
仅供实验用途:由于 CGO 和 Network.framework 占用的内存都很多,
不应在 iOS 和 tvOS 的代理路径中使用。
如果您想规避基于 TLS 指纹的代理审查,应使用 [NaiveProxy](/zh/configuration/outbound/naive/)。
支持的字段:
* `server_name`
* `insecure`
* `alpn`
* `min_version`
* `max_version`
* `certificate` / `certificate_path`
* `certificate_public_key_sha256`
* `handshake_timeout`
不支持的字段:
* `disable_sni`
* `cipher_suites`
* `curve_preferences`
* `client_certificate` / `client_certificate_path` / `client_key` / `client_key_path`
* `fragment` / `record_fragment`
* `kernel_tx` / `kernel_rx`
* `ech`
* `utls`
* `reality`
#### disable_sni
==仅客户端==
@@ -416,6 +464,14 @@ echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/
启用内核 TLS 接收支持。
#### handshake_timeout
!!! question "自 sing-box 1.14.0 起"
TLS 握手超时,采用 golang 的 Duration 格式。
默认使用 `15s`
#### certificate_provider
!!! question "自 sing-box 1.14.0 起"

View File

@@ -6,6 +6,27 @@ icon: material/delete-alert
## 1.14.0
#### Legacy `download_detour` remote rule-set option
Legacy `download_detour` remote rule-set option is deprecated,
use `http_client` instead.
Old field will be removed in sing-box 1.16.0.
#### Implicit default HTTP client
Implicit default HTTP client using the default outbound for remote rule-sets is deprecated.
Configure `http_clients` and `route.default_http_client` explicitly.
Old behavior will be removed in sing-box 1.16.0.
#### Legacy dialer options in Tailscale endpoint
Legacy dialer options in Tailscale endpoints are deprecated,
use `control_http_client` instead.
Old fields will be removed in sing-box 1.16.0.
#### Inline ACME options in TLS
Inline ACME options (`tls.acme`) are deprecated

View File

@@ -6,6 +6,27 @@ icon: material/delete-alert
## 1.14.0
#### 旧版远程规则集 `download_detour` 选项
旧版远程规则集 `download_detour` 选项已废弃,
请使用 `http_client` 代替。
旧字段将在 sing-box 1.16.0 中被移除。
#### 隐式默认 HTTP 客户端
使用默认出站为远程规则集隐式创建默认 HTTP 客户端的行为已废弃。
请显式配置 `http_clients``route.default_http_client`
旧行为将在 sing-box 1.16.0 中被移除。
#### Tailscale 端点中的旧版拨号选项
Tailscale 端点中的旧版拨号选项已废弃,
请使用 `control_http_client` 代替。
旧字段将在 sing-box 1.16.0 中被移除。
#### TLS 中的内联 ACME 选项
TLS 中的内联 ACME 选项(`tls.acme`)已废弃,

View File

@@ -93,6 +93,22 @@ var OptionInlineACME = Note{
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-inline-acme-to-certificate-provider",
}
var OptionLegacyRuleSetDownloadDetour = Note{
Name: "legacy-rule-set-download-detour",
Description: "legacy `download_detour` remote rule-set option",
DeprecatedVersion: "1.14.0",
ScheduledVersion: "1.16.0",
EnvName: "LEGACY_RULE_SET_DOWNLOAD_DETOUR",
}
var OptionLegacyTailscaleEndpointDialer = Note{
Name: "legacy-tailscale-endpoint-dialer",
Description: "legacy dialer options in Tailscale endpoint",
DeprecatedVersion: "1.14.0",
ScheduledVersion: "1.16.0",
EnvName: "LEGACY_TAILSCALE_ENDPOINT_DIALER",
}
var OptionRuleSetIPCIDRAcceptEmpty = Note{
Name: "dns-rule-rule-set-ip-cidr-accept-empty",
Description: "Legacy `rule_set_ip_cidr_accept_empty` DNS rule item",
@@ -138,14 +154,25 @@ var OptionStoreRDRC = Note{
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-store-rdrc",
}
var OptionImplicitDefaultHTTPClient = Note{
Name: "implicit-default-http-client",
Description: "implicit default HTTP client using default outbound for remote rule-sets",
DeprecatedVersion: "1.14.0",
ScheduledVersion: "1.16.0",
EnvName: "IMPLICIT_DEFAULT_HTTP_CLIENT",
}
var Options = []Note{
OptionOutboundDNSRuleItem,
OptionMissingDomainResolver,
OptionLegacyDomainStrategyOptions,
OptionInlineACME,
OptionLegacyRuleSetDownloadDetour,
OptionLegacyTailscaleEndpointDialer,
OptionRuleSetIPCIDRAcceptEmpty,
OptionLegacyDNSAddressFilter,
OptionLegacyDNSRuleStrategy,
OptionIndependentDNSCache,
OptionStoreRDRC,
OptionImplicitDefaultHTTPClient,
}

6
go.mod
View File

@@ -37,10 +37,10 @@ require (
github.com/sagernet/gomobile v0.1.12
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
github.com/sagernet/sing v0.8.5-0.20260404181712-947827ec3849
github.com/sagernet/sing v0.8.5-0.20260413075835-f518f5326bd0
github.com/sagernet/sing-cloudflared v0.0.0-20260407120610-7715dc2523fa
github.com/sagernet/sing-mux v0.3.4
github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212
github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
@@ -84,7 +84,7 @@ require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gaissmai/bart v0.18.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect

12
go.sum
View File

@@ -62,8 +62,8 @@ github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I=
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
@@ -242,14 +242,14 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
github.com/sagernet/sing v0.8.5-0.20260404181712-947827ec3849 h1:P8jaGN561IbHBxjlU8IGrFK65n1vDOrHo8FOMgHfn14=
github.com/sagernet/sing v0.8.5-0.20260404181712-947827ec3849/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.8.5-0.20260413075835-f518f5326bd0 h1:UY13Df9qLR5ZQfWP3kpUlkz12UXp+oc0yxUva7gM6rM=
github.com/sagernet/sing v0.8.5-0.20260413075835-f518f5326bd0/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-cloudflared v0.0.0-20260407120610-7715dc2523fa h1:165HiOfgfofJIirEp1NGSmsoJAi+++WhR29IhtAu4A4=
github.com/sagernet/sing-cloudflared v0.0.0-20260407120610-7715dc2523fa/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y=
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212 h1:7mFOUqy+DyOj7qKGd1X54UMXbnbJiiMileK/tn17xYc=
github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8=
github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6 h1:j3ISQRDyY5rs27NzUS/le+DHR0iOO0K0x+mWDLzu4Ok=
github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6/go.mod h1:r5Adw0EMUyhGBCjPI2JEupDtC040DrrvreXtua7Ifdc=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=

View File

@@ -122,6 +122,9 @@ nav:
- Listen Fields: configuration/shared/listen.md
- Dial Fields: configuration/shared/dial.md
- TLS: configuration/shared/tls.md
- HTTP Client: configuration/shared/http-client.md
- HTTP2 Fields: configuration/shared/http2.md
- QUIC Fields: configuration/shared/quic.md
- Certificate Provider:
- configuration/shared/certificate-provider/index.md
- ACME: configuration/shared/certificate-provider/acme.md

View File

@@ -24,7 +24,7 @@ type ACMECertificateProviderOptions struct {
ExternalAccount *ACMEExternalAccountOptions `json:"external_account,omitempty"`
DNS01Challenge *ACMEProviderDNS01ChallengeOptions `json:"dns01_challenge,omitempty"`
KeyType ACMEKeyType `json:"key_type,omitempty"`
Detour string `json:"detour,omitempty"`
HTTPClient *HTTPClientOptions `json:"http_client,omitempty"`
}
type _ACMEProviderDNS01ChallengeOptions struct {

View File

@@ -167,10 +167,10 @@ type RemoteTLSDNSServerOptions struct {
}
type RemoteHTTPSDNSServerOptions struct {
RemoteTLSDNSServerOptions
Path string `json:"path,omitempty"`
Method string `json:"method,omitempty"`
Headers badoption.HTTPHeader `json:"headers,omitempty"`
DNSServerAddressOptions
Path string `json:"path,omitempty"`
Method string `json:"method,omitempty"`
HTTPClientOptions
}
type FakeIPDNSServerOptions struct {

123
option/http.go Normal file
View File

@@ -0,0 +1,123 @@
package option
import (
"reflect"
"github.com/sagernet/sing/common/byteformats"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
)
type HTTP2Options struct {
IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"`
KeepAlivePeriod badoption.Duration `json:"keep_alive_period,omitempty"`
StreamReceiveWindow byteformats.MemoryBytes `json:"stream_receive_window,omitempty"`
ConnectionReceiveWindow byteformats.MemoryBytes `json:"connection_receive_window,omitempty"`
MaxConcurrentStreams int `json:"max_concurrent_streams,omitempty"`
}
type QUICOptions struct {
HTTP2Options
InitialPacketSize int `json:"initial_packet_size,omitempty"`
DisablePathMTUDiscovery bool `json:"disable_path_mtu_discovery,omitempty"`
}
type _HTTPClientOptions struct {
Tag string `json:"tag,omitempty"`
Version int `json:"version,omitempty"`
DisableVersionFallback bool `json:"disable_version_fallback,omitempty"`
Headers badoption.HTTPHeader `json:"headers,omitempty"`
HTTP2Options HTTP2Options `json:"-"`
HTTP3Options QUICOptions `json:"-"`
DefaultOutbound bool `json:"-"`
ResolveOnDetour bool `json:"-"`
OutboundTLSOptionsContainer
DialerOptions
}
type (
HTTPClient _HTTPClientOptions
HTTPClientOptions _HTTPClientOptions
)
func (h HTTPClient) Options() HTTPClientOptions {
options := HTTPClientOptions(h)
options.Tag = ""
return options
}
func (o HTTPClientOptions) IsEmpty() bool {
if o.Tag != "" {
return false
}
o.DefaultOutbound = false
o.ResolveOnDetour = false
return reflect.ValueOf(_HTTPClientOptions(o)).IsZero()
}
func (o HTTPClientOptions) MarshalJSON() ([]byte, error) {
if o.Tag != "" {
return json.Marshal(o.Tag)
}
return badjson.MarshallObjects(_HTTPClientOptions(o), httpClientVariant(_HTTPClientOptions(o)))
}
func (o *HTTPClientOptions) UnmarshalJSON(content []byte) error {
if len(content) > 0 && content[0] == '"' {
*o = HTTPClientOptions{}
return json.Unmarshal(content, &o.Tag)
}
var options _HTTPClientOptions
err := json.Unmarshal(content, &options)
if err != nil {
return err
}
err = unmarshalHTTPClientVersionOptions(content, &options, &options)
if err != nil {
return err
}
options.Tag = ""
*o = HTTPClientOptions(options)
return nil
}
func (h HTTPClient) MarshalJSON() ([]byte, error) {
return badjson.MarshallObjects(_HTTPClientOptions(h), httpClientVariant(_HTTPClientOptions(h)))
}
func (h *HTTPClient) UnmarshalJSON(content []byte) error {
err := json.Unmarshal(content, (*_HTTPClientOptions)(h))
if err != nil {
return err
}
return unmarshalHTTPClientVersionOptions(content, (*_HTTPClientOptions)(h), (*_HTTPClientOptions)(h))
}
func unmarshalHTTPClientVersionOptions(content []byte, baseStruct any, options *_HTTPClientOptions) error {
switch options.Version {
case 1:
return json.UnmarshalDisallowUnknownFields(content, baseStruct)
case 0, 2:
options.Version = 2
return badjson.UnmarshallExcluded(content, baseStruct, &options.HTTP2Options)
case 3:
return badjson.UnmarshallExcluded(content, baseStruct, &options.HTTP3Options)
default:
return E.New("unknown HTTP version: ", options.Version)
}
}
func httpClientVariant(options _HTTPClientOptions) any {
switch options.Version {
case 1:
return nil
case 0, 2:
return options.HTTP2Options
case 3:
return options.HTTP3Options
default:
return nil
}
}

View File

@@ -7,17 +7,22 @@ import (
type HysteriaInboundOptions struct {
ListenOptions
Up *byteformats.NetworkBytesCompat `json:"up,omitempty"`
UpMbps int `json:"up_mbps,omitempty"`
Down *byteformats.NetworkBytesCompat `json:"down,omitempty"`
DownMbps int `json:"down_mbps,omitempty"`
Obfs string `json:"obfs,omitempty"`
Users []HysteriaUser `json:"users,omitempty"`
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
ReceiveWindowClient uint64 `json:"recv_window_client,omitempty"`
MaxConnClient int `json:"max_conn_client,omitempty"`
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
Up *byteformats.NetworkBytesCompat `json:"up,omitempty"`
UpMbps int `json:"up_mbps,omitempty"`
Down *byteformats.NetworkBytesCompat `json:"down,omitempty"`
DownMbps int `json:"down_mbps,omitempty"`
Obfs string `json:"obfs,omitempty"`
Users []HysteriaUser `json:"users,omitempty"`
// Deprecated: use QUIC fields instead
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
// Deprecated: use QUIC fields instead
ReceiveWindowClient uint64 `json:"recv_window_client,omitempty"`
// Deprecated: use QUIC fields instead
MaxConnClient int `json:"max_conn_client,omitempty"`
// Deprecated: use QUIC fields instead
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
InboundTLSOptionsContainer
QUICOptions
}
type HysteriaUser struct {
@@ -29,18 +34,22 @@ type HysteriaUser struct {
type HysteriaOutboundOptions struct {
DialerOptions
ServerOptions
ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"`
HopInterval badoption.Duration `json:"hop_interval,omitempty"`
Up *byteformats.NetworkBytesCompat `json:"up,omitempty"`
UpMbps int `json:"up_mbps,omitempty"`
Down *byteformats.NetworkBytesCompat `json:"down,omitempty"`
DownMbps int `json:"down_mbps,omitempty"`
Obfs string `json:"obfs,omitempty"`
Auth []byte `json:"auth,omitempty"`
AuthString string `json:"auth_str,omitempty"`
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
ReceiveWindow uint64 `json:"recv_window,omitempty"`
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
Network NetworkList `json:"network,omitempty"`
ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"`
HopInterval badoption.Duration `json:"hop_interval,omitempty"`
Up *byteformats.NetworkBytesCompat `json:"up,omitempty"`
UpMbps int `json:"up_mbps,omitempty"`
Down *byteformats.NetworkBytesCompat `json:"down,omitempty"`
DownMbps int `json:"down_mbps,omitempty"`
Obfs string `json:"obfs,omitempty"`
Auth []byte `json:"auth,omitempty"`
AuthString string `json:"auth_str,omitempty"`
// Deprecated: use QUIC fields instead
ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"`
// Deprecated: use QUIC fields instead
ReceiveWindow uint64 `json:"recv_window,omitempty"`
// Deprecated: use QUIC fields instead
DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"`
Network NetworkList `json:"network,omitempty"`
OutboundTLSOptionsContainer
QUICOptions
}

View File

@@ -18,6 +18,7 @@ type Hysteria2InboundOptions struct {
Users []Hysteria2User `json:"users,omitempty"`
IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"`
InboundTLSOptionsContainer
QUICOptions
Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"`
BBRProfile string `json:"bbr_profile,omitempty"`
BrutalDebug bool `json:"brutal_debug,omitempty"`
@@ -122,6 +123,7 @@ type Hysteria2OutboundOptions struct {
Password string `json:"password,omitempty"`
Network NetworkList `json:"network,omitempty"`
OutboundTLSOptionsContainer
QUICOptions
BBRProfile string `json:"bbr_profile,omitempty"`
BrutalDebug bool `json:"brutal_debug,omitempty"`
}

View File

@@ -17,6 +17,7 @@ type _Options struct {
NTP *NTPOptions `json:"ntp,omitempty"`
Certificate *CertificateOptions `json:"certificate,omitempty"`
CertificateProviders []CertificateProvider `json:"certificate_providers,omitempty"`
HTTPClients []HTTPClient `json:"http_clients,omitempty"`
Endpoints []Endpoint `json:"endpoints,omitempty"`
Inbounds []Inbound `json:"inbounds,omitempty"`
Outbounds []Outbound `json:"outbounds,omitempty"`
@@ -61,6 +62,10 @@ func checkOptions(options *Options) error {
if err != nil {
return err
}
err = checkHTTPClients(options.HTTPClients)
if err != nil {
return err
}
return nil
}
@@ -79,6 +84,20 @@ func checkCertificateProviders(providers []CertificateProvider) error {
return nil
}
func checkHTTPClients(clients []HTTPClient) error {
seen := make(map[string]bool)
for _, client := range clients {
if client.Tag == "" {
return E.New("missing http client tag")
}
if seen[client.Tag] {
return E.New("duplicate http client tag: ", client.Tag)
}
seen[client.Tag] = true
}
return nil
}
func checkInbounds(inbounds []Inbound) error {
seen := make(map[string]bool)
for i, inbound := range inbounds {

View File

@@ -15,7 +15,7 @@ type CloudflareOriginCACertificateProviderOptions struct {
OriginCAKey string `json:"origin_ca_key,omitempty"`
RequestType CloudflareOriginCARequestType `json:"request_type,omitempty"`
RequestedValidity CloudflareOriginCARequestValidity `json:"requested_validity,omitempty"`
Detour string `json:"detour,omitempty"`
HTTPClient *HTTPClientOptions `json:"http_client,omitempty"`
}
type CloudflareOriginCARequestType string

View File

@@ -20,6 +20,7 @@ type RouteOptions struct {
DefaultNetworkType badoption.Listable[InterfaceType] `json:"default_network_type,omitempty"`
DefaultFallbackNetworkType badoption.Listable[InterfaceType] `json:"default_fallback_network_type,omitempty"`
DefaultFallbackDelay badoption.Duration `json:"default_fallback_delay,omitempty"`
DefaultHTTPClient string `json:"default_http_client,omitempty"`
}
type GeoIPOptions struct {

View File

@@ -122,8 +122,10 @@ type LocalRuleSet struct {
type RemoteRuleSet struct {
URL string `json:"url"`
DownloadDetour string `json:"download_detour,omitempty"`
HTTPClient *HTTPClientOptions `json:"http_client,omitempty"`
UpdateInterval badoption.Duration `json:"update_interval,omitempty"`
// Deprecated: use http_client instead
DownloadDetour string `json:"download_detour,omitempty"`
}
type _HeadlessRule struct {

View File

@@ -11,10 +11,12 @@ import (
)
type TailscaleEndpointOptions struct {
// Deprecated: use control_http_client instead
DialerOptions
StateDirectory string `json:"state_directory,omitempty"`
AuthKey string `json:"auth_key,omitempty"`
ControlURL string `json:"control_url,omitempty"`
ControlHTTPClient *HTTPClientOptions `json:"control_http_client,omitempty"`
Ephemeral bool `json:"ephemeral,omitempty"`
Hostname string `json:"hostname,omitempty"`
AcceptRoutes bool `json:"accept_routes,omitempty"`
@@ -55,7 +57,7 @@ type DERPServiceOptions struct {
type _DERPVerifyClientURLOptions struct {
URL string `json:"url,omitempty"`
DialerOptions
HTTPClientOptions
}
type DERPVerifyClientURLOptions _DERPVerifyClientURLOptions
@@ -69,7 +71,7 @@ func (d DERPVerifyClientURLOptions) ServerIsDomain() bool {
}
func (d DERPVerifyClientURLOptions) MarshalJSON() ([]byte, error) {
if reflect.DeepEqual(d, _DERPVerifyClientURLOptions{}) {
if d.URL != "" && d.TLS == nil && reflect.DeepEqual(d.DialerOptions, DialerOptions{}) {
return json.Marshal(d.URL)
} else {
return json.Marshal(_DERPVerifyClientURLOptions(d))

View File

@@ -28,6 +28,7 @@ type InboundTLSOptions struct {
KeyPath string `json:"key_path,omitempty"`
KernelTx bool `json:"kernel_tx,omitempty"`
KernelRx bool `json:"kernel_rx,omitempty"`
HandshakeTimeout badoption.Duration `json:"handshake_timeout,omitempty"`
CertificateProvider *CertificateProviderOptions `json:"certificate_provider,omitempty"`
// Deprecated: use certificate_provider
@@ -100,6 +101,7 @@ func (o *InboundTLSOptionsContainer) ReplaceInboundTLSOptions(options *InboundTL
type OutboundTLSOptions struct {
Enabled bool `json:"enabled,omitempty"`
Engine string `json:"engine,omitempty"`
DisableSNI bool `json:"disable_sni,omitempty"`
ServerName string `json:"server_name,omitempty"`
Insecure bool `json:"insecure,omitempty"`
@@ -120,6 +122,7 @@ type OutboundTLSOptions struct {
RecordFragment bool `json:"record_fragment,omitempty"`
KernelTx bool `json:"kernel_tx,omitempty"`
KernelRx bool `json:"kernel_rx,omitempty"`
HandshakeTimeout badoption.Duration `json:"handshake_timeout,omitempty"`
ECH *OutboundECHOptions `json:"ech,omitempty"`
UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
Reality *OutboundRealityOptions `json:"reality,omitempty"`

View File

@@ -10,6 +10,7 @@ type TUICInboundOptions struct {
ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"`
Heartbeat badoption.Duration `json:"heartbeat,omitempty"`
InboundTLSOptionsContainer
QUICOptions
}
type TUICUser struct {
@@ -30,4 +31,5 @@ type TUICOutboundOptions struct {
Heartbeat badoption.Duration `json:"heartbeat,omitempty"`
Network NetworkList `json:"network,omitempty"`
OutboundTLSOptionsContainer
QUICOptions
}

View File

@@ -77,15 +77,9 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
ReceiveBPS: receiveBps,
XPlusPassword: options.Obfs,
TLSConfig: tlsConfig,
QUICOptions: buildInboundQUICOptions(options),
UDPTimeout: udpTimeout,
Handler: inbound,
// Legacy options
ConnReceiveWindow: options.ReceiveWindowConn,
StreamReceiveWindow: options.ReceiveWindowClient,
MaxIncomingStreams: int64(options.MaxConnClient),
DisableMTUDiscovery: options.DisableMTUDiscovery,
})
if err != nil {
return nil, err

View File

@@ -70,21 +70,19 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
receiveBps = uint64(options.DownMbps) * hysteria.MbpsToBps
}
client, err := hysteria.NewClient(hysteria.ClientOptions{
Context: ctx,
Dialer: outboundDialer,
Logger: logger,
ServerAddress: options.ServerOptions.Build(),
ServerPorts: options.ServerPorts,
HopInterval: time.Duration(options.HopInterval),
SendBPS: sendBps,
ReceiveBPS: receiveBps,
XPlusPassword: options.Obfs,
Password: password,
TLSConfig: tlsConfig,
UDPDisabled: !common.Contains(networkList, N.NetworkUDP),
ConnReceiveWindow: options.ReceiveWindowConn,
StreamReceiveWindow: options.ReceiveWindow,
DisableMTUDiscovery: options.DisableMTUDiscovery,
Context: ctx,
Dialer: outboundDialer,
Logger: logger,
ServerAddress: options.ServerOptions.Build(),
ServerPorts: options.ServerPorts,
HopInterval: time.Duration(options.HopInterval),
SendBPS: sendBps,
ReceiveBPS: receiveBps,
XPlusPassword: options.Obfs,
Password: password,
TLSConfig: tlsConfig,
QUICOptions: buildOutboundQUICOptions(options),
UDPDisabled: !common.Contains(networkList, N.NetworkUDP),
})
if err != nil {
return nil, err

49
protocol/hysteria/quic.go Normal file
View File

@@ -0,0 +1,49 @@
package hysteria
import (
"github.com/sagernet/sing-box/option"
qtls "github.com/sagernet/sing-quic"
)
func buildBaseQUICOptions(options option.QUICOptions) qtls.QUICOptions {
return qtls.QUICOptions{
IdleTimeout: options.IdleTimeout.Build(),
KeepAlivePeriod: options.KeepAlivePeriod.Build(),
StreamReceiveWindow: options.StreamReceiveWindow.Value(),
ConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
MaxConcurrentStreams: options.MaxConcurrentStreams,
InitialPacketSize: options.InitialPacketSize,
DisablePathMTUDiscovery: options.DisablePathMTUDiscovery,
}
}
func buildInboundQUICOptions(options option.HysteriaInboundOptions) qtls.QUICOptions {
quicOptions := buildBaseQUICOptions(options.QUICOptions)
if quicOptions.ConnectionReceiveWindow == 0 {
quicOptions.ConnectionReceiveWindow = options.ReceiveWindowConn //nolint:staticcheck
}
if quicOptions.StreamReceiveWindow == 0 {
quicOptions.StreamReceiveWindow = options.ReceiveWindowClient //nolint:staticcheck
}
if quicOptions.MaxConcurrentStreams == 0 {
quicOptions.MaxConcurrentStreams = options.MaxConnClient //nolint:staticcheck
}
if !quicOptions.DisablePathMTUDiscovery {
quicOptions.DisablePathMTUDiscovery = options.DisableMTUDiscovery //nolint:staticcheck
}
return quicOptions
}
func buildOutboundQUICOptions(options option.HysteriaOutboundOptions) qtls.QUICOptions {
quicOptions := buildBaseQUICOptions(options.QUICOptions)
if quicOptions.ConnectionReceiveWindow == 0 {
quicOptions.ConnectionReceiveWindow = options.ReceiveWindowConn //nolint:staticcheck
}
if quicOptions.StreamReceiveWindow == 0 {
quicOptions.StreamReceiveWindow = options.ReceiveWindow //nolint:staticcheck
}
if !quicOptions.DisablePathMTUDiscovery {
quicOptions.DisablePathMTUDiscovery = options.DisableMTUDiscovery //nolint:staticcheck
}
return quicOptions
}

View File

@@ -15,6 +15,7 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
qtls "github.com/sagernet/sing-quic"
"github.com/sagernet/sing-quic/hysteria"
"github.com/sagernet/sing-quic/hysteria2"
"github.com/sagernet/sing/common"
@@ -114,13 +115,22 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
udpTimeout = C.UDPTimeout
}
service, err := hysteria2.NewService[int](hysteria2.ServiceOptions{
Context: ctx,
Logger: logger,
BrutalDebug: options.BrutalDebug,
SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps),
ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps),
SalamanderPassword: salamanderPassword,
TLSConfig: tlsConfig,
Context: ctx,
Logger: logger,
BrutalDebug: options.BrutalDebug,
SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps),
ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps),
SalamanderPassword: salamanderPassword,
TLSConfig: tlsConfig,
QUICOptions: qtls.QUICOptions{
IdleTimeout: options.IdleTimeout.Build(),
KeepAlivePeriod: options.KeepAlivePeriod.Build(),
StreamReceiveWindow: options.StreamReceiveWindow.Value(),
ConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
MaxConcurrentStreams: options.MaxConcurrentStreams,
InitialPacketSize: options.InitialPacketSize,
DisablePathMTUDiscovery: options.DisablePathMTUDiscovery,
},
IgnoreClientBandwidth: options.IgnoreClientBandwidth,
UDPTimeout: udpTimeout,
Handler: inbound,

View File

@@ -14,6 +14,7 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/tuic"
qtls "github.com/sagernet/sing-quic"
"github.com/sagernet/sing-quic/hysteria"
"github.com/sagernet/sing-quic/hysteria2"
"github.com/sagernet/sing/common"
@@ -79,8 +80,17 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
SalamanderPassword: salamanderPassword,
Password: options.Password,
TLSConfig: tlsConfig,
UDPDisabled: !common.Contains(networkList, N.NetworkUDP),
BBRProfile: options.BBRProfile,
QUICOptions: qtls.QUICOptions{
IdleTimeout: options.IdleTimeout.Build(),
KeepAlivePeriod: options.KeepAlivePeriod.Build(),
StreamReceiveWindow: options.StreamReceiveWindow.Value(),
ConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
MaxConcurrentStreams: options.MaxConcurrentStreams,
InitialPacketSize: options.InitialPacketSize,
DisablePathMTUDiscovery: options.DisablePathMTUDiscovery,
},
UDPDisabled: !common.Contains(networkList, N.NetworkUDP),
BBRProfile: options.BBRProfile,
})
if err != nil {
return nil, err

View File

@@ -171,13 +171,9 @@ func (t *DNSTransport) createResolver(directDialer func() N.Dialer, resolver *dn
tlsConfig := common.Must1(tls.NewClient(t.ctx, t.logger, serverAddr.AddrString(), option.OutboundTLSOptions{
ALPN: []string{http2.NextProtoTLS, "http/1.1"},
}))
return transport.NewHTTPSRaw(t.TransportAdapter, t.logger, myDialer, serverURL, http.Header{}, serverAddr, tlsConfig), nil
return transport.NewHTTPRaw(t.TransportAdapter, t.logger, myDialer, serverURL, http.Header{}, tlsConfig, option.HTTPClientOptions{}, http.MethodPost)
case "http":
serverAddr = M.ParseSocksaddrHostPortStr(serverURL.Hostname(), serverURL.Port())
if serverAddr.Port == 0 {
serverAddr.Port = 80
}
return transport.NewHTTPSRaw(t.TransportAdapter, t.logger, myDialer, serverURL, http.Header{}, serverAddr, nil), nil
return transport.NewHTTPRaw(t.TransportAdapter, t.logger, myDialer, serverURL, http.Header{}, nil, option.HTTPClientOptions{}, http.MethodPost)
// case "tls":
default:
return nil, E.New("unknown resolver scheme: ", serverURL.Scheme)

View File

@@ -4,7 +4,6 @@ package tailscale
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
@@ -28,6 +27,7 @@ import (
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/common/dialer"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/route/rule"
@@ -41,7 +41,6 @@ import (
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
_ "github.com/sagernet/tailscale/feature/relayserver"
@@ -195,6 +194,19 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
// controlplane.tailscale.com
remoteIsDomain = true
}
hasLegacyDialer := !reflect.DeepEqual(options.DialerOptions, option.DialerOptions{})
hasControlHTTPClient := options.ControlHTTPClient != nil && !options.ControlHTTPClient.IsEmpty()
if hasLegacyDialer && hasControlHTTPClient {
return nil, E.New("control_http_client is conflict with deprecated dialer options")
}
controlHTTPClientOptions := common.PtrValueOrDefault(options.ControlHTTPClient)
if hasLegacyDialer {
deprecated.Report(ctx, deprecated.OptionLegacyTailscaleEndpointDialer)
controlHTTPClientOptions.DialerOptions = options.DialerOptions
}
if remoteIsDomain {
controlHTTPClientOptions.ResolveOnDetour = true
}
outboundDialer, err := dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
@@ -206,6 +218,15 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
return nil, err
}
dnsRouter := service.FromContext[adapter.DNSRouter](ctx)
httpClientManager := service.FromContext[adapter.HTTPClientManager](ctx)
if httpClientManager == nil {
return nil, E.New("missing HTTP client manager")
}
controlTransport, err := httpClientManager.ResolveTransport(logger, controlHTTPClientOptions)
if err != nil {
return nil, E.Cause(err, "create control HTTP client")
}
controlHTTPClient := &http.Client{Transport: controlTransport}
server := &tsnet.Server{
Dir: stateDirectory,
Hostname: hostname,
@@ -223,19 +244,8 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
LookupHook: func(ctx context.Context, host string) ([]netip.Addr, error) {
return dnsRouter.Lookup(ctx, host, outboundDialer.(dialer.ResolveDialer).QueryOptions())
},
DNS: &dnsConfigurtor{},
HTTPClient: &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
return outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(address))
},
TLSClientConfig: &tls.Config{
RootCAs: adapter.RootPoolFromContext(ctx),
Time: ntp.TimeFuncFromContext(ctx),
},
},
},
DNS: &dnsConfigurtor{},
HTTPClient: controlHTTPClient,
}
return &Endpoint{
Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil),

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