Compare commits

...

75 Commits

Author SHA1 Message Date
renovate[bot]
ec66a359b4
[dependencies] Update golangci/golangci-lint-action action to v8 2025-05-12 10:15:57 +00:00
世界
1c36072120
documentation: Bump version 2025-05-12 18:12:57 +08:00
世界
6e9258bede
Add missing accept_routes option for Tailscale 2025-05-12 18:12:57 +08:00
世界
b0f4008071
Add TLS record fragment support 2025-05-12 17:43:54 +08:00
世界
3f26b7b4e9
release: Update Go to 1.24.3 2025-05-12 17:43:54 +08:00
世界
dbab3a2178
Fix set edns0 client subnet 2025-05-12 17:43:53 +08:00
世界
dbf17cca71
Update minor dependencies 2025-05-12 17:43:53 +08:00
世界
4a0fe88217
Update certmagic and providers 2025-05-12 17:43:53 +08:00
世界
4a62401f9e
Update protobuf and grpc 2025-05-12 17:43:52 +08:00
世界
09edc643cb
Add control options for listeners 2025-05-12 17:43:52 +08:00
世界
4be4220f20
Update quic-go to v0.51.0 2025-05-12 17:43:52 +08:00
世界
d1b0c967aa
Update utls to v1.7.0 2025-05-12 17:43:52 +08:00
世界
7608266ea7
Handle EDNS version downgrade 2025-05-12 17:43:22 +08:00
世界
081dad8f57
documentation: Fix anytls padding scheme description 2025-05-12 17:43:21 +08:00
安容
3ff990a6a3
Report invalid DNS address early 2025-05-12 17:43:21 +08:00
世界
9ff3da2bdb
Fix wireguard listen_port 2025-05-12 17:43:20 +08:00
世界
92c20a545d
clash-api: Add more meta api 2025-05-12 17:43:20 +08:00
世界
2e75cb3c22
Fix DNS lookup 2025-05-12 17:43:20 +08:00
世界
4414dc6bee
Fix fetch ECH configs 2025-05-12 17:43:19 +08:00
reletor
7d11245a90
documentation: Minor fixes 2025-05-12 17:43:19 +08:00
caelansar
744e9b4577
Fix callback deletion in UDP transport 2025-05-12 17:43:18 +08:00
世界
153a262f35
documentation: Try to make the play review happy 2025-05-12 17:43:18 +08:00
世界
39d2fb5467
Fix missing handling of legacy domain_strategy options 2025-05-12 17:43:18 +08:00
世界
85a2a5c169
Improve local DNS server 2025-05-12 17:43:18 +08:00
anytls
575b5dfb10
Update anytls
Co-authored-by: anytls <anytls>
2025-05-12 17:43:17 +08:00
世界
f19c500b4f
Fix DNS dialer 2025-05-12 17:43:17 +08:00
世界
8e5958a7e4
release: Skip override version for iOS 2025-05-12 17:43:17 +08:00
iikira
dc5e6ed488
Fix UDP DNS server crash
Signed-off-by: iikira <i2@mail.iikira.com>
2025-05-12 17:43:16 +08:00
ReleTor
049c18377c
Fix fetch ECH configs 2025-05-12 17:43:16 +08:00
世界
870f76a91e
Allow direct outbounds without domain_resolver 2025-05-12 17:43:15 +08:00
世界
d1a945f180
Fix Tailscale dialer 2025-05-12 17:43:15 +08:00
dyhkwong
6d56bc1528
Fix DNS over QUIC stream close 2025-05-12 17:43:14 +08:00
anytls
25e2d670e7
Update anytls
Co-authored-by: anytls <anytls>
2025-05-12 17:43:14 +08:00
Rambling2076
be8419fa84
Fix missing with_tailscale in Dockerfile
Signed-off-by: Rambling2076 <Rambling2076@proton.me>
2025-05-12 17:43:14 +08:00
世界
27acc99c35
Fail when default DNS server not found 2025-05-12 17:43:14 +08:00
世界
6053a3bad1
Update gVisor to 20250319.0 2025-05-12 17:43:14 +08:00
世界
c63a58c0fb
Explicitly reject detour to empty direct outbounds 2025-05-12 17:43:13 +08:00
世界
cc4354b18a
Add netns support 2025-05-12 17:43:13 +08:00
世界
8344653443
Add wildcard name support for predefined records 2025-05-12 17:43:13 +08:00
世界
26f185aa7c
Remove map usage in options 2025-05-12 17:43:13 +08:00
世界
a69007f1a2
Fix unhandled DNS loop 2025-05-12 17:43:13 +08:00
世界
20729defd6
Add wildcard-sni support for shadow-tls inbound 2025-05-12 17:43:12 +08:00
k9982874
ff751f8504
Add ntp protocol sniffing 2025-05-12 17:43:11 +08:00
世界
689c229fe6
option: Fix marshal legacy DNS options 2025-05-12 17:43:11 +08:00
世界
c4cdb68d10
Make domain_resolver optional when only one DNS server is configured 2025-05-12 17:43:11 +08:00
世界
dd99a45df6
Fix DNS lookup context pollution 2025-05-12 17:43:11 +08:00
世界
cbe077eaa9
Fix http3 DNS server connecting to wrong address 2025-05-12 17:43:10 +08:00
Restia-Ashbell
47fd88f450
documentation: Fix typo 2025-05-12 17:43:10 +08:00
anytls
5fdfba6816
Update sing-anytls
Co-authored-by: anytls <anytls>
2025-05-12 17:43:10 +08:00
k9982874
e38de3bd8f
Fix hosts DNS server 2025-05-12 17:43:10 +08:00
世界
6f9d507ab3
Fix UDP DNS server crash 2025-05-12 17:43:09 +08:00
世界
567de6aa88
documentation: Fix missing ip_accept_any DNS rule option 2025-05-12 17:43:09 +08:00
世界
5fd7fba75f
Fix anytls dialer usage 2025-05-12 17:43:09 +08:00
世界
d80f81157c
Move predefined DNS server to rule action 2025-05-12 17:43:09 +08:00
世界
133e57e85f
Fix domain resolver on direct outbound 2025-05-12 17:43:08 +08:00
Zephyruso
f854586ac8
Fix missing AnyTLS display name 2025-05-12 17:43:08 +08:00
anytls
a3a54b6e91
Update sing-anytls
Co-authored-by: anytls <anytls>
2025-05-12 17:43:08 +08:00
Estel
fa5dbfcca8
documentation: Fix typo
Signed-off-by: Estel <callmebedrockdigger@gmail.com>
2025-05-12 17:43:07 +08:00
TargetLocked
35d5a1f6a6
Fix parsing legacy DNS options 2025-05-12 17:43:07 +08:00
世界
9f26fea1fc
Fix DNS fallback 2025-05-12 17:43:07 +08:00
世界
5c71baa23c
documentation: Fix missing hosts DNS server 2025-05-12 17:43:06 +08:00
anytls
a75772d01e
Add MinIdleSession option to AnyTLS outbound
Co-authored-by: anytls <anytls>
2025-05-12 17:43:06 +08:00
ReleTor
ae263e2da3
documentation: Minor fixes 2025-05-12 17:43:05 +08:00
libtry486
031b6de8b5
documentation: Fix typo
fix typo

Signed-off-by: libtry486 <89328481+libtry486@users.noreply.github.com>
2025-05-12 17:43:05 +08:00
Alireza Ahmadi
6aef4ec1c6
Fix Outbound deadlock 2025-05-12 17:43:05 +08:00
世界
61b7666fd5
documentation: Fix AnyTLS doc 2025-05-12 17:43:04 +08:00
anytls
ea5339285e
Add AnyTLS protocol 2025-05-12 17:43:04 +08:00
世界
d38d58ff6e
Migrate to stdlib ECH support 2025-05-12 17:43:03 +08:00
世界
60570b966b
Add fallback local DNS server for iOS 2025-05-12 17:43:03 +08:00
世界
195e859bf7
Get darwin local DNS server from libresolv 2025-05-12 17:43:02 +08:00
世界
0ce1f21794
Improve resolve action 2025-05-12 17:43:02 +08:00
世界
ec5eba1d25
Fix toolchain version 2025-05-12 17:43:02 +08:00
世界
682a95c55e
Add back port hopping to hysteria 1 2025-05-12 17:43:01 +08:00
xchacha20-poly1305
34283f914a
Remove single quotes of raw Moziila certs 2025-05-12 17:43:01 +08:00
世界
fcce280a50
Add Tailscale endpoint 2025-05-12 17:42:41 +08:00
186 changed files with 6341 additions and 2795 deletions

