Compare commits

..

139 Commits

Author SHA1 Message Date
世界
0d8c15932f Add options to custom DNS query timeout 2025-06-30 18:29:00 +08:00
世界
70371c3cbe documentation: Bump version 2025-06-30 18:28:16 +08:00
世界
32bc8b48f1 Improve nftables rules for openwrt 2025-06-30 18:27:33 +08:00
世界
b5df508bc9 Fixed DoH server recover from conn freezes 2025-06-30 18:24:48 +08:00
世界
4789846113 Update libresolv usage 2025-06-29 19:20:31 +08:00
yu
ad94f94cfb documentation: Update client configuration manual 2025-06-29 19:11:15 +08:00
yanwo
1f9de9f321 documentation: Fix typo
Signed-off-by: yanwo <ogilvy@gmail.com>
2025-06-29 19:11:15 +08:00
anytinz
344bbf9494 documentation: Fix wrong SideStore loopback ip 2025-06-29 18:57:04 +08:00
世界
b7e16e70ab Revert "release: Add IPA build"
After testing, it seems that since extensions are not handled correctly, it cannot be installed by SideStore.
2025-06-29 18:57:04 +08:00
世界
391153ecb8 release: Add IPA build 2025-06-29 18:57:03 +08:00
世界
3d821db0b2 Add API to dump AdGuard rules 2025-06-29 18:57:03 +08:00
Sukka
344ecd3798 Improve AdGuard rule-set parser 2025-06-29 18:57:03 +08:00
Restia-Ashbell
a8a3f863cd Add ECH support for uTLS 2025-06-29 18:57:02 +08:00
世界
ea190ca428 Improve TLS fragments 2025-06-29 18:57:02 +08:00
世界
e65b78d1e4 Add cache support for ssm-api 2025-06-29 18:57:02 +08:00
世界
84f7d80da7 Fix service will not be closed 2025-06-29 18:57:02 +08:00
世界
2b57eb0b30 Add loopback address support for tun 2025-06-29 18:57:01 +08:00
世界
9884f81298 Fix tproxy listener 2025-06-29 18:57:01 +08:00
世界
8493b4f1da Fix systemd package 2025-06-29 18:57:01 +08:00
世界
8f1a8add85 Fix missing home for derp service 2025-06-29 18:57:01 +08:00
Zero Clover
85ee6bb266 documentation: Fix services 2025-06-29 18:57:01 +08:00
世界
c4387f7c37 Fix dns.client_subnet ignored 2025-06-29 18:57:01 +08:00
世界
dcb7a5caed documentation: Minor fixes 2025-06-29 18:57:00 +08:00
世界
ae77cdeedd Fix tailscale forward 2025-06-29 18:57:00 +08:00
世界
ec05d4e5e3 Minor fixes 2025-06-29 18:56:59 +08:00
世界
1f766d2b89 Add SSM API service 2025-06-29 18:56:59 +08:00
世界
a5a6c1f7d4 Add resolved service and DNS server 2025-06-29 18:56:59 +08:00
世界
cd43786279 Add DERP service 2025-06-29 18:56:59 +08:00
世界
43a211db28 Add service component type 2025-06-29 18:56:58 +08:00
世界
d4f6bdf792 Fix tproxy tcp control 2025-06-29 18:56:58 +08:00
愚者
99cf27bedd release: Fix build tags for android
Signed-off-by: 愚者 <11926619+FansChou@users.noreply.github.com>
2025-06-29 18:56:58 +08:00
世界
0a14c5ab1f prevent creation of bind and mark controls on unsupported platforms 2025-06-29 18:56:58 +08:00
PuerNya
533b31e1f6 documentation: Fix description of reject DNS action behavior 2025-06-29 18:56:57 +08:00
Restia-Ashbell
8ed6523872 Fix TLS record fragment 2025-06-29 18:56:57 +08:00
世界
e5c162222d Add missing accept_routes option for Tailscale 2025-06-29 18:56:57 +08:00
世界
88babbf3a7 Add TLS record fragment support 2025-06-29 18:56:56 +08:00
世界
49a03e0b23 Fix set edns0 client subnet 2025-06-29 18:56:56 +08:00
世界
9a8e9a34c0 Update minor dependencies 2025-06-29 18:56:56 +08:00
世界
17551db7be Update certmagic and providers 2025-06-29 18:56:56 +08:00
世界
83201bb088 Update protobuf and grpc 2025-06-29 18:56:56 +08:00
世界
f0f1942f1f Add control options for listeners 2025-06-29 18:56:55 +08:00
世界
c7e318be61 Update quic-go to v0.52.0 2025-06-29 18:56:54 +08:00
世界
2a5e0d0c92 Update utls to v1.7.2 2025-06-29 18:56:54 +08:00
世界
956e485342 Handle EDNS version downgrade 2025-06-29 18:56:54 +08:00
世界
23ede74e74 documentation: Fix anytls padding scheme description 2025-06-29 18:56:54 +08:00
安容
03317e61dd Report invalid DNS address early 2025-06-29 18:56:54 +08:00
世界
fc81bd9a5b Fix wireguard listen_port 2025-06-29 18:56:53 +08:00
世界
4d406cad84 clash-api: Add more meta api 2025-06-29 18:56:53 +08:00
世界
1f0282de9c Fix DNS lookup 2025-06-29 18:56:53 +08:00
世界
b097912418 Fix fetch ECH configs 2025-06-29 18:56:52 +08:00
reletor
056c29e73a documentation: Minor fixes 2025-06-29 18:56:52 +08:00
caelansar
7aa0a57e60 Fix callback deletion in UDP transport 2025-06-29 18:56:52 +08:00
世界
2673e64bcb documentation: Try to make the play review happy 2025-06-29 18:56:51 +08:00
世界
3d3c1709d7 Fix missing handling of legacy domain_strategy options 2025-06-29 18:56:51 +08:00
世界
9de29a590f Improve local DNS server 2025-06-29 18:56:50 +08:00
anytls
a5282b08ec Update anytls
Co-authored-by: anytls <anytls>
2025-06-29 18:56:50 +08:00
世界
b26c2083bf Fix DNS dialer 2025-06-29 18:56:50 +08:00
世界
c1f4c691dc release: Skip override version for iOS 2025-06-29 18:56:49 +08:00
iikira
c0ef6eb728 Fix UDP DNS server crash
Signed-off-by: iikira <i2@mail.iikira.com>
2025-06-29 18:56:49 +08:00
ReleTor
074d61021f Fix fetch ECH configs 2025-06-29 18:56:49 +08:00
世界
246c9d4e40 Allow direct outbounds without domain_resolver 2025-06-29 18:56:49 +08:00
世界
3e3466c8d7 Fix Tailscale dialer 2025-06-29 18:56:48 +08:00
dyhkwong
f73415a732 Fix DNS over QUIC stream close 2025-06-29 18:56:48 +08:00
anytls
ecabe9ffe1 Update anytls
Co-authored-by: anytls <anytls>
2025-06-29 18:56:48 +08:00
Rambling2076
2dae1ee284 Fix missing with_tailscale in Dockerfile
Signed-off-by: Rambling2076 <Rambling2076@proton.me>
2025-06-29 18:56:47 +08:00
世界
2afb24d698 Fail when default DNS server not found 2025-06-29 18:56:47 +08:00
世界
ade83ee758 Update gVisor to 20250319.0 2025-06-29 18:56:47 +08:00
世界
ac9c300ca7 Explicitly reject detour to empty direct outbounds 2025-06-29 18:56:47 +08:00
世界
e6eb3cec2b Add netns support 2025-06-29 18:56:46 +08:00
世界
601b79371b Add wildcard name support for predefined records 2025-06-29 18:56:46 +08:00
世界
56b957d30d Remove map usage in options 2025-06-29 18:56:45 +08:00
世界
eb87b1a708 Fix unhandled DNS loop 2025-06-29 18:56:45 +08:00
世界
c0a5561bd4 Add wildcard-sni support for shadow-tls inbound 2025-06-29 18:56:45 +08:00
k9982874
4a3fe1d41c Add ntp protocol sniffing 2025-06-29 18:56:45 +08:00
世界
4cbbcfb04d option: Fix marshal legacy DNS options 2025-06-29 18:56:44 +08:00
世界
b3ad4e0e39 Make domain_resolver optional when only one DNS server is configured 2025-06-29 18:56:44 +08:00
世界
49f9c0011d Fix DNS lookup context pollution 2025-06-29 18:56:44 +08:00
世界
c8c165af87 Fix http3 DNS server connecting to wrong address 2025-06-29 18:56:43 +08:00
Restia-Ashbell
3f790ff8c9 documentation: Fix typo 2025-06-29 18:56:43 +08:00
anytls
f2272ae1e7 Update sing-anytls
Co-authored-by: anytls <anytls>
2025-06-29 18:56:43 +08:00
k9982874
b40c264c0a Fix hosts DNS server 2025-06-29 18:56:43 +08:00
世界
29fc8d6a86 Fix UDP DNS server crash 2025-06-29 18:56:43 +08:00
世界
46ca27c926 documentation: Fix missing ip_accept_any DNS rule option 2025-06-29 18:56:42 +08:00
世界
243a5dd477 Fix anytls dialer usage 2025-06-29 18:56:42 +08:00
世界
844e9f09a6 Move predefined DNS server to rule action 2025-06-29 18:56:41 +08:00
世界
9aa673f79e Fix domain resolver on direct outbound 2025-06-29 18:56:41 +08:00
Zephyruso
5abd74ffe3 Fix missing AnyTLS display name 2025-06-29 18:56:41 +08:00
anytls
68a32960bd Update sing-anytls
Co-authored-by: anytls <anytls>
2025-06-29 18:56:41 +08:00
Estel
1e62e3e5d4 documentation: Fix typo
Signed-off-by: Estel <callmebedrockdigger@gmail.com>
2025-06-29 18:56:41 +08:00
TargetLocked
40c03a9913 Fix parsing legacy DNS options 2025-06-29 18:56:40 +08:00
世界
2ea4029868 Fix DNS fallback 2025-06-29 18:56:40 +08:00
世界
66f5cdd014 documentation: Fix missing hosts DNS server 2025-06-29 18:56:39 +08:00
anytls
89da2b6355 Add MinIdleSession option to AnyTLS outbound
Co-authored-by: anytls <anytls>
2025-06-29 18:56:39 +08:00
ReleTor
ee2b8498e6 documentation: Minor fixes 2025-06-29 18:56:39 +08:00
libtry486
b1eaf537bd documentation: Fix typo
fix typo

