Compare commits

..

92 Commits

Author SHA1 Message Date
世界
5d73dd617a
documentation: Bump version 2025-05-18 16:51:05 +08:00
世界
f399121721
Add SSM API service 2025-05-18 16:50:51 +08:00
世界
bbf7de181b
Add resolved service and DNS server 2025-05-18 16:50:51 +08:00
世界
1327c7940e
Add DERP service 2025-05-18 16:50:51 +08:00
世界
0771cb5b9e
Add service component type 2025-05-18 16:50:50 +08:00
世界
3350896751
Fix tproxy tcp control 2025-05-18 16:50:50 +08:00
愚者
f83abb5cd6
release: Fix build tags for android
Signed-off-by: 愚者 <11926619+FansChou@users.noreply.github.com>
2025-05-18 16:50:50 +08:00
世界
9bdf29c92b
prevent creation of bind and mark controls on unsupported platforms 2025-05-18 16:50:50 +08:00
PuerNya
b6546ba9ee
documentation: Fix description of reject DNS action behavior 2025-05-18 16:50:50 +08:00
Restia-Ashbell
12b88678cc
Fix TLS record fragment 2025-05-18 16:50:50 +08:00
世界
6c71cf88c5
Add missing accept_routes option for Tailscale 2025-05-18 16:50:50 +08:00
世界
eaae1a72e8
Add TLS record fragment support 2025-05-18 16:50:50 +08:00
世界
be8fa753f6
release: Update Go to 1.24.3 2025-05-18 16:50:49 +08:00
世界
9338611506
Fix set edns0 client subnet 2025-05-18 16:50:49 +08:00
世界
dd5e8c2fc3
Update minor dependencies 2025-05-18 16:50:49 +08:00
世界
5f630afabd
Update certmagic and providers 2025-05-18 16:50:49 +08:00
世界
afe039b2a0
Update protobuf and grpc 2025-05-18 16:50:49 +08:00
世界
d9dba4c3c2
Add control options for listeners 2025-05-18 16:50:49 +08:00
世界
a6f30adbdf
Update quic-go to v0.51.0 2025-05-18 16:50:49 +08:00
世界
86458bf26e
Update utls to v1.7.2 2025-05-18 16:50:48 +08:00
世界
381339f6aa
Handle EDNS version downgrade 2025-05-18 16:50:48 +08:00
世界
eae10a8342
documentation: Fix anytls padding scheme description 2025-05-18 16:50:48 +08:00
安容
1ec660cc84
Report invalid DNS address early 2025-05-18 16:50:47 +08:00
世界
664b5871f7
Fix wireguard listen_port 2025-05-18 16:50:47 +08:00
世界
5ff8df74b7
clash-api: Add more meta api 2025-05-18 16:50:47 +08:00
世界
79047deed1
Fix DNS lookup 2025-05-18 16:50:47 +08:00
世界
06a5c74470
Fix fetch ECH configs 2025-05-18 16:50:46 +08:00
reletor
3fbc2d8c70
documentation: Minor fixes 2025-05-18 16:50:46 +08:00
caelansar
85fa07a64c
Fix callback deletion in UDP transport 2025-05-18 16:50:46 +08:00
世界
8fe4fcb339
documentation: Try to make the play review happy 2025-05-18 16:50:46 +08:00
世界
822dc654b4
Fix missing handling of legacy domain_strategy options 2025-05-18 16:50:45 +08:00
世界
06d32ae5f5
Improve local DNS server 2025-05-18 16:50:45 +08:00
anytls
be03fd8736
Update anytls
Co-authored-by: anytls <anytls>
2025-05-18 16:50:45 +08:00
世界
4f520741d2
Fix DNS dialer 2025-05-18 16:50:45 +08:00
世界
7a0b0141c9
release: Skip override version for iOS 2025-05-18 16:50:44 +08:00
iikira
e88a91ccd1
Fix UDP DNS server crash
Signed-off-by: iikira <i2@mail.iikira.com>
2025-05-18 16:50:44 +08:00
ReleTor
a363096a80
Fix fetch ECH configs 2025-05-18 16:50:44 +08:00
世界
4c1ed5a3c7
Allow direct outbounds without domain_resolver 2025-05-18 16:50:43 +08:00
世界
633203aed7
Fix Tailscale dialer 2025-05-18 16:50:43 +08:00
dyhkwong
f4d997bbfc
Fix DNS over QUIC stream close 2025-05-18 16:50:42 +08:00
anytls
86ca81a989
Update anytls
Co-authored-by: anytls <anytls>
2025-05-18 16:50:42 +08:00
Rambling2076
6438029658
Fix missing with_tailscale in Dockerfile
Signed-off-by: Rambling2076 <Rambling2076@proton.me>
2025-05-18 16:50:42 +08:00
世界
5ba910997c
Fail when default DNS server not found 2025-05-18 16:50:41 +08:00
世界
df710eccbb
Update gVisor to 20250319.0 2025-05-18 16:50:41 +08:00
世界
ca3f70ac53
Explicitly reject detour to empty direct outbounds 2025-05-18 16:50:41 +08:00
世界
833f052f9c
Add netns support 2025-05-18 16:50:40 +08:00
世界
51fb60bca6
Add wildcard name support for predefined records 2025-05-18 16:50:40 +08:00
世界
c5e9888f1a
Remove map usage in options 2025-05-18 16:50:39 +08:00
世界
e49a589180
Fix unhandled DNS loop 2025-05-18 16:50:38 +08:00
世界
2eefbd8469
Add wildcard-sni support for shadow-tls inbound 2025-05-18 16:50:38 +08:00
k9982874
2f78acef13
Add ntp protocol sniffing 2025-05-18 16:50:38 +08:00
世界
1f677e54ed
option: Fix marshal legacy DNS options 2025-05-18 16:50:37 +08:00
世界
63bd625089
Make domain_resolver optional when only one DNS server is configured 2025-05-18 16:50:37 +08:00
世界
cb95f29763
Fix DNS lookup context pollution 2025-05-18 16:50:37 +08:00
世界
696c78604f
Fix http3 DNS server connecting to wrong address 2025-05-18 16:50:37 +08:00
Restia-Ashbell
be11352965
documentation: Fix typo 2025-05-18 16:50:36 +08:00
anytls
9756e482a0
Update sing-anytls
Co-authored-by: anytls <anytls>
2025-05-18 16:50:36 +08:00
k9982874
5041e7718a
Fix hosts DNS server 2025-05-18 16:50:35 +08:00
世界
c5447e2632
Fix UDP DNS server crash 2025-05-18 16:50:35 +08:00
世界
d81d47b4e4
documentation: Fix missing ip_accept_any DNS rule option 2025-05-18 16:50:35 +08:00
世界
97d0e5542f
Fix anytls dialer usage 2025-05-18 16:50:34 +08:00
世界
833971636f
Move predefined DNS server to rule action 2025-05-18 16:50:34 +08:00
世界
a6ae909675
Fix domain resolver on direct outbound 2025-05-18 16:50:33 +08:00
Zephyruso
608efa7a7c
Fix missing AnyTLS display name 2025-05-18 16:50:33 +08:00
anytls
a35d83f364
Update sing-anytls
Co-authored-by: anytls <anytls>
2025-05-18 16:50:33 +08:00
Estel
4401bedf96
documentation: Fix typo
Signed-off-by: Estel <callmebedrockdigger@gmail.com>
2025-05-18 16:50:32 +08:00
TargetLocked
6edc9485f6
Fix parsing legacy DNS options 2025-05-18 16:50:32 +08:00
世界
cb24b38822
Fix DNS fallback 2025-05-18 16:50:32 +08:00
世界
4217eaf1da
documentation: Fix missing hosts DNS server 2025-05-18 16:50:32 +08:00
anytls
8d8a0673d2
Add MinIdleSession option to AnyTLS outbound
Co-authored-by: anytls <anytls>
2025-05-18 16:50:31 +08:00
ReleTor
0746e69907
documentation: Minor fixes 2025-05-18 16:50:31 +08:00
libtry486
a5b941315b
documentation: Fix typo
fix typo

Signed-off-by: libtry486 <89328481+libtry486@users.noreply.github.com>
2025-05-18 16:50:31 +08:00
Alireza Ahmadi
9864706a4e
Fix Outbound deadlock 2025-05-18 16:50:31 +08:00
世界
c3fdf13da9
documentation: Fix AnyTLS doc 2025-05-18 16:50:30 +08:00
anytls
6355f48a47
Add AnyTLS protocol 2025-05-18 16:50:29 +08:00
世界
6a48e97439
Migrate to stdlib ECH support 2025-05-18 16:50:29 +08:00
世界
8d78d59f7c
Add fallback local DNS server for iOS 2025-05-18 16:50:28 +08:00
世界
c4272efe82
Get darwin local DNS server from libresolv 2025-05-18 16:50:28 +08:00
世界
bd0a0aef86
Improve resolve action 2025-05-18 16:50:28 +08:00
世界
ffba6cc930
Add back port hopping to hysteria 1 2025-05-18 16:50:27 +08:00
xchacha20-poly1305
56b1ea212f
Remove single quotes of raw Moziila certs 2025-05-18 16:50:26 +08:00
世界
e1f64b9c31
Add Tailscale endpoint 2025-05-18 16:50:26 +08:00
世界
9b185b7c92
Build legacy binaries with latest Go 2025-05-18 16:50:26 +08:00
世界
7afe7abd60
documentation: Remove outdated icons 2025-05-18 16:50:26 +08:00
世界
a9da8fce10
documentation: Certificate store 2025-05-18 16:50:26 +08:00
世界
9d96ba4496
documentation: TLS fragment 2025-05-18 16:50:26 +08:00
世界
f10191d9d0
documentation: Outbound domain resolver 2025-05-18 16:50:25 +08:00
世界
2d9f44269d
documentation: Refactor DNS 2025-05-18 16:50:24 +08:00
世界
b0447d54ec
Add certificate store 2025-05-18 16:50:24 +08:00
世界
11c58644e1
Add TLS fragment support 2025-05-18 16:50:24 +08:00
世界
1d5f1f32b5
refactor: Outbound domain resolver 2025-05-18 16:50:24 +08:00
世界
3f50776fc3
refactor: DNS 2025-05-18 16:50:23 +08:00
57 changed files with 268 additions and 800 deletions

View File

@ -8,7 +8,6 @@
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues" --deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
--no-deb-generate-changes --no-deb-generate-changes
--config-files /etc/sing-box/config.json --config-files /etc/sing-box/config.json
--after-install release/config/sing-box.postinst
release/config/config.json=/etc/sing-box/config.json release/config/config.json=/etc/sing-box/config.json

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.24.4 go-version: ^1.24.3
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@ -109,7 +109,7 @@ jobs:
if: ${{ ! matrix.legacy_go }} if: ${{ ! matrix.legacy_go }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.4 go-version: ^1.24.3
- name: Cache Legacy Go - name: Cache Legacy Go
if: matrix.require_legacy_go if: matrix.require_legacy_go
id: cache-legacy-go id: cache-legacy-go
@ -294,7 +294,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.4 go-version: ^1.24.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
@ -374,7 +374,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.4 go-version: ^1.24.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
@ -472,7 +472,7 @@ jobs:
if: matrix.if if: matrix.if
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.4 go-version: ^1.24.3
- name: Setup Xcode stable - name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next' if: matrix.if && github.ref == 'refs/heads/main-next'
run: |- run: |-

View File

@ -28,7 +28,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.4 go-version: ^1.24.3
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v6 uses: golangci/golangci-lint-action@v6
with: with:

View File

@ -25,7 +25,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.4 go-version: ^1.24.3
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@ -66,7 +66,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.4 go-version: ^1.24.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
@ -80,7 +80,7 @@ jobs:
- name: Set build tags - name: Set build tags
run: | run: |
set -xeuo pipefail set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale' TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api'
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build - name: Build
run: | run: |

View File

@ -1,10 +1,11 @@
NAME = sing-box NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD) COMMIT = $(shell git rev-parse --short HEAD)
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale TAGS ?= with_gvisor,with_dhcp,with_wireguard,with_clash_api,with_quic,with_utls,with_tailscale
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_utls
GOHOSTOS = $(shell go env GOHOSTOS) GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH) GOHOSTARCH = $(shell go env GOHOSTARCH)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest) VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid=" PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)" MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"

