Compare commits

..

152 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
世界
41226a6075 Fix interface finder 2025-05-23 10:57:38 +08:00
世界
81d32181ce Fix update route address set 2025-05-20 19:46:54 +08:00
世界
c5ecca3938 Bump version 2025-05-18 16:48:44 +08:00
世界
900888731c Fix DNS reject response 2025-05-13 18:05:31 +08:00
世界
13e648e4b1 Fix set edns0 subnet 2025-05-07 15:12:17 +08:00
世界
aff12ff671 Bump version 2025-05-05 09:37:47 +08:00
世界
101fb88255 Fix allocator put 2025-05-05 09:37:44 +08:00
世界
8b489354e4 Undeprecate the block outbound 2025-05-04 18:45:53 +08:00
世界
7dea6eb7a6 Fix missing read waiter for cancelers 2025-05-04 18:14:21 +08:00
世界
af1bfe4e3e Make rule_set.format optional 2025-05-04 18:14:21 +08:00
世界
d574e9eb52 Update smux to v1.5.34 2025-04-30 19:39:15 +08:00
世界
2d7df1e1f2 Fix hysteria bytes format 2025-04-29 20:45:19 +08:00
世界
1c0ffcf5b1 Fix counter position in auto redirect dnat rules 2025-04-28 11:20:23 +08:00
177 changed files with 5729 additions and 1349 deletions

View File

@@ -8,11 +8,15 @@
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues" --deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
--no-deb-generate-changes --no-deb-generate-changes
--config-files /etc/sing-box/config.json --config-files /etc/sing-box/config.json
--after-install release/config/sing-box.postinst
release/config/config.json=/etc/sing-box/config.json release/config/config.json=/etc/sing-box/config.json
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish

View File

@@ -46,7 +46,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.2 go-version: ^1.24
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -109,7 +109,7 @@ jobs:
if: ${{ ! matrix.legacy_go }} if: ${{ ! matrix.legacy_go }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.2 go-version: ^1.24
- name: Cache Legacy Go - name: Cache Legacy Go
if: matrix.require_legacy_go if: matrix.require_legacy_go
id: cache-legacy-go id: cache-legacy-go
@@ -140,7 +140,7 @@ jobs:
- name: Set build tags - name: Set build tags
run: | run: |
set -xeuo pipefail set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api,with_tailscale' TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build - name: Build
if: matrix.os != 'android' if: matrix.os != 'android'
@@ -294,7 +294,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.2 go-version: ^1.24
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -374,7 +374,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.2 go-version: ^1.24
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -472,15 +472,15 @@ jobs:
if: matrix.if if: matrix.if
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.2 go-version: ^1.24
- name: Setup Xcode stable - name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next' if: matrix.if && github.ref == 'refs/heads/main-next'
run: |- run: |-
sudo xcode-select -s /Applications/Xcode_16.2.app sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Setup Xcode beta - name: Setup Xcode beta
if: matrix.if && github.ref == 'refs/heads/dev-next' if: matrix.if && github.ref == 'refs/heads/dev-next'
run: |- run: |-
sudo xcode-select -s /Applications/Xcode_16.2.app sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Set tag - name: Set tag
if: matrix.if if: matrix.if
run: |- run: |-
@@ -615,7 +615,7 @@ jobs:
path: 'dist' path: 'dist'
upload: upload:
name: Upload builds 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 runs-on: ubuntu-latest
needs: needs:
- calculate_version - calculate_version

View File

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

View File

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

View File

@@ -21,14 +21,13 @@ linters-settings:
- -SA1003 - -SA1003
run: run:
go: "1.24" go: "1.23"
build-tags: build-tags:
- with_gvisor - with_gvisor
- with_quic - with_quic
- with_dhcp - with_dhcp
- with_wireguard - with_wireguard
- with_utls - with_utls
- with_reality_server
- with_acme - with_acme
- with_clash_api - with_clash_api

View File

@@ -15,7 +15,6 @@ builds:
- with_dhcp - with_dhcp
- with_wireguard - with_wireguard
- with_utls - with_utls
- with_reality_server
- with_acme - with_acme
- with_clash_api - with_clash_api
- with_tailscale - with_tailscale
@@ -56,6 +55,12 @@ nfpms:
dst: /usr/lib/systemd/system/sing-box.service dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service - src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service dst: /usr/lib/systemd/system/sing-box@.service
- src: release/config/sing-box.sysusers
dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
- src: release/completions/sing-box.bash - src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash dst: /usr/share/bash-completion/completions/sing-box.bash

View File

@@ -17,7 +17,6 @@ builds:
- with_dhcp - with_dhcp
- with_wireguard - with_wireguard
- with_utls - with_utls
- with_reality_server
- with_acme - with_acme
- with_clash_api - with_clash_api
- with_tailscale - with_tailscale
@@ -47,7 +46,6 @@ builds:
- with_dhcp - with_dhcp
- with_wireguard - with_wireguard
- with_utls - with_utls
- with_reality_server
- with_acme - with_acme
- with_clash_api - with_clash_api
- with_tailscale - with_tailscale
@@ -138,6 +136,12 @@ nfpms:
dst: /usr/lib/systemd/system/sing-box.service dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service - src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service dst: /usr/lib/systemd/system/sing-box@.service
- src: release/config/sing-box.sysusers
dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
- src: release/completions/sing-box.bash - src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash dst: /usr/share/bash-completion/completions/sing-box.bash

View File

@@ -13,7 +13,7 @@ RUN set -ex \
&& export COMMIT=$(git rev-parse --short HEAD) \ && export COMMIT=$(git rev-parse --short HEAD) \
&& export VERSION=$(go run ./cmd/internal/read_tag) \ && export VERSION=$(go run ./cmd/internal/read_tag) \
&& go build -v -trimpath -tags \ && go build -v -trimpath -tags \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api,with_tailscale" \ "with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale" \
-o /go/bin/sing-box \ -o /go/bin/sing-box \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \ -ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
./cmd/sing-box ./cmd/sing-box

View File

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

View File

@@ -3,11 +3,14 @@ package adapter
import ( import (
"context" "context"
"net/netip" "net/netip"
"time"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@@ -31,11 +34,32 @@ type DNSClient interface {
} }
type DNSQueryOptions struct { type DNSQueryOptions struct {
Transport DNSTransport Transport DNSTransport
Strategy C.DomainStrategy Strategy C.DomainStrategy
DisableCache bool LookupStrategy C.DomainStrategy
RewriteTTL *uint32 Timeout time.Duration
ClientSubnet netip.Prefix DisableCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
}
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
if options == nil {
return &DNSQueryOptions{}, nil
}
transportManager := service.FromContext[DNSTransportManager](ctx)
transport, loaded := transportManager.Transport(options.Server)
if !loaded {
return nil, E.New("domain resolver not found: " + options.Server)
}
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{}),
}, nil
} }
type RDRCStore interface { type RDRCStore interface {
@@ -49,6 +73,7 @@ type DNSTransport interface {
Type() string Type() string
Tag() string Tag() string
Dependencies() []string Dependencies() []string
HasDetour() bool
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error) Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
} }

View File