View File

@ -46,7 +46,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.24
go-version: ^1.24.3
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@ -109,7 +109,7 @@ jobs:
if: ${{ ! matrix.legacy_go }}
uses: actions/setup-go@v5
with:
go-version: ^1.24
go-version: ^1.24.3
- name: Cache Legacy Go
if: matrix.require_legacy_go
id: cache-legacy-go
@ -140,7 +140,7 @@ jobs:
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api,with_tailscale'
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build
if: matrix.os != 'android'
@ -294,7 +294,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.24
go-version: ^1.24.3
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@ -374,7 +374,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.24
go-version: ^1.24.3
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@ -472,7 +472,7 @@ jobs:
if: matrix.if
uses: actions/setup-go@v5
with:
go-version: ^1.24
go-version: ^1.24.3
- name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next'
run: |-
@ -549,10 +549,13 @@ jobs:
MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version)
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION"
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV"
- name: Update version
if: matrix.if && matrix.name != 'iOS'
run: |-
go run -v ./cmd/internal/update_apple_version --ci
- name: Build
if: matrix.if
run: |-
go run -v ./cmd/internal/update_apple_version --ci
cd clients/apple
xcodebuild archive \
-scheme "${{ matrix.scheme }}" \

View File

@ -28,9 +28,9 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.24
go-version: ^1.24.3
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v8
with:
version: latest
args: --timeout=30m

View File

@ -25,7 +25,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.24
go-version: ^1.24.3
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@ -66,7 +66,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.24
go-version: ^1.24.3
- name: Setup Android NDK
if: matrix.os == 'android'
uses: nttld/setup-ndk@v1
@ -80,10 +80,7 @@ jobs:
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api'
if [ ! '${{ matrix.legacy_go }}' = 'true' ]; then
TAGS="${TAGS},with_ech"
fi
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api'
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build
run: |

View File

@ -27,9 +27,7 @@ run:
- with_quic
- with_dhcp
- with_wireguard
- with_ech
- with_utls
- with_reality_server
- with_acme
- with_clash_api

View File

@ -14,11 +14,10 @@ builds:
- with_quic
- with_dhcp
- with_wireguard
- with_ech
- with_utls
- with_reality_server
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
targets:

View File

@ -16,13 +16,13 @@ builds:
- with_quic
- with_dhcp
- with_wireguard
- with_ech
- with_utls
- with_reality_server
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
- GOTOOLCHAIN=local
targets:
- linux_386
- linux_amd64_v1
@ -46,9 +46,9 @@ builds:
- with_dhcp
- with_wireguard
- with_utls
- with_reality_server
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
- GOROOT={{ .Env.GOPATH }}/go_legacy
@ -60,6 +60,7 @@ builds:
<<: *template
env:
- CGO_ENABLED=1
- GOTOOLCHAIN=local
overrides:
- goos: android
goarch: arm

View File

@ -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_acme,with_clash_api,with_tailscale" \
-o /go/bin/sing-box \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
./cmd/sing-box

View File

@ -1,9 +1,7 @@
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 ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121)
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
TAGS ?= with_gvisor,with_dhcp,with_wireguard,with_clash_api,with_quic,with_utls,with_tailscale
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_utls
GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH)
@ -17,14 +15,12 @@ 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)
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:
@ -230,8 +226,8 @@ lib:
go run ./cmd/internal/build_libbox -target ios
lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.4
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.4
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.6
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.6
docs:
venv/bin/mkdocs serve

View File

@ -45,10 +45,10 @@ type RDRCStore interface {
}
type DNSTransport interface {
Lifecycle
Type() string
Tag() string
Dependencies() []string
Reset()
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
}

View File

@ -74,6 +74,7 @@ type InboundContext struct {
UDPTimeout time.Duration
TLSFragment bool
TLSFragmentFallbackDelay time.Duration
TLSRecordFragment bool
NetworkStrategy *C.NetworkStrategy
NetworkType []C.InterfaceType

View File

@ -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)

View File

@ -45,6 +45,7 @@ var (
debugFlags []string
sharedTags []string
iosTags []string
memcTags []string
debugTags []string
)
@ -58,8 +59,9 @@ 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")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api")
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
memcTags = append(memcTags, "with_tailscale")
debugTags = append(debugTags, "debug")
}
@ -99,18 +101,19 @@ func buildAndroid() {
"-javapkg=io.nekohasekai",
"-libname=box",
}
if !debugEnabled {
args = append(args, sharedFlags...)
} else {
args = append(args, debugFlags...)
}
args = append(args, "-tags")
if !debugEnabled {
args = append(args, strings.Join(sharedTags, ","))
} else {
args = append(args, strings.Join(append(sharedTags, debugTags...), ","))
tags := append(sharedTags, memcTags...)
if debugEnabled {
tags = append(tags, debugTags...)
}
args = append(args, "-tags", strings.Join(tags, ","))
args = append(args, "./experimental/libbox")
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
@ -148,7 +151,9 @@ func buildApple() {
"-v",
"-target", bindTarget,
"-libname=box",
"-tags-macos=" + strings.Join(memcTags, ","),
}
if !debugEnabled {
args = append(args, sharedFlags...)
} else {
@ -156,12 +161,11 @@ func buildApple() {
}
tags := append(sharedTags, iosTags...)
args = append(args, "-tags")
if !debugEnabled {
args = append(args, strings.Join(tags, ","))
} else {
args = append(args, strings.Join(append(tags, debugTags...), ","))
if debugEnabled {
tags = append(tags, debugTags...)
}
args = append(args, "-tags", strings.Join(tags, ","))
args = append(args, "./experimental/libbox")
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)

View File

@ -60,7 +60,10 @@ func init() {
generated.WriteString(record[nameIndex])
generated.WriteString("\n")
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
generated.WriteString(record[certIndex])
cert := record[certIndex]
// Remove single quotes
cert = cert[1 : len(cert)-1]
generated.WriteString(cert)
generated.WriteString("`))\n")
}
generated.WriteString("}\n")

View File

@ -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
}

View File

@ -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

View File

@ -7,7 +7,8 @@ import (
_ "unsafe"
"github.com/sagernet/sing/common"
"github.com/sagernet/utls"
"github.com/metacubex/utls"
)
func init() {
@ -24,8 +25,8 @@ func init() {
})
}
//go:linkname utlsReadRecord github.com/sagernet/utls.(*Conn).readRecord
//go:linkname utlsReadRecord github.com/metacubex/utls.(*Conn).readRecord
func utlsReadRecord(c *tls.Conn) error
//go:linkname utlsHandlePostHandshakeMessage github.com/sagernet/utls.(*Conn).handlePostHandshakeMessage
//go:linkname utlsHandlePostHandshakeMessage github.com/metacubex/utls.(*Conn).handlePostHandshakeMessage
func utlsHandlePostHandshakeMessage(c *tls.Conn) error

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/conntrack"
"github.com/sagernet/sing-box/common/listener"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/option"
@ -35,6 +36,7 @@ type DefaultDialer struct {
udpListener net.ListenConfig
udpAddr4 string
udpAddr6 string
netns string
networkManager adapter.NetworkManager
networkStrategy *C.NetworkStrategy
defaultNetworkStrategy bool
@ -69,18 +71,8 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
listener.Control = control.Append(listener.Control, bindFunc)
}
if options.RoutingMark > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(uint32(options.RoutingMark)))
listener.Control = control.Append(listener.Control, control.RoutingMark(uint32(options.RoutingMark)))
}
if networkManager != nil {
autoRedirectOutputMark := networkManager.AutoRedirectOutputMark()
if autoRedirectOutputMark > 0 {
if options.RoutingMark > 0 {
return nil, E.New("`routing_mark` is conflict with `tun.auto_redirect` with `tun.route_[_exclude]_address_set")
}
dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
}
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
}
disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil
if disableDefaultBind || options.TCPFastOpen {
@ -125,8 +117,8 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
}
}
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(defaultOptions.RoutingMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(defaultOptions.RoutingMark))
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
}
}
if options.ReuseAddr {
@ -198,6 +190,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
udpListener: listener,
udpAddr4: udpAddr4,
udpAddr6: udpAddr6,
netns: options.NetNs,
networkManager: networkManager,
networkStrategy: networkStrategy,
defaultNetworkStrategy: defaultNetworkStrategy,
@ -207,24 +200,44 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
}, nil
}
func setMarkWrapper(networkManager adapter.NetworkManager, mark uint32, isDefault bool) control.Func {
if networkManager == nil {
return control.RoutingMark(mark)
}
return func(network, address string, conn syscall.RawConn) error {
if networkManager.AutoRedirectOutputMark() != 0 {
if isDefault {
return E.New("`route.default_mark` is conflict with `tun.auto_redirect`")
} else {
return E.New("`routing_mark` is conflict with `tun.auto_redirect`")
}
}
return control.RoutingMark(mark)(network, address, conn)
}
}
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) {
case N.NetworkUDP:
if !address.IsIPv6() {
return trackConn(d.udpDialer4.DialContext(ctx, network, address.String()))
} else {
return trackConn(d.udpDialer6.DialContext(ctx, network, address.String()))
return trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) {
switch N.NetworkName(network) {
case N.NetworkUDP:
if !address.IsIPv6() {
return d.udpDialer4.DialContext(ctx, network, address.String())
} else {
return d.udpDialer6.DialContext(ctx, network, address.String())
}
}
}
if !address.IsIPv6() {
return trackConn(DialSlowContext(&d.dialer4, ctx, network, address))
} else {
return trackConn(DialSlowContext(&d.dialer6, ctx, network, address))
}
if !address.IsIPv6() {
return DialSlowContext(&d.dialer4, ctx, network, address)
} else {
return DialSlowContext(&d.dialer6, ctx, network, address)
}
}))
} else {
return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
}
@ -280,13 +293,15 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if d.networkStrategy == nil {
if destination.IsIPv6() {
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6))
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4))
} else {
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4))
}
return trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) {
if destination.IsIPv6() {
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
return d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4)
} else {
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4)
}
}))
} else {
return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
}