2
box.go
View File

@ -498,7 +498,7 @@ func (s *Box) Close() error {
close(s.done) close(s.done)
} }
err := common.Close( err := common.Close(
s.service, s.endpoint, s.inbound, s.outbound, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network, s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
) )
for _, lifecycleService := range s.internalService { for _, lifecycleService := range s.internalService {
err = E.Append(err, lifecycleService.Close(), func(err error) error { err = E.Append(err, lifecycleService.Close(), func(err error) error {

@ -1 +1 @@
Subproject commit 320170a1077ea5c93872b3e055b96b8836615ef0 Subproject commit cec05bf6935eca219a722883212ae8880d2e863e

View File

@ -105,7 +105,7 @@ func publishTestflight(ctx context.Context) error {
return err return err
} }
tag := tagVersion.VersionString() tag := tagVersion.VersionString()
client := createClient(20 * time.Minute) client := createClient(10 * time.Minute)
log.Info(tag, " list build IDs") log.Info(tag, " list build IDs")
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil) buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
@ -145,7 +145,7 @@ 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 common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 5*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

View File

@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
@ -67,5 +68,6 @@ func preRun(cmd *cobra.Command, args []string) {
if len(configPaths) == 0 && len(configDirectories) == 0 { if len(configPaths) == 0 && len(configDirectories) == 0 {
configPaths = append(configPaths, "config.json") configPaths = append(configPaths, "config.json")
} }
globalCtx = include.Context(service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))) globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry())
} }

View File

@ -97,6 +97,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
} else if networkManager.AutoDetectInterface() { } else if networkManager.AutoDetectInterface() {
if platformInterface != nil { if platformInterface != nil {
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy) networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
if networkStrategy == nil {
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
defaultNetworkStrategy = true
}
networkType = common.Map(options.NetworkType, option.InterfaceType.Build) networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build) fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 { if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 {
@ -108,10 +112,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 { if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
networkFallbackDelay = defaultOptions.FallbackDelay networkFallbackDelay = defaultOptions.FallbackDelay
} }
if networkStrategy == nil {
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
defaultNetworkStrategy = true
}
bindFunc := networkManager.ProtectFunc() bindFunc := networkManager.ProtectFunc()
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)

View File

@ -10,7 +10,9 @@ import (
"sync" "sync"
"time" "time"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
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"
@ -24,9 +26,7 @@ type slowOpenConn struct {
destination M.Socksaddr destination M.Socksaddr
conn net.Conn conn net.Conn
create chan struct{} create chan struct{}
done chan struct{}
access sync.Mutex access sync.Mutex
closeOnce sync.Once
err error err error
} }
@ -45,7 +45,6 @@ func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, des
network: network, network: network,
destination: destination, destination: destination,
create: make(chan struct{}), create: make(chan struct{}),
done: make(chan struct{}),
}, nil }, nil
} }
@ -56,8 +55,8 @@ func (c *slowOpenConn) Read(b []byte) (n int, err error) {
if c.err != nil { if c.err != nil {
return 0, c.err return 0, c.err
} }
case <-c.done: case <-c.ctx.Done():
return 0, os.ErrClosed return 0, c.ctx.Err()
} }
} }
return c.conn.Read(b) return c.conn.Read(b)
@ -75,15 +74,12 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
return 0, c.err return 0, c.err
} }
return c.conn.Write(b) return c.conn.Write(b)
case <-c.done:
return 0, os.ErrClosed
default: default:
} }
conn, err := c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b) c.conn, err = c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
if err != nil { if err != nil {
c.err = err c.conn = nil
} else { c.err = E.Cause(err, "dial tcp fast open")
c.conn = conn
} }
n = len(b) n = len(b)
close(c.create) close(c.create)
@ -91,13 +87,7 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
} }
func (c *slowOpenConn) Close() error { func (c *slowOpenConn) Close() error {
c.closeOnce.Do(func() { return common.Close(c.conn)
close(c.done)
if c.conn != nil {
c.conn.Close()
}
})
return nil
} }
func (c *slowOpenConn) LocalAddr() net.Addr { func (c *slowOpenConn) LocalAddr() net.Addr {
@ -162,8 +152,8 @@ func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
if c.err != nil { if c.err != nil {
return 0, c.err return 0, c.err
} }
case <-c.done: case <-c.ctx.Done():
return 0, c.err return 0, c.ctx.Err()
} }
} }
return bufio.Copy(w, c.conn) return bufio.Copy(w, c.conn)

View File

@ -56,7 +56,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, M.ParseSocksaddr(address).IsIPv6(), false)
}) })
}) })
} }

View File

@ -41,7 +41,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, M.ParseSocksaddr(address).IsIPv6(), true)
}) })
}) })
} }

View File

@ -25,7 +25,7 @@ import (
"golang.org/x/crypto/cryptobyte" "golang.org/x/crypto/cryptobyte"
) )
func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, options option.OutboundTLSOptions) (Config, error) { func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
var echConfig []byte var echConfig []byte
if len(options.ECH.Config) > 0 { if len(options.ECH.Config) > 0 {
echConfig = []byte(strings.Join(options.ECH.Config, "\n")) echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
@ -45,11 +45,11 @@ func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, op
if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 { if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
return nil, E.New("invalid ECH configs pem") return nil, E.New("invalid ECH configs pem")
} }
clientConfig.SetECHConfigList(block.Bytes) tlsConfig.EncryptedClientHelloConfigList = block.Bytes
return clientConfig, nil return &STDClientConfig{tlsConfig}, nil
} else { } else {
return &ECHClientConfig{ return &STDECHClientConfig{
ECHCapableConfig: clientConfig, STDClientConfig: STDClientConfig{tlsConfig},
dnsRouter: service.FromContext[adapter.DNSRouter](ctx), dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
}, nil }, nil
} }
@ -102,15 +102,15 @@ func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
return nil return nil
} }
type ECHClientConfig struct { type STDECHClientConfig struct {
ECHCapableConfig STDClientConfig
access sync.Mutex access sync.Mutex
dnsRouter adapter.DNSRouter dnsRouter adapter.DNSRouter
lastTTL time.Duration lastTTL time.Duration
lastUpdate time.Time lastUpdate time.Time
} }
func (s *ECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
tlsConn, err := s.fetchAndHandshake(ctx, conn) tlsConn, err := s.fetchAndHandshake(ctx, conn)
if err != nil { if err != nil {
return nil, err return nil, err
@ -122,17 +122,17 @@ func (s *ECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (a
return tlsConn, nil return tlsConn, nil
} }
func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
s.access.Lock() s.access.Lock()
defer s.access.Unlock() defer s.access.Unlock()
if len(s.ECHConfigList()) == 0 || s.lastTTL == 0 || time.Now().Sub(s.lastUpdate) > s.lastTTL { if len(s.config.EncryptedClientHelloConfigList) == 0 || s.lastTTL == 0 || time.Now().Sub(s.lastUpdate) > s.lastTTL {
message := &mDNS.Msg{ message := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{ MsgHdr: mDNS.MsgHdr{
RecursionDesired: true, RecursionDesired: true,
}, },
Question: []mDNS.Question{ Question: []mDNS.Question{
{ {
Name: mDNS.Fqdn(s.ServerName()), Name: mDNS.Fqdn(s.config.ServerName),
Qtype: mDNS.TypeHTTPS, Qtype: mDNS.TypeHTTPS,
Qclass: mDNS.ClassINET, Qclass: mDNS.ClassINET,
}, },
@ -157,21 +157,21 @@ func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn)
} }
s.lastTTL = time.Duration(rr.Header().Ttl) * time.Second s.lastTTL = time.Duration(rr.Header().Ttl) * time.Second
s.lastUpdate = time.Now() s.lastUpdate = time.Now()
s.SetECHConfigList(echConfigList) s.config.EncryptedClientHelloConfigList = echConfigList
break match break match
} }
} }
} }
} }
if len(s.ECHConfigList()) == 0 { if len(s.config.EncryptedClientHelloConfigList) == 0 {
return nil, E.New("no ECH config found in DNS records") return nil, E.New("no ECH config found in DNS records")
} }
} }
return s.Client(conn) return s.Client(conn)
} }
func (s *ECHClientConfig) Clone() Config { func (s *STDECHClientConfig) Clone() Config {
return &ECHClientConfig{ECHCapableConfig: s.ECHCapableConfig.Clone().(ECHCapableConfig), dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate} return &STDECHClientConfig{STDClientConfig: STDClientConfig{s.config.Clone()}, dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate}
} }
func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) { func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {

View File

@ -11,12 +11,6 @@ import (
"github.com/cloudflare/circl/kem" "github.com/cloudflare/circl/kem"
) )
type ECHCapableConfig interface {
Config
ECHConfigList() []byte
SetECHConfigList([]byte)
}
func ECHKeygenDefault(serverName string) (configPem string, keyPem string, err error) { func ECHKeygenDefault(serverName string) (configPem string, keyPem string, err error) {
cipherSuites := []echCipherSuite{ cipherSuites := []echCipherSuite{
{ {

View File

@ -10,7 +10,7 @@ import (
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, options option.OutboundTLSOptions) (Config, error) { func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
return nil, E.New("ECH requires go1.24, please recompile your binary.") return nil, E.New("ECH requires go1.24, please recompile your binary.")
} }

View File

@ -74,7 +74,7 @@ func NewRealityClient(ctx context.Context, serverAddress string, options option.
if decodedLen > 8 { if decodedLen > 8 {
return nil, E.New("invalid short_id") return nil, E.New("invalid short_id")
} }
return &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID}, nil return &RealityClientConfig{ctx, uClient, publicKey, shortID}, nil
} }
func (e *RealityClientConfig) ServerName() string { func (e *RealityClientConfig) ServerName() string {

View File

@ -7,60 +7,43 @@ import (
"net" "net"
"os" "os"
"strings" "strings"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tlsfragment"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
) )
type STDClientConfig struct { type STDClientConfig struct {
ctx context.Context
config *tls.Config config *tls.Config
fragment bool
fragmentFallbackDelay time.Duration
recordFragment bool
} }
func (c *STDClientConfig) ServerName() string { func (s *STDClientConfig) ServerName() string {
return c.config.ServerName return s.config.ServerName
} }
func (c *STDClientConfig) SetServerName(serverName string) { func (s *STDClientConfig) SetServerName(serverName string) {
c.config.ServerName = serverName s.config.ServerName = serverName
} }
func (c *STDClientConfig) NextProtos() []string { func (s *STDClientConfig) NextProtos() []string {
return c.config.NextProtos return s.config.NextProtos
} }
func (c *STDClientConfig) SetNextProtos(nextProto []string) { func (s *STDClientConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto s.config.NextProtos = nextProto
} }
func (c *STDClientConfig) Config() (*STDConfig, error) { func (s *STDClientConfig) Config() (*STDConfig, error) {
return c.config, nil return s.config, nil
} }
func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) { func (s *STDClientConfig) Client(conn net.Conn) (Conn, error) {
if c.recordFragment { return tls.Client(conn, s.config), nil
conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay)
}
return tls.Client(conn, c.config), nil
} }
func (c *STDClientConfig) Clone() Config { func (s *STDClientConfig) Clone() Config {
return &STDClientConfig{c.ctx, c.config.Clone(), c.fragment, c.fragmentFallbackDelay, c.recordFragment} return &STDClientConfig{s.config.Clone()}
}
func (c *STDClientConfig) ECHConfigList() []byte {
return c.config.EncryptedClientHelloConfigList
}
func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte) {
c.config.EncryptedClientHelloConfigList = EncryptedClientHelloConfigList
} }
func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) { func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
@ -77,7 +60,9 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
var tlsConfig tls.Config var tlsConfig tls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx) tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx) tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if !options.DisableSNI { if options.DisableSNI {
tlsConfig.ServerName = "127.0.0.1"
} else {
tlsConfig.ServerName = serverName tlsConfig.ServerName = serverName
} }
if options.Insecure { if options.Insecure {
@ -142,10 +127,8 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
} }
tlsConfig.RootCAs = certPool tlsConfig.RootCAs = certPool
} }
stdConfig := &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
if options.ECH != nil && options.ECH.Enabled { if options.ECH != nil && options.ECH.Enabled {
return parseECHClientConfig(ctx, stdConfig, options) return parseECHClientConfig(ctx, options, &tlsConfig)
} else {
return stdConfig, nil
} }
return &STDClientConfig{&tlsConfig}, nil
} }