@@ -7,7 +7,7 @@ import (
) )
type FakeIPStore interface { type FakeIPStore interface {
Service SimpleLifecycle
Contains(address netip.Addr) bool Contains(address netip.Addr) bool
Create(domain string, isIPv6 bool) (netip.Addr, error) Create(domain string, isIPv6 bool) (netip.Addr, error)
Lookup(address netip.Addr) (string, bool) Lookup(address netip.Addr) (string, bool)

View File

@@ -53,11 +53,11 @@ type InboundContext struct {
// sniffer // sniffer
Protocol string Protocol string
Domain string Domain string
Client string Client string
SniffContext any SniffContext any
PacketSniffError error SniffError error
// cache // cache
@@ -74,6 +74,7 @@ type InboundContext struct {
UDPTimeout time.Duration UDPTimeout time.Duration
TLSFragment bool TLSFragment bool
TLSFragmentFallbackDelay time.Duration TLSFragmentFallbackDelay time.Duration
TLSRecordFragment bool
NetworkStrategy *C.NetworkStrategy NetworkStrategy *C.NetworkStrategy
NetworkType []C.InterfaceType NetworkType []C.InterfaceType

View File

@@ -37,13 +37,14 @@ func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endp
func (m *Manager) Start(stage adapter.StartStage) error { func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock() m.access.Lock()
defer m.access.Unlock()
if m.started && m.stage >= stage { if m.started && m.stage >= stage {
panic("already started") panic("already started")
} }
m.started = true m.started = true
m.stage = stage m.stage = stage
for _, inbound := range m.inbounds { inbounds := m.inbounds
m.access.Unlock()
for _, inbound := range inbounds {
err := adapter.LegacyStart(inbound, stage) err := adapter.LegacyStart(inbound, stage)
if err != nil { if err != nil {
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]") return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")

View File

@@ -2,6 +2,11 @@ package adapter
import E "github.com/sagernet/sing/common/exceptions" import E "github.com/sagernet/sing/common/exceptions"
type SimpleLifecycle interface {
Start() error
Close() error
}
type StartStage uint8 type StartStage uint8
const ( const (

View File

@@ -28,14 +28,14 @@ func LegacyStart(starter any, stage StartStage) error {
} }
type lifecycleServiceWrapper struct { type lifecycleServiceWrapper struct {
Service SimpleLifecycle
name string name string
} }
func NewLifecycleService(service Service, name string) LifecycleService { func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService {
return &lifecycleServiceWrapper{ return &lifecycleServiceWrapper{
Service: service, SimpleLifecycle: service,
name: name, name: name,
} }
} }
@@ -44,9 +44,9 @@ func (l *lifecycleServiceWrapper) Name() string {
} }
func (l *lifecycleServiceWrapper) Start(stage StartStage) error { func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
return LegacyStart(l.Service, stage) return LegacyStart(l.SimpleLifecycle, stage)
} }
func (l *lifecycleServiceWrapper) Close() error { func (l *lifecycleServiceWrapper) Close() error {
return l.Service.Close() return l.SimpleLifecycle.Close()
} }

View File

@@ -11,7 +11,7 @@ type HeadlessRule interface {
type Rule interface { type Rule interface {
HeadlessRule HeadlessRule
Service SimpleLifecycle
Type() string Type() string
Action() RuleAction Action() RuleAction
} }

View File

@@ -1,6 +1,27 @@
package adapter package adapter
import (
"context"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
type Service interface { type Service interface {
Start() error Lifecycle
Close() error Type() string
Tag() string
}
type ServiceRegistry interface {
option.ServiceOptionsRegistry
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) (Service, error)
}
type ServiceManager interface {
Lifecycle
Services() []Service
Get(tag string) (Service, bool)
Remove(tag string) error
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error
} }

View File

@@ -0,0 +1,21 @@
package service
type Adapter struct {
serviceType string
serviceTag string
}
func NewAdapter(serviceType string, serviceTag string) Adapter {
return Adapter{
serviceType: serviceType,
serviceTag: serviceTag,
}
}
func (a *Adapter) Type() string {
return a.serviceType
}
func (a *Adapter) Tag() string {
return a.serviceTag
}

144
adapter/service/manager.go Normal file
View File

@@ -0,0 +1,144 @@
package service
import (
"context"
"os"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
var _ adapter.ServiceManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.ServiceRegistry
access sync.Mutex
started bool
stage adapter.StartStage
services []adapter.Service
serviceByTag map[string]adapter.Service
}
func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
serviceByTag: make(map[string]adapter.Service),
}
}
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
services := m.services
m.access.Unlock()
for _, service := range services {
err := adapter.LegacyStart(service, stage)
if err != nil {
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
}
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
services := m.services
m.services = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, service := range services {
monitor.Start("close service/", service.Type(), "[", service.Tag(), "]")
err = E.Append(err, service.Close(), func(err error) error {
return E.Cause(err, "close service/", service.Type(), "[", service.Tag(), "]")
})
monitor.Finish()
}
return nil
}
func (m *Manager) Services() []adapter.Service {
m.access.Lock()
defer m.access.Unlock()
return m.services
}
func (m *Manager) Get(tag string) (adapter.Service, bool) {
m.access.Lock()
service, found := m.serviceByTag[tag]
m.access.Unlock()
return service, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
service, found := m.serviceByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.serviceByTag, tag)
index := common.Index(m.services, func(it adapter.Service) bool {
return it == service
})
if index == -1 {
panic("invalid service index")
}
m.services = append(m.services[:index], m.services[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return service.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error {
service, err := m.registry.Create(ctx, logger, tag, serviceType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(service, stage)
if err != nil {
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
}
}
}
if existsService, loaded := m.serviceByTag[tag]; loaded {
if m.started {
err = existsService.Close()
if err != nil {
return E.Cause(err, "close service/", existsService.Type(), "[", existsService.Tag(), "]")
}
}
existsIndex := common.Index(m.services, func(it adapter.Service) bool {
return it == existsService
})
if existsIndex == -1 {
panic("invalid service index")
}
m.services = append(m.services[:existsIndex], m.services[existsIndex+1:]...)
}
m.services = append(m.services, service)
m.serviceByTag[tag] = service
return nil
}

View File

@@ -0,0 +1,72 @@
package service
import (
"context"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type ConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.Service, error)
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
registry.register(outboundType, func() any {
return new(Options)
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.Service, error) {
var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
})
}
var _ adapter.ServiceRegistry = (*Registry)(nil)
type (
optionsConstructorFunc func() any
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.Service, error)
)
type Registry struct {
access sync.Mutex
optionsType map[string]optionsConstructorFunc
constructor map[string]constructorFunc
}
func NewRegistry() *Registry {
return &Registry{
optionsType: make(map[string]optionsConstructorFunc),
constructor: make(map[string]constructorFunc),
}
}
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
m.access.Lock()
defer m.access.Unlock()
optionsConstructor, loaded := m.optionsType[outboundType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Service, error) {
m.access.Lock()
defer m.access.Unlock()
constructor, loaded := m.constructor[outboundType]
if !loaded {
return nil, E.New("outbound type not found: " + outboundType)
}
return constructor(ctx, logger, tag, options)
}
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
m.access.Lock()
defer m.access.Unlock()
m.optionsType[outboundType] = optionsConstructor
m.constructor[outboundType] = constructor
}

18
adapter/ssm.go Normal file
View File

@@ -0,0 +1,18 @@
package adapter
import (
"net"
N "github.com/sagernet/sing/common/network"
)
type ManagedSSMServer interface {
Inbound
SetTracker(tracker SSMTracker)
UpdateUsers(users []string, uPSKs []string) error
}
type SSMTracker interface {
TrackConnection(conn net.Conn, metadata InboundContext) net.Conn
TrackPacketConnection(conn N.PacketConn, metadata InboundContext) N.PacketConn
}

View File

@@ -3,6 +3,6 @@ package adapter
import "time" import "time"
type TimeService interface { type TimeService interface {
Service SimpleLifecycle
TimeFunc() func() time.Time TimeFunc() func() time.Time
} }

125
box.go
View File

@@ -12,6 +12,7 @@ import (
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/adapter/outbound"
boxService "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/common/certificate" "github.com/sagernet/sing-box/common/certificate"
"github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/taskmonitor" "github.com/sagernet/sing-box/common/taskmonitor"
@@ -34,22 +35,23 @@ import (
"github.com/sagernet/sing/service/pause" "github.com/sagernet/sing/service/pause"
) )
var _ adapter.Service = (*Box)(nil) var _ adapter.SimpleLifecycle = (*Box)(nil)
type Box struct { type Box struct {
createdAt time.Time createdAt time.Time
logFactory log.Factory logFactory log.Factory
logger log.ContextLogger logger log.ContextLogger
network *route.NetworkManager network *route.NetworkManager
endpoint *endpoint.Manager endpoint *endpoint.Manager
inbound *inbound.Manager inbound *inbound.Manager
outbound *outbound.Manager outbound *outbound.Manager
dnsTransport *dns.TransportManager service *boxService.Manager
dnsRouter *dns.Router dnsTransport *dns.TransportManager
connection *route.ConnectionManager dnsRouter *dns.Router
router *route.Router connection *route.ConnectionManager
services []adapter.LifecycleService router *route.Router
done chan struct{} internalService []adapter.LifecycleService
done chan struct{}
} }
type Options struct { type Options struct {
@@ -64,6 +66,7 @@ func Context(
outboundRegistry adapter.OutboundRegistry, outboundRegistry adapter.OutboundRegistry,
endpointRegistry adapter.EndpointRegistry, endpointRegistry adapter.EndpointRegistry,
dnsTransportRegistry adapter.DNSTransportRegistry, dnsTransportRegistry adapter.DNSTransportRegistry,
serviceRegistry adapter.ServiceRegistry,
) context.Context { ) context.Context {
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil || if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.InboundRegistry](ctx) == nil { service.FromContext[adapter.InboundRegistry](ctx) == nil {
@@ -84,6 +87,10 @@ func Context(
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry) ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry) ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
} }
if service.FromContext[adapter.ServiceRegistry](ctx) == nil {
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
}
return ctx return ctx
} }
@@ -99,6 +106,7 @@ func New(options Options) (*Box, error) {
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx) inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx) outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx) dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
if endpointRegistry == nil { if endpointRegistry == nil {
return nil, E.New("missing endpoint registry in context") return nil, E.New("missing endpoint registry in context")
@@ -109,6 +117,12 @@ func New(options Options) (*Box, error) {
if outboundRegistry == nil { if outboundRegistry == nil {
return nil, E.New("missing outbound registry in context") return nil, E.New("missing outbound registry in context")
} }
if dnsTransportRegistry == nil {
return nil, E.New("missing DNS transport registry in context")
}
if serviceRegistry == nil {
return nil, E.New("missing service registry in context")
}
ctx = pause.WithDefaultManager(ctx) ctx = pause.WithDefaultManager(ctx)
experimentalOptions := common.PtrValueOrDefault(options.Experimental) experimentalOptions := common.PtrValueOrDefault(options.Experimental)
@@ -142,7 +156,7 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "create log factory") return nil, E.Cause(err, "create log factory")
} }
var services []adapter.LifecycleService var internalServices []adapter.LifecycleService
certificateOptions := common.PtrValueOrDefault(options.Certificate) certificateOptions := common.PtrValueOrDefault(options.Certificate)
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem || if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
len(certificateOptions.Certificate) > 0 || len(certificateOptions.Certificate) > 0 ||
@@ -153,7 +167,7 @@ func New(options Options) (*Box, error) {
return nil, err return nil, err
} }
service.MustRegister[adapter.CertificateStore](ctx, certificateStore) service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
services = append(services, certificateStore) internalServices = append(internalServices, certificateStore)
} }
routeOptions := common.PtrValueOrDefault(options.Route) routeOptions := common.PtrValueOrDefault(options.Route)
@@ -162,10 +176,12 @@ func New(options Options) (*Box, error) {
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager) inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final) outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final) dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
service.MustRegister[adapter.EndpointManager](ctx, endpointManager) service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager) service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager) service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager) service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions) dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter) service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions) networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
@@ -280,6 +296,24 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize outbound[", i, "]") return nil, E.Cause(err, "initialize outbound[", i, "]")
} }
} }
for i, serviceOptions := range options.Services {
var tag string
if serviceOptions.Tag != "" {
tag = serviceOptions.Tag
} else {
tag = F.ToString(i)
}
err = serviceManager.Create(
ctx,
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
tag,
serviceOptions.Type,
serviceOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize service[", i, "]")
}
}
outboundManager.Initialize(common.Must1( outboundManager.Initialize(common.Must1(
direct.NewOutbound( direct.NewOutbound(
ctx, ctx,
@@ -305,7 +339,7 @@ func New(options Options) (*Box, error) {
if needCacheFile { if needCacheFile {
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
service.MustRegister[adapter.CacheFile](ctx, cacheFile) service.MustRegister[adapter.CacheFile](ctx, cacheFile)
services = append(services, cacheFile) internalServices = append(internalServices, cacheFile)
} }
if needClashAPI { if needClashAPI {
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
@@ -316,7 +350,7 @@ func New(options Options) (*Box, error) {
} }
router.AppendTracker(clashServer) router.AppendTracker(clashServer)
service.MustRegister[adapter.ClashServer](ctx, clashServer) service.MustRegister[adapter.ClashServer](ctx, clashServer)
services = append(services, clashServer) internalServices = append(internalServices, clashServer)
} }
if needV2RayAPI { if needV2RayAPI {
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI)) v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
@@ -325,7 +359,7 @@ func New(options Options) (*Box, error) {
} }
if v2rayServer.StatsService() != nil { if v2rayServer.StatsService() != nil {
router.AppendTracker(v2rayServer.StatsService()) router.AppendTracker(v2rayServer.StatsService())
services = append(services, v2rayServer) internalServices = append(internalServices, v2rayServer)
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer) service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
} }
} }
@@ -343,22 +377,23 @@ func New(options Options) (*Box, error) {
WriteToSystem: ntpOptions.WriteToSystem, WriteToSystem: ntpOptions.WriteToSystem,
}) })
timeService.TimeService = ntpService timeService.TimeService = ntpService
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service")) internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
} }
return &Box{ return &Box{
network: networkManager, network: networkManager,
endpoint: endpointManager, endpoint: endpointManager,
inbound: inboundManager, inbound: inboundManager,
outbound: outboundManager, outbound: outboundManager,
dnsTransport: dnsTransportManager, dnsTransport: dnsTransportManager,
dnsRouter: dnsRouter, service: serviceManager,
connection: connectionManager, dnsRouter: dnsRouter,
router: router, connection: connectionManager,
createdAt: createdAt, router: router,
logFactory: logFactory, createdAt: createdAt,
logger: logFactory.Logger(), logFactory: logFactory,
services: services, logger: logFactory.Logger(),
done: make(chan struct{}), internalService: internalServices,
done: make(chan struct{}),
}, nil }, nil
} }
@@ -408,11 +443,11 @@ func (s *Box) preStart() error {
if err != nil { if err != nil {
return E.Cause(err, "start logger") return E.Cause(err, "start logger")
} }
err = adapter.StartNamed(adapter.StartStateInitialize, s.services) // cache-file clash-api v2ray-api err = adapter.StartNamed(adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint) err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
if err != nil { if err != nil {
return err return err
} }
@@ -428,31 +463,27 @@ func (s *Box) start() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.StartNamed(adapter.StartStateStart, s.services) err = adapter.StartNamed(adapter.StartStateStart, s.internalService)
if err != nil { if err != nil {
return err return err
} }
err = s.inbound.Start(adapter.StartStateStart) err = adapter.Start(adapter.StartStateStart, s.inbound, s.endpoint, s.service)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateStart, s.endpoint) err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint) err = adapter.StartNamed(adapter.StartStatePostStart, s.internalService)
if err != nil { if err != nil {
return err return err
} }
err = adapter.StartNamed(adapter.StartStatePostStart, s.services) err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint) err = adapter.StartNamed(adapter.StartStateStarted, s.internalService)
if err != nil {
return err
}
err = adapter.StartNamed(adapter.StartStateStarted, s.services)
if err != nil { if err != nil {
return err return err
} }
@@ -467,9 +498,9 @@ func (s *Box) Close() error {
close(s.done) close(s.done)
} }
err := common.Close( 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.services { for _, lifecycleService := range s.internalService {
err = E.Append(err, lifecycleService.Close(), func(err error) error { err = E.Append(err, lifecycleService.Close(), func(err error) error {
return E.Cause(err, "close ", lifecycleService.Name()) return E.Cause(err, "close ", lifecycleService.Name())
}) })

View File

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

View File

@@ -16,15 +16,17 @@ import (
) )
var ( var (
debugEnabled bool debugEnabled bool
target string target string
platform string platform string
withTailscale bool
) )
func init() { func init() {
flag.BoolVar(&debugEnabled, "debug", false, "enable debug") flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
flag.StringVar(&target, "target", "android", "target platform") flag.StringVar(&target, "target", "android", "target platform")
flag.StringVar(&platform, "platform", "", "specify platform") flag.StringVar(&platform, "platform", "", "specify platform")
flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS")
} }
func main() { func main() {
@@ -59,8 +61,8 @@ func init() {
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=") sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag) debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api") sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack")
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack") iosTags = append(iosTags, "with_dhcp", "with_low_memory")
memcTags = append(memcTags, "with_tailscale") memcTags = append(memcTags, "with_tailscale")
debugTags = append(debugTags, "debug") debugTags = append(debugTags, "debug")
} }
@@ -151,7 +153,9 @@ func buildApple() {
"-v", "-v",
"-target", bindTarget, "-target", bindTarget,
"-libname=box", "-libname=box",
"-tags-macos=" + strings.Join(memcTags, ","), }
if !withTailscale {
args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
} }
if !debugEnabled { if !debugEnabled {
@@ -161,6 +165,9 @@ func buildApple() {
} }
tags := append(sharedTags, iosTags...) tags := append(sharedTags, iosTags...)
if withTailscale {
tags = append(tags, memcTags...)
}
if debugEnabled { if debugEnabled {
tags = append(tags, debugTags...) tags = append(tags, debugTags...)
} }

View File

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

View File

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

View File

@@ -6,7 +6,10 @@ import (
"strings" "strings"
"github.com/sagernet/sing-box/common/srs" "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/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -50,6 +53,11 @@ func decompileRuleSet(sourcePath string) error {
if err != nil { if err != nil {
return err 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 var outputPath string
if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput { if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".srs") { if strings.HasSuffix(sourcePath, ".srs") {
@@ -75,3 +83,19 @@ func decompileRuleSet(sourcePath string) error {
outputFile.Close() outputFile.Close()
return nil 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

@@ -5,6 +5,7 @@ import (
"context" "context"
"io" "io"
"os" "os"
"path/filepath"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/srs" "github.com/sagernet/sing-box/common/srs"
@@ -56,6 +57,14 @@ func ruleSetMatch(sourcePath string, domain string) error {
if err != nil { if err != nil {
return E.Cause(err, "read rule-set") return E.Cause(err, "read rule-set")
} }
if flagRuleSetMatchFormat == "" {
switch filepath.Ext(sourcePath) {
case ".json":
flagRuleSetMatchFormat = C.RuleSetFormatSource
case ".srs":
flagRuleSetMatchFormat = C.RuleSetFormatBinary
}
}
var ruleSet option.PlainRuleSetCompat var ruleSet option.PlainRuleSetCompat
switch flagRuleSetMatchFormat { switch flagRuleSetMatchFormat {
case C.RuleSetFormatSource: case C.RuleSetFormatSource:

View File

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

View File

@@ -2,6 +2,7 @@ package adguard
import ( import (
"bufio" "bufio"
"bytes"
"io" "io"
"net/netip" "net/netip"
"os" "os"
@@ -9,10 +10,10 @@ import (
"strings" "strings"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
) )
@@ -27,7 +28,7 @@ type agdguardRuleLine struct {
isImportant bool 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) scanner := bufio.NewScanner(reader)
var ( var (
ruleLines []agdguardRuleLine ruleLines []agdguardRuleLine
@@ -36,7 +37,10 @@ func Convert(reader io.Reader) ([]option.HeadlessRule, error) {
parseLine: parseLine:
for scanner.Scan() { for scanner.Scan() {
ruleLine := scanner.Text() ruleLine := scanner.Text()
if ruleLine == "" || ruleLine[0] == '!' || ruleLine[0] == '#' { if ruleLine == "" {
continue
}
if strings.HasPrefix(ruleLine, "!") || strings.HasPrefix(ruleLine, "#") {
continue continue
} }
originRuleLine := ruleLine originRuleLine := ruleLine
@@ -92,7 +96,7 @@ parseLine:
} }
if !ignored { if !ignored {
ignoredLines++ ignoredLines++
log.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", ruleLine) logger.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", originRuleLine)
continue parseLine continue parseLine
} }
} }
@@ -120,27 +124,35 @@ parseLine:
ruleLine = ruleLine[1 : len(ruleLine)-1] ruleLine = ruleLine[1 : len(ruleLine)-1]
if ignoreIPCIDRRegexp(ruleLine) { if ignoreIPCIDRRegexp(ruleLine) {
ignoredLines++ ignoredLines++
log.Debug("ignored unsupported rule with IPCIDR regexp: ", ruleLine) logger.Debug("ignored unsupported rule with IPCIDR regexp: ", originRuleLine)
continue continue
} }
isRegexp = true isRegexp = true
} else { } else {
if strings.Contains(ruleLine, "://") { if strings.Contains(ruleLine, "://") {
ruleLine = common.SubstringAfter(ruleLine, "://") ruleLine = common.SubstringAfter(ruleLine, "://")
isSuffix = true
} }
if strings.Contains(ruleLine, "/") { if strings.Contains(ruleLine, "/") {
ignoredLines++ ignoredLines++
log.Debug("ignored unsupported rule with path: ", ruleLine) logger.Debug("ignored unsupported rule with path: ", originRuleLine)
continue continue
} }
if strings.Contains(ruleLine, "##") { if strings.Contains(ruleLine, "?") || strings.Contains(ruleLine, "&") {
ignoredLines++ ignoredLines++
log.Debug("ignored unsupported rule with element hiding: ", ruleLine) logger.Debug("ignored unsupported rule with query: ", originRuleLine)
continue 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++ 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 continue
} }
var domainCheck string var domainCheck string
@@ -151,7 +163,7 @@ parseLine:
} }
if ruleLine == "" { if ruleLine == "" {
ignoredLines++ ignoredLines++
log.Debug("ignored unsupported rule with empty domain", originRuleLine) logger.Debug("ignored unsupported rule with empty domain", originRuleLine)
continue continue
} else { } else {
domainCheck = strings.ReplaceAll(domainCheck, "*", "x") domainCheck = strings.ReplaceAll(domainCheck, "*", "x")
@@ -159,13 +171,13 @@ parseLine:
_, ipErr := parseADGuardIPCIDRLine(ruleLine) _, ipErr := parseADGuardIPCIDRLine(ruleLine)
if ipErr == nil { if ipErr == nil {
ignoredLines++ ignoredLines++
log.Debug("ignored unsupported rule with IPCIDR: ", ruleLine) logger.Debug("ignored unsupported rule with IPCIDR: ", originRuleLine)
continue continue
} }
if M.ParseSocksaddr(domainCheck).Port != 0 { if M.ParseSocksaddr(domainCheck).Port != 0 {
log.Debug("ignored unsupported rule with port: ", ruleLine) logger.Debug("ignored unsupported rule with port: ", originRuleLine)
} else { } else {
log.Debug("ignored unsupported rule with invalid domain: ", ruleLine) logger.Debug("ignored unsupported rule with invalid domain: ", originRuleLine)
} }
ignoredLines++ ignoredLines++
continue 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 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 { func ignoreIPCIDRRegexp(ruleLine string) bool {
if strings.HasPrefix(ruleLine, "(http?:\\/\\/)") { if strings.HasPrefix(ruleLine, "(http?:\\/\\/)") {
ruleLine = ruleLine[12:] ruleLine = ruleLine[12:]
@@ -294,11 +408,9 @@ func ignoreIPCIDRRegexp(ruleLine string) bool {
ruleLine = ruleLine[13:] ruleLine = ruleLine[13:]
} else if strings.HasPrefix(ruleLine, "^") { } else if strings.HasPrefix(ruleLine, "^") {
ruleLine = ruleLine[1:] ruleLine = ruleLine[1:]
} else {
return false
} }
_, parseErr := strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8) return common.Error(strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8)) == nil ||
return parseErr == nil common.Error(strconv.ParseUint(common.SubstringBefore(ruleLine, "."), 10, 8)) == nil
} }
func parseAdGuardHostLine(ruleLine string) (string, error) { func parseAdGuardHostLine(ruleLine string) (string, error) {

View File

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

View File

@@ -66,11 +66,17 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
interfaceFinder = control.NewDefaultInterfaceFinder() interfaceFinder = control.NewDefaultInterfaceFinder()
} }
if options.BindInterface != "" { if options.BindInterface != "" {
if !(C.IsLinux || C.IsDarwin || C.IsWindows) {
return nil, E.New("`bind_interface` is only supported on Linux, macOS and Windows")
}
bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1) bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1)
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} }
if options.RoutingMark > 0 { if options.RoutingMark > 0 {
if !C.IsLinux {
return nil, E.New("`routing_mark` is only supported on Linux")
}
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false)) dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false)) listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
} }
@@ -91,10 +97,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
} else if networkManager.AutoDetectInterface() { } else if networkManager.AutoDetectInterface() {
if platformInterface != nil { if platformInterface != nil {
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy) networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
if networkStrategy == nil {
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
defaultNetworkStrategy = true
}
networkType = common.Map(options.NetworkType, option.InterfaceType.Build) networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build) fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 { if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 {
@@ -106,6 +108,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 { if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
networkFallbackDelay = defaultOptions.FallbackDelay networkFallbackDelay = defaultOptions.FallbackDelay
} }
if networkStrategy == nil {
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
defaultNetworkStrategy = true
}
bindFunc := networkManager.ProtectFunc() bindFunc := networkManager.ProtectFunc()
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)

View File

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

View File

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

View File

@@ -1,158 +0,0 @@
package humanize
import (
"fmt"
"math"
"strconv"
"strings"
"unicode"
)
// IEC Sizes.
// kibis of bits
const (
Byte = 1 << (iota * 10)
KiByte
MiByte
GiByte
TiByte
PiByte
EiByte
)
// SI Sizes.
const (
IByte = 1
KByte = IByte * 1000
MByte = KByte * 1000
GByte = MByte * 1000
TByte = GByte * 1000
PByte = TByte * 1000
EByte = PByte * 1000
)
var defaultSizeTable = map[string]uint64{
"b": Byte,
"kib": KiByte,
"kb": KByte,
"mib": MiByte,
"mb": MByte,
"gib": GiByte,
"gb": GByte,
"tib": TiByte,
"tb": TByte,
"pib": PiByte,
"pb": PByte,
"eib": EiByte,
"eb": EByte,
// Without suffix
"": Byte,
"ki": KiByte,
"k": KByte,
"mi": MiByte,
"m": MByte,
"gi": GiByte,
"g": GByte,
"ti": TiByte,
"t": TByte,
"pi": PiByte,
"p": PByte,
"ei": EiByte,
"e": EByte,
}
var memorysSizeTable = map[string]uint64{
"b": Byte,
"kb": KiByte,
"mb": MiByte,
"gb": GiByte,
"tb": TiByte,
"pb": PiByte,
"eb": EiByte,
"": Byte,
"k": KiByte,
"m": MiByte,
"g": GiByte,
"t": TiByte,
"p": PiByte,
"e": EiByte,
}
var (
defaultSizes = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
iSizes = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
)
func Bytes(s uint64) string {
return humanateBytes(s, 1000, defaultSizes)
}
func MemoryBytes(s uint64) string {
return humanateBytes(s, 1024, defaultSizes)
}
func IBytes(s uint64) string {
return humanateBytes(s, 1024, iSizes)
}
func logn(n, b float64) float64 {
return math.Log(n) / math.Log(b)
}
func humanateBytes(s uint64, base float64, sizes []string) string {
if s < 10 {
return fmt.Sprintf("%d B", s)
}
e := math.Floor(logn(float64(s), base))
suffix := sizes[int(e)]
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
f := "%.0f %s"
if val < 10 {
f = "%.1f %s"
}
return fmt.Sprintf(f, val, suffix)
}
func ParseBytes(s string) (uint64, error) {
return parseBytes0(s, defaultSizeTable)
}
func ParseMemoryBytes(s string) (uint64, error) {
return parseBytes0(s, memorysSizeTable)
}
func parseBytes0(s string, sizeTable map[string]uint64) (uint64, error) {
lastDigit := 0
hasComma := false
for _, r := range s {
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
break
}
if r == ',' {
hasComma = true
}
lastDigit++
}
num := s[:lastDigit]
if hasComma {
num = strings.Replace(num, ",", "", -1)
}
f, err := strconv.ParseFloat(num, 64)
if err != nil {
return 0, err
}
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
if m, ok := sizeTable[extra]; ok {
f *= float64(m)
if f >= math.MaxUint64 {
return 0, fmt.Errorf("too large: %v", s)
}
return uint64(f), nil
}
return 0, fmt.Errorf("unhandled size name: %v", extra)
}

View File

@@ -32,6 +32,7 @@ type Listener struct {
disablePacketOutput bool disablePacketOutput bool
setSystemProxy bool setSystemProxy bool
systemProxySOCKS bool systemProxySOCKS bool
tproxy bool
tcpListener net.Listener tcpListener net.Listener
systemProxy settings.SystemProxy systemProxy settings.SystemProxy
@@ -54,6 +55,7 @@ type Options struct {
DisablePacketOutput bool DisablePacketOutput bool
SetSystemProxy bool SetSystemProxy bool
SystemProxySOCKS bool SystemProxySOCKS bool
TProxy bool
} }
func New( func New(
@@ -71,6 +73,7 @@ func New(
disablePacketOutput: options.DisablePacketOutput, disablePacketOutput: options.DisablePacketOutput,
setSystemProxy: options.SetSystemProxy, setSystemProxy: options.SetSystemProxy,
systemProxySOCKS: options.SystemProxySOCKS, systemProxySOCKS: options.SystemProxySOCKS,
tproxy: options.TProxy,
} }
} }

View File

@@ -3,14 +3,18 @@ package listener
import ( import (
"net" "net"
"net/netip" "net/netip"
"syscall"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/redir"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
"github.com/metacubex/tfo-go" "github.com/metacubex/tfo-go"
) )
@@ -23,6 +27,15 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
var err error var err error
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort) bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
var listenConfig net.ListenConfig var listenConfig net.ListenConfig
if l.listenOptions.BindInterface != "" {
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
}
if l.listenOptions.RoutingMark != 0 {
listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
}
if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
}
if l.listenOptions.TCPKeepAlive >= 0 { if l.listenOptions.TCPKeepAlive >= 0 {
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive) keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
if keepIdle == 0 { if keepIdle == 0 {
@@ -40,6 +53,13 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
} }
setMultiPathTCP(&listenConfig) setMultiPathTCP(&listenConfig)
} }
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).IsIPv4(), false)
})
})
}
tcpListener, err := ListenNetworkNamespace[net.Listener](l.listenOptions.NetNs, func() (net.Listener, error) { tcpListener, err := ListenNetworkNamespace[net.Listener](l.listenOptions.NetNs, func() (net.Listener, error) {
if l.listenOptions.TCPFastOpen { if l.listenOptions.TCPFastOpen {
var tfoConfig tfo.ListenConfig var tfoConfig tfo.ListenConfig

View File

@@ -5,17 +5,30 @@ import (
"net" "net"
"net/netip" "net/netip"
"os" "os"
"syscall"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/redir"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
) )
func (l *Listener) ListenUDP() (net.PacketConn, error) { func (l *Listener) ListenUDP() (net.PacketConn, error) {
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort) bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
var lc net.ListenConfig var listenConfig net.ListenConfig
if l.listenOptions.BindInterface != "" {
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
}
if l.listenOptions.RoutingMark != 0 {
listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
}
if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
}
var udpFragment bool var udpFragment bool
if l.listenOptions.UDPFragment != nil { if l.listenOptions.UDPFragment != nil {
udpFragment = *l.listenOptions.UDPFragment udpFragment = *l.listenOptions.UDPFragment
@@ -23,10 +36,17 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
udpFragment = l.listenOptions.UDPFragmentDefault udpFragment = l.listenOptions.UDPFragmentDefault
} }
if !udpFragment { if !udpFragment {
lc.Control = control.Append(lc.Control, control.DisableUDPFragment()) listenConfig.Control = control.Append(listenConfig.Control, control.DisableUDPFragment())
}
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).IsIPv4(), true)
})
})
} }
udpConn, err := ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) { udpConn, err := ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
return lc.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String()) return listenConfig.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -37,8 +57,32 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
return udpConn, err return udpConn, err
} }
func (l *Listener) DialContext(dialer net.Dialer, ctx context.Context, network string, address string) (net.Conn, error) {
return ListenNetworkNamespace[net.Conn](l.listenOptions.NetNs, func() (net.Conn, error) {
if l.listenOptions.BindInterface != "" {
dialer.Control = control.Append(dialer.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
}
if l.listenOptions.RoutingMark != 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
}
if l.listenOptions.ReuseAddr {
dialer.Control = control.Append(dialer.Control, control.ReuseAddr())
}
return dialer.DialContext(ctx, network, address)
})
}
func (l *Listener) ListenPacket(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.PacketConn, error) { func (l *Listener) ListenPacket(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.PacketConn, error) {
return ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) { return ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
if l.listenOptions.BindInterface != "" {
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
}
if l.listenOptions.RoutingMark != 0 {
listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
}
if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
}
return listenConfig.ListenPacket(ctx, network, address) return listenConfig.ListenPacket(ctx, network, address)
}) })
} }