View File

@ -6,26 +6,39 @@ import (
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type DirectDialer interface {
IsEmpty() bool
}
type DetourDialer struct {
outboundManager adapter.OutboundManager
detour string
legacyDNSDialer bool
dialer N.Dialer
initOnce sync.Once
initErr error
}
func NewDetour(outboundManager adapter.OutboundManager, detour string) N.Dialer {
return &DetourDialer{outboundManager: outboundManager, detour: detour}
func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNSDialer bool) N.Dialer {
return &DetourDialer{
outboundManager: outboundManager,
detour: detour,
legacyDNSDialer: legacyDNSDialer,
}
}
func (d *DetourDialer) Start() error {
_, err := d.Dialer()
return err
func InitializeDetour(dialer N.Dialer) error {
detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
if !isDetour {
return nil
}
return common.Error(detourDialer.Dialer())
}
func (d *DetourDialer) Dialer() (N.Dialer, error) {
@ -34,11 +47,20 @@ func (d *DetourDialer) Dialer() (N.Dialer, error) {
}
func (d *DetourDialer) init() {
var loaded bool
d.dialer, loaded = d.outboundManager.Outbound(d.detour)
dialer, loaded := d.outboundManager.Outbound(d.detour)
if !loaded {
d.initErr = E.New("outbound detour not found: ", d.detour)
return
}
if !d.legacyDNSDialer {
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
if directDialer.IsEmpty() {
d.initErr = E.New("detour to an empty direct outbound makes no sense")
return
}
}
}
d.dialer = dialer
}
func (d *DetourDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {

View File

@ -22,6 +22,9 @@ type Options struct {
RemoteIsDomain bool
DirectResolver bool
ResolverOnDetour bool
NewDialer bool
LegacyDNSDialer bool
DirectOutbound bool
}
// TODO: merge with NewWithOptions
@ -44,14 +47,14 @@ func NewWithOptions(options Options) (N.Dialer, error) {
if outboundManager == nil {
return nil, E.New("missing outbound manager")
}
dialer = NewDetour(outboundManager, dialOptions.Detour)
dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
} else {
dialer, err = NewDefault(options.Context, dialOptions)
if err != nil {
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
@ -80,6 +83,7 @@ func NewWithOptions(options Options) (N.Dialer, error) {
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
//nolint:staticcheck
strategy = C.DomainStrategy(dialOptions.DomainStrategy)
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
}
server = dialOptions.DomainResolver.Server
dnsQueryOptions = adapter.DNSQueryOptions{
@ -92,16 +96,32 @@ func NewWithOptions(options Options) (N.Dialer, error) {
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else if options.DirectResolver {
return nil, E.New("missing domain resolver for domain server address")
} else if defaultOptions.DomainResolver != "" {
dnsQueryOptions = defaultOptions.DomainResolveOptions
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
if !loaded {
return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
}
dnsQueryOptions.Transport = transport
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else {
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
if defaultOptions.DomainResolver != "" {
dnsQueryOptions = defaultOptions.DomainResolveOptions
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
if !loaded {
return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
}
dnsQueryOptions.Transport = transport
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else {
transports := dnsTransport.Transports()
if len(transports) < 2 {
dnsQueryOptions.Transport = dnsTransport.Default()
} else if options.NewDialer {
return nil, E.New("missing domain resolver for domain server address")
} else if !options.DirectOutbound {
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
}
}
if
//nolint:staticcheck
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
//nolint:staticcheck
dnsQueryOptions.Strategy = C.DomainStrategy(dialOptions.DomainStrategy)
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
}
}
dialer = NewResolveDialer(
options.Context,

View File

@ -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

View File

@ -4,6 +4,8 @@ import (
"context"
"net"
"net/netip"
"runtime"
"strings"
"sync/atomic"
"github.com/sagernet/sing-box/adapter"
@ -14,6 +16,8 @@ import (
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/vishvananda/netns"
)
type Listener struct {
@ -135,3 +139,30 @@ func (l *Listener) UDPConn() *net.UDPConn {
func (l *Listener) ListenOptions() option.ListenOptions {
return l.listenOptions
}
func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (T, error) {
if nameOrPath != "" {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
currentNs, err := netns.Get()
if err != nil {
return common.DefaultValue[T](), E.Cause(err, "get current netns")
}
defer netns.Set(currentNs)
var targetNs netns.NsHandle
if strings.HasPrefix(nameOrPath, "/") {
targetNs, err = netns.GetFromPath(nameOrPath)
} else {
targetNs, err = netns.GetFromName(nameOrPath)
}
if err != nil {
return common.DefaultValue[T](), E.Cause(err, "get netns ", nameOrPath)
}
defer targetNs.Close()
err = netns.Set(targetNs)
if err != nil {
return common.DefaultValue[T](), E.Cause(err, "set netns to ", nameOrPath)
}
}
return block()
}

View File

@ -8,18 +8,32 @@ import (
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
"github.com/metacubex/tfo-go"
)
func (l *Listener) ListenTCP() (net.Listener, error) {
//nolint:staticcheck
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
}
var err error
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
var tcpListener net.Listener
var listenConfig net.ListenConfig
if l.listenOptions.BindInterface != "" {
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
}
if l.listenOptions.RoutingMark != 0 {
listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
}
if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
}
if l.listenOptions.TCPKeepAlive >= 0 {
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
if keepIdle == 0 {
@ -37,20 +51,19 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
}
setMultiPathTCP(&listenConfig)
}
if l.listenOptions.TCPFastOpen {
var tfoConfig tfo.ListenConfig
tfoConfig.ListenConfig = listenConfig
tcpListener, err = tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
} else {
tcpListener, err = listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
}
if err == nil {
l.logger.Info("tcp server started at ", tcpListener.Addr())
}
//nolint:staticcheck
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
tcpListener, err := ListenNetworkNamespace[net.Listener](l.listenOptions.NetNs, func() (net.Listener, error) {
if l.listenOptions.TCPFastOpen {
var tfoConfig tfo.ListenConfig
tfoConfig.ListenConfig = listenConfig
return tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
} else {
return listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
}
})
if err != nil {
return nil, err
}
l.logger.Info("tcp server started at ", tcpListener.Addr())
l.tcpListener = tcpListener
return tcpListener, err
}

View File

@ -1,20 +1,32 @@
package listener
import (
"context"
"net"
"net/netip"
"os"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
func (l *Listener) ListenUDP() (net.PacketConn, error) {
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
var lc net.ListenConfig
var listenConfig net.ListenConfig
if l.listenOptions.BindInterface != "" {
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
}
if l.listenOptions.RoutingMark != 0 {
listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
}
if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
}
var udpFragment bool
if l.listenOptions.UDPFragment != nil {
udpFragment = *l.listenOptions.UDPFragment
@ -22,9 +34,11 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
udpFragment = l.listenOptions.UDPFragmentDefault
}
if !udpFragment {
lc.Control = control.Append(lc.Control, control.DisableUDPFragment())
listenConfig.Control = control.Append(listenConfig.Control, control.DisableUDPFragment())
}
udpConn, err := lc.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
udpConn, err := ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
return listenConfig.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
})
if err != nil {
return nil, err
}
@ -34,6 +48,36 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
return udpConn, err
}
func (l *Listener) DialContext(dialer net.Dialer, ctx context.Context, network string, address string) (net.Conn, error) {
return ListenNetworkNamespace[net.Conn](l.listenOptions.NetNs, func() (net.Conn, error) {
if l.listenOptions.BindInterface != "" {
dialer.Control = control.Append(dialer.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
}
if l.listenOptions.RoutingMark != 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
}
if l.listenOptions.ReuseAddr {
dialer.Control = control.Append(dialer.Control, control.ReuseAddr())
}
return dialer.DialContext(ctx, network, address)
})
}
func (l *Listener) ListenPacket(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.PacketConn, error) {
return ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
if l.listenOptions.BindInterface != "" {
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
}
if l.listenOptions.RoutingMark != 0 {
listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
}
if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
}
return listenConfig.ListenPacket(ctx, network, address)
})
}
func (l *Listener) UDPAddr() M.Socksaddr {
return l.udpAddr
}

58
common/sniff/ntp.go Normal file
View 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
View 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)
}

View File

@ -16,7 +16,7 @@ import (
"github.com/caddyserver/certmagic"
"github.com/libdns/alidns"
"github.com/libdns/cloudflare"
"github.com/mholt/acmez/acme"
"github.com/mholt/acmez/v3/acme"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

View File

@ -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) {

View File

@ -18,6 +18,7 @@ type (
STDConfig = tls.Config
STDConn = tls.Conn
ConnectionState = tls.ConnectionState
CurveID = tls.CurveID
)
func ParseTLSVersion(version string) (uint16, error) {

194
common/tls/ech.go Normal file
View File

@ -0,0 +1,194 @@
//go:build go1.24
package tls
import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/pem"
"net"
"os"
"strings"
"sync"
"time"
"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: STDClientConfig{tlsConfig},
dnsRouter: 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
access sync.Mutex
dnsRouter adapter.DNSRouter
lastTTL time.Duration
lastUpdate time.Time
}
func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
tlsConn, err := s.fetchAndHandshake(ctx, conn)
if err != nil {
return nil, err
}
err = tlsConn.HandshakeContext(ctx)
if err != nil {
return nil, err
}
return tlsConn, nil
}
func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
s.access.Lock()
defer s.access.Unlock()
if len(s.config.EncryptedClientHelloConfigList) == 0 || s.lastTTL == 0 || time.Now().Sub(s.lastUpdate) > s.lastTTL {
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")
}
match:
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.lastTTL = time.Duration(rr.Header().Ttl) * time.Second
s.lastUpdate = time.Now()
s.config.EncryptedClientHelloConfigList = echConfigList
break match
}
}
}
}
if len(s.config.EncryptedClientHelloConfigList) == 0 {
return nil, E.New("no ECH config found in DNS records")
}
}
return s.Client(conn)
}
func (s *STDECHClientConfig) Clone() Config {
return &STDECHClientConfig{STDClientConfig: STDClientConfig{s.config.Clone()}, dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate}
}
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
}