View File

@ -8,12 +8,11 @@ import (
"crypto/x509" "crypto/x509"
"math/rand" "math/rand"
"net" "net"
"net/netip"
"os" "os"
"strings" "strings"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tlsfragment"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
@ -23,62 +22,48 @@ import (
) )
type UTLSClientConfig struct { type UTLSClientConfig struct {
ctx context.Context
config *utls.Config config *utls.Config
id utls.ClientHelloID id utls.ClientHelloID
fragment bool
fragmentFallbackDelay time.Duration
recordFragment bool
} }
func (c *UTLSClientConfig) ServerName() string { func (e *UTLSClientConfig) ServerName() string {
return c.config.ServerName return e.config.ServerName
} }
func (c *UTLSClientConfig) SetServerName(serverName string) { func (e *UTLSClientConfig) SetServerName(serverName string) {
c.config.ServerName = serverName e.config.ServerName = serverName
} }
func (c *UTLSClientConfig) NextProtos() []string { func (e *UTLSClientConfig) NextProtos() []string {
return c.config.NextProtos return e.config.NextProtos
} }
func (c *UTLSClientConfig) SetNextProtos(nextProto []string) { func (e *UTLSClientConfig) SetNextProtos(nextProto []string) {
if len(nextProto) == 1 && nextProto[0] == http2.NextProtoTLS { if len(nextProto) == 1 && nextProto[0] == http2.NextProtoTLS {
nextProto = append(nextProto, "http/1.1") nextProto = append(nextProto, "http/1.1")
} }
c.config.NextProtos = nextProto e.config.NextProtos = nextProto
} }
func (c *UTLSClientConfig) Config() (*STDConfig, error) { func (e *UTLSClientConfig) Config() (*STDConfig, error) {
return nil, E.New("unsupported usage for uTLS") return nil, E.New("unsupported usage for uTLS")
} }
func (c *UTLSClientConfig) Client(conn net.Conn) (Conn, error) { func (e *UTLSClientConfig) Client(conn net.Conn) (Conn, error) {
if c.recordFragment { return &utlsALPNWrapper{utlsConnWrapper{utls.UClient(conn, e.config.Clone(), e.id)}, e.config.NextProtos}, nil
conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay)
}
return &utlsALPNWrapper{utlsConnWrapper{utls.UClient(conn, c.config.Clone(), c.id)}, c.config.NextProtos}, nil
} }
func (c *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) { func (e *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) {
c.config.SessionIDGenerator = generator e.config.SessionIDGenerator = generator
} }
func (c *UTLSClientConfig) Clone() Config { func (e *UTLSClientConfig) Clone() Config {
return &UTLSClientConfig{ return &UTLSClientConfig{
c.ctx, c.config.Clone(), c.id, c.fragment, c.fragmentFallbackDelay, c.recordFragment, config: e.config.Clone(),
id: e.id,
} }
} }
func (c *UTLSClientConfig) ECHConfigList() []byte {
return c.config.EncryptedClientHelloConfigList
}
func (c *UTLSClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte) {
c.config.EncryptedClientHelloConfigList = EncryptedClientHelloConfigList
}
type utlsConnWrapper struct { type utlsConnWrapper struct {
*utls.UConn *utls.UConn
} }
@ -131,13 +116,15 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
return c.UConn.HandshakeContext(ctx) return c.UConn.HandshakeContext(ctx)
} }
func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) { func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (*UTLSClientConfig, error) {
var serverName string var serverName string
if options.ServerName != "" { if options.ServerName != "" {
serverName = options.ServerName serverName = options.ServerName
} else if serverAddress != "" { } else if serverAddress != "" {
if _, err := netip.ParseAddr(serverName); err != nil {
serverName = serverAddress serverName = serverAddress
} }
}
if serverName == "" && !options.Insecure { if serverName == "" && !options.Insecure {
return nil, E.New("missing server_name or insecure=true") return nil, E.New("missing server_name or insecure=true")
} }
@ -145,7 +132,11 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
var tlsConfig utls.Config var tlsConfig utls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx) tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx) tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if options.DisableSNI {
tlsConfig.ServerName = "127.0.0.1"
} else {
tlsConfig.ServerName = serverName tlsConfig.ServerName = serverName
}
if options.Insecure { if options.Insecure {
tlsConfig.InsecureSkipVerify = options.Insecure tlsConfig.InsecureSkipVerify = options.Insecure
} else if options.DisableSNI { } else if options.DisableSNI {
@ -201,15 +192,7 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
if err != nil { if err != nil {
return nil, err return nil, err
} }
uConfig := &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment} return &UTLSClientConfig{&tlsConfig, id}, nil
if options.ECH != nil && options.ECH.Enabled {
if options.Reality != nil && options.Reality.Enabled {
return nil, E.New("Reality is conflict with ECH")
}
return parseECHClientConfig(ctx, uConfig, options)
} else {
return uConfig, nil
}
} }
var ( var (
@ -237,7 +220,7 @@ func init() {
func uTLSClientHelloID(name string) (utls.ClientHelloID, error) { func uTLSClientHelloID(name string) (utls.ClientHelloID, error) {
switch name { switch name {
case "chrome_psk", "chrome_psk_shuffle", "chrome_padding_psk_shuffle", "chrome_pq", "chrome_pq_psk": case "chrome_psk", "chrome_psk_shuffle", "chrome_padding_psk_shuffle", "chrome_pq":
fallthrough fallthrough
case "chrome", "": case "chrome", "":
return utls.HelloChrome_Auto, nil return utls.HelloChrome_Auto, nil

View File

@ -9,7 +9,6 @@ import (
"strings" "strings"
"time" "time"
C "github.com/sagernet/sing-box/constant"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"golang.org/x/net/publicsuffix" "golang.org/x/net/publicsuffix"
@ -20,21 +19,16 @@ type Conn struct {
tcpConn *net.TCPConn tcpConn *net.TCPConn
ctx context.Context ctx context.Context
firstPacketWritten bool firstPacketWritten bool
splitPacket bool
splitRecord bool splitRecord bool
fallbackDelay time.Duration fallbackDelay time.Duration
} }
func NewConn(conn net.Conn, ctx context.Context, splitPacket bool, splitRecord bool, fallbackDelay time.Duration) *Conn { func NewConn(conn net.Conn, ctx context.Context, splitRecord bool, fallbackDelay time.Duration) *Conn {
if fallbackDelay == 0 {
fallbackDelay = C.TLSFragmentFallbackDelay
}
tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn) tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn)
return &Conn{ return &Conn{
Conn: conn, Conn: conn,
tcpConn: tcpConn, tcpConn: tcpConn,
ctx: ctx, ctx: ctx,
splitPacket: splitPacket,
splitRecord: splitRecord, splitRecord: splitRecord,
fallbackDelay: fallbackDelay, fallbackDelay: fallbackDelay,
} }
@ -47,7 +41,7 @@ func (c *Conn) Write(b []byte) (n int, err error) {
}() }()
serverName := indexTLSServerName(b) serverName := indexTLSServerName(b)
if serverName != nil { if serverName != nil {
if c.splitPacket { if !c.splitRecord {
if c.tcpConn != nil { if c.tcpConn != nil {
err = c.tcpConn.SetNoDelay(true) err = c.tcpConn.SetNoDelay(true)
if err != nil { if err != nil {
@ -87,19 +81,11 @@ func (c *Conn) Write(b []byte) (n int, err error) {
payload = b[splitIndexes[i-1]:splitIndexes[i]] payload = b[splitIndexes[i-1]:splitIndexes[i]]
} }
if c.splitRecord { if c.splitRecord {
if c.splitPacket {
buffer.Reset()
}
payloadLen := uint16(len(payload)) payloadLen := uint16(len(payload))
buffer.Write(b[:3]) buffer.Write(b[:3])
binary.Write(&buffer, binary.BigEndian, payloadLen) binary.Write(&buffer, binary.BigEndian, payloadLen)
buffer.Write(payload) buffer.Write(payload)
if c.splitPacket { } else if c.tcpConn != nil && i != len(splitIndexes) {
payload = buffer.Bytes()
}
}
if c.splitPacket {
if c.tcpConn != nil && i != len(splitIndexes) {
err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay) err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay)
if err != nil { if err != nil {
return return
@ -111,19 +97,19 @@ func (c *Conn) Write(b []byte) (n int, err error) {
} }
} }
} }
} if c.splitRecord {
if c.splitRecord && !c.splitPacket {
_, err = c.Conn.Write(buffer.Bytes()) _, err = c.Conn.Write(buffer.Bytes())
if err != nil { if err != nil {
return return
} }
} } else {
if c.tcpConn != nil { if c.tcpConn != nil {
err = c.tcpConn.SetNoDelay(false) err = c.tcpConn.SetNoDelay(false)
if err != nil { if err != nil {
return return
} }
} }
}
return len(b), nil return len(b), nil
} }
} }

View File

@ -15,7 +15,7 @@ func TestTLSFragment(t *testing.T) {
t.Parallel() t.Parallel()
tcpConn, err := net.Dial("tcp", "1.1.1.1:443") tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
require.NoError(t, err) require.NoError(t, err)
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, false, 0), &tls.Config{ tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), false, 0), &tls.Config{
ServerName: "www.cloudflare.com", ServerName: "www.cloudflare.com",
}) })
require.NoError(t, tlsConn.Handshake()) require.NoError(t, tlsConn.Handshake())
@ -25,17 +25,7 @@ func TestTLSRecordFragment(t *testing.T) {
t.Parallel() t.Parallel()
tcpConn, err := net.Dial("tcp", "1.1.1.1:443") tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
require.NoError(t, err) require.NoError(t, err)
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), false, true, 0), &tls.Config{ tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, 0), &tls.Config{
ServerName: "www.cloudflare.com",
})
require.NoError(t, tlsConn.Handshake())
}
func TestTLS2Fragment(t *testing.T) {
t.Parallel()
tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
require.NoError(t, err)
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, true, 0), &tls.Config{
ServerName: "www.cloudflare.com", ServerName: "www.cloudflare.com",
}) })
require.NoError(t, tlsConn.Handshake()) require.NoError(t, tlsConn.Handshake())