View File

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

View File

@@ -12,7 +12,7 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
func TProxy(fd uintptr, isIPv6 bool) error { func TProxy(fd uintptr, isIPv6 bool, isUDP bool) error {
err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
if err == nil { if err == nil {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1) err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1)
@@ -20,11 +20,13 @@ func TProxy(fd uintptr, isIPv6 bool) error {
if err == nil && isIPv6 { if err == nil && isIPv6 {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_TRANSPARENT, 1) err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_TRANSPARENT, 1)
} }
if err == nil { if isUDP {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1) if err == nil {
} err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1)
if err == nil && isIPv6 { }
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_RECVORIGDSTADDR, 1) if err == nil && isIPv6 {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_RECVORIGDSTADDR, 1)
}
} }
return err return err
} }

View File

@@ -9,7 +9,7 @@ import (
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
) )
func TProxy(fd uintptr, isIPv6 bool) error { func TProxy(fd uintptr, isIPv6 bool, isUDP bool) error {
return os.ErrInvalid return os.ErrInvalid
} }

View File

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

View File

@@ -5,13 +5,13 @@ package tls
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"os"
"strings" "strings"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/libdns/alidns" "github.com/libdns/alidns"
@@ -37,7 +37,38 @@ func (w *acmeWrapper) Close() error {
return nil return nil
} }
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, 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 var acmeServer string
switch options.Provider { switch options.Provider {
case "", "letsencrypt": case "", "letsencrypt":
@@ -58,14 +89,15 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
} else { } else {
storage = certmagic.Default.Storage storage = certmagic.Default.Storage
} }
zapLogger := zap.New(zapcore.NewCore(
zapcore.NewConsoleEncoder(encoderConfig()),
&acmeLogWriter{logger: logger},
zap.DebugLevel,
))
config := &certmagic.Config{ config := &certmagic.Config{
DefaultServerName: options.DefaultServerName, DefaultServerName: options.DefaultServerName,
Storage: storage, Storage: storage,
Logger: zap.New(zapcore.NewCore( Logger: zapLogger,
zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()),
os.Stderr,
zap.InfoLevel,
)),
} }
acmeConfig := certmagic.ACMEIssuer{ acmeConfig := certmagic.ACMEIssuer{
CA: acmeServer, CA: acmeServer,
@@ -75,7 +107,7 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
DisableTLSALPNChallenge: options.DisableTLSALPNChallenge, DisableTLSALPNChallenge: options.DisableTLSALPNChallenge,
AltHTTPPort: int(options.AlternativeHTTPPort), AltHTTPPort: int(options.AlternativeHTTPPort),
AltTLSALPNPort: int(options.AlternativeTLSPort), AltTLSALPNPort: int(options.AlternativeTLSPort),
Logger: config.Logger, Logger: zapLogger,
} }
if dnsOptions := options.DNS01Challenge; dnsOptions != nil && dnsOptions.Provider != "" { if dnsOptions := options.DNS01Challenge; dnsOptions != nil && dnsOptions.Provider != "" {
var solver certmagic.DNS01Solver 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) { GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) {
return config, nil return config, nil
}, },
Logger: zapLogger,
}) })
config = certmagic.New(cache, *config) config = certmagic.New(cache, *config)
var tlsConfig *tls.Config var tlsConfig *tls.Config

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import (
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
func parseECHClientConfig(ctx context.Context, 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.") return nil, E.New("ECH requires go1.24, please recompile your binary.")
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ var errInsecureUnused = E.New("tls: insecure unused")
type STDServerConfig struct { type STDServerConfig struct {
config *tls.Config config *tls.Config
logger log.Logger logger log.Logger
acmeService adapter.Service acmeService adapter.SimpleLifecycle
certificate []byte certificate []byte
key []byte key []byte
certificatePath string certificatePath string
@@ -165,11 +165,11 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
return nil, nil return nil, nil
} }
var tlsConfig *tls.Config var tlsConfig *tls.Config
var acmeService adapter.Service var acmeService adapter.SimpleLifecycle
var err error var err error
if options.ACME != nil && len(options.ACME.Domain) > 0 { if options.ACME != nil && len(options.ACME.Domain) > 0 {
//nolint:staticcheck //nolint:staticcheck
tlsConfig, acmeService, err = startACME(ctx, common.PtrValueOrDefault(options.ACME)) tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

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

View File

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

View File

@@ -1,12 +1,15 @@
package tf package tf
import ( import (
"bytes"
"context" "context"
"encoding/binary"
"math/rand" "math/rand"
"net" "net"
"strings" "strings"
"time" "time"
C "github.com/sagernet/sing-box/constant"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"golang.org/x/net/publicsuffix" "golang.org/x/net/publicsuffix"
@@ -17,17 +20,24 @@ type Conn struct {
tcpConn *net.TCPConn tcpConn *net.TCPConn
ctx context.Context ctx context.Context
firstPacketWritten bool firstPacketWritten bool
splitPacket bool
splitRecord bool
fallbackDelay time.Duration fallbackDelay time.Duration
} }
func NewConn(conn net.Conn, ctx context.Context, fallbackDelay time.Duration) (*Conn, error) { 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) tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn)
return &Conn{ return &Conn{
Conn: conn, Conn: conn,
tcpConn: tcpConn, tcpConn: tcpConn,
ctx: ctx, ctx: ctx,
splitPacket: splitPacket,
splitRecord: splitRecord,
fallbackDelay: fallbackDelay, fallbackDelay: fallbackDelay,
}, nil }
} }
func (c *Conn) Write(b []byte) (n int, err error) { func (c *Conn) Write(b []byte) (n int, err error) {
@@ -37,10 +47,12 @@ func (c *Conn) Write(b []byte) (n int, err error) {
}() }()
serverName := indexTLSServerName(b) serverName := indexTLSServerName(b)
if serverName != nil { if serverName != nil {
if c.tcpConn != nil { if c.splitPacket {
err = c.tcpConn.SetNoDelay(true) if c.tcpConn != nil {
if err != nil { err = c.tcpConn.SetNoDelay(true)
return if err != nil {
return
}
} }
} }
splits := strings.Split(serverName.ServerName, ".") splits := strings.Split(serverName.ServerName, ".")
@@ -61,26 +73,50 @@ func (c *Conn) Write(b []byte) (n int, err error) {
currentIndex++ currentIndex++
} }
} }
var buffer bytes.Buffer
for i := 0; i <= len(splitIndexes); i++ { for i := 0; i <= len(splitIndexes); i++ {
var payload []byte var payload []byte
if i == 0 { if i == 0 {
payload = b[:splitIndexes[i]] payload = b[:splitIndexes[i]]
if c.splitRecord {
payload = payload[recordLayerHeaderLen:]
}
} else if i == len(splitIndexes) { } else if i == len(splitIndexes) {
payload = b[splitIndexes[i-1]:] payload = b[splitIndexes[i-1]:]
} else { } else {
payload = b[splitIndexes[i-1]:splitIndexes[i]] payload = b[splitIndexes[i-1]:splitIndexes[i]]
} }
if c.tcpConn != nil && i != len(splitIndexes) { if c.splitRecord {
err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay) if c.splitPacket {
if err != nil { buffer.Reset()
return
} }
} else { payloadLen := uint16(len(payload))
_, err = c.Conn.Write(payload) buffer.Write(b[:3])
if err != nil { binary.Write(&buffer, binary.BigEndian, payloadLen)
return buffer.Write(payload)
if c.splitPacket {
payload = buffer.Bytes()
} }
} }
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 && !c.splitPacket {
_, err = c.Conn.Write(buffer.Bytes())
if err != nil {
return
}
} }
if c.tcpConn != nil { if c.tcpConn != nil {
err = c.tcpConn.SetNoDelay(false) err = c.tcpConn.SetNoDelay(false)

View File

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

@@ -25,6 +25,9 @@ const (
TypeTUIC = "tuic" TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2" TypeHysteria2 = "hysteria2"
TypeTailscale = "tailscale" TypeTailscale = "tailscale"
TypeDERP = "derp"
TypeResolved = "resolved"
TypeSSMAPI = "ssm-api"
) )
const ( const (

View File

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

View File

@@ -24,9 +24,9 @@ func applyDebugOptions(options option.DebugOptions) {
if options.TraceBack != "" { if options.TraceBack != "" {
debug.SetTraceback(options.TraceBack) debug.SetTraceback(options.TraceBack)
} }
if options.MemoryLimit != 0 { if options.MemoryLimit.Value() != 0 {
debug.SetMemoryLimit(int64(float64(options.MemoryLimit) / 1.5)) debug.SetMemoryLimit(int64(float64(options.MemoryLimit.Value()) / 1.5))
conntrack.MemoryLimit = uint64(options.MemoryLimit) conntrack.MemoryLimit = options.MemoryLimit.Value()
} }
if options.OOMKiller != nil { if options.OOMKiller != nil {
conntrack.KillerEnabled = *options.OOMKiller conntrack.KillerEnabled = *options.OOMKiller

View File

@@ -7,9 +7,9 @@ import (
"runtime/debug" "runtime/debug"
"strings" "strings"
"github.com/sagernet/sing-box/common/humanize"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/byteformats"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson" "github.com/sagernet/sing/common/json/badjson"
@@ -38,9 +38,9 @@ func applyDebugListenOption(options option.DebugOptions) {
runtime.ReadMemStats(&memStats) runtime.ReadMemStats(&memStats)
var memObject badjson.JSONObject var memObject badjson.JSONObject
memObject.Put("heap", humanize.MemoryBytes(memStats.HeapInuse)) memObject.Put("heap", byteformats.FormatMemoryBytes(memStats.HeapInuse))
memObject.Put("stack", humanize.MemoryBytes(memStats.StackInuse)) memObject.Put("stack", byteformats.FormatMemoryBytes(memStats.StackInuse))
memObject.Put("idle", humanize.MemoryBytes(memStats.HeapIdle-memStats.HeapReleased)) memObject.Put("idle", byteformats.FormatMemoryBytes(memStats.HeapIdle-memStats.HeapReleased))
memObject.Put("goroutines", runtime.NumGoroutine()) memObject.Put("goroutines", runtime.NumGoroutine())
memObject.Put("rss", rusageMaxRSS()) memObject.Put("rss", rusageMaxRSS())

View File

@@ -30,10 +30,10 @@ var (
var _ adapter.DNSClient = (*Client)(nil) var _ adapter.DNSClient = (*Client)(nil)
type Client struct { type Client struct {
timeout time.Duration
disableCache bool disableCache bool
disableExpire bool disableExpire bool
independentCache bool independentCache bool
clientSubnet netip.Prefix
rdrc adapter.RDRCStore rdrc adapter.RDRCStore
initRDRCFunc func() adapter.RDRCStore initRDRCFunc func() adapter.RDRCStore
logger logger.ContextLogger logger logger.ContextLogger
@@ -42,27 +42,24 @@ type Client struct {
} }
type ClientOptions struct { type ClientOptions struct {
Timeout time.Duration
DisableCache bool DisableCache bool
DisableExpire bool DisableExpire bool
IndependentCache bool IndependentCache bool
CacheCapacity uint32 CacheCapacity uint32
ClientSubnet netip.Prefix
RDRC func() adapter.RDRCStore RDRC func() adapter.RDRCStore
Logger logger.ContextLogger Logger logger.ContextLogger
} }
func NewClient(options ClientOptions) *Client { func NewClient(options ClientOptions) *Client {
client := &Client{ client := &Client{
timeout: options.Timeout,
disableCache: options.DisableCache, disableCache: options.DisableCache,
disableExpire: options.DisableExpire, disableExpire: options.DisableExpire,
independentCache: options.IndependentCache, independentCache: options.IndependentCache,
clientSubnet: options.ClientSubnet,
initRDRCFunc: options.RDRC, initRDRCFunc: options.RDRC,
logger: options.Logger, logger: options.Logger,
} }
if client.timeout == 0 {
client.timeout = C.DNSTimeout
}
cacheCapacity := options.CacheCapacity cacheCapacity := options.CacheCapacity
if cacheCapacity < 1024 { if cacheCapacity < 1024 {
cacheCapacity = 1024 cacheCapacity = 1024
@@ -104,8 +101,12 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return &responseMessage, nil return &responseMessage, nil
} }
question := message.Question[0] question := message.Question[0]
if options.ClientSubnet.IsValid() { clientSubnet := options.ClientSubnet
message = SetClientSubnet(message, options.ClientSubnet, true) if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet
}
if clientSubnet.IsValid() {
message = SetClientSubnet(message, clientSubnet)
} }
isSimpleRequest := len(message.Question) == 1 && isSimpleRequest := len(message.Question) == 1 &&
len(message.Ns) == 0 && len(message.Ns) == 0 &&
@@ -146,7 +147,15 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return nil, ErrResponseRejectedCached 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) response, err := transport.Exchange(ctx, message)
cancel() cancel()
if err != nil { if err != nil {
@@ -232,10 +241,20 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
record.Header().Ttl = timeToLive record.Header().Ttl = timeToLive
} }
} }
response.Id = messageId
if !disableCache { if !disableCache {
c.storeCache(transport, question, response, timeToLive) c.storeCache(transport, question, response, timeToLive)
} }
response.Id = messageId
requestEDNSOpt := message.IsEdns0()
responseEDNSOpt := response.IsEdns0()
if responseEDNSOpt != nil && (requestEDNSOpt == nil || requestEDNSOpt.Version() < responseEDNSOpt.Version()) {
response.Extra = common.Filter(response.Extra, func(it dns.RR) bool {
return it.Header().Rrtype != dns.TypeOPT
})
if requestEDNSOpt != nil {
response.SetEdns0(responseEDNSOpt.UDPSize(), responseEDNSOpt.Do())
}
}
logExchangedResponse(c.logger, ctx, response, timeToLive) logExchangedResponse(c.logger, ctx, response, timeToLive)
return response, err return response, err
} }
@@ -243,9 +262,15 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) { func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
domain = FqdnToDomain(domain) domain = FqdnToDomain(domain)
dnsName := dns.Fqdn(domain) dnsName := dns.Fqdn(domain)
if options.Strategy == C.DomainStrategyIPv4Only { var strategy C.DomainStrategy
if options.LookupStrategy != C.DomainStrategyAsIS {
strategy = options.LookupStrategy
} else {
strategy = options.Strategy
}
if strategy == C.DomainStrategyIPv4Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker) return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
} else if options.Strategy == C.DomainStrategyIPv6Only { } else if strategy == C.DomainStrategyIPv6Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker) return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
} }
var response4 []netip.Addr var response4 []netip.Addr
@@ -271,7 +296,7 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
if len(response4) == 0 && len(response6) == 0 { if len(response4) == 0 && len(response6) == 0 {
return nil, err return nil, err
} }
return sortAddresses(response4, response6, options.Strategy), nil return sortAddresses(response4, response6, strategy), nil
} }
func (c *Client) ClearCache() { func (c *Client) ClearCache() {
@@ -527,12 +552,26 @@ func transportTagFromContext(ctx context.Context) (string, bool) {
return value, loaded return value, loaded
} }
func FixedResponseStatus(message *dns.Msg, rcode int) *dns.Msg {
return &dns.Msg{
MsgHdr: dns.MsgHdr{
Id: message.Id,
Rcode: rcode,
Response: true,
},
Question: message.Question,
}
}
func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, timeToLive uint32) *dns.Msg { func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, timeToLive uint32) *dns.Msg {
response := dns.Msg{ response := dns.Msg{
MsgHdr: dns.MsgHdr{ MsgHdr: dns.MsgHdr{
Id: id, Id: id,
Rcode: dns.RcodeSuccess, Response: true,
Response: true, Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
}, },
Question: []dns.Question{question}, Question: []dns.Question{question},
} }
@@ -565,9 +604,12 @@ func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, tim
func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToLive uint32) *dns.Msg { func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToLive uint32) *dns.Msg {
response := dns.Msg{ response := dns.Msg{
MsgHdr: dns.MsgHdr{ MsgHdr: dns.MsgHdr{
Id: id, Id: id,
Rcode: dns.RcodeSuccess, Response: true,
Response: true, Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
}, },
Question: []dns.Question{question}, Question: []dns.Question{question},
Answer: []dns.RR{ Answer: []dns.RR{
@@ -588,9 +630,12 @@ func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToL
func FixedResponseTXT(id uint16, question dns.Question, records []string, timeToLive uint32) *dns.Msg { func FixedResponseTXT(id uint16, question dns.Question, records []string, timeToLive uint32) *dns.Msg {
response := dns.Msg{ response := dns.Msg{
MsgHdr: dns.MsgHdr{ MsgHdr: dns.MsgHdr{
Id: id, Id: id,
Rcode: dns.RcodeSuccess, Response: true,
Response: true, Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
}, },
Question: []dns.Question{question}, Question: []dns.Question{question},
Answer: []dns.RR{ Answer: []dns.RR{
@@ -611,9 +656,12 @@ func FixedResponseTXT(id uint16, question dns.Question, records []string, timeTo
func FixedResponseMX(id uint16, question dns.Question, records []*net.MX, timeToLive uint32) *dns.Msg { func FixedResponseMX(id uint16, question dns.Question, records []*net.MX, timeToLive uint32) *dns.Msg {
response := dns.Msg{ response := dns.Msg{
MsgHdr: dns.MsgHdr{ MsgHdr: dns.MsgHdr{
Id: id, Id: id,
Rcode: dns.RcodeSuccess, Response: true,
Response: true, Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
}, },
Question: []dns.Question{question}, Question: []dns.Question{question},
} }

View File

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

View File

@@ -55,6 +55,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
DisableExpire: options.DNSClientOptions.DisableExpire, DisableExpire: options.DNSClientOptions.DisableExpire,
IndependentCache: options.DNSClientOptions.IndependentCache, IndependentCache: options.DNSClientOptions.IndependentCache,
CacheCapacity: options.DNSClientOptions.CacheCapacity, CacheCapacity: options.DNSClientOptions.CacheCapacity,
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}),
RDRC: func() adapter.RDRCStore { RDRC: func() adapter.RDRCStore {
cacheFile := service.FromContext[adapter.CacheFile](ctx) cacheFile := service.FromContext[adapter.CacheFile](ctx)
if cacheFile == nil { if cacheFile == nil {
@@ -157,6 +158,9 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
if action.Strategy != C.DomainStrategyAsIS { if action.Strategy != C.DomainStrategyAsIS {
options.Strategy = action.Strategy options.Strategy = action.Strategy
} }
if action.Timeout > 0 {
options.Timeout = action.Timeout
}
if isFakeIP || action.DisableCache { if isFakeIP || action.DisableCache {
options.DisableCache = true options.DisableCache = true
} }
@@ -179,6 +183,9 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
if action.Strategy != C.DomainStrategyAsIS { if action.Strategy != C.DomainStrategyAsIS {
options.Strategy = action.Strategy options.Strategy = action.Strategy
} }
if action.Timeout > 0 {
options.Timeout = action.Timeout
}
if action.DisableCache { if action.DisableCache {
options.DisableCache = true options.DisableCache = true
} }
@@ -258,7 +265,14 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
case *R.RuleActionReject: case *R.RuleActionReject:
switch action.Method { switch action.Method {
case C.RuleActionRejectMethodDefault: case C.RuleActionRejectMethodDefault:
return FixedResponse(message.Id, message.Question[0], nil, 0), nil return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeRefused,
Response: true,
},
Question: []mDNS.Question{message.Question[0]},
}, nil
case C.RuleActionRejectMethodDrop: case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop return nil, tun.ErrDrop
} }
@@ -285,7 +299,12 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
} else if errors.Is(err, ErrResponseRejected) { } else if errors.Is(err, ErrResponseRejected) {
rejected = true rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String()))) r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
/*} else if responseCheck!= nil && errors.Is(err, RcodeError(mDNS.RcodeNameError)) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
*/
} else if len(message.Question) > 0 { } else if len(message.Question) > 0 {
rejected = true
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String()))) r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
} else { } else {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>")) r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))

View File

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

View File

@@ -3,11 +3,15 @@ package transport
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"io" "io"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strconv" "strconv"
"sync"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/dialer"
@@ -39,11 +43,13 @@ func RegisterHTTPS(registry *dns.TransportRegistry) {
type HTTPSTransport struct { type HTTPSTransport struct {
dns.TransportAdapter dns.TransportAdapter
logger logger.ContextLogger logger logger.ContextLogger
dialer N.Dialer dialer N.Dialer
destination *url.URL destination *url.URL
headers http.Header headers http.Header
transport *http.Transport 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) { func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
@@ -93,11 +99,12 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
return nil, err return nil, err
} }
serverAddr := options.DNSServerAddressOptions.Build() serverAddr := options.DNSServerAddressOptions.Build()
if !serverAddr.Addr.IsValid() { if serverAddr.Port == 0 {
return nil, E.New("invalid server address: ", serverAddr)
} else if serverAddr.Port == 0 {
serverAddr.Port = 443 serverAddr.Port = 443
} }
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return NewHTTPSRaw( return NewHTTPSRaw(
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions), dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions),
logger, logger,
@@ -160,12 +167,33 @@ func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
} }
func (t *HTTPSTransport) Close() error { func (t *HTTPSTransport) Close() error {
t.transportAccess.Lock()
defer t.transportAccess.Unlock()
t.transport.CloseIdleConnections() t.transport.CloseIdleConnections()
t.transport = t.transport.Clone() t.transport = t.transport.Clone()
return nil return nil
} }
func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { 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 := *message
exMessage.Id = 0 exMessage.Id = 0
exMessage.Compress = true exMessage.Compress = true

View File

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

View File

@@ -89,11 +89,12 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
return nil, err return nil, err
} }
serverAddr := options.DNSServerAddressOptions.Build() serverAddr := options.DNSServerAddressOptions.Build()
if !serverAddr.Addr.IsValid() { if serverAddr.Port == 0 {
return nil, E.New("invalid server address: ", serverAddr)
} else if serverAddr.Port == 0 {
serverAddr.Port = 443 serverAddr.Port = 443
} }
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return &HTTP3Transport{ return &HTTP3Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions), TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
logger: logger, logger: logger,

View File

@@ -56,11 +56,12 @@ func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options
tlsConfig.SetNextProtos([]string{"doq"}) tlsConfig.SetNextProtos([]string{"doq"})
} }
serverAddr := options.DNSServerAddressOptions.Build() serverAddr := options.DNSServerAddressOptions.Build()
if !serverAddr.Addr.IsValid() { if serverAddr.Port == 0 {
return nil, E.New("invalid server address: ", serverAddr)
} else if serverAddr.Port == 0 {
serverAddr.Port = 853 serverAddr.Port = 853
} }
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return &Transport{ return &Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions), TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions),
ctx: ctx, ctx: ctx,

View File

@@ -38,11 +38,12 @@ func NewTCP(ctx context.Context, logger log.ContextLogger, tag string, options o
return nil, err return nil, err
} }
serverAddr := options.DNSServerAddressOptions.Build() serverAddr := options.DNSServerAddressOptions.Build()
if !serverAddr.Addr.IsValid() { if serverAddr.Port == 0 {
return nil, E.New("invalid server address: ", serverAddr)
} else if serverAddr.Port == 0 {
serverAddr.Port = 53 serverAddr.Port = 53
} }
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return &TCPTransport{ return &TCPTransport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTCP, tag, options), TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTCP, tag, options),
dialer: transportDialer, dialer: transportDialer,

View File

@@ -54,18 +54,23 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
return nil, err return nil, err
} }
serverAddr := options.DNSServerAddressOptions.Build() serverAddr := options.DNSServerAddressOptions.Build()
if !serverAddr.Addr.IsValid() { if serverAddr.Port == 0 {
return nil, E.New("invalid server address: ", serverAddr)
} else if serverAddr.Port == 0 {
serverAddr.Port = 853 serverAddr.Port = 853
} }
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return NewTLSRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions), transportDialer, serverAddr, tlsConfig), nil
}
func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport {
return &TLSTransport{ return &TLSTransport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions), TransportAdapter: adapter,
logger: logger, logger: logger,
dialer: transportDialer, dialer: dialer,
serverAddr: serverAddr, serverAddr: serverAddr,
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
}, nil }
} }
func (t *TLSTransport) Start(stage adapter.StartStage) error { func (t *TLSTransport) Start(stage adapter.StartStage) error {

View File

@@ -45,11 +45,12 @@ func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options o
return nil, err return nil, err
} }
serverAddr := options.DNSServerAddressOptions.Build() serverAddr := options.DNSServerAddressOptions.Build()
if !serverAddr.Addr.IsValid() { if serverAddr.Port == 0 {
return nil, E.New("invalid server address: ", serverAddr)
} else if serverAddr.Port == 0 {
serverAddr.Port = 53 serverAddr.Port = 53
} }
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return NewUDPRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeUDP, tag, options), transportDialer, serverAddr), nil return NewUDPRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeUDP, tag, options), transportDialer, serverAddr), nil
} }

