Compare commits

...

59 Commits

Author SHA1 Message Date
世界
444f454810 Bump version 2025-10-14 23:43:36 +08:00
世界
d0e1fd6c7e Update Go to 1.25.3 2025-10-14 23:43:36 +08:00
世界
17b4d1e010 Update uTLS to v1.8.1 2025-10-14 23:40:19 +08:00
世界
06791470c9 Fix DNS reject panic 2025-10-14 23:40:19 +08:00
世界
ef14c8ca0e Disable TCP slow open for anytls
Fixes #3459
2025-10-14 23:40:19 +08:00
世界
36dc883c7c Fix DNS negative caching to comply with RFC 2308 2025-10-09 23:45:23 +08:00
Mahdi
6557bd7029 Fix dns cache in lookup 2025-10-09 23:45:23 +08:00
世界
41b30c91d9 Improve HTTPS DNS transport 2025-10-09 23:45:23 +08:00
世界
0f767d5ce1 Update .gitignore 2025-10-07 13:37:11 +08:00
世界
328a6de797 Bump version 2025-10-05 17:58:21 +08:00
Mahdi
886be6414d Fix dns truncate 2025-10-05 17:58:21 +08:00
世界
9362d3cab3 Attempt to fix leak in quic-go 2025-10-01 11:59:17 +08:00
世界
ced2e39dbf Update dependencies 2025-10-01 11:55:33 +08:00
anytls
2159d8877b Update anytls v0.0.11
Co-authored-by: anytls <anytls>
2025-10-01 10:23:15 +08:00
世界
cb7dba3eff release: Improve publish testflight 2025-10-01 10:22:44 +08:00
世界
d9d7f7880d Pin gofumpt and golangci-lint versions
As we don't want to remove naked returns
2025-09-23 16:33:42 +08:00
世界
a031aaf2c0 Do not reset network on sleep or wake 2025-09-23 16:17:44 +08:00
世界
4bca951773 Fix adguard matcher 2025-09-23 16:12:29 +08:00
世界
140735dbde Fix websocket log handling 2025-09-23 16:12:29 +08:00
世界
714a68bba1 Update .gitignore 2025-09-23 16:12:29 +08:00
世界
573c6179ab Bump version 2025-09-13 13:16:13 +08:00
世界
510bf05e36 Fix UDP exchange for local/dhcp DNS servers 2025-09-13 12:26:48 +08:00
世界
ae852e0be4 Bump version 2025-09-13 03:09:10 +08:00
世界
1955002ed8 Do not cache DNS responses with empty answers 2025-09-13 03:04:08 +08:00
世界
44559fb7b9 Bump version 2025-09-13 00:07:57 +08:00
世界
0977c5cf73 release: Disable Apple platform CI builds, since `-allowProvisioningUpdates is broken by Apple 2025-09-13 00:07:57 +08:00
世界
07697bf931 release: Fix xcode build 2025-09-12 22:57:44 +08:00
世界
5d1d1a1456 Fix TCP exchange for local/dhcp DNS servers 2025-09-12 21:58:48 +08:00
世界
146383499e Fix race codes 2025-09-12 21:58:48 +08:00
世界
e81a76fdf9 Fix DNS exchange 2025-09-12 18:05:02 +08:00
世界
de13137418 Fix auto redirect 2025-09-12 11:04:12 +08:00
世界
e42b818c2a Fix dhcp fetch 2025-09-12 11:03:13 +08:00
世界
fcde0c94e0 Bump version 2025-09-10 22:57:24 +08:00
世界
1af83e997d Update Go to 1.25.1 2025-09-10 22:57:24 +08:00
世界
59ee7be72a Fix SyscallVectorisedPacketWriter 2025-09-10 22:46:41 +08:00
世界
c331ee3d5c Fix timeout check 2025-09-10 22:42:40 +08:00
世界
36babe4bef Fix hysteria2 handshake timeout 2025-09-09 18:03:18 +08:00
世界
c5f2cea802 Prevent panic when wintun dll fails to load 2025-09-09 14:49:00 +08:00
世界
8a200bf913 Fix auto redirect output 2025-09-09 14:29:40 +08:00
世界
f16468e74f Fix ipv6 tproxy listener 2025-09-09 14:16:40 +08:00
世界
79c0b9f51d Fix tls options ignored in mixed inbounds 2025-09-08 19:45:52 +08:00
世界
f98a3a4f65 Treat requests with OPT extra but no options as simple requests 2025-09-08 09:12:30 +08:00
世界
b14cecaeb2 Fix DNS packet size 2025-09-08 09:12:30 +08:00
世界
2594745ef8 Fix DNS client 2025-09-08 09:12:30 +08:00
世界
cc3041322e Fix DNS cache 2025-09-08 09:12:30 +08:00
世界
f352f84483 Fix read address 2025-09-05 15:16:14 +08:00
世界
cbf48e9b8c Fix multiple sniff 2025-09-03 20:09:05 +08:00
世界
0ef7e8eca2 Fix route.default_interface not taking effect 2025-09-02 18:00:02 +08:00
世界
1a18e43a88 Fix linux icmp routes 2025-09-02 17:55:48 +08:00
世界
6849288d6d Fix typo in TestSniffUQUICChrome115 2025-09-02 17:55:26 +08:00
世界
2edfed7d91 Improve DHCP DNS server 2025-09-02 17:55:26 +08:00
世界
30c069f5b7 Fix local DNS server on legacy windows 2025-09-02 17:55:26 +08:00
世界
649163cb7b Fix domain strategy not taking effect 2025-09-02 17:35:27 +08:00
世界
980e96250b Bump version 2025-08-28 12:11:30 +08:00
世界
963bc4b647 Enforce Tailscale NoLogsNoSupport 2025-08-28 10:30:13 +08:00
世界
031f25c1c1 Deprecate common/atomic 2025-08-25 19:49:12 +08:00
世界
b40f642fa4 Bump version 2025-08-21 09:43:47 +08:00
世界
22782ca6fc Fix outbound start 2025-08-21 09:41:31 +08:00
世界
1468d83895 Make realityClientConnWrapper replaceable 2025-08-20 16:26:27 +08:00
60 changed files with 1011 additions and 505 deletions

View File

@@ -1,25 +1,27 @@
#!/usr/bin/env bash #!/usr/bin/env bash
VERSION="1.23.12" VERSION="1.25.3"
mkdir -p $HOME/go mkdir -p $HOME/go
cd $HOME/go cd $HOME/go
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz" wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
tar -xzf "go${VERSION}.linux-amd64.tar.gz" tar -xzf "go${VERSION}.linux-amd64.tar.gz"
mv go go_legacy mv go go_win7
cd go_legacy cd go_win7
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.23.x # this patch file only works on golang1.25.x
# that means after golang1.24 release it must be changed # that means after golang1.26 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/ # see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
# revert: # revert:
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" # 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7" # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1 alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1 curl https://github.com/MetaCubeX/go/commit/8cb5472d94c34b88733a81091bd328e70ee565a4.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1 curl https://github.com/MetaCubeX/go/commit/6788c4c6f9fafb56729bad6b660f7ee2272d699f.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/a5b2168bb836ed9d6601c626f95e56c07923f906.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/f56f1e23507e646c85243a71bde7b9629b2f970c.diff | patch --verbose -p 1

View File

@@ -46,7 +46,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.0 go-version: ^1.25.3
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -88,9 +88,9 @@ jobs:
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" } - { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
- { os: windows, arch: amd64 } - { os: windows, arch: amd64 }
- { os: windows, arch: amd64, legacy_go123: true, legacy_name: "windows-7" } - { os: windows, arch: amd64, legacy_win7: true, legacy_name: "windows-7" }
- { os: windows, arch: "386" } - { os: windows, arch: "386" }
- { os: windows, arch: "386", legacy_go123: true, legacy_name: "windows-7" } - { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" }
- { os: windows, arch: arm64 } - { os: windows, arch: arm64 }
- { os: darwin, arch: amd64 } - { os: darwin, arch: amd64 }
@@ -110,29 +110,29 @@ jobs:
if: ${{ ! (matrix.legacy_go123 || matrix.legacy_go124) }} if: ${{ ! (matrix.legacy_go123 || matrix.legacy_go124) }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.0 go-version: ^1.25.3
- name: Setup Go 1.24 - name: Setup Go 1.24
if: matrix.legacy_go124 if: matrix.legacy_go124
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.24.6 go-version: ~1.24.6
- name: Cache Go 1.23 - name: Cache Go for Windows 7
if: matrix.legacy_go123 if: matrix.legacy_win7
id: cache-legacy-go id: cache-go-for-windows7
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: | path: |
~/go/go_legacy ~/go/go_win7
key: go_legacy_12312 key: go_win7_1253
- name: Setup Go 1.23 - name: Setup Go for Windows 7
if: matrix.legacy_go123 && steps.cache-legacy-go.outputs.cache-hit != 'true' if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
run: |- run: |-
.github/setup_legacy_go.sh .github/setup_go_for_windows7.sh
- name: Setup Go 1.23 - name: Setup Go for Windows 7
if: matrix.legacy_go123 if: matrix.legacy_win7
run: |- run: |-
echo "PATH=$HOME/go/go_legacy/bin:$PATH" >> $GITHUB_ENV echo "PATH=$HOME/go/go_win7/bin:$PATH" >> $GITHUB_ENV
echo "GOROOT=$HOME/go/go_legacy" >> $GITHUB_ENV echo "GOROOT=$HOME/go/go_win7" >> $GITHUB_ENV
- name: Setup Android NDK - name: Setup Android NDK
if: matrix.os == 'android' if: matrix.os == 'android'
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -300,7 +300,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.0 go-version: ^1.25.3
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -380,7 +380,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.0 go-version: ^1.25.3
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -432,7 +432,8 @@ jobs:
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }} SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
build_apple: build_apple:
name: Build Apple clients name: Build Apple clients
runs-on: macos-15 runs-on: macos-26
if: false
needs: needs:
- calculate_version - calculate_version
strategy: strategy:
@@ -478,15 +479,7 @@ jobs:
if: matrix.if if: matrix.if
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.0 go-version: ^1.25.3
- name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Setup Xcode beta
if: matrix.if && github.ref == 'refs/heads/dev-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Set tag - name: Set tag
if: matrix.if if: matrix.if
run: |- run: |-

View File

@@ -32,7 +32,7 @@ jobs:
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v8 uses: golangci/golangci-lint-action@v8
with: with:
version: latest version: v2.4.0
args: --timeout=30m args: --timeout=30m
install-mode: binary install-mode: binary
verify: false verify: false

View File

@@ -30,7 +30,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.0 go-version: ^1.25.3
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -71,7 +71,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.0 go-version: ^1.25.3
- name: Setup Android NDK - name: Setup Android NDK
if: matrix.os == 'android' if: matrix.os == 'android'
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1

4
.gitignore vendored
View File

@@ -15,4 +15,6 @@
.DS_Store .DS_Store
/config.d/ /config.d/
/venv/ /venv/
CLAUDE.md
AGENTS.md
/.claude/

View File

@@ -17,6 +17,10 @@ build:
export GOTOOLCHAIN=local && \ export GOTOOLCHAIN=local && \
go build $(MAIN_PARAMS) $(MAIN) go build $(MAIN_PARAMS) $(MAIN)
race:
export GOTOOLCHAIN=local && \
go build -race $(MAIN_PARAMS) $(MAIN)
ci_build: ci_build:
export GOTOOLCHAIN=local && \ export GOTOOLCHAIN=local && \
go build $(PARAMS) $(MAIN) && \ go build $(PARAMS) $(MAIN) && \
@@ -34,7 +38,7 @@ fmt:
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" . @gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
fmt_install: fmt_install:
go install -v mvdan.cc/gofumpt@latest go install -v mvdan.cc/gofumpt@v0.8.0
go install -v github.com/daixiang0/gci@latest go install -v github.com/daixiang0/gci@latest
lint: lint:
@@ -45,7 +49,7 @@ lint:
GOOS=freebsd golangci-lint run ./... GOOS=freebsd golangci-lint run ./...
lint_install: lint_install:
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0
proto: proto:
@go run ./cmd/internal/protogen @go run ./cmd/internal/protogen

View File

@@ -57,6 +57,7 @@ type InboundContext struct {
Domain string Domain string
Client string Client string
SniffContext any SniffContext any
SnifferNames []string
SniffError error SniffError error
// cache // cache

View File

@@ -56,6 +56,14 @@ func (m *Manager) Start(stage adapter.StartStage) error {
m.started = true m.started = true
m.stage = stage m.stage = stage
if stage == adapter.StartStateStart { if stage == adapter.StartStateStart {
if m.defaultTag != "" && m.defaultOutbound == nil {
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
if !loaded {
m.access.Unlock()
return E.New("default outbound not found: ", m.defaultTag)
}
m.defaultOutbound = defaultEndpoint
}
if m.defaultOutbound == nil { if m.defaultOutbound == nil {
directOutbound, err := m.defaultOutboundFallback() directOutbound, err := m.defaultOutboundFallback()
if err != nil { if err != nil {
@@ -66,14 +74,6 @@ func (m *Manager) Start(stage adapter.StartStage) error {
m.outboundByTag[directOutbound.Tag()] = directOutbound m.outboundByTag[directOutbound.Tag()] = directOutbound
m.defaultOutbound = directOutbound m.defaultOutbound = directOutbound
} }
if m.defaultTag != "" && m.defaultOutbound == nil {
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
if !loaded {
m.access.Unlock()
return E.New("default outbound not found: ", m.defaultTag)
}
m.defaultOutbound = defaultEndpoint
}
outbounds := m.outbounds outbounds := m.outbounds
m.access.Unlock() m.access.Unlock()
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...)) return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))

View File

@@ -78,8 +78,8 @@ func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
// Deprecated: removed // Deprecated: removed
func UpstreamMetadata(metadata InboundContext) M.Metadata { func UpstreamMetadata(metadata InboundContext) M.Metadata {
return M.Metadata{ return M.Metadata{
Source: metadata.Source, Source: metadata.Source.Unwrap(),
Destination: metadata.Destination, Destination: metadata.Destination.Unwrap(),
} }
} }

View File

@@ -134,6 +134,7 @@ func publishTestflight(ctx context.Context) error {
asc.PlatformTVOS, asc.PlatformTVOS,
} }
} }
waitingForProcess := false
for _, platform := range platforms { for _, platform := range platforms {
log.Info(string(platform), " list builds") log.Info(string(platform), " list builds")
for { for {
@@ -145,12 +146,13 @@ func publishTestflight(ctx context.Context) error {
return err return err
} }
build := builds.Data[0] build := builds.Data[0]
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute { if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) {
log.Info(string(platform), " ", tag, " waiting for process") log.Info(string(platform), " ", tag, " waiting for process")
time.Sleep(15 * time.Second) time.Sleep(15 * time.Second)
continue continue
} }
if *build.Attributes.ProcessingState != "VALID" { if *build.Attributes.ProcessingState != "VALID" {
waitingForProcess = true
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState) log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
time.Sleep(15 * time.Second) time.Sleep(15 * time.Second)
continue continue

View File

@@ -7,6 +7,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"github.com/sagernet/fswatch" "github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -21,6 +22,7 @@ import (
var _ adapter.CertificateStore = (*Store)(nil) var _ adapter.CertificateStore = (*Store)(nil)
type Store struct { type Store struct {
access sync.RWMutex
systemPool *x509.CertPool systemPool *x509.CertPool
currentPool *x509.CertPool currentPool *x509.CertPool
certificate string certificate string
@@ -115,10 +117,14 @@ func (s *Store) Close() error {
} }
func (s *Store) Pool() *x509.CertPool { func (s *Store) Pool() *x509.CertPool {
s.access.RLock()
defer s.access.RUnlock()
return s.currentPool return s.currentPool
} }
func (s *Store) update() error { func (s *Store) update() error {
s.access.Lock()
defer s.access.Unlock()
var currentPool *x509.CertPool var currentPool *x509.CertPool
if s.systemPool == nil { if s.systemPool == nil {
currentPool = x509.NewCertPool() currentPool = x509.NewCertPool()

View File

@@ -15,7 +15,6 @@ import (
"github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
@@ -43,7 +42,7 @@ type DefaultDialer struct {
networkType []C.InterfaceType networkType []C.InterfaceType
fallbackNetworkType []C.InterfaceType fallbackNetworkType []C.InterfaceType
networkFallbackDelay time.Duration networkFallbackDelay time.Duration
networkLastFallback atomic.TypedValue[time.Time] networkLastFallback common.TypedValue[time.Time]
} }
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) { func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
@@ -89,43 +88,41 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
if networkManager != nil { if networkManager != nil {
defaultOptions := networkManager.DefaultOptions() defaultOptions := networkManager.DefaultOptions()
if !disableDefaultBind { if defaultOptions.BindInterface != "" {
if defaultOptions.BindInterface != "" { bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
} else if networkManager.AutoDetectInterface() && !disableDefaultBind {
if platformInterface != nil {
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 {
networkStrategy = defaultOptions.NetworkStrategy
networkType = defaultOptions.NetworkType
fallbackNetworkType = defaultOptions.FallbackNetworkType
}
networkFallbackDelay = time.Duration(options.FallbackDelay)
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
networkFallbackDelay = defaultOptions.FallbackDelay
}
if networkStrategy == nil {
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
defaultNetworkStrategy = true
}
bindFunc := networkManager.ProtectFunc()
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
} else {
bindFunc := networkManager.AutoDetectInterfaceFunc()
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} else if networkManager.AutoDetectInterface() {
if platformInterface != nil {
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 {
networkStrategy = defaultOptions.NetworkStrategy
networkType = defaultOptions.NetworkType
fallbackNetworkType = defaultOptions.FallbackNetworkType
}
networkFallbackDelay = time.Duration(options.FallbackDelay)
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
networkFallbackDelay = defaultOptions.FallbackDelay
}
if networkStrategy == nil {
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
defaultNetworkStrategy = true
}
bindFunc := networkManager.ProtectFunc()
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
} else {
bindFunc := networkManager.AutoDetectInterfaceFunc()
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
}
}
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
} }
} }
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
}
} }
if networkManager != nil { if networkManager != nil {
markFunc := networkManager.AutoRedirectOutputMarkFunc() markFunc := networkManager.AutoRedirectOutputMarkFunc()

View File

@@ -8,10 +8,10 @@ import (
"net" "net"
"os" "os"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"

View File

@@ -3,6 +3,7 @@ package listener
import ( import (
"net" "net"
"net/netip" "net/netip"
"strings"
"syscall" "syscall"
"time" "time"
@@ -56,7 +57,7 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
if l.tproxy { if l.tproxy {
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error { listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
return control.Raw(conn, func(fd uintptr) error { return control.Raw(conn, func(fd uintptr) error {
return redir.TProxy(fd, !M.ParseSocksaddr(address).IsIPv4(), false) return redir.TProxy(fd, !strings.HasSuffix(network, "4"), false)
}) })
}) })
} }

View File

@@ -5,6 +5,7 @@ import (
"net" "net"
"net/netip" "net/netip"
"os" "os"
"strings"
"syscall" "syscall"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -41,7 +42,7 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
if l.tproxy { if l.tproxy {
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error { listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
return control.Raw(conn, func(fd uintptr) error { return control.Raw(conn, func(fd uintptr) error {
return redir.TProxy(fd, !M.ParseSocksaddr(address).IsIPv4(), true) return redir.TProxy(fd, !strings.HasSuffix(network, "4"), true)
}) })
}) })
} }

View File

@@ -56,7 +56,7 @@ func TestSniffUQUICChrome115(t *testing.T) {
err = sniff.QUICClientHello(context.Background(), &metadata, pkt) err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC) require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientQUICGo) require.Equal(t, metadata.Client, C.ClientChromium)
require.Equal(t, metadata.Domain, "www.google.com") require.Equal(t, metadata.Domain, "www.google.com")
} }

View File

@@ -53,26 +53,48 @@ func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, e
return tlsConn, nil return tlsConn, nil
} }
type Dialer struct { type Dialer interface {
N.Dialer
DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error)
}
type defaultDialer struct {
dialer N.Dialer dialer N.Dialer
config Config config Config
} }
func NewDialer(dialer N.Dialer, config Config) N.Dialer { func NewDialer(dialer N.Dialer, config Config) Dialer {
return &Dialer{dialer, config} return &defaultDialer{dialer, config}
} }
func (d *Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { func (d *defaultDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if network != N.NetworkTCP { if N.NetworkName(network) != N.NetworkTCP {
return nil, os.ErrInvalid return nil, os.ErrInvalid
} }
conn, err := d.dialer.DialContext(ctx, network, destination) return d.DialTLSContext(ctx, destination)
}
func (d *defaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return nil, os.ErrInvalid
}
func (d *defaultDialer) DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error) {
return d.dialContext(ctx, destination)
}
func (d *defaultDialer) dialContext(ctx context.Context, destination M.Socksaddr) (Conn, error) {
conn, err := d.dialer.DialContext(ctx, N.NetworkTCP, destination)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return ClientHandshake(ctx, conn, d.config) tlsConn, err := aTLS.ClientHandshake(ctx, conn, d.config)
if err != nil {
conn.Close()
return nil, err
}
return tlsConn, nil
} }
func (d *Dialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { func (d *defaultDialer) Upstream() any {
return nil, os.ErrInvalid return d.dialer
} }

View File

@@ -69,11 +69,7 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
} else { } else {
return E.New("missing ECH keys") return E.New("missing ECH keys")
} }
block, rest := pem.Decode(echKey) echKeys, err := parseECHKeys(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 { if err != nil {
return E.Cause(err, "parse ECH keys") return E.Cause(err, "parse ECH keys")
} }
@@ -85,21 +81,29 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
return nil return nil
} }
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error { func (c *STDServerConfig) setECHServerConfig(echKey []byte) error {
echKey, err := os.ReadFile(echKeyPath) echKeys, err := parseECHKeys(echKey)
if err != nil { if err != nil {
return E.Cause(err, "reload ECH keys from ", echKeyPath) return err
} }
c.access.Lock()
config := c.config.Clone()
config.EncryptedClientHelloKeys = echKeys
c.config = config
c.access.Unlock()
return nil
}
func parseECHKeys(echKey []byte) ([]tls.EncryptedClientHelloKey, error) {
block, _ := pem.Decode(echKey) block, _ := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" { if block == nil || block.Type != "ECH KEYS" {
return E.New("invalid ECH keys pem") return nil, E.New("invalid ECH keys pem")
} }
echKeys, err := UnmarshalECHKeys(block.Bytes) echKeys, err := UnmarshalECHKeys(block.Bytes)
if err != nil { if err != nil {
return E.Cause(err, "parse ECH keys") return nil, E.Cause(err, "parse ECH keys")
} }
tlsConfig.EncryptedClientHelloKeys = echKeys return echKeys, nil
return nil
} }
type ECHClientConfig struct { type ECHClientConfig struct {

View File

@@ -18,6 +18,6 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
return E.New("ECH requires go1.24, please recompile your binary.") return E.New("ECH requires go1.24, please recompile your binary.")
} }
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error { func (c *STDServerConfig) setECHServerConfig(echKey []byte) error {
return E.New("ECH requires go1.24, please recompile your binary.") panic("unreachable")
} }

View File

@@ -307,3 +307,11 @@ func (c *realityClientConnWrapper) Upstream() any {
func (c *realityClientConnWrapper) CloseWrite() error { func (c *realityClientConnWrapper) CloseWrite() error {
return c.Close() return c.Close()
} }
func (c *realityClientConnWrapper) ReaderReplaceable() bool {
return true
}
func (c *realityClientConnWrapper) WriterReplaceable() bool {
return true
}

View File

@@ -6,6 +6,7 @@ import (
"net" "net"
"os" "os"
"strings" "strings"
"sync"
"time" "time"
"github.com/sagernet/fswatch" "github.com/sagernet/fswatch"
@@ -20,6 +21,7 @@ import (
var errInsecureUnused = E.New("tls: insecure unused") var errInsecureUnused = E.New("tls: insecure unused")
type STDServerConfig struct { type STDServerConfig struct {
access sync.RWMutex
config *tls.Config config *tls.Config
logger log.Logger logger log.Logger
acmeService adapter.SimpleLifecycle acmeService adapter.SimpleLifecycle
@@ -32,14 +34,22 @@ type STDServerConfig struct {
} }
func (c *STDServerConfig) ServerName() string { func (c *STDServerConfig) ServerName() string {
c.access.RLock()
defer c.access.RUnlock()
return c.config.ServerName return c.config.ServerName
} }
func (c *STDServerConfig) SetServerName(serverName string) { func (c *STDServerConfig) SetServerName(serverName string) {
c.config.ServerName = serverName c.access.Lock()
defer c.access.Unlock()
config := c.config.Clone()
config.ServerName = serverName
c.config = config
} }
func (c *STDServerConfig) NextProtos() []string { func (c *STDServerConfig) NextProtos() []string {
c.access.RLock()
defer c.access.RUnlock()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol { if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
return c.config.NextProtos[1:] return c.config.NextProtos[1:]
} else { } else {
@@ -48,11 +58,15 @@ func (c *STDServerConfig) NextProtos() []string {
} }
func (c *STDServerConfig) SetNextProtos(nextProto []string) { func (c *STDServerConfig) SetNextProtos(nextProto []string) {
c.access.Lock()
defer c.access.Unlock()
config := c.config.Clone()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol { if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
c.config.NextProtos = append(c.config.NextProtos[:1], nextProto...) config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
} else { } else {
c.config.NextProtos = nextProto config.NextProtos = nextProto
} }
c.config = config
} }
func (c *STDServerConfig) Config() (*STDConfig, error) { func (c *STDServerConfig) Config() (*STDConfig, error) {
@@ -77,9 +91,6 @@ func (c *STDServerConfig) Start() error {
if c.acmeService != nil { if c.acmeService != nil {
return c.acmeService.Start() return c.acmeService.Start()
} else { } else {
if c.certificatePath == "" && c.keyPath == "" {
return nil
}
err := c.startWatcher() err := c.startWatcher()
if err != nil { if err != nil {
c.logger.Warn("create fsnotify watcher: ", err) c.logger.Warn("create fsnotify watcher: ", err)
@@ -99,6 +110,9 @@ func (c *STDServerConfig) startWatcher() error {
if c.echKeyPath != "" { if c.echKeyPath != "" {
watchPath = append(watchPath, c.echKeyPath) watchPath = append(watchPath, c.echKeyPath)
} }
if len(watchPath) == 0 {
return nil
}
watcher, err := fswatch.NewWatcher(fswatch.Options{ watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath, Path: watchPath,
Callback: func(path string) { Callback: func(path string) {
@@ -138,10 +152,18 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
if err != nil { if err != nil {
return E.Cause(err, "reload key pair") return E.Cause(err, "reload key pair")
} }
c.config.Certificates = []tls.Certificate{keyPair} c.access.Lock()
config := c.config.Clone()
config.Certificates = []tls.Certificate{keyPair}
c.config = config
c.access.Unlock()
c.logger.Info("reloaded TLS certificate") c.logger.Info("reloaded TLS certificate")
} else if path == c.echKeyPath { } else if path == c.echKeyPath {
err := reloadECHKeys(c.echKeyPath, c.config) echKey, err := os.ReadFile(c.echKeyPath)
if err != nil {
return E.Cause(err, "reload ECH keys from ", c.echKeyPath)
}
err = c.setECHServerConfig(echKey)
if err != nil { if err != nil {
return err return err
} }
@@ -262,7 +284,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
return nil, err return nil, err
} }
} }
return &STDServerConfig{ serverConfig := &STDServerConfig{
config: tlsConfig, config: tlsConfig,
logger: logger, logger: logger,
acmeService: acmeService, acmeService: acmeService,
@@ -271,5 +293,11 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
certificatePath: options.CertificatePath, certificatePath: options.CertificatePath,
keyPath: options.KeyPath, keyPath: options.KeyPath,
echKeyPath: echKeyPath, echKeyPath: echKeyPath,
}, nil }
serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
serverConfig.access.Lock()
defer serverConfig.access.Unlock()
return serverConfig.config, nil
}
return serverConfig, nil
} }

View File

@@ -47,15 +47,15 @@ func (s *HistoryStorage) LoadURLTestHistory(tag string) *adapter.URLTestHistory
func (s *HistoryStorage) DeleteURLTestHistory(tag string) { func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
s.access.Lock() s.access.Lock()
delete(s.delayHistory, tag) delete(s.delayHistory, tag)
s.access.Unlock()
s.notifyUpdated() s.notifyUpdated()
s.access.Unlock()
} }
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) { func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) {
s.access.Lock() s.access.Lock()
s.delayHistory[tag] = history s.delayHistory[tag] = history
s.access.Unlock()
s.notifyUpdated() s.notifyUpdated()
s.access.Unlock()
} }
func (s *HistoryStorage) notifyUpdated() { func (s *HistoryStorage) notifyUpdated() {
@@ -69,6 +69,8 @@ func (s *HistoryStorage) notifyUpdated() {
} }
func (s *HistoryStorage) Close() error { func (s *HistoryStorage) Close() error {
s.access.Lock()
defer s.access.Unlock()
s.updateHook = nil s.updateHook = nil
return nil return nil
} }

View File

@@ -2,12 +2,14 @@ package dns
import ( import (
"context" "context"
"errors"
"net" "net"
"net/netip" "net/netip"
"strings" "strings"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/compatible"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -17,7 +19,7 @@ import (
"github.com/sagernet/sing/contrab/freelru" "github.com/sagernet/sing/contrab/freelru"
"github.com/sagernet/sing/contrab/maphash" "github.com/sagernet/sing/contrab/maphash"
dns "github.com/miekg/dns" "github.com/miekg/dns"
) )
var ( var (
@@ -30,16 +32,18 @@ var (
var _ adapter.DNSClient = (*Client)(nil) var _ adapter.DNSClient = (*Client)(nil)
type Client struct { type Client struct {
timeout time.Duration timeout time.Duration
disableCache bool disableCache bool
disableExpire bool disableExpire bool
independentCache bool independentCache bool
clientSubnet netip.Prefix clientSubnet netip.Prefix
rdrc adapter.RDRCStore rdrc adapter.RDRCStore
initRDRCFunc func() adapter.RDRCStore initRDRCFunc func() adapter.RDRCStore
logger logger.ContextLogger logger logger.ContextLogger
cache freelru.Cache[dns.Question, *dns.Msg] cache freelru.Cache[dns.Question, *dns.Msg]
transportCache freelru.Cache[transportCacheKey, *dns.Msg] cacheLock compatible.Map[dns.Question, chan struct{}]
transportCache freelru.Cache[transportCacheKey, *dns.Msg]
transportCacheLock compatible.Map[dns.Question, chan struct{}]
} }
type ClientOptions struct { type ClientOptions struct {
@@ -91,22 +95,34 @@ func (c *Client) Start() {
} }
} }
func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
for _, record := range response.Ns {
if soa, isSOA := record.(*dns.SOA); isSOA {
soaTTL := soa.Header().Ttl
soaMinimum := soa.Minttl
if soaTTL < soaMinimum {
return soaTTL, true
}
return soaMinimum, true
}
}
return 0, false
}
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) { func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
if len(message.Question) == 0 { if len(message.Question) == 0 {
if c.logger != nil { if c.logger != nil {
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question)) c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
} }
responseMessage := dns.Msg{ return FixedResponseStatus(message, dns.RcodeFormatError), nil
MsgHdr: dns.MsgHdr{
Id: message.Id,
Response: true,
Rcode: dns.RcodeFormatError,
},
Question: message.Question,
}
return &responseMessage, nil
} }
question := message.Question[0] question := message.Question[0]
if question.Qtype == dns.TypeA && options.Strategy == C.DomainStrategyIPv6Only || question.Qtype == dns.TypeAAAA && options.Strategy == C.DomainStrategyIPv4Only {
if c.logger != nil {
c.logger.DebugContext(ctx, "strategy rejected")
}
return FixedResponseStatus(message, dns.RcodeSuccess), nil
}
clientSubnet := options.ClientSubnet clientSubnet := options.ClientSubnet
if !clientSubnet.IsValid() { if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet clientSubnet = c.clientSubnet
@@ -114,12 +130,38 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
if clientSubnet.IsValid() { if clientSubnet.IsValid() {
message = SetClientSubnet(message, clientSubnet) message = SetClientSubnet(message, clientSubnet)
} }
isSimpleRequest := len(message.Question) == 1 && isSimpleRequest := len(message.Question) == 1 &&
len(message.Ns) == 0 && len(message.Ns) == 0 &&
len(message.Extra) == 0 && (len(message.Extra) == 0 || len(message.Extra) == 1 &&
message.Extra[0].Header().Rrtype == dns.TypeOPT &&
message.Extra[0].Header().Class > 0 &&
message.Extra[0].Header().Ttl == 0 &&
len(message.Extra[0].(*dns.OPT).Option) == 0) &&
!options.ClientSubnet.IsValid() !options.ClientSubnet.IsValid()
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
if !disableCache { if !disableCache {
if c.cache != nil {
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
if loaded {
<-cond
} else {
defer func() {
c.cacheLock.Delete(question)
close(cond)
}()
}
} else if c.transportCache != nil {
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
if loaded {
<-cond
} else {
defer func() {
c.transportCacheLock.Delete(question)
close(cond)
}()
}
}
response, ttl := c.loadResponse(question, transport) response, ttl := c.loadResponse(question, transport)
if response != nil { if response != nil {
logCachedResponse(c.logger, ctx, response, ttl) logCachedResponse(c.logger, ctx, response, ttl)
@@ -127,27 +169,14 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return response, nil return response, nil
} }
} }
if question.Qtype == dns.TypeA && options.Strategy == C.DomainStrategyIPv6Only || question.Qtype == dns.TypeAAAA && options.Strategy == C.DomainStrategyIPv4Only {
responseMessage := dns.Msg{
MsgHdr: dns.MsgHdr{
Id: message.Id,
Response: true,
Rcode: dns.RcodeSuccess,
},
Question: []dns.Question{question},
}
if c.logger != nil {
c.logger.DebugContext(ctx, "strategy rejected")
}
return &responseMessage, nil
}
messageId := message.Id messageId := message.Id
contextTransport, clientSubnetLoaded := transportTagFromContext(ctx) contextTransport, clientSubnetLoaded := transportTagFromContext(ctx)
if clientSubnetLoaded && transport.Tag() == contextTransport { if clientSubnetLoaded && transport.Tag() == contextTransport {
return nil, E.New("DNS query loopback in transport[", contextTransport, "]") return nil, E.New("DNS query loopback in transport[", contextTransport, "]")
} }
ctx = contextWithTransportTag(ctx, transport.Tag()) ctx = contextWithTransportTag(ctx, transport.Tag())
if responseChecker != nil && c.rdrc != nil { if !disableCache && responseChecker != nil && c.rdrc != nil {
rejected := c.rdrc.LoadRDRC(transport.Tag(), question.Name, question.Qtype) rejected := c.rdrc.LoadRDRC(transport.Tag(), question.Name, question.Qtype)
if rejected { if rejected {
return nil, ErrResponseRejectedCached return nil, ErrResponseRejectedCached
@@ -157,7 +186,12 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
response, err := transport.Exchange(ctx, message) response, err := transport.Exchange(ctx, message)
cancel() cancel()
if err != nil { if err != nil {
return nil, err var rcodeError RcodeError
if errors.As(err, &rcodeError) {
response = FixedResponseStatus(message, int(rcodeError))
} else {
return nil, err
}
} }
/*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA { /*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
validResponse := response validResponse := response
@@ -194,15 +228,17 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
response.Answer = append(response.Answer, validResponse.Answer...) response.Answer = append(response.Answer, validResponse.Answer...)
} }
}*/ }*/
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
if responseChecker != nil { if responseChecker != nil {
var rejected bool var rejected bool
if !(response.Rcode == dns.RcodeSuccess || response.Rcode == dns.RcodeNameError) { // TODO: add accept_any rule and support to check response instead of addresses
if response.Rcode != dns.RcodeSuccess || len(response.Answer) == 0 {
rejected = true rejected = true
} else { } else {
rejected = !responseChecker(MessageToAddresses(response)) rejected = !responseChecker(MessageToAddresses(response))
} }
if rejected { if rejected {
if c.rdrc != nil { if !disableCache && c.rdrc != nil {
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger) c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
} }
logRejectedResponse(c.logger, ctx, response) logRejectedResponse(c.logger, ctx, response)
@@ -229,10 +265,17 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
} }
} }
var timeToLive uint32 var timeToLive uint32
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { if len(response.Answer) == 0 {
for _, record := range recordList { if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive { timeToLive = soaTTL
timeToLive = record.Header().Ttl }
}
if timeToLive == 0 {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
timeToLive = record.Header().Ttl
}
} }
} }
} }
@@ -259,7 +302,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
} }
} }
logExchangedResponse(c.logger, ctx, response, timeToLive) logExchangedResponse(c.logger, ctx, response, timeToLive)
return response, err return response, nil
} }
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) { func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
@@ -305,8 +348,7 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
func (c *Client) ClearCache() { func (c *Client) ClearCache() {
if c.cache != nil { if c.cache != nil {
c.cache.Purge() c.cache.Purge()
} } else if c.transportCache != nil {
if c.transportCache != nil {
c.transportCache.Purge() c.transportCache.Purge()
} }
} }
@@ -320,37 +362,41 @@ func (c *Client) LookupCache(domain string, strategy C.DomainStrategy) ([]netip.
} }
dnsName := dns.Fqdn(domain) dnsName := dns.Fqdn(domain)
if strategy == C.DomainStrategyIPv4Only { if strategy == C.DomainStrategyIPv4Only {
response, err := c.questionCache(dns.Question{ addresses, err := c.questionCache(dns.Question{
Name: dnsName, Name: dnsName,
Qtype: dns.TypeA, Qtype: dns.TypeA,
Qclass: dns.ClassINET, Qclass: dns.ClassINET,
}, nil) }, nil)
if err != ErrNotCached { if err != ErrNotCached {
return response, true return addresses, true
} }
} else if strategy == C.DomainStrategyIPv6Only { } else if strategy == C.DomainStrategyIPv6Only {
response, err := c.questionCache(dns.Question{ addresses, err := c.questionCache(dns.Question{
Name: dnsName, Name: dnsName,
Qtype: dns.TypeAAAA, Qtype: dns.TypeAAAA,
Qclass: dns.ClassINET, Qclass: dns.ClassINET,
}, nil) }, nil)
if err != ErrNotCached { if err != ErrNotCached {
return response, true return addresses, true
} }
} else { } else {
response4, _ := c.questionCache(dns.Question{ response4, _ := c.loadResponse(dns.Question{
Name: dnsName, Name: dnsName,
Qtype: dns.TypeA, Qtype: dns.TypeA,
Qclass: dns.ClassINET, Qclass: dns.ClassINET,
}, nil) }, nil)
response6, _ := c.questionCache(dns.Question{ if response4 == nil {
return nil, false
}
response6, _ := c.loadResponse(dns.Question{
Name: dnsName, Name: dnsName,
Qtype: dns.TypeAAAA, Qtype: dns.TypeAAAA,
Qclass: dns.ClassINET, Qclass: dns.ClassINET,
}, nil) }, nil)
if len(response4) > 0 || len(response6) > 0 { if response6 == nil {
return sortAddresses(response4, response6, strategy), true return nil, false
} }
return sortAddresses(MessageToAddresses(response4), MessageToAddresses(response6), strategy), true
} }
return nil, false return nil, false
} }
@@ -390,15 +436,15 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
transportTag: transport.Tag(), transportTag: transport.Tag(),
}, message) }, message)
} }
return
}
if !c.independentCache {
c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
} else { } else {
c.transportCache.AddWithLifetime(transportCacheKey{ if !c.independentCache {
Question: question, c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
transportTag: transport.Tag(), } else {
}, message, time.Second*time.Duration(timeToLive)) c.transportCache.AddWithLifetime(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
}, message, time.Second*time.Duration(timeToLive))
}
} }
} }
@@ -517,6 +563,9 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
} }
func MessageToAddresses(response *dns.Msg) []netip.Addr { func MessageToAddresses(response *dns.Msg) []netip.Addr {
if response == nil || response.Rcode != dns.RcodeSuccess {
return nil
}
addresses := make([]netip.Addr, 0, len(response.Answer)) addresses := make([]netip.Addr, 0, len(response.Answer))
for _, rawAnswer := range response.Answer { for _, rawAnswer := range response.Answer {
switch answer := rawAnswer.(type) { switch answer := rawAnswer.(type) {
@@ -561,9 +610,12 @@ func transportTagFromContext(ctx context.Context) (string, bool) {
func FixedResponseStatus(message *dns.Msg, rcode int) *dns.Msg { func FixedResponseStatus(message *dns.Msg, rcode int) *dns.Msg {
return &dns.Msg{ return &dns.Msg{
MsgHdr: dns.MsgHdr{ MsgHdr: dns.MsgHdr{
Id: message.Id, Id: message.Id,
Rcode: rcode, Response: true,
Response: true, Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: rcode,
}, },
Question: message.Question, Question: message.Question,
} }

View File

@@ -15,8 +15,7 @@ func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf
} }
responseLen := response.Len() responseLen := response.Len()
if responseLen > maxLen { if responseLen > maxLen {
copyResponse := *response response = response.Copy()
response = &copyResponse
response.Truncate(maxLen) response.Truncate(maxLen)
} }
buffer := buf.NewSize(headroom*2 + 1 + responseLen) buffer := buf.NewSize(headroom*2 + 1 + responseLen)

View File

@@ -5,6 +5,7 @@ import (
) )
const ( const (
RcodeSuccess RcodeError = mDNS.RcodeSuccess
RcodeFormatError RcodeError = mDNS.RcodeFormatError RcodeFormatError RcodeError = mDNS.RcodeFormatError
RcodeNameError RcodeError = mDNS.RcodeNameError RcodeNameError RcodeError = mDNS.RcodeNameError
RcodeRefused RcodeError = mDNS.RcodeRefused RcodeRefused RcodeError = mDNS.RcodeRefused

View File

@@ -386,12 +386,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
if rule != nil { if rule != nil {
switch action := rule.Action().(type) { switch action := rule.Action().(type) {
case *R.RuleActionReject: case *R.RuleActionReject:
switch action.Method { return nil, &R.RejectedError{Cause: action.Error(ctx)}
case C.RuleActionRejectMethodDefault:
return nil, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
case *R.RuleActionPredefined: case *R.RuleActionPredefined:
if action.Rcode != mDNS.RcodeSuccess { if action.Rcode != mDNS.RcodeSuccess {
err = RcodeError(action.Rcode) err = RcodeError(action.Rcode)

View File

@@ -2,17 +2,18 @@ package dhcp
import ( import (
"context" "context"
"errors"
"io"
"net" "net"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
@@ -29,6 +30,7 @@ import (
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
mDNS "github.com/miekg/dns" mDNS "github.com/miekg/dns"
"golang.org/x/exp/slices"
) )
func RegisterTransport(registry *dns.TransportRegistry) { func RegisterTransport(registry *dns.TransportRegistry) {
@@ -45,9 +47,12 @@ type Transport struct {
networkManager adapter.NetworkManager networkManager adapter.NetworkManager
interfaceName string interfaceName string
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback] interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
transports []adapter.DNSTransport transportLock sync.RWMutex
updateAccess sync.Mutex
updatedAt time.Time updatedAt time.Time
servers []M.Socksaddr
search []string
ndots int
attempts int
} }
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.DHCPDNSServerOptions) (adapter.DNSTransport, error) { func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.DHCPDNSServerOptions) (adapter.DNSTransport, error) {
@@ -62,27 +67,40 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
logger: logger, logger: logger,
networkManager: service.FromContext[adapter.NetworkManager](ctx), networkManager: service.FromContext[adapter.NetworkManager](ctx),
interfaceName: options.Interface, interfaceName: options.Interface,
ndots: 1,
attempts: 2,
}, nil }, nil
} }
func NewRawTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) *Transport {
return &Transport{
TransportAdapter: transportAdapter,
ctx: ctx,
dialer: dialer,
logger: logger,
networkManager: service.FromContext[adapter.NetworkManager](ctx),
ndots: 1,
attempts: 2,
}
}
func (t *Transport) Start(stage adapter.StartStage) error { func (t *Transport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart { if stage != adapter.StartStateStart {
return nil return nil
} }
err := t.fetchServers()
if err != nil {
return err
}
if t.interfaceName == "" { if t.interfaceName == "" {
t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated) t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated)
} }
go func() {
_, err := t.Fetch()
if err != nil {
t.logger.Error(E.Cause(err, "fetch DNS servers"))
}
}()
return nil return nil
} }
func (t *Transport) Close() error { func (t *Transport) Close() error {
for _, transport := range t.transports {
transport.Close()
}
if t.interfaceCallback != nil { if t.interfaceCallback != nil {
t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback) t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback)
} }
@@ -90,23 +108,44 @@ func (t *Transport) Close() error {
} }
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
err := t.fetchServers() servers, err := t.Fetch()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(servers) == 0 {
if len(t.transports) == 0 {
return nil, E.New("dhcp: empty DNS servers from response") return nil, E.New("dhcp: empty DNS servers from response")
} }
return t.Exchange0(ctx, message, servers)
}
var response *mDNS.Msg func (t *Transport) Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, error) {
for _, transport := range t.transports { question := message.Question[0]
response, err = transport.Exchange(ctx, message) domain := dns.FqdnToDomain(question.Name)
if err == nil { if len(servers) == 1 || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
return response, nil return t.exchangeSingleRequest(ctx, servers, message, domain)
} } else {
return t.exchangeParallel(ctx, servers, message, domain)
} }
return nil, err }
func (t *Transport) Fetch() ([]M.Socksaddr, error) {
t.transportLock.RLock()
updatedAt := t.updatedAt
servers := t.servers
t.transportLock.RUnlock()
if time.Since(updatedAt) < C.DHCPTTL {
return servers, nil
}
t.transportLock.Lock()
defer t.transportLock.Unlock()
if time.Since(t.updatedAt) < C.DHCPTTL {
return t.servers, nil
}
err := t.updateServers()
if err != nil {
return nil, err
}
return t.servers, nil
} }
func (t *Transport) fetchInterface() (*control.Interface, error) { func (t *Transport) fetchInterface() (*control.Interface, error) {
@@ -124,18 +163,6 @@ func (t *Transport) fetchInterface() (*control.Interface, error) {
} }
} }
func (t *Transport) fetchServers() error {
if time.Since(t.updatedAt) < C.DHCPTTL {
return nil
}
t.updateAccess.Lock()
defer t.updateAccess.Unlock()
if time.Since(t.updatedAt) < C.DHCPTTL {
return nil
}
return t.updateServers()
}
func (t *Transport) updateServers() error { func (t *Transport) updateServers() error {
iface, err := t.fetchInterface() iface, err := t.fetchInterface()
if err != nil { if err != nil {
@@ -148,7 +175,7 @@ func (t *Transport) updateServers() error {
cancel() cancel()
if err != nil { if err != nil {
return err return err
} else if len(t.transports) == 0 { } else if len(t.servers) == 0 {
return E.New("dhcp: empty DNS servers response") return E.New("dhcp: empty DNS servers response")
} else { } else {
t.updatedAt = time.Now() t.updatedAt = time.Now()
@@ -171,13 +198,27 @@ func (t *Transport) fetchServers0(ctx context.Context, iface *control.Interface)
if runtime.GOOS == "linux" || runtime.GOOS == "android" { if runtime.GOOS == "linux" || runtime.GOOS == "android" {
listenAddr = "255.255.255.255:68" listenAddr = "255.255.255.255:68"
} }
packetConn, err := listener.ListenPacket(t.ctx, "udp4", listenAddr) var (
packetConn net.PacketConn
err error
)
for i := 0; i < 5; i++ {
packetConn, err = listener.ListenPacket(t.ctx, "udp4", listenAddr)
if err == nil || !errors.Is(err, syscall.EADDRINUSE) {
break
}
time.Sleep(time.Second)
}
if err != nil { if err != nil {
return err return err
} }
defer packetConn.Close() defer packetConn.Close()
discovery, err := dhcpv4.NewDiscovery(iface.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions(dhcpv4.OptionDomainNameServer)) discovery, err := dhcpv4.NewDiscovery(iface.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions(
dhcpv4.OptionDomainName,
dhcpv4.OptionDomainNameServer,
dhcpv4.OptionDNSDomainSearchList,
))
if err != nil { if err != nil {
return err return err
} }
@@ -204,6 +245,9 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
for { for {
_, _, err := buffer.ReadPacketFrom(packetConn) _, _, err := buffer.ReadPacketFrom(packetConn)
if err != nil { if err != nil {
if errors.Is(err, io.ErrShortBuffer) {
continue
}
return err return err
} }
@@ -223,31 +267,23 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
continue continue
} }
dns := dhcpPacket.DNS() return t.recreateServers(iface, dhcpPacket)
if len(dns) == 0 {
return nil
}
return t.recreateServers(iface, common.Map(dns, func(it net.IP) M.Socksaddr {
return M.SocksaddrFrom(M.AddrFromIP(it), 53)
}))
} }
} }
func (t *Transport) recreateServers(iface *control.Interface, serverAddrs []M.Socksaddr) error { func (t *Transport) recreateServers(iface *control.Interface, dhcpPacket *dhcpv4.DHCPv4) error {
if len(serverAddrs) > 0 { searchList := dhcpPacket.DomainSearch()
t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, M.Socksaddr.String), ","), "]") if searchList != nil && len(searchList.Labels) > 0 {
t.search = searchList.Labels
} else if dhcpPacket.DomainName() != "" {
t.search = []string{dhcpPacket.DomainName()}
} }
serverDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{ serverAddrs := common.Map(dhcpPacket.DNS(), func(it net.IP) M.Socksaddr {
BindInterface: iface.Name, return M.SocksaddrFrom(M.AddrFromIP(it), 53)
UDPFragmentDefault: true, })
})) if len(serverAddrs) > 0 && !slices.Equal(t.servers, serverAddrs) {
var transports []adapter.DNSTransport t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, M.Socksaddr.String), ","), "], search: [", strings.Join(t.search, ","), "]")
for _, serverAddr := range serverAddrs {
transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, serverAddr))
} }
for _, transport := range t.transports { t.servers = serverAddrs
transport.Close()
}
t.transports = transports
return nil return nil
} }

View File

@@ -0,0 +1,213 @@
package dhcp
import (
"context"
"errors"
"math/rand"
"strings"
"syscall"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
"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"
mDNS "github.com/miekg/dns"
)
func (t *Transport) exchangeSingleRequest(ctx context.Context, servers []M.Socksaddr, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
var lastErr error
for _, fqdn := range t.nameList(domain) {
response, err := t.tryOneName(ctx, servers, fqdn, message)
if err != nil {
lastErr = err
continue
}
return response, nil
}
return nil, lastErr
}
func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
returned := make(chan struct{})
defer close(returned)
type queryResult struct {
response *mDNS.Msg
err error
}
results := make(chan queryResult)
startRacer := func(ctx context.Context, fqdn string) {
response, err := t.tryOneName(ctx, servers, fqdn, message)
if err == nil {
if response.Rcode != mDNS.RcodeSuccess {
err = dns.RcodeError(response.Rcode)
} else if len(dns.MessageToAddresses(response)) == 0 {
err = dns.RcodeSuccess
}
}
select {
case results <- queryResult{response, err}:
case <-returned:
}
}
queryCtx, queryCancel := context.WithCancel(ctx)
defer queryCancel()
var nameCount int
for _, fqdn := range t.nameList(domain) {
nameCount++
go startRacer(queryCtx, fqdn)
}
var errors []error
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case result := <-results:
if result.err == nil {
return result.response, nil
}
errors = append(errors, result.err)
if len(errors) == nameCount {
return nil, E.Errors(errors...)
}
}
}
}
func (t *Transport) tryOneName(ctx context.Context, servers []M.Socksaddr, fqdn string, message *mDNS.Msg) (*mDNS.Msg, error) {
sLen := len(servers)
var lastErr error
for i := 0; i < t.attempts; i++ {
for j := 0; j < sLen; j++ {
server := servers[j]
question := message.Question[0]
question.Name = fqdn
response, err := t.exchangeOne(ctx, server, question)
if err != nil {
lastErr = err
continue
}
return response, nil
}
}
return nil, E.Cause(lastErr, fqdn)
}
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question) (*mDNS.Msg, error) {
if server.Port == 0 {
server.Port = 53
}
request := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: uint16(rand.Uint32()),
RecursionDesired: true,
AuthenticatedData: true,
},
Question: []mDNS.Question{question},
Compress: true,
}
request.SetEdns0(buf.UDPBufferSize, false)
return t.exchangeUDP(ctx, server, request)
}
func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server)
if err != nil {
return nil, err
}
defer conn.Close()
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
conn.SetDeadline(deadline)
}
buffer := buf.Get(buf.UDPBufferSize)
defer buf.Put(buffer)
rawMessage, err := request.PackBuffer(buffer)
if err != nil {
return nil, E.Cause(err, "pack request")
}
_, err = conn.Write(rawMessage)
if err != nil {
if errors.Is(err, syscall.EMSGSIZE) {
return t.exchangeTCP(ctx, server, request)
}
return nil, E.Cause(err, "write request")
}
n, err := conn.Read(buffer)
if err != nil {
if errors.Is(err, syscall.EMSGSIZE) {
return t.exchangeTCP(ctx, server, request)
}
return nil, E.Cause(err, "read response")
}
var response mDNS.Msg
err = response.Unpack(buffer[:n])
if err != nil {
return nil, E.Cause(err, "unpack response")
}
if response.Truncated {
return t.exchangeTCP(ctx, server, request)
}
return &response, nil
}
func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server)
if err != nil {
return nil, err
}
defer conn.Close()
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
conn.SetDeadline(deadline)
}
err = transport.WriteMessage(conn, 0, request)
if err != nil {
return nil, err
}
return transport.ReadMessage(conn)
}
func (t *Transport) nameList(name string) []string {
l := len(name)
rooted := l > 0 && name[l-1] == '.'
if l > 254 || l == 254 && !rooted {
return nil
}
if rooted {
if avoidDNS(name) {
return nil
}
return []string{name}
}
hasNdots := strings.Count(name, ".") >= t.ndots
name += "."
// l++
names := make([]string, 0, 1+len(t.search))
if hasNdots && !avoidDNS(name) {
names = append(names, name)
}
for _, suffix := range t.search {
fqdn := name + suffix
if !avoidDNS(fqdn) && len(fqdn) <= 254 {
names = append(names, fqdn)
}
}
if !hasNdots && !avoidDNS(name) {
names = append(names, name)
}
return names
}
func avoidDNS(name string) bool {
if name == "" {
return true
}
if name[len(name)-1] == '.' {
name = name[:len(name)-1]
}
return strings.HasSuffix(name, ".onion")
}

View File

@@ -8,7 +8,6 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strconv" "strconv"
"sync" "sync"
"time" "time"
@@ -26,7 +25,6 @@ import (
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls"
sHTTP "github.com/sagernet/sing/protocol/http" sHTTP "github.com/sagernet/sing/protocol/http"
mDNS "github.com/miekg/dns" mDNS "github.com/miekg/dns"
@@ -48,7 +46,7 @@ type HTTPSTransport struct {
destination *url.URL destination *url.URL
headers http.Header headers http.Header
transportAccess sync.Mutex transportAccess sync.Mutex
transport *http.Transport transport *HTTPSTransportWrapper
transportResetAt time.Time transportResetAt time.Time
} }
@@ -63,11 +61,8 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
if err != nil { if err != nil {
return nil, err return nil, err
} }
if common.Error(tlsConfig.Config()) == nil && !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) { if len(tlsConfig.NextProtos()) == 0 {
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), http2.NextProtoTLS)) tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
}
if !common.Contains(tlsConfig.NextProtos(), "http/1.1") {
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), "http/1.1"))
} }
headers := options.Headers.Build() headers := options.Headers.Build()
host := headers.Get("Host") host := headers.Get("Host")
@@ -125,37 +120,13 @@ func NewHTTPSRaw(
serverAddr M.Socksaddr, serverAddr M.Socksaddr,
tlsConfig tls.Config, tlsConfig tls.Config,
) *HTTPSTransport { ) *HTTPSTransport {
var transport *http.Transport
if tlsConfig != nil {
transport = &http.Transport{
ForceAttemptHTTP2: true,
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
tcpConn, hErr := dialer.DialContext(ctx, network, serverAddr)
if hErr != nil {
return nil, hErr
}
tlsConn, hErr := aTLS.ClientHandshake(ctx, tcpConn, tlsConfig)
if hErr != nil {
tcpConn.Close()
return nil, hErr
}
return tlsConn, nil
},
}
} else {
transport = &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, serverAddr)
},
}
}
return &HTTPSTransport{ return &HTTPSTransport{
TransportAdapter: adapter, TransportAdapter: adapter,
logger: logger, logger: logger,
dialer: dialer, dialer: dialer,
destination: destination, destination: destination,
headers: headers, headers: headers,
transport: transport, transport: NewHTTPSTransportWrapper(tls.NewDialer(dialer, tlsConfig), serverAddr),
} }
} }
@@ -178,7 +149,7 @@ func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
startAt := time.Now() startAt := time.Now()
response, err := t.exchange(ctx, message) response, err := t.exchange(ctx, message)
if err != nil { if err != nil {
if errors.Is(err, os.ErrDeadlineExceeded) { if errors.Is(err, context.DeadlineExceeded) {
t.transportAccess.Lock() t.transportAccess.Lock()
defer t.transportAccess.Unlock() defer t.transportAccess.Unlock()
if t.transportResetAt.After(startAt) { if t.transportResetAt.After(startAt) {

View File

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

View File

@@ -2,12 +2,15 @@ package local
import ( import (
"context" "context"
"errors"
"math/rand" "math/rand"
"syscall"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing-box/dns/transport/hosts" "github.com/sagernet/sing-box/dns/transport/hosts"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
@@ -93,7 +96,7 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
if response.Rcode != mDNS.RcodeSuccess { if response.Rcode != mDNS.RcodeSuccess {
err = dns.RcodeError(response.Rcode) err = dns.RcodeError(response.Rcode)
} else if len(dns.MessageToAddresses(response)) == 0 { } else if len(dns.MessageToAddresses(response)) == 0 {
err = E.New(fqdn, ": empty result") err = dns.RcodeSuccess
} }
} }
select { select {
@@ -149,12 +152,6 @@ func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, questio
if server.Port == 0 { if server.Port == 0 {
server.Port = 53 server.Port = 53
} }
var networks []string
if useTCP {
networks = []string{N.NetworkTCP}
} else {
networks = []string{N.NetworkUDP, N.NetworkTCP}
}
request := &mDNS.Msg{ request := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{ MsgHdr: mDNS.MsgHdr{
Id: uint16(rand.Uint32()), Id: uint16(rand.Uint32()),
@@ -164,41 +161,74 @@ func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, questio
Question: []mDNS.Question{question}, Question: []mDNS.Question{question},
Compress: true, Compress: true,
} }
request.SetEdns0(maxDNSPacketSize, false) request.SetEdns0(buf.UDPBufferSize, false)
if !useTCP {
return t.exchangeUDP(ctx, server, request, timeout)
} else {
return t.exchangeTCP(ctx, server, request, timeout)
}
}
func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server)
if err != nil {
return nil, err
}
defer conn.Close()
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
newDeadline := time.Now().Add(timeout)
if deadline.After(newDeadline) {
deadline = newDeadline
}
conn.SetDeadline(deadline)
}
buffer := buf.Get(buf.UDPBufferSize) buffer := buf.Get(buf.UDPBufferSize)
defer buf.Put(buffer) defer buf.Put(buffer)
for _, network := range networks { rawMessage, err := request.PackBuffer(buffer)
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout)) if err != nil {
defer cancel() return nil, E.Cause(err, "pack request")
conn, err := t.dialer.DialContext(ctx, network, server)
if err != nil {
return nil, err
}
defer conn.Close()
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
conn.SetDeadline(deadline)
}
rawMessage, err := request.PackBuffer(buffer)
if err != nil {
return nil, E.Cause(err, "pack request")
}
_, err = conn.Write(rawMessage)
if err != nil {
return nil, E.Cause(err, "write request")
}
n, err := conn.Read(buffer)
if err != nil {
return nil, E.Cause(err, "read response")
}
var response mDNS.Msg
err = response.Unpack(buffer[:n])
if err != nil {
return nil, E.Cause(err, "unpack response")
}
if response.Truncated && network == N.NetworkUDP {
continue
}
return &response, nil
} }
panic("unexpected") _, err = conn.Write(rawMessage)
if err != nil {
if errors.Is(err, syscall.EMSGSIZE) {
return t.exchangeTCP(ctx, server, request, timeout)
}
return nil, E.Cause(err, "write request")
}
n, err := conn.Read(buffer)
if err != nil {
if errors.Is(err, syscall.EMSGSIZE) {
return t.exchangeTCP(ctx, server, request, timeout)
}
return nil, E.Cause(err, "read response")
}
var response mDNS.Msg
err = response.Unpack(buffer[:n])
if err != nil {
return nil, E.Cause(err, "unpack response")
}
if response.Truncated {
return t.exchangeTCP(ctx, server, request, timeout)
}
return &response, nil
}
func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server)
if err != nil {
return nil, err
}
defer conn.Close()
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
newDeadline := time.Now().Add(timeout)
if deadline.After(newDeadline) {
deadline = newDeadline
}
conn.SetDeadline(deadline)
}
err = transport.WriteMessage(conn, 0, request)
if err != nil {
return nil, err
}
return transport.ReadMessage(conn)
} }

View File

@@ -10,11 +10,6 @@ import (
"time" "time"
) )
const (
// net.maxDNSPacketSize
maxDNSPacketSize = 1232
)
type resolverConfig struct { type resolverConfig struct {
initOnce sync.Once initOnce sync.Once
ch chan struct{} ch chan struct{}

View File

@@ -2,6 +2,36 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.12.10
* Update uTLS to v1.8.1 **1**
* Fixes and improvements
**1**:
This update fixes an critical issue that could cause simulated Chrome fingerprints to be detected,
see https://github.com/refraction-networking/utls/pull/375.
#### 1.12.9
* Fixes and improvements
#### 1.12.8
* Fixes and improvements
#### 1.12.5
* Fixes and improvements
#### 1.12.4
* Fixes and improvements
#### 1.12.3
* Fixes and improvements
#### 1.12.2 #### 1.12.2
* Fixes and improvements * Fixes and improvements
@@ -76,7 +106,8 @@ See [Tailscale](/configuration/endpoint/tailscale/).
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile. 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). For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches
from [MetaCubeX/go](https://github.com/MetaCubeX/go).
**7**: **7**:
@@ -138,7 +169,8 @@ See [Tun](/configuration/inbound/tun/#loopback_address).
We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack. We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack.
The following data was tested using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/internal/tun_bench/main.go) on M4 MacBook pro. The following data was tested
using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/internal/tun_bench/main.go) on M4 MacBook pro.
| Version | Stack | MTU | Upload | Download | | Version | Stack | MTU | Upload | Download |
|-------------|--------|-------|--------|----------| |-------------|--------|-------|--------|----------|
@@ -157,8 +189,8 @@ The following data was tested using [tun_bench](https://github.com/SagerNet/sing
**18**: **18**:
We continue to experience issues updating our sing-box apps on the App Store and Play Store. We continue to experience issues updating our sing-box apps on the App Store and Play Store.
Until we rewrite and resubmit the apps, they are considered irrecoverable. Until we rewrite and resubmit the apps, they are considered irrecoverable.
Therefore, after this release, we will not be repeating this notice unless there is new information. Therefore, after this release, we will not be repeating this notice unless there is new information.
### 1.11.15 ### 1.11.15
@@ -439,7 +471,8 @@ See [AnyTLS Inbound](/configuration/inbound/anytls/) and [AnyTLS Outbound](/conf
**2**: **2**:
`resolve` route action now accepts `disable_cache` and other options like in DNS route actions, see [Route Action](/configuration/route/rule_action). `resolve` route action now accepts `disable_cache` and other options like in DNS route actions,
see [Route Action](/configuration/route/rule_action).
**3**: **3**:
@@ -470,7 +503,8 @@ See [Tailscale](/configuration/endpoint/tailscale/).
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile. 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). 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 ### 1.11.3

View File

@@ -3,12 +3,12 @@ package trafficontrol
import ( import (
"runtime" "runtime"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/sagernet/sing-box/common/compatible"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/clashapi/compatible"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/common/x/list"

View File

@@ -2,11 +2,11 @@ package trafficontrol
import ( import (
"net" "net"
"sync/atomic"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"

View File

@@ -12,9 +12,7 @@ type iOSPauseFields struct {
func (s *BoxService) Pause() { func (s *BoxService) Pause() {
s.pauseManager.DevicePause() s.pauseManager.DevicePause()
if !C.IsIos { if C.IsIos {
s.instance.Router().ResetNetwork()
} else {
if s.endPauseTimer == nil { if s.endPauseTimer == nil {
s.endPauseTimer = time.AfterFunc(time.Minute, s.pauseManager.DeviceWake) s.endPauseTimer = time.AfterFunc(time.Minute, s.pauseManager.DeviceWake)
} else { } else {
@@ -26,7 +24,6 @@ func (s *BoxService) Pause() {
func (s *BoxService) Wake() { func (s *BoxService) Wake() {
if !C.IsIos { if !C.IsIos {
s.pauseManager.DeviceWake() s.pauseManager.DeviceWake()
s.instance.Router().ResetNetwork()
} }
} }

View File

@@ -7,11 +7,11 @@ import (
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"

14
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/sagernet/sing-box
go 1.23.1 go 1.23.1
require ( require (
github.com/anytls/sing-anytls v0.0.8 github.com/anytls/sing-anytls v0.0.11
github.com/caddyserver/certmagic v0.23.0 github.com/caddyserver/certmagic v0.23.0
github.com/coder/websocket v1.8.13 github.com/coder/websocket v1.8.13
github.com/cretz/bine v0.2.0 github.com/cretz/bine v0.2.0
@@ -16,7 +16,7 @@ require (
github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6 github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6
github.com/logrusorgru/aurora v2.0.3+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4
github.com/metacubex/utls v1.8.0 github.com/metacubex/utls v1.8.2
github.com/mholt/acmez/v3 v3.1.2 github.com/mholt/acmez/v3 v3.1.2
github.com/miekg/dns v1.1.67 github.com/miekg/dns v1.1.67
github.com/oschwald/maxminddb-golang v1.13.1 github.com/oschwald/maxminddb-golang v1.13.1
@@ -26,17 +26,17 @@ require (
github.com/sagernet/fswatch v0.1.1 github.com/sagernet/fswatch v0.1.1
github.com/sagernet/gomobile v0.1.8 github.com/sagernet/gomobile v0.1.8
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
github.com/sagernet/quic-go v0.52.0-beta.1 github.com/sagernet/quic-go v0.52.0-sing-box-mod.2
github.com/sagernet/sing v0.7.6-0.20250815070458-d33ece7a184f github.com/sagernet/sing v0.7.12
github.com/sagernet/sing-mux v0.3.3 github.com/sagernet/sing-mux v0.3.3
github.com/sagernet/sing-quic v0.5.0 github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb
github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
github.com/sagernet/sing-tun v0.7.0-beta.1 github.com/sagernet/sing-tun v0.7.2
github.com/sagernet/sing-vmess v0.2.7 github.com/sagernet/sing-vmess v0.2.7
github.com/sagernet/smux v1.5.34-mod.2 github.com/sagernet/smux v1.5.34-mod.2
github.com/sagernet/tailscale v1.80.3-mod.5 github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.1
github.com/sagernet/wireguard-go v0.0.1-beta.7 github.com/sagernet/wireguard-go v0.0.1-beta.7
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1

26
go.sum
View File

@@ -8,8 +8,8 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/anytls/sing-anytls v0.0.8 h1:1u/fnH1HoeeMV5mX7/eUOjLBvPdkd1UJRmXiRi6Vymc= github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc=
github.com/anytls/sing-anytls v0.0.8/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8= github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU= github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU=
@@ -126,6 +126,8 @@ github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 h1:j1VRTiC9JLR4nU
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.8.0 h1:mSYi6FMnmc5riARl5UZDmWVy710z+P5b7xuGW0lV9ac= github.com/metacubex/utls v1.8.0 h1:mSYi6FMnmc5riARl5UZDmWVy710z+P5b7xuGW0lV9ac=
github.com/metacubex/utls v1.8.0/go.mod h1:FdjYzVfCtgtna19hX0ER1Xsa5uJInwdQ4IcaaI98lEQ= github.com/metacubex/utls v1.8.0/go.mod h1:FdjYzVfCtgtna19hX0ER1Xsa5uJInwdQ4IcaaI98lEQ=
github.com/metacubex/utls v1.8.2 h1:d7KalMZ5hnOJ6lThMz8Ykd+5dvmXH3Eoeyfv2jUuG3w=
github.com/metacubex/utls v1.8.2/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0= github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
@@ -164,29 +166,29 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.52.0-beta.1 h1:hWkojLg64zjV+MJOvJU/kOeWndm3tiEfBLx5foisszs= github.com/sagernet/quic-go v0.52.0-sing-box-mod.2 h1:QTPr/ptUPsgregVfFXReBFrhv/8U83deZG8urQ7pWYI=
github.com/sagernet/quic-go v0.52.0-beta.1/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4= github.com/sagernet/quic-go v0.52.0-sing-box-mod.2/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.7.6-0.20250815070458-d33ece7a184f h1:HIBo8l+tsS3wLwuI1E56uRTQw46QytXSUpZTP3vwG/U= github.com/sagernet/sing v0.7.12 h1:MpMbO56crPRZTbltoj1wGk4Xj9+GiwH1wTO4s3fz1EA=
github.com/sagernet/sing v0.7.6-0.20250815070458-d33ece7a184f/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.7.12/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.3 h1:YFgt9plMWzH994BMZLmyKL37PdIVaIilwP0Jg+EcLfw= github.com/sagernet/sing-mux v0.3.3 h1:YFgt9plMWzH994BMZLmyKL37PdIVaIilwP0Jg+EcLfw=
github.com/sagernet/sing-mux v0.3.3/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA= github.com/sagernet/sing-mux v0.3.3/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
github.com/sagernet/sing-quic v0.5.0 h1:jNLIyVk24lFPvu8A4x+ZNEnZdI+Tg1rp7eCJ6v0Csak= github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb h1:5Wx3XeTiKrrrcrAky7Hc1bO3CGxrvho2Vu5b/adlEIM=
github.com/sagernet/sing-quic v0.5.0/go.mod h1:SAv/qdeDN+75msGG5U5ZIwG+3Ua50jVIKNrRSY8pkx0= github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.7.0-beta.1 h1:mBIFXYAnGO5ey/HcCYanqnBx61E7yF8zTFGRZonGYmY= github.com/sagernet/sing-tun v0.7.2 h1:uJkAZM0KBqIYzrq077QGqdvj/+4i/pMOx6Pnx0jYqAs=
github.com/sagernet/sing-tun v0.7.0-beta.1/go.mod h1:AHJuRrLbNRJuivuFZ2VhXwDj4ViYp14szG5EkkKAqRQ= github.com/sagernet/sing-tun v0.7.2/go.mod h1:pUEjh9YHQ2gJT6Lk0TYDklh3WJy7lz+848vleGM3JPM=
github.com/sagernet/sing-vmess v0.2.7 h1:2ee+9kO0xW5P4mfe6TYVWf9VtY8k1JhNysBqsiYj0sk= github.com/sagernet/sing-vmess v0.2.7 h1:2ee+9kO0xW5P4mfe6TYVWf9VtY8k1JhNysBqsiYj0sk=
github.com/sagernet/sing-vmess v0.2.7/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs= github.com/sagernet/sing-vmess v0.2.7/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4= github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4=
github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc= github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc=
github.com/sagernet/tailscale v1.80.3-mod.5 h1:7V7z+p2C//TGtff20pPnDCt3qP6uFyY62peJoKF9z/A= github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.1 h1:gMC0q+0VvZBotZMZ9G0R8ZMEIT/Q6KnXbw0/OgMjmdk=
github.com/sagernet/tailscale v1.80.3-mod.5/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI= github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.1/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
github.com/sagernet/wireguard-go v0.0.1-beta.7 h1:ltgBwYHfr+9Wz1eG59NiWnHrYEkDKHG7otNZvu85DXI= github.com/sagernet/wireguard-go v0.0.1-beta.7 h1:ltgBwYHfr+9Wz1eG59NiWnHrYEkDKHG7otNZvu85DXI=
github.com/sagernet/wireguard-go v0.0.1-beta.7/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo= github.com/sagernet/wireguard-go v0.0.1-beta.7/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=

View File

@@ -124,7 +124,7 @@ func (h *inboundHandler) NewConnectionEx(ctx context.Context, conn net.Conn, sou
//nolint:staticcheck //nolint:staticcheck
metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
metadata.Source = source metadata.Source = source
metadata.Destination = destination metadata.Destination = destination.Unwrap()
if userName, _ := auth.UserFromContext[string](ctx); userName != "" { if userName, _ := auth.UserFromContext[string](ctx); userName != "" {
metadata.User = userName metadata.User = userName
h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination) h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination)

View File

@@ -13,6 +13,7 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/uot" "github.com/sagernet/sing/common/uot"
@@ -43,6 +44,13 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
if options.TLS == nil || !options.TLS.Enabled { if options.TLS == nil || !options.TLS.Enabled {
return nil, C.ErrTLSRequired return nil, C.ErrTLSRequired
} }
// TCP Fast Open is incompatible with anytls because TFO creates a lazy connection
// that only establishes on first write. The lazy connection returns an empty address
// before establishment, but anytls SOCKS wrapper tries to access the remote address
// during handshake, causing a null pointer dereference crash.
if options.DialerOptions.TCPFastOpen {
return nil, E.New("tcp_fast_open is not supported with anytls outbound")
}
tlsConfig, err := tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS)) tlsConfig, err := tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
if err != nil { if err != nil {

View File

@@ -13,7 +13,6 @@ import (
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
@@ -164,26 +163,7 @@ func (h *Outbound) DialParallel(ctx context.Context, network string, destination
case N.NetworkUDP: case N.NetworkUDP:
h.logger.InfoContext(ctx, "outbound packet connection to ", destination) h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
} }
var domainStrategy C.DomainStrategy return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, len(destinationAddresses) > 0 && destinationAddresses[0].Is6(), nil, nil, nil, h.fallbackDelay)
if h.domainStrategy != C.DomainStrategyAsIS {
domainStrategy = h.domainStrategy
} else {
//nolint:staticcheck
domainStrategy = C.DomainStrategy(metadata.InboundOptions.DomainStrategy)
}
switch domainStrategy {
case C.DomainStrategyIPv4Only:
destinationAddresses = common.Filter(destinationAddresses, netip.Addr.Is4)
if len(destinationAddresses) == 0 {
return nil, E.New("no IPv4 address available for ", destination)
}
case C.DomainStrategyIPv6Only:
destinationAddresses = common.Filter(destinationAddresses, netip.Addr.Is6)
if len(destinationAddresses) == 0 {
return nil, E.New("no IPv6 address available for ", destination)
}
}
return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == C.DomainStrategyPreferIPv6, nil, nil, nil, h.fallbackDelay)
} }
func (h *Outbound) DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy *C.NetworkStrategy, networkType []C.InterfaceType, fallbackNetworkType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { func (h *Outbound) DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy *C.NetworkStrategy, networkType []C.InterfaceType, fallbackNetworkType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
@@ -204,26 +184,7 @@ func (h *Outbound) DialParallelNetwork(ctx context.Context, network string, dest
case N.NetworkUDP: case N.NetworkUDP:
h.logger.InfoContext(ctx, "outbound packet connection to ", destination) h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
} }
var domainStrategy C.DomainStrategy return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, len(destinationAddresses) > 0 && destinationAddresses[0].Is6(), networkStrategy, networkType, fallbackNetworkType, fallbackDelay)
if h.domainStrategy != C.DomainStrategyAsIS {
domainStrategy = h.domainStrategy
} else {
//nolint:staticcheck
domainStrategy = C.DomainStrategy(metadata.InboundOptions.DomainStrategy)
}
switch domainStrategy {
case C.DomainStrategyIPv4Only:
destinationAddresses = common.Filter(destinationAddresses, netip.Addr.Is4)
if len(destinationAddresses) == 0 {
return nil, E.New("no IPv4 address available for ", destination)
}
case C.DomainStrategyIPv6Only:
destinationAddresses = common.Filter(destinationAddresses, netip.Addr.Is6)
if len(destinationAddresses) == 0 {
return nil, E.New("no IPv6 address available for ", destination)
}
}
return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == C.DomainStrategyPreferIPv6, networkStrategy, networkType, fallbackNetworkType, fallbackDelay)
} }
func (h *Outbound) ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy *C.NetworkStrategy, networkType []C.InterfaceType, fallbackNetworkType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { func (h *Outbound) ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy *C.NetworkStrategy, networkType []C.InterfaceType, fallbackNetworkType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) {

View File

@@ -10,7 +10,7 @@ import (
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
@@ -37,7 +37,7 @@ type Selector struct {
tags []string tags []string
defaultTag string defaultTag string
outbounds map[string]adapter.Outbound outbounds map[string]adapter.Outbound
selected atomic.TypedValue[adapter.Outbound] selected common.TypedValue[adapter.Outbound]
interruptGroup *interrupt.Group interruptGroup *interrupt.Group
interruptExternalConnections bool interruptExternalConnections bool
} }

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"net" "net"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -14,7 +15,6 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/batch" "github.com/sagernet/sing/common/batch"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
@@ -192,7 +192,7 @@ type URLTestGroup struct {
ticker *time.Ticker ticker *time.Ticker
close chan struct{} close chan struct{}
started bool started bool
lastActive atomic.TypedValue[time.Time] lastActive common.TypedValue[time.Time]
} }
func NewURLTestGroup(ctx context.Context, outboundManager adapter.OutboundManager, logger log.Logger, outbounds []adapter.Outbound, link string, interval time.Duration, tolerance uint16, idleTimeout time.Duration, interruptExternalConnections bool) (*URLTestGroup, error) { func NewURLTestGroup(ctx context.Context, outboundManager adapter.OutboundManager, logger log.Logger, outbounds []adapter.Outbound, link string, interval time.Duration, tolerance uint16, idleTimeout time.Duration, interruptExternalConnections bool) (*URLTestGroup, error) {

View File

@@ -8,10 +8,12 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/listener"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/common/uot" "github.com/sagernet/sing-box/common/uot"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/auth"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
@@ -33,6 +35,7 @@ type Inbound struct {
logger log.ContextLogger logger log.ContextLogger
listener *listener.Listener listener *listener.Listener
authenticator *auth.Authenticator authenticator *auth.Authenticator
tlsConfig tls.ServerConfig
} }
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) { func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) {
@@ -42,6 +45,13 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
logger: logger, logger: logger,
authenticator: auth.NewAuthenticator(options.Users), authenticator: auth.NewAuthenticator(options.Users),
} }
if options.TLS != nil {
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
if err != nil {
return nil, err
}
inbound.tlsConfig = tlsConfig
}
inbound.listener = listener.New(listener.Options{ inbound.listener = listener.New(listener.Options{
Context: ctx, Context: ctx,
Logger: logger, Logger: logger,
@@ -58,11 +68,20 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart { if stage != adapter.StartStateStart {
return nil return nil
} }
if h.tlsConfig != nil {
err := h.tlsConfig.Start()
if err != nil {
return E.Cause(err, "create TLS config")
}
}
return h.listener.Start() return h.listener.Start()
} }
func (h *Inbound) Close() error { func (h *Inbound) Close() error {
return h.listener.Close() return common.Close(
h.listener,
h.tlsConfig,
)
} }
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
@@ -78,6 +97,13 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a
} }
func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
if h.tlsConfig != nil {
tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig)
if err != nil {
return E.Cause(err, "TLS handshake")
}
conn = tlsConn
}
reader := std_bufio.NewReader(conn) reader := std_bufio.NewReader(conn)
headerBytes, err := reader.Peek(1) headerBytes, err := reader.Peek(1)
if err != nil { if err != nil {

View File

@@ -170,7 +170,7 @@ func (n *Inbound) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
hostPort = request.Host hostPort = request.Host
} }
source := sHttp.SourceAddress(request) source := sHttp.SourceAddress(request)
destination := M.ParseSocksaddr(hostPort) destination := M.ParseSocksaddr(hostPort).Unwrap()
if hijacker, isHijacker := writer.(http.Hijacker); isHijacker { if hijacker, isHijacker := writer.(http.Hijacker); isHijacker {
conn, _, err := hijacker.Hijack() conn, _, err := hijacker.Hijack()

View File

@@ -19,7 +19,6 @@ import (
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
@@ -37,7 +36,7 @@ var _ adapter.NetworkManager = (*NetworkManager)(nil)
type NetworkManager struct { type NetworkManager struct {
logger logger.ContextLogger logger logger.ContextLogger
interfaceFinder *control.DefaultInterfaceFinder interfaceFinder *control.DefaultInterfaceFinder
networkInterfaces atomic.TypedValue[[]adapter.NetworkInterface] networkInterfaces common.TypedValue[[]adapter.NetworkInterface]
autoDetectInterface bool autoDetectInterface bool
defaultOptions adapter.NetworkOptions defaultOptions adapter.NetworkOptions

View File

@@ -5,7 +5,6 @@ import (
"errors" "errors"
"net" "net"
"net/netip" "net/netip"
"os"
"strings" "strings"
"time" "time"
@@ -27,6 +26,8 @@ import (
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/uot" "github.com/sagernet/sing/common/uot"
"golang.org/x/exp/slices"
) )
// Deprecated: use RouteConnectionEx instead. // Deprecated: use RouteConnectionEx instead.
@@ -345,16 +346,16 @@ func (r *Router) matchRule(
newBuffer, newPackerBuffers, newErr := r.actionSniff(ctx, metadata, &R.RuleActionSniff{ newBuffer, newPackerBuffers, newErr := r.actionSniff(ctx, metadata, &R.RuleActionSniff{
OverrideDestination: metadata.InboundOptions.SniffOverrideDestination, OverrideDestination: metadata.InboundOptions.SniffOverrideDestination,
Timeout: time.Duration(metadata.InboundOptions.SniffTimeout), Timeout: time.Duration(metadata.InboundOptions.SniffTimeout),
}, inputConn, inputPacketConn, nil) }, inputConn, inputPacketConn, nil, nil)
if newErr != nil {
fatalErr = newErr
return
}
if newBuffer != nil { if newBuffer != nil {
buffers = []*buf.Buffer{newBuffer} buffers = []*buf.Buffer{newBuffer}
} else if len(newPackerBuffers) > 0 { } else if len(newPackerBuffers) > 0 {
packetBuffers = newPackerBuffers packetBuffers = newPackerBuffers
} }
if newErr != nil {
fatalErr = newErr
return
}
} }
if C.DomainStrategy(metadata.InboundOptions.DomainStrategy) != C.DomainStrategyAsIS { if C.DomainStrategy(metadata.InboundOptions.DomainStrategy) != C.DomainStrategyAsIS {
fatalErr = r.actionResolve(ctx, metadata, &R.RuleActionResolve{ fatalErr = r.actionResolve(ctx, metadata, &R.RuleActionResolve{
@@ -453,16 +454,16 @@ match:
switch action := currentRule.Action().(type) { switch action := currentRule.Action().(type) {
case *R.RuleActionSniff: case *R.RuleActionSniff:
if !preMatch { if !preMatch {
newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn, buffers) newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn, buffers, packetBuffers)
if newErr != nil {
fatalErr = newErr
return
}
if newBuffer != nil { if newBuffer != nil {
buffers = append(buffers, newBuffer) buffers = append(buffers, newBuffer)
} else if len(newPacketBuffers) > 0 { } else if len(newPacketBuffers) > 0 {
packetBuffers = append(packetBuffers, newPacketBuffers...) packetBuffers = append(packetBuffers, newPacketBuffers...)
} }
if newErr != nil {
fatalErr = newErr
return
}
} else { } else {
selectedRule = currentRule selectedRule = currentRule
selectedRuleIndex = currentRuleIndex selectedRuleIndex = currentRuleIndex
@@ -489,7 +490,7 @@ match:
func (r *Router) actionSniff( func (r *Router) actionSniff(
ctx context.Context, metadata *adapter.InboundContext, action *R.RuleActionSniff, ctx context.Context, metadata *adapter.InboundContext, action *R.RuleActionSniff,
inputConn net.Conn, inputPacketConn N.PacketConn, inputBuffers []*buf.Buffer, inputConn net.Conn, inputPacketConn N.PacketConn, inputBuffers []*buf.Buffer, inputPacketBuffers []*N.PacketBuffer,
) (buffer *buf.Buffer, packetBuffers []*N.PacketBuffer, fatalErr error) { ) (buffer *buf.Buffer, packetBuffers []*N.PacketBuffer, fatalErr error) {
if sniff.Skip(metadata) { if sniff.Skip(metadata) {
r.logger.DebugContext(ctx, "sniff skipped due to port considered as server-first") r.logger.DebugContext(ctx, "sniff skipped due to port considered as server-first")
@@ -501,7 +502,7 @@ func (r *Router) actionSniff(
if inputConn != nil { if inputConn != nil {
if len(action.StreamSniffers) == 0 && len(action.PacketSniffers) > 0 { if len(action.StreamSniffers) == 0 && len(action.PacketSniffers) > 0 {
return return
} else if metadata.SniffError != nil && !errors.Is(metadata.SniffError, sniff.ErrNeedMoreData) { } else if slices.Equal(metadata.SnifferNames, action.SnifferNames) && metadata.SniffError != nil && !errors.Is(metadata.SniffError, sniff.ErrNeedMoreData) {
r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.SniffError) r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.SniffError)
return return
} }
@@ -528,6 +529,7 @@ func (r *Router) actionSniff(
action.Timeout, action.Timeout,
streamSniffers..., streamSniffers...,
) )
metadata.SnifferNames = action.SnifferNames
metadata.SniffError = err metadata.SniffError = err
if err == nil { if err == nil {
//goland:noinspection GoDeprecation //goland:noinspection GoDeprecation
@@ -553,10 +555,13 @@ func (r *Router) actionSniff(
} else if inputPacketConn != nil { } else if inputPacketConn != nil {
if len(action.PacketSniffers) == 0 && len(action.StreamSniffers) > 0 { if len(action.PacketSniffers) == 0 && len(action.StreamSniffers) > 0 {
return return
} else if metadata.SniffError != nil && !errors.Is(metadata.SniffError, sniff.ErrNeedMoreData) { } else if slices.Equal(metadata.SnifferNames, action.SnifferNames) && metadata.SniffError != nil && !errors.Is(metadata.SniffError, sniff.ErrNeedMoreData) {
r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.SniffError) r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.SniffError)
return return
} }
quicMoreData := func() bool {
return slices.Equal(metadata.SnifferNames, action.SnifferNames) && errors.Is(metadata.SniffError, sniff.ErrNeedMoreData)
}
var packetSniffers []sniff.PacketSniffer var packetSniffers []sniff.PacketSniffer
if len(action.PacketSniffers) > 0 { if len(action.PacketSniffers) > 0 {
packetSniffers = action.PacketSniffers packetSniffers = action.PacketSniffers
@@ -571,12 +576,37 @@ func (r *Router) actionSniff(
sniff.NTP, sniff.NTP,
} }
} }
var err error
for _, packetBuffer := range inputPacketBuffers {
if quicMoreData() {
err = sniff.PeekPacket(
ctx,
metadata,
packetBuffer.Buffer.Bytes(),
sniff.QUICClientHello,
)
} else {
err = sniff.PeekPacket(
ctx, metadata,
packetBuffer.Buffer.Bytes(),
packetSniffers...,
)
}
metadata.SnifferNames = action.SnifferNames
metadata.SniffError = err
if errors.Is(err, sniff.ErrNeedMoreData) {
// TODO: replace with generic message when there are more multi-packet protocols
r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello")
continue
}
goto finally
}
packetBuffers = inputPacketBuffers
for { for {
var ( var (
sniffBuffer = buf.NewPacket() sniffBuffer = buf.NewPacket()
destination M.Socksaddr destination M.Socksaddr
done = make(chan struct{}) done = make(chan struct{})
err error
) )
go func() { go func() {
sniffTimeout := C.ReadPayloadTimeout sniffTimeout := C.ReadPayloadTimeout
@@ -597,12 +627,12 @@ func (r *Router) actionSniff(
} }
if err != nil { if err != nil {
sniffBuffer.Release() sniffBuffer.Release()
if !errors.Is(err, os.ErrDeadlineExceeded) { if !errors.Is(err, context.DeadlineExceeded) {
fatalErr = err fatalErr = err
return return
} }
} else { } else {
if len(packetBuffers) > 0 || metadata.SniffError != nil { if quicMoreData() {
err = sniff.PeekPacket( err = sniff.PeekPacket(
ctx, ctx,
metadata, metadata,
@@ -622,32 +652,34 @@ func (r *Router) actionSniff(
Destination: destination, Destination: destination,
} }
packetBuffers = append(packetBuffers, packetBuffer) packetBuffers = append(packetBuffers, packetBuffer)
metadata.SnifferNames = action.SnifferNames
metadata.SniffError = err metadata.SniffError = err
if errors.Is(err, sniff.ErrNeedMoreData) { if errors.Is(err, sniff.ErrNeedMoreData) {
// TODO: replace with generic message when there are more multi-packet protocols // TODO: replace with generic message when there are more multi-packet protocols
r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello") r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello")
continue continue
} }
if metadata.Protocol != "" { }
//goland:noinspection GoDeprecation goto finally
if action.OverrideDestination && M.IsDomainName(metadata.Domain) { }
metadata.Destination = M.Socksaddr{ finally:
Fqdn: metadata.Domain, if err == nil {
Port: metadata.Destination.Port, //goland:noinspection GoDeprecation
} if action.OverrideDestination && M.IsDomainName(metadata.Domain) {
} metadata.Destination = M.Socksaddr{
if metadata.Domain != "" && metadata.Client != "" { Fqdn: metadata.Domain,
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain, ", client: ", metadata.Client) Port: metadata.Destination.Port,
} else if metadata.Domain != "" {
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain)
} else if metadata.Client != "" {
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", client: ", metadata.Client)
} else {
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol)
}
} }
} }
break if metadata.Domain != "" && metadata.Client != "" {
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain, ", client: ", metadata.Client)
} else if metadata.Domain != "" {
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain)
} else if metadata.Client != "" {
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", client: ", metadata.Client)
} else {
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol)
}
} }
} }
return return
@@ -675,11 +707,6 @@ func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundCon
} }
metadata.DestinationAddresses = addresses metadata.DestinationAddresses = addresses
r.logger.DebugContext(ctx, "resolved [", strings.Join(F.MapToString(metadata.DestinationAddresses), " "), "]") r.logger.DebugContext(ctx, "resolved [", strings.Join(F.MapToString(metadata.DestinationAddresses), " "), "]")
if metadata.Destination.IsIPv4() {
metadata.IPVersion = 4
} else if metadata.Destination.IsIPv6() {
metadata.IPVersion = 6
}
} }
return nil return nil
} }

View File

@@ -87,7 +87,7 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
return &RuleActionHijackDNS{}, nil return &RuleActionHijackDNS{}, nil
case C.RuleActionTypeSniff: case C.RuleActionTypeSniff:
sniffAction := &RuleActionSniff{ sniffAction := &RuleActionSniff{
snifferNames: action.SniffOptions.Sniffer, SnifferNames: action.SniffOptions.Sniffer,
Timeout: time.Duration(action.SniffOptions.Timeout), Timeout: time.Duration(action.SniffOptions.Timeout),
} }
return sniffAction, sniffAction.build() return sniffAction, sniffAction.build()
@@ -361,7 +361,7 @@ func (r *RuleActionHijackDNS) String() string {
} }
type RuleActionSniff struct { type RuleActionSniff struct {
snifferNames []string SnifferNames []string
StreamSniffers []sniff.StreamSniffer StreamSniffers []sniff.StreamSniffer
PacketSniffers []sniff.PacketSniffer PacketSniffers []sniff.PacketSniffer
Timeout time.Duration Timeout time.Duration
@@ -374,7 +374,7 @@ func (r *RuleActionSniff) Type() string {
} }
func (r *RuleActionSniff) build() error { func (r *RuleActionSniff) build() error {
for _, name := range r.snifferNames { for _, name := range r.SnifferNames {
switch name { switch name {
case C.ProtocolTLS: case C.ProtocolTLS:
r.StreamSniffers = append(r.StreamSniffers, sniff.TLSClientHello) r.StreamSniffers = append(r.StreamSniffers, sniff.TLSClientHello)
@@ -407,14 +407,14 @@ func (r *RuleActionSniff) build() error {
} }
func (r *RuleActionSniff) String() string { func (r *RuleActionSniff) String() string {
if len(r.snifferNames) == 0 && r.Timeout == 0 { if len(r.SnifferNames) == 0 && r.Timeout == 0 {
return "sniff" return "sniff"
} else if len(r.snifferNames) > 0 && r.Timeout == 0 { } else if len(r.SnifferNames) > 0 && r.Timeout == 0 {
return F.ToString("sniff(", strings.Join(r.snifferNames, ","), ")") return F.ToString("sniff(", strings.Join(r.SnifferNames, ","), ")")
} else if len(r.snifferNames) == 0 && r.Timeout > 0 { } else if len(r.SnifferNames) == 0 && r.Timeout > 0 {
return F.ToString("sniff(", r.Timeout.String(), ")") return F.ToString("sniff(", r.Timeout.String(), ")")
} else { } else {
return F.ToString("sniff(", strings.Join(r.snifferNames, ","), ",", r.Timeout.String(), ")") return F.ToString("sniff(", strings.Join(r.SnifferNames, ","), ",", r.Timeout.String(), ")")
} }
} }

View File

@@ -6,6 +6,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"github.com/sagernet/fswatch" "github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -13,7 +14,6 @@ import (
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
@@ -27,16 +27,16 @@ import (
var _ adapter.RuleSet = (*LocalRuleSet)(nil) var _ adapter.RuleSet = (*LocalRuleSet)(nil)
type LocalRuleSet struct { type LocalRuleSet struct {
ctx context.Context ctx context.Context
logger logger.Logger logger logger.Logger
tag string tag string
rules []adapter.HeadlessRule access sync.RWMutex
metadata adapter.RuleSetMetadata rules []adapter.HeadlessRule
fileFormat string metadata adapter.RuleSetMetadata
watcher *fswatch.Watcher fileFormat string
callbackAccess sync.Mutex watcher *fswatch.Watcher
callbacks list.List[adapter.RuleSetUpdateCallback] callbacks list.List[adapter.RuleSetUpdateCallback]
refs atomic.Int32 refs atomic.Int32
} }
func NewLocalRuleSet(ctx context.Context, logger logger.Logger, options option.RuleSet) (*LocalRuleSet, error) { func NewLocalRuleSet(ctx context.Context, logger logger.Logger, options option.RuleSet) (*LocalRuleSet, error) {
@@ -141,11 +141,11 @@ func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error {
metadata.ContainsProcessRule = hasHeadlessRule(headlessRules, isProcessHeadlessRule) metadata.ContainsProcessRule = hasHeadlessRule(headlessRules, isProcessHeadlessRule)
metadata.ContainsWIFIRule = hasHeadlessRule(headlessRules, isWIFIHeadlessRule) metadata.ContainsWIFIRule = hasHeadlessRule(headlessRules, isWIFIHeadlessRule)
metadata.ContainsIPCIDRRule = hasHeadlessRule(headlessRules, isIPCIDRHeadlessRule) metadata.ContainsIPCIDRRule = hasHeadlessRule(headlessRules, isIPCIDRHeadlessRule)
s.access.Lock()
s.rules = rules s.rules = rules
s.metadata = metadata s.metadata = metadata
s.callbackAccess.Lock()
callbacks := s.callbacks.Array() callbacks := s.callbacks.Array()
s.callbackAccess.Unlock() s.access.Unlock()
for _, callback := range callbacks { for _, callback := range callbacks {
callback(s) callback(s)
} }
@@ -157,10 +157,14 @@ func (s *LocalRuleSet) PostStart() error {
} }
func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata { func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata {
s.access.RLock()
defer s.access.RUnlock()
return s.metadata return s.metadata
} }
func (s *LocalRuleSet) ExtractIPSet() []*netipx.IPSet { func (s *LocalRuleSet) ExtractIPSet() []*netipx.IPSet {
s.access.RLock()
defer s.access.RUnlock()
return common.FlatMap(s.rules, extractIPSetFromRule) return common.FlatMap(s.rules, extractIPSetFromRule)
} }
@@ -181,14 +185,14 @@ func (s *LocalRuleSet) Cleanup() {
} }
func (s *LocalRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { func (s *LocalRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
s.callbackAccess.Lock() s.access.Lock()
defer s.callbackAccess.Unlock() defer s.access.Unlock()
return s.callbacks.PushBack(callback) return s.callbacks.PushBack(callback)
} }
func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) {
s.callbackAccess.Lock() s.access.Lock()
defer s.callbackAccess.Unlock() defer s.access.Unlock()
s.callbacks.Remove(element) s.callbacks.Remove(element)
} }

View File

@@ -10,6 +10,7 @@ import (
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -17,7 +18,6 @@ import (
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
@@ -40,16 +40,16 @@ type RemoteRuleSet struct {
logger logger.ContextLogger logger logger.ContextLogger
outbound adapter.OutboundManager outbound adapter.OutboundManager
options option.RuleSet options option.RuleSet
metadata adapter.RuleSetMetadata
updateInterval time.Duration updateInterval time.Duration
dialer N.Dialer dialer N.Dialer
access sync.RWMutex
rules []adapter.HeadlessRule rules []adapter.HeadlessRule
metadata adapter.RuleSetMetadata
lastUpdated time.Time lastUpdated time.Time
lastEtag string lastEtag string
updateTicker *time.Ticker updateTicker *time.Ticker
cacheFile adapter.CacheFile cacheFile adapter.CacheFile
pauseManager pause.Manager pauseManager pause.Manager
callbackAccess sync.Mutex
callbacks list.List[adapter.RuleSetUpdateCallback] callbacks list.List[adapter.RuleSetUpdateCallback]
refs atomic.Int32 refs atomic.Int32
} }
@@ -120,10 +120,14 @@ func (s *RemoteRuleSet) PostStart() error {
} }
func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata { func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata {
s.access.RLock()
defer s.access.RUnlock()
return s.metadata return s.metadata
} }
func (s *RemoteRuleSet) ExtractIPSet() []*netipx.IPSet { func (s *RemoteRuleSet) ExtractIPSet() []*netipx.IPSet {
s.access.RLock()
defer s.access.RUnlock()
return common.FlatMap(s.rules, extractIPSetFromRule) return common.FlatMap(s.rules, extractIPSetFromRule)
} }
@@ -144,14 +148,14 @@ func (s *RemoteRuleSet) Cleanup() {
} }
func (s *RemoteRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { func (s *RemoteRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
s.callbackAccess.Lock() s.access.Lock()
defer s.callbackAccess.Unlock() defer s.access.Unlock()
return s.callbacks.PushBack(callback) return s.callbacks.PushBack(callback)
} }
func (s *RemoteRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { func (s *RemoteRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) {
s.callbackAccess.Lock() s.access.Lock()
defer s.callbackAccess.Unlock() defer s.access.Unlock()
s.callbacks.Remove(element) s.callbacks.Remove(element)
} }
@@ -185,13 +189,13 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
return E.Cause(err, "parse rule_set.rules.[", i, "]") return E.Cause(err, "parse rule_set.rules.[", i, "]")
} }
} }
s.access.Lock()
s.metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) s.metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
s.metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) s.metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
s.rules = rules s.rules = rules
s.callbackAccess.Lock()
callbacks := s.callbacks.Array() callbacks := s.callbacks.Array()
s.callbackAccess.Unlock() s.access.Unlock()
for _, callback := range callbacks { for _, callback := range callbacks {
callback(s) callback(s)
} }

View File

@@ -5,8 +5,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"sync/atomic"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson" "github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/service/filemanager" "github.com/sagernet/sing/service/filemanager"

View File

@@ -3,9 +3,9 @@ package ssmapi
import ( import (
"net" "net"
"sync" "sync"
"sync/atomic"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )

View File

@@ -292,7 +292,7 @@ func ReadPacket(conn net.Conn, buffer *buf.Buffer) (M.Socksaddr, error) {
} }
_, err = buffer.ReadFullFrom(conn, int(length)) _, err = buffer.ReadFullFrom(conn, int(length))
return destination.Unwrap(), err return destination, err
} }
func WritePacket(conn net.Conn, buffer *buf.Buffer, destination M.Socksaddr) error { func WritePacket(conn net.Conn, buffer *buf.Buffer, destination M.Socksaddr) error {

View File

@@ -15,7 +15,6 @@ import (
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-quic" "github.com/sagernet/sing-quic"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
@@ -30,7 +29,7 @@ type Client struct {
tlsConfig tls.Config tlsConfig tls.Config
quicConfig *quic.Config quicConfig *quic.Config
connAccess sync.Mutex connAccess sync.Mutex
conn atomic.TypedValue[quic.Connection] conn common.TypedValue[quic.Connection]
rawConn net.Conn rawConn net.Conn
} }

View File

@@ -291,7 +291,7 @@ func wrapWsError(err error) error {
} }
var closedErr wsutil.ClosedError var closedErr wsutil.ClosedError
if errors.As(err, &closedErr) { if errors.As(err, &closedErr) {
if closedErr.Code == ws.StatusNormalClosure { if closedErr.Code == ws.StatusNormalClosure || closedErr.Code == ws.StatusNoStatusRcvd {
err = io.EOF err = io.EOF
} }
} }

View File

@@ -138,7 +138,7 @@ func (c *ClientBind) receive(packets [][]byte, sizes []int, eps []conn.Endpoint)
b := packets[0] b := packets[0]
common.ClearArray(b[1:4]) common.ClearArray(b[1:4])
} }
eps[0] = remoteEndpoint(M.AddrPortFromNet(addr)) eps[0] = remoteEndpoint(M.SocksaddrFromNet(addr).Unwrap().AddrPort())
count = 1 count = 1
return return
} }