View File

@ -34,7 +34,6 @@ type Client struct {
disableCache bool disableCache bool
disableExpire bool disableExpire bool
independentCache bool independentCache bool
clientSubnet netip.Prefix
rdrc adapter.RDRCStore rdrc adapter.RDRCStore
initRDRCFunc func() adapter.RDRCStore initRDRCFunc func() adapter.RDRCStore
logger logger.ContextLogger logger logger.ContextLogger
@ -48,7 +47,6 @@ type ClientOptions struct {
DisableExpire bool DisableExpire bool
IndependentCache bool IndependentCache bool
CacheCapacity uint32 CacheCapacity uint32
ClientSubnet netip.Prefix
RDRC func() adapter.RDRCStore RDRC func() adapter.RDRCStore
Logger logger.ContextLogger Logger logger.ContextLogger
} }
@ -59,7 +57,6 @@ func NewClient(options ClientOptions) *Client {
disableCache: options.DisableCache, disableCache: options.DisableCache,
disableExpire: options.DisableExpire, disableExpire: options.DisableExpire,
independentCache: options.IndependentCache, independentCache: options.IndependentCache,
clientSubnet: options.ClientSubnet,
initRDRCFunc: options.RDRC, initRDRCFunc: options.RDRC,
logger: options.Logger, logger: options.Logger,
} }
@ -107,12 +104,8 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return &responseMessage, nil return &responseMessage, nil
} }
question := message.Question[0] question := message.Question[0]
clientSubnet := options.ClientSubnet if options.ClientSubnet.IsValid() {
if !clientSubnet.IsValid() { message = SetClientSubnet(message, options.ClientSubnet)
clientSubnet = c.clientSubnet
}
if clientSubnet.IsValid() {
message = SetClientSubnet(message, clientSubnet)
} }
isSimpleRequest := len(message.Question) == 1 && isSimpleRequest := len(message.Question) == 1 &&
len(message.Ns) == 0 && len(message.Ns) == 0 &&

View File