Signed-off-by: libtry486 <89328481+libtry486@users.noreply.github.com>
2025-06-29 18:56:39 +08:00
Alireza Ahmadi
63739c1621 Fix Outbound deadlock 2025-06-29 18:56:38 +08:00
世界
3c8ddee029 documentation: Fix AnyTLS doc 2025-06-29 18:56:38 +08:00
anytls
23aad70045 Add AnyTLS protocol 2025-06-29 18:56:38 +08:00
世界
0d67b51267 Migrate to stdlib ECH support 2025-06-29 18:56:37 +08:00
世界
c02f939265 Add fallback local DNS server for iOS 2025-06-29 18:56:37 +08:00
世界
36b84e25c2 Get darwin local DNS server from libresolv 2025-06-29 18:56:37 +08:00
世界
9fd4b0e9ae Improve resolve action 2025-06-29 18:56:36 +08:00
世界
e68cdcc98a Add back port hopping to hysteria 1 2025-06-29 18:56:36 +08:00
xchacha20-poly1305
2691617c5e Remove single quotes of raw Moziila certs 2025-06-29 18:56:35 +08:00
世界
04056a1357 Add Tailscale endpoint 2025-06-29 18:56:35 +08:00
世界
faac858e5d Build legacy binaries with latest Go 2025-06-29 18:56:35 +08:00
世界
a818c8abeb documentation: Remove outdated icons 2025-06-29 18:56:34 +08:00
世界
15cbe9fc87 documentation: Certificate store 2025-06-29 18:56:34 +08:00
世界
e186f2d31e documentation: TLS fragment 2025-06-29 18:56:34 +08:00
世界
e9323481a4 documentation: Outbound domain resolver 2025-06-29 18:56:33 +08:00
世界
a0a41ff2bb documentation: Refactor DNS 2025-06-29 18:56:33 +08:00
世界
549daf9d41 Add certificate store 2025-06-29 18:56:33 +08:00
世界
fa370f7d04 Add TLS fragment support 2025-06-29 18:56:33 +08:00
世界
795cb17bfa refactor: Outbound domain resolver 2025-06-29 18:56:32 +08:00
世界
00d8add761 refactor: DNS 2025-06-29 18:56:32 +08:00
Kyson
36db31c55a documentation: Fix typo
Co-authored-by: chenqixin <chenqixin@bytedance.com>
2025-06-29 18:54:05 +08:00
世界
4dbbf59c82 Fix logger for acme 2025-06-29 18:44:40 +08:00
世界
832eb4458d release: Fix xcode version 2025-06-29 18:44:40 +08:00
dyhkwong
2cf989d306 Fix inbound with v2ray transport missing InboundOptions 2025-06-25 13:20:00 +08:00
世界
7d3ee29bd0 Also skip duplicate sniff for TCP 2025-06-21 12:57:27 +08:00
世界
cba0e46aba Fix log for rejected connections 2025-06-21 12:57:26 +08:00
世界
9b8ab3e61e Bump version 2025-06-19 11:57:44 +08:00
dyhkwong
47f18e823a Fix: macOS udp find process should use unspecified fallback
be8d63ba8f
2025-06-18 08:34:59 +08:00
世界
2d1b824b62 Fix gLazyConn race 2025-06-17 14:24:11 +08:00
世界
d511698f3f Fix slowOpenConn 2025-06-12 08:05:04 +08:00
世界
cb435ea232 Fix default network strategy 2025-06-12 08:05:04 +08:00
世界
43a9016c83 Fix leak in hijack-dns 2025-06-06 14:28:09 +08:00
世界
255068fd40 Bump version 2025-06-04 23:32:10 +08:00
世界
098a00b025 Fix v2ray websocket transport 2025-06-04 23:23:36 +08:00
世界
dba0b5276b Bump version 2025-06-04 20:06:38 +08:00
Sentsuki
78ae935468 documentation: Fix typo
Signed-off-by: Sentsuki <52487960+Sentsuki@users.noreply.github.com>
2025-06-04 20:06:38 +08:00
Mahdi
3ea5f76470 Fix nil logger at v2rayhttp server 2025-06-04 20:06:20 +08:00
世界
b4d294c05e Fix TUIC read buffer 2025-06-04 20:03:51 +08:00
世界
83cf5f5c6a Fix ws closed error message 2025-05-27 14:30:07 +08:00
世界
e7b3a8eebe Fix vmess read request 2025-05-27 14:11:05 +08:00
世界
ee3a42a67e Fix none method read buffer 2025-05-27 14:03:48 +08:00
世界
50227c0f5f Fix sniff action 2025-05-26 18:24:35 +08:00
世界
bc5eb1e1a5 Fix RoutePacketConnectionEx 2025-05-24 08:14:43 +08:00
世界
995267a042 Remove wrong ALPNs in DOH/DOH3 2025-05-24 08:00:13 +08:00
85 changed files with 1302 additions and 547 deletions

View File

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

View File

@@ -46,7 +46,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.24.3
go-version: ^1.24
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -109,7 +109,7 @@ jobs:
if: ${{ ! matrix.legacy_go }}
uses: actions/setup-go@v5
with:
go-version: ^1.24.3
go-version: ^1.24
- name: Cache Legacy Go
if: matrix.require_legacy_go
id: cache-legacy-go
@@ -294,7 +294,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.24.3
go-version: ^1.24
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -374,7 +374,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.24.3
go-version: ^1.24
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -472,15 +472,15 @@ jobs:
if: matrix.if
uses: actions/setup-go@v5
with:
go-version: ^1.24.3
go-version: ^1.24
- name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.2.app
sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Setup Xcode beta
if: matrix.if && github.ref == 'refs/heads/dev-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.2.app
sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Set tag
if: matrix.if
run: |-
@@ -615,7 +615,7 @@ jobs:
path: 'dist'
upload:
name: Upload builds
if: always() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')
if: "!failure() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')"
runs-on: ubuntu-latest
needs:
- calculate_version

View File

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

View File

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

View File

@@ -1,11 +1,10 @@
NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD)
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
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale
GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
@@ -109,6 +108,16 @@ upload_ios_app_store:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
export_ios_ipa:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Export.plist -allowProvisioningUpdates -exportPath build/SFI && \
cp build/SFI/sing-box.ipa dist/SFI.ipa
upload_ios_ipa:
cd dist && \
cp SFI.ipa "SFI-${VERSION}.ipa" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFI-${VERSION}.ipa"
release_ios: build_ios upload_ios_app_store
build_macos:
@@ -176,6 +185,16 @@ upload_tvos_app_store:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
export_tvos_ipa:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Export.plist -allowProvisioningUpdates -exportPath build/SFT && \
cp build/SFT/sing-box.ipa dist/SFT.ipa
upload_tvos_ipa:
cd dist && \
cp SFT.ipa "SFT-${VERSION}.ipa" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFT-${VERSION}.ipa"
release_tvos: build_tvos upload_tvos_app_store
update_apple_version:

View File

