mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-12 01:57:18 +10:00
Compare commits
75 Commits
dev-ping
...
v1.12.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcc92c7ed7 | ||
|
|
5623128c14 | ||
|
|
c0fc63542a | ||
|
|
cc35789604 | ||
|
|
bc3dc96777 | ||
|
|
34193b2e79 | ||
|
|
6c0d10c0a1 | ||
|
|
284e147518 | ||
|
|
3ea844baed | ||
|
|
8d32bf986a | ||
|
|
1701eebeb3 | ||
|
|
2b7ec7a91d | ||
|
|
95b2580c9e | ||
|
|
b295295f5f | ||
|
|
8cf92076d3 | ||
|
|
18f291df9a | ||
|
|
ebcceecb2b | ||
|
|
96dc1410fb | ||
|
|
fa2d63b9ac | ||
|
|
4e0e42dbf7 | ||
|
|
242afecee4 | ||
|
|
2ff91f578d | ||
|
|
daf2f57815 | ||
|
|
95ad31b6f5 | ||
|
|
c679656bfc | ||
|
|
c9a7e5c5b3 | ||
|
|
8b54d1136d | ||
|
|
6b01ee1aac | ||
|
|
7b13fce38e | ||
|
|
41f653a4cc | ||
|
|
6250beaacd | ||
|
|
2a4313a48e | ||
|
|
988a08b76c | ||
|
|
a655c2dc1f | ||
|
|
c2f14757c8 | ||
|
|
6a7e61fd9e | ||
|
|
8f1a63f3df | ||
|
|
d2f614eeba | ||
|
|
4e4be13506 | ||
|
|
55491c4ded | ||
|
|
8c574eee12 | ||
|
|
30996c6d94 | ||
|
|
a87583e22f | ||
|
|
aea4470135 | ||
|
|
54418bf9e1 | ||
|
|
da66c0e1ea | ||
|
|
fcb194ed15 | ||
|
|
885e02f218 | ||
|
|
9aca54d039 | ||
|
|
d55d5009c2 | ||
|
|
4f3ee61104 | ||
|
|
96eb98c00a | ||
|
|
68ce9577c6 | ||
|
|
3ae036e997 | ||
|
|
5da2d1d470 | ||
|
|
8e2baf40f1 | ||
|
|
c24c40dfee | ||
|
|
32e52ce1ed | ||
|
|
ed46438359 | ||
|
|
0b5490d5a3 | ||
|
|
2d73ef511d | ||
|
|
63e6c85f6f | ||
|
|
8946a6d2d0 | ||
|
|
d3132645fb | ||
|
|
373f158fe0 | ||
|
|
ce36835fab | ||
|
|
619fa671d7 | ||
|
|
eb07c7a79e | ||
|
|
7eb3535094 | ||
|
|
93b68312cf | ||
|
|
97ce666e43 | ||
|
|
4000e1e66d | ||
|
|
270740e859 | ||
|
|
6cad142cfe | ||
|
|
093013687c |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -155,7 +155,7 @@ jobs:
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: 2.5.1
|
||||
version: '~> v2'
|
||||
install-only: true
|
||||
- name: Extract signing key
|
||||
run: |-
|
||||
@@ -551,7 +551,7 @@ jobs:
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: 2.5.1
|
||||
version: '~> v2'
|
||||
install-only: true
|
||||
- name: Cache ghr
|
||||
uses: actions/cache@v4
|
||||
|
||||
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
version: '~> v2'
|
||||
args: release -f .goreleaser.fury.yaml --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -27,7 +27,6 @@ run:
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_ech
|
||||
- with_utls
|
||||
- with_reality_server
|
||||
- with_acme
|
||||
|
||||
@@ -14,7 +14,6 @@ builds:
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_ech
|
||||
- with_utls
|
||||
- with_reality_server
|
||||
- with_acme
|
||||
|
||||
@@ -16,7 +16,6 @@ builds:
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_ech
|
||||
- with_utls
|
||||
- with_reality_server
|
||||
- with_acme
|
||||
@@ -24,6 +23,7 @@ builds:
|
||||
- with_tailscale
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
targets:
|
||||
- linux_386
|
||||
- linux_amd64_v1
|
||||
@@ -62,6 +62,7 @@ builds:
|
||||
<<: *template
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- GOTOOLCHAIN=local
|
||||
overrides:
|
||||
- goos: android
|
||||
goarch: arm
|
||||
@@ -96,10 +97,12 @@ archives:
|
||||
builds:
|
||||
- main
|
||||
- android
|
||||
format: tar.gz
|
||||
formats:
|
||||
- tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
formats:
|
||||
- zip
|
||||
wrap_in_directory: true
|
||||
files:
|
||||
- LICENSE
|
||||
|
||||
@@ -13,7 +13,7 @@ RUN set -ex \
|
||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||
&& go build -v -trimpath -tags \
|
||||
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \
|
||||
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api" \
|
||||
-o /go/bin/sing-box \
|
||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
||||
./cmd/sing-box
|
||||
|
||||
12
Makefile
12
Makefile
@@ -1,10 +1,9 @@
|
||||
NAME = sing-box
|
||||
COMMIT = $(shell git rev-parse --short HEAD)
|
||||
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
|
||||
TAGS_GO121 = with_ech
|
||||
TAGS_GO123 = with_tailscale
|
||||
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121),$(TAGS_GO123)
|
||||
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
|
||||
TAGS ?= $(TAGS_GO120),$(TAGS_GO123)
|
||||
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_utls,with_reality_server
|
||||
|
||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||
@@ -18,14 +17,17 @@ PREFIX ?= $(shell go env GOPATH)
|
||||
.PHONY: test release docs build
|
||||
|
||||
build:
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
ci_build_go120:
|
||||
go build $(PARAMS) $(MAIN)
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(PARAMS) $(MAIN) && \
|
||||
go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN)
|
||||
|
||||
ci_build:
|
||||
go build $(PARAMS) $(MAIN)
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(PARAMS) $(MAIN) && \
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
generate_completions:
|
||||
|
||||
@@ -53,10 +53,11 @@ type InboundContext struct {
|
||||
|
||||
// sniffer
|
||||
|
||||
Protocol string
|
||||
Domain string
|
||||
Client string
|
||||
SniffContext any
|
||||
Protocol string
|
||||
Domain string
|
||||
Client string
|
||||
SniffContext any
|
||||
PacketSniffError error
|
||||
|
||||
// cache
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ type NetworkManager interface {
|
||||
PackageManager() tun.PackageManager
|
||||
WIFIState() WIFIState
|
||||
ResetNetwork()
|
||||
UpdateWIFIState()
|
||||
}
|
||||
|
||||
type NetworkOptions struct {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-tun"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
@@ -19,11 +18,6 @@ type Outbound interface {
|
||||
N.Dialer
|
||||
}
|
||||
|
||||
type DirectRouteOutbound interface {
|
||||
Outbound
|
||||
NewDirectRouteConnection(metadata InboundContext, routeContext tun.DirectRouteContext) (tun.DirectRouteDestination, error)
|
||||
}
|
||||
|
||||
type OutboundRegistry interface {
|
||||
option.OutboundOptionsRegistry
|
||||
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
|
||||
|
||||
@@ -246,8 +246,6 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started {
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
err = adapter.LegacyStart(outbound, stage)
|
||||
@@ -256,6 +254,8 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
|
||||
}
|
||||
}
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = common.Close(existsOutbound)
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"sync"
|
||||
|
||||
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"
|
||||
@@ -20,7 +19,7 @@ import (
|
||||
type Router interface {
|
||||
Lifecycle
|
||||
ConnectionRouter
|
||||
PreMatch(metadata InboundContext, context tun.DirectRouteContext) (tun.DirectRouteDestination, error)
|
||||
PreMatch(metadata InboundContext) error
|
||||
ConnectionRouterEx
|
||||
RuleSet(tag string) (RuleSet, bool)
|
||||
NeedWIFIState() bool
|
||||
|
||||
9
box.go
9
box.go
@@ -216,8 +216,15 @@ func New(options Options) (*Box, error) {
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
endpointCtx := ctx
|
||||
if tag != "" {
|
||||
// TODO: remove this
|
||||
endpointCtx = adapter.WithContext(endpointCtx, &adapter.InboundContext{
|
||||
Outbound: tag,
|
||||
})
|
||||
}
|
||||
err = endpointManager.Create(
|
||||
ctx,
|
||||
endpointCtx,
|
||||
router,
|
||||
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
|
||||
Submodule clients/android updated: 3a2fc9c880...aefe3c0290
Submodule clients/apple updated: 3d5d7343fb...ae5818ee5a
@@ -58,7 +58,7 @@ func init() {
|
||||
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
||||
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api", "with_tailscale")
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_tailscale")
|
||||
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
||||
debugTags = append(debugTags, "debug")
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var pqSignatureSchemesEnabled bool
|
||||
|
||||
var commandGenerateECHKeyPair = &cobra.Command{
|
||||
Use: "ech-keypair <plain_server_name>",
|
||||
Short: "Generate TLS ECH key pair",
|
||||
@@ -24,12 +22,11 @@ var commandGenerateECHKeyPair = &cobra.Command{
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGenerateECHKeyPair.Flags().BoolVar(&pqSignatureSchemesEnabled, "pq-signature-schemes-enabled", false, "Enable PQ signature schemes")
|
||||
commandGenerate.AddCommand(commandGenerateECHKeyPair)
|
||||
}
|
||||
|
||||
func generateECHKeyPair(serverName string) error {
|
||||
configPem, keyPem, err := tls.ECHKeygenDefault(serverName, pqSignatureSchemesEnabled)
|
||||
configPem, keyPem, err := tls.ECHKeygenDefault(serverName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
//go:build go1.21 && !without_badtls && with_ech
|
||||
|
||||
package badtls
|
||||
|
||||
import (
|
||||
"net"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/sagernet/cloudflare-tls"
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
func init() {
|
||||
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
|
||||
tlsConn, loaded := common.Cast[*tls.Conn](conn)
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
return true, func() error {
|
||||
return echReadRecord(tlsConn)
|
||||
}, func() error {
|
||||
return echHandlePostHandshakeMessage(tlsConn)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//go:linkname echReadRecord github.com/sagernet/cloudflare-tls.(*Conn).readRecord
|
||||
func echReadRecord(c *tls.Conn) error
|
||||
|
||||
//go:linkname echHandlePostHandshakeMessage github.com/sagernet/cloudflare-tls.(*Conn).handlePostHandshakeMessage
|
||||
func echHandlePostHandshakeMessage(c *tls.Conn) error
|
||||
@@ -34,10 +34,13 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
|
||||
switch options.Store {
|
||||
case C.CertificateStoreSystem, "":
|
||||
systemPool = x509.NewCertPool()
|
||||
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||
var systemValid bool
|
||||
for _, cert := range service.FromContext[platform.Interface](ctx).SystemCertificates() {
|
||||
if systemPool.AppendCertsFromPEM([]byte(cert)) {
|
||||
systemValid = true
|
||||
if platformInterface != nil {
|
||||
for _, cert := range platformInterface.SystemCertificates() {
|
||||
if systemPool.AppendCertsFromPEM([]byte(cert)) {
|
||||
systemValid = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !systemValid {
|
||||
|
||||
@@ -210,6 +210,8 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
|
||||
if !address.IsValid() {
|
||||
return nil, E.New("invalid address")
|
||||
} else if address.IsFqdn() {
|
||||
return nil, E.New("domain not resolved")
|
||||
}
|
||||
if d.networkStrategy == nil {
|
||||
switch N.NetworkName(network) {
|
||||
|
||||
@@ -18,6 +18,7 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di
|
||||
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
||||
return nil, false, E.New("no available network interface")
|
||||
}
|
||||
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = N.DefaultFallbackDelay
|
||||
}
|
||||
@@ -31,7 +32,9 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di
|
||||
results := make(chan dialResult) // unbuffered
|
||||
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
|
||||
perNetDialer := dialer
|
||||
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
||||
if defaultInterface == nil || iif.Index != defaultInterface.Index {
|
||||
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
||||
}
|
||||
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
select {
|
||||
@@ -89,6 +92,7 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d
|
||||
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
||||
return nil, false, E.New("no available network interface")
|
||||
}
|
||||
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = N.DefaultFallbackDelay
|
||||
}
|
||||
@@ -103,7 +107,9 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d
|
||||
results := make(chan dialResult) // unbuffered
|
||||
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
|
||||
perNetDialer := dialer
|
||||
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
||||
if defaultInterface == nil || iif.Index != defaultInterface.Index {
|
||||
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
||||
}
|
||||
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
select {
|
||||
@@ -149,10 +155,13 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene
|
||||
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
||||
return nil, E.New("no available network interface")
|
||||
}
|
||||
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
|
||||
var errors []error
|
||||
for _, primaryInterface := range primaryInterfaces {
|
||||
perNetListener := listener
|
||||
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, primaryInterface.Name, primaryInterface.Index))
|
||||
if defaultInterface == nil || primaryInterface.Index != defaultInterface.Index {
|
||||
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, primaryInterface.Name, primaryInterface.Index))
|
||||
}
|
||||
conn, err := perNetListener.ListenPacket(ctx, network, addr)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
@@ -161,7 +170,9 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene
|
||||
}
|
||||
for _, fallbackInterface := range fallbackInterfaces {
|
||||
perNetListener := listener
|
||||
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, fallbackInterface.Name, fallbackInterface.Index))
|
||||
if defaultInterface == nil || fallbackInterface.Index != defaultInterface.Index {
|
||||
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, fallbackInterface.Name, fallbackInterface.Index))
|
||||
}
|
||||
conn, err := perNetListener.ListenPacket(ctx, network, addr)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
|
||||
@@ -52,7 +52,7 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour) {
|
||||
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
||||
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
|
||||
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
|
||||
var defaultOptions adapter.NetworkOptions
|
||||
@@ -104,7 +104,12 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
} else if options.NewDialer {
|
||||
return nil, E.New("missing domain resolver for domain server address")
|
||||
} else {
|
||||
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
|
||||
transports := dnsTransport.Transports()
|
||||
if len(transports) < 2 {
|
||||
dnsQueryOptions.Transport = dnsTransport.Default()
|
||||
} else {
|
||||
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
|
||||
}
|
||||
}
|
||||
dialer = NewResolveDialer(
|
||||
options.Context,
|
||||
|
||||
@@ -44,6 +44,20 @@ type resolveDialer struct {
|
||||
}
|
||||
|
||||
func NewResolveDialer(ctx context.Context, dialer N.Dialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ResolveDialer {
|
||||
if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel {
|
||||
return &resolveParallelNetworkDialer{
|
||||
resolveDialer{
|
||||
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
||||
router: service.FromContext[adapter.DNSRouter](ctx),
|
||||
dialer: dialer,
|
||||
parallel: parallel,
|
||||
server: server,
|
||||
queryOptions: queryOptions,
|
||||
fallbackDelay: fallbackDelay,
|
||||
},
|
||||
parallelDialer,
|
||||
}
|
||||
}
|
||||
return &resolveDialer{
|
||||
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
||||
router: service.FromContext[adapter.DNSRouter](ctx),
|
||||
@@ -60,21 +74,6 @@ type resolveParallelNetworkDialer struct {
|
||||
dialer ParallelInterfaceDialer
|
||||
}
|
||||
|
||||
func NewResolveParallelInterfaceDialer(ctx context.Context, dialer ParallelInterfaceDialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ParallelInterfaceResolveDialer {
|
||||
return &resolveParallelNetworkDialer{
|
||||
resolveDialer{
|
||||
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
||||
router: service.FromContext[adapter.DNSRouter](ctx),
|
||||
dialer: dialer,
|
||||
parallel: parallel,
|
||||
server: server,
|
||||
queryOptions: queryOptions,
|
||||
fallbackDelay: fallbackDelay,
|
||||
},
|
||||
dialer,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *resolveDialer) initialize() error {
|
||||
d.initOnce.Do(d.initServer)
|
||||
return d.initErr
|
||||
|
||||
58
common/sniff/ntp.go
Normal file
58
common/sniff/ntp.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package sniff
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
)
|
||||
|
||||
func NTP(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||
// NTP packets must be at least 48 bytes long (standard NTP header size).
|
||||
pLen := len(packet)
|
||||
if pLen < 48 {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
// Check the LI (Leap Indicator) and Version Number (VN) in the first byte.
|
||||
// We'll primarily focus on ensuring the version is valid for NTP.
|
||||
// Many NTP versions are used, but let's check for generally accepted ones (3 & 4 for IPv4, plus potential extensions/customizations)
|
||||
firstByte := packet[0]
|
||||
li := (firstByte >> 6) & 0x03 // Extract LI
|
||||
vn := (firstByte >> 3) & 0x07 // Extract VN
|
||||
mode := firstByte & 0x07 // Extract Mode
|
||||
|
||||
// Leap Indicator should be a valid value (0-3).
|
||||
if li > 3 {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
// Version Check (common NTP versions are 3 and 4)
|
||||
if vn != 3 && vn != 4 {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
// Check the Mode field for a client request (Mode 3). This validates it *is* a request.
|
||||
if mode != 3 {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
// Check Root Delay and Root Dispersion. While not strictly *required* for a request,
|
||||
// we can check if they appear to be reasonable values (not excessively large).
|
||||
rootDelay := binary.BigEndian.Uint32(packet[4:8])
|
||||
rootDispersion := binary.BigEndian.Uint32(packet[8:12])
|
||||
|
||||
// Check for unreasonably large root delay and dispersion. NTP RFC specifies max values of approximately 16 seconds.
|
||||
// Convert to milliseconds for easy comparison. Each unit is 1/2^16 seconds.
|
||||
if float64(rootDelay)/65536.0 > 16.0 {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
if float64(rootDispersion)/65536.0 > 16.0 {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
metadata.Protocol = C.ProtocolNTP
|
||||
|
||||
return nil
|
||||
}
|
||||
33
common/sniff/ntp_test.go
Normal file
33
common/sniff/ntp_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package sniff_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/sniff"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSniffNTP(t *testing.T) {
|
||||
t.Parallel()
|
||||
packet, err := hex.DecodeString("1b0006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
|
||||
require.NoError(t, err)
|
||||
var metadata adapter.InboundContext
|
||||
err = sniff.NTP(context.Background(), &metadata, packet)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, metadata.Protocol, C.ProtocolNTP)
|
||||
}
|
||||
|
||||
func TestSniffNTPFailed(t *testing.T) {
|
||||
t.Parallel()
|
||||
packet, err := hex.DecodeString("400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
|
||||
require.NoError(t, err)
|
||||
var metadata adapter.InboundContext
|
||||
err = sniff.NTP(context.Background(), &metadata, packet)
|
||||
require.ErrorIs(t, err, os.ErrInvalid)
|
||||
}
|
||||
@@ -12,6 +12,26 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSniffQUICChromeNew(t *testing.T) {
|
||||
t.Parallel()
|
||||
pkt, err := hex.DecodeString("ca0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489ad89c322f75f9a383c90d126a0b21104cb519c2bb32e6a134e86896452e942b26c519b8c7ac9e4c99fae5e1f65cf08fb98443b30e4567932e8fb0789820d8f33037b59ac8113530258c9467dfb52489396dae01f099d28b234efa107fa411f2a1ffa2abe74988e03d662d4296024e95ce0fe1671724937157f77b84990478a2d4060676cf0827b4e8c600654111750414dafa0cccb332f3020c2922a015f445df5edc9c7d2d1ceea9fddcc9ff821c9183aa39a70da20fcc057579e1051c1c899148d6cf9d08b4919822082d040d1ce03ca4f216be6cb7ef03db6df0993ef1ccce5c8c648980554f41704526e1809d2545739f5872e75ec797db1c99f5682e2eda9363cb32aa367b7b363c782ddbacf874183cc15c8a2db068dd4093eebdd096ad33832a7939deb0a872279744f5a56dc001ba62fac973bf680f3b362bdd336add4dd102f462b773bf70bfce1921070a802a92025273a177186d1a643081b42175eb789ccddadb71033ef4feacbf6fd282ab622cf61669d73cda559e411c6ccdd8f003443b6933b7729b7a357aa4aa2fba0f365f829a4d497afb5dc2648a53bc9f3e786d955069d0a4781088a5463747dfe9958ea19ea444eae947ec6a67640955f710f93640084f3fbb8ad259b68dbc0ee0b7fab2d81bffd83ed8a6d33522dbfef43bec0a0fb4bdf1cb712dc4ced0680c0687fa240fd157baa232b1c84e14adce6421cf9270f9b3972f98fc67b344b8a4f1fb551e26f7f76d484ed9f8197f231dc5d9a44cc0ddce73d7f810a620851f4e97eb5037ab5135d7c3be5b80cc32d19910b8387aca64c93c02dc3e35238b78e6aff470722078982e58802844932b6041446bfdcc97ba640cbb86721bcd0f40f27b77aa6287ce5674ec1720134b9302875482c3269787e004b9edb483d44f326eef38c0e83cb46af96488c2e696bc2524567fb29c1e8edcd5a73615496d172d46a9d29e0505c0018b7bbb00165eca0389e09c4b1d73b6cc4a2f735a720650134a2e98e8105e20695cf231b92586237dfe0f99c897414e51c21627496276535f07abb53fb2b554376fe520fa45a3e944fd91dfe7a72aead08842b6b63d8edf861fb911954c83bd9a896eb9da4af5eff646455069d747facd4e77c254096843bff7c3e9031dbdf8dc37ea45f1122922fcbc322ec1378f3c7c1af0da62e1052e6210f1b23073f93a82d90e14cb20bc4501d487a1c848674d57a7c269b13590b3a99d8b8b4f6d0dfbd1d2cbbe7a32c0d5c84ae7ec438b0b19f3862d8fabaa828d06c7e3c6967405cd56a1ae90f38633e2ee0e3ecfca3df399fe12f029e0860a1a30da010300d0c94f0bf56091d00011488c1429928b21c739ebf50ba8be91116315d3173f6d2c56735722478c4d74392ba84d1727036b3d64e8c2263b0f33cb8086be587ca6b3940259c06afa2683868856529303ae12e91d7ca874568be7f2bfaa0656dfab0ed31ed90eaea10fb7f3433ec59a334abe6211d547fa0c825ac45d3691e749d15432008de83e9f6d98f368359137ae803d9189b3386f800c7c0cf4b615d1983cf82d9981a8105b60a80fe66c9b0d439b5ba153dd19e9e7483a01cf3b02b4597540b38e658d4eb8455e030b2bf2690bdd78c23f16fe5")
|
||||
require.NoError(t, err)
|
||||
var metadata adapter.InboundContext
|
||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||
require.Equal(t, metadata.Client, C.ClientChromium)
|
||||
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
|
||||
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
|
||||
require.NoError(t, err)
|
||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
|
||||
pkt, err = hex.DecodeString("c20000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489e2ff30c43a5f63beb2e4501ce7754085bcbe838003a0b4bccb53863c0766df7eac073c2bdc170772b157997945acdc2ab2e84750cc9aa0ffa0fdc023da7fc565a14f87f7c563dbc9183dd226aab79957d263f66e64b85a1b15a24516bd2c7c04eea4fa0a34ef9849c21585db2e4adb7c05e265c4f38d8ffe4cbed0f3b0e68f3693bf1f726c3fb135b8e32a5d22931d7c55fc2ff4b9a354933ab14544df3cdaf3e3217dfb8d7feb3465dc34df6320ea486f12e5b2d609aaa5f4515c20c86fc440f8087be0ee3d339835746ae2573c2afdee6bb6ef7e9eb541feae9209391b2902cfb0bdaccd9da8d290714638b7da588d4a656ca6eabba78b7363922d6037cf060b161a42019d4feb4156459103cffdeefd0e63114af2b0e0c39e70ebc7fecb8dd1ebb8d60b2137f509bb7dcef5f1d3e06ab1d391466652d57440a410fb4f58a6ce1fb62feb453241f64e110709f59a3d9ebdac94f811337d0e4a80fd6b56b2a70cd6eebbf98e1661291da6bf5beb8b8afc376dfd20eb76afe709e8e8f28e0ef82105954e346546ad25973df43f4acddbec0ffd9b215f62abebebf71305b5ea993560316f69430bf5afe50420340622f802b5830f3bcebffff04980c75a59d28902879e5d51a4fb21062a4ae13c42297075b21d54ee04303879c1157e7470c1451673c98a2f3921f2f3e8f6acfe85b01caaca66b59e5ebffbfe68e5e9ab17e9a1b857eb409df91cb76767fc1814fd3c522a9b117edd0b02526e469cb4afb291a4dcc74c79b47ec6e7ce558c597129366f83ec306b11d2598c705fd4ee9ee99df6b7039bef13b08fc6f26853ad213829d24f895747d45a47414f931c583fb6c3e4f6c27d0c2b81a5f3cee390ec6314e1fec637e8d28b675e97caafdfbf8c25d34a635083a7553d219dd80dbb39087d74c6ad6192ca6f48a3ff8d47db41b2a492c63fcd780012780931dae0a325f9dcbd772d09a700f132c4bc1d9809b25b9751b694eb72a8ba4db7208d2b1bab63e1845208e4f841ea30218a559db98751589716b6d059ca673378f5fe7c7d8a1c82e14a561c47313bbcc278412ba86ffb2b87ec308eab9df696f5b4b54f8e361731bf232820a02a35fda7e5d4bf01b8f005ad299a055116e7b23c181f15a66442cf6032ca477bccc55b79d424eb4f245847bd81a581dc369dd20b1a4892733bde3c38e492c0039f69f2b947a4dc251a49ee7ccc0f36b3b75a555fa1d126db75f94dab60f52f6b15a877a0c380b59f82d35c570bc5f8051e9ef87db51f52383d47b50829b7f9e947ccc67aa280566aa48b4a85c1c7eca6f542789d8abcc050f1aa3cc221b6859656a21454aa21c7bfb9d12115f61c3ed46263ade68a8d3679fa62a659a5da7817406bd16618fccf33ed208ada1b03584e8b485d3cb6ed80a0774e60b6cd55aff64169ea998cf8235997049515abac58e0169ca07fb1c8c4c8b2803ba9d27b44c045d0a1cac86e5e188195c68001f53eb44851b6d821fc01ccbb41e27f38e6ddd66540c2d62ed6e0d551e22c0f26b60078c74a6302a1ed3d9e8fc0861257a63f6ac4e759fd54bff088becd28e30944a6c15db4fc8ae6244346869add946d9d92c430d737e042fa18b28a8ed64d1e8987ad9061cdc1335f")
|
||||
require.NoError(t, err)
|
||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "www.google.com", metadata.Domain)
|
||||
}
|
||||
|
||||
func TestSniffQUICChromium(t *testing.T) {
|
||||
t.Parallel()
|
||||
pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620")
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
@@ -34,7 +35,7 @@ func Skip(metadata *adapter.InboundContext) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error {
|
||||
func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffers []*buf.Buffer, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error {
|
||||
if timeout == 0 {
|
||||
timeout = C.ReadPayloadTimeout
|
||||
}
|
||||
@@ -55,7 +56,10 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.
|
||||
}
|
||||
errors = nil
|
||||
for _, sniffer := range sniffers {
|
||||
err = sniffer(ctx, metadata, bytes.NewReader(buffer.Bytes()))
|
||||
reader := io.MultiReader(common.Map(append(buffers, buffer), func(it *buf.Buffer) io.Reader {
|
||||
return bytes.NewReader(it.Bytes())
|
||||
})...)
|
||||
err = sniffer(ctx, metadata, reader)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -29,15 +29,12 @@ func NewClient(ctx context.Context, serverAddress string, options option.Outboun
|
||||
if !options.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
return NewECHClient(ctx, serverAddress, options)
|
||||
} else if options.Reality != nil && options.Reality.Enabled {
|
||||
if options.Reality != nil && options.Reality.Enabled {
|
||||
return NewRealityClient(ctx, serverAddress, options)
|
||||
} else if options.UTLS != nil && options.UTLS.Enabled {
|
||||
return NewUTLSClient(ctx, serverAddress, options)
|
||||
} else {
|
||||
return NewSTDClient(ctx, serverAddress, options)
|
||||
}
|
||||
return NewSTDClient(ctx, serverAddress, options)
|
||||
}
|
||||
|
||||
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
||||
|
||||
174
common/tls/ech.go
Normal file
174
common/tls/ech.go
Normal file
@@ -0,0 +1,174 @@
|
||||
//go:build go1.24
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
)
|
||||
|
||||
func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
|
||||
var echConfig []byte
|
||||
if len(options.ECH.Config) > 0 {
|
||||
echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
|
||||
} else if options.ECH.ConfigPath != "" {
|
||||
content, err := os.ReadFile(options.ECH.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read ECH config")
|
||||
}
|
||||
echConfig = content
|
||||
}
|
||||
//nolint:staticcheck
|
||||
if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled {
|
||||
deprecated.Report(ctx, deprecated.OptionLegacyECHOptions)
|
||||
}
|
||||
if len(echConfig) > 0 {
|
||||
block, rest := pem.Decode(echConfig)
|
||||
if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
|
||||
return nil, E.New("invalid ECH configs pem")
|
||||
}
|
||||
tlsConfig.EncryptedClientHelloConfigList = block.Bytes
|
||||
return &STDClientConfig{tlsConfig}, nil
|
||||
} else {
|
||||
return &STDECHClientConfig{STDClientConfig{tlsConfig}, service.FromContext[adapter.DNSRouter](ctx)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions, tlsConfig *tls.Config, echKeyPath *string) error {
|
||||
var echKey []byte
|
||||
if len(options.ECH.Key) > 0 {
|
||||
echKey = []byte(strings.Join(options.ECH.Key, "\n"))
|
||||
} else if options.ECH.KeyPath != "" {
|
||||
content, err := os.ReadFile(options.ECH.KeyPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "read ECH keys")
|
||||
}
|
||||
echKey = content
|
||||
*echKeyPath = options.ECH.KeyPath
|
||||
} else {
|
||||
return E.New("missing ECH keys")
|
||||
}
|
||||
block, rest := pem.Decode(echKey)
|
||||
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
|
||||
return E.New("invalid ECH keys pem")
|
||||
}
|
||||
echKeys, err := UnmarshalECHKeys(block.Bytes)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse ECH keys")
|
||||
}
|
||||
tlsConfig.EncryptedClientHelloKeys = echKeys
|
||||
//nolint:staticcheck
|
||||
if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled {
|
||||
deprecated.Report(ctx, deprecated.OptionLegacyECHOptions)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
|
||||
echKey, err := os.ReadFile(echKeyPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "reload ECH keys from ", echKeyPath)
|
||||
}
|
||||
block, _ := pem.Decode(echKey)
|
||||
if block == nil || block.Type != "ECH KEYS" {
|
||||
return E.New("invalid ECH keys pem")
|
||||
}
|
||||
echKeys, err := UnmarshalECHKeys(block.Bytes)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse ECH keys")
|
||||
}
|
||||
tlsConfig.EncryptedClientHelloKeys = echKeys
|
||||
return nil
|
||||
}
|
||||
|
||||
type STDECHClientConfig struct {
|
||||
STDClientConfig
|
||||
dnsRouter adapter.DNSRouter
|
||||
}
|
||||
|
||||
func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
|
||||
if len(s.config.EncryptedClientHelloConfigList) == 0 {
|
||||
message := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
RecursionDesired: true,
|
||||
},
|
||||
Question: []mDNS.Question{
|
||||
{
|
||||
Name: mDNS.Fqdn(s.config.ServerName),
|
||||
Qtype: mDNS.TypeHTTPS,
|
||||
Qclass: mDNS.ClassINET,
|
||||
},
|
||||
},
|
||||
}
|
||||
response, err := s.dnsRouter.Exchange(ctx, message, adapter.DNSQueryOptions{})
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "fetch ECH config list")
|
||||
}
|
||||
if response.Rcode != mDNS.RcodeSuccess {
|
||||
return nil, E.Cause(dns.RcodeError(response.Rcode), "fetch ECH config list")
|
||||
}
|
||||
for _, rr := range response.Answer {
|
||||
switch resource := rr.(type) {
|
||||
case *mDNS.HTTPS:
|
||||
for _, value := range resource.Value {
|
||||
if value.Key().String() == "ech" {
|
||||
echConfigList, err := base64.StdEncoding.DecodeString(value.String())
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode ECH config")
|
||||
}
|
||||
s.config.EncryptedClientHelloConfigList = echConfigList
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, E.New("no ECH config found in DNS records")
|
||||
}
|
||||
tlsConn, err := s.Client(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = tlsConn.HandshakeContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
func (s *STDECHClientConfig) Clone() Config {
|
||||
return &STDECHClientConfig{STDClientConfig{s.config.Clone()}, s.dnsRouter}
|
||||
}
|
||||
|
||||
func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {
|
||||
var keys []tls.EncryptedClientHelloKey
|
||||
rawString := cryptobyte.String(raw)
|
||||
for !rawString.Empty() {
|
||||
var key tls.EncryptedClientHelloKey
|
||||
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.PrivateKey)) {
|
||||
return nil, E.New("error parsing private key")
|
||||
}
|
||||
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) {
|
||||
return nil, E.New("error parsing config")
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil, E.New("empty ECH keys")
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
//go:build with_ech
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
cftls "github.com/sagernet/cloudflare-tls"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type echClientConfig struct {
|
||||
config *cftls.Config
|
||||
}
|
||||
|
||||
func (c *echClientConfig) ServerName() string {
|
||||
return c.config.ServerName
|
||||
}
|
||||
|
||||
func (c *echClientConfig) SetServerName(serverName string) {
|
||||
c.config.ServerName = serverName
|
||||
}
|
||||
|
||||
func (c *echClientConfig) NextProtos() []string {
|
||||
return c.config.NextProtos
|
||||
}
|
||||
|
||||
func (c *echClientConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *echClientConfig) Config() (*STDConfig, error) {
|
||||
return nil, E.New("unsupported usage for ECH")
|
||||
}
|
||||
|
||||
func (c *echClientConfig) Client(conn net.Conn) (Conn, error) {
|
||||
return &echConnWrapper{cftls.Client(conn, c.config)}, nil
|
||||
}
|
||||
|
||||
func (c *echClientConfig) Clone() Config {
|
||||
return &echClientConfig{
|
||||
config: c.config.Clone(),
|
||||
}
|
||||
}
|
||||
|
||||
type echConnWrapper struct {
|
||||
*cftls.Conn
|
||||
}
|
||||
|
||||
func (c *echConnWrapper) ConnectionState() tls.ConnectionState {
|
||||
state := c.Conn.ConnectionState()
|
||||
//nolint:staticcheck
|
||||
return tls.ConnectionState{
|
||||
Version: state.Version,
|
||||
HandshakeComplete: state.HandshakeComplete,
|
||||
DidResume: state.DidResume,
|
||||
CipherSuite: state.CipherSuite,
|
||||
NegotiatedProtocol: state.NegotiatedProtocol,
|
||||
NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual,
|
||||
ServerName: state.ServerName,
|
||||
PeerCertificates: state.PeerCertificates,
|
||||
VerifiedChains: state.VerifiedChains,
|
||||
SignedCertificateTimestamps: state.SignedCertificateTimestamps,
|
||||
OCSPResponse: state.OCSPResponse,
|
||||
TLSUnique: state.TLSUnique,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *echConnWrapper) Upstream() any {
|
||||
return c.Conn
|
||||
}
|
||||
|
||||
func NewECHClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
var serverName string
|
||||
if options.ServerName != "" {
|
||||
serverName = options.ServerName
|
||||
} else if serverAddress != "" {
|
||||
if _, err := netip.ParseAddr(serverName); err != nil {
|
||||
serverName = serverAddress
|
||||
}
|
||||
}
|
||||
if serverName == "" && !options.Insecure {
|
||||
return nil, E.New("missing server_name or insecure=true")
|
||||
}
|
||||
|
||||
var tlsConfig cftls.Config
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||
if options.DisableSNI {
|
||||
tlsConfig.ServerName = "127.0.0.1"
|
||||
} else {
|
||||
tlsConfig.ServerName = serverName
|
||||
}
|
||||
if options.Insecure {
|
||||
tlsConfig.InsecureSkipVerify = options.Insecure
|
||||
} else if options.DisableSNI {
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
tlsConfig.VerifyConnection = func(state cftls.ConnectionState) error {
|
||||
verifyOptions := x509.VerifyOptions{
|
||||
DNSName: serverName,
|
||||
Intermediates: x509.NewCertPool(),
|
||||
}
|
||||
for _, cert := range state.PeerCertificates[1:] {
|
||||
verifyOptions.Intermediates.AddCert(cert)
|
||||
}
|
||||
_, err := state.PeerCertificates[0].Verify(verifyOptions)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(options.ALPN) > 0 {
|
||||
tlsConfig.NextProtos = options.ALPN
|
||||
}
|
||||
if options.MinVersion != "" {
|
||||
minVersion, err := ParseTLSVersion(options.MinVersion)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse min_version")
|
||||
}
|
||||
tlsConfig.MinVersion = minVersion
|
||||
}
|
||||
if options.MaxVersion != "" {
|
||||
maxVersion, err := ParseTLSVersion(options.MaxVersion)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse max_version")
|
||||
}
|
||||
tlsConfig.MaxVersion = maxVersion
|
||||
}
|
||||
if options.CipherSuites != nil {
|
||||
find:
|
||||
for _, cipherSuite := range options.CipherSuites {
|
||||
for _, tlsCipherSuite := range cftls.CipherSuites() {
|
||||
if cipherSuite == tlsCipherSuite.Name {
|
||||
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
|
||||
continue find
|
||||
}
|
||||
}
|
||||
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
||||
}
|
||||
}
|
||||
var certificate []byte
|
||||
if len(options.Certificate) > 0 {
|
||||
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
||||
} else if options.CertificatePath != "" {
|
||||
content, err := os.ReadFile(options.CertificatePath)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read certificate")
|
||||
}
|
||||
certificate = content
|
||||
}
|
||||
if len(certificate) > 0 {
|
||||
certPool := x509.NewCertPool()
|
||||
if !certPool.AppendCertsFromPEM(certificate) {
|
||||
return nil, E.New("failed to parse certificate:\n\n", certificate)
|
||||
}
|
||||
tlsConfig.RootCAs = certPool
|
||||
}
|
||||
|
||||
// ECH Config
|
||||
|
||||
tlsConfig.ECHEnabled = true
|
||||
tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
|
||||
tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
|
||||
|
||||
var echConfig []byte
|
||||
if len(options.ECH.Config) > 0 {
|
||||
echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
|
||||
} else if options.ECH.ConfigPath != "" {
|
||||
content, err := os.ReadFile(options.ECH.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read ECH config")
|
||||
}
|
||||
echConfig = content
|
||||
}
|
||||
|
||||
if len(echConfig) > 0 {
|
||||
block, rest := pem.Decode(echConfig)
|
||||
if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
|
||||
return nil, E.New("invalid ECH configs pem")
|
||||
}
|
||||
echConfigs, err := cftls.UnmarshalECHConfigs(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse ECH configs")
|
||||
}
|
||||
tlsConfig.ClientECHConfigs = echConfigs
|
||||
} else {
|
||||
tlsConfig.GetClientECHConfigs = fetchECHClientConfig(ctx)
|
||||
}
|
||||
return &echClientConfig{&tlsConfig}, nil
|
||||
}
|
||||
|
||||
func fetchECHClientConfig(ctx context.Context) func(_ context.Context, serverName string) ([]cftls.ECHConfig, error) {
|
||||
return func(_ context.Context, serverName string) ([]cftls.ECHConfig, error) {
|
||||
message := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
RecursionDesired: true,
|
||||
},
|
||||
Question: []mDNS.Question{
|
||||
{
|
||||
Name: serverName + ".",
|
||||
Qtype: mDNS.TypeHTTPS,
|
||||
Qclass: mDNS.ClassINET,
|
||||
},
|
||||
},
|
||||
}
|
||||
response, err := service.FromContext[adapter.DNSRouter](ctx).Exchange(ctx, message, adapter.DNSQueryOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if response.Rcode != mDNS.RcodeSuccess {
|
||||
return nil, dns.RCodeError(response.Rcode)
|
||||
}
|
||||
for _, rr := range response.Answer {
|
||||
switch resource := rr.(type) {
|
||||
case *mDNS.HTTPS:
|
||||
for _, value := range resource.Value {
|
||||
if value.Key().String() == "ech" {
|
||||
echConfig, err := base64.StdEncoding.DecodeString(value.String())
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode ECH config")
|
||||
}
|
||||
return cftls.UnmarshalECHConfigs(echConfig)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, E.New("unknown resource record type: ", resource.Header().Rrtype)
|
||||
}
|
||||
}
|
||||
return nil, E.New("no ECH config found")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
//go:build with_ech
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
@@ -7,14 +5,13 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
|
||||
cftls "github.com/sagernet/cloudflare-tls"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/cloudflare/circl/hpke"
|
||||
"github.com/cloudflare/circl/kem"
|
||||
)
|
||||
|
||||
func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (configPem string, keyPem string, err error) {
|
||||
func ECHKeygenDefault(serverName string) (configPem string, keyPem string, err error) {
|
||||
cipherSuites := []echCipherSuite{
|
||||
{
|
||||
kdf: hpke.KDF_HKDF_SHA256,
|
||||
@@ -24,13 +21,9 @@ func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (config
|
||||
aead: hpke.AEAD_ChaCha20Poly1305,
|
||||
},
|
||||
}
|
||||
|
||||
keyConfig := []myECHKeyConfig{
|
||||
{id: 0, kem: hpke.KEM_X25519_HKDF_SHA256},
|
||||
}
|
||||
if pqSignatureSchemesEnabled {
|
||||
keyConfig = append(keyConfig, myECHKeyConfig{id: 1, kem: hpke.KEM_X25519_KYBER768_DRAFT00})
|
||||
}
|
||||
|
||||
keyPairs, err := echKeygen(0xfe0d, serverName, keyConfig, cipherSuites)
|
||||
if err != nil {
|
||||
@@ -59,7 +52,6 @@ func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (config
|
||||
|
||||
type echKeyConfigPair struct {
|
||||
id uint8
|
||||
key cftls.EXP_ECHKey
|
||||
rawKey []byte
|
||||
conf myECHKeyConfig
|
||||
rawConf []byte
|
||||
@@ -155,15 +147,6 @@ func echKeygen(version uint16, serverName string, conf []myECHKeyConfig, suite [
|
||||
sk = append(sk, secBuf...)
|
||||
sk = be.AppendUint16(sk, uint16(len(b)))
|
||||
sk = append(sk, b...)
|
||||
|
||||
cfECHKeys, err := cftls.EXP_UnmarshalECHKeys(sk)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "bug: can't parse generated ECH server key")
|
||||
}
|
||||
if len(cfECHKeys) != 1 {
|
||||
return nil, E.New("bug: unexpected server key count")
|
||||
}
|
||||
pair.key = cfECHKeys[0]
|
||||
pair.rawKey = sk
|
||||
|
||||
pairs = append(pairs, pair)
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
//go:build with_quic && with_ech
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/cloudflare-tls"
|
||||
"github.com/sagernet/quic-go/ech"
|
||||
"github.com/sagernet/quic-go/http3_ech"
|
||||
"github.com/sagernet/sing-quic"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
var (
|
||||
_ qtls.Config = (*echClientConfig)(nil)
|
||||
_ qtls.ServerConfig = (*echServerConfig)(nil)
|
||||
)
|
||||
|
||||
func (c *echClientConfig) Dial(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.Connection, error) {
|
||||
return quic.Dial(ctx, conn, addr, c.config, config)
|
||||
}
|
||||
|
||||
func (c *echClientConfig) DialEarly(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.EarlyConnection, error) {
|
||||
return quic.DialEarly(ctx, conn, addr, c.config, config)
|
||||
}
|
||||
|
||||
func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config) http.RoundTripper {
|
||||
return &http3.Transport{
|
||||
TLSClientConfig: c.config,
|
||||
QUICConfig: quicConfig,
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||
quicConn, err := quic.DialEarly(ctx, conn, serverAddr.UDPAddr(), tlsCfg, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
*quicConnPtr = quicConn
|
||||
return quicConn, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *echServerConfig) Listen(conn net.PacketConn, config *quic.Config) (qtls.Listener, error) {
|
||||
return quic.Listen(conn, c.config, config)
|
||||
}
|
||||
|
||||
func (c *echServerConfig) ListenEarly(conn net.PacketConn, config *quic.Config) (qtls.EarlyListener, error) {
|
||||
return quic.ListenEarly(conn, c.config, config)
|
||||
}
|
||||
|
||||
func (c *echServerConfig) ConfigureHTTP3() {
|
||||
http3.ConfigureTLSConfig(c.config)
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
//go:build with_ech
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/pem"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
cftls "github.com/sagernet/cloudflare-tls"
|
||||
"github.com/sagernet/fswatch"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
)
|
||||
|
||||
type echServerConfig struct {
|
||||
config *cftls.Config
|
||||
logger log.Logger
|
||||
certificate []byte
|
||||
key []byte
|
||||
certificatePath string
|
||||
keyPath string
|
||||
echKeyPath string
|
||||
watcher *fswatch.Watcher
|
||||
}
|
||||
|
||||
func (c *echServerConfig) ServerName() string {
|
||||
return c.config.ServerName
|
||||
}
|
||||
|
||||
func (c *echServerConfig) SetServerName(serverName string) {
|
||||
c.config.ServerName = serverName
|
||||
}
|
||||
|
||||
func (c *echServerConfig) NextProtos() []string {
|
||||
return c.config.NextProtos
|
||||
}
|
||||
|
||||
func (c *echServerConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *echServerConfig) Config() (*STDConfig, error) {
|
||||
return nil, E.New("unsupported usage for ECH")
|
||||
}
|
||||
|
||||
func (c *echServerConfig) Client(conn net.Conn) (Conn, error) {
|
||||
return &echConnWrapper{cftls.Client(conn, c.config)}, nil
|
||||
}
|
||||
|
||||
func (c *echServerConfig) Server(conn net.Conn) (Conn, error) {
|
||||
return &echConnWrapper{cftls.Server(conn, c.config)}, nil
|
||||
}
|
||||
|
||||
func (c *echServerConfig) Clone() Config {
|
||||
return &echServerConfig{
|
||||
config: c.config.Clone(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *echServerConfig) Start() error {
|
||||
err := c.startWatcher()
|
||||
if err != nil {
|
||||
c.logger.Warn("create credentials watcher: ", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *echServerConfig) startWatcher() error {
|
||||
var watchPath []string
|
||||
if c.certificatePath != "" {
|
||||
watchPath = append(watchPath, c.certificatePath)
|
||||
}
|
||||
if c.keyPath != "" {
|
||||
watchPath = append(watchPath, c.keyPath)
|
||||
}
|
||||
if c.echKeyPath != "" {
|
||||
watchPath = append(watchPath, c.echKeyPath)
|
||||
}
|
||||
if len(watchPath) == 0 {
|
||||
return nil
|
||||
}
|
||||
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
||||
Path: watchPath,
|
||||
Callback: func(path string) {
|
||||
err := c.credentialsUpdated(path)
|
||||
if err != nil {
|
||||
c.logger.Error(E.Cause(err, "reload credentials"))
|
||||
}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = watcher.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.watcher = watcher
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *echServerConfig) credentialsUpdated(path string) error {
|
||||
if path == c.certificatePath || path == c.keyPath {
|
||||
if path == c.certificatePath {
|
||||
certificate, err := os.ReadFile(c.certificatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.certificate = certificate
|
||||
} else {
|
||||
key, err := os.ReadFile(c.keyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.key = key
|
||||
}
|
||||
keyPair, err := cftls.X509KeyPair(c.certificate, c.key)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse key pair")
|
||||
}
|
||||
c.config.Certificates = []cftls.Certificate{keyPair}
|
||||
c.logger.Info("reloaded TLS certificate")
|
||||
} else {
|
||||
echKeyContent, err := os.ReadFile(c.echKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
block, rest := pem.Decode(echKeyContent)
|
||||
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
|
||||
return E.New("invalid ECH keys pem")
|
||||
}
|
||||
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse ECH keys")
|
||||
}
|
||||
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
|
||||
if err != nil {
|
||||
return E.Cause(err, "create ECH key set")
|
||||
}
|
||||
c.config.ServerECHProvider = echKeySet
|
||||
c.logger.Info("reloaded ECH keys")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *echServerConfig) Close() error {
|
||||
var err error
|
||||
if c.watcher != nil {
|
||||
err = E.Append(err, c.watcher.Close(), func(err error) error {
|
||||
return E.Cause(err, "close credentials watcher")
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func NewECHServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||
if !options.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
var tlsConfig cftls.Config
|
||||
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
||||
return nil, E.New("acme is unavailable in ech")
|
||||
}
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
if options.ServerName != "" {
|
||||
tlsConfig.ServerName = options.ServerName
|
||||
}
|
||||
if len(options.ALPN) > 0 {
|
||||
tlsConfig.NextProtos = append(options.ALPN, tlsConfig.NextProtos...)
|
||||
}
|
||||
if options.MinVersion != "" {
|
||||
minVersion, err := ParseTLSVersion(options.MinVersion)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse min_version")
|
||||
}
|
||||
tlsConfig.MinVersion = minVersion
|
||||
}
|
||||
if options.MaxVersion != "" {
|
||||
maxVersion, err := ParseTLSVersion(options.MaxVersion)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse max_version")
|
||||
}
|
||||
tlsConfig.MaxVersion = maxVersion
|
||||
}
|
||||
if options.CipherSuites != nil {
|
||||
find:
|
||||
for _, cipherSuite := range options.CipherSuites {
|
||||
for _, tlsCipherSuite := range tls.CipherSuites() {
|
||||
if cipherSuite == tlsCipherSuite.Name {
|
||||
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
|
||||
continue find
|
||||
}
|
||||
}
|
||||
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
||||
}
|
||||
}
|
||||
var certificate []byte
|
||||
var key []byte
|
||||
if len(options.Certificate) > 0 {
|
||||
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
||||
} else if options.CertificatePath != "" {
|
||||
content, err := os.ReadFile(options.CertificatePath)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read certificate")
|
||||
}
|
||||
certificate = content
|
||||
}
|
||||
if len(options.Key) > 0 {
|
||||
key = []byte(strings.Join(options.Key, "\n"))
|
||||
} else if options.KeyPath != "" {
|
||||
content, err := os.ReadFile(options.KeyPath)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read key")
|
||||
}
|
||||
key = content
|
||||
}
|
||||
|
||||
if certificate == nil {
|
||||
return nil, E.New("missing certificate")
|
||||
} else if key == nil {
|
||||
return nil, E.New("missing key")
|
||||
}
|
||||
|
||||
keyPair, err := cftls.X509KeyPair(certificate, key)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse x509 key pair")
|
||||
}
|
||||
tlsConfig.Certificates = []cftls.Certificate{keyPair}
|
||||
|
||||
var echKey []byte
|
||||
if len(options.ECH.Key) > 0 {
|
||||
echKey = []byte(strings.Join(options.ECH.Key, "\n"))
|
||||
} else if options.ECH.KeyPath != "" {
|
||||
content, err := os.ReadFile(options.ECH.KeyPath)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read ECH key")
|
||||
}
|
||||
echKey = content
|
||||
} else {
|
||||
return nil, E.New("missing ECH key")
|
||||
}
|
||||
|
||||
block, rest := pem.Decode(echKey)
|
||||
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
|
||||
return nil, E.New("invalid ECH keys pem")
|
||||
}
|
||||
|
||||
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse ECH keys")
|
||||
}
|
||||
|
||||
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create ECH key set")
|
||||
}
|
||||
|
||||
tlsConfig.ECHEnabled = true
|
||||
tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
|
||||
tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
|
||||
tlsConfig.ServerECHProvider = echKeySet
|
||||
|
||||
return &echServerConfig{
|
||||
config: &tlsConfig,
|
||||
logger: logger,
|
||||
certificate: certificate,
|
||||
key: key,
|
||||
certificatePath: options.CertificatePath,
|
||||
keyPath: options.KeyPath,
|
||||
echKeyPath: options.ECH.KeyPath,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,25 +1,23 @@
|
||||
//go:build !with_ech
|
||||
//go:build !go1.24
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var errECHNotIncluded = E.New(`ECH is not included in this build, rebuild with -tags with_ech`)
|
||||
|
||||
func NewECHServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||
return nil, errECHNotIncluded
|
||||
func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
|
||||
return nil, E.New("ECH requires go1.24, please recompile your binary.")
|
||||
}
|
||||
|
||||
func NewECHClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return nil, errECHNotIncluded
|
||||
func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions, tlsConfig *tls.Config, echKeyPath *string) error {
|
||||
return E.New("ECH requires go1.24, please recompile your binary.")
|
||||
}
|
||||
|
||||
func ECHKeygenDefault(host string, pqSignatureSchemesEnabled bool) (configPem string, keyPem string, err error) {
|
||||
return "", "", errECHNotIncluded
|
||||
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
|
||||
return E.New("ECH requires go1.24, please recompile your binary.")
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import (
|
||||
)
|
||||
|
||||
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
||||
if timeFunc == nil {
|
||||
timeFunc = time.Now
|
||||
}
|
||||
privateKeyPem, publicKeyPem, err := GenerateCertificate(parent, parentKey, timeFunc, serverName, timeFunc().Add(time.Hour))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -24,9 +27,6 @@ func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() ti
|
||||
}
|
||||
|
||||
func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string, expire time.Time) (privateKeyPem []byte, publicKeyPem []byte, err error) {
|
||||
if timeFunc == nil {
|
||||
timeFunc = time.Now
|
||||
}
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -16,13 +16,10 @@ func NewServer(ctx context.Context, logger log.Logger, options option.InboundTLS
|
||||
if !options.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
return NewECHServer(ctx, logger, options)
|
||||
} else if options.Reality != nil && options.Reality.Enabled {
|
||||
if options.Reality != nil && options.Reality.Enabled {
|
||||
return NewRealityServer(ctx, logger, options)
|
||||
} else {
|
||||
return NewSTDServer(ctx, logger, options)
|
||||
}
|
||||
return NewSTDServer(ctx, logger, options)
|
||||
}
|
||||
|
||||
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
||||
|
||||
@@ -127,5 +127,8 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
||||
}
|
||||
tlsConfig.RootCAs = certPool
|
||||
}
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
return parseECHClientConfig(ctx, options, &tlsConfig)
|
||||
}
|
||||
return &STDClientConfig{&tlsConfig}, nil
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ type STDServerConfig struct {
|
||||
key []byte
|
||||
certificatePath string
|
||||
keyPath string
|
||||
echKeyPath string
|
||||
watcher *fswatch.Watcher
|
||||
}
|
||||
|
||||
@@ -94,6 +95,9 @@ func (c *STDServerConfig) startWatcher() error {
|
||||
if c.keyPath != "" {
|
||||
watchPath = append(watchPath, c.keyPath)
|
||||
}
|
||||
if c.echKeyPath != "" {
|
||||
watchPath = append(watchPath, c.echKeyPath)
|
||||
}
|
||||
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
||||
Path: watchPath,
|
||||
Callback: func(path string) {
|
||||
@@ -115,25 +119,33 @@ func (c *STDServerConfig) startWatcher() error {
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) certificateUpdated(path string) error {
|
||||
if path == c.certificatePath {
|
||||
certificate, err := os.ReadFile(c.certificatePath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "reload certificate from ", c.certificatePath)
|
||||
if path == c.certificatePath || path == c.keyPath {
|
||||
if path == c.certificatePath {
|
||||
certificate, err := os.ReadFile(c.certificatePath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "reload certificate from ", c.certificatePath)
|
||||
}
|
||||
c.certificate = certificate
|
||||
} else if path == c.keyPath {
|
||||
key, err := os.ReadFile(c.keyPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "reload key from ", c.keyPath)
|
||||
}
|
||||
c.key = key
|
||||
}
|
||||
c.certificate = certificate
|
||||
} else if path == c.keyPath {
|
||||
key, err := os.ReadFile(c.keyPath)
|
||||
keyPair, err := tls.X509KeyPair(c.certificate, c.key)
|
||||
if err != nil {
|
||||
return E.Cause(err, "reload key from ", c.keyPath)
|
||||
return E.Cause(err, "reload key pair")
|
||||
}
|
||||
c.key = key
|
||||
c.config.Certificates = []tls.Certificate{keyPair}
|
||||
c.logger.Info("reloaded TLS certificate")
|
||||
} else if path == c.echKeyPath {
|
||||
err := reloadECHKeys(c.echKeyPath, c.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.logger.Info("reloaded ECH keys")
|
||||
}
|
||||
keyPair, err := tls.X509KeyPair(c.certificate, c.key)
|
||||
if err != nil {
|
||||
return E.Cause(err, "reload key pair")
|
||||
}
|
||||
c.config.Certificates = []tls.Certificate{keyPair}
|
||||
c.logger.Info("reloaded TLS certificate")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -238,6 +250,13 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
||||
tlsConfig.Certificates = []tls.Certificate{keyPair}
|
||||
}
|
||||
}
|
||||
var echKeyPath string
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
err = parseECHServerConfig(ctx, options, tlsConfig, &echKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &STDServerConfig{
|
||||
config: tlsConfig,
|
||||
logger: logger,
|
||||
@@ -246,5 +265,6 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
||||
key: key,
|
||||
certificatePath: options.CertificatePath,
|
||||
keyPath: options.KeyPath,
|
||||
echKeyPath: echKeyPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -15,19 +15,19 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
DNSTypeLegacy = "legacy"
|
||||
DNSTypeUDP = "udp"
|
||||
DNSTypeTCP = "tcp"
|
||||
DNSTypeTLS = "tls"
|
||||
DNSTypeHTTPS = "https"
|
||||
DNSTypeQUIC = "quic"
|
||||
DNSTypeHTTP3 = "h3"
|
||||
DNSTypeHosts = "hosts"
|
||||
DNSTypeLocal = "local"
|
||||
DNSTypePreDefined = "predefined"
|
||||
DNSTypeFakeIP = "fakeip"
|
||||
DNSTypeDHCP = "dhcp"
|
||||
DNSTypeTailscale = "tailscale"
|
||||
DNSTypeLegacy = "legacy"
|
||||
DNSTypeLegacyRcode = "legacy_rcode"
|
||||
DNSTypeUDP = "udp"
|
||||
DNSTypeTCP = "tcp"
|
||||
DNSTypeTLS = "tls"
|
||||
DNSTypeHTTPS = "https"
|
||||
DNSTypeQUIC = "quic"
|
||||
DNSTypeHTTP3 = "h3"
|
||||
DNSTypeLocal = "local"
|
||||
DNSTypeHosts = "hosts"
|
||||
DNSTypeFakeIP = "fakeip"
|
||||
DNSTypeDHCP = "dhcp"
|
||||
DNSTypeTailscale = "tailscale"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
TypeTor = "tor"
|
||||
TypeSSH = "ssh"
|
||||
TypeShadowTLS = "shadowtls"
|
||||
TypeAnyTLS = "anytls"
|
||||
TypeShadowsocksR = "shadowsocksr"
|
||||
TypeVLESS = "vless"
|
||||
TypeTUIC = "tuic"
|
||||
@@ -77,6 +78,8 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "TUIC"
|
||||
case TypeHysteria2:
|
||||
return "Hysteria2"
|
||||
case TypeAnyTLS:
|
||||
return "AnyTLS"
|
||||
case TypeSelector:
|
||||
return "Selector"
|
||||
case TypeURLTest:
|
||||
|
||||
@@ -33,6 +33,7 @@ const (
|
||||
RuleActionTypeHijackDNS = "hijack-dns"
|
||||
RuleActionTypeSniff = "sniff"
|
||||
RuleActionTypeResolve = "resolve"
|
||||
RuleActionTypePredefined = "predefined"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/sagernet/sing/contrab/freelru"
|
||||
"github.com/sagernet/sing/contrab/maphash"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
dns "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -484,7 +484,7 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
|
||||
|
||||
func MessageToAddresses(response *dns.Msg) ([]netip.Addr, error) {
|
||||
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
||||
return nil, RCodeError(response.Rcode)
|
||||
return nil, RcodeError(response.Rcode)
|
||||
}
|
||||
addresses := make([]netip.Addr, 0, len(response.Answer))
|
||||
for _, rawAnswer := range response.Answer {
|
||||
@@ -508,10 +508,10 @@ func wrapError(err error) error {
|
||||
switch dnsErr := err.(type) {
|
||||
case *net.DNSError:
|
||||
if dnsErr.IsNotFound {
|
||||
return RCodeNameError
|
||||
return RcodeNameError
|
||||
}
|
||||
case *net.AddrError:
|
||||
return RCodeNameError
|
||||
return RcodeNameError
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -537,7 +537,7 @@ func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, tim
|
||||
Question: []dns.Question{question},
|
||||
}
|
||||
for _, address := range addresses {
|
||||
if address.Is4() {
|
||||
if address.Is4() && question.Qtype == dns.TypeA {
|
||||
response.Answer = append(response.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
@@ -547,7 +547,7 @@ func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, tim
|
||||
},
|
||||
A: address.AsSlice(),
|
||||
})
|
||||
} else {
|
||||
} else if address.Is6() && question.Qtype == dns.TypeAAAA {
|
||||
response.Answer = append(response.Answer, &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
@@ -561,3 +561,73 @@ func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, tim
|
||||
}
|
||||
return &response
|
||||
}
|
||||
|
||||
func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToLive uint32) *dns.Msg {
|
||||
response := dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: id,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Response: true,
|
||||
},
|
||||
Question: []dns.Question{question},
|
||||
Answer: []dns.RR{
|
||||
&dns.CNAME{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: timeToLive,
|
||||
},
|
||||
Target: record,
|
||||
},
|
||||
},
|
||||
}
|
||||
return &response
|
||||
}
|
||||
|
||||
func FixedResponseTXT(id uint16, question dns.Question, records []string, timeToLive uint32) *dns.Msg {
|
||||
response := dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: id,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Response: true,
|
||||
},
|
||||
Question: []dns.Question{question},
|
||||
Answer: []dns.RR{
|
||||
&dns.TXT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: timeToLive,
|
||||
},
|
||||
Txt: records,
|
||||
},
|
||||
},
|
||||
}
|
||||
return &response
|
||||
}
|
||||
|
||||
func FixedResponseMX(id uint16, question dns.Question, records []*net.MX, timeToLive uint32) *dns.Msg {
|
||||
response := dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: id,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Response: true,
|
||||
},
|
||||
Question: []dns.Question{question},
|
||||
}
|
||||
for _, record := range records {
|
||||
response.Answer = append(response.Answer, &dns.MX{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: timeToLive,
|
||||
},
|
||||
Preference: record.Pref,
|
||||
Mx: record.Host,
|
||||
})
|
||||
}
|
||||
return &response
|
||||
}
|
||||
|
||||
38
dns/rcode.go
38
dns/rcode.go
@@ -1,33 +1,17 @@
|
||||
package dns
|
||||
|
||||
import F "github.com/sagernet/sing/common/format"
|
||||
|
||||
const (
|
||||
RCodeSuccess RCodeError = 0 // NoError
|
||||
RCodeFormatError RCodeError = 1 // FormErr
|
||||
RCodeServerFailure RCodeError = 2 // ServFail
|
||||
RCodeNameError RCodeError = 3 // NXDomain
|
||||
RCodeNotImplemented RCodeError = 4 // NotImp
|
||||
RCodeRefused RCodeError = 5 // Refused
|
||||
import (
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type RCodeError uint16
|
||||
const (
|
||||
RcodeFormatError RcodeError = mDNS.RcodeFormatError
|
||||
RcodeNameError RcodeError = mDNS.RcodeNameError
|
||||
RcodeRefused RcodeError = mDNS.RcodeRefused
|
||||
)
|
||||
|
||||
func (e RCodeError) Error() string {
|
||||
switch e {
|
||||
case RCodeSuccess:
|
||||
return "success"
|
||||
case RCodeFormatError:
|
||||
return "format error"
|
||||
case RCodeServerFailure:
|
||||
return "server failure"
|
||||
case RCodeNameError:
|
||||
return "name error"
|
||||
case RCodeNotImplemented:
|
||||
return "not implemented"
|
||||
case RCodeRefused:
|
||||
return "refused"
|
||||
default:
|
||||
return F.ToString("unknown error: ", uint16(e))
|
||||
}
|
||||
type RcodeError int
|
||||
|
||||
func (e RcodeError) Error() string {
|
||||
return mDNS.RcodeToString[int(e)]
|
||||
}
|
||||
|
||||
@@ -190,6 +190,8 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
|
||||
}
|
||||
case *R.RuleActionReject:
|
||||
return nil, currentRule, currentRuleIndex
|
||||
case *R.RuleActionPredefined:
|
||||
return nil, currentRule, currentRuleIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,6 +262,21 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
case C.RuleActionRejectMethodDrop:
|
||||
return nil, tun.ErrDrop
|
||||
}
|
||||
case *R.RuleActionPredefined:
|
||||
return &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: message.Id,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
RecursionDesired: true,
|
||||
RecursionAvailable: true,
|
||||
Rcode: action.Rcode,
|
||||
},
|
||||
Question: message.Question,
|
||||
Answer: action.Answer,
|
||||
Ns: action.Ns,
|
||||
Extra: action.Extra,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
var responseCheck func(responseAddrs []netip.Addr) bool
|
||||
@@ -329,13 +346,13 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
||||
}
|
||||
} else if len(responseAddrs) == 0 {
|
||||
r.logger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result")
|
||||
err = RCodeNameError
|
||||
err = RcodeNameError
|
||||
}
|
||||
}
|
||||
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
|
||||
if cached {
|
||||
if len(responseAddrs) == 0 {
|
||||
return nil, RCodeNameError
|
||||
return nil, RcodeNameError
|
||||
}
|
||||
return responseAddrs, nil
|
||||
}
|
||||
@@ -366,7 +383,8 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
||||
ruleIndex = -1
|
||||
for {
|
||||
dnsCtx := adapter.OverrideContext(ctx)
|
||||
transport, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true, &options)
|
||||
dnsOptions := options
|
||||
transport, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true, &dnsOptions)
|
||||
if rule != nil {
|
||||
switch action := rule.Action().(type) {
|
||||
case *R.RuleActionReject:
|
||||
@@ -376,6 +394,20 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
||||
case C.RuleActionRejectMethodDrop:
|
||||
return nil, tun.ErrDrop
|
||||
}
|
||||
case *R.RuleActionPredefined:
|
||||
if action.Rcode != mDNS.RcodeSuccess {
|
||||
err = RcodeError(action.Rcode)
|
||||
} else {
|
||||
for _, answer := range action.Answer {
|
||||
switch record := answer.(type) {
|
||||
case *mDNS.A:
|
||||
responseAddrs = append(responseAddrs, M.AddrFromIP(record.A))
|
||||
case *mDNS.AAAA:
|
||||
responseAddrs = append(responseAddrs, M.AddrFromIP(record.AAAA))
|
||||
}
|
||||
}
|
||||
}
|
||||
goto response
|
||||
}
|
||||
}
|
||||
var responseCheck func(responseAddrs []netip.Addr) bool
|
||||
@@ -385,16 +417,17 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
||||
return rule.MatchAddressLimit(metadata)
|
||||
}
|
||||
}
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = r.defaultDomainStrategy
|
||||
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
||||
dnsOptions.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
responseAddrs, err = r.client.Lookup(dnsCtx, transport, domain, options, responseCheck)
|
||||
responseAddrs, err = r.client.Lookup(dnsCtx, transport, domain, dnsOptions, responseCheck)
|
||||
if responseCheck == nil || err == nil {
|
||||
break
|
||||
}
|
||||
printResult()
|
||||
}
|
||||
}
|
||||
response:
|
||||
printResult()
|
||||
if len(responseAddrs) > 0 {
|
||||
r.logger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(responseAddrs), " "))
|
||||
|
||||
@@ -55,7 +55,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
|
||||
if question.Qtype != mDNS.TypeA && question.Qtype != mDNS.TypeAAAA {
|
||||
return nil, E.New("only IP queries are supported by fakeip")
|
||||
}
|
||||
address, err := t.store.Create(question.Name, question.Qtype == mDNS.TypeAAAA)
|
||||
address, err := t.store.Create(dns.FqdnToDomain(question.Name), question.Qtype == mDNS.TypeAAAA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,12 +2,15 @@ package hosts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
@@ -20,21 +23,31 @@ var _ adapter.DNSTransport = (*Transport)(nil)
|
||||
|
||||
type Transport struct {
|
||||
dns.TransportAdapter
|
||||
files []*File
|
||||
files []*File
|
||||
predefined map[string][]netip.Addr
|
||||
}
|
||||
|
||||
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.HostsDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
var files []*File
|
||||
var (
|
||||
files []*File
|
||||
predefined = make(map[string][]netip.Addr)
|
||||
)
|
||||
if len(options.Path) == 0 {
|
||||
files = append(files, NewFile(DefaultPath))
|
||||
} else {
|
||||
for _, path := range options.Path {
|
||||
files = append(files, NewFile(path))
|
||||
files = append(files, NewFile(filemanager.BasePath(ctx, os.ExpandEnv(path))))
|
||||
}
|
||||
}
|
||||
if options.Predefined != nil {
|
||||
for _, entry := range options.Predefined.Entries() {
|
||||
predefined[mDNS.CanonicalName(entry.Key)] = entry.Value
|
||||
}
|
||||
}
|
||||
return &Transport{
|
||||
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeHosts, tag, nil),
|
||||
files: files,
|
||||
predefined: predefined,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -43,8 +56,11 @@ func (t *Transport) Reset() {
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
question := message.Question[0]
|
||||
domain := dns.FqdnToDomain(question.Name)
|
||||
domain := mDNS.CanonicalName(question.Name)
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||
if addresses, ok := t.predefined[domain]; ok {
|
||||
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||
}
|
||||
for _, file := range t.files {
|
||||
addresses := file.Lookup(domain)
|
||||
if len(addresses) > 0 {
|
||||
|
||||
@@ -34,7 +34,7 @@ func (f *File) Lookup(name string) []netip.Addr {
|
||||
f.access.Lock()
|
||||
defer f.access.Unlock()
|
||||
f.update()
|
||||
return f.byName[name]
|
||||
return f.byName[dns.CanonicalName(name)]
|
||||
}
|
||||
|
||||
func (f *File) update() {
|
||||
|
||||
@@ -11,6 +11,6 @@ import (
|
||||
|
||||
func TestHosts(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Equal(t, []netip.Addr{netip.AddrFrom4([4]byte{127, 0, 0, 1}), netip.IPv6Loopback()}, hosts.NewFile("testdata/hosts").Lookup("localhost."))
|
||||
require.NotEmpty(t, hosts.NewFile(hosts.DefaultPath).Lookup("localhost."))
|
||||
require.Equal(t, []netip.Addr{netip.AddrFrom4([4]byte{127, 0, 0, 1}), netip.IPv6Loopback()}, hosts.NewFile("testdata/hosts").Lookup("localhost"))
|
||||
require.NotEmpty(t, hosts.NewFile(hosts.DefaultPath).Lookup("localhost"))
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverAddr := options.ServerOptions.Build()
|
||||
serverAddr := options.DNSServerAddressOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 443
|
||||
}
|
||||
|
||||
@@ -19,14 +19,11 @@ import (
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewTransport)
|
||||
}
|
||||
|
||||
var _ adapter.DNSTransport = (*Transport)(nil)
|
||||
|
||||
type Transport struct {
|
||||
dns.TransportAdapter
|
||||
ctx context.Context
|
||||
hosts *hosts.File
|
||||
dialer N.Dialer
|
||||
}
|
||||
@@ -142,6 +139,9 @@ func (t *Transport) tryOneName(ctx context.Context, config *dnsConfig, fqdn stri
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question, timeout time.Duration, useTCP, ad bool) (*mDNS.Msg, error) {
|
||||
if server.Port == 0 {
|
||||
server.Port = 53
|
||||
}
|
||||
var networks []string
|
||||
if useTCP {
|
||||
networks = []string{N.NetworkTCP}
|
||||
|
||||
205
dns/transport/local/local_fallback.go
Normal file
205
dns/transport/local/local_fallback.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewFallbackTransport)
|
||||
}
|
||||
|
||||
type FallbackTransport struct {
|
||||
adapter.DNSTransport
|
||||
ctx context.Context
|
||||
fallback bool
|
||||
resolver net.Resolver
|
||||
}
|
||||
|
||||
func NewFallbackTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
transport, err := NewTransport(ctx, logger, tag, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &FallbackTransport{
|
||||
DNSTransport: transport,
|
||||
ctx: ctx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *FallbackTransport) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
platformInterface := service.FromContext[platform.Interface](f.ctx)
|
||||
if platformInterface == nil {
|
||||
return nil
|
||||
}
|
||||
inboundManager := service.FromContext[adapter.InboundManager](f.ctx)
|
||||
for _, inbound := range inboundManager.Inbounds() {
|
||||
if inbound.Type() == C.TypeTun {
|
||||
// platform tun hijacks DNS, so we can only use cgo resolver here
|
||||
f.fallback = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FallbackTransport) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
if !f.fallback {
|
||||
return f.DNSTransport.Exchange(ctx, message)
|
||||
}
|
||||
question := message.Question[0]
|
||||
domain := dns.FqdnToDomain(question.Name)
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||
var network string
|
||||
if question.Qtype == mDNS.TypeA {
|
||||
network = "ip4"
|
||||
} else {
|
||||
network = "ip6"
|
||||
}
|
||||
addresses, err := f.resolver.LookupNetIP(ctx, network, domain)
|
||||
if err != nil {
|
||||
var dnsError *net.DNSError
|
||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
||||
return nil, dns.RcodeRefused
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||
} else if question.Qtype == mDNS.TypeNS {
|
||||
records, err := f.resolver.LookupNS(ctx, domain)
|
||||
if err != nil {
|
||||
var dnsError *net.DNSError
|
||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
||||
return nil, dns.RcodeRefused
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
response := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: message.Id,
|
||||
Rcode: mDNS.RcodeSuccess,
|
||||
Response: true,
|
||||
},
|
||||
Question: []mDNS.Question{question},
|
||||
}
|
||||
for _, record := range records {
|
||||
response.Answer = append(response.Answer, &mDNS.NS{
|
||||
Hdr: mDNS.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: mDNS.TypeNS,
|
||||
Class: mDNS.ClassINET,
|
||||
Ttl: C.DefaultDNSTTL,
|
||||
},
|
||||
Ns: record.Host,
|
||||
})
|
||||
}
|
||||
return response, nil
|
||||
} else if question.Qtype == mDNS.TypeCNAME {
|
||||
cname, err := f.resolver.LookupCNAME(ctx, domain)
|
||||
if err != nil {
|
||||
var dnsError *net.DNSError
|
||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
||||
return nil, dns.RcodeRefused
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: message.Id,
|
||||
Rcode: mDNS.RcodeSuccess,
|
||||
Response: true,
|
||||
},
|
||||
Question: []mDNS.Question{question},
|
||||
Answer: []mDNS.RR{
|
||||
&mDNS.CNAME{
|
||||
Hdr: mDNS.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: mDNS.TypeCNAME,
|
||||
Class: mDNS.ClassINET,
|
||||
Ttl: C.DefaultDNSTTL,
|
||||
},
|
||||
Target: cname,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
} else if question.Qtype == mDNS.TypeTXT {
|
||||
records, err := f.resolver.LookupTXT(ctx, domain)
|
||||
if err != nil {
|
||||
var dnsError *net.DNSError
|
||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
||||
return nil, dns.RcodeRefused
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: message.Id,
|
||||
Rcode: mDNS.RcodeSuccess,
|
||||
Response: true,
|
||||
},
|
||||
Question: []mDNS.Question{question},
|
||||
Answer: []mDNS.RR{
|
||||
&mDNS.TXT{
|
||||
Hdr: mDNS.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: mDNS.TypeCNAME,
|
||||
Class: mDNS.ClassINET,
|
||||
Ttl: C.DefaultDNSTTL,
|
||||
},
|
||||
Txt: records,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
} else if question.Qtype == mDNS.TypeMX {
|
||||
records, err := f.resolver.LookupMX(ctx, domain)
|
||||
if err != nil {
|
||||
var dnsError *net.DNSError
|
||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
||||
return nil, dns.RcodeRefused
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
response := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: message.Id,
|
||||
Rcode: mDNS.RcodeSuccess,
|
||||
Response: true,
|
||||
},
|
||||
Question: []mDNS.Question{question},
|
||||
}
|
||||
for _, record := range records {
|
||||
response.Answer = append(response.Answer, &mDNS.MX{
|
||||
Hdr: mDNS.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: mDNS.TypeA,
|
||||
Class: mDNS.ClassINET,
|
||||
Ttl: C.DefaultDNSTTL,
|
||||
},
|
||||
Preference: record.Pref,
|
||||
Mx: record.Host,
|
||||
})
|
||||
}
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, E.New("only A, AAAA, NS, CNAME, TXT, MX queries are supported on current platform when using TUN, please switch to a fixed DNS server.")
|
||||
}
|
||||
}
|
||||
53
dns/transport/local/resolv_darwin_cgo.go
Normal file
53
dns/transport/local/resolv_darwin_cgo.go
Normal file
@@ -0,0 +1,53 @@
|
||||
//go:build darwin && cgo
|
||||
|
||||
package local
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <resolv.h>
|
||||
#include <arpa/inet.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func dnsReadConfig(_ string) *dnsConfig {
|
||||
if C.res_init() != 0 {
|
||||
return &dnsConfig{
|
||||
servers: defaultNS,
|
||||
search: dnsDefaultSearch(),
|
||||
ndots: 1,
|
||||
timeout: 5 * time.Second,
|
||||
attempts: 2,
|
||||
err: E.New("libresolv initialization failed"),
|
||||
}
|
||||
}
|
||||
conf := &dnsConfig{
|
||||
ndots: 1,
|
||||
timeout: 5 * time.Second,
|
||||
attempts: int(C._res.retry),
|
||||
}
|
||||
for i := 0; i < int(C._res.nscount); i++ {
|
||||
ns := C._res.nsaddr_list[i]
|
||||
addr := C.inet_ntoa(ns.sin_addr)
|
||||
if addr == nil {
|
||||
continue
|
||||
}
|
||||
conf.servers = append(conf.servers, C.GoString(addr))
|
||||
}
|
||||
for i := 0; ; i++ {
|
||||
search := C._res.dnsrch[i]
|
||||
if search == nil {
|
||||
break
|
||||
}
|
||||
conf.search = append(conf.search, dns.Fqdn(C.GoString(search)))
|
||||
}
|
||||
return conf
|
||||
}
|
||||
23
dns/transport/local/resolv_default.go
Normal file
23
dns/transport/local/resolv_default.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
//go:linkname defaultNS net.defaultNS
|
||||
var defaultNS []string
|
||||
|
||||
func dnsDefaultSearch() []string {
|
||||
hn, err := os.Hostname()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if i := strings.IndexRune(hn, '.'); i >= 0 && i < len(hn)-1 {
|
||||
return []string{dns.Fqdn(hn[i+1:])}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !windows
|
||||
//go:build !windows && !(darwin && cgo)
|
||||
|
||||
package local
|
||||
|
||||
@@ -9,7 +9,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func dnsReadConfig(name string) *dnsConfig {
|
||||
@@ -69,13 +70,13 @@ func dnsReadConfig(name string) *dnsConfig {
|
||||
}
|
||||
case "domain":
|
||||
if len(f) > 1 {
|
||||
conf.search = []string{ensureRooted(f[1])}
|
||||
conf.search = []string{dns.Fqdn(f[1])}
|
||||
}
|
||||
|
||||
case "search":
|
||||
conf.search = make([]string, 0, len(f)-1)
|
||||
for i := 1; i < len(f); i++ {
|
||||
name := ensureRooted(f[i])
|
||||
name := dns.Fqdn(f[i])
|
||||
if name == "." {
|
||||
continue
|
||||
}
|
||||
@@ -137,27 +138,6 @@ func dnsReadConfig(name string) *dnsConfig {
|
||||
return conf
|
||||
}
|
||||
|
||||
//go:linkname defaultNS net.defaultNS
|
||||
var defaultNS []string
|
||||
|
||||
func dnsDefaultSearch() []string {
|
||||
hn, err := os.Hostname()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if i := strings.IndexRune(hn, '.'); i >= 0 && i < len(hn)-1 {
|
||||
return []string{ensureRooted(hn[i+1:])}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureRooted(s string) string {
|
||||
if len(s) > 0 && s[len(s)-1] == '.' {
|
||||
return s
|
||||
}
|
||||
return s + "."
|
||||
}
|
||||
|
||||
const big = 0xFFFFFF
|
||||
|
||||
func dtoi(s string) (n int, i int, ok bool) {
|
||||
|
||||
@@ -69,9 +69,6 @@ func dnsReadConfig(_ string) *dnsConfig {
|
||||
return conf
|
||||
}
|
||||
|
||||
//go:linkname defaultNS net.defaultNS
|
||||
var defaultNS []string
|
||||
|
||||
func adapterAddresses() ([]*windows.IpAdapterAddresses, error) {
|
||||
var b []byte
|
||||
l := uint32(15000) // recommended initial size
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var _ adapter.DNSTransport = (*PredefinedTransport)(nil)
|
||||
|
||||
func RegisterPredefined(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.PredefinedDNSServerOptions](registry, C.DNSTypePreDefined, NewPredefined)
|
||||
}
|
||||
|
||||
type PredefinedTransport struct {
|
||||
dns.TransportAdapter
|
||||
responses []*predefinedResponse
|
||||
}
|
||||
|
||||
type predefinedResponse struct {
|
||||
questions []mDNS.Question
|
||||
answer *mDNS.Msg
|
||||
}
|
||||
|
||||
func NewPredefined(ctx context.Context, logger log.ContextLogger, tag string, options option.PredefinedDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
var responses []*predefinedResponse
|
||||
for _, response := range options.Responses {
|
||||
questions, msg, err := response.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
responses = append(responses, &predefinedResponse{
|
||||
questions: questions,
|
||||
answer: msg,
|
||||
})
|
||||
}
|
||||
if len(responses) == 0 {
|
||||
return nil, E.New("empty predefined responses")
|
||||
}
|
||||
return &PredefinedTransport{
|
||||
TransportAdapter: dns.NewTransportAdapter(C.DNSTypePreDefined, tag, nil),
|
||||
responses: responses,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *PredefinedTransport) Reset() {
|
||||
}
|
||||
|
||||
func (t *PredefinedTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
for _, response := range t.responses {
|
||||
for _, question := range response.questions {
|
||||
if func() bool {
|
||||
if question.Name == "" && question.Qtype == mDNS.TypeNone {
|
||||
return true
|
||||
} else if question.Name == "" {
|
||||
return common.Any(message.Question, func(it mDNS.Question) bool {
|
||||
return it.Qtype == question.Qtype
|
||||
})
|
||||
} else if question.Qtype == mDNS.TypeNone {
|
||||
return common.Any(message.Question, func(it mDNS.Question) bool {
|
||||
return it.Name == question.Name
|
||||
})
|
||||
} else {
|
||||
return common.Contains(message.Question, question)
|
||||
}
|
||||
}() {
|
||||
copyAnswer := *response.answer
|
||||
copyAnswer.Id = message.Id
|
||||
return ©Answer, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, dns.RCodeNameError
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
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"
|
||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
||||
|
||||
@@ -72,7 +71,7 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
}
|
||||
}
|
||||
destinationURL := url.URL{
|
||||
Scheme: "HTTP3",
|
||||
Scheme: "https",
|
||||
Host: host,
|
||||
}
|
||||
if destinationURL.Host == "" {
|
||||
@@ -89,7 +88,7 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverAddr := options.ServerOptions.Build()
|
||||
serverAddr := options.DNSServerAddressOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 443
|
||||
}
|
||||
@@ -101,8 +100,7 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
headers: headers,
|
||||
transport: &http3.Transport{
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||
destinationAddr := M.ParseSocksaddr(addr)
|
||||
conn, dialErr := transportDialer.DialContext(ctx, N.NetworkUDP, destinationAddr)
|
||||
conn, dialErr := transportDialer.DialContext(ctx, N.NetworkUDP, serverAddr)
|
||||
if dialErr != nil {
|
||||
return nil, dialErr
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
if len(tlsConfig.NextProtos()) == 0 {
|
||||
tlsConfig.SetNextProtos([]string{"doq"})
|
||||
}
|
||||
serverAddr := options.ServerOptions.Build()
|
||||
serverAddr := options.DNSServerAddressOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 853
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func NewTCP(ctx context.Context, logger log.ContextLogger, tag string, options o
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverAddr := options.ServerOptions.Build()
|
||||
serverAddr := options.DNSServerAddressOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 53
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverAddr := options.ServerOptions.Build()
|
||||
serverAddr := options.DNSServerAddressOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 853
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options o
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverAddr := options.ServerOptions.Build()
|
||||
serverAddr := options.DNSServerAddressOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 53
|
||||
}
|
||||
@@ -110,13 +110,6 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
|
||||
conn.access.Lock()
|
||||
delete(conn.callbacks, messageId)
|
||||
conn.access.Unlock()
|
||||
callback.access.Lock()
|
||||
select {
|
||||
case <-callback.done:
|
||||
default:
|
||||
close(callback.done)
|
||||
}
|
||||
callback.access.Unlock()
|
||||
}()
|
||||
rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes())
|
||||
if err != nil {
|
||||
|
||||
@@ -56,12 +56,12 @@ func (m *TransportManager) Start(stage adapter.StartStage) error {
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
outbounds := m.transports
|
||||
transports := m.transports
|
||||
m.access.Unlock()
|
||||
if stage == adapter.StartStateStart {
|
||||
return m.startTransports(m.transports)
|
||||
} else {
|
||||
for _, outbound := range outbounds {
|
||||
for _, outbound := range transports {
|
||||
err := adapter.LegacyStart(outbound, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " dns/", outbound.Type(), "[", outbound.Tag(), "]")
|
||||
|
||||
@@ -2,10 +2,81 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.12.0-alpha.8
|
||||
#### 1.12.0-alpha.18
|
||||
|
||||
* Add wildcard SNI support for ShadowTLS inbound **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
See [ShadowTLS](/configuration/inbound/shadowtls/#wildcard_sni).
|
||||
|
||||
#### 1.12.0-alpha.17
|
||||
|
||||
* Add NTP sniffer **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
See [Protocol Sniff](/configuration/route/sniff/).
|
||||
|
||||
#### 1.12.0-alpha.16
|
||||
|
||||
* Update `domain_resolver` behavior **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
`route.default_domain_resolver` or `outbound.domain_resolver` is now optional when only one DNS server is configured.
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/#domain_resolver).
|
||||
|
||||
### 1.11.5
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._
|
||||
|
||||
#### 1.12.0-alpha.13
|
||||
|
||||
* Move `predefined` DNS server to DNS rule action **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
See [DNS Rule Action](/configuration/dns/rule_action/#predefined).
|
||||
|
||||
### 1.11.4
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.0-alpha.11
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.0-alpha.10
|
||||
|
||||
* Add AnyTLS protocol **1**
|
||||
* Improve `resolve` route action **2**
|
||||
* Migrate to stdlib ECH implementation **3**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
The new AnyTLS protocol claims to mitigate TLS proxy traffic characteristics and comes with a new multiplexing scheme.
|
||||
|
||||
See [AnyTLS Inbound](/configuration/inbound/anytls/) and [AnyTLS Outbound](/configuration/outbound/anytls/).
|
||||
|
||||
**2**:
|
||||
|
||||
`resolve` route action now accepts `disable_cache` and other options like in DNS route actions, see [Route Action](/configuration/route/rule_action).
|
||||
|
||||
**3**:
|
||||
|
||||
See [TLS](/configuration/shared/tls).
|
||||
|
||||
The build tag `with_ech` is no longer needed and has been removed.
|
||||
|
||||
#### 1.12.0-alpha.7
|
||||
|
||||
* Add Tailscale DNS server **1**
|
||||
|
||||
@@ -7,6 +7,10 @@ icon: material/apple
|
||||
SFI/SFM/SFT allows users to manage and run local or remote sing-box configuration files, and provides
|
||||
platform-specific function implementation, such as TUN transparent proxy implementation.
|
||||
|
||||
!!! failure ""
|
||||
|
||||
We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected).
|
||||
|
||||
## :material-graph: Requirements
|
||||
|
||||
* iOS 15.0+ / macOS 13.0+ / Apple tvOS 17.0+
|
||||
|
||||
@@ -4,6 +4,7 @@ icon: material/alert-decagram
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [ip_accept_any](#ip_accept_any)
|
||||
:material-delete-clock: [outbound](#outbound)
|
||||
|
||||
!!! quote "Changes in sing-box 1.11.0"
|
||||
@@ -77,15 +78,6 @@ icon: material/alert-decagram
|
||||
"domain_regex": [
|
||||
"^stun\\..+"
|
||||
],
|
||||
"geosite": [
|
||||
"cn"
|
||||
],
|
||||
"source_geoip": [
|
||||
"private"
|
||||
],
|
||||
"geoip": [
|
||||
"cn"
|
||||
],
|
||||
"source_ip_cidr": [
|
||||
"10.0.0.0/24",
|
||||
"192.168.0.1"
|
||||
@@ -96,6 +88,7 @@ icon: material/alert-decagram
|
||||
"192.168.0.1"
|
||||
],
|
||||
"ip_is_private": false,
|
||||
"ip_accept_any": false,
|
||||
"source_port": [
|
||||
12345
|
||||
],
|
||||
@@ -147,8 +140,6 @@ icon: material/alert-decagram
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
],
|
||||
// deprecated
|
||||
"rule_set_ipcidr_match_source": false,
|
||||
"rule_set_ip_cidr_match_source": false,
|
||||
"rule_set_ip_cidr_accept_empty": false,
|
||||
"invert": false,
|
||||
@@ -156,7 +147,20 @@ icon: material/alert-decagram
|
||||
"direct"
|
||||
],
|
||||
"action": "route",
|
||||
"server": "local"
|
||||
"server": "local",
|
||||
|
||||
// Deprecated
|
||||
|
||||
"rule_set_ipcidr_match_source": false,
|
||||
"geosite": [
|
||||
"cn"
|
||||
],
|
||||
"source_geoip": [
|
||||
"private"
|
||||
],
|
||||
"geoip": [
|
||||
"cn"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "logical",
|
||||
@@ -451,7 +455,9 @@ Only takes effect for address requests (A/AAAA/HTTPS). When the query results do
|
||||
|
||||
#### geoip
|
||||
|
||||
!!! question "Since sing-box 1.9.0"
|
||||
!!! failure "Removed in sing-box 1.12.0"
|
||||
|
||||
GeoIP is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets).
|
||||
|
||||
Match GeoIP with query response.
|
||||
|
||||
@@ -473,6 +479,12 @@ Match private IP with query response.
|
||||
|
||||
Make `ip_cidr` rules in rule-sets accept empty query response.
|
||||
|
||||
#### ip_accept_any
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
Match any IP with query response.
|
||||
|
||||
### Logical Fields
|
||||
|
||||
#### type
|
||||
|
||||
@@ -4,6 +4,7 @@ icon: material/alert-decagram
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [ip_accept_any](#ip_accept_any)
|
||||
:material-delete-clock: [outbound](#outbound)
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
@@ -77,15 +78,6 @@ icon: material/alert-decagram
|
||||
"domain_regex": [
|
||||
"^stun\\..+"
|
||||
],
|
||||
"geosite": [
|
||||
"cn"
|
||||
],
|
||||
"source_geoip": [
|
||||
"private"
|
||||
],
|
||||
"geoip": [
|
||||
"cn"
|
||||
],
|
||||
"source_ip_cidr": [
|
||||
"10.0.0.0/24",
|
||||
"192.168.0.1"
|
||||
@@ -96,6 +88,7 @@ icon: material/alert-decagram
|
||||
"192.168.0.1"
|
||||
],
|
||||
"ip_is_private": false,
|
||||
"ip_accept_any": false,
|
||||
"source_port": [
|
||||
12345
|
||||
],
|
||||
@@ -147,8 +140,6 @@ icon: material/alert-decagram
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
],
|
||||
// 已弃用
|
||||
"rule_set_ipcidr_match_source": false,
|
||||
"rule_set_ip_cidr_match_source": false,
|
||||
"rule_set_ip_cidr_accept_empty": false,
|
||||
"invert": false,
|
||||
@@ -156,7 +147,19 @@ icon: material/alert-decagram
|
||||
"direct"
|
||||
],
|
||||
"action": "route",
|
||||
"server": "local"
|
||||
"server": "local",
|
||||
|
||||
// 已弃用
|
||||
"rule_set_ipcidr_match_source": false,
|
||||
"geosite": [
|
||||
"cn"
|
||||
],
|
||||
"source_geoip": [
|
||||
"private"
|
||||
],
|
||||
"geoip": [
|
||||
"cn"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "logical",
|
||||
@@ -232,17 +235,17 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
#### geosite
|
||||
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
!!! failure "已在 sing-box 1.12.0 中被移除"
|
||||
|
||||
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。
|
||||
GeoSite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geosite)。
|
||||
|
||||
匹配 Geosite。
|
||||
|
||||
#### source_geoip
|
||||
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
!!! failure "已在 sing-box 1.12.0 中被移除"
|
||||
|
||||
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。
|
||||
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
|
||||
|
||||
匹配源 GeoIP。
|
||||
|
||||
@@ -451,7 +454,10 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
#### geoip
|
||||
|
||||
!!! question "自 sing-box 1.9.0 起"
|
||||
!!! failure "已在 sing-box 1.12.0 中被移除"
|
||||
|
||||
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
|
||||
|
||||
|
||||
与查询响应匹配 GeoIP。
|
||||
|
||||
@@ -467,6 +473,12 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
与查询响应匹配非公开 IP。
|
||||
|
||||
#### ip_accept_any
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
匹配任意 IP。
|
||||
|
||||
#### rule_set_ip_cidr_accept_empty
|
||||
|
||||
!!! question "自 sing-box 1.10.0 起"
|
||||
|
||||
@@ -4,7 +4,8 @@ icon: material/new-box
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [strategy](#strategy)
|
||||
:material-plus: [strategy](#strategy)
|
||||
:material-plus: [predefined](#predefined)
|
||||
|
||||
!!! question "Since sing-box 1.11.0"
|
||||
|
||||
@@ -16,7 +17,7 @@ icon: material/new-box
|
||||
"server": "",
|
||||
"strategy": "",
|
||||
"disable_cache": false,
|
||||
"rewrite_ttl": 0,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
```
|
||||
@@ -31,6 +32,8 @@ Tag of target server.
|
||||
|
||||
#### strategy
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
Set domain strategy for this query.
|
||||
|
||||
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
||||
@@ -49,7 +52,7 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
|
||||
|
||||
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||
|
||||
Will overrides `dns.client_subnet` and `servers.[].client_subnet`.
|
||||
Will overrides `dns.client_subnet`.
|
||||
|
||||
### route-options
|
||||
|
||||
@@ -69,7 +72,7 @@ Will overrides `dns.client_subnet` and `servers.[].client_subnet`.
|
||||
```json
|
||||
{
|
||||
"action": "reject",
|
||||
"method": "default", // default
|
||||
"method": "",
|
||||
"no_drop": false
|
||||
}
|
||||
```
|
||||
@@ -81,8 +84,61 @@ Will overrides `dns.client_subnet` and `servers.[].client_subnet`.
|
||||
- `default`: Reply with NXDOMAIN.
|
||||
- `drop`: Drop the request.
|
||||
|
||||
`default` will be used by default.
|
||||
|
||||
#### no_drop
|
||||
|
||||
If not enabled, `method` will be temporarily overwritten to `drop` after 50 triggers in 30s.
|
||||
|
||||
Not available when `method` is set to drop.
|
||||
|
||||
### predefined
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "predefined",
|
||||
"rcode": "",
|
||||
"answer": [],
|
||||
"ns": [],
|
||||
"extra": []
|
||||
}
|
||||
```
|
||||
|
||||
`predefined` responds with predefined DNS records.
|
||||
|
||||
#### rcode
|
||||
|
||||
The response code.
|
||||
|
||||
| Value | Value in the legacy rcode server | Description |
|
||||
|------------|----------------------------------|-----------------|
|
||||
| `NOERROR` | `success` | Ok |
|
||||
| `FORMERR` | `format_error` | Bad request |
|
||||
| `SERVFAIL` | `server_failure` | Server failure |
|
||||
| `NXDOMAIN` | `name_error` | Not found |
|
||||
| `NOTIMP` | `not_implemented` | Not implemented |
|
||||
| `REFUSED` | `refused` | Refused |
|
||||
|
||||
`NOERROR` will be used by default.
|
||||
|
||||
#### answer
|
||||
|
||||
List of text DNS record to respond as answers.
|
||||
|
||||
Examples:
|
||||
|
||||
| Record Type | Example |
|
||||
|-------------|-------------------------------|
|
||||
| `A` | `localhost. IN A 127.0.0.1` |
|
||||
| `AAAA` | `localhost. IN AAAA ::1` |
|
||||
| `TXT` | `localhost. IN TXT \"Hello\"` |
|
||||
|
||||
#### ns
|
||||
|
||||
List of text DNS record to respond as name servers.
|
||||
|
||||
#### extra
|
||||
|
||||
List of text DNS record to respond as extra records.
|
||||
|
||||
@@ -4,7 +4,8 @@ icon: material/new-box
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [strategy](#strategy)
|
||||
:material-plus: [strategy](#strategy)
|
||||
:material-plus: [predefined](#predefined)
|
||||
|
||||
!!! question "自 sing-box 1.11.0 起"
|
||||
|
||||
@@ -12,12 +13,11 @@ icon: material/new-box
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "route", // 默认
|
||||
"action": "route", // 默认
|
||||
"server": "",
|
||||
|
||||
"strategy": "",
|
||||
"disable_cache": false,
|
||||
"rewrite_ttl": 0,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
```
|
||||
@@ -32,6 +32,8 @@ icon: material/new-box
|
||||
|
||||
#### strategy
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
为此查询设置域名策略。
|
||||
|
||||
可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。
|
||||
@@ -50,7 +52,7 @@ icon: material/new-box
|
||||
|
||||
如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。
|
||||
|
||||
将覆盖 `dns.client_subnet` 与 `servers.[].client_subnet`。
|
||||
将覆盖 `dns.client_subnet`.
|
||||
|
||||
### route-options
|
||||
|
||||
@@ -70,7 +72,7 @@ icon: material/new-box
|
||||
```json
|
||||
{
|
||||
"action": "reject",
|
||||
"method": "default", // default
|
||||
"method": "",
|
||||
"no_drop": false
|
||||
}
|
||||
```
|
||||
@@ -82,8 +84,61 @@ icon: material/new-box
|
||||
- `default`: 返回 NXDOMAIN。
|
||||
- `drop`: 丢弃请求。
|
||||
|
||||
默认使用 `defualt`。
|
||||
|
||||
#### no_drop
|
||||
|
||||
如果未启用,则 30 秒内触发 50 次后,`method` 将被暂时覆盖为 `drop`。
|
||||
|
||||
当 `method` 设为 `drop` 时不可用。
|
||||
|
||||
### predefined
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "predefined",
|
||||
"rcode": "",
|
||||
"answer": [],
|
||||
"ns": [],
|
||||
"extra": []
|
||||
}
|
||||
```
|
||||
|
||||
`predefined` 以预定义的 DNS 记录响应。
|
||||
|
||||
#### rcode
|
||||
|
||||
响应码。
|
||||
|
||||
| 值 | 旧 rcode DNS 服务器中的值 | 描述 |
|
||||
|------------|--------------------|-----------------|
|
||||
| `NOERROR` | `success` | Ok |
|
||||
| `FORMERR` | `format_error` | Bad request |
|
||||
| `SERVFAIL` | `server_failure` | Server failure |
|
||||
| `NXDOMAIN` | `name_error` | Not found |
|
||||
| `NOTIMP` | `not_implemented` | Not implemented |
|
||||
| `REFUSED` | `refused` | Refused |
|
||||
|
||||
默认使用 `NOERROR`。
|
||||
|
||||
#### answer
|
||||
|
||||
用于作为回答响应的文本 DNS 记录列表。
|
||||
|
||||
例子:
|
||||
|
||||
| 记录类型 | 例子 |
|
||||
|--------|-------------------------------|
|
||||
| `A` | `localhost. IN A 127.0.0.1` |
|
||||
| `AAAA` | `localhost. IN AAAA ::1` |
|
||||
| `TXT` | `localhost. IN TXT \"Hello\"` |
|
||||
|
||||
#### ns
|
||||
|
||||
用于作为名称服务器响应的文本 DNS 记录列表。
|
||||
|
||||
#### extra
|
||||
|
||||
用于作为额外记录响应的文本 DNS 记录列表。
|
||||
|
||||
96
docs/configuration/dns/server/hosts.md
Normal file
96
docs/configuration/dns/server/hosts.md
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
# Hosts
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "hosts",
|
||||
"tag": "",
|
||||
|
||||
"path": [],
|
||||
"predefined": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! note ""
|
||||
|
||||
You can ignore the JSON Array [] tag when the content is only one item
|
||||
|
||||
### Fields
|
||||
|
||||
#### path
|
||||
|
||||
List of paths to hosts files.
|
||||
|
||||
`/etc/hosts` is used by default.
|
||||
|
||||
`C:\Windows\System32\Drivers\etc\hosts` is used by default on Windows.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
// "path": "/etc/hosts"
|
||||
|
||||
"path": [
|
||||
"/etc/hosts",
|
||||
"$HOME/.hosts"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### predefined
|
||||
|
||||
Predefined hosts.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"predefined": {
|
||||
"www.google.com": "127.0.0.1",
|
||||
"localhost": [
|
||||
"127.0.0.1",
|
||||
"::1"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
=== "Use hosts if available"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
...
|
||||
},
|
||||
{
|
||||
"type": "hosts",
|
||||
"tag": "hosts"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "hosts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -50,7 +50,7 @@ If domain name is used, `domain_resolver` must also be set to resolve IP address
|
||||
|
||||
The port of the DNS server.
|
||||
|
||||
`853` will be used by default.
|
||||
`443` will be used by default.
|
||||
|
||||
#### path
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ If domain name is used, `domain_resolver` must also be set to resolve IP address
|
||||
|
||||
The port of the DNS server.
|
||||
|
||||
`853` will be used by default.
|
||||
`443` will be used by default.
|
||||
|
||||
#### path
|
||||
|
||||
|
||||
@@ -27,19 +27,19 @@ icon: material/alert-decagram
|
||||
|
||||
The type of the DNS server.
|
||||
|
||||
| Type | Format |
|
||||
|-----------------|-----------------------------------------------------|
|
||||
| empty (default) | [Legacy](/configuration/dns/server/legacy/) |
|
||||
| `tcp` | [TCP](/configuration/dns/server/tcp/) |
|
||||
| `udp` | [UDP](/configuration/dns/server/udp/) |
|
||||
| `tls` | [TLS](/configuration/dns/server/tls/) |
|
||||
| `https` | [HTTPS](/configuration/dns/server/https/) |
|
||||
| `quic` | [QUIC](/configuration/dns/server/quic/) |
|
||||
| `h3` | [HTTP/3](/configuration/dns/server/http3/) |
|
||||
| `predefined` | [Predefined](/configuration/dns/server/predefined/) |
|
||||
| `dhcp` | [DHCP](/configuration/dns/server/dhcp/) |
|
||||
| `fakeip` | [Fake IP](/configuration/dns/server/fakeip/) |
|
||||
|
||||
| Type | Format |
|
||||
|-----------------|-----------------------------|
|
||||
| empty (default) | [Legacy](./legacy/) |
|
||||
| `tcp` | [TCP](./tcp/) |
|
||||
| `udp` | [UDP](./udp/) |
|
||||
| `tls` | [TLS](./tls/) |
|
||||
| `https` | [HTTPS](./https/) |
|
||||
| `quic` | [QUIC](./quic/) |
|
||||
| `h3` | [HTTP/3](./http3/) |
|
||||
| `predefined` | [Predefined](./predefined/) |
|
||||
| `dhcp` | [DHCP](./dhcp/) |
|
||||
| `fakeip` | [Fake IP](./fakeip/) |
|
||||
| `tailscale` | [Tailscale](./tailscale/) |
|
||||
|
||||
#### tag
|
||||
|
||||
|
||||
46
docs/configuration/dns/server/index.zh.md
Normal file
46
docs/configuration/dns/server/index.zh.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [type](#type)
|
||||
|
||||
# DNS Server
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "",
|
||||
"tag": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### type
|
||||
|
||||
DNS 服务器的类型。
|
||||
|
||||
| 类型 | 格式 |
|
||||
|-----------------|-----------------------------|
|
||||
| empty (default) | [Legacy](./legacy/) |
|
||||
| `tcp` | [TCP](./tcp/) |
|
||||
| `udp` | [UDP](./udp/) |
|
||||
| `tls` | [TLS](./tls/) |
|
||||
| `https` | [HTTPS](./https/) |
|
||||
| `quic` | [QUIC](./quic/) |
|
||||
| `h3` | [HTTP/3](./http3/) |
|
||||
| `predefined` | [Predefined](./predefined/) |
|
||||
| `dhcp` | [DHCP](./dhcp/) |
|
||||
| `fakeip` | [Fake IP](./fakeip/) |
|
||||
| `tailscale` | [Tailscale](./tailscale/) |
|
||||
|
||||
#### tag
|
||||
|
||||
DNS 服务器的标签。
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
# Predefined
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "predefined",
|
||||
"tag": "",
|
||||
"responses": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### responses
|
||||
|
||||
==Required==
|
||||
|
||||
List of [Response](#response-structure).
|
||||
|
||||
### Response Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"query": [],
|
||||
"query_type": [],
|
||||
"rcode": "",
|
||||
"answer": [],
|
||||
"ns": [],
|
||||
"extra": []
|
||||
}
|
||||
```
|
||||
|
||||
!!! note ""
|
||||
|
||||
You can ignore the JSON Array [] tag when the content is only one item
|
||||
|
||||
### Response Fields
|
||||
|
||||
#### query
|
||||
|
||||
List of domain name to match.
|
||||
|
||||
#### query_type
|
||||
|
||||
List of query type to match.
|
||||
|
||||
#### rcode
|
||||
|
||||
The response code.
|
||||
|
||||
| Value | Value in the legacy rcode server | Description |
|
||||
|------------|----------------------------------|-----------------|
|
||||
| `NOERROR` | `success` | Ok |
|
||||
| `FORMERR` | `format_error` | Bad request |
|
||||
| `SERVFAIL` | `server_failure` | Server failure |
|
||||
| `NXDOMAIN` | `name_error` | Not found |
|
||||
| `NOTIMP` | `not_implemented` | Not implemented |
|
||||
| `REFUSED` | `refused` | Refused |
|
||||
|
||||
`NOERROR` will be used by default.
|
||||
|
||||
#### answer
|
||||
|
||||
List of text DNS record to respond as answers.
|
||||
|
||||
Examples:
|
||||
|
||||
| Record Type | Example |
|
||||
|-------------|-------------------------------|
|
||||
| `A` | `localhost. IN A 127.0.0.1` |
|
||||
| `AAAA` | `localhost. IN AAAA ::1` |
|
||||
| `TXT` | `localhost. IN TXT \"Hello\"` |
|
||||
|
||||
#### ns
|
||||
|
||||
List of text DNS record to respond as name servers.
|
||||
|
||||
#### extra
|
||||
|
||||
List of text DNS record to respond as extra records.
|
||||
@@ -4,7 +4,7 @@ icon: material/new-box
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
# TCP
|
||||
# UDP
|
||||
|
||||
### Structure
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ Endpoint is protocols that has both inbound and outbound behavior.
|
||||
| Type | Format |
|
||||
|-------------|---------------------------|
|
||||
| `wireguard` | [WireGuard](./wireguard/) |
|
||||
| `tailscale` | [Tailscale](./tailscale/) |
|
||||
|
||||
#### tag
|
||||
|
||||
|
||||
@@ -23,9 +23,10 @@ icon: material/new-box
|
||||
|
||||
### 字段
|
||||
|
||||
| 类型 | 格式 |
|
||||
| 类型 | 格式 |
|
||||
|-------------|---------------------------|
|
||||
| `wireguard` | [WireGuard](./wiregaurd/) |
|
||||
| `wireguard` | [WireGuard](./wiregaurd/) |
|
||||
| `tailscale` | [Tailscale](./tailscale/) |
|
||||
|
||||
#### tag
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ icon: material/new-box
|
||||
|
||||
### 字段
|
||||
|
||||
#### system_interface
|
||||
#### system
|
||||
|
||||
使用系统设备。
|
||||
|
||||
|
||||
59
docs/configuration/inbound/anytls.md
Normal file
59
docs/configuration/inbound/anytls.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "anytls",
|
||||
"tag": "anytls-in",
|
||||
|
||||
... // Listen Fields
|
||||
|
||||
"users": [
|
||||
{
|
||||
"name": "sekai",
|
||||
"password": "8JCsPssfgS8tiRwiMlhARg=="
|
||||
}
|
||||
],
|
||||
"padding_scheme": [],
|
||||
"tls": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Listen Fields
|
||||
|
||||
See [Listen Fields](/configuration/shared/listen/) for details.
|
||||
|
||||
### Fields
|
||||
|
||||
#### users
|
||||
|
||||
==Required==
|
||||
|
||||
AnyTLS users.
|
||||
|
||||
#### padding_scheme
|
||||
|
||||
AnyTLS padding scheme line array.
|
||||
|
||||
Default padding scheme:
|
||||
|
||||
```
|
||||
stop=8
|
||||
0=34-120
|
||||
1=100-400
|
||||
2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500
|
||||
3=500-1000
|
||||
4=500-1000
|
||||
5=500-1000
|
||||
6=500-1000
|
||||
7=500-1000
|
||||
```
|
||||
|
||||
#### tls
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
59
docs/configuration/inbound/anytls.zh.md
Normal file
59
docs/configuration/inbound/anytls.zh.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "anytls",
|
||||
"tag": "anytls-in",
|
||||
|
||||
... // 监听字段
|
||||
|
||||
"users": [
|
||||
{
|
||||
"name": "sekai",
|
||||
"password": "8JCsPssfgS8tiRwiMlhARg=="
|
||||
}
|
||||
],
|
||||
"padding_scheme": [],
|
||||
"tls": {}
|
||||
}
|
||||
```
|
||||
|
||||
### 监听字段
|
||||
|
||||
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
||||
|
||||
### 字段
|
||||
|
||||
#### users
|
||||
|
||||
==必填==
|
||||
|
||||
AnyTLS 用户。
|
||||
|
||||
#### padding_scheme
|
||||
|
||||
AnyTLS 填充方案行数组。
|
||||
|
||||
默认填充方案:
|
||||
|
||||
```
|
||||
stop=8
|
||||
0=34-120
|
||||
1=100-400
|
||||
2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500
|
||||
3=500-1000
|
||||
4=500-1000
|
||||
5=500-1000
|
||||
6=500-1000
|
||||
7=500-1000
|
||||
```
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
@@ -30,6 +30,7 @@
|
||||
| `tuic` | [TUIC](./tuic/) | :material-close: |
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
|
||||
| `vless` | [VLESS](./vless/) | TCP |
|
||||
| `anytls` | [AnyTLS](./anytls/) | TCP |
|
||||
| `tun` | [Tun](./tun/) | :material-close: |
|
||||
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
||||
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
| `tuic` | [TUIC](./tuic/) | :material-close: |
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
|
||||
| `vless` | [VLESS](./vless/) | TCP |
|
||||
| `anytls` | [AnyTLS](./anytls/) | TCP |
|
||||
| `tun` | [Tun](./tun/) | :material-close: |
|
||||
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
||||
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [wildcard_sni](#wildcard_sni)
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
@@ -29,7 +37,8 @@
|
||||
... // Dial Fields
|
||||
}
|
||||
},
|
||||
"strict_mode": false
|
||||
"strict_mode": false,
|
||||
"wildcard_sni": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -55,7 +64,6 @@ ShadowTLS password.
|
||||
|
||||
Only available in the ShadowTLS protocol 2.
|
||||
|
||||
|
||||
#### users
|
||||
|
||||
ShadowTLS users.
|
||||
@@ -66,6 +74,8 @@ Only available in the ShadowTLS protocol 3.
|
||||
|
||||
==Required==
|
||||
|
||||
When `wildcard_sni` is configured to `all`, the server address is optional.
|
||||
|
||||
Handshake server address and [Dial Fields](/configuration/shared/dial/).
|
||||
|
||||
#### handshake_for_server_name
|
||||
@@ -79,3 +89,19 @@ Only available in the ShadowTLS protocol 2/3.
|
||||
ShadowTLS strict mode.
|
||||
|
||||
Only available in the ShadowTLS protocol 3.
|
||||
|
||||
#### wildcard_sni
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
ShadowTLS wildcard SNI mode.
|
||||
|
||||
Available values are:
|
||||
|
||||
* `off`: (default) Disabled.
|
||||
* `authed`: Authenticated connections will have their destination overwritten to `(servername):443`
|
||||
* `all`: All connections will have their destination overwritten to `(servername):443`
|
||||
|
||||
Additionally, connections matching `handshake_for_server_name` are not affected.
|
||||
|
||||
Only available in the ShadowTLS protocol 3.
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [wildcard_sni](#wildcard_sni)
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
@@ -29,7 +37,8 @@
|
||||
... // 拨号字段
|
||||
}
|
||||
},
|
||||
"strict_mode": false
|
||||
"strict_mode": false,
|
||||
"wildcard_sni": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -80,3 +89,19 @@ ShadowTLS 用户。
|
||||
ShadowTLS 严格模式。
|
||||
|
||||
仅在 ShadowTLS 协议版本 3 中可用。
|
||||
|
||||
#### wildcard_sni
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
ShadowTLS 通配符 SNI 模式。
|
||||
|
||||
可用值:
|
||||
|
||||
* `off`:(默认)禁用。
|
||||
* `authed`:已认证的连接的目标将被重写为 `(servername):443`。
|
||||
* `all`:所有连接的目标将被重写为 `(servername):443`。
|
||||
|
||||
此外,匹配 `handshake_for_server_name` 的连接不受影响。
|
||||
|
||||
仅在 ShadowTLS 协议 3 中可用。
|
||||
|
||||
@@ -9,6 +9,7 @@ sing-box uses JSON for configuration files.
|
||||
"log": {},
|
||||
"dns": {},
|
||||
"ntp": {},
|
||||
"certificate": {},
|
||||
"endpoints": [],
|
||||
"inbounds": [],
|
||||
"outbounds": [],
|
||||
@@ -24,6 +25,7 @@ sing-box uses JSON for configuration files.
|
||||
| `log` | [Log](./log/) |
|
||||
| `dns` | [DNS](./dns/) |
|
||||
| `ntp` | [NTP](./ntp/) |
|
||||
| `certificate` | [Certificate](./certificate/) |
|
||||
| `endpoints` | [Endpoint](./endpoint/) |
|
||||
| `inbounds` | [Inbound](./inbound/) |
|
||||
| `outbounds` | [Outbound](./outbound/) |
|
||||
|
||||
@@ -9,6 +9,7 @@ sing-box 使用 JSON 作为配置文件格式。
|
||||
"log": {},
|
||||
"dns": {},
|
||||
"ntp": {},
|
||||
"certificate": {},
|
||||
"endpoints": [],
|
||||
"inbounds": [],
|
||||
"outbounds": [],
|
||||
@@ -24,6 +25,7 @@ sing-box 使用 JSON 作为配置文件格式。
|
||||
| `log` | [日志](./log/) |
|
||||
| `dns` | [DNS](./dns/) |
|
||||
| `ntp` | [NTP](./ntp/) |
|
||||
| `certificate` | [证书](./certificate/) |
|
||||
| `endpoints` | [端点](./endpoint/) |
|
||||
| `inbounds` | [入站](./inbound/) |
|
||||
| `outbounds` | [出站](./outbound/) |
|
||||
|
||||
66
docs/configuration/outbound/anytls.md
Normal file
66
docs/configuration/outbound/anytls.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "anytls",
|
||||
"tag": "anytls-out",
|
||||
|
||||
"server": "127.0.0.1",
|
||||
"server_port": 1080,
|
||||
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
||||
"idle_session_check_interval": "30s",
|
||||
"idle_session_timeout": "30s",
|
||||
"min_idle_session": 5,
|
||||
"tls": {},
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### server
|
||||
|
||||
==Required==
|
||||
|
||||
The server address.
|
||||
|
||||
#### server_port
|
||||
|
||||
==Required==
|
||||
|
||||
The server port.
|
||||
|
||||
#### password
|
||||
|
||||
==Required==
|
||||
|
||||
The AnyTLS password.
|
||||
|
||||
#### idle_session_check_interval
|
||||
|
||||
Interval checking for idle sessions. Default: 30s.
|
||||
|
||||
#### idle_session_timeout
|
||||
|
||||
In the check, close sessions that have been idle for longer than this. Default: 30s.
|
||||
|
||||
#### min_idle_session
|
||||
|
||||
In the check, at least the first `n` idle sessions are kept open. Default value: `n`=0
|
||||
|
||||
#### tls
|
||||
|
||||
==Required==
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
66
docs/configuration/outbound/anytls.zh.md
Normal file
66
docs/configuration/outbound/anytls.zh.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "anytls",
|
||||
"tag": "anytls-out",
|
||||
|
||||
"server": "127.0.0.1",
|
||||
"server_port": 1080,
|
||||
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
||||
"idle_session_check_interval": "30s",
|
||||
"idle_session_timeout": "30s",
|
||||
"min_idle_session": 5,
|
||||
"tls": {},
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### server
|
||||
|
||||
==必填==
|
||||
|
||||
服务器地址。
|
||||
|
||||
#### server_port
|
||||
|
||||
==必填==
|
||||
|
||||
服务器端口。
|
||||
|
||||
#### password
|
||||
|
||||
==必填==
|
||||
|
||||
AnyTLS 密码。
|
||||
|
||||
#### idle_session_check_interval
|
||||
|
||||
检查空闲会话的时间间隔。默认值:30秒。
|
||||
|
||||
#### idle_session_timeout
|
||||
|
||||
在检查中,关闭闲置时间超过此值的会话。默认值:30秒。
|
||||
|
||||
#### min_idle_session
|
||||
|
||||
在检查中,至少前 `n` 个空闲会话保持打开状态。默认值:`n`=0
|
||||
|
||||
#### tls
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||
@@ -30,6 +30,7 @@
|
||||
| `shadowtls` | [ShadowTLS](./shadowtls/) |
|
||||
| `tuic` | [TUIC](./tuic/) |
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||
| `anytls` | [AnyTLS](./anytls/) |
|
||||
| `tor` | [Tor](./tor/) |
|
||||
| `ssh` | [SSH](./ssh/) |
|
||||
| `dns` | [DNS](./dns/) |
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
| `shadowtls` | [ShadowTLS](./shadowtls/) |
|
||||
| `tuic` | [TUIC](./tuic/) |
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||
| `anytls` | [AnyTLS](./anytls/) |
|
||||
| `tor` | [Tor](./tor/) |
|
||||
| `ssh` | [SSH](./ssh/) |
|
||||
| `dns` | [DNS](./dns/) |
|
||||
|
||||
@@ -5,7 +5,10 @@ icon: material/new-box
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [tls_fragment](#tls_fragment)
|
||||
:material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay)
|
||||
:material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay)
|
||||
:material-plus: [resolve.disable_cache](#disable_cache)
|
||||
:material-plus: [resolve.rewrite_ttl](#rewrite_ttl)
|
||||
:material-plus: [resolve.client_subnet](#client_subnet)
|
||||
|
||||
## Final actions
|
||||
|
||||
@@ -210,19 +213,44 @@ Timeout for sniffing.
|
||||
```json
|
||||
{
|
||||
"action": "resolve",
|
||||
"server": "",
|
||||
"strategy": "",
|
||||
"server": ""
|
||||
"disable_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
```
|
||||
|
||||
`resolve` resolve request destination from domain to IP addresses.
|
||||
|
||||
#### server
|
||||
|
||||
Specifies DNS server tag to use instead of selecting through DNS routing.
|
||||
|
||||
#### strategy
|
||||
|
||||
DNS resolution strategy, available values are: `prefer_ipv4`, `prefer_ipv6`, `ipv4_only`, `ipv6_only`.
|
||||
|
||||
`dns.strategy` will be used by default.
|
||||
|
||||
#### server
|
||||
#### disable_cache
|
||||
|
||||
Specifies DNS server tag to use instead of selecting through DNS routing.
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
Disable cache and save cache in this query.
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
Rewrite TTL in DNS responses.
|
||||
|
||||
#### client_subnet
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default.
|
||||
|
||||
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||
|
||||
Will overrides `dns.client_subnet`.
|
||||
|
||||
@@ -206,19 +206,44 @@ UDP 连接超时时间。
|
||||
```json
|
||||
{
|
||||
"action": "resolve",
|
||||
"server": "",
|
||||
"strategy": "",
|
||||
"server": ""
|
||||
"disable_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
```
|
||||
|
||||
`resolve` 将请求的目标从域名解析为 IP 地址。
|
||||
|
||||
#### server
|
||||
|
||||
指定要使用的 DNS 服务器的标签,而不是通过 DNS 路由进行选择。
|
||||
|
||||
#### strategy
|
||||
|
||||
DNS 解析策略,可用值有:`prefer_ipv4`、`prefer_ipv6`、`ipv4_only`、`ipv6_only`。
|
||||
|
||||
默认使用 `dns.strategy`。
|
||||
|
||||
#### server
|
||||
#### disable_cache
|
||||
|
||||
指定要使用的 DNS 服务器的标签,而不是通过 DNS 路由进行选择。
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
在此查询中禁用缓存。
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
重写 DNS 回应中的 TTL。
|
||||
|
||||
#### client_subnet
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
|
||||
|
||||
如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。
|
||||
|
||||
将覆盖 `dns.client_subnet`.
|
||||
|
||||
@@ -22,6 +22,7 @@ If enabled in the inbound, the protocol and domain name (if present) of by the c
|
||||
| UDP | `dtls` | / | / |
|
||||
| TCP | `ssh` | / | SSH Client Name |
|
||||
| TCP | `rdp` | / | / |
|
||||
| UDP | `ntp` | / | / |
|
||||
|
||||
| QUIC Client | Type |
|
||||
|:------------------------:|:----------:|
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
| UDP | `dtls` | / | / |
|
||||
| TCP | `ssh` | / | SSH 客户端名称 |
|
||||
| TCP | `rdp` | / | / |
|
||||
| UDP | `ntp` | / | / |
|
||||
|
||||
| QUIC 客户端 | 类型 |
|
||||
|:------------------------:|:----------:|
|
||||
|
||||
@@ -106,6 +106,10 @@ Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||
|
||||
`outbound` DNS rule items are deprecated and will be removed in sing-box 1.14.0, so this item will be required for outbound/endpoints using domain name in server address since sing-box 1.14.0.
|
||||
|
||||
!!! info ""
|
||||
|
||||
`domain_resolver` or `route.default_domain_resolver` is optional when only one DNS server is configured.
|
||||
|
||||
Set domain resolver to use for resolving domain names.
|
||||
|
||||
This option uses the same format as the [route DNS rule action](/configuration/dns/rule_action/#route) without the `action` field.
|
||||
|
||||
@@ -105,6 +105,10 @@ icon: material/new-box
|
||||
|
||||
`outbound` DNS 规则项已弃用,且将在 sing-box 1.14.0 中被移除。因此,从 sing-box 1.14.0 版本开始,所有在服务器地址中使用域名的出站/端点均需配置此项。
|
||||
|
||||
!!! info ""
|
||||
|
||||
当只有一个 DNS 服务器已配置时,`domain_resolver` 或 `route.default_domain_resolver` 是可选的。
|
||||
|
||||
用于设置解析域名的域名解析器。
|
||||
|
||||
此选项的格式与 [路由 DNS 规则动作](/configuration/dns/rule_action/#route) 相同,但不包含 `action` 字段。
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled)
|
||||
:material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled)
|
||||
|
||||
!!! quote "Changes in sing-box 1.10.0"
|
||||
|
||||
:material-alert-decagram: [utls](#utls)
|
||||
@@ -34,10 +43,13 @@
|
||||
},
|
||||
"ech": {
|
||||
"enabled": false,
|
||||
"pq_signature_schemes_enabled": false,
|
||||
"dynamic_record_sizing_disabled": false,
|
||||
"key": [],
|
||||
"key_path": ""
|
||||
"key_path": "",
|
||||
|
||||
// Deprecated
|
||||
|
||||
"pq_signature_schemes_enabled": false,
|
||||
"dynamic_record_sizing_disabled": false
|
||||
},
|
||||
"reality": {
|
||||
"enabled": false,
|
||||
@@ -72,10 +84,12 @@
|
||||
"certificate_path": "",
|
||||
"ech": {
|
||||
"enabled": false,
|
||||
"pq_signature_schemes_enabled": false,
|
||||
"dynamic_record_sizing_disabled": false,
|
||||
"config": [],
|
||||
"config_path": ""
|
||||
"config_path": "",
|
||||
|
||||
// Deprecated
|
||||
"pq_signature_schemes_enabled": false,
|
||||
"dynamic_record_sizing_disabled": false
|
||||
},
|
||||
"utls": {
|
||||
"enabled": false,
|
||||
@@ -246,16 +260,22 @@ Chrome fingerprint will be used if empty.
|
||||
ECH (Encrypted Client Hello) is a TLS extension that allows a client to encrypt the first part of its ClientHello
|
||||
message.
|
||||
|
||||
The ECH key and configuration can be generated by `sing-box generate ech-keypair [--pq-signature-schemes-enabled]`.
|
||||
The ECH key and configuration can be generated by `sing-box generate ech-keypair`.
|
||||
|
||||
#### pq_signature_schemes_enabled
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.12.0"
|
||||
|
||||
ECH support has been migrated to use stdlib in sing-box 1.12.0, which does not come with support for PQ signature schemes, so `pq_signature_schemes_enabled` has been deprecated and no longer works.
|
||||
|
||||
Enable support for post-quantum peer certificate signature schemes.
|
||||
|
||||
It is recommended to match the parameters of `sing-box generate ech-keypair`.
|
||||
|
||||
#### dynamic_record_sizing_disabled
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.12.0"
|
||||
|
||||
`dynamic_record_sizing_disabled` has nothing to do with ECH, was added by mistake, has been deprecated and no longer works.
|
||||
|
||||
Disables adaptive sizing of TLS records.
|
||||
|
||||
When true, the largest possible TLS record size is always used.
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled)
|
||||
:material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled)
|
||||
|
||||
!!! quote "sing-box 1.10.0 中的更改"
|
||||
|
||||
:material-alert-decagram: [utls](#utls)
|
||||
@@ -34,18 +43,21 @@
|
||||
},
|
||||
"ech": {
|
||||
"enabled": false,
|
||||
"pq_signature_schemes_enabled": false,
|
||||
"dynamic_record_sizing_disabled": false,
|
||||
"key": [],
|
||||
"key_path": ""
|
||||
"key_path": "",
|
||||
|
||||
// 废弃的
|
||||
|
||||
"pq_signature_schemes_enabled": false,
|
||||
"dynamic_record_sizing_disabled": false
|
||||
},
|
||||
"reality": {
|
||||
"enabled": false,
|
||||
"handshake": {
|
||||
"server": "google.com",
|
||||
"server_port": 443,
|
||||
...
|
||||
// 拨号字段
|
||||
|
||||
... // 拨号字段
|
||||
},
|
||||
"private_key": "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc",
|
||||
"short_id": [
|
||||
@@ -240,19 +252,6 @@ ECH (Encrypted Client Hello) 是一个 TLS 扩展,它允许客户端加密其
|
||||
|
||||
ECH 配置和密钥可以通过 `sing-box generate ech-keypair [--pq-signature-schemes-enabled]` 生成。
|
||||
|
||||
#### pq_signature_schemes_enabled
|
||||
|
||||
启用对后量子对等证书签名方案的支持。
|
||||
|
||||
建议匹配 `sing-box generate ech-keypair` 的参数。
|
||||
|
||||
#### dynamic_record_sizing_disabled
|
||||
|
||||
禁用 TLS 记录的自适应大小调整。
|
||||
|
||||
如果为 true,则始终使用最大可能的 TLS 记录大小。
|
||||
如果为 false,则可能会调整 TLS 记录的大小以尝试改善延迟。
|
||||
|
||||
#### key
|
||||
|
||||
==仅服务器==
|
||||
@@ -285,6 +284,27 @@ ECH PEM 配置路径
|
||||
|
||||
如果为空,将尝试从 DNS 加载。
|
||||
|
||||
#### pq_signature_schemes_enabled
|
||||
|
||||
!!! failure "已在 sing-box 1.12.0 废弃"
|
||||
|
||||
ECH 支持已在 sing-box 1.12.0 迁移至使用标准库,但标准库不支持后量子对等证书签名方案,因此 `pq_signature_schemes_enabled` 已被弃用且不再工作。
|
||||
|
||||
启用对后量子对等证书签名方案的支持。
|
||||
|
||||
建议匹配 `sing-box generate ech-keypair` 的参数。
|
||||
|
||||
#### dynamic_record_sizing_disabled
|
||||
|
||||
!!! failure "已在 sing-box 1.12.0 废弃"
|
||||
|
||||
`dynamic_record_sizing_disabled` 与 ECH 无关,是错误添加的,现已弃用且不再工作。
|
||||
|
||||
禁用 TLS 记录的自适应大小调整。
|
||||
|
||||
如果为 true,则始终使用最大可能的 TLS 记录大小。
|
||||
如果为 false,则可能会调整 TLS 记录的大小以尝试改善延迟。
|
||||
|
||||
### ACME 字段
|
||||
|
||||
#### domain
|
||||
|
||||
@@ -31,12 +31,11 @@ The protocol version, `1` or `2`.
|
||||
|
||||
### Application support
|
||||
|
||||
| Project | UoT v1 | UoT v2 |
|
||||
|--------------|----------------------|-------------------------------------------------------------------------------------------------------------------|
|
||||
| sing-box | v0 (2022/08/11) | v1.2-beta9 |
|
||||
| Xray-core | v1.5.7 (2022/06/05) | [f57ec13](https://github.com/XTLS/Xray-core/commit/f57ec1388084df041a2289bacab14e446bf1b357) (Not released) |
|
||||
| Clash.Meta | v1.12.0 (2022/07/02) | [8cb67b6](https://github.com/MetaCubeX/Clash.Meta/commit/8cb67b6480649edfa45dcc9ac89ce0789651e8b3) (Not released) |
|
||||
| Shadowrocket | v2.2.12 (2022/08/13) | / |
|
||||
| Project | UoT v1 | UoT v2 |
|
||||
|--------------|----------------------|----------------------|
|
||||
| sing-box | v0 (2022/08/11) | v1.2-beta9 |
|
||||
| Clash.Meta | v1.12.0 (2022/07/02) | v1.14.3 (2023/03/31) |
|
||||
| Shadowrocket | v2.2.12 (2022/08/13) | / |
|
||||
|
||||
### Protocol details
|
||||
|
||||
@@ -50,7 +49,13 @@ The client requests the magic address to the upper layer proxy protocol to indic
|
||||
|------|----------|-------|--------|----------|
|
||||
| u8 | variable | u16be | u16be | variable |
|
||||
|
||||
**ATYP / address / port**: Uses the SOCKS address format.
|
||||
**ATYP / address / port**: Uses the SOCKS address format, but with different address types:
|
||||
|
||||
| ATYP | Address type |
|
||||
|--------|--------------|
|
||||
| `0x00` | IPv4 Address |
|
||||
| `0x01` | IPv6 Address |
|
||||
| `0x02` | Domain Name |
|
||||
|
||||
#### Protocol version 2
|
||||
|
||||
|
||||
@@ -19,6 +19,17 @@ Legacy `outbound` DNS rules are deprecated
|
||||
and can be replaced by dial fields,
|
||||
check [Migration](../migration/#migrate-outbound-dns-rule-items-to-domain-resolver).
|
||||
|
||||
#### Legacy ECH fields
|
||||
|
||||
ECH support has been migrated to use stdlib in sing-box 1.12.0,
|
||||
which does not come with support for PQ signature schemes,
|
||||
so `pq_signature_schemes_enabled` has been deprecated and no longer works.
|
||||
|
||||
Also, `dynamic_record_sizing_disabled` has nothing to do with ECH,
|
||||
was added by mistake, has been deprecated and no longer works.
|
||||
|
||||
These fields will be removed in sing-box 1.13.0.
|
||||
|
||||
## 1.11.0
|
||||
|
||||
#### Legacy special outbounds
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user