View File

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

View File

@@ -2,15 +2,154 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.12.0-beta.6 #### 1.12.0-beta.30
* Fixes and improvements * 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
* Fixes and improvements
#### 1.12.0-beta.15
* Add DERP service **1**
* Add Resolved service and DNS server **2**
* Add SSM API service **3**
* Fixes and improvements
**1**:
DERP service is a Tailscale DERP server, similar to [derper](https://pkg.go.dev/tailscale.com/cmd/derper).
See [DERP Service](/configuration/service/derp/).
**2**:
Resolved service is a fake systemd-resolved DBUS service to receive DNS settings from other programs
(e.g. NetworkManager) and provide DNS resolution.
See [Resolved Service](/configuration/service/resolved/) and [Resolved DNS Server](/configuration/dns/server/resolved/).
**3**:
SSM API service is a RESTful API server for managing Shadowsocks servers.
See [SSM API Service](/configuration/service/ssm-api/).
### 1.11.11
* 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.13
* Add TLS record fragment route options **1**
* Add missing `accept_routes` option for Tailscale **2**
* Fixes and improvements
**1**:
See [Route Action](/configuration/route/rule_action/#tls_record_fragment).
**2**:
See [Tailscale](/configuration/endpoint/tailscale/#accept_routes).
#### 1.12.0-beta.10
* Add control options for listeners **1**
* Fixes and improvements
**1**:
You can now set `bind_interface`, `routing_mark` and `reuse_addr` in Listen Fields.
See [Listen Fields](/configuration/shared/listen/).
### 1.11.10
* Undeprecate the `block` outbound **1**
* Fixes and improvements
**1**:
Since we dont have a replacement for using the `block` outbound in selectors yet,
we decided to temporarily undeprecate the `block` outbound until a replacement is available in the future.
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we
violated the rules (TestFlight users are not affected)._
#### 1.12.0-beta.9
* Update quic-go to v0.51.0
* Fixes and improvements
### 1.11.9 ### 1.11.9
* Fixes and improvements * 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)._ _We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we
violated the rules (TestFlight users are not affected)._
#### 1.12.0-beta.5 #### 1.12.0-beta.5
@@ -26,7 +165,8 @@ _We are temporarily unable to update sing-box apps on the App Store because the
Now `auto_redirect` fixes compatibility issues between TUN and Docker bridge networks, Now `auto_redirect` fixes compatibility issues between TUN and Docker bridge networks,
see [Tun](/configuration/inbound/tun/#auto_redirect). see [Tun](/configuration/inbound/tun/#auto_redirect).
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._ _We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we
violated the rules (TestFlight users are not affected)._
#### 1.12.0-beta.3 #### 1.12.0-beta.3
@@ -36,7 +176,8 @@ _We are temporarily unable to update sing-box apps on the App Store because the
* Fixes and improvements * 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)._ _We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we
violated the rules (TestFlight users are not affected)._
#### 1.12.0-beta.1 #### 1.12.0-beta.1
@@ -51,7 +192,8 @@ see [Tun](/configuration/inbound/tun/#auto_redirect).
* Fixes and improvements * 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)._ _We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we
violated the rules (TestFlight users are not affected)._
#### 1.12.0-alpha.19 #### 1.12.0-alpha.19
@@ -91,7 +233,8 @@ See [Dial Fields](/configuration/shared/dial/#domain_resolver).
* Fixes and improvements * 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)._ _We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we
violated the rules (TestFlight users are not affected)._
#### 1.12.0-alpha.13 #### 1.12.0-alpha.13
@@ -162,7 +305,8 @@ For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and pa
* Fixes and improvements * Fixes and improvements
_This version overwrites 1.11.2, as incorrect binaries were released due to a bug in the continuous integration process._ _This version overwrites 1.11.2, as incorrect binaries were released due to a bug in the continuous integration
process._
#### 1.12.0-alpha.5 #### 1.12.0-alpha.5

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" !!! quote "Changes in sing-box 1.11.0"
:material-plus: [cache_capacity](#cache_capacity) :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 中的更改" !!! quote "sing-box 1.11.0 中的更改"
:material-plus: [cache_capacity](#cache_capacity) :material-plus: [cache_capacity](#cache_capacity)

View File

@@ -81,7 +81,7 @@ Will overrides `dns.client_subnet`.
#### method #### method
- `default`: Reply with NXDOMAIN. - `default`: Reply with REFUSED.
- `drop`: Drop the request. - `drop`: Drop the request.
`default` will be used by default. `default` will be used by default.

View File

@@ -81,7 +81,7 @@ icon: material/new-box
#### method #### method
- `default`: 返回 NXDOMAIN - `default`: 返回 REFUSED
- `drop`: 丢弃请求。 - `drop`: 丢弃请求。
默认使用 `defualt` 默认使用 `defualt`

View File

@@ -41,6 +41,7 @@ The type of the DNS server.
| `dhcp` | [DHCP](./dhcp/) | | `dhcp` | [DHCP](./dhcp/) |
| `fakeip` | [Fake IP](./fakeip/) | | `fakeip` | [Fake IP](./fakeip/) |
| `tailscale` | [Tailscale](./tailscale/) | | `tailscale` | [Tailscale](./tailscale/) |
| `resolved` | [Resolved](./resolved/) |
#### tag #### tag

View File

@@ -41,6 +41,7 @@ DNS 服务器的类型。
| `dhcp` | [DHCP](./dhcp/) | | `dhcp` | [DHCP](./dhcp/) |
| `fakeip` | [Fake IP](./fakeip/) | | `fakeip` | [Fake IP](./fakeip/) |
| `tailscale` | [Tailscale](./tailscale/) | | `tailscale` | [Tailscale](./tailscale/) |
| `resolved` | [Resolved](./resolved/) |
#### tag #### tag

View File

@@ -0,0 +1,84 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.12.0"
# Resolved
```json
{
"dns": {
"servers": [
{
"type": "resolved",
"tag": "",
"service": "resolved",
"accept_default_resolvers": false
}
]
}
}
```
### Fields
#### service
==Required==
The tag of the [Resolved Service](/configuration/service/resolved).
#### accept_default_resolvers
Indicates whether the default DNS resolvers should be accepted for fallback queries in addition to matching domains.
Specifically, default DNS resolvers are DNS servers that have `SetLinkDefaultRoute` or `SetLinkDomains ~.` set.
If not enabled, `NXDOMAIN` will be returned for requests that do not match search or match domains.
### Examples
=== "Split DNS only"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "resolved",
"tag": "resolved",
"service": "resolved"
}
],
"rules": [
{
"ip_accept_any": true,
"server": "resolved"
}
]
}
}
```
=== "Use as global DNS"
```json
{
"dns": {
"servers": [
{
"type": "resolved",
"service": "resolved",
"accept_default_resolvers": true
}
]
}
}
```

View File

@@ -30,13 +30,13 @@ icon: material/new-box
==Required== ==Required==
The tag of the Tailscale endpoint. The tag of the [Tailscale Endpoint](/configuration/endpoint/tailscale).
#### accept_default_resolvers #### accept_default_resolvers
Indicates whether default DNS resolvers should be accepted for fallback queries in addition to MagicDNS。 Indicates whether default DNS resolvers should be accepted for fallback queries in addition to MagicDNS。
if not enabled, NXDOMAIN will be returned for non-Tailscale domain queries. if not enabled, `NXDOMAIN` will be returned for non-Tailscale domain queries.
### Examples ### Examples
@@ -80,4 +80,4 @@ if not enabled, NXDOMAIN will be returned for non-Tailscale domain queries.
] ]
} }
} }
``` ```

View File

@@ -6,7 +6,7 @@ icon: material/new-box
# Endpoint # Endpoint
Endpoint is protocols that has both inbound and outbound behavior. An endpoint is a protocol with inbound and outbound behavior.
### Structure ### Structure

View File

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

View File

@@ -15,6 +15,7 @@ icon: material/new-box
"control_url": "", "control_url": "",
"ephemeral": false, "ephemeral": false,
"hostname": "", "hostname": "",
"accept_routes": false,
"exit_node": "", "exit_node": "",
"exit_node_allow_lan_access": false, "exit_node_allow_lan_access": false,
"advertise_routes": [], "advertise_routes": [],
@@ -62,6 +63,10 @@ System hostname is used by default.
Example: `localhost` Example: `localhost`
#### accept_routes
Indicates whether the node should accept routes advertised by other nodes.
#### exit_node #### exit_node
The exit node name or IP address to use. The exit node name or IP address to use.

View File

@@ -59,7 +59,7 @@
{ {
"external_controller": "0.0.0.0:9090", "external_controller": "0.0.0.0:9090",
"external_ui": "dashboard" "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_controller": "0.0.0.0:9090",
"external_ui": "dashboard" "external_ui": "dashboard"
// external_ui_download_detour: "direct" // "external_ui_download_detour": "direct"
} }
``` ```

View File

@@ -42,16 +42,18 @@ AnyTLS padding scheme line array.
Default padding scheme: Default padding scheme:
``` ```json
stop=8 [
0=30-30 "stop=8",
1=100-400 "0=30-30",
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000 "1=100-400",
3=9-9,500-1000 "2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000",
4=500-1000 "3=9-9,500-1000",
5=500-1000 "4=500-1000",
6=500-1000 "5=500-1000",
7=500-1000 "6=500-1000",
"7=500-1000"
]
``` ```
#### tls #### tls

View File

@@ -42,16 +42,18 @@ AnyTLS 填充方案行数组。
默认填充方案: 默认填充方案:
``` ```json
stop=8 [
0=30-30 "stop=8",
1=100-400 "0=30-30",
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000 "1=100-400",
3=9-9,500-1000 "2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000",
4=500-1000 "3=9-9,500-1000",
5=500-1000 "4=500-1000",
6=500-1000 "5=500-1000",
7=500-1000 "6=500-1000",
"7=500-1000"
]
``` ```
#### tls #### tls

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" !!! quote "Changes in sing-box 1.11.0"
:material-delete-alert: [gso](#gso) :material-delete-alert: [gso](#gso)
@@ -56,9 +60,12 @@ icon: material/alert-decagram
"auto_route": true, "auto_route": true,
"iproute2_table_index": 2022, "iproute2_table_index": 2022,
"iproute2_rule_index": 9000, "iproute2_rule_index": 9000,
"auto_redirect": false, "auto_redirect": true,
"auto_redirect_input_mark": "0x2023", "auto_redirect_input_mark": "0x2023",
"auto_redirect_output_mark": "0x2024", "auto_redirect_output_mark": "0x2024",
"loopback_address": [
"10.7.0.1"
],
"strict_route": true, "strict_route": true,
"route_address": [ "route_address": [
"0.0.0.0/1", "0.0.0.0/1",
@@ -66,7 +73,6 @@ icon: material/alert-decagram
"::/1", "::/1",
"8000::/1" "8000::/1"
], ],
"route_exclude_address": [ "route_exclude_address": [
"192.168.0.0/16", "192.168.0.0/16",
"fc00::/7" "fc00::/7"
@@ -117,7 +123,6 @@ icon: material/alert-decagram
"match_domain": [] "match_domain": []
} }
}, },
// Deprecated // Deprecated
"gso": false, "gso": false,
"inet4_address": [ "inet4_address": [
@@ -140,8 +145,8 @@ icon: material/alert-decagram
"inet6_route_exclude_address": [ "inet6_route_exclude_address": [
"fc00::/7" "fc00::/7"
], ],
...
... // Listen Fields // Listen Fields
} }
``` ```
@@ -273,6 +278,16 @@ Connection output mark used by `auto_redirect`.
`0x2024` is used by default. `0x2024` is used by default.
#### loopback_address
!!! question "Since sing-box 1.12.0"
Loopback addresses make TCP connections to the specified address connect to the source address.
Setting option value to `10.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 #### strict_route
Enforce strict routing rules when `auto_route` is enabled: 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 中的更改" !!! quote "sing-box 1.11.0 中的更改"
:material-delete-alert: [gso](#gso) :material-delete-alert: [gso](#gso)
@@ -56,9 +60,12 @@ icon: material/alert-decagram
"auto_route": true, "auto_route": true,
"iproute2_table_index": 2022, "iproute2_table_index": 2022,
"iproute2_rule_index": 9000, "iproute2_rule_index": 9000,
"auto_redirect": false, "auto_redirect": true,
"auto_redirect_input_mark": "0x2023", "auto_redirect_input_mark": "0x2023",
"auto_redirect_output_mark": "0x2024", "auto_redirect_output_mark": "0x2024",
"loopback_address": [
"10.7.0.1"
],
"strict_route": true, "strict_route": true,
"route_address": [ "route_address": [
"0.0.0.0/1", "0.0.0.0/1",
@@ -270,6 +277,16 @@ tun 接口的 IPv6 前缀。
默认使用 `0x2024` 默认使用 `0x2024`
#### loopback_address
!!! question "自 sing-box 1.12.0 起"
环回地址是用于使指向指定地址的 TCP 连接连接到来源地址的。
将选项值设置为 `10.7.0.1` 可实现与 SideStore/StosVPN 相同的行为。
当启用 `auto_redirect` 时,可以作为网关为局域网设备(而不仅仅是本地)实现相同的行为。
#### strict_route #### strict_route
当启用 `auto_route` 时,强制执行严格的路由规则: 当启用 `auto_route` 时,强制执行严格的路由规则:
@@ -398,11 +415,11 @@ UDP NAT 过期时间。
TCP/IP 栈。 TCP/IP 栈。
| 栈 | 描述 | | 栈 | 描述 |
|--------|------------------------------------------------------------------| |----------|-------------------------------------------------------------------------------------------------------|
| system | 基于系统网络栈执行 L3 到 L4 转换 | | `system` | 基于系统网络栈执行 L3 到 L4 转换 |
| gVisor | 基于 [gVisor](https://github.com/google/gvisor) 虚拟网络栈执行 L3 到 L4 转换 | | `gvisor` | 基于 [gVisor](https://github.com/google/gvisor) 虚拟网络栈执行 L3 到 L4 转换 |
| mixed | 混合 `system` TCP 栈与 `gvisor` UDP 栈 | | `mixed` | 混合 `system` TCP 栈与 `gvisor` UDP 栈 |
默认使用 `mixed` 栈如果 gVisor 构建标记已启用,否则默认使用 `system` 栈。 默认使用 `mixed` 栈如果 gVisor 构建标记已启用,否则默认使用 `system` 栈。

View File

@@ -14,6 +14,7 @@ sing-box uses JSON for configuration files.
"inbounds": [], "inbounds": [],
"outbounds": [], "outbounds": [],
"route": {}, "route": {},
"services": [],
"experimental": {} "experimental": {}
} }
``` ```
@@ -30,6 +31,7 @@ sing-box uses JSON for configuration files.
| `inbounds` | [Inbound](./inbound/) | | `inbounds` | [Inbound](./inbound/) |
| `outbounds` | [Outbound](./outbound/) | | `outbounds` | [Outbound](./outbound/) |
| `route` | [Route](./route/) | | `route` | [Route](./route/) |
| `services` | [Service](./service/) |
| `experimental` | [Experimental](./experimental/) | | `experimental` | [Experimental](./experimental/) |
### Check ### Check

View File

@@ -14,6 +14,7 @@ sing-box 使用 JSON 作为配置文件格式。
"inbounds": [], "inbounds": [],
"outbounds": [], "outbounds": [],
"route": {}, "route": {},
"services": [],
"experimental": {} "experimental": {}
} }
``` ```
@@ -30,6 +31,7 @@ sing-box 使用 JSON 作为配置文件格式。
| `inbounds` | [入站](./inbound/) | | `inbounds` | [入站](./inbound/) |
| `outbounds` | [出站](./outbound/) | | `outbounds` | [出站](./outbound/) |
| `route` | [路由](./route/) | | `route` | [路由](./route/) |
| `services` | [服务](./service/) |
| `experimental` | [实验性](./experimental/) | | `experimental` | [实验性](./experimental/) |
### 检查 ### 检查