View File

@ -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")
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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
}

View File

@ -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.")
}

View File

@ -0,0 +1,5 @@
//go:build with_ech
package tls
var _ int = "Due to the migration to stdlib, the separate `with_ech` build tag has been deprecated and is no longer needed, please update your build configuration."

View File

@ -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

View File

@ -29,12 +29,13 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/debug"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
aTLS "github.com/sagernet/sing/common/tls"
utls "github.com/sagernet/utls"
utls "github.com/metacubex/utls"
"golang.org/x/crypto/hkdf"
"golang.org/x/net/http2"
)
@ -114,6 +115,22 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
if err != nil {
return nil, err
}
for _, extension := range uConn.Extensions {
if ce, ok := extension.(*utls.SupportedCurvesExtension); ok {
ce.Curves = common.Filter(ce.Curves, func(curveID utls.CurveID) bool {
return curveID != utls.X25519MLKEM768
})
}
if ks, ok := extension.(*utls.KeyShareExtension); ok {
ks.KeyShares = common.Filter(ks.KeyShares, func(share utls.KeyShare) bool {
return share.Group != utls.X25519MLKEM768
})
}
}
err = uConn.BuildHandshakeState()
if err != nil {
return nil, err
}
if len(uConfig.NextProtos) > 0 {
for _, extension := range uConn.Extensions {
@ -148,9 +165,13 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
if err != nil {
return nil, err
}
ecdheKey := uConn.HandshakeState.State13.EcdheKey
keyShareKeys := uConn.HandshakeState.State13.KeyShareKeys
if keyShareKeys == nil {
return nil, E.New("nil KeyShareKeys")
}
ecdheKey := keyShareKeys.Ecdhe
if ecdheKey == nil {
return nil, E.New("nil ecdhe_key")
return nil, E.New("nil ecdheKey")
}
authKey, err := ecdheKey.ECDH(publicKey)
if err != nil {
@ -214,10 +235,6 @@ func realityClientFallback(ctx context.Context, uConn net.Conn, serverName strin
response.Body.Close()
}
func (e *RealityClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) {
e.uClient.config.SessionIDGenerator = generator
}
func (e *RealityClientConfig) Clone() Config {
return &RealityClientConfig{
e.ctx,

View File

@ -1,4 +1,4 @@
//go:build with_reality_server
//go:build with_utls
package tls
@ -7,28 +7,29 @@ import (
"crypto/tls"
"encoding/base64"
"encoding/hex"
"fmt"
"net"
"time"
"github.com/sagernet/reality"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/debug"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
utls "github.com/metacubex/utls"
)
var _ ServerConfigCompat = (*RealityServerConfig)(nil)
type RealityServerConfig struct {
config *reality.Config
config *utls.RealityConfig
}
func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (*RealityServerConfig, error) {
var tlsConfig reality.Config
var tlsConfig utls.RealityConfig
if options.ACME != nil && len(options.ACME.Domain) > 0 {
return nil, E.New("acme is unavailable in reality")
@ -74,6 +75,11 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
}
tlsConfig.SessionTicketsDisabled = true
tlsConfig.Log = func(format string, v ...any) {
if logger != nil {
logger.Trace(fmt.Sprintf(format, v...))
}
}
tlsConfig.Type = N.NetworkTCP
tlsConfig.Dest = options.Reality.Handshake.ServerOptions.Build().String()
@ -113,10 +119,6 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
}
if debug.Enabled {
tlsConfig.Show = true
}
return &RealityServerConfig{&tlsConfig}, nil
}
@ -157,7 +159,7 @@ func (c *RealityServerConfig) Server(conn net.Conn) (Conn, error) {
}
func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn) (Conn, error) {
tlsConn, err := reality.Server(ctx, conn, c.config)
tlsConn, err := utls.RealityServer(ctx, conn, c.config)
if err != nil {
return nil, err
}
@ -173,7 +175,7 @@ func (c *RealityServerConfig) Clone() Config {
var _ Conn = (*realityConnWrapper)(nil)
type realityConnWrapper struct {
*reality.Conn
*utls.Conn
}
func (c *realityConnWrapper) ConnectionState() ConnectionState {

View File

@ -1,15 +1,5 @@
//go:build !with_reality_server
//go:build with_reality_server
package tls
import (
"context"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
return nil, E.New(`reality server is not included in this build, rebuild with -tags with_reality_server`)
}
var _ int = "The separate `with_reality_server` build tag has been merged into `with_utls` and is no longer needed, please update your build configuration."

View File

@ -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) {

View File

@ -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
}

View File

@ -27,6 +27,7 @@ type STDServerConfig struct {
key []byte
certificatePath string
keyPath string
echKeyPath string
watcher *fswatch.Watcher
}
@ -95,6 +96,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) {
@ -116,25 +120,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
}
@ -243,6 +255,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,
@ -251,5 +270,6 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
key: key,
certificatePath: options.CertificatePath,
keyPath: options.KeyPath,
echKeyPath: echKeyPath,
}, nil
}

View File

@ -16,8 +16,8 @@ import (
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
utls "github.com/sagernet/utls"
utls "github.com/metacubex/utls"
"golang.org/x/net/http2"
)

View File

@ -5,6 +5,7 @@ package tls
import (
"context"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
@ -14,5 +15,9 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
}
func NewRealityClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return nil, E.New(`uTLS, which is required by reality client is not included in this build, rebuild with -tags with_utls`)
return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`)
}
func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`)
}