@ -55,7 +55,6 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
DisableExpire: options.DNSClientOptions.DisableExpire, DisableExpire: options.DNSClientOptions.DisableExpire,
IndependentCache: options.DNSClientOptions.IndependentCache, IndependentCache: options.DNSClientOptions.IndependentCache,
CacheCapacity: options.DNSClientOptions.CacheCapacity, CacheCapacity: options.DNSClientOptions.CacheCapacity,
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}),
RDRC: func() adapter.RDRCStore { RDRC: func() adapter.RDRCStore {
cacheFile := service.FromContext[adapter.CacheFile](ctx) cacheFile := service.FromContext[adapter.CacheFile](ctx)
if cacheFile == nil { if cacheFile == nil {

View File

@ -2,61 +2,6 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.12.0-beta.24
* Allow `tls_fragment` and `tls_record_fragment` to be enabled together **1**
* Also add fragment options for TLS client configuration **2**
* Fixes and improvements
**1**:
For debugging only, it is recommended to disable if record fragmentation works.
See [Route Action](/configuration/route/rule_action/#tls_fragment).
**2**:
See [TLS](/configuration/shared/tls/).
#### 1.12.0-beta.23
* Add loopback address support for tun **1**
* Add cache support for ssm-api **2**
* Fixes and improvements
**1**:
TUN now implements SideStore's StosVPN.
See [Tun](/configuration/inbound/tun/#loopback_address).
**2**:
See [SSM API Service](/configuration/service/ssm-api/#cache_path).
#### 1.12.0-beta.21
* Fix missing `home` option for DERP service **1**
* Fixes and improvements
**1**:
You can now choose what the DERP home page shows, just like with derper's `-home` flag.
See [DERP](/configuration/service/derp/#home).
### 1.11.13
* Fixes and improvements
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we
violated the rules (TestFlight users are not affected)._
#### 1.12.0-beta.17
* Update quic-go to v0.52.0
* Fixes and improvements
#### 1.12.0-beta.15 #### 1.12.0-beta.15
* Add DERP service **1** * Add DERP service **1**

View File

@ -1,11 +1,7 @@
--- ---
icon: material/alert-decagram icon: material/new-box
--- ---
!!! quote "Changes in sing-box 1.12.0"
:material-decagram: [servers](#servers)
!!! quote "Changes in sing-box 1.11.0" !!! quote "Changes in sing-box 1.11.0"
:material-plus: [cache_capacity](#cache_capacity) :material-plus: [cache_capacity](#cache_capacity)

View File

@ -1,11 +1,7 @@
--- ---
icon: material/alert-decagram icon: material/new-box
--- ---
!!! quote "sing-box 1.12.0 中的更改"
:material-decagram: [servers](#servers)
!!! quote "sing-box 1.11.0 中的更改" !!! quote "sing-box 1.11.0 中的更改"
:material-plus: [cache_capacity](#cache_capacity) :material-plus: [cache_capacity](#cache_capacity)

View File

@ -1,11 +1,7 @@
--- ---
icon: material/new-box icon: material/alert-decagram
--- ---
!!! quote "Changes in sing-box 1.12.0"
:material-plus: [loopback_address](#loopback_address)
!!! quote "Changes in sing-box 1.11.0" !!! quote "Changes in sing-box 1.11.0"
:material-delete-alert: [gso](#gso) :material-delete-alert: [gso](#gso)
@ -60,12 +56,9 @@ icon: material/new-box
"auto_route": true, "auto_route": true,
"iproute2_table_index": 2022, "iproute2_table_index": 2022,
"iproute2_rule_index": 9000, "iproute2_rule_index": 9000,
"auto_redirect": true, "auto_redirect": false,
"auto_redirect_input_mark": "0x2023", "auto_redirect_input_mark": "0x2023",
"auto_redirect_output_mark": "0x2024", "auto_redirect_output_mark": "0x2024",
"loopback_address": [
"10.0.7.1"
],
"strict_route": true, "strict_route": true,
"route_address": [ "route_address": [
"0.0.0.0/1", "0.0.0.0/1",
@ -73,6 +66,7 @@ icon: material/new-box
"::/1", "::/1",
"8000::/1" "8000::/1"
], ],
"route_exclude_address": [ "route_exclude_address": [
"192.168.0.0/16", "192.168.0.0/16",
"fc00::/7" "fc00::/7"
@ -123,6 +117,7 @@ icon: material/new-box
"match_domain": [] "match_domain": []
} }
}, },
// Deprecated // Deprecated
"gso": false, "gso": false,
"inet4_address": [ "inet4_address": [
@ -145,8 +140,8 @@ icon: material/new-box
"inet6_route_exclude_address": [ "inet6_route_exclude_address": [
"fc00::/7" "fc00::/7"
], ],
...
// Listen Fields ... // Listen Fields
} }
``` ```
@ -278,16 +273,6 @@ Connection output mark used by `auto_redirect`.
`0x2024` is used by default. `0x2024` is used by default.
#### loopback_address
!!! question "Since sing-box 1.12.0"
Loopback addresses make TCP connections to the specified address connect to the source address.
Setting option value to `10.0.7.1` achieves the same behavior as SideStore/StosVPN.
When `auto_redirect` is enabled, the same behavior can be achieved for LAN devices (not just local) as a gateway.
#### strict_route #### strict_route
Enforce strict routing rules when `auto_route` is enabled: Enforce strict routing rules when `auto_route` is enabled:

View File

@ -1,11 +1,7 @@
--- ---
icon: material/new-box icon: material/alert-decagram
--- ---
!!! quote "sing-box 1.12.0 中的更改"
:material-plus: [loopback_address](#loopback_address)
!!! quote "sing-box 1.11.0 中的更改" !!! quote "sing-box 1.11.0 中的更改"
:material-delete-alert: [gso](#gso) :material-delete-alert: [gso](#gso)
@ -60,12 +56,9 @@ icon: material/new-box
"auto_route": true, "auto_route": true,
"iproute2_table_index": 2022, "iproute2_table_index": 2022,
"iproute2_rule_index": 9000, "iproute2_rule_index": 9000,
"auto_redirect": true, "auto_redirect": false,
"auto_redirect_input_mark": "0x2023", "auto_redirect_input_mark": "0x2023",
"auto_redirect_output_mark": "0x2024", "auto_redirect_output_mark": "0x2024",
"loopback_address": [
"10.0.7.1"
],
"strict_route": true, "strict_route": true,
"route_address": [ "route_address": [
"0.0.0.0/1", "0.0.0.0/1",
@ -277,16 +270,6 @@ tun 接口的 IPv6 前缀。
默认使用 `0x2024` 默认使用 `0x2024`
#### loopback_address
!!! question "自 sing-box 1.12.0 起"
环回地址是用于使指向指定地址的 TCP 连接连接到来源地址的。
将选项值设置为 `10.0.7.1` 可实现与 SideStore/StosVPN 相同的行为。
当启用 `auto_redirect` 时,可以作为网关为局域网设备(而不仅仅是本地)实现相同的行为。
#### strict_route #### strict_route
当启用 `auto_route` 时,强制执行严格的路由规则: 当启用 `auto_route` 时,强制执行严格的路由规则:
@ -416,10 +399,10 @@ UDP NAT 过期时间。
TCP/IP 栈。 TCP/IP 栈。
| 栈 | 描述 | | 栈 | 描述 |
|----------|-------------------------------------------------------------------------------------------------------| |--------|------------------------------------------------------------------|
| `system` | 基于系统网络栈执行 L3 到 L4 转换 | | system | 基于系统网络栈执行 L3 到 L4 转换 |
| `gvisor` | 基于 [gVisor](https://github.com/google/gvisor) 虚拟网络栈执行 L3 到 L4 转换 | | gVisor | 基于 [gVisor](https://github.com/google/gvisor) 虚拟网络栈执行 L3 到 L4 转换 |
| `mixed` | 混合 `system` TCP 栈与 `gvisor` UDP 栈 | | mixed | 混合 `system` TCP 栈与 `gvisor` UDP 栈 |
默认使用 `mixed` 栈如果 gVisor 构建标记已启用,否则默认使用 `system` 栈。 默认使用 `mixed` 栈如果 gVisor 构建标记已启用,否则默认使用 `system` 栈。

View File

@ -172,12 +172,14 @@ and should not be used to circumvent real censorship.
Due to poor performance, try `tls_record_fragment` first, and only apply to server names known to be blocked. Due to poor performance, try `tls_record_fragment` first, and only apply to server names known to be blocked.
On Linux, Apple platforms, (administrator privileges required) Windows, On Linux, Apple platforms, (administrator privileges required) Windows,
the wait time can be automatically detected. Otherwise, it will fall back to the wait time can be automatically detected, otherwise it will fall back to
waiting for a fixed time specified by `tls_fragment_fallback_delay`. waiting for a fixed time specified by `tls_fragment_fallback_delay`.
In addition, if the actual wait time is less than 20ms, it will also fall back to waiting for a fixed time, In addition, if the actual wait time is less than 20ms, it will also fall back to waiting for a fixed time,
because the target is considered to be local or behind a transparent proxy. because the target is considered to be local or behind a transparent proxy.
Conflict with `tls_record_fragment`.
#### tls_fragment_fallback_delay #### tls_fragment_fallback_delay
!!! question "Since sing-box 1.12.0" !!! question "Since sing-box 1.12.0"
@ -192,6 +194,11 @@ The fallback value used when TLS segmentation cannot automatically determine the
Fragment TLS handshake into multiple TLS records to bypass firewalls. Fragment TLS handshake into multiple TLS records to bypass firewalls.
This feature is intended to circumvent simple firewalls based on **plaintext packet matching**,
and should not be used to circumvent real censorship.
Conflict with `tls_fragment`.
### sniff ### sniff
```json ```json

View File

@ -170,6 +170,8 @@ UDP 连接超时时间。
此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。 此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。
`tls_record_fragment` 冲突。
#### tls_fragment_fallback_delay #### tls_fragment_fallback_delay
!!! question "自 sing-box 1.12.0 起" !!! question "自 sing-box 1.12.0 起"
@ -184,6 +186,10 @@ UDP 连接超时时间。
通过分段 TLS 握手数据包到多个 TLS 记录来绕过防火墙检测。 通过分段 TLS 握手数据包到多个 TLS 记录来绕过防火墙检测。
此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。
`tls_fragment` 冲突。
### sniff ### sniff
```json ```json

View File

@ -20,7 +20,6 @@ DERP service is a Tailscale DERP server, similar to [derper](https://pkg.go.dev/
"config_path": "", "config_path": "",
"verify_client_endpoint": [], "verify_client_endpoint": [],
"verify_client_url": [], "verify_client_url": [],
"home": "",
"mesh_with": [], "mesh_with": [],
"mesh_psk": "", "mesh_psk": "",
"mesh_psk_file": "", "mesh_psk_file": "",
@ -70,10 +69,6 @@ Setting Array value to a string `__URL__` is equivalent to configuring:
{ "url": __URL__ } { "url": __URL__ }
``` ```
#### home
What to serve at the root path. It may be left empty (the default, for a default homepage), `blank` for a blank page, or a URL to redirect to
#### mesh_with #### mesh_with
Mesh with other DERP servers. Mesh with other DERP servers.

View File

@ -10,7 +10,7 @@ icon: material/new-box
```json ```json
{ {
"services": [ "endpoints": [
{ {
"type": "", "type": "",
"tag": "" "tag": ""
@ -25,7 +25,6 @@ icon: material/new-box
|------------|------------------------| |------------|------------------------|
| `derp` | [DERP](./derp) | | `derp` | [DERP](./derp) |
| `resolved` | [Resolved](./resolved) | | `resolved` | [Resolved](./resolved) |
| `ssm-api` | [SSM API](./ssm-api) |
#### tag #### tag

View File

@ -19,7 +19,6 @@ See https://github.com/Shadowsocks-NET/shadowsocks-specs/blob/main/2023-1-shadow
... // Listen Fields ... // Listen Fields
"servers": {}, "servers": {},
"cache_path": "",
"tls": {} "tls": {}
} }
``` ```
@ -34,9 +33,7 @@ See [Listen Fields](/configuration/shared/listen/) for details.
==Required== ==Required==
A mapping Object from HTTP endpoints to [Shadowsocks Inbound](/configuration/inbound/shadowsocks) tags. A mapping Object from HTTP endpoints to Shadowsocks inbound tags.
Selected Shadowsocks inbounds must be configured with [managed](/configuration/inbound/shadowsocks#managed) enabled.
Example: Example:
@ -48,11 +45,6 @@ Example:
} }
``` ```
#### cache_path
If set, when the server is about to stop, traffic and user state will be saved to the specified JSON file
to be restored on the next startup.
#### tls #### tls
TLS configuration, see [TLS](/configuration/shared/tls/#inbound). TLS configuration, see [TLS](/configuration/shared/tls/#inbound).

View File

@ -4,9 +4,6 @@ icon: material/alert-decagram
!!! quote "Changes in sing-box 1.12.0" !!! quote "Changes in sing-box 1.12.0"
:material-plus: [fragment](#fragment)
:material-plus: [fragment_fallback_delay](#fragment_fallback_delay)
:material-plus: [record_fragment](#record_fragment)
:material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled) :material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled)
:material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled) :material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled)
@ -85,9 +82,6 @@ icon: material/alert-decagram
"cipher_suites": [], "cipher_suites": [],
"certificate": "", "certificate": "",
"certificate_path": "", "certificate_path": "",
"fragment": false,
"fragment_fallback_delay": "",
"record_fragment": false,
"ech": { "ech": {
"enabled": false, "enabled": false,
"config": [], "config": [],
@ -319,44 +313,6 @@ The path to ECH configuration, in PEM format.
If empty, load from DNS will be attempted. If empty, load from DNS will be attempted.
#### fragment
!!! question "Since sing-box 1.12.0"
==Client only==
Fragment TLS handshakes to bypass firewalls.
This feature is intended to circumvent simple firewalls based on **plaintext packet matching**,
and should not be used to circumvent real censorship.
Due to poor performance, try `record_fragment` first, and only apply to server names known to be blocked.
On Linux, Apple platforms, (administrator privileges required) Windows,
the wait time can be automatically detected. Otherwise, it will fall back to
waiting for a fixed time specified by `fragment_fallback_delay`.
In addition, if the actual wait time is less than 20ms, it will also fall back to waiting for a fixed time,
because the target is considered to be local or behind a transparent proxy.
#### fragment_fallback_delay
!!! question "Since sing-box 1.12.0"
==Client only==
The fallback value used when TLS segmentation cannot automatically determine the wait time.
`500ms` is used by default.
#### record_fragment
!!! question "Since sing-box 1.12.0"
==Client only==
Fragment TLS handshake into multiple TLS records to bypass firewalls.
### ACME Fields ### ACME Fields
#### domain #### domain

View File

@ -4,9 +4,6 @@ icon: material/alert-decagram
!!! quote "sing-box 1.12.0 中的更改" !!! quote "sing-box 1.12.0 中的更改"
:material-plus: [tls_fragment](#tls_fragment)
:material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay)
:material-plus: [tls_record_fragment](#tls_record_fragment)
:material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled) :material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled)
:material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled) :material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled)
@ -85,9 +82,6 @@ icon: material/alert-decagram
"cipher_suites": [], "cipher_suites": [],
"certificate": [], "certificate": [],
"certificate_path": "", "certificate_path": "",
"fragment": false,
"fragment_fallback_delay": "",
"record_fragment": false,
"ech": { "ech": {
"enabled": false, "enabled": false,
"pq_signature_schemes_enabled": false, "pq_signature_schemes_enabled": false,
@ -311,41 +305,6 @@ ECH PEM 配置路径
如果为 true则始终使用最大可能的 TLS 记录大小。 如果为 true则始终使用最大可能的 TLS 记录大小。
如果为 false则可能会调整 TLS 记录的大小以尝试改善延迟。 如果为 false则可能会调整 TLS 记录的大小以尝试改善延迟。
#### tls_fragment
!!! question "自 sing-box 1.12.0 起"
==仅客户端==
通过分段 TLS 握手数据包来绕过防火墙检测。
此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。
由于性能不佳,请首先尝试 `tls_record_fragment`,且仅应用于已知被阻止的服务器名称。
在 Linux、Apple 平台和需要管理员权限的 Windows 系统上,可自动检测等待时间。
若无法自动检测,将回退使用 `tls_fragment_fallback_delay` 指定的固定等待时间。
此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。
#### tls_fragment_fallback_delay
!!! question "自 sing-box 1.12.0 起"
==仅客户端==
当 TLS 分片功能无法自动判定等待时间时使用的回退值。
默认使用 `500ms`
#### tls_record_fragment
==仅客户端==
!!! question "自 sing-box 1.12.0 起"
通过分段 TLS 握手数据包到多个 TLS 记录来绕过防火墙检测。
### ACME 字段 ### ACME 字段
#### domain #### domain

16
go.mod
View File

@ -10,7 +10,6 @@ require (
github.com/cretz/bine v0.2.0 github.com/cretz/bine v0.2.0
github.com/go-chi/chi/v5 v5.2.1 github.com/go-chi/chi/v5 v5.2.1
github.com/go-chi/render v1.0.3 github.com/go-chi/render v1.0.3
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
github.com/gofrs/uuid/v5 v5.3.2 github.com/gofrs/uuid/v5 v5.3.2
github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f
github.com/libdns/alidns v1.0.4-libdns.v1.beta1 github.com/libdns/alidns v1.0.4-libdns.v1.beta1
@ -27,15 +26,15 @@ require (
github.com/sagernet/fswatch v0.1.1 github.com/sagernet/fswatch v0.1.1
github.com/sagernet/gomobile v0.1.6 github.com/sagernet/gomobile v0.1.6
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.51.0-beta.5
github.com/sagernet/sing v0.6.11-0.20250521033217-30d675ea099b github.com/sagernet/sing v0.6.10-0.20250505040842-ba62fee9470f
github.com/sagernet/sing-mux v0.3.2 github.com/sagernet/sing-mux v0.3.2
github.com/sagernet/sing-quic v0.5.0-beta.2 github.com/sagernet/sing-quic v0.4.1-0.20250511050139-d459f561c9c3
github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks v0.2.7
github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowsocks2 v0.2.0
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.6.6-0.20250610083027-da0a50057fb5 github.com/sagernet/sing-tun v0.6.6-0.20250428031943-0686f8c4f210
github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88 github.com/sagernet/sing-vmess v0.2.2-0.20250503051933-9b4cf17393f8
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-mod.5
github.com/sagernet/wireguard-go v0.0.1-beta.7 github.com/sagernet/wireguard-go v0.0.1-beta.7
@ -79,6 +78,7 @@ require (
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect

28
go.sum
View File

@ -165,25 +165,25 @@ 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.51.0-beta.5 h1:/mME3sJvQ8k/JKP0oC/9XoWrm0znO7hWXviB5yiipJY=
github.com/sagernet/quic-go v0.52.0-beta.1/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4= github.com/sagernet/quic-go v0.51.0-beta.5/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.6.11-0.20250521033217-30d675ea099b h1:ZjTCYPb5f7aHdf1UpUvE22dVmf7BL8eQ/zLZhjgh7Wo= github.com/sagernet/sing v0.6.10-0.20250505040842-ba62fee9470f h1:lttLhNtFuMItQcTD29QP6aBS8kR1UhG7zZ+pwzTYkFM=
github.com/sagernet/sing v0.6.11-0.20250521033217-30d675ea099b/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.6.10-0.20250505040842-ba62fee9470f/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.2 h1:meZVFiiStvHThb/trcpAkCrmtJOuItG5Dzl1RRP5/NE= github.com/sagernet/sing-mux v0.3.2 h1:meZVFiiStvHThb/trcpAkCrmtJOuItG5Dzl1RRP5/NE=
github.com/sagernet/sing-mux v0.3.2/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA= github.com/sagernet/sing-mux v0.3.2/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
github.com/sagernet/sing-quic v0.5.0-beta.2 h1:j7KAbBuGmsKwSxVAQL5soJ+wDqxim4/llK2kxB0hSKk= github.com/sagernet/sing-quic v0.4.1-0.20250511050139-d459f561c9c3 h1:1J+s1yyZ8+YAYaClI+az8YuFgV9NGXUUCZnriKmos6w=
github.com/sagernet/sing-quic v0.5.0-beta.2/go.mod h1:SAv/qdeDN+75msGG5U5ZIwG+3Ua50jVIKNrRSY8pkx0= github.com/sagernet/sing-quic v0.4.1-0.20250511050139-d459f561c9c3/go.mod h1:Mv7CdSyLepmqoLT8rd88Qn3QMv5AbsgjEm3DvEhDVNE=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg=
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowsocks2 v0.2.0/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.6.6-0.20250610083027-da0a50057fb5 h1:zlcioVa11g8VLz5L0yPG7PbvQrw7mrxkDDdlMPEgqDk= github.com/sagernet/sing-tun v0.6.6-0.20250428031943-0686f8c4f210 h1:6H4BZaTqKI3YcDMyTV3E576LuJM4S4wY99xoq2T1ECw=
github.com/sagernet/sing-tun v0.6.6-0.20250610083027-da0a50057fb5/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE= github.com/sagernet/sing-tun v0.6.6-0.20250428031943-0686f8c4f210/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88 h1:0pVm8sPOel+BoiCddW3pV3cKDKEaSioVTYDdTSKjyFI= github.com/sagernet/sing-vmess v0.2.2-0.20250503051933-9b4cf17393f8 h1:zW+zAOCxUIqBCgnZiPovt1uQ3S+zBS+w0NGp+1zITGA=
github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88/go.mod h1:IL8Rr+EGwuqijszZkNrEFTQDKhilEpkqFqOlvdpS6/w= github.com/sagernet/sing-vmess v0.2.2-0.20250503051933-9b4cf17393f8/go.mod h1:IL8Rr+EGwuqijszZkNrEFTQDKhilEpkqFqOlvdpS6/w=
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-mod.5 h1:7V7z+p2C//TGtff20pPnDCt3qP6uFyY62peJoKF9z/A=

View File

@ -3,7 +3,6 @@ package include
import ( import (
"context" "context"
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/inbound"
@ -40,10 +39,6 @@ import (
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
func Context(ctx context.Context) context.Context {
return box.Context(ctx, InboundRegistry(), OutboundRegistry(), EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry())
}
func InboundRegistry() *inbound.Registry { func InboundRegistry() *inbound.Registry {
registry := inbound.NewRegistry() registry := inbound.NewRegistry()

View File

@ -7,6 +7,5 @@ import (
type SSMAPIServiceOptions struct { type SSMAPIServiceOptions struct {
ListenOptions ListenOptions
Servers *badjson.TypedMap[string, string] `json:"servers"` Servers *badjson.TypedMap[string, string] `json:"servers"`
CachePath string `json:"cache_path,omitempty"`
InboundTLSOptionsContainer InboundTLSOptionsContainer
} }

View File

@ -36,7 +36,6 @@ type DERPServiceOptions struct {
ConfigPath string `json:"config_path,omitempty"` ConfigPath string `json:"config_path,omitempty"`
VerifyClientEndpoint badoption.Listable[string] `json:"verify_client_endpoint,omitempty"` VerifyClientEndpoint badoption.Listable[string] `json:"verify_client_endpoint,omitempty"`
VerifyClientURL badoption.Listable[*DERPVerifyClientURLOptions] `json:"verify_client_url,omitempty"` VerifyClientURL badoption.Listable[*DERPVerifyClientURLOptions] `json:"verify_client_url,omitempty"`
Home string `json:"home,omitempty"`
MeshWith badoption.Listable[*DERPMeshOptions] `json:"mesh_with,omitempty"` MeshWith badoption.Listable[*DERPMeshOptions] `json:"mesh_with,omitempty"`
MeshPSK string `json:"mesh_psk,omitempty"` MeshPSK string `json:"mesh_psk,omitempty"`
MeshPSKFile string `json:"mesh_psk_file,omitempty"` MeshPSKFile string `json:"mesh_psk_file,omitempty"`

View File

@ -47,9 +47,6 @@ type OutboundTLSOptions struct {
CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"` CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"`
Certificate badoption.Listable[string] `json:"certificate,omitempty"` Certificate badoption.Listable[string] `json:"certificate,omitempty"`
CertificatePath string `json:"certificate_path,omitempty"` CertificatePath string `json:"certificate_path,omitempty"`
Fragment bool `json:"fragment,omitempty"`
FragmentFallbackDelay badoption.Duration `json:"fragment_fallback_delay,omitempty"`
RecordFragment bool `json:"record_fragment,omitempty"`
ECH *OutboundECHOptions `json:"ech,omitempty"` ECH *OutboundECHOptions `json:"ech,omitempty"`
UTLS *OutboundUTLSOptions `json:"utls,omitempty"` UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
Reality *OutboundRealityOptions `json:"reality,omitempty"` Reality *OutboundRealityOptions `json:"reality,omitempty"`

View File

@ -20,7 +20,6 @@ type TunInboundOptions struct {
AutoRedirect bool `json:"auto_redirect,omitempty"` AutoRedirect bool `json:"auto_redirect,omitempty"`
AutoRedirectInputMark FwMark `json:"auto_redirect_input_mark,omitempty"` AutoRedirectInputMark FwMark `json:"auto_redirect_input_mark,omitempty"`
AutoRedirectOutputMark FwMark `json:"auto_redirect_output_mark,omitempty"` AutoRedirectOutputMark FwMark `json:"auto_redirect_output_mark,omitempty"`
LoopbackAddress badoption.Listable[netip.Addr] `json:"loopback_address,omitempty"`
StrictRoute bool `json:"strict_route,omitempty"` StrictRoute bool `json:"strict_route,omitempty"`
RouteAddress badoption.Listable[netip.Prefix] `json:"route_address,omitempty"` RouteAddress badoption.Listable[netip.Prefix] `json:"route_address,omitempty"`
RouteAddressSet badoption.Listable[string] `json:"route_address_set,omitempty"` RouteAddressSet badoption.Listable[string] `json:"route_address_set,omitempty"`

View File

@ -221,14 +221,6 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
} }
ipStack := t.server.ExportNetstack().ExportIPStack() ipStack := t.server.ExportNetstack().ExportIPStack()
gErr := ipStack.SetSpoofing(tun.DefaultNIC, true)
if gErr != nil {
return gonet.TranslateNetstackError(gErr)
}
gErr = ipStack.SetPromiscuousMode(tun.DefaultNIC, true)
if gErr != nil {
return gonet.TranslateNetstackError(gErr)
}
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(t.ctx, ipStack, t).HandlePacket) ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(t.ctx, ipStack, t).HandlePacket)
udpForwarder := tun.NewUDPForwarder(t.ctx, ipStack, t, t.udpTimeout) udpForwarder := tun.NewUDPForwarder(t.ctx, ipStack, t, t.udpTimeout)
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket) ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)

View File

@ -190,8 +190,6 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
IPRoute2RuleIndex: ruleIndex, IPRoute2RuleIndex: ruleIndex,
AutoRedirectInputMark: inputMark, AutoRedirectInputMark: inputMark,
AutoRedirectOutputMark: outputMark, AutoRedirectOutputMark: outputMark,
Inet4LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is4),
Inet6LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is6),
StrictRoute: options.StrictRoute, StrictRoute: options.StrictRoute,
IncludeInterface: options.IncludeInterface, IncludeInterface: options.IncludeInterface,
ExcludeInterface: options.ExcludeInterface, ExcludeInterface: options.ExcludeInterface,
@ -216,6 +214,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
if !loaded { if !loaded {
return nil, E.New("parse route_address_set: rule-set not found: ", routeAddressSet) return nil, E.New("parse route_address_set: rule-set not found: ", routeAddressSet)
} }
ruleSet.IncRef()
inbound.routeRuleSet = append(inbound.routeRuleSet, ruleSet) inbound.routeRuleSet = append(inbound.routeRuleSet, ruleSet)
} }
for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet { for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet {
@ -223,6 +222,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
if !loaded { if !loaded {
return nil, E.New("parse route_exclude_address_set: rule-set not found: ", routeExcludeAddressSet) return nil, E.New("parse route_exclude_address_set: rule-set not found: ", routeExcludeAddressSet)
} }
ruleSet.IncRef()
inbound.routeExcludeRuleSet = append(inbound.routeExcludeRuleSet, ruleSet) inbound.routeExcludeRuleSet = append(inbound.routeExcludeRuleSet, ruleSet)
} }
if options.AutoRedirect { if options.AutoRedirect {
@ -312,7 +312,7 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
if len(ipSets) == 0 { if len(ipSets) == 0 {
t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeRuleSet.Name()) t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeRuleSet.Name())
} }
routeRuleSet.IncRef() routeRuleSet.DecRef()
t.routeAddressSet = append(t.routeAddressSet, ipSets...) t.routeAddressSet = append(t.routeAddressSet, ipSets...)
if t.autoRedirect != nil { if t.autoRedirect != nil {
t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet)) t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet))
@ -324,7 +324,7 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
if len(ipSets) == 0 { if len(ipSets) == 0 {
t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeExcludeRuleSet.Name()) t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeExcludeRuleSet.Name())
} }
routeExcludeRuleSet.IncRef() routeExcludeRuleSet.DecRef()
t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ipSets...) t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ipSets...)
if t.autoRedirect != nil { if t.autoRedirect != nil {
t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet)) t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet))

View File

@ -1,3 +0,0 @@
#!/bin/sh
systemd-sysusers sing-box.conf

View File

@ -90,8 +90,14 @@ func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, co
m.logger.ErrorContext(ctx, err) m.logger.ErrorContext(ctx, err)
return return
} }
if metadata.TLSFragment || metadata.TLSRecordFragment { if metadata.TLSFragment {
remoteConn = tf.NewConn(remoteConn, ctx, metadata.TLSFragment, metadata.TLSRecordFragment, metadata.TLSFragmentFallbackDelay) fallbackDelay := metadata.TLSFragmentFallbackDelay
if fallbackDelay == 0 {
fallbackDelay = C.TLSFragmentFallbackDelay
}
remoteConn = tf.NewConn(remoteConn, ctx, false, fallbackDelay)
} else if metadata.TLSRecordFragment {
remoteConn = tf.NewConn(remoteConn, ctx, true, 0)
} }
m.access.Lock() m.access.Lock()
element := m.connections.PushBack(conn) element := m.connections.PushBack(conn)

View File

@ -36,7 +36,7 @@ func (r *Router) hijackDNSStream(ctx context.Context, conn net.Conn, metadata ad
} }
} }
func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext) error {
if natConn, isNatConn := conn.(udpnat.Conn); isNatConn { if natConn, isNatConn := conn.(udpnat.Conn); isNatConn {
metadata.Destination = M.Socksaddr{} metadata.Destination = M.Socksaddr{}
for _, packet := range packetBuffers { for _, packet := range packetBuffers {
@ -51,12 +51,10 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB
conn: conn, conn: conn,
ctx: ctx, ctx: ctx,
metadata: metadata, metadata: metadata,
onClose: onClose,
}) })
return nil return nil
} }
err := dnsOutbound.NewDNSPacketConnection(ctx, r.dns, conn, packetBuffers, metadata) err := dnsOutbound.NewDNSPacketConnection(ctx, r.dns, conn, packetBuffers, metadata)
N.CloseOnHandshakeFailure(conn, onClose, err)
if err != nil && !E.IsClosedOrCanceled(err) { if err != nil && !E.IsClosedOrCanceled(err) {
return E.Cause(err, "process DNS packet") return E.Cause(err, "process DNS packet")
} }
@ -95,16 +93,8 @@ type dnsHijacker struct {
conn N.PacketConn conn N.PacketConn
ctx context.Context ctx context.Context
metadata adapter.InboundContext metadata adapter.InboundContext
onClose N.CloseHandlerFunc
} }
func (h *dnsHijacker) NewPacketEx(buffer *buf.Buffer, destination M.Socksaddr) { func (h *dnsHijacker) NewPacketEx(buffer *buf.Buffer, destination M.Socksaddr) {
go ExchangeDNSPacket(h.ctx, h.router, h.logger, h.conn, buffer, h.metadata, destination) go ExchangeDNSPacket(h.ctx, h.router, h.logger, h.conn, buffer, h.metadata, destination)
} }
func (h *dnsHijacker) Close() error {
if h.onClose != nil {
h.onClose(nil)
}
return nil
}

View File

@ -117,8 +117,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
for _, buffer := range buffers { for _, buffer := range buffers {
conn = bufio.NewCachedConn(conn, buffer) conn = bufio.NewCachedConn(conn, buffer)
} }
N.CloseOnHandshakeFailure(conn, onClose, r.hijackDNSStream(ctx, conn, metadata)) return r.hijackDNSStream(ctx, conn, metadata)
return nil
} }
} }
if selectedRule == nil { if selectedRule == nil {
@ -173,6 +172,8 @@ func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn,
} else { } else {
r.logger.ErrorContext(ctx, err) r.logger.ErrorContext(ctx, err)
} }
} else if onClose != nil {
onClose(nil)
} }
} }
@ -229,7 +230,8 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
N.ReleaseMultiPacketBuffer(packetBuffers) N.ReleaseMultiPacketBuffer(packetBuffers)
return action.Error(ctx) return action.Error(ctx)
case *rule.RuleActionHijackDNS: case *rule.RuleActionHijackDNS:
return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose) return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata)
} }
} }
if selectedRule == nil || selectReturn { if selectedRule == nil || selectReturn {
@ -499,9 +501,7 @@ func (r *Router) actionSniff(
return return
} }
if inputConn != nil { if inputConn != nil {
if len(action.StreamSniffers) == 0 && len(action.PacketSniffers) > 0 { sniffBuffer := buf.NewPacket()
return
}
var streamSniffers []sniff.StreamSniffer var streamSniffers []sniff.StreamSniffer
if len(action.StreamSniffers) > 0 { if len(action.StreamSniffers) > 0 {
streamSniffers = action.StreamSniffers streamSniffers = action.StreamSniffers
@ -515,7 +515,6 @@ func (r *Router) actionSniff(
sniff.RDP, sniff.RDP,
} }
} }
sniffBuffer := buf.NewPacket()
err := sniff.PeekStream( err := sniff.PeekStream(
ctx, ctx,
metadata, metadata,
@ -547,26 +546,10 @@ func (r *Router) actionSniff(
sniffBuffer.Release() sniffBuffer.Release()
} }
} else if inputPacketConn != nil { } else if inputPacketConn != nil {
if len(action.PacketSniffers) == 0 && len(action.StreamSniffers) > 0 { if metadata.PacketSniffError != nil && !errors.Is(metadata.PacketSniffError, sniff.ErrNeedMoreData) {
return
} else if metadata.PacketSniffError != nil && !errors.Is(metadata.PacketSniffError, sniff.ErrNeedMoreData) {
r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.PacketSniffError) r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.PacketSniffError)
return return
} }
var packetSniffers []sniff.PacketSniffer
if len(action.PacketSniffers) > 0 {
packetSniffers = action.PacketSniffers
} else {
packetSniffers = []sniff.PacketSniffer{
sniff.DomainNameQuery,
sniff.QUICClientHello,
sniff.STUNMessage,
sniff.UTP,
sniff.UDPTracker,
sniff.DTLSRecord,
sniff.NTP,
}
}
for { for {
var ( var (
sniffBuffer = buf.NewPacket() sniffBuffer = buf.NewPacket()
@ -606,6 +589,20 @@ func (r *Router) actionSniff(
sniff.QUICClientHello, sniff.QUICClientHello,
) )
} else { } else {
var packetSniffers []sniff.PacketSniffer
if len(action.PacketSniffers) > 0 {
packetSniffers = action.PacketSniffers
} else {
packetSniffers = []sniff.PacketSniffer{
sniff.DomainNameQuery,
sniff.QUICClientHello,
sniff.STUNMessage,
sniff.UTP,
sniff.UDPTracker,
sniff.DTLSRecord,
sniff.NTP,
}
}
err = sniff.PeekPacket( err = sniff.PeekPacket(
ctx, metadata, ctx, metadata,
sniffBuffer.Bytes(), sniffBuffer.Bytes(),

View File

@ -124,7 +124,6 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
configPath: configPath, configPath: configPath,
verifyClientEndpoint: options.VerifyClientEndpoint, verifyClientEndpoint: options.VerifyClientEndpoint,
verifyClientURL: options.VerifyClientURL, verifyClientURL: options.VerifyClientURL,
home: options.Home,
meshKey: options.MeshPSK, meshKey: options.MeshPSK,
meshKeyPath: options.MeshPSKFile, meshKeyPath: options.MeshPSKFile,
meshWith: options.MeshWith, meshWith: options.MeshWith,
@ -134,7 +133,7 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
func (d *Service) Start(stage adapter.StartStage) error { func (d *Service) Start(stage adapter.StartStage) error {
switch stage { switch stage {
case adapter.StartStateStart: case adapter.StartStateStart:
config, err := readDERPConfig(filemanager.BasePath(d.ctx, d.configPath)) config, err := readDERPConfig(d.configPath)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,222 +0,0 @@
package ssmapi
import (
"bytes"
"os"
"path/filepath"
"sort"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/service/filemanager"
)
type Cache struct {
Endpoints *badjson.TypedMap[string, *EndpointCache] `json:"endpoints"`
}
type EndpointCache struct {
GlobalUplink int64 `json:"global_uplink"`
GlobalDownlink int64 `json:"global_downlink"`
GlobalUplinkPackets int64 `json:"global_uplink_packets"`
GlobalDownlinkPackets int64 `json:"global_downlink_packets"`
GlobalTCPSessions int64 `json:"global_tcp_sessions"`
GlobalUDPSessions int64 `json:"global_udp_sessions"`
UserUplink *badjson.TypedMap[string, int64] `json:"user_uplink"`
UserDownlink *badjson.TypedMap[string, int64] `json:"user_downlink"`
UserUplinkPackets *badjson.TypedMap[string, int64] `json:"user_uplink_packets"`
UserDownlinkPackets *badjson.TypedMap[string, int64] `json:"user_downlink_packets"`
UserTCPSessions *badjson.TypedMap[string, int64] `json:"user_tcp_sessions"`
UserUDPSessions *badjson.TypedMap[string, int64] `json:"user_udp_sessions"`
Users *badjson.TypedMap[string, string] `json:"users"`
}
func (s *Service) loadCache() error {
if s.cachePath == "" {
return nil
}
basePath := filemanager.BasePath(s.ctx, s.cachePath)
cacheBinary, err := os.ReadFile(basePath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
err = s.decodeCache(cacheBinary)
if err != nil {
os.RemoveAll(basePath)
return err
}
return nil
}
func (s *Service) saveCache() error {
if s.cachePath == "" {
return nil
}
basePath := filemanager.BasePath(s.ctx, s.cachePath)
err := os.MkdirAll(filepath.Dir(basePath), 0o777)
if err != nil {
return err
}
cacheBinary, err := s.encodeCache()
if err != nil {
return err
}
return os.WriteFile(s.cachePath, cacheBinary, 0o644)
}
func (s *Service) decodeCache(cacheBinary []byte) error {
if len(cacheBinary) == 0 {
return nil
}
cache, err := json.UnmarshalExtended[*Cache](cacheBinary)
if err != nil {
return err
}
if cache.Endpoints == nil || cache.Endpoints.Size() == 0 {
return nil
}
for _, entry := range cache.Endpoints.Entries() {
trafficManager, loaded := s.traffics[entry.Key]
if !loaded {
continue
}
trafficManager.globalUplink.Store(entry.Value.GlobalUplink)
trafficManager.globalDownlink.Store(entry.Value.GlobalDownlink)
trafficManager.globalUplinkPackets.Store(entry.Value.GlobalUplinkPackets)
trafficManager.globalDownlinkPackets.Store(entry.Value.GlobalDownlinkPackets)
trafficManager.globalTCPSessions.Store(entry.Value.GlobalTCPSessions)
trafficManager.globalUDPSessions.Store(entry.Value.GlobalUDPSessions)
trafficManager.userUplink = typedAtomicInt64Map(entry.Value.UserUplink)
trafficManager.userDownlink = typedAtomicInt64Map(entry.Value.UserDownlink)
trafficManager.userUplinkPackets = typedAtomicInt64Map(entry.Value.UserUplinkPackets)
trafficManager.userDownlinkPackets = typedAtomicInt64Map(entry.Value.UserDownlinkPackets)
trafficManager.userTCPSessions = typedAtomicInt64Map(entry.Value.UserTCPSessions)
trafficManager.userUDPSessions = typedAtomicInt64Map(entry.Value.UserUDPSessions)
userManager, loaded := s.users[entry.Key]
if !loaded {
continue
}
userManager.usersMap = typedMap(entry.Value.Users)
_ = userManager.postUpdate(false)
}
return nil
}
func (s *Service) encodeCache() ([]byte, error) {
endpoints := new(badjson.TypedMap[string, *EndpointCache])
for tag, traffic := range s.traffics {
var (
userUplink = new(badjson.TypedMap[string, int64])
userDownlink = new(badjson.TypedMap[string, int64])
userUplinkPackets = new(badjson.TypedMap[string, int64])
userDownlinkPackets = new(badjson.TypedMap[string, int64])
userTCPSessions = new(badjson.TypedMap[string, int64])
userUDPSessions = new(badjson.TypedMap[string, int64])
userMap = new(badjson.TypedMap[string, string])
)
for user, uplink := range traffic.userUplink {
if uplink.Load() > 0 {
userUplink.Put(user, uplink.Load())
}
}
for user, downlink := range traffic.userDownlink {
if downlink.Load() > 0 {
userDownlink.Put(user, downlink.Load())
}
}
for user, uplinkPackets := range traffic.userUplinkPackets {
if uplinkPackets.Load() > 0 {
userUplinkPackets.Put(user, uplinkPackets.Load())
}
}
for user, downlinkPackets := range traffic.userDownlinkPackets {
if downlinkPackets.Load() > 0 {
userDownlinkPackets.Put(user, downlinkPackets.Load())
}
}
for user, tcpSessions := range traffic.userTCPSessions {
if tcpSessions.Load() > 0 {
userTCPSessions.Put(user, tcpSessions.Load())
}
}
for user, udpSessions := range traffic.userUDPSessions {
if udpSessions.Load() > 0 {
userUDPSessions.Put(user, udpSessions.Load())
}
}
userManager := s.users[tag]
if userManager != nil && len(userManager.usersMap) > 0 {
userMap = new(badjson.TypedMap[string, string])
for username, password := range userManager.usersMap {
if username != "" && password != "" {
userMap.Put(username, password)
}
}
}
endpoints.Put(tag, &EndpointCache{
GlobalUplink: traffic.globalUplink.Load(),
GlobalDownlink: traffic.globalDownlink.Load(),
GlobalUplinkPackets: traffic.globalUplinkPackets.Load(),
GlobalDownlinkPackets: traffic.globalDownlinkPackets.Load(),
GlobalTCPSessions: traffic.globalTCPSessions.Load(),
GlobalUDPSessions: traffic.globalUDPSessions.Load(),
UserUplink: sortTypedMap(userUplink),
UserDownlink: sortTypedMap(userDownlink),
UserUplinkPackets: sortTypedMap(userUplinkPackets),
UserDownlinkPackets: sortTypedMap(userDownlinkPackets),
UserTCPSessions: sortTypedMap(userTCPSessions),
UserUDPSessions: sortTypedMap(userUDPSessions),
Users: sortTypedMap(userMap),
})
}
var buffer bytes.Buffer
encoder := json.NewEncoder(&buffer)
encoder.SetIndent("", " ")
err := encoder.Encode(&Cache{
Endpoints: sortTypedMap(endpoints),
})
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func sortTypedMap[T comparable](trafficMap *badjson.TypedMap[string, T]) *badjson.TypedMap[string, T] {
if trafficMap == nil {
return nil
}
keys := trafficMap.Keys()
sort.Strings(keys)
sortedMap := new(badjson.TypedMap[string, T])
for _, key := range keys {
value, _ := trafficMap.Get(key)
sortedMap.Put(key, value)
}
return sortedMap
}
func typedAtomicInt64Map(trafficMap *badjson.TypedMap[string, int64]) map[string]*atomic.Int64 {
result := make(map[string]*atomic.Int64)
if trafficMap != nil {
for _, entry := range trafficMap.Entries() {
counter := new(atomic.Int64)
counter.Store(entry.Value)
result[entry.Key] = counter
}
}
return result
}
func typedMap[T comparable](trafficMap *badjson.TypedMap[string, T]) map[string]T {
result := make(map[string]T)
if trafficMap != nil {
for _, entry := range trafficMap.Entries() {
result[entry.Key] = entry.Value
}
}
return result
}

View File

@ -33,9 +33,6 @@ type Service struct {
listener *listener.Listener listener *listener.Listener
tlsConfig tls.ServerConfig tlsConfig tls.ServerConfig
httpServer *http.Server httpServer *http.Server
traffics map[string]*TrafficManager
users map[string]*UserManager
cachePath string
} }
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.SSMAPIServiceOptions) (adapter.Service, error) { func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.SSMAPIServiceOptions) (adapter.Service, error) {
@ -53,9 +50,6 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
httpServer: &http.Server{ httpServer: &http.Server{
Handler: chiRouter, Handler: chiRouter,
}, },
traffics: make(map[string]*TrafficManager),
users: make(map[string]*UserManager),
cachePath: options.CachePath,
} }
inboundManager := service.FromContext[adapter.InboundManager](ctx) inboundManager := service.FromContext[adapter.InboundManager](ctx)
if options.Servers.Size() == 0 { if options.Servers.Size() == 0 {
@ -64,7 +58,7 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
for i, entry := range options.Servers.Entries() { for i, entry := range options.Servers.Entries() {
inbound, loaded := inboundManager.Get(entry.Value) inbound, loaded := inboundManager.Get(entry.Value)
if !loaded { if !loaded {
return nil, E.New("parse SSM server[", i, "]: inbound ", entry.Value, " not found") return nil, E.New("parse SSM server[", i, "]: inbound ", entry.Value, "not found")
} }
managedServer, isManaged := inbound.(adapter.ManagedSSMServer) managedServer, isManaged := inbound.(adapter.ManagedSSMServer)
if !isManaged { if !isManaged {
@ -74,8 +68,6 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
managedServer.SetTracker(traffic) managedServer.SetTracker(traffic)
user := NewUserManager(managedServer, traffic) user := NewUserManager(managedServer, traffic)
chiRouter.Route(entry.Key, NewAPIServer(logger, traffic, user).Route) chiRouter.Route(entry.Key, NewAPIServer(logger, traffic, user).Route)
s.traffics[entry.Key] = traffic
s.users[entry.Key] = user
} }
if options.TLS != nil { if options.TLS != nil {
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
@ -91,12 +83,8 @@ func (s *Service) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart { if stage != adapter.StartStateStart {
return nil return nil
} }
err := s.loadCache()
if err != nil {
s.logger.Error(E.Cause(err, "load cache"))
}
if s.tlsConfig != nil { if s.tlsConfig != nil {
err = s.tlsConfig.Start() err := s.tlsConfig.Start()
if err != nil { if err != nil {
return E.Cause(err, "create TLS config") return E.Cause(err, "create TLS config")
} }
@ -121,10 +109,6 @@ func (s *Service) Start(stage adapter.StartStage) error {
} }
func (s *Service) Close() error { func (s *Service) Close() error {
err := s.saveCache()
if err != nil {
s.logger.Error(E.Cause(err, "save cache"))
}
return common.Close( return common.Close(
common.PtrOrNil(s.httpServer), common.PtrOrNil(s.httpServer),
common.PtrOrNil(s.listener), common.PtrOrNil(s.listener),

View File

@ -22,7 +22,7 @@ func NewUserManager(inbound adapter.ManagedSSMServer, trafficManager *TrafficMan
} }
} }
func (m *UserManager) postUpdate(updated bool) error { func (m *UserManager) postUpdate() error {
users := make([]string, 0, len(m.usersMap)) users := make([]string, 0, len(m.usersMap))
uPSKs := make([]string, 0, len(m.usersMap)) uPSKs := make([]string, 0, len(m.usersMap))
for username, password := range m.usersMap { for username, password := range m.usersMap {
@ -33,9 +33,7 @@ func (m *UserManager) postUpdate(updated bool) error {
if err != nil { if err != nil {
return err return err
} }
if updated {
m.trafficManager.UpdateUsers(users) m.trafficManager.UpdateUsers(users)
}
return nil return nil
} }
@ -57,10 +55,10 @@ func (m *UserManager) Add(username string, password string) error {
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
if _, found := m.usersMap[username]; found { if _, found := m.usersMap[username]; found {
return E.New("user ", username, " already exists") return E.New("user", username, "already exists")
} }
m.usersMap[username] = password m.usersMap[username] = password
return m.postUpdate(true) return m.postUpdate()
} }
func (m *UserManager) Get(username string) (string, bool) { func (m *UserManager) Get(username string) (string, bool) {
@ -76,12 +74,12 @@ func (m *UserManager) Update(username string, password string) error {
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
m.usersMap[username] = password m.usersMap[username] = password
return m.postUpdate(true) return m.postUpdate()
} }
func (m *UserManager) Delete(username string) error { func (m *UserManager) Delete(username string) error {
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
delete(m.usersMap, username) delete(m.usersMap, username)
return m.postUpdate(true) return m.postUpdate()
} }

View File

@ -32,7 +32,7 @@ func TestMain(m *testing.M) {
var globalCtx context.Context var globalCtx context.Context
func init() { func init() {
globalCtx = include.Context(context.Background()) globalCtx = box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry())
} }
func startInstance(t *testing.T, options option.Options) *box.Box { func startInstance(t *testing.T, options option.Options) *box.Box {

View File

@ -47,7 +47,6 @@ func NewServer(ctx context.Context, logger logger.ContextLogger, options option.
server := &Server{ server := &Server{
ctx: ctx, ctx: ctx,
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
logger: logger,
handler: handler, handler: handler,
h2Server: &http2.Server{ h2Server: &http2.Server{
IdleTimeout: time.Duration(options.IdleTimeout), IdleTimeout: time.Duration(options.IdleTimeout),

View File

@ -3,7 +3,6 @@ package v2raywebsocket
import ( import (
"context" "context"
"encoding/base64" "encoding/base64"
"errors"
"io" "io"
"net" "net"
"os" "os"
@ -68,10 +67,9 @@ func (c *WebsocketConn) Read(b []byte) (n int, err error) {
return return
} }
if !E.IsMulti(err, io.EOF, wsutil.ErrNoFrameAdvance) { if !E.IsMulti(err, io.EOF, wsutil.ErrNoFrameAdvance) {
err = wrapWsError(err)
return return
} }
header, err = wrapWsError0(c.reader.NextFrame()) header, err = c.reader.NextFrame()
if err != nil { if err != nil {
return return
} }
@ -80,14 +78,14 @@ func (c *WebsocketConn) Read(b []byte) (n int, err error) {
err = wsutil.ErrFrameTooLarge err = wsutil.ErrFrameTooLarge
return return
} }
err = wrapWsError(c.controlHandler(header, c.reader)) err = c.controlHandler(header, c.reader)
if err != nil { if err != nil {
return return
} }
continue continue
} }
if header.OpCode&ws.OpBinary == 0 { if header.OpCode&ws.OpBinary == 0 {
err = wrapWsError(c.reader.Discard()) err = c.reader.Discard()
if err != nil { if err != nil {
return return
} }
@ -97,7 +95,7 @@ func (c *WebsocketConn) Read(b []byte) (n int, err error) {
} }
func (c *WebsocketConn) Write(p []byte) (n int, err error) { func (c *WebsocketConn) Write(p []byte) (n int, err error) {
err = wrapWsError(wsutil.WriteMessage(c.Conn, c.state, ws.OpBinary, p)) err = wsutil.WriteMessage(c.Conn, c.state, ws.OpBinary, p)
if err != nil { if err != nil {
return return
} }
@ -148,7 +146,7 @@ func (c *EarlyWebsocketConn) Read(b []byte) (n int, err error) {
return 0, c.err return 0, c.err
} }
} }
return wrapWsError0(c.conn.Read(b)) return c.conn.Read(b)
} }
func (c *EarlyWebsocketConn) writeRequest(content []byte) error { func (c *EarlyWebsocketConn) writeRequest(content []byte) error {
@ -193,7 +191,7 @@ func (c *EarlyWebsocketConn) writeRequest(content []byte) error {
func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) { func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) {
if c.conn != nil { if c.conn != nil {
return wrapWsError0(c.conn.Write(b)) return c.conn.Write(b)
} }
c.access.Lock() c.access.Lock()
defer c.access.Unlock() defer c.access.Unlock()
@ -201,7 +199,7 @@ func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) {
return 0, c.err return 0, c.err
} }
if c.conn != nil { if c.conn != nil {
return wrapWsError0(c.conn.Write(b)) return c.conn.Write(b)
} }
err = c.writeRequest(b) err = c.writeRequest(b)
c.err = err c.err = err
@ -214,12 +212,12 @@ func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) {
func (c *EarlyWebsocketConn) WriteBuffer(buffer *buf.Buffer) error { func (c *EarlyWebsocketConn) WriteBuffer(buffer *buf.Buffer) error {
if c.conn != nil { if c.conn != nil {
return wrapWsError(c.conn.WriteBuffer(buffer)) return c.conn.WriteBuffer(buffer)
} }
c.access.Lock() c.access.Lock()
defer c.access.Unlock() defer c.access.Unlock()
if c.conn != nil { if c.conn != nil {
return wrapWsError(c.conn.WriteBuffer(buffer)) return c.conn.WriteBuffer(buffer)
} }
if c.err != nil { if c.err != nil {
return c.err return c.err
@ -274,23 +272,3 @@ func (c *EarlyWebsocketConn) Upstream() any {
func (c *EarlyWebsocketConn) LazyHeadroom() bool { func (c *EarlyWebsocketConn) LazyHeadroom() bool {
return c.conn == nil return c.conn == nil
} }
func wrapWsError(err error) error {
if err == nil {
return nil
}
var closedErr wsutil.ClosedError
if errors.As(err, &closedErr) {
if closedErr.Code == ws.StatusNormalClosure {
err = io.EOF
}
}
return err
}
func wrapWsError0[T any](value T, err error) (T, error) {
if err == nil {
return value, nil
}
return value, wrapWsError(err)
}

View File

@ -0,0 +1,22 @@
package v2raywebsocket
import (
"net"
"time"
)
type deadConn struct {
net.Conn
}
func (c *deadConn) SetDeadline(t time.Time) error {
return nil
}
func (c *deadConn) SetReadDeadline(t time.Time) error {
return nil
}
func (c *deadConn) SetWriteDeadline(t time.Time) error {
return nil
}

View File

@ -66,7 +66,7 @@ func (w *Writer) WriteBuffer(buffer *buf.Buffer) error {
ws.Cipher(data, *(*[4]byte)(header[1+payloadBitLength:]), 0) ws.Cipher(data, *(*[4]byte)(header[1+payloadBitLength:]), 0)
} }
return wrapWsError(w.writer.WriteBuffer(buffer)) return w.writer.WriteBuffer(buffer)
} }
func (w *Writer) FrontHeadroom() int { func (w *Writer) FrontHeadroom() int {