@@ -3,6 +3,7 @@ package adapter
import (
"context"
"net/netip"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
@@ -36,6 +37,7 @@ type DNSQueryOptions struct {
Transport DNSTransport
Strategy C.DomainStrategy
LookupStrategy C.DomainStrategy
Timeout time.Duration
DisableCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
@@ -53,6 +55,7 @@ func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptio
return &DNSQueryOptions{
Transport: transport,
Strategy: C.DomainStrategy(options.Strategy),
Timeout: time.Duration(options.Timeout),
DisableCache: options.DisableCache,
RewriteTTL: options.RewriteTTL,
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
@@ -70,6 +73,7 @@ type DNSTransport interface {
Type() string
Tag() string
Dependencies() []string
HasDetour() bool
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
}

View File

@@ -53,11 +53,11 @@ type InboundContext struct {
// sniffer
Protocol string
Domain string
Client string
SniffContext any
PacketSniffError error
Protocol string
Domain string
Client string
SniffContext any
SniffError error
// cache

2
box.go
View File

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

View File

@@ -105,7 +105,7 @@ func publishTestflight(ctx context.Context) error {
return err
}
tag := tagVersion.VersionString()
client := createClient(10 * time.Minute)
client := createClient(20 * time.Minute)
log.Info(tag, " list build IDs")
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
@@ -145,7 +145,7 @@ func publishTestflight(ctx context.Context) error {
return err
}
build := builds.Data[0]
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 5*time.Minute {
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute {
log.Info(string(platform), " ", tag, " waiting for process")
time.Sleep(15 * time.Second)
continue

View File

@@ -16,15 +16,17 @@ import (
)
var (
debugEnabled bool
target string
platform string
debugEnabled bool
target string
platform string
withTailscale bool
)
func init() {
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
flag.StringVar(&target, "target", "android", "target platform")
flag.StringVar(&platform, "platform", "", "specify platform")
flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS")
}
func main() {
@@ -151,7 +153,9 @@ func buildApple() {
"-v",
"-target", bindTarget,
"-libname=box",
"-tags-macos=" + strings.Join(memcTags, ","),
}
if !withTailscale {
args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
}
if !debugEnabled {
@@ -161,6 +165,9 @@ func buildApple() {
}
tags := append(sharedTags, iosTags...)
if withTailscale {
tags = append(tags, memcTags...)
}
if debugEnabled {
tags = append(tags, debugTags...)
}

View File

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

View File

@@ -5,7 +5,7 @@ import (
"os"
"strings"
"github.com/sagernet/sing-box/cmd/sing-box/internal/convertor/adguard"
"github.com/sagernet/sing-box/common/convertor/adguard"
"github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
@@ -54,7 +54,7 @@ func convertRuleSet(sourcePath string) error {
var rules []option.HeadlessRule
switch flagRuleSetConvertType {
case "adguard":
rules, err = adguard.Convert(reader)
rules, err = adguard.ToOptions(reader, log.StdLogger())
case "":
return E.New("source type is required")
default:

View File

@@ -6,7 +6,10 @@ import (
"strings"
"github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/spf13/cobra"
@@ -50,6 +53,11 @@ func decompileRuleSet(sourcePath string) error {
if err != nil {
return err
}
if hasRule(ruleSet.Options.Rules, func(rule option.DefaultHeadlessRule) bool {
return len(rule.AdGuardDomain) > 0
}) {
return E.New("unable to decompile binary AdGuard rules to rule-set.")
}
var outputPath string
if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".srs") {
@@ -75,3 +83,19 @@ func decompileRuleSet(sourcePath string) error {
outputFile.Close()
return nil
}
func hasRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool {
for _, rule := range rules {
switch rule.Type {
case C.RuleTypeDefault:
if cond(rule.DefaultOptions) {
return true
}
case C.RuleTypeLogical:
if hasRule(rule.LogicalOptions.Rules, cond) {
return true
}
}
}
return false
}

View File

@@ -2,6 +2,7 @@ package adguard
import (
"bufio"
"bytes"
"io"
"net/netip"
"os"
@@ -9,10 +10,10 @@ import (
"strings"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
)
@@ -27,7 +28,7 @@ type agdguardRuleLine struct {
isImportant bool
}
func Convert(reader io.Reader) ([]option.HeadlessRule, error) {
func ToOptions(reader io.Reader, logger logger.Logger) ([]option.HeadlessRule, error) {
scanner := bufio.NewScanner(reader)
var (
ruleLines []agdguardRuleLine
@@ -36,7 +37,10 @@ func Convert(reader io.Reader) ([]option.HeadlessRule, error) {
parseLine:
for scanner.Scan() {
ruleLine := scanner.Text()
if ruleLine == "" || ruleLine[0] == '!' || ruleLine[0] == '#' {
if ruleLine == "" {
continue
}
if strings.HasPrefix(ruleLine, "!") || strings.HasPrefix(ruleLine, "#") {
continue
}
originRuleLine := ruleLine
@@ -92,7 +96,7 @@ parseLine:
}
if !ignored {
ignoredLines++
log.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", ruleLine)
logger.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", originRuleLine)
continue parseLine
}
}
@@ -120,27 +124,35 @@ parseLine:
ruleLine = ruleLine[1 : len(ruleLine)-1]
if ignoreIPCIDRRegexp(ruleLine) {
ignoredLines++
log.Debug("ignored unsupported rule with IPCIDR regexp: ", ruleLine)
logger.Debug("ignored unsupported rule with IPCIDR regexp: ", originRuleLine)
continue
}
isRegexp = true
} else {
if strings.Contains(ruleLine, "://") {
ruleLine = common.SubstringAfter(ruleLine, "://")
isSuffix = true
}
if strings.Contains(ruleLine, "/") {
ignoredLines++
log.Debug("ignored unsupported rule with path: ", ruleLine)
logger.Debug("ignored unsupported rule with path: ", originRuleLine)
continue
}
if strings.Contains(ruleLine, "##") {
if strings.Contains(ruleLine, "?") || strings.Contains(ruleLine, "&") {
ignoredLines++
log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
logger.Debug("ignored unsupported rule with query: ", originRuleLine)
continue
}
if strings.Contains(ruleLine, "#$#") {
if strings.Contains(ruleLine, "[") || strings.Contains(ruleLine, "]") ||
strings.Contains(ruleLine, "(") || strings.Contains(ruleLine, ")") ||
strings.Contains(ruleLine, "!") || strings.Contains(ruleLine, "#") {
ignoredLines++
log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
logger.Debug("ignored unsupported cosmetic filter: ", originRuleLine)
continue
}
if strings.Contains(ruleLine, "~") {
ignoredLines++
logger.Debug("ignored unsupported rule modifier: ", originRuleLine)
continue
}
var domainCheck string
@@ -151,7 +163,7 @@ parseLine:
}
if ruleLine == "" {
ignoredLines++
log.Debug("ignored unsupported rule with empty domain", originRuleLine)
logger.Debug("ignored unsupported rule with empty domain", originRuleLine)
continue
} else {
domainCheck = strings.ReplaceAll(domainCheck, "*", "x")
@@ -159,13 +171,13 @@ parseLine:
_, ipErr := parseADGuardIPCIDRLine(ruleLine)
if ipErr == nil {
ignoredLines++
log.Debug("ignored unsupported rule with IPCIDR: ", ruleLine)
logger.Debug("ignored unsupported rule with IPCIDR: ", originRuleLine)
continue
}
if M.ParseSocksaddr(domainCheck).Port != 0 {
log.Debug("ignored unsupported rule with port: ", ruleLine)
logger.Debug("ignored unsupported rule with port: ", originRuleLine)
} else {
log.Debug("ignored unsupported rule with invalid domain: ", ruleLine)
logger.Debug("ignored unsupported rule with invalid domain: ", originRuleLine)
}
ignoredLines++
continue
@@ -283,10 +295,112 @@ parseLine:
},
}
}
log.Info("parsed rules: ", len(ruleLines), "/", len(ruleLines)+ignoredLines)
if ignoredLines > 0 {
logger.Info("parsed rules: ", len(ruleLines), "/", len(ruleLines)+ignoredLines)
}
return []option.HeadlessRule{currentRule}, nil
}
var ErrInvalid = E.New("invalid binary AdGuard rule-set")
func FromOptions(rules []option.HeadlessRule) ([]byte, error) {
if len(rules) != 1 {
return nil, ErrInvalid
}
rule := rules[0]
var (
importantDomain []string
importantDomainRegex []string
importantExcludeDomain []string
importantExcludeDomainRegex []string
domain []string
domainRegex []string
excludeDomain []string
excludeDomainRegex []string
)
parse:
for {
switch rule.Type {
case C.RuleTypeLogical:
if !(len(rule.LogicalOptions.Rules) == 2 && rule.LogicalOptions.Rules[0].Type == C.RuleTypeDefault) {
return nil, ErrInvalid
}
if rule.LogicalOptions.Mode == C.LogicalTypeAnd && rule.LogicalOptions.Rules[0].DefaultOptions.Invert {
if len(importantExcludeDomain) == 0 && len(importantExcludeDomainRegex) == 0 {
importantExcludeDomain = rule.LogicalOptions.Rules[0].DefaultOptions.AdGuardDomain
importantExcludeDomainRegex = rule.LogicalOptions.Rules[0].DefaultOptions.DomainRegex
if len(importantExcludeDomain)+len(importantExcludeDomainRegex) == 0 {
return nil, ErrInvalid
}
} else {
excludeDomain = rule.LogicalOptions.Rules[0].DefaultOptions.AdGuardDomain
excludeDomainRegex = rule.LogicalOptions.Rules[0].DefaultOptions.DomainRegex
if len(excludeDomain)+len(excludeDomainRegex) == 0 {
return nil, ErrInvalid
}
}
} else if rule.LogicalOptions.Mode == C.LogicalTypeOr && !rule.LogicalOptions.Rules[0].DefaultOptions.Invert {
importantDomain = rule.LogicalOptions.Rules[0].DefaultOptions.AdGuardDomain
importantDomainRegex = rule.LogicalOptions.Rules[0].DefaultOptions.DomainRegex
if len(importantDomain)+len(importantDomainRegex) == 0 {
return nil, ErrInvalid
}
} else {
return nil, ErrInvalid
}
rule = rule.LogicalOptions.Rules[1]
case C.RuleTypeDefault:
domain = rule.DefaultOptions.AdGuardDomain
domainRegex = rule.DefaultOptions.DomainRegex
if len(domain)+len(domainRegex) == 0 {
return nil, ErrInvalid
}
break parse
}
}
var output bytes.Buffer
for _, ruleLine := range importantDomain {
output.WriteString(ruleLine)
output.WriteString("$important\n")
}
for _, ruleLine := range importantDomainRegex {
output.WriteString("/")
output.WriteString(ruleLine)
output.WriteString("/$important\n")
}
for _, ruleLine := range importantExcludeDomain {
output.WriteString("@@")
output.WriteString(ruleLine)
output.WriteString("$important\n")
}
for _, ruleLine := range importantExcludeDomainRegex {
output.WriteString("@@/")
output.WriteString(ruleLine)
output.WriteString("/$important\n")
}
for _, ruleLine := range domain {
output.WriteString(ruleLine)
output.WriteString("\n")
}
for _, ruleLine := range domainRegex {
output.WriteString("/")
output.WriteString(ruleLine)
output.WriteString("/\n")
}
for _, ruleLine := range excludeDomain {
output.WriteString("@@")
output.WriteString(ruleLine)
output.WriteString("\n")
}
for _, ruleLine := range excludeDomainRegex {
output.WriteString("@@/")
output.WriteString(ruleLine)
output.WriteString("/\n")
}
return output.Bytes(), nil
}
func ignoreIPCIDRRegexp(ruleLine string) bool {
if strings.HasPrefix(ruleLine, "(http?:\\/\\/)") {
ruleLine = ruleLine[12:]
@@ -294,11 +408,9 @@ func ignoreIPCIDRRegexp(ruleLine string) bool {
ruleLine = ruleLine[13:]
} else if strings.HasPrefix(ruleLine, "^") {
ruleLine = ruleLine[1:]
} else {
return false
}
_, parseErr := strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8)
return parseErr == nil
return common.Error(strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8)) == nil ||
common.Error(strconv.ParseUint(common.SubstringBefore(ruleLine, "."), 10, 8)) == nil
}
func parseAdGuardHostLine(ruleLine string) (string, error) {

View File

@@ -7,13 +7,15 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing/common/logger"
"github.com/stretchr/testify/require"
)
func TestConverter(t *testing.T) {
t.Parallel()
rules, err := Convert(strings.NewReader(`
ruleString := `||sagernet.org^$important
@@|sing-box.sagernet.org^$important
||example.org^
|example.com^
example.net^
@@ -21,10 +23,9 @@ example.net^
||example.edu.tw^
|example.gov
example.arpa
@@|sagernet.example.org|
||sagernet.org^$important
@@|sing-box.sagernet.org^$important
`))
@@|sagernet.example.org^
`
rules, err := ToOptions(strings.NewReader(ruleString), logger.NOP())
require.NoError(t, err)
require.Len(t, rules, 1)
rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
@@ -75,15 +76,18 @@ example.arpa
Domain: domain,
}), domain)
}
ruleFromOptions, err := FromOptions(rules)
require.NoError(t, err)
require.Equal(t, ruleString, string(ruleFromOptions))
}
func TestHosts(t *testing.T) {
t.Parallel()
rules, err := Convert(strings.NewReader(`
rules, err := ToOptions(strings.NewReader(`
127.0.0.1 localhost
::1 localhost #[IPv6]
0.0.0.0 google.com
`))
`), logger.NOP())
require.NoError(t, err)
require.Len(t, rules, 1)
rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
@@ -110,10 +114,10 @@ func TestHosts(t *testing.T) {
func TestSimpleHosts(t *testing.T) {
t.Parallel()
rules, err := Convert(strings.NewReader(`
rules, err := ToOptions(strings.NewReader(`
example.com
www.example.org
`))
`), logger.NOP())
require.NoError(t, err)
require.Len(t, rules, 1)
rule, err := rule.NewHeadlessRule(context.Background(), rules[0])

View File

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

View File

@@ -89,6 +89,7 @@ func NewWithOptions(options Options) (N.Dialer, error) {
dnsQueryOptions = adapter.DNSQueryOptions{
Transport: transport,
Strategy: strategy,
Timeout: time.Duration(dialOptions.DomainResolver.Timeout),
DisableCache: dialOptions.DomainResolver.DisableCache,
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),

View File

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

View File

@@ -56,7 +56,7 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
if l.tproxy {
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
return control.Raw(conn, func(fd uintptr) error {
return redir.TProxy(fd, M.ParseSocksaddr(address).IsIPv6(), false)
return redir.TProxy(fd, !M.ParseSocksaddr(address).IsIPv4(), false)
})
})
}

View File

@@ -41,7 +41,7 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
if l.tproxy {
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
return control.Raw(conn, func(fd uintptr) error {
return redir.TProxy(fd, M.ParseSocksaddr(address).IsIPv6(), true)
return redir.TProxy(fd, !M.ParseSocksaddr(address).IsIPv4(), true)
})
})
}

View File

@@ -76,6 +76,8 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
// rup8(sizeof(xtcpcb_n))
itemSize += 208
}
var fallbackUDPProcess string
// skip the first xinpgen(24 bytes) block
for i := 24; i+itemSize <= len(buf); i += itemSize {
// offset of xinpcb_n and xsocket_n
@@ -90,10 +92,12 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
flag := buf[inp+44]
var srcIP netip.Addr
srcIsIPv4 := false
switch {
case flag&0x1 > 0 && isIPv4:
// ipv4
srcIP = netip.AddrFrom4(*(*[4]byte)(buf[inp+76 : inp+80]))
srcIsIPv4 = true
case flag&0x2 > 0 && !isIPv4:
// ipv6
srcIP = netip.AddrFrom16(*(*[16]byte)(buf[inp+64 : inp+80]))
@@ -101,13 +105,21 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
continue
}
if ip != srcIP {
continue
if ip == srcIP {
// xsocket_n.so_last_pid
pid := readNativeUint32(buf[so+68 : so+72])
return getExecPathFromPID(pid)
}
// xsocket_n.so_last_pid
pid := readNativeUint32(buf[so+68 : so+72])
return getExecPathFromPID(pid)
// udp packet connection may be not equal with srcIP
if network == N.NetworkUDP && srcIP.IsUnspecified() && isIPv4 == srcIsIPv4 {
pid := readNativeUint32(buf[so+68 : so+72])
fallbackUDPProcess, _ = getExecPathFromPID(pid)
}
}
if network == N.NetworkUDP && len(fallbackUDPProcess) > 0 {
return fallbackUDPProcess, nil
}
return "", ErrNotFound

View File

@@ -215,16 +215,15 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
case ruleItemWIFIBSSID:
rule.WIFIBSSID, err = readRuleItemString(reader)
case ruleItemAdGuardDomain:
if recover {
err = E.New("unable to decompile binary AdGuard rules to rule-set")
return
}
var matcher *domain.AdGuardMatcher
matcher, err = domain.ReadAdGuardMatcher(reader)
if err != nil {
return
}
rule.AdGuardDomainMatcher = matcher
if recover {
rule.AdGuardDomain = matcher.Dump()
}
case ruleItemNetworkType:
rule.NetworkType, err = readRuleItemUint8[option.InterfaceType](reader)
case ruleItemNetworkIsExpensive:

View File

@@ -5,13 +5,13 @@ package tls
import (
"context"
"crypto/tls"
"os"
"strings"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/caddyserver/certmagic"
"github.com/libdns/alidns"
@@ -37,7 +37,38 @@ func (w *acmeWrapper) Close() error {
return nil
}
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
type acmeLogWriter struct {
logger logger.Logger
}
func (w *acmeLogWriter) Write(p []byte) (n int, err error) {
logLine := strings.ReplaceAll(string(p), " ", ": ")
switch {
case strings.HasPrefix(logLine, "error: "):
w.logger.Error(logLine[7:])
case strings.HasPrefix(logLine, "warn: "):
w.logger.Warn(logLine[6:])
case strings.HasPrefix(logLine, "info: "):
w.logger.Info(logLine[6:])
case strings.HasPrefix(logLine, "debug: "):
w.logger.Debug(logLine[7:])
default:
w.logger.Debug(logLine)
}
return len(p), nil
}
func (w *acmeLogWriter) Sync() error {
return nil
}
func encoderConfig() zapcore.EncoderConfig {
config := zap.NewProductionEncoderConfig()
config.TimeKey = zapcore.OmitKey
return config
}
func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
var acmeServer string
switch options.Provider {
case "", "letsencrypt":
@@ -58,14 +89,15 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
} else {
storage = certmagic.Default.Storage
}
zapLogger := zap.New(zapcore.NewCore(
zapcore.NewConsoleEncoder(encoderConfig()),
&acmeLogWriter{logger: logger},
zap.DebugLevel,
))
config := &certmagic.Config{
DefaultServerName: options.DefaultServerName,
Storage: storage,
Logger: zap.New(zapcore.NewCore(
zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()),
os.Stderr,
zap.InfoLevel,
)),
Logger: zapLogger,
}
acmeConfig := certmagic.ACMEIssuer{
CA: acmeServer,
@@ -75,7 +107,7 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
DisableTLSALPNChallenge: options.DisableTLSALPNChallenge,
AltHTTPPort: int(options.AlternativeHTTPPort),
AltTLSALPNPort: int(options.AlternativeTLSPort),
Logger: config.Logger,
Logger: zapLogger,
}
if dnsOptions := options.DNS01Challenge; dnsOptions != nil && dnsOptions.Provider != "" {
var solver certmagic.DNS01Solver
@@ -103,6 +135,7 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) {
return config, nil
},
Logger: zapLogger,
})
config = certmagic.New(cache, *config)
var tlsConfig *tls.Config

View File

@@ -9,8 +9,9 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
return nil, nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`)
}

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import (
E "github.com/sagernet/sing/common/exceptions"
)
func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, options option.OutboundTLSOptions) (Config, error) {
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 {
return nil, E.New("invalid short_id")
}
return &RealityClientConfig{ctx, uClient, publicKey, shortID}, nil
return &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID}, nil
}
func (e *RealityClientConfig) ServerName() string {

View File

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

View File

@@ -169,7 +169,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
var err error
if options.ACME != nil && len(options.ACME.Domain) > 0 {
//nolint:staticcheck
tlsConfig, acmeService, err = startACME(ctx, common.PtrValueOrDefault(options.ACME))
tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME))
if err != nil {
return nil, err
}

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ const (
TCPTimeout = 15 * time.Second
ReadPayloadTimeout = 300 * time.Millisecond
DNSTimeout = 10 * time.Second
DirectDNSTimeout = 5 * time.Second
UDPTimeout = 5 * time.Minute
DefaultURLTestInterval = 3 * time.Minute
DefaultURLTestIdleTimeout = 30 * time.Minute

View File

@@ -30,10 +30,10 @@ var (
var _ adapter.DNSClient = (*Client)(nil)
type Client struct {
timeout time.Duration
disableCache bool
disableExpire bool
independentCache bool
clientSubnet netip.Prefix
rdrc adapter.RDRCStore
initRDRCFunc func() adapter.RDRCStore
logger logger.ContextLogger
@@ -42,27 +42,24 @@ type Client struct {
}
type ClientOptions struct {
Timeout time.Duration
DisableCache bool
DisableExpire bool
IndependentCache bool
CacheCapacity uint32
ClientSubnet netip.Prefix
RDRC func() adapter.RDRCStore
Logger logger.ContextLogger
}
func NewClient(options ClientOptions) *Client {
client := &Client{
timeout: options.Timeout,
disableCache: options.DisableCache,
disableExpire: options.DisableExpire,
independentCache: options.IndependentCache,
clientSubnet: options.ClientSubnet,
initRDRCFunc: options.RDRC,
logger: options.Logger,
}
if client.timeout == 0 {
client.timeout = C.DNSTimeout
}
cacheCapacity := options.CacheCapacity
if cacheCapacity < 1024 {
cacheCapacity = 1024
@@ -104,8 +101,12 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return &responseMessage, nil
}
question := message.Question[0]
if options.ClientSubnet.IsValid() {
message = SetClientSubnet(message, options.ClientSubnet)
clientSubnet := options.ClientSubnet
if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet
}
if clientSubnet.IsValid() {
message = SetClientSubnet(message, clientSubnet)
}
isSimpleRequest := len(message.Question) == 1 &&
len(message.Ns) == 0 &&
@@ -146,7 +147,15 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return nil, ErrResponseRejectedCached
}
}
ctx, cancel := context.WithTimeout(ctx, c.timeout)
timeout := options.Timeout
if timeout == 0 {
if transport.HasDetour() {
timeout = C.DNSTimeout
} else {
timeout = C.DirectDNSTimeout
}
}
ctx, cancel := context.WithTimeout(ctx, timeout)
response, err := transport.Exchange(ctx, message)
cancel()
if err != nil {

View File

@@ -55,6 +55,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
DisableExpire: options.DNSClientOptions.DisableExpire,
IndependentCache: options.DNSClientOptions.IndependentCache,
CacheCapacity: options.DNSClientOptions.CacheCapacity,
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}),
RDRC: func() adapter.RDRCStore {
cacheFile := service.FromContext[adapter.CacheFile](ctx)
if cacheFile == nil {
@@ -157,6 +158,9 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
if action.Strategy != C.DomainStrategyAsIS {
options.Strategy = action.Strategy
}
if action.Timeout > 0 {
options.Timeout = action.Timeout
}
if isFakeIP || action.DisableCache {
options.DisableCache = true
}
@@ -179,6 +183,9 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
if action.Strategy != C.DomainStrategyAsIS {
options.Strategy = action.Strategy
}
if action.Timeout > 0 {
options.Timeout = action.Timeout
}
if action.DisableCache {
options.DisableCache = true
}

View File

@@ -41,6 +41,7 @@ type Transport struct {
dns.TransportAdapter
ctx context.Context
dialer N.Dialer
hasDetour bool
logger logger.ContextLogger
networkManager adapter.NetworkManager
interfaceName string
@@ -59,6 +60,7 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeDHCP, tag, options.LocalDNSServerOptions),
ctx: ctx,
dialer: transportDialer,
hasDetour: options.Detour != "",
logger: logger,
networkManager: service.FromContext[adapter.NetworkManager](ctx),
interfaceName: options.Interface,
@@ -89,6 +91,10 @@ func (t *Transport) Close() error {
return nil
}
func (t *Transport) HasDetour() bool {
return t.hasDetour
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
err := t.fetchServers()
if err != nil {

View File

@@ -3,11 +3,15 @@ package transport
import (
"bytes"
"context"
"errors"
"io"
"net"
"net/http"
"net/url"
"os"
"strconv"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
@@ -39,11 +43,13 @@ func RegisterHTTPS(registry *dns.TransportRegistry) {
type HTTPSTransport struct {
dns.TransportAdapter
logger logger.ContextLogger
dialer N.Dialer
destination *url.URL
headers http.Header
transport *http.Transport
logger logger.ContextLogger
dialer N.Dialer
destination *url.URL
headers http.Header
transportAccess sync.Mutex
transport *http.Transport
transportResetAt time.Time
}
func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
@@ -161,12 +167,33 @@ func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
}
func (t *HTTPSTransport) Close() error {
t.transportAccess.Lock()
defer t.transportAccess.Unlock()
t.transport.CloseIdleConnections()
t.transport = t.transport.Clone()
return nil
}
func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
startAt := time.Now()
response, err := t.exchange(ctx, message)
if err != nil {
if errors.Is(err, os.ErrDeadlineExceeded) {
t.transportAccess.Lock()
defer t.transportAccess.Unlock()
if t.transportResetAt.After(startAt) {
return nil, err
}
t.transport.CloseIdleConnections()
t.transport = t.transport.Clone()
t.transportResetAt = time.Now()
}
return nil, err
}
return response, nil
}
func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
exMessage := *message
exMessage.Id = 0
exMessage.Compress = true

View File

@@ -20,7 +20,8 @@ import (
)
func dnsReadConfig(_ context.Context, _ string) *dnsConfig {
if C.res_init() != 0 {
var state C.res_state
if C.res_ninit(state) != 0 {
return &dnsConfig{
servers: defaultNS,
search: dnsDefaultSearch(),
@@ -33,10 +34,10 @@ func dnsReadConfig(_ context.Context, _ string) *dnsConfig {
conf := &dnsConfig{
ndots: 1,
timeout: 5 * time.Second,
attempts: int(C._res.retry),
attempts: int(state.retry),
}
for i := 0; i < int(C._res.nscount); i++ {
ns := C._res.nsaddr_list[i]
for i := 0; i < int(state.nscount); i++ {
ns := state.nsaddr_list[i]
addr := C.inet_ntoa(ns.sin_addr)
if addr == nil {
continue
@@ -44,7 +45,7 @@ func dnsReadConfig(_ context.Context, _ string) *dnsConfig {
conf.servers = append(conf.servers, C.GoString(addr))
}
for i := 0; ; i++ {
search := C._res.dnsrch[i]
search := state.dnsrch[i]
if search == nil {
break
}

View File

@@ -14,6 +14,7 @@ type TransportAdapter struct {
transportType string
transportTag string
dependencies []string
hasDetour bool
strategy C.DomainStrategy
clientSubnet netip.Prefix
}
@@ -35,6 +36,7 @@ func NewTransportAdapterWithLocalOptions(transportType string, transportTag stri
transportType: transportType,
transportTag: transportTag,
dependencies: dependencies,
hasDetour: localOptions.Detour != "",
strategy: C.DomainStrategy(localOptions.LegacyStrategy),
clientSubnet: localOptions.LegacyClientSubnet,
}
@@ -69,6 +71,10 @@ func (a *TransportAdapter) Dependencies() []string {
return a.dependencies
}
func (a *TransportAdapter) HasDetour() bool {
return a.hasDetour
}
func (a *TransportAdapter) LegacyStrategy() C.DomainStrategy {
return a.strategy
}

View File

@@ -2,6 +2,67 @@
icon: material/alert-decagram
---
#### 1.12.0-beta.30
* Fixes and improvements
### 1.11.14
* 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.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

View File

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

View File

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

View File

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

View File

@@ -59,7 +59,7 @@
{
"external_controller": "0.0.0.0:9090",
"external_ui": "dashboard"
// external_ui_download_detour: "direct"
// "external_ui_download_detour": "direct"
}
```

View File

@@ -59,7 +59,7 @@
{
"external_controller": "0.0.0.0:9090",
"external_ui": "dashboard"
// external_ui_download_detour: "direct"
// "external_ui_download_detour": "direct"
}
```

View File

@@ -1,7 +1,11 @@
---
icon: material/alert-decagram
icon: material/new-box
---
!!! quote "Changes in sing-box 1.12.0"
:material-plus: [loopback_address](#loopback_address)
!!! quote "Changes in sing-box 1.11.0"
:material-delete-alert: [gso](#gso)
@@ -56,9 +60,12 @@ icon: material/alert-decagram
"auto_route": true,
"iproute2_table_index": 2022,
"iproute2_rule_index": 9000,
"auto_redirect": false,
"auto_redirect": true,
"auto_redirect_input_mark": "0x2023",
"auto_redirect_output_mark": "0x2024",
"loopback_address": [
"10.7.0.1"
],
"strict_route": true,
"route_address": [
"0.0.0.0/1",
@@ -66,7 +73,6 @@ icon: material/alert-decagram
"::/1",
"8000::/1"
],
"route_exclude_address": [
"192.168.0.0/16",
"fc00::/7"
@@ -117,7 +123,6 @@ icon: material/alert-decagram
"match_domain": []
}
},
// Deprecated
"gso": false,
"inet4_address": [
@@ -140,8 +145,8 @@ icon: material/alert-decagram
"inet6_route_exclude_address": [
"fc00::/7"
],
... // Listen Fields
...
// Listen Fields
}
```
@@ -273,6 +278,16 @@ Connection output mark used by `auto_redirect`.
`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.7.0.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
Enforce strict routing rules when `auto_route` is enabled:

View File

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

View File

@@ -172,14 +172,12 @@ 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.
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`.
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.
Conflict with `tls_record_fragment`.
#### tls_fragment_fallback_delay
!!! question "Since sing-box 1.12.0"
@@ -194,11 +192,6 @@ The fallback value used when TLS segmentation cannot automatically determine the
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
```json

View File

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

View File

@@ -20,6 +20,7 @@ DERP service is a Tailscale DERP server, similar to [derper](https://pkg.go.dev/
"config_path": "",
"verify_client_endpoint": [],
"verify_client_url": [],
"home": "",
"mesh_with": [],
"mesh_psk": "",
"mesh_psk_file": "",
@@ -69,6 +70,10 @@ Setting Array value to a string `__URL__` is equivalent to configuring:
{ "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 other DERP servers.

View File

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

View File

@@ -19,6 +19,7 @@ See https://github.com/Shadowsocks-NET/shadowsocks-specs/blob/main/2023-1-shadow
... // Listen Fields
"servers": {},
"cache_path": "",
"tls": {}
}
```
@@ -37,7 +38,7 @@ A mapping Object from HTTP endpoints to [Shadowsocks Inbound](/configuration/inb
Selected Shadowsocks inbounds must be configured with [managed](/configuration/inbound/shadowsocks#managed) enabled.
Example:
Example:
```json
{
@@ -47,6 +48,11 @@ 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 configuration, see [TLS](/configuration/shared/tls/#inbound).

View File

@@ -4,6 +4,9 @@ icon: material/alert-decagram
!!! 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.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled)
@@ -82,6 +85,9 @@ icon: material/alert-decagram
"cipher_suites": [],
"certificate": "",
"certificate_path": "",
"fragment": false,
"fragment_fallback_delay": "",
"record_fragment": false,
"ech": {
"enabled": false,
"config": [],
@@ -313,6 +319,44 @@ The path to ECH configuration, in PEM format.
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
#### domain

View File

@@ -4,6 +4,9 @@ icon: material/alert-decagram
!!! 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.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled)
@@ -82,6 +85,9 @@ icon: material/alert-decagram
"cipher_suites": [],
"certificate": [],
"certificate_path": "",
"fragment": false,
"fragment_fallback_delay": "",
"record_fragment": false,
"ech": {
"enabled": false,
"pq_signature_schemes_enabled": false,
@@ -305,6 +311,41 @@ ECH PEM 配置路径
如果为 true则始终使用最大可能的 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 字段
#### domain

View File

@@ -94,18 +94,13 @@ flowchart TB
"servers": [
{
"tag": "google",
"address": "tls://8.8.8.8"
"type": "tls",
"server": "8.8.8.8"
},
{
"tag": "local",
"address": "223.5.5.5",
"detour": "direct"
}
],
"rules": [
{
"outbound": "any",
"server": "local"
"type": "udp",
"server": "223.5.5.5"
}
],
"strategy": "ipv4_only"
@@ -115,7 +110,8 @@ flowchart TB
"type": "tun",
"inet4_address": "172.19.0.1/30",
"auto_route": true,
"strict_route": false
// "auto_redirect": true, // On linux
"strict_route": true
}
],
"outbounds": [
@@ -123,25 +119,23 @@ flowchart TB
{
"type": "direct",
"tag": "direct"
},
{
"type": "dns",
"tag": "dns-out"
}
],
"route": {
"rules": [
{
"protocol": "dns",
"outbound": "dns-out"
"action": "sniff"
},
{
"geoip": [
"private"
],
"protocol": "dns",
"action": "hijack-dns"
},
{
"ip_is_private": true,
"outbound": "direct"
}
],
"default_domain_resolver": "local",
"auto_detect_interface": true
}
}
@@ -155,18 +149,13 @@ flowchart TB
"servers": [
{
"tag": "google",
"address": "tls://8.8.8.8"
"type": "tls",
"server": "8.8.8.8"
},
{
"tag": "local",
"address": "223.5.5.5",
"detour": "direct"
}
],
"rules": [
{
"outbound": "any",
"server": "local"
"type": "udp",
"server": "223.5.5.5"
}
]
},
@@ -176,7 +165,8 @@ flowchart TB
"inet4_address": "172.19.0.1/30",
"inet6_address": "fdfe:dcba:9876::1/126",
"auto_route": true,
"strict_route": false
// "auto_redirect": true, // On linux
"strict_route": true
}
],
"outbounds": [
@@ -184,25 +174,23 @@ flowchart TB
{
"type": "direct",
"tag": "direct"
},
{
"type": "dns",
"tag": "dns-out"
}
],
"route": {
"rules": [
{
"protocol": "dns",
"outbound": "dns-out"
"action": "sniff"
},
{
"geoip": [
"private"
],
"protocol": "dns",
"action": "hijack-dns"
},
{
"ip_is_private": true,
"outbound": "direct"
}
],
"default_domain_resolver": "local",
"auto_detect_interface": true
}
}
@@ -216,23 +204,22 @@ flowchart TB
"servers": [
{
"tag": "google",
"address": "tls://8.8.8.8"
"type": "tls",
"server": "8.8.8.8"
},
{
"tag": "local",
"address": "223.5.5.5",
"detour": "direct"
"type": "udp",
"server": "223.5.5.5"
},
{
"tag": "remote",
"address": "fakeip"
"type": "fakeip",
"inet4_range": "198.18.0.0/15",
"inet6_range": "fc00::/18"
}
],
"rules": [
{
"outbound": "any",
"server": "local"
},
{
"query_type": [
"A",
@@ -241,11 +228,6 @@ flowchart TB
"server": "remote"
}
],
"fakeip": {
"enabled": true,
"inet4_range": "198.18.0.0/15",
"inet6_range": "fc00::/18"
},
"independent_cache": true
},
"inbounds": [
@@ -254,6 +236,7 @@ flowchart TB
"inet4_address": "172.19.0.1/30",
"inet6_address": "fdfe:dcba:9876::1/126",
"auto_route": true,
// "auto_redirect": true, // On linux
"strict_route": true
}
],
@@ -262,25 +245,23 @@ flowchart TB
{
"type": "direct",
"tag": "direct"
},
{
"type": "dns",
"tag": "dns-out"
}
],
"route": {
"rules": [
{
"protocol": "dns",
"outbound": "dns-out"
"action": "sniff"
},
{
"geoip": [
"private"
],
"protocol": "dns",
"action": "hijack-dns"
},
{
"ip_is_private": true,
"outbound": "direct"
}
],
"default_domain_resolver": "local",
"auto_detect_interface": true
}
}
@@ -290,54 +271,6 @@ flowchart TB
=== ":material-dns: DNS rules"
```json
{
"dns": {
"servers": [
{
"tag": "google",
"address": "tls://8.8.8.8"
},
{
"tag": "local",
"address": "223.5.5.5",
"detour": "direct"
}
],
"rules": [
{
"outbound": "any",
"server": "local"
},
{
"clash_mode": "Direct",
"server": "local"
},
{
"clash_mode": "Global",
"server": "google"
},
{
"rule_set": "geosite-geolocation-cn",
"server": "local"
}
]
},
"route": {
"rule_set": [
{
"type": "remote",
"tag": "geosite-geolocation-cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs"
}
]
}
}
```
=== ":material-dns: DNS rules (Enhanced, but slower) (1.9.0+)"
=== ":material-shield-off: With DNS leaks"
```json
@@ -346,35 +279,20 @@ flowchart TB
"servers": [
{
"tag": "google",
"address": "tls://8.8.8.8"
"type": "tls",
"server": "8.8.8.8"
},
{
"tag": "local",
"address": "https://223.5.5.5/dns-query",
"detour": "direct"
"type": "https",
"server": "223.5.5.5"
}
],
"rules": [
{
"outbound": "any",
"server": "local"
},
{
"clash_mode": "Direct",
"server": "local"
},
{
"clash_mode": "Global",
"server": "google"
},
{
"rule_set": "geosite-geolocation-cn",
"server": "local"
},
{
"clash_mode": "Default",
"server": "google"
},
{
"type": "logical",
"mode": "and",
@@ -392,6 +310,7 @@ flowchart TB
]
},
"route": {
"default_domain_resolver": "local",
"rule_set": [
{
"type": "remote",
@@ -425,35 +344,24 @@ flowchart TB
}
```
=== ":material-security: Without DNS leaks, but slower (1.9.0-alpha.2+)"
=== ":material-security: Without DNS leaks, but slower"
```json
{
"dns": {
"servers": [
{
"tag": "google",
"address": "tls://8.8.8.8"
"type": "tls",
"server": "8.8.8.8"
},
{
"tag": "local",
"address": "https://223.5.5.5/dns-query",
"detour": "direct"
"type": "https",
"server": "223.5.5.5"
}
],
"rules": [
{
"outbound": "any",
"server": "local"
},
{
"clash_mode": "Direct",
"server": "local"
},
{
"clash_mode": "Global",
"server": "google"
},
{
"rule_set": "geosite-geolocation-cn",
"server": "local"
@@ -476,6 +384,7 @@ flowchart TB
]
},
"route": {
"default_domain_resolver": "local",
"rule_set": [
{
"type": "remote",
@@ -517,14 +426,13 @@ flowchart TB
{
"type": "direct",
"tag": "direct"
},
{
"type": "block",
"tag": "block"
}
],
"route": {
"rules": [
{
"action": "sniff"
},
{
"type": "logical",
"mode": "or",
@@ -536,20 +444,12 @@ flowchart TB
"port": 53
}
],
"outbound": "dns"
"action": "hijack-dns"
},
{
"ip_is_private": true,
"outbound": "direct"
},
{
"clash_mode": "Direct",
"outbound": "direct"
},
{
"clash_mode": "Global",
"outbound": "default"
},
{
"type": "logical",
"mode": "or",
@@ -565,12 +465,23 @@ flowchart TB
"protocol": "stun"
}
],
"outbound": "block"
"action": "reject"
},
{
"rule_set": [
"geoip-cn",
"geosite-geolocation-cn"
"rule_set": "geosite-geolocation-cn",
"outbound": "direct"
},
{
"type": "logical",
"mode": "and",
"rules": [
{
"rule_set": "geoip-cn"
},
{
"rule_set": "geosite-geolocation-!cn",
"invert": true
}
],
"outbound": "direct"
}
@@ -591,4 +502,4 @@ flowchart TB
]
}
}
```
```

10
go.mod
View File

@@ -30,12 +30,12 @@ require (
github.com/sagernet/quic-go v0.52.0-beta.1
github.com/sagernet/sing v0.6.11-0.20250521033217-30d675ea099b
github.com/sagernet/sing-mux v0.3.2
github.com/sagernet/sing-quic v0.5.0-beta.1
github.com/sagernet/sing-shadowsocks v0.2.7
github.com/sagernet/sing-shadowsocks2 v0.2.0
github.com/sagernet/sing-quic v0.5.0-beta.2
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
github.com/sagernet/sing-tun v0.6.6-0.20250428031943-0686f8c4f210
github.com/sagernet/sing-vmess v0.2.2-0.20250503051933-9b4cf17393f8
github.com/sagernet/sing-tun v0.6.10-0.20250630100036-8763c24e4935
github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88
github.com/sagernet/smux v1.5.34-mod.2
github.com/sagernet/tailscale v1.80.3-mod.5
github.com/sagernet/wireguard-go v0.0.1-beta.7

24
go.sum
View File

@@ -165,8 +165,6 @@ 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/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/quic-go v0.51.0-beta.5 h1:/mME3sJvQ8k/JKP0oC/9XoWrm0znO7hWXviB5yiipJY=
github.com/sagernet/quic-go v0.51.0-beta.5/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
github.com/sagernet/quic-go v0.52.0-beta.1 h1:hWkojLg64zjV+MJOvJU/kOeWndm3tiEfBLx5foisszs=
github.com/sagernet/quic-go v0.52.0-beta.1/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
@@ -174,20 +172,18 @@ github.com/sagernet/sing v0.6.11-0.20250521033217-30d675ea099b h1:ZjTCYPb5f7aHdf
github.com/sagernet/sing v0.6.11-0.20250521033217-30d675ea099b/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/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
github.com/sagernet/sing-quic v0.4.1-0.20250511050139-d459f561c9c3 h1:1J+s1yyZ8+YAYaClI+az8YuFgV9NGXUUCZnriKmos6w=
github.com/sagernet/sing-quic v0.4.1-0.20250511050139-d459f561c9c3/go.mod h1:Mv7CdSyLepmqoLT8rd88Qn3QMv5AbsgjEm3DvEhDVNE=
github.com/sagernet/sing-quic v0.5.0-beta.1 h1:nC0i/s8LhlZB8ev6laZCXF/uiwAE4kRdT4PcDdE4rI4=
github.com/sagernet/sing-quic v0.5.0-beta.1/go.mod h1:SAv/qdeDN+75msGG5U5ZIwG+3Ua50jVIKNrRSY8pkx0=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg=
github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-quic v0.5.0-beta.2 h1:j7KAbBuGmsKwSxVAQL5soJ+wDqxim4/llK2kxB0hSKk=
github.com/sagernet/sing-quic v0.5.0-beta.2/go.mod h1:SAv/qdeDN+75msGG5U5ZIwG+3Ua50jVIKNrRSY8pkx0=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.6.6-0.20250428031943-0686f8c4f210 h1:6H4BZaTqKI3YcDMyTV3E576LuJM4S4wY99xoq2T1ECw=
github.com/sagernet/sing-tun v0.6.6-0.20250428031943-0686f8c4f210/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-vmess v0.2.2-0.20250503051933-9b4cf17393f8 h1:zW+zAOCxUIqBCgnZiPovt1uQ3S+zBS+w0NGp+1zITGA=
github.com/sagernet/sing-vmess v0.2.2-0.20250503051933-9b4cf17393f8/go.mod h1:IL8Rr+EGwuqijszZkNrEFTQDKhilEpkqFqOlvdpS6/w=
github.com/sagernet/sing-tun v0.6.10-0.20250630100036-8763c24e4935 h1:wha4BG4mrEKaIoouVyiU5BcPfKD1n0LkiL4vqdjaVps=
github.com/sagernet/sing-tun v0.6.10-0.20250630100036-8763c24e4935/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.4-0.20250605032146-38cc72672c88/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/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc=
github.com/sagernet/tailscale v1.80.3-mod.5 h1:7V7z+p2C//TGtff20pPnDCt3qP6uFyY62peJoKF9z/A=

View File

@@ -3,6 +3,7 @@ package include
import (
"context"
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound"
@@ -39,6 +40,10 @@ import (
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 {
registry := inbound.NewRegistry()

View File

@@ -91,6 +91,7 @@ type DialerOptions struct {
type _DomainResolveOptions struct {
Server string `json:"server"`
Strategy DomainStrategy `json:"strategy,omitempty"`
Timeout badoption.Duration `json:"timeout,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
@@ -102,6 +103,7 @@ func (o DomainResolveOptions) MarshalJSON() ([]byte, error) {
if o.Server == "" {
return []byte("{}"), nil
} else if o.Strategy == DomainStrategy(C.DomainStrategyAsIS) &&
o.Timeout == 0 &&
!o.DisableCache &&
o.RewriteTTL == nil &&
o.ClientSubnet == nil {

View File

@@ -180,6 +180,7 @@ func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error {
type DNSRouteActionOptions struct {
Server string `json:"server,omitempty"`
Strategy DomainStrategy `json:"strategy,omitempty"`
Timeout badoption.Duration `json:"timeout,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
@@ -187,6 +188,7 @@ type DNSRouteActionOptions struct {
type _DNSRouteOptionsActionOptions struct {
Strategy DomainStrategy `json:"strategy,omitempty"`
Timeout badoption.Duration `json:"timeout,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`

View File

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

View File

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

View File

@@ -37,19 +37,22 @@ func (o *InboundTLSOptionsContainer) ReplaceInboundTLSOptions(options *InboundTL
}
type OutboundTLSOptions struct {
Enabled bool `json:"enabled,omitempty"`
DisableSNI bool `json:"disable_sni,omitempty"`
ServerName string `json:"server_name,omitempty"`
Insecure bool `json:"insecure,omitempty"`
ALPN badoption.Listable[string] `json:"alpn,omitempty"`
MinVersion string `json:"min_version,omitempty"`
MaxVersion string `json:"max_version,omitempty"`
CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"`
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
CertificatePath string `json:"certificate_path,omitempty"`
ECH *OutboundECHOptions `json:"ech,omitempty"`
UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
Reality *OutboundRealityOptions `json:"reality,omitempty"`
Enabled bool `json:"enabled,omitempty"`
DisableSNI bool `json:"disable_sni,omitempty"`
ServerName string `json:"server_name,omitempty"`
Insecure bool `json:"insecure,omitempty"`
ALPN badoption.Listable[string] `json:"alpn,omitempty"`
MinVersion string `json:"min_version,omitempty"`
MaxVersion string `json:"max_version,omitempty"`
CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"`
Certificate badoption.Listable[string] `json:"certificate,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"`
UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
Reality *OutboundRealityOptions `json:"reality,omitempty"`
}
type OutboundTLSOptionsContainer struct {

View File

@@ -20,6 +20,7 @@ type TunInboundOptions struct {
AutoRedirect bool `json:"auto_redirect,omitempty"`
AutoRedirectInputMark FwMark `json:"auto_redirect_input_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"`
RouteAddress badoption.Listable[netip.Prefix] `json:"route_address,omitempty"`
RouteAddressSet badoption.Listable[string] `json:"route_address_set,omitempty"`

View File

@@ -221,6 +221,14 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
}
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)
udpForwarder := tun.NewUDPForwarder(t.ctx, ipStack, t, t.udpTimeout)
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)

View File

@@ -190,6 +190,8 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
IPRoute2RuleIndex: ruleIndex,
AutoRedirectInputMark: inputMark,
AutoRedirectOutputMark: outputMark,
Inet4LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is4),
Inet6LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is6),
StrictRoute: options.StrictRoute,
IncludeInterface: options.IncludeInterface,
ExcludeInterface: options.ExcludeInterface,

View File

@@ -205,6 +205,10 @@ func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.
var metadata adapter.InboundContext
metadata.Source = source
metadata.Destination = destination
//nolint:staticcheck
metadata.InboundDetour = h.listener.ListenOptions().Detour
//nolint:staticcheck
metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
(*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose)
}

View File

@@ -219,6 +219,10 @@ func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.
var metadata adapter.InboundContext
metadata.Source = source
metadata.Destination = destination
//nolint:staticcheck
metadata.InboundDetour = h.listener.ListenOptions().Detour
//nolint:staticcheck
metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
(*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose)
}

View File

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

View File

@@ -90,14 +90,8 @@ func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, co
m.logger.ErrorContext(ctx, err)
return
}
if metadata.TLSFragment {
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)
if metadata.TLSFragment || metadata.TLSRecordFragment {
remoteConn = tf.NewConn(remoteConn, ctx, metadata.TLSFragment, metadata.TLSRecordFragment, metadata.TLSFragmentFallbackDelay)
}
m.access.Lock()
element := m.connections.PushBack(conn)

View File

@@ -2,7 +2,6 @@ package route
import (
"context"
"errors"
"net"
"time"
@@ -10,7 +9,7 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
dnsOutbound "github.com/sagernet/sing-box/protocol/dns"
"github.com/sagernet/sing-tun"
R "github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
@@ -36,7 +35,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) error {
func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
if natConn, isNatConn := conn.(udpnat.Conn); isNatConn {
metadata.Destination = M.Socksaddr{}
for _, packet := range packetBuffers {
@@ -51,10 +50,12 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB
conn: conn,
ctx: ctx,
metadata: metadata,
onClose: onClose,
})
return nil
}
err := dnsOutbound.NewDNSPacketConnection(ctx, r.dns, conn, packetBuffers, metadata)
N.CloseOnHandshakeFailure(conn, onClose, err)
if err != nil && !E.IsClosedOrCanceled(err) {
return E.Cause(err, "process DNS packet")
}
@@ -63,7 +64,7 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB
func ExchangeDNSPacket(ctx context.Context, router adapter.DNSRouter, logger logger.ContextLogger, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext, destination M.Socksaddr) {
err := exchangeDNSPacket(ctx, router, conn, buffer, metadata, destination)
if err != nil && !errors.Is(err, tun.ErrDrop) && !E.IsClosedOrCanceled(err) {
if err != nil && !R.IsRejected(err) && !E.IsClosedOrCanceled(err) {
logger.ErrorContext(ctx, E.Cause(err, "process DNS packet"))
}
}
@@ -93,8 +94,16 @@ type dnsHijacker struct {
conn N.PacketConn
ctx context.Context
metadata adapter.InboundContext
onClose N.CloseHandlerFunc
}
func (h *dnsHijacker) NewPacketEx(buffer *buf.Buffer, destination M.Socksaddr) {
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

@@ -76,6 +76,7 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOp
DomainResolver: defaultDomainResolver.Server,
DomainResolveOptions: adapter.DNSQueryOptions{
Strategy: C.DomainStrategy(defaultDomainResolver.Strategy),
Timeout: time.Duration(defaultDomainResolver.Timeout),
DisableCache: defaultDomainResolver.DisableCache,
RewriteTTL: defaultDomainResolver.RewriteTTL,
ClientSubnet: defaultDomainResolver.ClientSubnet.Build(netip.Prefix{}),

View File

@@ -15,7 +15,7 @@ import (
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/route/rule"
R "github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing-mux"
"github.com/sagernet/sing-vmess"
"github.com/sagernet/sing/common"
@@ -49,7 +49,7 @@ func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata
err := r.routeConnection(ctx, conn, metadata, onClose)
if err != nil {
N.CloseOnHandshakeFailure(conn, onClose, err)
if E.IsClosedOrCanceled(err) {
if E.IsClosedOrCanceled(err) || R.IsRejected(err) {
r.logger.DebugContext(ctx, "connection closed: ", err)
} else {
r.logger.ErrorContext(ctx, err)
@@ -99,7 +99,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
var selectedOutbound adapter.Outbound
if selectedRule != nil {
switch action := selectedRule.Action().(type) {
case *rule.RuleActionRoute:
case *R.RuleActionRoute:
var loaded bool
selectedOutbound, loaded = r.outbound.Outbound(action.Outbound)
if !loaded {
@@ -110,14 +110,15 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
buf.ReleaseMulti(buffers)
return E.New("TCP is not supported by outbound: ", selectedOutbound.Tag())
}
case *rule.RuleActionReject:
case *R.RuleActionReject:
buf.ReleaseMulti(buffers)
return action.Error(ctx)
case *rule.RuleActionHijackDNS:
case *R.RuleActionHijackDNS:
for _, buffer := range buffers {
conn = bufio.NewCachedConn(conn, buffer)
}
return r.hijackDNSStream(ctx, conn, metadata)
N.CloseOnHandshakeFailure(conn, onClose, r.hijackDNSStream(ctx, conn, metadata))
return nil
}
}
if selectedRule == nil {
@@ -150,7 +151,7 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
}))
if err != nil {
conn.Close()
if E.IsClosedOrCanceled(err) {
if E.IsClosedOrCanceled(err) || R.IsRejected(err) {
r.logger.DebugContext(ctx, "connection closed: ", err)
} else {
r.logger.ErrorContext(ctx, err)
@@ -167,13 +168,11 @@ func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn,
err := r.routePacketConnection(ctx, conn, metadata, onClose)
if err != nil {
N.CloseOnHandshakeFailure(conn, onClose, err)
if E.IsClosedOrCanceled(err) {
if E.IsClosedOrCanceled(err) || R.IsRejected(err) {
r.logger.DebugContext(ctx, "connection closed: ", err)
} else {
r.logger.ErrorContext(ctx, err)
}
} else if onClose != nil {
onClose(nil)
}
}
@@ -215,7 +214,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
var selectReturn bool
if selectedRule != nil {
switch action := selectedRule.Action().(type) {
case *rule.RuleActionRoute:
case *R.RuleActionRoute:
var loaded bool
selectedOutbound, loaded = r.outbound.Outbound(action.Outbound)
if !loaded {
@@ -226,12 +225,11 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
N.ReleaseMultiPacketBuffer(packetBuffers)
return E.New("UDP is not supported by outbound: ", selectedOutbound.Tag())
}
case *rule.RuleActionReject:
case *R.RuleActionReject:
N.ReleaseMultiPacketBuffer(packetBuffers)
return action.Error(ctx)
case *rule.RuleActionHijackDNS:
return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata)
case *R.RuleActionHijackDNS:
return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose)
}
}
if selectedRule == nil || selectReturn {
@@ -268,7 +266,7 @@ func (r *Router) PreMatch(metadata adapter.InboundContext) error {
if selectedRule == nil {
return nil
}
rejectAction, isReject := selectedRule.Action().(*rule.RuleActionReject)
rejectAction, isReject := selectedRule.Action().(*R.RuleActionReject)
if !isReject {
return nil
}
@@ -344,7 +342,7 @@ func (r *Router) matchRule(
//nolint:staticcheck
if metadata.InboundOptions != common.DefaultValue[option.InboundOptions]() {
if !preMatch && metadata.InboundOptions.SniffEnabled {
newBuffer, newPackerBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{
newBuffer, newPackerBuffers, newErr := r.actionSniff(ctx, metadata, &R.RuleActionSniff{
OverrideDestination: metadata.InboundOptions.SniffOverrideDestination,
Timeout: time.Duration(metadata.InboundOptions.SniffTimeout),
}, inputConn, inputPacketConn, nil)
@@ -359,7 +357,7 @@ func (r *Router) matchRule(
}
}
if C.DomainStrategy(metadata.InboundOptions.DomainStrategy) != C.DomainStrategyAsIS {
fatalErr = r.actionResolve(ctx, metadata, &rule.RuleActionResolve{
fatalErr = r.actionResolve(ctx, metadata, &R.RuleActionResolve{
Strategy: C.DomainStrategy(metadata.InboundOptions.DomainStrategy),
})
if fatalErr != nil {
@@ -396,11 +394,11 @@ match:
}
}
}
var routeOptions *rule.RuleActionRouteOptions
var routeOptions *R.RuleActionRouteOptions
switch action := currentRule.Action().(type) {
case *rule.RuleActionRoute:
case *R.RuleActionRoute:
routeOptions = &action.RuleActionRouteOptions
case *rule.RuleActionRouteOptions:
case *R.RuleActionRouteOptions:
routeOptions = action
}
if routeOptions != nil {
@@ -453,7 +451,7 @@ match:
}
}
switch action := currentRule.Action().(type) {
case *rule.RuleActionSniff:
case *R.RuleActionSniff:
if !preMatch {
newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn, buffers)
if newErr != nil {
@@ -470,7 +468,7 @@ match:
selectedRuleIndex = currentRuleIndex
break match
}
case *rule.RuleActionResolve:
case *R.RuleActionResolve:
fatalErr = r.actionResolve(ctx, metadata, action)
if fatalErr != nil {
return
@@ -490,7 +488,7 @@ match:
}
func (r *Router) actionSniff(
ctx context.Context, metadata *adapter.InboundContext, action *rule.RuleActionSniff,
ctx context.Context, metadata *adapter.InboundContext, action *R.RuleActionSniff,
inputConn net.Conn, inputPacketConn N.PacketConn, inputBuffers []*buf.Buffer,
) (buffer *buf.Buffer, packetBuffers []*N.PacketBuffer, fatalErr error) {
if sniff.Skip(metadata) {
@@ -501,7 +499,12 @@ func (r *Router) actionSniff(
return
}
if inputConn != nil {
sniffBuffer := buf.NewPacket()
if len(action.StreamSniffers) == 0 && len(action.PacketSniffers) > 0 {
return
} else if metadata.SniffError != nil && !errors.Is(metadata.SniffError, sniff.ErrNeedMoreData) {
r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.SniffError)
return
}
var streamSniffers []sniff.StreamSniffer
if len(action.StreamSniffers) > 0 {
streamSniffers = action.StreamSniffers
@@ -515,6 +518,7 @@ func (r *Router) actionSniff(
sniff.RDP,
}
}
sniffBuffer := buf.NewPacket()
err := sniff.PeekStream(
ctx,
metadata,
@@ -524,6 +528,7 @@ func (r *Router) actionSniff(
action.Timeout,
streamSniffers...,
)
metadata.SniffError = err
if err == nil {
//goland:noinspection GoDeprecation
if action.OverrideDestination && M.IsDomainName(metadata.Domain) {
@@ -546,9 +551,25 @@ func (r *Router) actionSniff(
sniffBuffer.Release()
}
} else if inputPacketConn != nil {
if metadata.PacketSniffError != nil && !errors.Is(metadata.PacketSniffError, sniff.ErrNeedMoreData) {
r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.PacketSniffError)
if len(action.PacketSniffers) == 0 && len(action.StreamSniffers) > 0 {
return
} else if metadata.SniffError != nil && !errors.Is(metadata.SniffError, sniff.ErrNeedMoreData) {
r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.SniffError)
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 {
var (
@@ -581,7 +602,7 @@ func (r *Router) actionSniff(
return
}
} else {
if len(packetBuffers) > 0 || metadata.PacketSniffError != nil {
if len(packetBuffers) > 0 || metadata.SniffError != nil {
err = sniff.PeekPacket(
ctx,
metadata,
@@ -589,20 +610,6 @@ func (r *Router) actionSniff(
sniff.QUICClientHello,
)
} 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(
ctx, metadata,
sniffBuffer.Bytes(),
@@ -615,7 +622,7 @@ func (r *Router) actionSniff(
Destination: destination,
}
packetBuffers = append(packetBuffers, packetBuffer)
metadata.PacketSniffError = err
metadata.SniffError = err
if errors.Is(err, sniff.ErrNeedMoreData) {
// TODO: replace with generic message when there are more multi-packet protocols
r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello")
@@ -646,7 +653,7 @@ func (r *Router) actionSniff(
return
}
func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundContext, action *rule.RuleActionResolve) error {
func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundContext, action *R.RuleActionResolve) error {
if metadata.Destination.IsFqdn() {
var transport adapter.DNSTransport
if action.Server != "" {
@@ -659,6 +666,7 @@ func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundCon
addresses, err := r.dns.Lookup(adapter.WithContext(ctx, metadata), metadata.Destination.Fqdn, adapter.DNSQueryOptions{
Transport: transport,
Strategy: action.Strategy,
Timeout: action.Timeout,
DisableCache: action.DisableCache,
RewriteTTL: action.RewriteTTL,
ClientSubnet: action.ClientSubnet,

View File

@@ -2,6 +2,7 @@ package rule
import (
"context"
"errors"
"net/netip"
"strings"
"sync"
@@ -112,6 +113,7 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction)
Server: action.RouteOptions.Server,
RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{
Strategy: C.DomainStrategy(action.RouteOptions.Strategy),
Timeout: time.Duration(action.RouteOptions.Timeout),
DisableCache: action.RouteOptions.DisableCache,
RewriteTTL: action.RouteOptions.RewriteTTL,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)),
@@ -120,6 +122,7 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction)
case C.RuleActionTypeRouteOptions:
return &RuleActionDNSRouteOptions{
Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy),
Timeout: time.Duration(action.RouteOptionsOptions.Timeout),
DisableCache: action.RouteOptionsOptions.DisableCache,
RewriteTTL: action.RouteOptionsOptions.RewriteTTL,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptionsOptions.ClientSubnet)),
@@ -234,20 +237,13 @@ func (r *RuleActionDNSRoute) Type() string {
func (r *RuleActionDNSRoute) String() string {
var descriptions []string
descriptions = append(descriptions, r.Server)
if r.DisableCache {
descriptions = append(descriptions, "disable-cache")
}
if r.RewriteTTL != nil {
descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL))
}
if r.ClientSubnet.IsValid() {
descriptions = append(descriptions, F.ToString("client-subnet=", r.ClientSubnet))
}
descriptions = append(descriptions, r.Descriptions()...)
return F.ToString("route(", strings.Join(descriptions, ","), ")")
}
type RuleActionDNSRouteOptions struct {
Strategy C.DomainStrategy
Timeout time.Duration
DisableCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
@@ -258,7 +254,17 @@ func (r *RuleActionDNSRouteOptions) Type() string {
}
func (r *RuleActionDNSRouteOptions) String() string {
return F.ToString("route-options(", strings.Join(r.Descriptions(), ","), ")")
}
func (r *RuleActionDNSRouteOptions) Descriptions() []string {
var descriptions []string
if r.Strategy != C.DomainStrategyAsIS {
descriptions = append(descriptions, F.ToString("strategy=", option.DomainStrategy(r.Strategy)))
}
if r.Timeout > 0 {
descriptions = append(descriptions, F.ToString("timeout=", r.Timeout.String()))
}
if r.DisableCache {
descriptions = append(descriptions, "disable-cache")
}
@@ -268,7 +274,7 @@ func (r *RuleActionDNSRouteOptions) String() string {
if r.ClientSubnet.IsValid() {
descriptions = append(descriptions, F.ToString("client-subnet=", r.ClientSubnet))
}
return F.ToString("route-options(", strings.Join(descriptions, ","), ")")
return descriptions
}
type RuleActionDirect struct {
@@ -284,6 +290,23 @@ func (r *RuleActionDirect) String() string {
return "direct" + r.description
}
type RejectedError struct {
Cause error
}
func (r *RejectedError) Error() string {
return "rejected"
}
func (r *RejectedError) Unwrap() error {
return r.Cause
}
func IsRejected(err error) bool {
var rejected *RejectedError
return errors.As(err, &rejected)
}
type RuleActionReject struct {
Method string
NoDrop bool
@@ -307,9 +330,9 @@ func (r *RuleActionReject) Error(ctx context.Context) error {
var returnErr error
switch r.Method {
case C.RuleActionRejectMethodDefault:
returnErr = syscall.ECONNREFUSED
returnErr = &RejectedError{syscall.ECONNREFUSED}
case C.RuleActionRejectMethodDrop:
return tun.ErrDrop
return &RejectedError{tun.ErrDrop}
default:
panic(F.ToString("unknown reject method: ", r.Method))
}
@@ -327,7 +350,7 @@ func (r *RuleActionReject) Error(ctx context.Context) error {
if ctx != nil {
r.logger.DebugContext(ctx, "dropped due to flooding")
}
return tun.ErrDrop
return &RejectedError{tun.ErrDrop}
}
return returnErr
}
@@ -403,6 +426,7 @@ func (r *RuleActionSniff) String() string {
type RuleActionResolve struct {
Server string
Strategy C.DomainStrategy
Timeout time.Duration
DisableCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
@@ -420,6 +444,9 @@ func (r *RuleActionResolve) String() string {
if r.Strategy != C.DomainStrategyAsIS {
options = append(options, F.ToString(option.DomainStrategy(r.Strategy)))
}
if r.Timeout > 0 {
options = append(options, F.ToString("timeout=", r.Timeout.String()))
}
if r.DisableCache {
options = append(options, "disable_cache")
}

View File

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

222
service/ssmapi/cache.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

@@ -1,22 +0,0 @@
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)
}
return w.writer.WriteBuffer(buffer)
return wrapWsError(w.writer.WriteBuffer(buffer))
}
func (w *Writer) FrontHeadroom() int {