View File

@ -1,7 +1,9 @@
package tf
import (
"bytes"
"context"
"encoding/binary"
"math/rand"
"net"
"strings"
@ -17,17 +19,19 @@ type Conn struct {
tcpConn *net.TCPConn
ctx context.Context
firstPacketWritten bool
splitRecord bool
fallbackDelay time.Duration
}
func NewConn(conn net.Conn, ctx context.Context, fallbackDelay time.Duration) (*Conn, error) {
func NewConn(conn net.Conn, ctx context.Context, splitRecord bool, fallbackDelay time.Duration) *Conn {
tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn)
return &Conn{
Conn: conn,
tcpConn: tcpConn,
ctx: ctx,
splitRecord: splitRecord,
fallbackDelay: fallbackDelay,
}, nil
}
}
func (c *Conn) Write(b []byte) (n int, err error) {
@ -37,10 +41,12 @@ func (c *Conn) Write(b []byte) (n int, err error) {
}()
serverName := indexTLSServerName(b)
if serverName != nil {
if c.tcpConn != nil {
err = c.tcpConn.SetNoDelay(true)
if err != nil {
return
if !c.splitRecord {
if c.tcpConn != nil {
err = c.tcpConn.SetNoDelay(true)
if err != nil {
return
}
}
}
splits := strings.Split(serverName.ServerName, ".")
@ -61,16 +67,25 @@ func (c *Conn) Write(b []byte) (n int, err error) {
currentIndex++
}
}
var buffer bytes.Buffer
for i := 0; i <= len(splitIndexes); i++ {
var payload []byte
if i == 0 {
payload = b[:splitIndexes[i]]
if c.splitRecord {
payload = payload[recordLayerHeaderLen:]
}
} else if i == len(splitIndexes) {
payload = b[splitIndexes[i-1]:]
} else {
payload = b[splitIndexes[i-1]:splitIndexes[i]]
}
if c.tcpConn != nil && i != len(splitIndexes) {
if c.splitRecord {
payloadLen := uint16(len(payload))
buffer.Write(b[:3])
binary.Write(&buffer, binary.BigEndian, payloadLen)
buffer.Write(payload)
} else if c.tcpConn != nil && i != len(splitIndexes) {
err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay)
if err != nil {
return
@ -82,11 +97,18 @@ func (c *Conn) Write(b []byte) (n int, err error) {
}
}
}
if c.tcpConn != nil {
err = c.tcpConn.SetNoDelay(false)
if c.splitRecord {
_, err = c.tcpConn.Write(buffer.Bytes())
if err != nil {
return
}
} else {
if c.tcpConn != nil {
err = c.tcpConn.SetNoDelay(false)
if err != nil {
return
}
}
}
return len(b), nil
}

View File

@ -0,0 +1,32 @@
package tf_test
import (
"context"
"crypto/tls"
"net"
"testing"
tf "github.com/sagernet/sing-box/common/tlsfragment"
"github.com/stretchr/testify/require"
)
func TestTLSFragment(t *testing.T) {
t.Parallel()
tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
require.NoError(t, err)
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), false, 0), &tls.Config{
ServerName: "www.cloudflare.com",
})
require.NoError(t, tlsConn.Handshake())
}
func TestTLSRecordFragment(t *testing.T) {
t.Parallel()
tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
require.NoError(t, err)
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, 0), &tls.Config{
ServerName: "www.cloudflare.com",
})
require.NoError(t, tlsConn.Handshake())
}

View File

@ -15,18 +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"
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 (

View File

@ -19,10 +19,12 @@ const (
TypeTor = "tor"
TypeSSH = "ssh"
TypeShadowTLS = "shadowtls"
TypeAnyTLS = "anytls"
TypeShadowsocksR = "shadowsocksr"
TypeVLESS = "vless"
TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2"
TypeTailscale = "tailscale"
)
const (
@ -76,6 +78,8 @@ func ProxyDisplayName(proxyType string) string {
return "TUIC"
case TypeHysteria2:
return "Hysteria2"
case TypeAnyTLS:
return "AnyTLS"
case TypeSelector:
return "Selector"
case TypeURLTest:

View File

@ -33,6 +33,7 @@ const (
RuleActionTypeHijackDNS = "hijack-dns"
RuleActionTypeSniff = "sniff"
RuleActionTypeResolve = "resolve"
RuleActionTypePredefined = "predefined"
)
const (

View File

@ -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 (
@ -105,7 +105,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
}
question := message.Question[0]
if options.ClientSubnet.IsValid() {
message = SetClientSubnet(message, options.ClientSubnet, true)
message = SetClientSubnet(message, options.ClientSubnet)
}
isSimpleRequest := len(message.Question) == 1 &&
len(message.Ns) == 0 &&
@ -232,10 +232,20 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
record.Header().Ttl = timeToLive
}
}
response.Id = messageId
if !disableCache {
c.storeCache(transport, question, response, timeToLive)
}
response.Id = messageId
requestEDNSOpt := message.IsEdns0()
responseEDNSOpt := response.IsEdns0()
if responseEDNSOpt != nil && (requestEDNSOpt == nil || requestEDNSOpt.Version() < responseEDNSOpt.Version()) {
response.Extra = common.Filter(response.Extra, func(it dns.RR) bool {
return it.Header().Rrtype != dns.TypeOPT
})
if requestEDNSOpt != nil {
response.SetEdns0(responseEDNSOpt.UDPSize(), responseEDNSOpt.Do())
}
}
logExchangedResponse(c.logger, ctx, response, timeToLive)
return response, err
}
@ -483,8 +493,8 @@ 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)
if response.Rcode != dns.RcodeSuccess {
return nil, RcodeError(response.Rcode)
}
addresses := make([]netip.Addr, 0, len(response.Answer))
for _, rawAnswer := range response.Answer {
@ -508,10 +518,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 +547,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 +557,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 +571,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
}

View File

@ -6,7 +6,11 @@ import (
"github.com/miekg/dns"
)
func SetClientSubnet(message *dns.Msg, clientSubnet netip.Prefix, override bool) *dns.Msg {
func SetClientSubnet(message *dns.Msg, clientSubnet netip.Prefix) *dns.Msg {
return setClientSubnet(message, clientSubnet, true)
}
func setClientSubnet(message *dns.Msg, clientSubnet netip.Prefix, clone bool) *dns.Msg {
var (
optRecord *dns.OPT
subnetOption *dns.EDNS0_SUBNET
@ -19,9 +23,6 @@ findExists:
var isEDNS0Subnet bool
subnetOption, isEDNS0Subnet = option.(*dns.EDNS0_SUBNET)
if isEDNS0Subnet {
if !override {
return message
}
break findExists
}
}
@ -37,14 +38,14 @@ findExists:
},
}
message.Extra = append(message.Extra, optRecord)
} else {
message = message.Copy()
} else if clone {
return setClientSubnet(message.Copy(), clientSubnet, false)
}
if subnetOption == nil {
subnetOption = new(dns.EDNS0_SUBNET)
subnetOption.Code = dns.EDNS0SUBNET
optRecord.Option = append(optRecord.Option, subnetOption)
}
subnetOption.Code = dns.EDNS0SUBNET
if clientSubnet.Addr().Is4() {
subnetOption.Family = 1
} else {

View File

@ -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)]
}

View File