View File

@@ -2,10 +2,6 @@
icon: material/delete-clock icon: material/delete-clock
--- ---
!!! failure "Deprecated in sing-box 1.11.0"
Legacy special outbounds are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-special-outbounds-to-rule-actions).
### Structure ### Structure
```json ```json

View File

@@ -2,10 +2,6 @@
icon: material/delete-clock icon: material/delete-clock
--- ---
!!! failure "已在 sing-box 1.11.0 废弃"
旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions).
`block` 出站关闭所有传入请求。 `block` 出站关闭所有传入请求。
### 结构 ### 结构

View File

@@ -6,6 +6,7 @@ icon: material/new-box
:material-plus: [tls_fragment](#tls_fragment) :material-plus: [tls_fragment](#tls_fragment)
:material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay) :material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay)
:material-plus: [tls_record_fragment](#tls_record_fragment)
:material-plus: [resolve.disable_cache](#disable_cache) :material-plus: [resolve.disable_cache](#disable_cache)
:material-plus: [resolve.rewrite_ttl](#rewrite_ttl) :material-plus: [resolve.rewrite_ttl](#rewrite_ttl)
:material-plus: [resolve.client_subnet](#client_subnet) :material-plus: [resolve.client_subnet](#client_subnet)
@@ -91,7 +92,8 @@ Not available when `method` is set to drop.
"udp_connect": false, "udp_connect": false,
"udp_timeout": "", "udp_timeout": "",
"tls_fragment": false, "tls_fragment": false,
"tls_fragment_fallback_delay": "" "tls_fragment_fallback_delay": "",
"tls_record_fragment": ""
} }
``` ```
@@ -164,13 +166,17 @@ If no protocol is sniffed, the following ports will be recognized as protocols b
Fragment TLS handshakes to bypass firewalls. 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. This feature is intended to circumvent simple firewalls based on **plaintext packet matching**,
and should not be used to circumvent real censorship.
Since it is not designed for performance, it should not be applied to all connections, but only to server names that are known to be blocked. 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 waiting for a fixed time specified by `tls_fragment_fallback_delay`. 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 `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. 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.
#### tls_fragment_fallback_delay #### tls_fragment_fallback_delay
@@ -180,6 +186,12 @@ The fallback value used when TLS segmentation cannot automatically determine the
`500ms` is used by default. `500ms` is used by default.
#### tls_record_fragment
!!! question "Since sing-box 1.12.0"
Fragment TLS handshake into multiple TLS records to bypass firewalls.
### sniff ### sniff
```json ```json

View File

@@ -5,7 +5,11 @@ icon: material/new-box
!!! quote "sing-box 1.12.0 中的更改" !!! quote "sing-box 1.12.0 中的更改"
:material-plus: [tls_fragment](#tls_fragment) :material-plus: [tls_fragment](#tls_fragment)
:material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay) :material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay)
:material-plus: [tls_record_fragment](#tls_record_fragment)
:material-plus: [resolve.disable_cache](#disable_cache)
:material-plus: [resolve.rewrite_ttl](#rewrite_ttl)
:material-plus: [resolve.client_subnet](#client_subnet)
## 最终动作 ## 最终动作
@@ -159,9 +163,10 @@ UDP 连接超时时间。
此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。 此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。
由于它不是为性能设计的,不应被应用于所有连接,而仅应用于已知被阻止的服务器名称。 由于性能不佳,请首先尝试 `tls_record_fragment`,且仅应用于已知被阻止的服务器名称。
在 Linux、Apple 平台和需要管理员权限的 Windows 系统上,可自动检测等待时间。若无法自动检测,将回退使用 `tls_fragment_fallback_delay` 指定的固定等待时间。 在 Linux、Apple 平台和需要管理员权限的 Windows 系统上,可自动检测等待时间。
若无法自动检测,将回退使用 `tls_fragment_fallback_delay` 指定的固定等待时间。
此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。 此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。
@@ -173,6 +178,12 @@ UDP 连接超时时间。
默认使用 `500ms` 默认使用 `500ms`
#### tls_record_fragment
!!! question "自 sing-box 1.12.0 起"
通过分段 TLS 握手数据包到多个 TLS 记录来绕过防火墙检测。
### sniff ### sniff
```json ```json

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