@ -174,7 +174,6 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
return transport, currentRule, currentRuleIndex
case *R.RuleActionDNSRouteOptions:
if action.Strategy != C.DomainStrategyAsIS {
@ -189,9 +188,9 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
if action.ClientSubnet.IsValid() {
options.ClientSubnet = action.ClientSubnet
}
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
case *R.RuleActionReject:
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
return nil, currentRule, currentRuleIndex
case *R.RuleActionPredefined:
return nil, currentRule, currentRuleIndex
}
}
@ -263,6 +262,8 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
case *R.RuleActionPredefined:
return action.Response(message), nil
}
}
var responseCheck func(responseAddrs []netip.Addr) bool
@ -322,6 +323,9 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
err error
)
printResult := func() {
if err == nil && len(responseAddrs) == 0 {
err = E.New("empty result")
}
if err != nil {
if errors.Is(err, ErrResponseRejectedCached) {
r.logger.DebugContext(ctx, "response rejected for ", domain, " (cached)")
@ -330,15 +334,15 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
}
} else if len(responseAddrs) == 0 {
r.logger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result")
err = RCodeNameError
}
if err != nil {
err = E.Cause(err, "lookup ", domain)
}
}
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
if cached {
if len(responseAddrs) == 0 {
return nil, RCodeNameError
return nil, E.New("lookup ", domain, ": empty result (cached)")
}
return responseAddrs, nil
}
@ -369,7 +373,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:
@ -379,6 +384,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
@ -388,16 +407,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), " "))
@ -432,6 +452,6 @@ func (r *Router) LookupReverseMapping(ip netip.Addr) (string, bool) {
func (r *Router) ResetNetwork() {
r.ClearCache()
for _, transport := range r.transport.Transports() {
transport.Reset()
transport.Close()
}
}

View File

@ -81,7 +81,7 @@ func (t *Transport) Start(stage adapter.StartStage) error {
func (t *Transport) Close() error {
for _, transport := range t.transports {
transport.Reset()
transport.Close()
}
if t.interfaceCallback != nil {
t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback)
@ -89,12 +89,6 @@ func (t *Transport) Close() error {
return nil
}
func (t *Transport) Reset() {
for _, transport := range t.transports {
transport.Reset()
}
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
err := t.fetchServers()
if err != nil {
@ -252,7 +246,7 @@ func (t *Transport) recreateServers(iface *control.Interface, serverAddrs []M.So
transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, serverAddr))
}
for _, transport := range t.transports {
transport.Reset()
transport.Close()
}
t.transports = transports
return nil

View File

@ -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,31 +23,49 @@ 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
}
func (t *Transport) Reset() {
func (t *Transport) Start(stage adapter.StartStage) error {
return nil
}
func (t *Transport) Close() error {
return nil
}
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 {

View File

@ -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() {

View File

@ -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"))
}

View File

@ -10,6 +10,7 @@ import (
"strconv"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
@ -91,10 +92,13 @@ 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
}
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return NewHTTPSRaw(
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions),
logger,
@ -149,9 +153,17 @@ func NewHTTPSRaw(
}
}
func (t *HTTPSTransport) Reset() {
func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
return dialer.InitializeDetour(t.dialer)
}
func (t *HTTPSTransport) Close() error {
t.transport.CloseIdleConnections()
t.transport = t.transport.Clone()
return nil
}
func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {

View File

@ -3,6 +3,7 @@ package local
import (
"context"
"math/rand"
"net/netip"
"time"
"github.com/sagernet/sing-box/adapter"
@ -19,14 +20,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
}
@ -38,12 +36,18 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
}
return &Transport{
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
ctx: ctx,
hosts: hosts.NewFile(hosts.DefaultPath),
dialer: transportDialer,
}, nil
}
func (t *Transport) Reset() {
func (t *Transport) Start(stage adapter.StartStage) error {
return nil
}
func (t *Transport) Close() error {
return nil
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
@ -55,7 +59,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
}
}
systemConfig := getSystemDNSConfig()
systemConfig := getSystemDNSConfig(t.ctx)
if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
return t.exchangeSingleRequest(ctx, systemConfig, message, domain)
} else {
@ -87,8 +91,9 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
startRacer := func(ctx context.Context, fqdn string) {
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
if err == nil {
addresses, _ := dns.MessageToAddresses(response)
if len(addresses) == 0 {
var addresses []netip.Addr
addresses, err = dns.MessageToAddresses(response)
if err == nil && len(addresses) == 0 {
err = E.New(fqdn, ": empty result")
}
}
@ -142,6 +147,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}

View 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.")
}
}

View File

@ -1,6 +1,7 @@
package local
import (
"context"
"os"
"runtime"
"strings"
@ -23,19 +24,21 @@ type resolverConfig struct {
var resolvConf resolverConfig
func getSystemDNSConfig() *dnsConfig {
resolvConf.tryUpdate("/etc/resolv.conf")
func getSystemDNSConfig(ctx context.Context) *dnsConfig {
resolvConf.tryUpdate(ctx, "/etc/resolv.conf")
return resolvConf.dnsConfig.Load()
}
func (conf *resolverConfig) init() {
conf.dnsConfig.Store(dnsReadConfig("/etc/resolv.conf"))
func (conf *resolverConfig) init(ctx context.Context) {
conf.dnsConfig.Store(dnsReadConfig(ctx, "/etc/resolv.conf"))
conf.lastChecked = time.Now()
conf.ch = make(chan struct{}, 1)
}
func (conf *resolverConfig) tryUpdate(name string) {
conf.initOnce.Do(conf.init)
func (conf *resolverConfig) tryUpdate(ctx context.Context, name string) {
conf.initOnce.Do(func() {
conf.init(ctx)
})
if conf.dnsConfig.Load().noReload {
return
@ -59,7 +62,7 @@ func (conf *resolverConfig) tryUpdate(name string) {
return
}
}
dnsConf := dnsReadConfig(name)
dnsConf := dnsReadConfig(ctx, name)
conf.dnsConfig.Store(dnsConf)
}

View File

@ -0,0 +1,54 @@
//go:build darwin && cgo
package local
/*
#include <stdlib.h>
#include <stdio.h>
#include <resolv.h>
#include <arpa/inet.h>
*/
import "C"
import (
"context"
"time"
E "github.com/sagernet/sing/common/exceptions"
"github.com/miekg/dns"
)
func dnsReadConfig(_ context.Context, _ 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
}

View 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
}

View File

@ -1,18 +1,20 @@
//go:build !windows
//go:build !windows && !(darwin && cgo)
package local
import (
"bufio"
"context"
"net"
"net/netip"
"os"
"strings"
"time"
_ "unsafe"
"github.com/miekg/dns"
)
func dnsReadConfig(name string) *dnsConfig {
func dnsReadConfig(_ context.Context, name string) *dnsConfig {
conf := &dnsConfig{
ndots: 1,
timeout: 5 * time.Second,
@ -69,13 +71,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 +139,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) {

View File

@ -1,6 +1,7 @@
package local
import (
"context"
"net"
"net/netip"
"os"
@ -8,10 +9,13 @@ import (
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/service"
"golang.org/x/sys/windows"
)
func dnsReadConfig(_ string) *dnsConfig {
func dnsReadConfig(ctx context.Context, _ string) *dnsConfig {
conf := &dnsConfig{
ndots: 1,
timeout: 5 * time.Second,
@ -22,35 +26,35 @@ func dnsReadConfig(_ string) *dnsConfig {
conf.servers = defaultNS
}
}()
aas, err := adapterAddresses()
addresses, err := adapterAddresses()
if err != nil {
return nil
}
for _, aa := range aas {
// Only take interfaces whose OperStatus is IfOperStatusUp(0x01) into DNS configs.
if aa.OperStatus != windows.IfOperStatusUp {
var dnsAddresses []struct {
ifName string
netip.Addr
}
for _, address := range addresses {
if address.OperStatus != windows.IfOperStatusUp {
continue
}
// Only take interfaces which have at least one gateway
if aa.FirstGatewayAddress == nil {
if address.IfType == windows.IF_TYPE_TUNNEL {
continue
}
for dns := aa.FirstDnsServerAddress; dns != nil; dns = dns.Next {
sa, err := dns.Address.Sockaddr.Sockaddr()
if address.FirstGatewayAddress == nil {
continue
}
for dnsServerAddress := address.FirstDnsServerAddress; dnsServerAddress != nil; dnsServerAddress = dnsServerAddress.Next {
rawSockaddr, err := dnsServerAddress.Address.Sockaddr.Sockaddr()
if err != nil {
continue
}
var ip netip.Addr
switch sa := sa.(type) {
var dnsServerAddr netip.Addr
switch sockaddr := rawSockaddr.(type) {
case *syscall.SockaddrInet4:
ip = netip.AddrFrom4([4]byte{sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3]})
dnsServerAddr = netip.AddrFrom4(sockaddr.Addr)
case *syscall.SockaddrInet6:
var addr16 [16]byte
copy(addr16[:], sa.Addr[:])
if addr16[0] == 0xfe && addr16[1] == 0xc0 {
if sockaddr.Addr[0] == 0xfe && sockaddr.Addr[1] == 0xc0 {
// fec0/10 IPv6 addresses are site local anycast DNS
// addresses Microsoft sets by default if no other
// IPv6 DNS address is set. Site local anycast is
@ -58,20 +62,30 @@ func dnsReadConfig(_ string) *dnsConfig {
// https://datatracker.ietf.org/doc/html/rfc3879
continue
}
ip = netip.AddrFrom16(addr16)
dnsServerAddr = netip.AddrFrom16(sockaddr.Addr)
default:
// Unexpected type.
continue
}
conf.servers = append(conf.servers, net.JoinHostPort(ip.String(), "53"))
dnsAddresses = append(dnsAddresses, struct {
ifName string
netip.Addr
}{ifName: windows.UTF16PtrToString(address.FriendlyName), Addr: dnsServerAddr})
}
}
var myInterface string
if networkManager := service.FromContext[adapter.NetworkManager](ctx); networkManager != nil {
myInterface = networkManager.InterfaceMonitor().MyInterface()
}
for _, address := range dnsAddresses {
if address.ifName == myInterface {
continue
}
conf.servers = append(conf.servers, net.JoinHostPort(address.String(), "53"))
}
return conf
}
//go:linkname defaultNS net.defaultNS
var defaultNS []string
func adapterAddresses() ([]*windows.IpAdapterAddresses, error) {
var b []byte
l := uint32(15000) // recommended initial size

View File

@ -1,83 +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
copyAnswer.Question = message.Question
return &copyAnswer, nil
}
}
}
return nil, dns.RCodeNameError
}

View File

@ -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"
@ -89,10 +88,13 @@ 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
}
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return &HTTP3Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
logger: logger,
@ -101,8 +103,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
}
@ -113,8 +114,12 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
}, nil
}
func (t *HTTP3Transport) Reset() {
t.transport.Close()
func (t *HTTP3Transport) Start(stage adapter.StartStage) error {
return nil
}
func (t *HTTP3Transport) Close() error {
return t.transport.Close()
}
func (t *HTTP3Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {

View File

@ -16,6 +16,7 @@ import (
sQUIC "github.com/sagernet/sing-quic"
"github.com/sagernet/sing/common"
"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"
@ -54,10 +55,13 @@ 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
}
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return &Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions),
ctx: ctx,
@ -68,13 +72,18 @@ func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options
}, nil
}
func (t *Transport) Reset() {
func (t *Transport) Start(stage adapter.StartStage) error {
return nil
}
func (t *Transport) Close() error {
t.access.Lock()
defer t.access.Unlock()
connection := t.connection
if connection != nil {
connection.CloseWithError(0, "")
}
return nil
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
@ -135,12 +144,12 @@ func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn quic.C
if err != nil {
return nil, err
}
defer stream.Close()
defer stream.CancelRead(0)
err = transport.WriteMessage(stream, 0, message)
if err != nil {
stream.Close()
return nil, err
}
stream.Close()
return transport.ReadMessage(stream)
}

View File

@ -6,12 +6,14 @@ import (
"io"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
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"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
@ -35,10 +37,13 @@ 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
}
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return &TCPTransport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTCP, tag, options),
dialer: transportDialer,
@ -46,7 +51,15 @@ func NewTCP(ctx context.Context, logger log.ContextLogger, tag string, options o
}, nil
}
func (t *TCPTransport) Reset() {
func (t *TCPTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
return dialer.InitializeDetour(t.dialer)
}
func (t *TCPTransport) Close() error {
return nil
}
func (t *TCPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {

View File

@ -5,6 +5,7 @@ import (
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
@ -52,10 +53,13 @@ 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
}
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return &TLSTransport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions),
logger: logger,
@ -65,13 +69,21 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
}, nil
}
func (t *TLSTransport) Reset() {
func (t *TLSTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
return dialer.InitializeDetour(t.dialer)
}
func (t *TLSTransport) Close() error {
t.access.Lock()
defer t.access.Unlock()
for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
connection.Value.Close()
}
t.connections.Init()
return nil
}
func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {

View File

@ -7,11 +7,13 @@ import (
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
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/buf"
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"
@ -42,10 +44,13 @@ 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
}
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return NewUDPRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeUDP, tag, options), transportDialer, serverAddr), nil
}
@ -64,11 +69,19 @@ func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer
}
}
func (t *UDPTransport) Reset() {
func (t *UDPTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
return dialer.InitializeDetour(t.dialer)
}
func (t *UDPTransport) Close() error {
t.access.Lock()
defer t.access.Unlock()
close(t.done)
t.done = make(chan struct{})
return nil
}
func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
@ -108,15 +121,8 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
conn.access.Unlock()
defer func() {
conn.access.Lock()
delete(conn.callbacks, messageId)
delete(conn.callbacks, exMessage.Id)
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 {
@ -210,8 +216,8 @@ type dnsConnection struct {
func (c *dnsConnection) Close(err error) {
c.closeOnce.Do(func() {
close(c.done)
c.err = err
close(c.done)
})
c.Conn.Close()
}

View File

@ -20,9 +20,10 @@ func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (
return dialer.NewDefaultOutbound(ctx), nil
} else {
return dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
DirectResolver: true,
Context: ctx,
Options: options.DialerOptions,
DirectResolver: true,
LegacyDNSDialer: options.Legacy,
})
}
}
@ -43,10 +44,11 @@ func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions)
return transportDialer, nil
} else {
return dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
RemoteIsDomain: options.ServerIsDomain(),
DirectResolver: true,
Context: ctx,
Options: options.DialerOptions,
RemoteIsDomain: options.ServerIsDomain(),
DirectResolver: true,
LegacyDNSDialer: options.Legacy,
})
}
}

View File

@ -56,12 +56,15 @@ 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 {
if m.defaultTag != "" && m.defaultTransport == nil {
return E.New("default DNS server not found: ", m.defaultTag)
}
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(), "]")
@ -225,7 +228,7 @@ func (m *TransportManager) Remove(tag string) error {
}
}
if started {
transport.Reset()
transport.Close()
}
return nil
}

View File

@ -2,6 +2,31 @@
icon: material/alert-decagram
---
#### 1.12.0-beta.13
* Add TLS record fragment route options **1**
* Add missing `accept_routes` option for Tailscale **2**
* Fixes and improvements
**1**:
See [Route Action](/configuration/route/rule_action/#tls_record_fragment).
**2**:
See [Tailscale](/configuration/endpoint/tailscale/#accept_routes).
#### 1.12.0-beta.10
* Add control options for listeners **1**
* Fixes and improvements
**1**:
You can now set `bind_interface`, `routing_mark` and `reuse_addr` in Listen Fields.
See [Listen Fields](/configuration/shared/listen/).
### 1.11.10
* Undeprecate the `block` outbound **1**
@ -15,6 +40,11 @@ we decided to temporarily undeprecate the `block` outbound until a replacement i
_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-beta.9
* Update quic-go to v0.51.0
* Fixes and improvements
### 1.11.9
* Fixes and improvements
@ -22,6 +52,10 @@ violated the rules (TestFlight users are not affected)._
_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-beta.5
* Fixes and improvements
### 1.11.8
* Improve `auto_redirect` **1**
@ -35,6 +69,10 @@ see [Tun](/configuration/inbound/tun/#auto_redirect).
_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-beta.3
* Fixes and improvements
### 1.11.7
* Fixes and improvements
@ -42,6 +80,15 @@ violated the rules (TestFlight users are not affected)._
_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-beta.1
* Fixes and improvements
**1**:
Now `auto_redirect` fixes compatibility issues between tun and Docker bridge networks,
see [Tun](/configuration/inbound/tun/#auto_redirect).
### 1.11.6
* Fixes and improvements
@ -49,6 +96,40 @@ violated the rules (TestFlight users are not affected)._
_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.19
* Update gVisor to 20250319.0
* Fixes and improvements
#### 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
@ -56,10 +137,71 @@ violated the rules (TestFlight users are not affected)._
_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**
* Fixes and improvements
**1**:
See [Tailscale](/configuration/dns/server/tailscale/).
#### 1.12.0-alpha.6
* Add Tailscale endpoint **1**
* Drop support for go1.22 **2**
* Fixes and improvements
**1**:
See [Tailscale](/configuration/endpoint/tailscale/).
**2**:
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go).
### 1.11.3
* Fixes and improvements
@ -67,10 +209,69 @@ violated the rules (TestFlight users are not affected)._
_This version overwrites 1.11.2, as incorrect binaries were released due to a bug in the continuous integration
process._
#### 1.12.0-alpha.5
* Fixes and improvements
### 1.11.1
* Fixes and improvements
#### 1.12.0-alpha.2
* Update quic-go to v0.49.0
* Fixes and improvements
#### 1.12.0-alpha.1
* Refactor DNS servers **1**
* Add domain resolver options**2**
* Add TLS fragment route options **3**
* Add certificate options **4**
**1**:
DNS servers are refactored for better performance and scalability.
See [DNS server](/configuration/dns/server/).
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-servers).
Compatibility for old formats will be removed in sing-box 1.14.0.
**2**:
Legacy `outbound` DNS rules are deprecated
and can be replaced by the new `domain_resolver` option.
See [Dial Fields](/configuration/shared/dial/#domain_resolver) and
[Route](/configuration/route/#default_domain_resolver).
For migration,
see [Migrate outbound DNS rule items to domain resolver](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver).
**3**:
The new TLS fragment route options allow you to fragment TLS handshakes to bypass firewalls.
This feature is intended to circumvent simple firewalls based on **plaintext packet matching**, and should not be used
to circumvent real censorship.
Since it is not designed for performance, it should not be applied to all connections, but only to server names that are
known to be blocked.
See [Route Action](/configuration/route/rule_action/#tls_fragment).
**4**:
New certificate options allow you to manage the default list of trusted X509 CA certificates.
For the system certificate list, fixed Go not reading Android trusted certificates correctly.
You can also use the Mozilla Included List instead, or add trusted certificates yourself.
See [Certificate](/configuration/certificate/).
### 1.11.0
Important changes since 1.10:

View File

@ -9,6 +9,10 @@ and the data generated by the software is always on your device.
## Android
The broad package (App) visibility (QUERY_ALL_PACKAGES) permission
is used to provide per-application proxy features for VPN,
sing-box will not collect your app list.
If your configuration contains `wifi_ssid` or `wifi_bssid` routing rules,
sing-box uses the location permission in the background
to get information about the connected Wi-Fi network to make them work.

View File

@ -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

View File

@ -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 起"

View File

@ -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.

View File

@ -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 记录列表。

View 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"
}
]
}
}
```

View File

@ -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

View File

@ -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

View File

@ -27,19 +27,20 @@ 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/) |
| `local` | [Local](./local/) |
| `hosts` | [Hosts](./hosts/) |
| `tcp` | [TCP](./tcp/) |
| `udp` | [UDP](./udp/) |
| `tls` | [TLS](./tls/) |
| `quic` | [QUIC](./quic/) |
| `https` | [HTTPS](./https/) |
| `h3` | [HTTP/3](./http3/) |
| `dhcp` | [DHCP](./dhcp/) |
| `fakeip` | [Fake IP](./fakeip/) |
| `tailscale` | [Tailscale](./tailscale/) |
#### tag

View File

@ -0,0 +1,47 @@
---
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/) |
| `local` | [Local](./local/) |
| `hosts` | [Hosts](./hosts/) |
| `tcp` | [TCP](./tcp/) |
| `udp` | [UDP](./udp/) |
| `tls` | [TLS](./tls/) |
| `quic` | [QUIC](./quic/) |
| `https` | [HTTPS](./https/) |
| `h3` | [HTTP/3](./http3/) |
| `dhcp` | [DHCP](./dhcp/) |
| `fakeip` | [Fake IP](./fakeip/) |
| `tailscale` | [Tailscale](./tailscale/) |
#### tag
DNS 服务器的标签。

View File

@ -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.

View File

@ -0,0 +1,83 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.12.0"
# Tailscale
### Structure
```json
{
"dns": {
"servers": [
{
"type": "tailscale",
"tag": "",
"endpoint": "ts-ep",
"accept_default_resolvers": false
}
]
}
}
```
### Fields
#### endpoint
==Required==
The tag of the Tailscale endpoint.
#### accept_default_resolvers
Indicates whether default DNS resolvers should be accepted for fallback queries in addition to MagicDNS。
if not enabled, NXDOMAIN will be returned for non-Tailscale domain queries.
### Examples
=== "MagicDNS only"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "tailscale",
"tag": "ts",
"endpoint": "ts-ep"
}
],
"rules": [
{
"ip_accept_any": true,
"server": "ts"
}
]
}
}
```
=== "Use as global DNS"
```json
{
"dns": {
"servers": [
{
"type": "tailscale",
"endpoint": "ts-ep",
"accept_default_resolvers": true
}
]
}
}
```

View File

@ -4,7 +4,7 @@ icon: material/new-box
!!! question "Since sing-box 1.12.0"
# TCP
# UDP
### Structure

View File

@ -26,6 +26,7 @@ Endpoint is protocols that has both inbound and outbound behavior.
| Type | Format |
|-------------|---------------------------|
| `wireguard` | [WireGuard](./wireguard/) |
| `tailscale` | [Tailscale](./tailscale/) |
#### tag

View File

@ -23,9 +23,10 @@ icon: material/new-box
### 字段
| 类型 | 格式 |
| 类型 | 格式 |
|-------------|---------------------------|
| `wireguard` | [WireGuard](./wiregaurd/) |
| `wireguard` | [WireGuard](./wiregaurd/) |
| `tailscale` | [Tailscale](./tailscale/) |
#### tag

View File

@ -0,0 +1,104 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.12.0"
### Structure
```json
{
"type": "tailscale",
"tag": "ts-ep",
"state_directory": "",
"auth_key": "",
"control_url": "",
"ephemeral": false,
"hostname": "",
"accept_routes": false,
"exit_node": "",
"exit_node_allow_lan_access": false,
"advertise_routes": [],
"advertise_exit_node": false,
"udp_timeout": "5m",
... // Dial Fields
}
```
### Fields
#### state_directory
The directory where the Tailscale state is stored.
`tailscale` is used by default.
Example: `$HOME/.tailscale`
#### auth_key
!!! note
Auth key is not required. By default, sing-box will log the login URL (or popup a notification on graphical clients).
The auth key to create the node. If the node is already created (from state previously stored), then this field is not
used.
#### control_url
The coordination server URL.
`https://controlplane.tailscale.com` is used by default.
#### ephemeral
Indicates whether the instance should register as an Ephemeral node (https://tailscale.com/s/ephemeral-nodes).
#### hostname
The hostname of the node.
System hostname is used by default.
Example: `localhost`
#### accept_routes
Indicates whether the node should accept routes advertised by other nodes.
#### exit_node
The exit node name or IP address to use.
#### exit_node_allow_lan_access
!!! note
When the exit node does not have a corresponding advertised route, private traffics cannot be routed to the exit node even if `exit_node_allow_lan_access is` set.
Indicates whether locally accessible subnets should be routed directly or via the exit node.
#### advertise_routes
CIDR prefixes to advertise into the Tailscale network as reachable through the current node.
Example: `["192.168.1.1/24"]`
#### advertise_exit_node
Indicates whether the node should advertise itself as an exit node.
#### udp_timeout
UDP NAT expiration time.
`5m` will be used by default.
### Dial Fields
!!! note
Dial Fields in Tailscale endpoints only control how it connects to the control plane and have nothing to do with actual connections.
See [Dial Fields](/configuration/shared/dial/) for details.

View File

@ -41,7 +41,7 @@ icon: material/new-box
### 字段
#### system_interface
#### system
使用系统设备。

View File

@ -0,0 +1,61 @@
---
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:
```json
[
"stop=8",
"0=30-30",
"1=100-400",
"2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000",
"3=9-9,500-1000",
"4=500-1000",
"5=500-1000",
"6=500-1000",
"7=500-1000"
]
```
#### tls
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).

View File

@ -0,0 +1,61 @@
---
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 填充方案行数组。
默认填充方案:
```json
[
"stop=8",
"0=30-30",
"1=100-400",
"2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000",
"3=9-9,500-1000",
"4=500-1000",
"5=500-1000",
"6=500-1000",
"7=500-1000"
]
```
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。

View File

@ -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: |

View File

@ -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: |

View File

@ -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.

View File

@ -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 中可用。

View File

@ -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/) |

View File

@ -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/) |

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