Compare commits

...

219 Commits

Author SHA1 Message Date
世界
17019f1729 Bump version 2024-11-20 12:03:42 +08:00
世界
6be07ed51f Fix start watcher 2024-11-20 11:32:53 +08:00
世界
af58e3bec0 Fix debug listener 2024-11-20 11:32:53 +08:00
世界
e58b549d0f Fix "Fix reloading of tls.certificate_path, tls.key_path and tls.ech.key_path" 2024-11-18 19:03:24 +08:00
zeetex
1d81996ceb Fix reloading of tls.certificate_path, tls.key_path and tls.ech.key_path 2024-11-18 14:09:54 +08:00
世界
97c47e72c4 Update dependencies 2024-11-18 14:07:00 +08:00
世界
122be275b0 release: Notarize macos standalone manually with --no-s3-acceleration 2024-11-18 14:07:00 +08:00
世界
0bb1132034 selector: Fix crash before start 2024-11-18 14:07:00 +08:00
世界
de14337b4b Fix deprecated check 2024-11-18 14:07:00 +08:00
世界
1e07633914 Downgrade NDK to 26.2.11394342 2024-11-18 13:10:06 +08:00
世界
e3e203844e Fix decompile rule-set 2024-11-18 13:10:06 +08:00
世界
84a102a6ef Fix mux stream accept 2024-11-09 12:26:49 +08:00
世界
f1c76c4dde Fix deprecated version check 2024-11-08 20:31:29 +08:00
世界
8df0aa5719 Downgrade NDK to r26d 2024-11-07 19:58:53 +08:00
世界
21faadb992 Uniq deprecated notes 2024-11-07 19:58:53 +08:00
世界
88099a304a platform: Add SendNotification 2024-11-06 12:53:32 +08:00
世界
f504fb0d46 Revert "platform: Add openURL event"
This reverts commit 718cffea9a.
2024-11-05 20:03:39 +08:00
世界
1d517b6ca5 platform: Add link flags 2024-11-05 18:28:13 +08:00
世界
b702d0b67a Update dependencies 2024-11-05 18:28:03 +08:00
世界
a001e30d8b platform: Remove SetTraceback("all") 2024-11-05 18:28:02 +08:00
世界
cdb93f0bb2 Fix "Fix metadata context" 2024-11-05 18:27:51 +08:00
世界
718cffea9a platform: Add openURL event 2024-11-05 18:27:51 +08:00
世界
9585c53e9f release: Add upload dSYMs 2024-10-30 15:35:20 +08:00
世界
d66d5cd457 Add deprecated warnings 2024-10-30 14:01:28 +08:00
世界
8c143feec8 Increase timeouts 2024-10-30 14:01:28 +08:00
世界
419058f466 Update NDK version 2024-10-30 14:01:13 +08:00
世界
1a6047a61b Fix metadata context 2024-10-30 14:01:13 +08:00
世界
327bb35ddd Rename HTTP start context 2024-10-30 14:01:13 +08:00
世界
6ed9a06394 Fix rule-set format 2024-10-25 22:12:47 +08:00
世界
b80ec55ba0 Bump version 2024-10-16 21:11:31 +08:00
世界
08718112ae Retry system forwarder listen 2024-10-16 20:47:26 +08:00
TsingShui
956ee361df Fix corrected improper use of reader and bReader
Co-authored-by: x_123 <x@a>
2024-10-16 20:45:18 +08:00
世界
e93d0408be documentation: Fix release notes 2024-10-16 20:45:13 +08:00
世界
137832ff3e Bump version 2024-10-13 21:17:59 +08:00
世界
3ede29fb6d documentation: Improve theme 2024-10-13 13:07:18 +08:00
世界
82ab68b542 build: Fix find NDK 2024-10-13 13:07:18 +08:00
renovate[bot]
e55723d84d [dependencies] Update actions/checkout digest to eef6144 2024-10-13 13:07:18 +08:00
世界
2f4d2d97f9 auto-redirect: Let fw4 take precedence over prerouting 2024-10-13 13:07:18 +08:00
世界
926d6f769e Update utls to v1.6.7 2024-10-13 13:07:02 +08:00
srk24
846777cd0c Add process_path_regex rule type 2024-10-13 13:07:02 +08:00
世界
06533b7a3b clash-api: Add PNA support 2024-10-13 13:07:02 +08:00
世界
4a95558c53 Add RDP sniffer 2024-10-13 13:07:02 +08:00
世界
e39a28ed5a Add SSH sniffer 2024-10-13 13:07:02 +08:00
世界
b2c708a3e6 Write close error to log 2024-10-13 13:07:02 +08:00
世界
a9209bb3e5 Add AdGuard DNS filter support 2024-10-13 13:07:02 +08:00
世界
9dc3bb975a Improve QUIC sniffer 2024-10-13 13:07:02 +08:00
世界
3a7acaa92a Add inline rule-set & Add reload for local rule-set 2024-10-13 13:07:02 +08:00
世界
6bebe2483b Unique rule-set names 2024-10-13 13:07:02 +08:00
世界
93cf134995 Add accept empty DNS rule option 2024-10-13 13:07:02 +08:00
世界
ff7d8c9ba8 Add custom options for TUN auto-route and auto-redirect 2024-10-13 13:07:02 +08:00
世界
50f07b42f6 Improve base DNS transports & Minor fixes 2024-10-13 13:07:02 +08:00
世界
db3a0c636d Add auto-redirect & Improve auto-route 2024-10-13 13:07:02 +08:00
世界
fec38f85cd Add rule-set decompile command 2024-10-13 13:07:02 +08:00
世界
dcb0141646 Add IP address support for rule-set match match 2024-10-13 13:07:02 +08:00
世界
f4f5a3c925 Improve usages of json.Unmarshal 2024-10-13 13:07:02 +08:00
世界
9b8d6c1b73 Bump rule-set version 2024-10-13 13:07:02 +08:00
世界
2f776168de Implement read deadline for QUIC based UDP inbounds 2024-10-13 13:07:02 +08:00
世界
923d3222b0 WTF is this 2024-10-13 13:07:01 +08:00
世界
bda93d516b platform: Fix clash server reload on android 2024-10-13 13:06:57 +08:00
世界
7eec3fb57a platform: Add log update interval 2024-10-13 13:06:57 +08:00
世界
b1d75812c5 platform: Prepare connections list 2024-10-13 13:06:55 +08:00
世界
d44e7d9834 Drop support for go1.18 and go1.19 2024-10-07 04:58:48 +08:00
世界
369bc7cea3 Add DTLS sniffer 2024-10-07 04:58:48 +08:00
iosmanthus
4b7a83da16 Introduce bittorrent related protocol sniffers
* Introduce bittorrent related protocol sniffers

including, sniffers of
1. BitTorrent Protocol (TCP)
2. uTorrent Transport Protocol (UDP)

Signed-off-by: iosmanthus <myosmanthustree@gmail.com>
Co-authored-by: 世界 <i@sekai.icu>
2024-10-07 04:58:48 +08:00
世界
0f7154afbd Update workflow to go1.23 2024-10-07 04:58:47 +08:00
世界
a06d10c3bc Bump version 2024-10-07 04:34:48 +08:00
世界
63cc6cc76c Fix Makefile 2024-10-07 04:34:48 +08:00
世界
d55c5b5cab documentation: Update package status 2024-10-07 04:34:48 +08:00
世界
b624c2dcc7 Fix context used by DNS outbounds 2024-10-07 04:34:48 +08:00
世界
9415444ebd Fix base path not applied to local rule-sets 2024-10-07 04:34:48 +08:00
世界
95606191d8 Add completions for linux packages 2024-10-07 04:34:48 +08:00
世界
e586d9e9bc Bump version 2024-09-20 23:37:06 +08:00
世界
8c7eaa4477 Fix docker build 2024-09-20 23:37:06 +08:00
世界
8464c8cb7c Fix version script 2024-09-20 21:10:15 +08:00
世界
39d7127651 Revert "Fix stream sniffer" 2024-09-20 20:40:02 +08:00
世界
e2077009c4 documentation: Update client status 2024-09-20 20:13:55 +08:00
世界
700a8eb425 Minor fixes 2024-09-20 20:13:14 +08:00
世界
3b0cba0852 Fix wireguard start 2024-09-20 20:12:52 +08:00
世界
f5554dd8b8 Bump version 2024-09-18 07:04:29 +08:00
世界
4d0362d530 Update macOS build workflow 2024-09-17 22:01:05 +08:00
世界
97ccd2ca04 documentation: Add sponsors page 2024-09-17 18:47:33 +08:00
世界
1ed6654ad4 Add mips64 build 2024-09-15 12:12:25 +08:00
世界
5385f75f53 documentation: Update build requirements 2024-09-15 12:10:00 +08:00
世界
ad97d4e11f Fix disconnected interface selected as default in windows 2024-09-15 11:59:32 +08:00
世界
09d4e91b77 Fix cached conn eats up read deadlines 2024-09-15 11:56:04 +08:00
Monica
3dbdda9555 documentation: Fix dial.zh.md
The Chinese documentation incorrectly stated that the default value for the domain_strategy field in the direct outbound module is dns.strategy. The correct value should be inbound.domain_strategy, as specified in the English documentation. This commit corrects the Chinese documentation to align with the accurate behavior described in the English version.

Signed-off-by: Monica <1379531829@qq.com>
2024-09-15 11:53:03 +08:00
世界
1f4ed6ff8f documentation: Update client status 2024-09-13 10:09:08 +08:00
世界
6ddbe19bc0 platform: Update bundle id 2024-09-12 17:55:53 +08:00
世界
d7205ecc60 Fix Makefile 2024-09-12 17:55:53 +08:00
世界
9e243e0ff9 gomobile: Fix go mod version 2024-09-10 23:05:13 +08:00
世界
02bc3e0a0a Update quic-go to v0.47.0 2024-09-09 14:45:46 +08:00
世界
87be6dc235 Update README 2024-09-09 08:48:35 +08:00
世界
c1c30976dc Improve docker workflow 2024-09-08 11:11:47 +08:00
世界
9bac18bcd1 wireguard: Fix events chan leak 2024-09-08 10:07:12 +08:00
世界
ceda5cc95d clash-api: Fix bad redirect 2024-09-08 10:07:07 +08:00
世界
27d6b63e71 Fix stream sniffer 2024-09-08 10:07:07 +08:00
世界
b57abcc73c tfo: Fix build with go1.23 2024-08-27 11:24:51 +08:00
世界
f1147965dd documentation: Fix missing zh headline 2024-08-21 18:52:38 +08:00
世界
45f3234c73 documentation: Update package status 2024-08-21 11:39:07 +08:00
Mingye Wang
aae3fded32 documentation: Two updates
* Copyedit documentation

Close #1378

* remove yum, go full on dnf

fixes #2049
2024-08-21 11:32:43 +08:00
世界
090494faf5 Do not close bug and enhancement issues 2024-08-21 08:09:15 +08:00
世界
db5719e22f Fix no error return when empty DNS cache retrieved 2024-08-20 23:23:48 +08:00
世界
064fb9b873 Fix direct dialer not resolving domain 2024-08-20 21:02:52 +08:00
世界
f6a1e123fc Make linter happy 2024-08-19 18:42:02 +08:00
世界
3066dfe3b3 Bump version 2024-08-19 10:43:09 +08:00
Liu Bingyan
1128fdd8c7 documentation: Update injectable description 2024-08-19 07:40:31 +08:00
世界
cfd9879b17 documentation: Remove unused 2024-08-19 06:39:33 +08:00
世界
9ceb660c57 documentation: Announce that our apps on Apple platforms are no longer available 2024-08-19 06:39:33 +08:00
世界
7d00d7df28 Update quic-go to v0.46.0 2024-08-19 06:25:15 +08:00
世界
21b1ac26b9 Fix UDP conn stuck on sniff
This change only avoids permanent hangs. We need to implement read deadlines for UDP conns in 1.10 for server inbounds.
2024-08-18 11:27:12 +08:00
世界
7fec8d842e Update golangci-lint configuration 2024-08-18 11:17:01 +08:00
世界
07c678fb85 Fix crash on Android when using process rules 2024-08-18 10:23:28 +08:00
世界
baecfc7778 Update quic-go to v0.45.2 2024-08-18 10:23:17 +08:00
世界
07de36ecdb Fix tests 2024-08-03 12:46:38 +08:00
世界
2c8a8303cd dns: Minor fixes 2024-07-27 11:04:57 +08:00
ruokeqx
e5991cae0b Use unix.SysctlRaw for macOS 2024-07-22 12:42:22 +08:00
世界
1349acfd5a Fix panic caused by possible generation of duplicate keys for domain_suffix 2024-07-17 16:02:17 +08:00
世界
98ff897f35 Fix reset outbounds 2024-07-13 17:46:22 +08:00
ayooh
6144c8e340 Fix default end value of the rangeItem 2024-07-13 17:45:32 +08:00
HystericalDragon
c8caac9f67 Fix v2rayquic default NextProtos (#1934) 2024-07-11 23:34:09 +08:00
printfer
81e9eda357 documentation: Fix typo 2024-07-07 16:09:41 +08:00
世界
7cba3da108 shadowsocks: Fix packet process for AEAD multi-user server 2024-07-05 17:09:09 +08:00
世界
82d06b43e7 vless: Fix missing deadline interfaces 2024-07-05 17:08:10 +08:00
世界
a7ac91f573 Move legacy vless implemetation to library 2024-07-03 20:17:32 +08:00
世界
0540a95a43 Fix v2ray-plugin 2024-07-02 14:08:17 +08:00
世界
94707dfcdd Add name to errors from v2ray HTTP transports 2024-07-02 14:08:17 +08:00
世界
8a17043502 Fix command output for rule-set match 2024-06-26 12:26:35 +08:00
世界
b0aaa86806 Minor fixes 2024-06-25 13:14:45 +08:00
世界
8a2d3fbb28 Update quic-go to v0.45.1 & Filter HTTPS ipv4/6hint 2024-06-24 11:07:32 +08:00
renovate[bot]
4652019608 [dependencies] Update docker/build-push-action action to v6 2024-06-24 10:24:13 +08:00
世界
06fa5abf63 Fix non-IP queries accepted by address filter rules 2024-06-24 10:13:16 +08:00
世界
996fbbf0c3 docs: Switch to venv 2024-06-24 10:10:49 +08:00
renovate[bot]
142ff1b455 [dependencies] Update actions/checkout digest to 692973e 2024-06-17 13:05:54 +08:00
世界
74d662f7a3 Fix always create package manager 2024-06-11 21:16:57 +08:00
世界
085f603377 Bump version 2024-06-09 13:20:56 +08:00
世界
460fae83dc Fix quic-go is caching dialErr since v0.43.0 2024-06-09 13:15:40 +08:00
世界
bb9bd9bff6 Fix package manager start order 2024-06-09 07:21:50 +08:00
世界
c2354ebf25 Bump version 2024-06-08 22:00:46 +08:00
世界
c1f4755c4e Fix parse UDP DNS server with addr:port address 2024-06-08 22:00:46 +08:00
世界
0ca5909b06 Stop passing device sleep events on Android and Apple platforms 2024-06-08 22:00:46 +08:00
世界
e77a8114c5 Fix rule-set start order 2024-06-08 22:00:46 +08:00
renovate[bot]
f1393235ff [dependencies] Update actions/checkout digest to a5ac7e5 2024-06-08 22:00:46 +08:00
renovate[bot]
bdba2365de [dependencies] Update goreleaser/goreleaser-action action to v6 2024-06-08 18:58:22 +08:00
世界
ce0da5b557 Fix typo 2024-06-08 18:58:14 +08:00
世界
3853201412 Bump version 2024-06-07 19:07:46 +08:00
世界
7003ef40a3 Fix wireguard start 2024-06-07 19:07:46 +08:00
世界
59ec92228c Fix repeatedly no route logs 2024-06-07 15:03:48 +08:00
世界
0eeb2da323 Fix reset HTTP3 DNS transport 2024-06-06 22:28:43 +08:00
世界
977b0fac02 Fix get source address from X-Forwarded-For 2024-06-06 22:21:37 +08:00
世界
51964801ff Fix enforced power listener on windows 2024-06-06 22:20:23 +08:00
世界
e08c052fc9 Fix wireguard client bind 2024-06-06 22:20:21 +08:00
世界
53927d8bbd Remove logs in router initialize 2024-06-06 22:20:19 +08:00
世界
968b9bc217 Fix crash on *bsd 2024-06-06 22:20:17 +08:00
lgjint
69dc87aa6d Fix set KDE6 system proxy 2024-06-06 22:20:14 +08:00
Fei1Yang
4193df375f build: Remove vendor in RPM packages 2024-05-27 19:28:15 +08:00
世界
5ff7006326 Bump version 2024-05-25 11:30:43 +08:00
世界
a89107ea9d documentation: Bump version 2024-05-23 14:57:45 +08:00
世界
9ffdbba2ed documentation: Add manuel for mitigating tunnelvision attacks 2024-05-23 14:57:27 +08:00
世界
65c71049ea documentation: Update DNS manual 2024-05-23 14:57:26 +08:00
世界
7d4e6a7f4e dialer: Allow nil router 2024-05-23 14:57:26 +08:00
世界
d612620c5d Add rule-set match command 2024-05-23 14:57:26 +08:00
世界
8a9a77a438 Add bypass_domain and search_domain platform HTTP proxy options 2024-05-23 14:57:26 +08:00
世界
a2098c18e1 Handle includeAllNetworks 2024-05-23 14:57:26 +08:00
世界
cf2181dd3a Update gVisor to 20240422.0 2024-05-23 14:57:15 +08:00
世界
5899e95ff1 Update quic-go to v0.43.1 2024-05-21 15:12:05 +08:00
世界
d7160c19cf Fixed order for Clash modes 2024-05-21 15:12:05 +08:00
世界
da9e22b4e6 Add custom prefix support in EDNS0 client subnet options 2024-05-21 15:12:05 +08:00
气息
0e120f8a44 Fix DNS exchange index
Signed-off-by: 气息 <qdshizh@gmail.com>
2024-05-21 15:12:05 +08:00
dyhkwong
d918863ac5 Always disable cache for fake-ip servers 2024-05-21 15:12:04 +08:00
PuerNya
2ae192305c Always disable cache for fake-ip DNS transport if independent_cache disabled 2024-05-21 15:12:03 +08:00
世界
71d1879bd6 Fix missing rule_set_ipcidr_match_source item in DNS rules 2024-05-21 15:12:03 +08:00
世界
917514e09f Improve DNS truncate behavior 2024-05-21 15:12:03 +08:00
世界
5327aeaea4 Fix DNS fallthrough incorrectly 2024-05-21 15:12:03 +08:00
世界
93ae3f7a1e Add rejected DNS response cache support 2024-05-21 15:12:03 +08:00
世界
f24a2aed7d Add support for client-subnet DNS options 2024-05-21 15:12:03 +08:00
世界
0517ceef76 Add address filter support for DNS rules 2024-05-21 15:12:02 +08:00
世界
830ea46932 Fix timezone for Android and iOS 2024-05-21 15:11:52 +08:00
世界
cd0fcd5ddc Improve loopback detector 2024-05-21 15:11:52 +08:00
世界
003176f069 Remove unused fakeip packet conn 2024-05-21 15:11:52 +08:00
世界
71d92518c1 Set the default TCP keep alive period 2024-05-21 15:11:52 +08:00
世界
b5dcd6bf59 Migrate ntp service to library 2024-05-21 15:11:52 +08:00
世界
11c7b4a866 Handle Windows power events 2024-05-21 15:11:52 +08:00
世界
ee14135298 Improve domain suffix match behavior
For historical reasons, sing-box's `domain_suffix` rule matches literal prefixes instead of the same as other projects.

This change modifies the behavior of `domain_suffix`: If the rule value is prefixed with `.`,
the behavior is unchanged, otherwise it matches `(domain|.+\.domain)` instead.
2024-05-21 15:11:41 +08:00
世界
cbcf005f37 Remove PROCESS_NAME_NATIVE dwFlag in process query output
The `process_path` rule of sing-box is inherited from Clash,
the original code uses the local system's path format (e.g. `\Device\HarddiskVolume1\folder\program.exe`),
but when the device has multiple disks, the HarddiskVolume serial number is not stable.

This change make QueryFullProcessImageNameW output a Win32 path (such as `C:\folder\program.exe`),
which will disrupt the existing `process_path` use cases in Windows.
2024-05-18 17:22:14 +08:00
世界
daee0b154e badtls: Support uTLS and TLS ECH for read waiter 2024-05-18 17:22:14 +08:00
世界
d530c724c0 Bump version 2024-05-18 16:53:05 +08:00
世界
7f698c1104 Fix hysteria2 panic 2024-05-18 16:20:32 +08:00
renovate[bot]
7a4a44c6d2 [dependencies] Update golangci/golangci-lint-action action to v6 2024-05-10 19:49:20 +08:00
世界
44277e5dd2 Fix urltest logic 2024-05-10 17:41:20 +08:00
世界
1f470c69c4 Update docker workflow 2024-05-03 19:33:20 +08:00
世界
742adacce7 Bump version 2024-05-03 17:38:26 +08:00
世界
32e1d5a5e2 documentation: Update package status 2024-05-03 17:38:26 +08:00
世界
cb9f4ce597 Minor fixes 2024-05-03 15:34:47 +08:00
世界
4b1a6185ba Fix DNF repo 2024-04-29 23:13:04 +08:00
世界
8d85c92356 Fix multiplex client dialer context 2024-04-29 11:58:20 +08:00
世界
c6164c9eca Fix usage of github actions 2024-04-29 11:58:20 +08:00
dyhkwong
3c85b8bc48 Fix fake-ip mapping 2024-04-29 11:58:20 +08:00
HystericalDragon
8b8fb4344c Remove unused encoder 2024-04-29 11:58:20 +08:00
renovate[bot]
e85a38e059 [dependencies] Update golangci/golangci-lint-action action to v5 2024-04-29 11:58:20 +08:00
renovate[bot]
f3ac91673a [dependencies] Update actions/checkout digest to 0ad4b8f 2024-04-29 11:58:20 +08:00
世界
0f1e58b917 documentation: Update TestFlight 2024-04-29 11:58:20 +08:00
世界
c4cfe24aef documentation: Fix strict_route description 2024-04-29 11:58:20 +08:00
世界
3d73b159ba Fix linux workflow 2024-04-29 11:58:20 +08:00
世界
0ae1afef44 Bump version 2024-04-23 14:14:40 +08:00
世界
a5e2a4073b Update dependencies 2024-04-23 14:03:55 +08:00
世界
b6cb3948a3 Fix initial packet MTU for QUIC protocols 2024-04-23 11:12:31 +08:00
世界
7b0f5061dc Fix loopback detector 2024-04-17 21:45:54 +08:00
世界
76f20482f7 Fix linux repo 2024-04-12 22:52:37 +08:00
世界
e735a5bdc8 Update dependencies 2024-04-12 22:52:37 +08:00
世界
70381e93c8 Bump version 2024-04-08 11:55:41 +08:00
世界
07a40716e8 Update dependencies 2024-04-08 11:45:00 +08:00
世界
5fea5956db Fix linux network monitor 2024-04-08 11:45:00 +08:00
世界
d20a389043 Fix timer usage 2024-04-08 11:45:00 +08:00
世界
4a4180bde5 Fixes for QUIC protocols 2024-04-08 11:44:55 +08:00
世界
7ecb6daabb Update dependencies 2024-03-29 04:52:06 +00:00
世界
712bdd9ae5 Fix fury release 2024-03-25 10:44:57 +08:00
世界
a3b74591a7 Fix syscall packet read waiter 2024-03-25 01:49:59 +08:00
世界
2f4abc6523 Fix canceler 2024-03-24 19:12:07 +08:00
dyhkwong
965ab075d9 Fix source_ip_is_private matching 2024-03-24 19:06:02 +08:00
303 changed files with 11865 additions and 5821 deletions

View File

@@ -22,43 +22,22 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.22 go-version: ^1.23
continue-on-error: true
- name: Run Test - name: Run Test
run: | run: |
go test -v ./... go test -v ./...
build_go118:
name: Debug build (Go 1.18)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.18
- name: Cache go module
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
key: go118-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: make ci_build_go118
build_go120: build_go120:
name: Debug build (Go 1.20) name: Debug build (Go 1.20)
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
@@ -78,7 +57,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
@@ -93,6 +72,26 @@ jobs:
key: go121-${{ hashFiles('**/go.sum') }} key: go121-${{ hashFiles('**/go.sum') }}
- name: Run Test - name: Run Test
run: make ci_build run: make ci_build
build_go122:
name: Debug build (Go 1.22)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.22
- name: Cache go module
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
key: go122-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: make ci_build
cross: cross:
strategy: strategy:
matrix: matrix:
@@ -208,7 +207,7 @@ jobs:
TAGS: with_clash_api,with_quic TAGS: with_clash_api,with_quic
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go

View File

@@ -1,39 +1,133 @@
name: Build Docker Images name: Publish Docker Images
on: on:
release: release:
types: types:
- released - published
workflow_dispatch:
inputs:
tag:
description: "The tag version you want to build"
env:
REGISTRY_IMAGE: ghcr.io/sagernet/sing-box
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
platform:
- linux/amd64
- linux/arm/v6
- linux/arm/v7
- linux/arm64
- linux/386
- linux/ppc64le
- linux/riscv64
- linux/s390x
steps: steps:
- name: Get commit to build
id: ref
run: |-
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
ref="${{ github.ref_name }}"
else
ref="${{ github.event.inputs.tag }}"
fi
echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT
- name: Checkout - name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
ref: ${{ steps.ref.outputs.ref }}
fetch-depth: 0
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx - name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Setup QEMU for Docker Buildx
uses: docker/setup-qemu-action@v3
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata - name: Docker meta
id: metadata id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ghcr.io/sagernet/sing-box images: ${{ env.REGISTRY_IMAGE }}
- name: Build and release Docker images - name: Build and push by digest
uses: docker/build-push-action@v5 id: build
uses: docker/build-push-action@v6
with: with:
platforms: linux/386,linux/amd64,linux/arm64,linux/s390x platforms: ${{ matrix.platform }}
target: dist context: .
build-args: | build-args: |
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
tags: | labels: ${{ steps.meta.outputs.labels }}
ghcr.io/sagernet/sing-box:latest outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
ghcr.io/sagernet/sing-box:${{ github.ref_name }} - name: Export digest
push: true run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
needs:
- build
steps:
- name: Get commit to build
id: ref
run: |-
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
ref="${{ github.ref_name }}"
else
ref="${{ github.event.inputs.tag }}"
fi
echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT
if [[ $ref == *"-"* ]]; then
latest=latest-beta
else
latest=latest
fi
echo "latest=$latest"
echo "latest=$latest" >> $GITHUB_OUTPUT
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}" \
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}

View File

@@ -22,15 +22,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.22 go-version: ^1.23
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v4 uses: golangci/golangci-lint-action@v6
with: with:
version: latest version: latest
args: --timeout=30m args: --timeout=30m

View File

@@ -10,15 +10,23 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.22 go-version: ^1.23
- name: Extract signing key
run: |-
mkdir -p $HOME/.gnupg
cat > $HOME/.gnupg/sagernet.key <<EOF
${{ secrets.GPG_KEY }}
echo "HOME=$HOME" >> "$GITHUB_ENV"
EOF
echo "HOME=$HOME" >> "$GITHUB_ENV"
- name: Publish release - name: Publish release
uses: goreleaser/goreleaser-action@v5 uses: goreleaser/goreleaser-action@v6
with: with:
distribution: goreleaser-pro distribution: goreleaser-pro
version: latest version: latest
@@ -27,3 +35,5 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
FURY_TOKEN: ${{ secrets.FURY_TOKEN }} FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}

View File

@@ -12,4 +12,5 @@ jobs:
with: with:
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days' stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
days-before-stale: 60 days-before-stale: 60
days-before-close: 5 days-before-close: 5
exempt-issue-labels: 'bug,enhancement'

2
.gitignore vendored
View File

@@ -14,3 +14,5 @@
/*.xcframework/ /*.xcframework/
.DS_Store .DS_Store
/config.d/ /config.d/
/venv/

View File

@@ -6,14 +6,7 @@ linters:
- gci - gci
- staticcheck - staticcheck
- paralleltest - paralleltest
- ineffassign
run:
skip-dirs:
- transport/simple-obfs
- transport/clashssr
- transport/cloudflaretls
- transport/shadowtls/tls
- transport/shadowtls/tls_go119
linters-settings: linters-settings:
gci: gci:
@@ -23,4 +16,13 @@ linters-settings:
- prefix(github.com/sagernet/) - prefix(github.com/sagernet/)
- default - default
staticcheck: staticcheck:
go: '1.20' checks:
- all
- -SA1003
run:
go: "1.23"
issues:
exclude-dirs:
- transport/simple-obfs

View File

@@ -26,6 +26,7 @@ builds:
- linux_arm_7 - linux_arm_7
- linux_s390x - linux_s390x
- linux_riscv64 - linux_riscv64
- linux_mips64le
mod_timestamp: '{{ .CommitTimestamp }}' mod_timestamp: '{{ .CommitTimestamp }}'
snapshot: snapshot:
name_template: "{{ .Version }}.{{ .ShortCommit }}" name_template: "{{ .Version }}.{{ .ShortCommit }}"
@@ -36,7 +37,6 @@ nfpms:
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
builds: builds:
- main - main
vendor: sagernet
homepage: https://sing-box.sagernet.org/ homepage: https://sing-box.sagernet.org/
maintainer: nekohasekai <contact-git@sekai.icu> maintainer: nekohasekai <contact-git@sekai.icu>
description: The universal proxy platform. description: The universal proxy platform.
@@ -49,17 +49,35 @@ nfpms:
- src: release/config/config.json - src: release/config/config.json
dst: /etc/sing-box/config.json dst: /etc/sing-box/config.json
type: config type: config
- 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@.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/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE - src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE dst: /usr/share/licenses/sing-box/LICENSE
deb:
signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}"
fields:
Bugs: https://github.com/SagerNet/sing-box/issues
rpm:
signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}"
conflicts: conflicts:
- sing-box-beta - sing-box-beta
- id: package_beta - id: package_beta
<<: *template <<: *template
package_name: sing-box-beta package_name: sing-box-beta
file_name_template: '{{ .ProjectName }}-beta_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
formats: formats:
- deb - deb
- rpm - rpm
@@ -71,10 +89,8 @@ furies:
- account: sagernet - account: sagernet
ids: ids:
- package - package
skip: |- disable: "{{ not (not .Prerelease) }}"
{{ eq .Prerelease "" }}
- account: sagernet - account: sagernet
ids: ids:
- package_beta - package_beta
skip: |- disable: "{{ not .Prerelease }}"
{{ ne .Prerelease "" }}

View File

@@ -1,3 +1,4 @@
version: 2
project_name: sing-box project_name: sing-box
builds: builds:
- &template - &template
@@ -7,7 +8,9 @@ builds:
- -v - -v
- -trimpath - -trimpath
ldflags: ldflags:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid= - -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
- -s
- -buildid=
tags: tags:
- with_gvisor - with_gvisor
- with_quic - with_quic
@@ -23,13 +26,13 @@ builds:
targets: targets:
- linux_386 - linux_386
- linux_amd64_v1 - linux_amd64_v1
- linux_amd64_v3
- linux_arm64 - linux_arm64
- linux_arm_6
- linux_arm_7 - linux_arm_7
- linux_s390x - linux_s390x
- linux_riscv64 - linux_riscv64
- linux_mips64le
- windows_amd64_v1 - windows_amd64_v1
- windows_amd64_v3
- windows_386 - windows_386
- windows_arm64 - windows_arm64
- darwin_amd64_v1 - darwin_amd64_v1
@@ -86,8 +89,6 @@ builds:
- android_arm64 - android_arm64
- android_386 - android_386
- android_amd64 - android_amd64
snapshot:
name_template: "{{ .Version }}.{{ .ShortCommit }}"
archives: archives:
- &template - &template
id: archive id: archive
@@ -101,7 +102,7 @@ archives:
wrap_in_directory: true wrap_in_directory: true
files: files:
- LICENSE - LICENSE
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
- id: archive-legacy - id: archive-legacy
<<: *template <<: *template
builds: builds:
@@ -110,10 +111,9 @@ archives:
nfpms: nfpms:
- id: package - id: package
package_name: sing-box package_name: sing-box
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
builds: builds:
- main - main
vendor: sagernet
homepage: https://sing-box.sagernet.org/ homepage: https://sing-box.sagernet.org/
maintainer: nekohasekai <contact-git@sekai.icu> maintainer: nekohasekai <contact-git@sekai.icu>
description: The universal proxy platform. description: The universal proxy platform.
@@ -122,31 +122,65 @@ nfpms:
- deb - deb
- rpm - rpm
- archlinux - archlinux
# - apk
# - ipk
priority: extra priority: extra
contents: contents:
- src: release/config/config.json - src: release/config/config.json
dst: /etc/sing-box/config.json dst: /etc/sing-box/config.json
type: config type: config
- 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@.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/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE - src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE dst: /usr/share/licenses/sing-box/LICENSE
deb: deb:
signature: signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}" key_file: "{{ .Env.NFPM_KEY_PATH }}"
fields:
Bugs: https://github.com/SagerNet/sing-box/issues
rpm: rpm:
signature: signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}" key_file: "{{ .Env.NFPM_KEY_PATH }}"
overrides: overrides:
deb: apk:
conflicts: contents:
- sing-box-beta - src: release/config/config.json
rpm: dst: /etc/sing-box/config.json
conflicts: type: config
- sing-box-beta
- src: release/config/sing-box.initd
dst: /etc/init.d/sing-box
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE
ipk:
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: config
- src: release/config/openwrt.init
dst: /etc/init.d/sing-box
- src: release/config/openwrt.conf
dst: /etc/config/sing-box
source: source:
enabled: false enabled: false
name_template: '{{ .ProjectName }}-{{ .Version }}.source' name_template: '{{ .ProjectName }}-{{ .Version }}.source'

View File

@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder
LABEL maintainer="nekohasekai <contact-git@sekai.icu>" LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
COPY . /go/src/github.com/sagernet/sing-box COPY . /go/src/github.com/sagernet/sing-box
WORKDIR /go/src/github.com/sagernet/sing-box WORKDIR /go/src/github.com/sagernet/sing-box
@@ -21,7 +21,7 @@ FROM --platform=$TARGETPLATFORM alpine AS dist
LABEL maintainer="nekohasekai <contact-git@sekai.icu>" LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
RUN set -ex \ RUN set -ex \
&& apk upgrade \ && apk upgrade \
&& apk add bash tzdata ca-certificates \ && apk add bash tzdata ca-certificates nftables \
&& rm -rf /var/cache/apk/* && rm -rf /var/cache/apk/*
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
ENTRYPOINT ["sing-box"] ENTRYPOINT ["sing-box"]

View File

@@ -1,7 +1,6 @@
NAME = sing-box NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD) COMMIT = $(shell git rev-parse --short HEAD)
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
TAGS_GO120 = with_quic,with_utls
TAGS_GO121 = with_ech TAGS_GO121 = with_ech
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121) TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121)
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
@@ -20,18 +19,17 @@ PREFIX ?= $(shell go env GOPATH)
build: build:
go build $(MAIN_PARAMS) $(MAIN) go build $(MAIN_PARAMS) $(MAIN)
ci_build_go118:
go build $(PARAMS) $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO118)" $(MAIN)
ci_build_go120: ci_build_go120:
go build $(PARAMS) $(MAIN) go build $(PARAMS) $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO118),$(TAGS_GO120)" $(MAIN) go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN)
ci_build: ci_build:
go build $(PARAMS) $(MAIN) go build $(PARAMS) $(MAIN)
go build $(MAIN_PARAMS) $(MAIN) go build $(MAIN_PARAMS) $(MAIN)
generate_completions:
go run -v --tags generate,generate_completions $(MAIN)
install: install:
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN) go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
@@ -71,7 +69,6 @@ release:
dist/*.deb \ dist/*.deb \
dist/*.rpm \ dist/*.rpm \
dist/*_amd64.pkg.tar.zst \ dist/*_amd64.pkg.tar.zst \
dist/*_amd64v3.pkg.tar.zst \
dist/*_arm64.pkg.tar.zst \ dist/*_arm64.pkg.tar.zst \
dist/release dist/release
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release
@@ -81,7 +78,6 @@ release_repo:
go run ./cmd/internal/build goreleaser release -f .goreleaser.fury.yaml --clean go run ./cmd/internal/build goreleaser release -f .goreleaser.fury.yaml --clean
release_install: release_install:
go install -v github.com/goreleaser/goreleaser@latest
go install -v github.com/tcnksm/ghr@latest go install -v github.com/tcnksm/ghr@latest
update_android_version: update_android_version:
@@ -100,15 +96,13 @@ upload_android:
release_android: lib_android update_android_version build_android upload_android release_android: lib_android update_android_version build_android upload_android
publish_android: publish_android:
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop
publish_android_appcenter:
cd ../sing-box-for-android && ./gradlew :app:appCenterAssembleAndUploadPlayRelease
# TODO: find why and remove `-destination 'generic/platform=iOS'`
build_ios: build_ios:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
rm -rf build/SFI.xcarchive && \ rm -rf build/SFI.xcarchive && \
xcodebuild archive -scheme SFI -configuration Release -archivePath build/SFI.xcarchive xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates
upload_ios_app_store: upload_ios_app_store:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
@@ -119,55 +113,74 @@ release_ios: build_ios upload_ios_app_store
build_macos: build_macos:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
rm -rf build/SFM.xcarchive && \ rm -rf build/SFM.xcarchive && \
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates
upload_macos_app_store: upload_macos_app_store:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath build/SFM.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates xcodebuild -exportArchive -archivePath build/SFM.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
release_macos: build_macos upload_macos_app_store release_macos: build_macos upload_macos_app_store
build_macos_independent: build_macos_standalone:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
rm -rf build/SFT.System.xcarchive && \ rm -rf build/SFM.System.xcarchive && \
xcodebuild archive -scheme SFM.System -configuration Release -archivePath build/SFM.System.xcarchive xcodebuild archive -scheme SFM.System -configuration Release -archivePath build/SFM.System.xcarchive -allowProvisioningUpdates
notarize_macos_independent: build_macos_dmg:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath "build/SFM.System.xcarchive" -exportOptionsPlist SFM.System/Upload.plist -allowProvisioningUpdates
wait_notarize_macos_independent:
sleep 60
export_macos_independent:
rm -rf dist/SFM rm -rf dist/SFM
mkdir -p dist/SFM mkdir -p dist/SFM
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
xcodebuild -exportNotarizedApp -archivePath build/SFM.System.xcarchive -exportPath "../sing-box/dist/SFM" rm -rf build/SFM.System && \
rm -rf build/SFM.dmg && \
xcodebuild -exportArchive \
-archivePath "build/SFM.System.xcarchive" \
-exportOptionsPlist SFM.System/Export.plist -allowProvisioningUpdates \
-exportPath "build/SFM.System" && \
create-dmg \
--volname "sing-box" \
--volicon "build/SFM.System/SFM.app/Contents/Resources/AppIcon.icns" \
--icon "SFM.app" 0 0 \
--hide-extension "SFM.app" \
--app-drop-link 0 0 \
--skip-jenkins \
"../sing-box/dist/SFM/SFM.dmg" "build/SFM.System/SFM.app"
upload_macos_independent: notarize_macos_dmg:
xcrun notarytool submit "dist/SFM/SFM.dmg" --wait \
--keychain-profile "notarytool-password" \
--no-s3-acceleration
upload_macos_dmg:
cd dist/SFM && \ cd dist/SFM && \
rm -f *.zip && \ cp SFM.dmg "SFM-${VERSION}-universal.dmg" && \
zip -ry "SFM-${VERSION}-universal.zip" SFM.app && \ ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dmg"
ghr --replace --draft --prerelease "v${VERSION}" *.zip
release_macos_independent: build_macos_independent notarize_macos_independent wait_notarize_macos_independent export_macos_independent upload_macos_independent upload_macos_dsyms:
pushd ../sing-box-for-apple/build/SFM.System.xcarchive && \
zip -r SFM.dSYMs.zip dSYMs && \
mv SFM.dSYMs.zip ../../../sing-box/dist/SFM && \
popd && \
cd dist/SFM && \
cp SFM.dSYMs.zip "SFM-${VERSION}-universal.dSYMs.zip" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dSYMs.zip"
release_macos_standalone: build_macos_standalone build_macos_dmg notarize_macos_dmg upload_macos_dmg upload_macos_dsyms
build_tvos: build_tvos:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
rm -rf build/SFT.xcarchive && \ rm -rf build/SFT.xcarchive && \
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive -allowProvisioningUpdates
upload_tvos_app_store: 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
release_tvos: build_tvos upload_tvos_app_store release_tvos: build_tvos upload_tvos_app_store
update_apple_version: update_apple_version:
go run ./cmd/internal/update_apple_version go run ./cmd/internal/update_apple_version
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_independent release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_standalone
release_apple_beta: update_apple_version release_ios release_macos release_tvos release_apple_beta: update_apple_version release_ios release_macos release_tvos
@@ -194,17 +207,19 @@ lib:
go run ./cmd/internal/build_libbox -target ios go run ./cmd/internal/build_libbox -target ios
lib_install: lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.3 go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.4
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.3 go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.4
docs: docs:
mkdocs serve venv/bin/mkdocs serve
publish_docs: publish_docs:
mkdocs gh-deploy -m "Update" --force --ignore-version --no-history venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
docs_install: docs_install:
pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*" python -m venv venv
source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
clean: clean:
rm -rf bin dist sing-box rm -rf bin dist sing-box
rm -f $(shell go env GOPATH)/sing-box rm -f $(shell go env GOPATH)/sing-box

View File

@@ -8,10 +8,6 @@ The universal proxy platform.
https://sing-box.sagernet.org https://sing-box.sagernet.org
## Support
https://community.sagernet.org/c/sing-box/
## License ## License
``` ```

View File

@@ -4,13 +4,13 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/binary" "encoding/binary"
"io"
"net" "net"
"time" "time"
"github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-dns"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/varbin"
) )
type ClashServer interface { type ClashServer interface {
@@ -30,6 +30,9 @@ type CacheFile interface {
StoreFakeIP() bool StoreFakeIP() bool
FakeIPStorage FakeIPStorage
StoreRDRC() bool
dns.RDRCStore
LoadMode() string LoadMode() string
StoreMode(mode string) error StoreMode(mode string) error
LoadSelected(group string) string LoadSelected(group string) string
@@ -52,16 +55,15 @@ func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = rw.WriteUVariant(&buffer, uint64(len(s.Content))) err = varbin.Write(&buffer, binary.BigEndian, s.Content)
if err != nil { if err != nil {
return nil, err return nil, err
} }
buffer.Write(s.Content)
err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix()) err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix())
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = rw.WriteVString(&buffer, s.LastEtag) err = varbin.Write(&buffer, binary.BigEndian, s.LastEtag)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -75,12 +77,7 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
if err != nil { if err != nil {
return err return err
} }
contentLen, err := rw.ReadUVariant(reader) err = varbin.Read(reader, binary.BigEndian, &s.Content)
if err != nil {
return err
}
s.Content = make([]byte, contentLen)
_, err = io.ReadFull(reader, s.Content)
if err != nil { if err != nil {
return err return err
} }
@@ -90,7 +87,7 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
return err return err
} }
s.LastUpdated = time.Unix(lastUpdated, 0) s.LastUpdated = time.Unix(lastUpdated, 0)
s.LastEtag, err = rw.ReadVString(reader) err = varbin.Read(reader, binary.BigEndian, &s.LastEtag)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -31,11 +31,16 @@ type InboundContext struct {
Network string Network string
Source M.Socksaddr Source M.Socksaddr
Destination M.Socksaddr Destination M.Socksaddr
Domain string
Protocol string
User string User string
Outbound string Outbound string
// sniffer
Protocol string
Domain string
Client string
SniffContext any
// cache // cache
InboundDetour string InboundDetour string
@@ -51,19 +56,25 @@ type InboundContext struct {
// rule cache // rule cache
IPCIDRMatchSource bool IPCIDRMatchSource bool
SourceAddressMatch bool IPCIDRAcceptEmpty bool
SourcePortMatch bool
DestinationAddressMatch bool SourceAddressMatch bool
DestinationPortMatch bool SourcePortMatch bool
DestinationAddressMatch bool
DestinationPortMatch bool
DidMatch bool
IgnoreDestinationIPCIDRMatch bool
} }
func (c *InboundContext) ResetRuleCache() { func (c *InboundContext) ResetRuleCache() {
c.IPCIDRMatchSource = false c.IPCIDRMatchSource = false
c.IPCIDRAcceptEmpty = false
c.SourceAddressMatch = false c.SourceAddressMatch = false
c.SourcePortMatch = false c.SourcePortMatch = false
c.DestinationAddressMatch = false c.DestinationAddressMatch = false
c.DestinationPortMatch = false c.DestinationPortMatch = false
c.DidMatch = false
} }
type inboundContextKey struct{} type inboundContextKey struct{}
@@ -80,15 +91,6 @@ func ContextFrom(ctx context.Context) *InboundContext {
return metadata.(*InboundContext) return metadata.(*InboundContext)
} }
func AppendContext(ctx context.Context) (context.Context, *InboundContext) {
metadata := ContextFrom(ctx)
if metadata != nil {
return ctx, metadata
}
metadata = new(InboundContext)
return WithContext(ctx, metadata), metadata
}
func ExtendContext(ctx context.Context) (context.Context, *InboundContext) { func ExtendContext(ctx context.Context) (context.Context, *InboundContext) {
var newMetadata InboundContext var newMetadata InboundContext
if metadata := ContextFrom(ctx); metadata != nil { if metadata := ContextFrom(ctx); metadata != nil {
@@ -96,3 +98,12 @@ func ExtendContext(ctx context.Context) (context.Context, *InboundContext) {
} }
return WithContext(ctx, &newMetadata), &newMetadata return WithContext(ctx, &newMetadata), &newMetadata
} }
func OverrideContext(ctx context.Context) context.Context {
if metadata := ContextFrom(ctx); metadata != nil {
var newMetadata InboundContext
newMetadata = *metadata
return WithContext(ctx, &newMetadata)
}
return ctx
}

View File

@@ -2,23 +2,30 @@ package adapter
import ( import (
"context" "context"
"net"
"net/http" "net/http"
"net/netip" "net/netip"
"sync"
"github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-box/common/geoip"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-dns" "github.com/sagernet/sing-dns"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
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/x/list"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
mdns "github.com/miekg/dns" mdns "github.com/miekg/dns"
"go4.org/netipx"
) )
type Router interface { type Router interface {
Service Service
PreStarter PreStarter
PostStarter PostStarter
Cleanup() error
Outbounds() []Outbound Outbounds() []Outbound
Outbound(tag string) (Outbound, bool) Outbound(tag string) (Outbound, bool)
@@ -45,7 +52,9 @@ type Router interface {
DefaultInterface() string DefaultInterface() string
AutoDetectInterface() bool AutoDetectInterface() bool
AutoDetectInterfaceFunc() control.Func AutoDetectInterfaceFunc() control.Func
DefaultMark() int DefaultMark() uint32
RegisterAutoRedirectOutputMark(mark uint32) error
AutoRedirectOutputMark() uint32
NetworkMonitor() tun.NetworkUpdateMonitor NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager PackageManager() tun.PackageManager
@@ -71,6 +80,7 @@ func RouterFromContext(ctx context.Context) Router {
type HeadlessRule interface { type HeadlessRule interface {
Match(metadata *InboundContext) bool Match(metadata *InboundContext) bool
String() string
} }
type Rule interface { type Rule interface {
@@ -79,31 +89,75 @@ type Rule interface {
Type() string Type() string
UpdateGeosite() error UpdateGeosite() error
Outbound() string Outbound() string
String() string
} }
type DNSRule interface { type DNSRule interface {
Rule Rule
DisableCache() bool DisableCache() bool
RewriteTTL() *uint32 RewriteTTL() *uint32
ClientSubnet() *netip.Prefix
WithAddressLimit() bool
MatchAddressLimit(metadata *InboundContext) bool
} }
type RuleSet interface { type RuleSet interface {
StartContext(ctx context.Context, startContext RuleSetStartContext) error Name() string
StartContext(ctx context.Context, startContext *HTTPStartContext) error
PostStart() error PostStart() error
Metadata() RuleSetMetadata Metadata() RuleSetMetadata
ExtractIPSet() []*netipx.IPSet
IncRef()
DecRef()
Cleanup()
RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback]
UnregisterCallback(element *list.Element[RuleSetUpdateCallback])
Close() error Close() error
HeadlessRule HeadlessRule
} }
type RuleSetUpdateCallback func(it RuleSet)
type RuleSetMetadata struct { type RuleSetMetadata struct {
ContainsProcessRule bool ContainsProcessRule bool
ContainsWIFIRule bool ContainsWIFIRule bool
ContainsIPCIDRRule bool
}
type HTTPStartContext struct {
access sync.Mutex
httpClientCache map[string]*http.Client
} }
type RuleSetStartContext interface { func NewHTTPStartContext() *HTTPStartContext {
HTTPClient(detour string, dialer N.Dialer) *http.Client return &HTTPStartContext{
Close() httpClientCache: make(map[string]*http.Client),
}
}
func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
c.access.Lock()
defer c.access.Unlock()
if httpClient, loaded := c.httpClientCache[detour]; loaded {
return httpClient
}
httpClient := &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: C.TCPTimeout,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
},
}
c.httpClientCache[detour] = httpClient
return httpClient
}
func (c *HTTPStartContext) Close() {
c.access.Lock()
defer c.access.Unlock()
for _, client := range c.httpClientCache {
client.CloseIdleConnections()
}
} }
type InterfaceUpdateListener interface { type InterfaceUpdateListener interface {

View File

@@ -22,4 +22,5 @@ type V2RayServerTransportHandler interface {
type V2RayClientTransport interface { type V2RayClientTransport interface {
DialContext(ctx context.Context) (net.Conn, error) DialContext(ctx context.Context) (net.Conn, error)
Close() error
} }

39
box.go
View File

@@ -111,6 +111,7 @@ func New(options Options) (*Box, error) {
ctx, ctx,
router, router,
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")), logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
tag,
inboundOptions, inboundOptions,
options.PlatformInterface, options.PlatformInterface,
) )
@@ -203,7 +204,7 @@ func (s *Box) PreStart() error {
defer func() { defer func() {
v := recover() v := recover()
if v != nil { if v != nil {
log.Error(E.Cause(err, "origin error")) println(err.Error())
debug.PrintStack() debug.PrintStack()
panic("panic on early close: " + fmt.Sprint(v)) panic("panic on early close: " + fmt.Sprint(v))
} }
@@ -222,9 +223,9 @@ func (s *Box) Start() error {
defer func() { defer func() {
v := recover() v := recover()
if v != nil { if v != nil {
log.Error(E.Cause(err, "origin error")) println(err.Error())
debug.PrintStack() debug.PrintStack()
panic("panic on early close: " + fmt.Sprint(v)) println("panic on early start: " + fmt.Sprint(v))
} }
}() }()
s.Close() s.Close()
@@ -235,7 +236,7 @@ func (s *Box) Start() error {
} }
func (s *Box) preStart() error { func (s *Box) preStart() error {
monitor := taskmonitor.New(s.logger, C.DefaultStartTimeout) monitor := taskmonitor.New(s.logger, C.StartTimeout)
monitor.Start("start logger") monitor.Start("start logger")
err := s.logFactory.Start() err := s.logFactory.Start()
monitor.Finish() monitor.Finish()
@@ -302,7 +303,11 @@ func (s *Box) start() error {
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]") return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
} }
} }
return s.postStart() err = s.postStart()
if err != nil {
return err
}
return s.router.Cleanup()
} }
func (s *Box) postStart() error { func (s *Box) postStart() error {
@@ -312,16 +317,28 @@ func (s *Box) postStart() error {
return E.Cause(err, "start ", serviceName) return E.Cause(err, "start ", serviceName)
} }
} }
for _, outbound := range s.outbounds { // TODO: reorganize ALL start order
if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound { for _, out := range s.outbounds {
if lateOutbound, isLateOutbound := out.(adapter.PostStarter); isLateOutbound {
err := lateOutbound.PostStart() err := lateOutbound.PostStart()
if err != nil { if err != nil {
return E.Cause(err, "post-start outbound/", outbound.Tag()) return E.Cause(err, "post-start outbound/", out.Tag())
} }
} }
} }
err := s.router.PostStart()
return s.router.PostStart() if err != nil {
return err
}
for _, in := range s.inbounds {
if lateInbound, isLateInbound := in.(adapter.PostStarter); isLateInbound {
err = lateInbound.PostStart()
if err != nil {
return E.Cause(err, "post-start inbound/", in.Tag())
}
}
}
return nil
} }
func (s *Box) Close() error { func (s *Box) Close() error {
@@ -331,7 +348,7 @@ func (s *Box) Close() error {
default: default:
close(s.done) close(s.done)
} }
monitor := taskmonitor.New(s.logger, C.DefaultStopTimeout) monitor := taskmonitor.New(s.logger, C.StopTimeout)
var errors error var errors error
for serviceName, service := range s.postServices { for serviceName, service := range s.postServices {
monitor.Start("close ", serviceName) monitor.Start("close ", serviceName)

View File

@@ -12,7 +12,7 @@ import (
) )
func (s *Box) startOutbounds() error { func (s *Box) startOutbounds() error {
monitor := taskmonitor.New(s.logger, C.DefaultStartTimeout) monitor := taskmonitor.New(s.logger, C.StartTimeout)
outboundTags := make(map[adapter.Outbound]string) outboundTags := make(map[adapter.Outbound]string)
outbounds := make(map[string]adapter.Outbound) outbounds := make(map[string]adapter.Outbound)
for i, outboundToStart := range s.outbounds { for i, outboundToStart := range s.outbounds {
@@ -45,7 +45,9 @@ func (s *Box) startOutbounds() error {
} }
started[outboundTag] = true started[outboundTag] = true
canContinue = true canContinue = true
if starter, isStarter := outboundToStart.(common.Starter); isStarter { if starter, isStarter := outboundToStart.(interface {
Start() error
}); isStarter {
monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]") monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
err := starter.Start() err := starter.Start()
monitor.Finish() monitor.Finish()

View File

@@ -93,7 +93,7 @@ func buildAndroid() {
const name = "libbox.aar" const name = "libbox.aar"
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs") copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
if rw.FileExists(copyPath) { if rw.IsDir(copyPath) {
copyPath, _ = filepath.Abs(copyPath) copyPath, _ = filepath.Abs(copyPath)
err = rw.CopyFile(name, filepath.Join(copyPath, name)) err = rw.CopyFile(name, filepath.Join(copyPath, name))
if err != nil { if err != nil {
@@ -134,7 +134,7 @@ func buildiOS() {
} }
copyPath := filepath.Join("..", "sing-box-for-apple") copyPath := filepath.Join("..", "sing-box-for-apple")
if rw.FileExists(copyPath) { if rw.IsDir(copyPath) {
targetDir := filepath.Join(copyPath, "Libbox.xcframework") targetDir := filepath.Join(copyPath, "Libbox.xcframework")
targetDir, _ = filepath.Abs(targetDir) targetDir, _ = filepath.Abs(targetDir)
os.RemoveAll(targetDir) os.RemoveAll(targetDir)

View File

@@ -30,7 +30,7 @@ func FindSDK() {
} }
for _, path := range searchPath { for _, path := range searchPath {
path = os.ExpandEnv(path) path = os.ExpandEnv(path)
if rw.FileExists(filepath.Join(path, "licenses", "android-sdk-license")) { if rw.IsFile(filepath.Join(path, "licenses", "android-sdk-license")) {
androidSDKPath = path androidSDKPath = path
break break
} }
@@ -60,7 +60,7 @@ func FindSDK() {
func findNDK() bool { func findNDK() bool {
const fixedVersion = "26.2.11394342" const fixedVersion = "26.2.11394342"
const versionFile = "source.properties" const versionFile = "source.properties"
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.FileExists(filepath.Join(fixedPath, versionFile)) { if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
androidNDKPath = fixedPath androidNDKPath = fixedPath
return true return true
} }
@@ -86,7 +86,7 @@ func findNDK() bool {
}) })
for _, versionName := range versionNames { for _, versionName := range versionNames {
currentNDKPath := filepath.Join(androidSDKPath, "ndk", versionName) currentNDKPath := filepath.Join(androidSDKPath, "ndk", versionName)
if rw.FileExists(filepath.Join(androidSDKPath, versionFile)) { if rw.IsFile(filepath.Join(currentNDKPath, versionFile)) {
androidNDKPath = currentNDKPath androidNDKPath = currentNDKPath
log.Warn("reproducibility warning: using NDK version " + versionName + " instead of " + fixedVersion) log.Warn("reproducibility warning: using NDK version " + versionName + " instead of " + fixedVersion)
return true return true
@@ -100,11 +100,11 @@ var GoBinPath string
func FindMobile() { func FindMobile() {
goBin := filepath.Join(build.Default.GOPATH, "bin") goBin := filepath.Join(build.Default.GOPATH, "bin")
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
if !rw.FileExists(filepath.Join(goBin, "gobind.exe")) { if !rw.IsFile(filepath.Join(goBin, "gobind.exe")) {
log.Fatal("missing gomobile installation") log.Fatal("missing gomobile installation")
} }
} else { } else {
if !rw.FileExists(filepath.Join(goBin, "gobind")) { if !rw.IsFile(filepath.Join(goBin, "gobind")) {
log.Fatal("missing gomobile installation") log.Fatal("missing gomobile installation")
} }
} }

View File

@@ -26,8 +26,8 @@ func main() {
common.Must(decoder.Decode(&project)) common.Must(decoder.Decode(&project))
objectsMap := project["objects"].(map[string]any) objectsMap := project["objects"].(map[string]any)
projectContent := string(common.Must1(os.ReadFile("sing-box.xcodeproj/project.pbxproj"))) projectContent := string(common.Must1(os.ReadFile("sing-box.xcodeproj/project.pbxproj")))
newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfa"}, newVersion.VersionString()) newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfavt"}, newVersion.VersionString())
newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfa.independent", "io.nekohasekai.sfa.system"}, newVersion.String()) newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfavt.standalone", "io.nekohasekai.sfavt.system"}, newVersion.String())
if updated0 || updated1 { if updated0 || updated1 {
log.Info("updated version to ", newVersion.VersionString(), " (", newVersion.String(), ")") log.Info("updated version to ", newVersion.VersionString(), " (", newVersion.String(), ")")
} }

71
cmd/sing-box/cmd.go Normal file
View File

@@ -0,0 +1,71 @@
package main
import (
"context"
"os"
"os/user"
"strconv"
"time"
"github.com/sagernet/sing-box/experimental/deprecated"
_ "github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
"github.com/spf13/cobra"
)
var (
globalCtx context.Context
configPaths []string
configDirectories []string
workingDir string
disableColor bool
)
var mainCommand = &cobra.Command{
Use: "sing-box",
PersistentPreRun: preRun,
}
func init() {
mainCommand.PersistentFlags().StringArrayVarP(&configPaths, "config", "c", nil, "set configuration file path")
mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path")
mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
}
func preRun(cmd *cobra.Command, args []string) {
globalCtx = context.Background()
sudoUser := os.Getenv("SUDO_USER")
sudoUID, _ := strconv.Atoi(os.Getenv("SUDO_UID"))
sudoGID, _ := strconv.Atoi(os.Getenv("SUDO_GID"))
if sudoUID == 0 && sudoGID == 0 && sudoUser != "" {
sudoUserObject, _ := user.Lookup(sudoUser)
if sudoUserObject != nil {
sudoUID, _ = strconv.Atoi(sudoUserObject.Uid)
sudoGID, _ = strconv.Atoi(sudoUserObject.Gid)
}
}
if sudoUID > 0 && sudoGID > 0 {
globalCtx = filemanager.WithDefault(globalCtx, "", "", sudoUID, sudoGID)
}
if disableColor {
log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger())
}
if workingDir != "" {
_, err := os.Stat(workingDir)
if err != nil {
filemanager.MkdirAll(globalCtx, workingDir, 0o777)
}
err = os.Chdir(workingDir)
if err != nil {
log.Fatal(err)
}
}
if len(configPaths) == 0 && len(configDirectories) == 0 {
configPaths = append(configPaths, "config.json")
}
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
}

View File

@@ -30,7 +30,7 @@ func check() error {
if err != nil { if err != nil {
return err return err
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(globalCtx)
instance, err := box.New(box.Options{ instance, err := box.New(box.Options{
Context: ctx, Context: ctx,
Options: options, Options: options,

View File

@@ -87,7 +87,7 @@ func geoipExport(countryCode string) error {
headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String()) headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String())
} }
var plainRuleSet option.PlainRuleSetCompat var plainRuleSet option.PlainRuleSetCompat
plainRuleSet.Version = C.RuleSetVersion1 plainRuleSet.Version = C.RuleSetVersion2
plainRuleSet.Options.Rules = []option.HeadlessRule{ plainRuleSet.Options.Rules = []option.HeadlessRule{
{ {
Type: C.RuleTypeDefault, Type: C.RuleTypeDefault,

View File

@@ -70,7 +70,7 @@ func geositeExport(category string) error {
headlessRule.DomainKeyword = defaultRule.DomainKeyword headlessRule.DomainKeyword = defaultRule.DomainKeyword
headlessRule.DomainRegex = defaultRule.DomainRegex headlessRule.DomainRegex = defaultRule.DomainRegex
var plainRuleSet option.PlainRuleSetCompat var plainRuleSet option.PlainRuleSetCompat
plainRuleSet.Version = C.RuleSetVersion1 plainRuleSet.Version = C.RuleSetVersion2
plainRuleSet.Options.Rules = []option.HeadlessRule{ plainRuleSet.Options.Rules = []option.HeadlessRule{
{ {
Type: C.RuleTypeDefault, Type: C.RuleTypeDefault,

View File

@@ -54,7 +54,11 @@ func merge(outputPath string) error {
return nil return nil
} }
} }
err = rw.WriteFile(outputPath, buffer.Bytes()) err = rw.MkdirParent(outputPath)
if err != nil {
return err
}
err = os.WriteFile(outputPath, buffer.Bytes(), 0o644)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -6,7 +6,7 @@ import (
var commandRuleSet = &cobra.Command{ var commandRuleSet = &cobra.Command{
Use: "rule-set", Use: "rule-set",
Short: "Manage rule sets", Short: "Manage rule-sets",
} }
func init() { func init() {

View File

@@ -55,10 +55,6 @@ func compileRuleSet(sourcePath string) error {
if err != nil { if err != nil {
return err return err
} }
if err != nil {
return err
}
ruleSet := plainRuleSet.Upgrade()
var outputPath string var outputPath string
if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".json") { if strings.HasSuffix(sourcePath, ".json") {
@@ -73,7 +69,7 @@ func compileRuleSet(sourcePath string) error {
if err != nil { if err != nil {
return err return err
} }
err = srs.Write(outputFile, ruleSet) err = srs.Write(outputFile, plainRuleSet.Options, plainRuleSet.Version)
if err != nil { if err != nil {
outputFile.Close() outputFile.Close()
os.Remove(outputPath) os.Remove(outputPath)

View File

@@ -0,0 +1,89 @@
package main
import (
"io"
"os"
"strings"
"github.com/sagernet/sing-box/cmd/sing-box/internal/convertor/adguard"
"github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra"
)
var (
flagRuleSetConvertType string
flagRuleSetConvertOutput string
)
var commandRuleSetConvert = &cobra.Command{
Use: "convert [source-path]",
Short: "Convert adguard DNS filter to rule-set",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := convertRuleSet(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandRuleSet.AddCommand(commandRuleSetConvert)
commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertType, "type", "t", "", "Source type, available: adguard")
commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file")
}
func convertRuleSet(sourcePath string) error {
var (
reader io.Reader
err error
)
if sourcePath == "stdin" {
reader = os.Stdin
} else {
reader, err = os.Open(sourcePath)
if err != nil {
return err
}
}
var rules []option.HeadlessRule
switch flagRuleSetConvertType {
case "adguard":
rules, err = adguard.Convert(reader)
case "":
return E.New("source type is required")
default:
return E.New("unsupported source type: ", flagRuleSetConvertType)
}
if err != nil {
return err
}
var outputPath string
if flagRuleSetConvertOutput == flagRuleSetCompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".txt") {
outputPath = sourcePath[:len(sourcePath)-4] + ".srs"
} else {
outputPath = sourcePath + ".srs"
}
} else {
outputPath = flagRuleSetConvertOutput
}
outputFile, err := os.Create(outputPath)
if err != nil {
return err
}
defer outputFile.Close()
err = srs.Write(outputFile, option.PlainRuleSet{Rules: rules}, C.RuleSetVersion2)
if err != nil {
outputFile.Close()
os.Remove(outputPath)
return err
}
outputFile.Close()
return nil
}

View File

@@ -0,0 +1,77 @@
package main
import (
"io"
"os"
"strings"
"github.com/sagernet/sing-box/common/srs"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/json"
"github.com/spf13/cobra"
)
var flagRuleSetDecompileOutput string
const flagRuleSetDecompileDefaultOutput = "<file_name>.json"
var commandRuleSetDecompile = &cobra.Command{
Use: "decompile [binary-path]",
Short: "Decompile rule-set binary to json",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := decompileRuleSet(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandRuleSet.AddCommand(commandRuleSetDecompile)
commandRuleSetDecompile.Flags().StringVarP(&flagRuleSetDecompileOutput, "output", "o", flagRuleSetDecompileDefaultOutput, "Output file")
}
func decompileRuleSet(sourcePath string) error {
var (
reader io.Reader
err error
)
if sourcePath == "stdin" {
reader = os.Stdin
} else {
reader, err = os.Open(sourcePath)
if err != nil {
return err
}
}
ruleSet, err := srs.Read(reader, true)
if err != nil {
return err
}
var outputPath string
if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".srs") {
outputPath = sourcePath[:len(sourcePath)-4] + ".json"
} else {
outputPath = sourcePath + ".json"
}
} else {
outputPath = flagRuleSetDecompileOutput
}
outputFile, err := os.Create(outputPath)
if err != nil {
return err
}
encoder := json.NewEncoder(outputFile)
encoder.SetIndent("", " ")
err = encoder.Encode(ruleSet)
if err != nil {
outputFile.Close()
os.Remove(outputPath)
return err
}
outputFile.Close()
return nil
}

View File

@@ -0,0 +1,95 @@
package main
import (
"bytes"
"io"
"os"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/route"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json"
M "github.com/sagernet/sing/common/metadata"
"github.com/spf13/cobra"
)
var flagRuleSetMatchFormat string
var commandRuleSetMatch = &cobra.Command{
Use: "match <rule-set path> <IP address/domain>",
Short: "Check if an IP address or a domain matches the rule-set",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
err := ruleSetMatch(args[0], args[1])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandRuleSetMatch.Flags().StringVarP(&flagRuleSetMatchFormat, "format", "f", "source", "rule-set format")
commandRuleSet.AddCommand(commandRuleSetMatch)
}
func ruleSetMatch(sourcePath string, domain string) error {
var (
reader io.Reader
err error
)
if sourcePath == "stdin" {
reader = os.Stdin
} else {
reader, err = os.Open(sourcePath)
if err != nil {
return E.Cause(err, "read rule-set")
}
}
content, err := io.ReadAll(reader)
if err != nil {
return E.Cause(err, "read rule-set")
}
var ruleSet option.PlainRuleSetCompat
switch flagRuleSetMatchFormat {
case C.RuleSetFormatSource:
ruleSet, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content)
if err != nil {
return err
}
case C.RuleSetFormatBinary:
ruleSet, err = srs.Read(bytes.NewReader(content), false)
if err != nil {
return err
}
default:
return E.New("unknown rule-set format: ", flagRuleSetMatchFormat)
}
plainRuleSet, err := ruleSet.Upgrade()
if err != nil {
return err
}
ipAddress := M.ParseAddr(domain)
var metadata adapter.InboundContext
if ipAddress.IsValid() {
metadata.Destination = M.SocksaddrFrom(ipAddress, 0)
} else {
metadata.Domain = domain
}
for i, ruleOptions := range plainRuleSet.Rules {
var currentRule adapter.HeadlessRule
currentRule, err = route.NewHeadlessRule(nil, ruleOptions)
if err != nil {
return E.Cause(err, "parse rule_set.rules.[", i, "]")
}
if currentRule.Match(&metadata) {
println(F.ToString("match rules.[", i, "]: ", currentRule))
}
}
return nil
}

View File

@@ -0,0 +1,94 @@
package main
import (
"bytes"
"io"
"os"
"path/filepath"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/spf13/cobra"
)
var commandRuleSetUpgradeFlagWrite bool
var commandRuleSetUpgrade = &cobra.Command{
Use: "upgrade <source-path>",
Short: "Upgrade rule-set json",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := upgradeRuleSet(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandRuleSetUpgrade.Flags().BoolVarP(&commandRuleSetUpgradeFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
commandRuleSet.AddCommand(commandRuleSetUpgrade)
}
func upgradeRuleSet(sourcePath string) error {
var (
reader io.Reader
err error
)
if sourcePath == "stdin" {
reader = os.Stdin
} else {
reader, err = os.Open(sourcePath)
if err != nil {
return err
}
}
content, err := io.ReadAll(reader)
if err != nil {
return err
}
plainRuleSetCompat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
if err != nil {
return err
}
switch plainRuleSetCompat.Version {
case C.RuleSetVersion1:
default:
log.Info("already up-to-date")
return nil
}
plainRuleSet, err := plainRuleSetCompat.Upgrade()
if err != nil {
return err
}
buffer := new(bytes.Buffer)
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
err = encoder.Encode(plainRuleSet)
if err != nil {
return E.Cause(err, "encode config")
}
outputPath, _ := filepath.Abs(sourcePath)
if !commandRuleSetUpgradeFlagWrite || sourcePath == "stdin" {
os.Stdout.WriteString(buffer.String() + "\n")
return nil
}
if bytes.Equal(content, buffer.Bytes()) {
return nil
}
output, err := os.Create(sourcePath)
if err != nil {
return E.Cause(err, "open output")
}
_, err = output.Write(buffer.Bytes())
output.Close()
if err != nil {
return E.Cause(err, "write output")
}
os.Stderr.WriteString(outputPath + "\n")
return nil
}

View File

@@ -109,7 +109,7 @@ func readConfigAndMerge() (option.Options, error) {
} }
var mergedMessage json.RawMessage var mergedMessage json.RawMessage
for _, options := range optionsList { for _, options := range optionsList {
mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage) mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage, false)
if err != nil { if err != nil {
return option.Options{}, E.Cause(err, "merge config at ", options.path) return option.Options{}, E.Cause(err, "merge config at ", options.path)
} }
@@ -188,9 +188,12 @@ func run() error {
cancel() cancel()
closeCtx, closed := context.WithCancel(context.Background()) closeCtx, closed := context.WithCancel(context.Background())
go closeMonitor(closeCtx) go closeMonitor(closeCtx)
instance.Close() err = instance.Close()
closed() closed()
if osSignal != syscall.SIGHUP { if osSignal != syscall.SIGHUP {
if err != nil {
log.Error(E.Cause(err, "sing-box did not closed properly"))
}
return nil return nil
} }
break break
@@ -199,7 +202,7 @@ func run() error {
} }
func closeMonitor(ctx context.Context) { func closeMonitor(ctx context.Context) {
time.Sleep(C.DefaultStopFatalTimeout) time.Sleep(C.FatalStopTimeout)
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return

View File

@@ -9,8 +9,10 @@ import (
"net/url" "net/url"
"os" "os"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"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"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -32,7 +34,10 @@ func init() {
commandTools.AddCommand(commandFetch) commandTools.AddCommand(commandFetch)
} }
var httpClient *http.Client var (
httpClient *http.Client
http3Client *http.Client
)
func fetch(args []string) error { func fetch(args []string) error {
instance, err := createPreStartedClient() instance, err := createPreStartedClient()
@@ -53,8 +58,16 @@ func fetch(args []string) error {
}, },
} }
defer httpClient.CloseIdleConnections() defer httpClient.CloseIdleConnections()
if C.WithQUIC {
err = initializeHTTP3Client(instance)
if err != nil {
return err
}
defer http3Client.CloseIdleConnections()
}
for _, urlString := range args { for _, urlString := range args {
parsedURL, err := url.Parse(urlString) var parsedURL *url.URL
parsedURL, err = url.Parse(urlString)
if err != nil { if err != nil {
return err return err
} }
@@ -63,16 +76,27 @@ func fetch(args []string) error {
parsedURL.Scheme = "http" parsedURL.Scheme = "http"
fallthrough fallthrough
case "http", "https": case "http", "https":
err = fetchHTTP(parsedURL) err = fetchHTTP(httpClient, parsedURL)
if err != nil { if err != nil {
return err return err
} }
case "http3":
if !C.WithQUIC {
return C.ErrQUICNotIncluded
}
parsedURL.Scheme = "https"
err = fetchHTTP(http3Client, parsedURL)
if err != nil {
return err
}
default:
return E.New("unsupported scheme: ", parsedURL.Scheme)
} }
} }
return nil return nil
} }
func fetchHTTP(parsedURL *url.URL) error { func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error {
request, err := http.NewRequest("GET", parsedURL.String(), nil) request, err := http.NewRequest("GET", parsedURL.String(), nil)
if err != nil { if err != nil {
return err return err

View File

@@ -0,0 +1,36 @@
//go:build with_quic
package main
import (
"context"
"crypto/tls"
"net/http"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
box "github.com/sagernet/sing-box"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func initializeHTTP3Client(instance *box.Box) error {
dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound)
if err != nil {
return err
}
http3Client = &http.Client{
Transport: &http3.RoundTripper{
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
destination := M.ParseSocksaddr(addr)
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
if dErr != nil {
return nil, dErr
}
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg)
},
},
}
return nil
}

View File

@@ -0,0 +1,18 @@
//go:build !with_quic
package main
import (
"net/url"
"os"
box "github.com/sagernet/sing-box"
)
func initializeHTTP3Client(instance *box.Box) error {
return os.ErrInvalid
}
func fetchHTTP3(parsedURL *url.URL) error {
return os.ErrInvalid
}

View File

@@ -0,0 +1,28 @@
//go:build generate && generate_completions
package main
import "github.com/sagernet/sing-box/log"
func main() {
err := generateCompletions()
if err != nil {
log.Fatal(err)
}
}
func generateCompletions() error {
err := mainCommand.GenBashCompletionFile("release/completions/sing-box.bash")
if err != nil {
return err
}
err = mainCommand.GenFishCompletionFile("release/completions/sing-box.fish", true)
if err != nil {
return err
}
err = mainCommand.GenZshCompletionFile("release/completions/sing-box.zsh")
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,346 @@
package adguard
import (
"bufio"
"io"
"net/netip"
"os"
"strconv"
"strings"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
)
type agdguardRuleLine struct {
ruleLine string
isRawDomain bool
isExclude bool
isSuffix bool
hasStart bool
hasEnd bool
isRegexp bool
isImportant bool
}
func Convert(reader io.Reader) ([]option.HeadlessRule, error) {
scanner := bufio.NewScanner(reader)
var (
ruleLines []agdguardRuleLine
ignoredLines int
)
parseLine:
for scanner.Scan() {
ruleLine := scanner.Text()
if ruleLine == "" || ruleLine[0] == '!' || ruleLine[0] == '#' {
continue
}
originRuleLine := ruleLine
if M.IsDomainName(ruleLine) {
ruleLines = append(ruleLines, agdguardRuleLine{
ruleLine: ruleLine,
isRawDomain: true,
})
continue
}
hostLine, err := parseAdGuardHostLine(ruleLine)
if err == nil {
if hostLine != "" {
ruleLines = append(ruleLines, agdguardRuleLine{
ruleLine: hostLine,
isRawDomain: true,
hasStart: true,
hasEnd: true,
})
}
continue
}
if strings.HasSuffix(ruleLine, "|") {
ruleLine = ruleLine[:len(ruleLine)-1]
}
var (
isExclude bool
isSuffix bool
hasStart bool
hasEnd bool
isRegexp bool
isImportant bool
)
if !strings.HasPrefix(ruleLine, "/") && strings.Contains(ruleLine, "$") {
params := common.SubstringAfter(ruleLine, "$")
for _, param := range strings.Split(params, ",") {
paramParts := strings.Split(param, "=")
var ignored bool
if len(paramParts) > 0 && len(paramParts) <= 2 {
switch paramParts[0] {
case "app", "network":
// maybe support by package_name/process_name
case "dnstype":
// maybe support by query_type
case "important":
ignored = true
isImportant = true
case "dnsrewrite":
if len(paramParts) == 2 && M.ParseAddr(paramParts[1]).IsUnspecified() {
ignored = true
}
}
}
if !ignored {
ignoredLines++
log.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", ruleLine)
continue parseLine
}
}
ruleLine = common.SubstringBefore(ruleLine, "$")
}
if strings.HasPrefix(ruleLine, "@@") {
ruleLine = ruleLine[2:]
isExclude = true
}
if strings.HasSuffix(ruleLine, "|") {
ruleLine = ruleLine[:len(ruleLine)-1]
}
if strings.HasPrefix(ruleLine, "||") {
ruleLine = ruleLine[2:]
isSuffix = true
} else if strings.HasPrefix(ruleLine, "|") {
ruleLine = ruleLine[1:]
hasStart = true
}
if strings.HasSuffix(ruleLine, "^") {
ruleLine = ruleLine[:len(ruleLine)-1]
hasEnd = true
}
if strings.HasPrefix(ruleLine, "/") && strings.HasSuffix(ruleLine, "/") {
ruleLine = ruleLine[1 : len(ruleLine)-1]
if ignoreIPCIDRRegexp(ruleLine) {
ignoredLines++
log.Debug("ignored unsupported rule with IPCIDR regexp: ", ruleLine)
continue
}
isRegexp = true
} else {
if strings.Contains(ruleLine, "://") {
ruleLine = common.SubstringAfter(ruleLine, "://")
}
if strings.Contains(ruleLine, "/") {
ignoredLines++
log.Debug("ignored unsupported rule with path: ", ruleLine)
continue
}
if strings.Contains(ruleLine, "##") {
ignoredLines++
log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
continue
}
if strings.Contains(ruleLine, "#$#") {
ignoredLines++
log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
continue
}
var domainCheck string
if strings.HasPrefix(ruleLine, ".") || strings.HasPrefix(ruleLine, "-") {
domainCheck = "r" + ruleLine
} else {
domainCheck = ruleLine
}
if ruleLine == "" {
ignoredLines++
log.Debug("ignored unsupported rule with empty domain", originRuleLine)
continue
} else {
domainCheck = strings.ReplaceAll(domainCheck, "*", "x")
if !M.IsDomainName(domainCheck) {
_, ipErr := parseADGuardIPCIDRLine(ruleLine)
if ipErr == nil {
ignoredLines++
log.Debug("ignored unsupported rule with IPCIDR: ", ruleLine)
continue
}
if M.ParseSocksaddr(domainCheck).Port != 0 {
log.Debug("ignored unsupported rule with port: ", ruleLine)
} else {
log.Debug("ignored unsupported rule with invalid domain: ", ruleLine)
}
ignoredLines++
continue
}
}
}
ruleLines = append(ruleLines, agdguardRuleLine{
ruleLine: ruleLine,
isExclude: isExclude,
isSuffix: isSuffix,
hasStart: hasStart,
hasEnd: hasEnd,
isRegexp: isRegexp,
isImportant: isImportant,
})
}
if len(ruleLines) == 0 {
return nil, E.New("AdGuard rule-set is empty or all rules are unsupported")
}
if common.All(ruleLines, func(it agdguardRuleLine) bool {
return it.isRawDomain
}) {
return []option.HeadlessRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
Domain: common.Map(ruleLines, func(it agdguardRuleLine) string {
return it.ruleLine
}),
},
},
}, nil
}
mapDomain := func(it agdguardRuleLine) string {
ruleLine := it.ruleLine
if it.isSuffix {
ruleLine = "||" + ruleLine
} else if it.hasStart {
ruleLine = "|" + ruleLine
}
if it.hasEnd {
ruleLine += "^"
}
return ruleLine
}
importantDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain)
importantDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && !it.isExclude }), mapDomain)
importantExcludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && it.isExclude }), mapDomain)
importantExcludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && it.isExclude }), mapDomain)
domain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain)
domainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && !it.isExclude }), mapDomain)
excludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && it.isExclude }), mapDomain)
excludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && it.isExclude }), mapDomain)
currentRule := option.HeadlessRule{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
AdGuardDomain: domain,
DomainRegex: domainRegex,
},
}
if len(excludeDomain) > 0 || len(excludeDomainRegex) > 0 {
currentRule = option.HeadlessRule{
Type: C.RuleTypeLogical,
LogicalOptions: option.LogicalHeadlessRule{
Mode: C.LogicalTypeAnd,
Rules: []option.HeadlessRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
AdGuardDomain: excludeDomain,
DomainRegex: excludeDomainRegex,
Invert: true,
},
},
currentRule,
},
},
}
}
if len(importantDomain) > 0 || len(importantDomainRegex) > 0 {
currentRule = option.HeadlessRule{
Type: C.RuleTypeLogical,
LogicalOptions: option.LogicalHeadlessRule{
Mode: C.LogicalTypeOr,
Rules: []option.HeadlessRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
AdGuardDomain: importantDomain,
DomainRegex: importantDomainRegex,
},
},
currentRule,
},
},
}
}
if len(importantExcludeDomain) > 0 || len(importantExcludeDomainRegex) > 0 {
currentRule = option.HeadlessRule{
Type: C.RuleTypeLogical,
LogicalOptions: option.LogicalHeadlessRule{
Mode: C.LogicalTypeAnd,
Rules: []option.HeadlessRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
AdGuardDomain: importantExcludeDomain,
DomainRegex: importantExcludeDomainRegex,
Invert: true,
},
},
currentRule,
},
},
}
}
log.Info("parsed rules: ", len(ruleLines), "/", len(ruleLines)+ignoredLines)
return []option.HeadlessRule{currentRule}, nil
}
func ignoreIPCIDRRegexp(ruleLine string) bool {
if strings.HasPrefix(ruleLine, "(http?:\\/\\/)") {
ruleLine = ruleLine[12:]
} else if strings.HasPrefix(ruleLine, "(https?:\\/\\/)") {
ruleLine = ruleLine[13:]
} else if strings.HasPrefix(ruleLine, "^") {
ruleLine = ruleLine[1:]
} else {
return false
}
_, parseErr := strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8)
return parseErr == nil
}
func parseAdGuardHostLine(ruleLine string) (string, error) {
idx := strings.Index(ruleLine, " ")
if idx == -1 {
return "", os.ErrInvalid
}
address, err := netip.ParseAddr(ruleLine[:idx])
if err != nil {
return "", err
}
if !address.IsUnspecified() {
return "", nil
}
domain := ruleLine[idx+1:]
if !M.IsDomainName(domain) {
return "", E.New("invalid domain name: ", domain)
}
return domain, nil
}
func parseADGuardIPCIDRLine(ruleLine string) (netip.Prefix, error) {
var isPrefix bool
if strings.HasSuffix(ruleLine, ".") {
isPrefix = true
ruleLine = ruleLine[:len(ruleLine)-1]
}
ruleStringParts := strings.Split(ruleLine, ".")
if len(ruleStringParts) > 4 || len(ruleStringParts) < 4 && !isPrefix {
return netip.Prefix{}, os.ErrInvalid
}
ruleParts := make([]uint8, 0, len(ruleStringParts))
for _, part := range ruleStringParts {
rulePart, err := strconv.ParseUint(part, 10, 8)
if err != nil {
return netip.Prefix{}, err
}
ruleParts = append(ruleParts, uint8(rulePart))
}
bitLen := len(ruleParts) * 8
for len(ruleParts) < 4 {
ruleParts = append(ruleParts, 0)
}
return netip.PrefixFrom(netip.AddrFrom4(*(*[4]byte)(ruleParts)), bitLen), nil
}

View File

@@ -0,0 +1,140 @@
package adguard
import (
"strings"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/route"
"github.com/stretchr/testify/require"
)
func TestConverter(t *testing.T) {
t.Parallel()
rules, err := Convert(strings.NewReader(`
||example.org^
|example.com^
example.net^
||example.edu
||example.edu.tw^
|example.gov
example.arpa
@@|sagernet.example.org|
||sagernet.org^$important
@@|sing-box.sagernet.org^$important
`))
require.NoError(t, err)
require.Len(t, rules, 1)
rule, err := route.NewHeadlessRule(nil, rules[0])
require.NoError(t, err)
matchDomain := []string{
"example.org",
"www.example.org",
"example.com",
"example.net",
"isexample.net",
"www.example.net",
"example.edu",
"example.edu.cn",
"example.edu.tw",
"www.example.edu",
"www.example.edu.cn",
"example.gov",
"example.gov.cn",
"example.arpa",
"www.example.arpa",
"isexample.arpa",
"example.arpa.cn",
"www.example.arpa.cn",
"isexample.arpa.cn",
"sagernet.org",
"www.sagernet.org",
}
notMatchDomain := []string{
"example.org.cn",
"notexample.org",
"example.com.cn",
"www.example.com.cn",
"example.net.cn",
"notexample.edu",
"notexample.edu.cn",
"www.example.gov",
"notexample.gov",
"sagernet.example.org",
"sing-box.sagernet.org",
}
for _, domain := range matchDomain {
require.True(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
for _, domain := range notMatchDomain {
require.False(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
}
func TestHosts(t *testing.T) {
t.Parallel()
rules, err := Convert(strings.NewReader(`
127.0.0.1 localhost
::1 localhost #[IPv6]
0.0.0.0 google.com
`))
require.NoError(t, err)
require.Len(t, rules, 1)
rule, err := route.NewHeadlessRule(nil, rules[0])
require.NoError(t, err)
matchDomain := []string{
"google.com",
}
notMatchDomain := []string{
"www.google.com",
"notgoogle.com",
"localhost",
}
for _, domain := range matchDomain {
require.True(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
for _, domain := range notMatchDomain {
require.False(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
}
func TestSimpleHosts(t *testing.T) {
t.Parallel()
rules, err := Convert(strings.NewReader(`
example.com
www.example.org
`))
require.NoError(t, err)
require.Len(t, rules, 1)
rule, err := route.NewHeadlessRule(nil, rules[0])
require.NoError(t, err)
matchDomain := []string{
"example.com",
"www.example.org",
}
notMatchDomain := []string{
"example.com.cn",
"www.example.com",
"notexample.com",
"example.org",
}
for _, domain := range matchDomain {
require.True(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
for _, domain := range notMatchDomain {
require.False(t, rule.Match(&adapter.InboundContext{
Domain: domain,
}), domain)
}
}

View File

@@ -1,74 +1,11 @@
//go:build !generate
package main package main
import ( import "github.com/sagernet/sing-box/log"
"context"
"os"
"os/user"
"strconv"
"time"
_ "github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/service/filemanager"
"github.com/spf13/cobra"
)
var (
globalCtx context.Context
configPaths []string
configDirectories []string
workingDir string
disableColor bool
)
var mainCommand = &cobra.Command{
Use: "sing-box",
PersistentPreRun: preRun,
}
func init() {
mainCommand.PersistentFlags().StringArrayVarP(&configPaths, "config", "c", nil, "set configuration file path")
mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path")
mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
}
func main() { func main() {
if err := mainCommand.Execute(); err != nil { if err := mainCommand.Execute(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
func preRun(cmd *cobra.Command, args []string) {
globalCtx = context.Background()
sudoUser := os.Getenv("SUDO_USER")
sudoUID, _ := strconv.Atoi(os.Getenv("SUDO_UID"))
sudoGID, _ := strconv.Atoi(os.Getenv("SUDO_GID"))
if sudoUID == 0 && sudoGID == 0 && sudoUser != "" {
sudoUserObject, _ := user.Lookup(sudoUser)
if sudoUserObject != nil {
sudoUID, _ = strconv.Atoi(sudoUserObject.Uid)
sudoGID, _ = strconv.Atoi(sudoUserObject.Gid)
}
}
if sudoUID > 0 && sudoGID > 0 {
globalCtx = filemanager.WithDefault(globalCtx, "", "", sudoUID, sudoGID)
}
if disableColor {
log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger())
}
if workingDir != "" {
_, err := os.Stat(workingDir)
if err != nil {
filemanager.MkdirAll(globalCtx, workingDir, 0o777)
}
err = os.Chdir(workingDir)
if err != nil {
log.Fatal(err)
}
}
if len(configPaths) == 0 && len(configDirectories) == 0 {
configPaths = append(configPaths, "config.json")
}
}

View File

@@ -4,6 +4,8 @@ package badtls
import ( import (
"bytes" "bytes"
"context"
"net"
"os" "os"
"reflect" "reflect"
"sync" "sync"
@@ -18,20 +20,32 @@ import (
var _ N.ReadWaiter = (*ReadWaitConn)(nil) var _ N.ReadWaiter = (*ReadWaitConn)(nil)
type ReadWaitConn struct { type ReadWaitConn struct {
*tls.STDConn tls.Conn
halfAccess *sync.Mutex halfAccess *sync.Mutex
rawInput *bytes.Buffer rawInput *bytes.Buffer
input *bytes.Reader input *bytes.Reader
hand *bytes.Buffer hand *bytes.Buffer
readWaitOptions N.ReadWaitOptions readWaitOptions N.ReadWaitOptions
tlsReadRecord func() error
tlsHandlePostHandshakeMessage func() error
} }
func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) { func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) {
stdConn, isSTDConn := conn.(*tls.STDConn) var (
if !isSTDConn { loaded bool
tlsReadRecord func() error
tlsHandlePostHandshakeMessage func() error
)
for _, tlsCreator := range tlsRegistry {
loaded, tlsReadRecord, tlsHandlePostHandshakeMessage = tlsCreator(conn)
if loaded {
break
}
}
if !loaded {
return nil, os.ErrInvalid return nil, os.ErrInvalid
} }
rawConn := reflect.Indirect(reflect.ValueOf(stdConn)) rawConn := reflect.Indirect(reflect.ValueOf(conn))
rawHalfConn := rawConn.FieldByName("in") rawHalfConn := rawConn.FieldByName("in")
if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct { if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid half conn") return nil, E.New("badtls: invalid half conn")
@@ -57,11 +71,13 @@ func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) {
} }
hand := (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr())) hand := (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr()))
return &ReadWaitConn{ return &ReadWaitConn{
STDConn: stdConn, Conn: conn,
halfAccess: halfAccess, halfAccess: halfAccess,
rawInput: rawInput, rawInput: rawInput,
input: input, input: input,
hand: hand, hand: hand,
tlsReadRecord: tlsReadRecord,
tlsHandlePostHandshakeMessage: tlsHandlePostHandshakeMessage,
}, nil }, nil
} }
@@ -71,19 +87,19 @@ func (c *ReadWaitConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy
} }
func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) { func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
err = c.Handshake() err = c.HandshakeContext(context.Background())
if err != nil { if err != nil {
return return
} }
c.halfAccess.Lock() c.halfAccess.Lock()
defer c.halfAccess.Unlock() defer c.halfAccess.Unlock()
for c.input.Len() == 0 { for c.input.Len() == 0 {
err = tlsReadRecord(c.STDConn) err = c.tlsReadRecord()
if err != nil { if err != nil {
return return
} }
for c.hand.Len() > 0 { for c.hand.Len() > 0 {
err = tlsHandlePostHandshakeMessage(c.STDConn) err = c.tlsHandlePostHandshakeMessage()
if err != nil { if err != nil {
return return
} }
@@ -100,7 +116,7 @@ func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
if n != 0 && c.input.Len() == 0 && c.rawInput.Len() > 0 && if n != 0 && c.input.Len() == 0 && c.rawInput.Len() > 0 &&
// recordType(c.rawInput.Bytes()[0]) == recordTypeAlert { // recordType(c.rawInput.Bytes()[0]) == recordTypeAlert {
c.rawInput.Bytes()[0] == 21 { c.rawInput.Bytes()[0] == 21 {
_ = tlsReadRecord(c.STDConn) _ = c.tlsReadRecord()
// return n, err // will be io.EOF on closeNotify // return n, err // will be io.EOF on closeNotify
} }
@@ -109,11 +125,27 @@ func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
} }
func (c *ReadWaitConn) Upstream() any { func (c *ReadWaitConn) Upstream() any {
return c.STDConn return c.Conn
} }
//go:linkname tlsReadRecord crypto/tls.(*Conn).readRecord var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error)
func tlsReadRecord(c *tls.STDConn) error
//go:linkname tlsHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage func init() {
func tlsHandlePostHandshakeMessage(c *tls.STDConn) error tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
tlsConn, loaded := conn.(*tls.STDConn)
if !loaded {
return
}
return true, func() error {
return stdTLSReadRecord(tlsConn)
}, func() error {
return stdTLSHandlePostHandshakeMessage(tlsConn)
}
})
}
//go:linkname stdTLSReadRecord crypto/tls.(*Conn).readRecord
func stdTLSReadRecord(c *tls.STDConn) error
//go:linkname stdTLSHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage
func stdTLSHandlePostHandshakeMessage(c *tls.STDConn) error

View File

@@ -0,0 +1,31 @@
//go:build go1.21 && !without_badtls && with_ech
package badtls
import (
"net"
_ "unsafe"
"github.com/sagernet/cloudflare-tls"
"github.com/sagernet/sing/common"
)
func init() {
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
tlsConn, loaded := common.Cast[*tls.Conn](conn)
if !loaded {
return
}
return true, func() error {
return echReadRecord(tlsConn)
}, func() error {
return echHandlePostHandshakeMessage(tlsConn)
}
})
}
//go:linkname echReadRecord github.com/sagernet/cloudflare-tls.(*Conn).readRecord
func echReadRecord(c *tls.Conn) error
//go:linkname echHandlePostHandshakeMessage github.com/sagernet/cloudflare-tls.(*Conn).handlePostHandshakeMessage
func echHandlePostHandshakeMessage(c *tls.Conn) error

View File

@@ -0,0 +1,31 @@
//go:build go1.21 && !without_badtls && with_utls
package badtls
import (
"net"
_ "unsafe"
"github.com/sagernet/sing/common"
"github.com/sagernet/utls"
)
func init() {
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
tlsConn, loaded := common.Cast[*tls.UConn](conn)
if !loaded {
return
}
return true, func() error {
return utlsReadRecord(tlsConn.Conn)
}, func() error {
return utlsHandlePostHandshakeMessage(tlsConn.Conn)
}
})
}
//go:linkname utlsReadRecord github.com/sagernet/utls.(*Conn).readRecord
func utlsReadRecord(c *tls.Conn) error
//go:linkname utlsHandlePostHandshakeMessage github.com/sagernet/utls.(*Conn).handlePostHandshakeMessage
func utlsHandlePostHandshakeMessage(c *tls.Conn) error

View File

@@ -32,24 +32,44 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
var dialer net.Dialer var dialer net.Dialer
var listener net.ListenConfig var listener net.ListenConfig
if options.BindInterface != "" { if options.BindInterface != "" {
bindFunc := control.BindToInterface(router.InterfaceFinder(), options.BindInterface, -1) var interfaceFinder control.InterfaceFinder
if router != nil {
interfaceFinder = router.InterfaceFinder()
} else {
interfaceFinder = control.NewDefaultInterfaceFinder()
}
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)
} else if router.AutoDetectInterface() { } else if router != nil && router.AutoDetectInterface() {
bindFunc := router.AutoDetectInterfaceFunc() bindFunc := router.AutoDetectInterfaceFunc()
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} else if router.DefaultInterface() != "" { } else if router != nil && router.DefaultInterface() != "" {
bindFunc := control.BindToInterface(router.InterfaceFinder(), router.DefaultInterface(), -1) bindFunc := control.BindToInterface(router.InterfaceFinder(), router.DefaultInterface(), -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 { var autoRedirectOutputMark uint32
if router != nil {
autoRedirectOutputMark = router.AutoRedirectOutputMark()
}
if autoRedirectOutputMark > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
}
if options.RoutingMark > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark)) dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark)) listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark))
} else if router.DefaultMark() != 0 { if autoRedirectOutputMark > 0 {
return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `routing_mark`")
}
} else if router != nil && router.DefaultMark() > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark())) dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark()))
listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark())) listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark()))
if autoRedirectOutputMark > 0 {
return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `default_mark`")
}
} }
if options.ReuseAddr { if options.ReuseAddr {
listener.Control = control.Append(listener.Control, control.ReuseAddr()) listener.Control = control.Append(listener.Control, control.ReuseAddr())
@@ -61,8 +81,11 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
if options.ConnectTimeout != 0 { if options.ConnectTimeout != 0 {
dialer.Timeout = time.Duration(options.ConnectTimeout) dialer.Timeout = time.Duration(options.ConnectTimeout)
} else { } else {
dialer.Timeout = C.TCPTimeout dialer.Timeout = C.TCPConnectTimeout
} }
// TODO: Add an option to customize the keep alive period
dialer.KeepAlive = C.TCPKeepAliveInitial
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(C.TCPKeepAliveInitial, C.TCPKeepAliveInterval))
var udpFragment bool var udpFragment bool
if options.UDPFragment != nil { if options.UDPFragment != nil {
udpFragment = *options.UDPFragment udpFragment = *options.UDPFragment

View File

@@ -5,7 +5,7 @@ package dialer
import ( import (
"net" "net"
"github.com/sagernet/tfo-go" "github.com/metacubex/tfo-go"
) )
type tcpDialer = tfo.Dialer type tcpDialer = tfo.Dialer

View File

@@ -13,6 +13,9 @@ func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error)
if options.IsWireGuardListener { if options.IsWireGuardListener {
return NewDefault(router, options) return NewDefault(router, options)
} }
if router == nil {
return NewDefault(nil, options)
}
var ( var (
dialer N.Dialer dialer N.Dialer
err error err error
@@ -25,13 +28,12 @@ func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error)
} else { } else {
dialer = NewDetour(router, options.Detour) dialer = NewDetour(router, options.Detour)
} }
domainStrategy := dns.DomainStrategy(options.DomainStrategy) if options.Detour == "" {
if domainStrategy != dns.DomainStrategyAsIS || options.Detour == "" {
dialer = NewResolveDialer( dialer = NewResolveDialer(
router, router,
dialer, dialer,
options.Detour == "" && !options.TCPFastOpen, options.Detour == "" && !options.TCPFastOpen,
domainStrategy, dns.DomainStrategy(options.DomainStrategy),
time.Duration(options.FallbackDelay)) time.Duration(options.FallbackDelay))
} }
return dialer, nil return dialer, nil

View File

@@ -15,7 +15,8 @@ import (
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/tfo-go"
"github.com/metacubex/tfo-go"
) )
type slowOpenConn struct { type slowOpenConn struct {

View File

@@ -0,0 +1,34 @@
package geosite_test
import (
"bytes"
"testing"
"github.com/sagernet/sing-box/common/geosite"
"github.com/stretchr/testify/require"
)
func TestGeosite(t *testing.T) {
t.Parallel()
var buffer bytes.Buffer
err := geosite.Write(&buffer, map[string][]geosite.Item{
"test": {
{
Type: geosite.RuleTypeDomain,
Value: "example.org",
},
},
})
require.NoError(t, err)
reader, codes, err := geosite.NewReader(bytes.NewReader(buffer.Bytes()))
require.NoError(t, err)
require.Equal(t, []string{"test"}, codes)
items, err := reader.Read("test")
require.NoError(t, err)
require.Equal(t, []geosite.Item{{
Type: geosite.RuleTypeDomain,
Value: "example.org",
}}, items)
}

View File

@@ -1,17 +1,24 @@
package geosite package geosite
import ( import (
"bufio"
"encoding/binary"
"io" "io"
"os" "os"
"sync"
"sync/atomic"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/varbin"
) )
type Reader struct { type Reader struct {
reader io.ReadSeeker access sync.Mutex
domainIndex map[string]int reader io.ReadSeeker
domainLength map[string]int bufferedReader *bufio.Reader
metadataIndex int64
domainIndex map[string]int
domainLength map[string]int
} }
func Open(path string) (*Reader, []string, error) { func Open(path string) (*Reader, []string, error) {
@@ -19,14 +26,22 @@ func Open(path string) (*Reader, []string, error) {
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
reader := &Reader{ reader, codes, err := NewReader(content)
reader: content,
}
err = reader.readMetadata()
if err != nil { if err != nil {
content.Close() content.Close()
return nil, nil, err return nil, nil, err
} }
return reader, codes, nil
}
func NewReader(readSeeker io.ReadSeeker) (*Reader, []string, error) {
reader := &Reader{
reader: readSeeker,
}
err := reader.readMetadata()
if err != nil {
return nil, nil, err
}
codes := make([]string, 0, len(reader.domainIndex)) codes := make([]string, 0, len(reader.domainIndex))
for code := range reader.domainIndex { for code := range reader.domainIndex {
codes = append(codes, code) codes = append(codes, code)
@@ -34,15 +49,23 @@ func Open(path string) (*Reader, []string, error) {
return reader, codes, nil return reader, codes, nil
} }
type geositeMetadata struct {
Code string
Index uint64
Length uint64
}
func (r *Reader) readMetadata() error { func (r *Reader) readMetadata() error {
version, err := rw.ReadByte(r.reader) counter := &readCounter{Reader: r.reader}
reader := bufio.NewReader(counter)
version, err := reader.ReadByte()
if err != nil { if err != nil {
return err return err
} }
if version != 0 { if version != 0 {
return E.New("unknown version") return E.New("unknown version")
} }
entryLength, err := rw.ReadUVariant(r.reader) entryLength, err := binary.ReadUvarint(reader)
if err != nil { if err != nil {
return err return err
} }
@@ -55,16 +78,16 @@ func (r *Reader) readMetadata() error {
codeIndex uint64 codeIndex uint64
codeLength uint64 codeLength uint64
) )
code, err = rw.ReadVString(r.reader) code, err = varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil { if err != nil {
return err return err
} }
keys[i] = code keys[i] = code
codeIndex, err = rw.ReadUVariant(r.reader) codeIndex, err = binary.ReadUvarint(reader)
if err != nil { if err != nil {
return err return err
} }
codeLength, err = rw.ReadUVariant(r.reader) codeLength, err = binary.ReadUvarint(reader)
if err != nil { if err != nil {
return err return err
} }
@@ -73,6 +96,8 @@ func (r *Reader) readMetadata() error {
} }
r.domainIndex = domainIndex r.domainIndex = domainIndex
r.domainLength = domainLength r.domainLength = domainLength
r.metadataIndex = counter.count - int64(reader.Buffered())
r.bufferedReader = reader
return nil return nil
} }
@@ -81,31 +106,32 @@ func (r *Reader) Read(code string) ([]Item, error) {
if !exists { if !exists {
return nil, E.New("code ", code, " not exists!") return nil, E.New("code ", code, " not exists!")
} }
_, err := r.reader.Seek(int64(index), io.SeekCurrent) _, err := r.reader.Seek(r.metadataIndex+int64(index), io.SeekStart)
if err != nil { if err != nil {
return nil, err return nil, err
} }
counter := &rw.ReadCounter{Reader: r.reader} r.bufferedReader.Reset(r.reader)
domain := make([]Item, r.domainLength[code]) itemList := make([]Item, r.domainLength[code])
for i := range domain { err = varbin.Read(r.bufferedReader, binary.BigEndian, &itemList)
var ( if err != nil {
item Item return nil, err
err error
)
item.Type, err = rw.ReadByte(counter)
if err != nil {
return nil, err
}
item.Value, err = rw.ReadVString(counter)
if err != nil {
return nil, err
}
domain[i] = item
} }
_, err = r.reader.Seek(int64(-index)-counter.Count(), io.SeekCurrent) return itemList, nil
return domain, err
} }
func (r *Reader) Upstream() any { func (r *Reader) Upstream() any {
return r.reader return r.reader
} }
type readCounter struct {
io.Reader
count int64
}
func (r *readCounter) Read(p []byte) (n int, err error) {
n, err = r.Reader.Read(p)
if n > 0 {
atomic.AddInt64(&r.count, int64(n))
}
return
}

View File

@@ -2,13 +2,13 @@ package geosite
import ( import (
"bytes" "bytes"
"io" "encoding/binary"
"sort" "sort"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/varbin"
) )
func Write(writer io.Writer, domains map[string][]Item) error { func Write(writer varbin.Writer, domains map[string][]Item) error {
keys := make([]string, 0, len(domains)) keys := make([]string, 0, len(domains))
for code := range domains { for code := range domains {
keys = append(keys, code) keys = append(keys, code)
@@ -19,35 +19,34 @@ func Write(writer io.Writer, domains map[string][]Item) error {
index := make(map[string]int) index := make(map[string]int)
for _, code := range keys { for _, code := range keys {
index[code] = content.Len() index[code] = content.Len()
for _, domain := range domains[code] { for _, item := range domains[code] {
content.WriteByte(domain.Type) err := varbin.Write(content, binary.BigEndian, item)
err := rw.WriteVString(content, domain.Value)
if err != nil { if err != nil {
return err return err
} }
} }
} }
err := rw.WriteByte(writer, 0) err := writer.WriteByte(0)
if err != nil { if err != nil {
return err return err
} }
err = rw.WriteUVariant(writer, uint64(len(keys))) _, err = varbin.WriteUvarint(writer, uint64(len(keys)))
if err != nil { if err != nil {
return err return err
} }
for _, code := range keys { for _, code := range keys {
err = rw.WriteVString(writer, code) err = varbin.Write(writer, binary.BigEndian, code)
if err != nil { if err != nil {
return err return err
} }
err = rw.WriteUVariant(writer, uint64(index[code])) _, err = varbin.WriteUvarint(writer, uint64(index[code]))
if err != nil { if err != nil {
return err return err
} }
err = rw.WriteUVariant(writer, uint64(len(domains[code]))) _, err = varbin.WriteUvarint(writer, uint64(len(domains[code])))
if err != nil { if err != nil {
return err return err
} }

29
common/ja3/LICENSE Normal file
View File

@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2018, Open Systems AG
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

3
common/ja3/README.md Normal file
View File

@@ -0,0 +1,3 @@
# JA3
mod from: https://github.com/open-ch/ja3

31
common/ja3/error.go Normal file
View File

@@ -0,0 +1,31 @@
// Copyright (c) 2018, Open Systems AG. All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file in the root of the source
// tree.
package ja3
import "fmt"
// Error types
const (
LengthErr string = "length check %v failed"
ContentTypeErr string = "content type not matching"
VersionErr string = "version check %v failed"
HandshakeTypeErr string = "handshake type not matching"
SNITypeErr string = "SNI type not supported"
)
// ParseError can be encountered while parsing a segment
type ParseError struct {
errType string
check int
}
func (e *ParseError) Error() string {
if e.errType == LengthErr || e.errType == VersionErr {
return fmt.Sprintf(e.errType, e.check)
}
return fmt.Sprint(e.errType)
}

83
common/ja3/ja3.go Normal file
View File

@@ -0,0 +1,83 @@
// Copyright (c) 2018, Open Systems AG. All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file in the root of the source
// tree.
package ja3
import (
"crypto/md5"
"encoding/hex"
"golang.org/x/exp/slices"
)
type ClientHello struct {
Version uint16
CipherSuites []uint16
Extensions []uint16
EllipticCurves []uint16
EllipticCurvePF []uint8
Versions []uint16
SignatureAlgorithms []uint16
ServerName string
ja3ByteString []byte
ja3Hash string
}
func (j *ClientHello) Equals(another *ClientHello, ignoreExtensionsSequence bool) bool {
if j.Version != another.Version {
return false
}
if !slices.Equal(j.CipherSuites, another.CipherSuites) {
return false
}
if !ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.Extensions) {
return false
}
if ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.sortedExtensions()) {
return false
}
if !slices.Equal(j.EllipticCurves, another.EllipticCurves) {
return false
}
if !slices.Equal(j.EllipticCurvePF, another.EllipticCurvePF) {
return false
}
if !slices.Equal(j.SignatureAlgorithms, another.SignatureAlgorithms) {
return false
}
return true
}
func (j *ClientHello) sortedExtensions() []uint16 {
extensions := make([]uint16, len(j.Extensions))
copy(extensions, j.Extensions)
slices.Sort(extensions)
return extensions
}
func Compute(payload []byte) (*ClientHello, error) {
ja3 := ClientHello{}
err := ja3.parseSegment(payload)
return &ja3, err
}
func (j *ClientHello) String() string {
if j.ja3ByteString == nil {
j.marshalJA3()
}
return string(j.ja3ByteString)
}
func (j *ClientHello) Hash() string {
if j.ja3ByteString == nil {
j.marshalJA3()
}
if j.ja3Hash == "" {
h := md5.Sum(j.ja3ByteString)
j.ja3Hash = hex.EncodeToString(h[:])
}
return j.ja3Hash
}

357
common/ja3/parser.go Normal file
View File

@@ -0,0 +1,357 @@
// Copyright (c) 2018, Open Systems AG. All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file in the root of the source
// tree.
package ja3
import (
"encoding/binary"
"strconv"
)
const (
// Constants used for parsing
recordLayerHeaderLen int = 5
handshakeHeaderLen int = 6
randomDataLen int = 32
sessionIDHeaderLen int = 1
cipherSuiteHeaderLen int = 2
compressMethodHeaderLen int = 1
extensionsHeaderLen int = 2
extensionHeaderLen int = 4
sniExtensionHeaderLen int = 5
ecExtensionHeaderLen int = 2
ecpfExtensionHeaderLen int = 1
versionExtensionHeaderLen int = 1
signatureAlgorithmsExtensionHeaderLen int = 2
contentType uint8 = 22
handshakeType uint8 = 1
sniExtensionType uint16 = 0
sniNameDNSHostnameType uint8 = 0
ecExtensionType uint16 = 10
ecpfExtensionType uint16 = 11
versionExtensionType uint16 = 43
signatureAlgorithmsExtensionType uint16 = 13
// Versions
// The bitmask covers the versions SSL3.0 to TLS1.2
tlsVersionBitmask uint16 = 0xFFFC
tls13 uint16 = 0x0304
// GREASE values
// The bitmask covers all GREASE values
GreaseBitmask uint16 = 0x0F0F
// Constants used for marshalling
dashByte = byte(45)
commaByte = byte(44)
)
// parseSegment to populate the corresponding ClientHello object or return an error
func (j *ClientHello) parseSegment(segment []byte) error {
// Check if we can decode the next fields
if len(segment) < recordLayerHeaderLen {
return &ParseError{LengthErr, 1}
}
// Check if we have "Content Type: Handshake (22)"
contType := uint8(segment[0])
if contType != contentType {
return &ParseError{errType: ContentTypeErr}
}
// Check if TLS record layer version is supported
tlsRecordVersion := uint16(segment[1])<<8 | uint16(segment[2])
if tlsRecordVersion&tlsVersionBitmask != 0x0300 && tlsRecordVersion != tls13 {
return &ParseError{VersionErr, 1}
}
// Check that the Handshake is as long as expected from the length field
segmentLen := uint16(segment[3])<<8 | uint16(segment[4])
if len(segment[recordLayerHeaderLen:]) < int(segmentLen) {
return &ParseError{LengthErr, 2}
}
// Keep the Handshake messege, ignore any additional following record types
hs := segment[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)]
err := j.parseHandshake(hs)
return err
}
// parseHandshake body
func (j *ClientHello) parseHandshake(hs []byte) error {
// Check if we can decode the next fields
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen {
return &ParseError{LengthErr, 3}
}
// Check if we have "Handshake Type: Client Hello (1)"
handshType := uint8(hs[0])
if handshType != handshakeType {
return &ParseError{errType: HandshakeTypeErr}
}
// Check if actual length of handshake matches (this is a great exclusion criterion for false positives,
// as these fields have to match the actual length of the rest of the segment)
handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3])
if len(hs[4:]) != int(handshakeLen) {
return &ParseError{LengthErr, 4}
}
// Check if Client Hello version is supported
tlsVersion := uint16(hs[4])<<8 | uint16(hs[5])
if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 {
return &ParseError{VersionErr, 2}
}
j.Version = tlsVersion
// Check if we can decode the next fields
sessionIDLen := uint8(hs[38])
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) {
return &ParseError{LengthErr, 5}
}
// Cipher Suites
cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):]
// Check if we can decode the next fields
if len(cs) < cipherSuiteHeaderLen {
return &ParseError{LengthErr, 6}
}
csLen := uint16(cs[0])<<8 | uint16(cs[1])
numCiphers := int(csLen / 2)
cipherSuites := make([]uint16, 0, numCiphers)
// Check if we can decode the next fields
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen {
return &ParseError{LengthErr, 7}
}
for i := 0; i < numCiphers; i++ {
cipherSuite := uint16(cs[2+i<<1])<<8 | uint16(cs[3+i<<1])
cipherSuites = append(cipherSuites, cipherSuite)
}
j.CipherSuites = cipherSuites
// Check if we can decode the next fields
compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)])
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) {
return &ParseError{LengthErr, 8}
}
// Extensions
exs := cs[cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen):]
err := j.parseExtensions(exs)
return err
}
// parseExtensions of the handshake
func (j *ClientHello) parseExtensions(exs []byte) error {
// Check for no extensions, this fields header is nonexistent if no body is used
if len(exs) == 0 {
return nil
}
// Check if we can decode the next fields
if len(exs) < extensionsHeaderLen {
return &ParseError{LengthErr, 9}
}
exsLen := uint16(exs[0])<<8 | uint16(exs[1])
exs = exs[extensionsHeaderLen:]
// Check if we can decode the next fields
if len(exs) < int(exsLen) {
return &ParseError{LengthErr, 10}
}
var sni []byte
var extensions, ellipticCurves []uint16
var ellipticCurvePF []uint8
var versions []uint16
var signatureAlgorithms []uint16
for len(exs) > 0 {
// Check if we can decode the next fields
if len(exs) < extensionHeaderLen {
return &ParseError{LengthErr, 11}
}
exType := uint16(exs[0])<<8 | uint16(exs[1])
exLen := uint16(exs[2])<<8 | uint16(exs[3])
// Ignore any GREASE extensions
extensions = append(extensions, exType)
// Check if we can decode the next fields
if len(exs) < extensionHeaderLen+int(exLen) {
return &ParseError{LengthErr, 12}
}
sex := exs[extensionHeaderLen : extensionHeaderLen+int(exLen)]
switch exType {
case sniExtensionType: // Extensions: server_name
// Check if we can decode the next fields
if len(sex) < sniExtensionHeaderLen {
return &ParseError{LengthErr, 13}
}
sniType := uint8(sex[2])
sniLen := uint16(sex[3])<<8 | uint16(sex[4])
sex = sex[sniExtensionHeaderLen:]
// Check if we can decode the next fields
if len(sex) != int(sniLen) {
return &ParseError{LengthErr, 14}
}
switch sniType {
case sniNameDNSHostnameType:
sni = sex
default:
return &ParseError{errType: SNITypeErr}
}
case ecExtensionType: // Extensions: supported_groups
// Check if we can decode the next fields
if len(sex) < ecExtensionHeaderLen {
return &ParseError{LengthErr, 15}
}
ecsLen := uint16(sex[0])<<8 | uint16(sex[1])
numCurves := int(ecsLen / 2)
ellipticCurves = make([]uint16, 0, numCurves)
sex = sex[ecExtensionHeaderLen:]
// Check if we can decode the next fields
if len(sex) != int(ecsLen) {
return &ParseError{LengthErr, 16}
}
for i := 0; i < numCurves; i++ {
ecType := uint16(sex[i*2])<<8 | uint16(sex[1+i*2])
ellipticCurves = append(ellipticCurves, ecType)
}
case ecpfExtensionType: // Extensions: ec_point_formats
// Check if we can decode the next fields
if len(sex) < ecpfExtensionHeaderLen {
return &ParseError{LengthErr, 17}
}
ecpfsLen := uint8(sex[0])
numPF := int(ecpfsLen)
ellipticCurvePF = make([]uint8, numPF)
sex = sex[ecpfExtensionHeaderLen:]
// Check if we can decode the next fields
if len(sex) != numPF {
return &ParseError{LengthErr, 18}
}
for i := 0; i < numPF; i++ {
ellipticCurvePF[i] = uint8(sex[i])
}
case versionExtensionType:
if len(sex) < versionExtensionHeaderLen {
return &ParseError{LengthErr, 19}
}
versionsLen := int(sex[0])
for i := 0; i < versionsLen; i += 2 {
versions = append(versions, binary.BigEndian.Uint16(sex[1:][i:]))
}
case signatureAlgorithmsExtensionType:
if len(sex) < signatureAlgorithmsExtensionHeaderLen {
return &ParseError{LengthErr, 20}
}
ssaLen := binary.BigEndian.Uint16(sex)
for i := 0; i < int(ssaLen); i += 2 {
signatureAlgorithms = append(signatureAlgorithms, binary.BigEndian.Uint16(sex[2:][i:]))
}
}
exs = exs[4+exLen:]
}
j.ServerName = string(sni)
j.Extensions = extensions
j.EllipticCurves = ellipticCurves
j.EllipticCurvePF = ellipticCurvePF
j.Versions = versions
j.SignatureAlgorithms = signatureAlgorithms
return nil
}
// marshalJA3 into a byte string
func (j *ClientHello) marshalJA3() {
// An uint16 can contain numbers with up to 5 digits and an uint8 can contain numbers with up to 3 digits, but we
// also need a byte for each separating character, except at the end.
byteStringLen := 6*(1+len(j.CipherSuites)+len(j.Extensions)+len(j.EllipticCurves)) + 4*len(j.EllipticCurvePF) - 1
byteString := make([]byte, 0, byteStringLen)
// Version
byteString = strconv.AppendUint(byteString, uint64(j.Version), 10)
byteString = append(byteString, commaByte)
// Cipher Suites
if len(j.CipherSuites) != 0 {
for _, val := range j.CipherSuites {
if val&GreaseBitmask != 0x0A0A {
continue
}
byteString = strconv.AppendUint(byteString, uint64(val), 10)
byteString = append(byteString, dashByte)
}
// Replace last dash with a comma
byteString[len(byteString)-1] = commaByte
} else {
byteString = append(byteString, commaByte)
}
// Extensions
if len(j.Extensions) != 0 {
for _, val := range j.Extensions {
if val&GreaseBitmask != 0x0A0A {
continue
}
byteString = strconv.AppendUint(byteString, uint64(val), 10)
byteString = append(byteString, dashByte)
}
// Replace last dash with a comma
byteString[len(byteString)-1] = commaByte
} else {
byteString = append(byteString, commaByte)
}
// Elliptic curves
if len(j.EllipticCurves) != 0 {
for _, val := range j.EllipticCurves {
if val&GreaseBitmask != 0x0A0A {
continue
}
byteString = strconv.AppendUint(byteString, uint64(val), 10)
byteString = append(byteString, dashByte)
}
// Replace last dash with a comma
byteString[len(byteString)-1] = commaByte
} else {
byteString = append(byteString, commaByte)
}
// ECPF
if len(j.EllipticCurvePF) != 0 {
for _, val := range j.EllipticCurvePF {
byteString = strconv.AppendUint(byteString, uint64(val), 10)
byteString = append(byteString, dashByte)
}
// Remove last dash
byteString = byteString[:len(byteString)-1]
}
j.ja3ByteString = byteString
}

View File

@@ -1,11 +1,16 @@
package mux package mux
import ( import (
"context"
"net"
"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"
"github.com/sagernet/sing-mux" "github.com/sagernet/sing-mux"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
@@ -30,7 +35,7 @@ func NewClientWithOptions(dialer N.Dialer, logger logger.Logger, options option.
} }
} }
return mux.NewClient(mux.Options{ return mux.NewClient(mux.Options{
Dialer: dialer, Dialer: &clientDialer{dialer},
Logger: logger, Logger: logger,
Protocol: options.Protocol, Protocol: options.Protocol,
MaxConnections: options.MaxConnections, MaxConnections: options.MaxConnections,
@@ -40,3 +45,15 @@ func NewClientWithOptions(dialer N.Dialer, logger logger.Logger, options option.
Brutal: brutalOptions, Brutal: brutalOptions,
}) })
} }
type clientDialer struct {
N.Dialer
}
func (d *clientDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
return d.Dialer.DialContext(adapter.OverrideContext(ctx), network, destination)
}
func (d *clientDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return d.Dialer.ListenPacket(adapter.OverrideContext(ctx), destination)
}

View File

@@ -60,12 +60,12 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
isIPv4 := ip.Is4() isIPv4 := ip.Is4()
value, err := syscall.Sysctl(spath) value, err := unix.SysctlRaw(spath)
if err != nil { if err != nil {
return "", err return "", err
} }
buf := []byte(value) buf := value
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n // from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
// size/offset are round up (aligned) to 8 bytes in darwin // size/offset are round up (aligned) to 8 bytes in darwin

View File

@@ -223,7 +223,7 @@ func getExecPathFromPID(pid uint32) (string, error) {
r1, _, err := syscall.SyscallN( r1, _, err := syscall.SyscallN(
procQueryFullProcessImageNameW.Addr(), procQueryFullProcessImageNameW.Addr(),
uintptr(h), uintptr(h),
uintptr(1), uintptr(0),
uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)), uintptr(unsafe.Pointer(&size)),
) )

View File

@@ -16,30 +16,40 @@ import (
) )
type LinuxSystemProxy struct { type LinuxSystemProxy struct {
hasGSettings bool hasGSettings bool
hasKWriteConfig5 bool kWriteConfigCmd string
sudoUser string sudoUser string
serverAddr M.Socksaddr serverAddr M.Socksaddr
supportSOCKS bool supportSOCKS bool
isEnabled bool isEnabled bool
} }
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*LinuxSystemProxy, error) { func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*LinuxSystemProxy, error) {
hasGSettings := common.Error(exec.LookPath("gsettings")) == nil hasGSettings := common.Error(exec.LookPath("gsettings")) == nil
hasKWriteConfig5 := common.Error(exec.LookPath("kwriteconfig5")) == nil kWriteConfigCmds := []string{
"kwriteconfig5",
"kwriteconfig6",
}
var kWriteConfigCmd string
for _, cmd := range kWriteConfigCmds {
if common.Error(exec.LookPath(cmd)) == nil {
kWriteConfigCmd = cmd
break
}
}
var sudoUser string var sudoUser string
if os.Getuid() == 0 { if os.Getuid() == 0 {
sudoUser = os.Getenv("SUDO_USER") sudoUser = os.Getenv("SUDO_USER")
} }
if !hasGSettings && !hasKWriteConfig5 { if !hasGSettings && kWriteConfigCmd == "" {
return nil, E.New("unsupported desktop environment") return nil, E.New("unsupported desktop environment")
} }
return &LinuxSystemProxy{ return &LinuxSystemProxy{
hasGSettings: hasGSettings, hasGSettings: hasGSettings,
hasKWriteConfig5: hasKWriteConfig5, kWriteConfigCmd: kWriteConfigCmd,
sudoUser: sudoUser, sudoUser: sudoUser,
serverAddr: serverAddr, serverAddr: serverAddr,
supportSOCKS: supportSOCKS, supportSOCKS: supportSOCKS,
}, nil }, nil
} }
@@ -70,8 +80,8 @@ func (p *LinuxSystemProxy) Enable() error {
return err return err
} }
} }
if p.hasKWriteConfig5 { if p.kWriteConfigCmd != "" {
err := p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "1") err := p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "1")
if err != nil { if err != nil {
return err return err
} }
@@ -83,7 +93,7 @@ func (p *LinuxSystemProxy) Enable() error {
if err != nil { if err != nil {
return err return err
} }
err = p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "Authmode", "0") err = p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "Authmode", "0")
if err != nil { if err != nil {
return err return err
} }
@@ -103,8 +113,8 @@ func (p *LinuxSystemProxy) Disable() error {
return err return err
} }
} }
if p.hasKWriteConfig5 { if p.kWriteConfigCmd != "" {
err := p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "0") err := p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "0")
if err != nil { if err != nil {
return err return err
} }
@@ -150,7 +160,7 @@ func (p *LinuxSystemProxy) setKDEProxy(proxyTypes ...string) error {
proxyUrl = "http://" + p.serverAddr.String() proxyUrl = "http://" + p.serverAddr.String()
} }
err := p.runAsUser( err := p.runAsUser(
"kwriteconfig5", p.kWriteConfigCmd,
"--file", "--file",
"kioslaverc", "kioslaverc",
"--group", "--group",

View File

@@ -0,0 +1,99 @@
package sniff
import (
"bytes"
"context"
"encoding/binary"
"io"
"os"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
)
const (
trackerConnectFlag = 0
trackerProtocolID = 0x41727101980
trackerConnectMinSize = 16
)
// BitTorrent detects if the stream is a BitTorrent connection.
// For the BitTorrent protocol specification, see https://www.bittorrent.org/beps/bep_0003.html
func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
var first byte
err := binary.Read(reader, binary.BigEndian, &first)
if err != nil {
return err
}
if first != 19 {
return os.ErrInvalid
}
var protocol [19]byte
_, err = reader.Read(protocol[:])
if err != nil {
return err
}
if string(protocol[:]) != "BitTorrent protocol" {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolBitTorrent
return nil
}
// UTP detects if the packet is a uTP connection packet.
// For the uTP protocol specification, see
// 1. https://www.bittorrent.org/beps/bep_0029.html
// 2. https://github.com/bittorrent/libutp/blob/2b364cbb0650bdab64a5de2abb4518f9f228ec44/utp_internal.cpp#L112
func UTP(_ context.Context, metadata *adapter.InboundContext, packet []byte) error {
// A valid uTP packet must be at least 20 bytes long.
if len(packet) < 20 {
return os.ErrInvalid
}
version := packet[0] & 0x0F
ty := packet[0] >> 4
if version != 1 || ty > 4 {
return os.ErrInvalid
}
// Validate the extensions
extension := packet[1]
reader := bytes.NewReader(packet[20:])
for extension != 0 {
err := binary.Read(reader, binary.BigEndian, &extension)
if err != nil {
return err
}
var length byte
err = binary.Read(reader, binary.BigEndian, &length)
if err != nil {
return err
}
_, err = reader.Seek(int64(length), io.SeekCurrent)
if err != nil {
return err
}
}
metadata.Protocol = C.ProtocolBitTorrent
return nil
}
// UDPTracker detects if the packet is a UDP Tracker Protocol packet.
// For the UDP Tracker Protocol specification, see https://www.bittorrent.org/beps/bep_0015.html
func UDPTracker(_ context.Context, metadata *adapter.InboundContext, packet []byte) error {
if len(packet) < trackerConnectMinSize {
return os.ErrInvalid
}
if binary.BigEndian.Uint64(packet[:8]) != trackerProtocolID {
return os.ErrInvalid
}
if binary.BigEndian.Uint32(packet[8:12]) != trackerConnectFlag {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolBitTorrent
return nil
}

View File

@@ -0,0 +1,73 @@
package sniff_test
import (
"bytes"
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffBittorrent(t *testing.T) {
t.Parallel()
packets := []string{
"13426974546f7272656e742070726f746f636f6c0000000000100000e21ea9569b69bab33c97851d0298bdfa89bc90922d5554313631302dea812fcd6a3563e3be40c1d1",
"13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452333030302d653369733079647675763638",
"13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452343035302d6f7a316c6e79377931716130",
}
for _, pkt := range packets {
pkt, err := hex.DecodeString(pkt)
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt))
require.NoError(t, err)
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
}
}
func TestSniffUTP(t *testing.T) {
t.Parallel()
packets := []string{
"010041a282d7ee7b583afb160004000006d8318da776968f92d666f7963f32dae23ba0d2c810d8b8209cc4939f54fde9eeaa521c2c20c9ba7f43f4fb0375f28de06643b5e3ca4685ab7ac76adca99783be72ef05ed59ef4234f5712b75b4c7c0d7bee8fe2ca20ad626ba5bb0ffcc16bf06790896f888048cf72716419a07db1a3dca4550fbcea75b53e97235168a221cf3e553dfbb723961bd719fab038d86e0ecb74747f5a2cd669de1c4b9ad375f3a492d09d98cdfad745435625401315bbba98d35d32086299801377b93495a63a9efddb8d05f5b37a5c5b1c0a25e917f12007bb5e05013ada8aff544fab8cadf61d80ddb0b60f12741e44515a109d144fd53ef845acb4b5ccf0d6fc302d7003d76df3fc3423bb0237301c9e88f900c2d392a8e0fdb36d143cf7527a93fd0a2638b746e72f6699fffcd4fd15348fce780d4caa04382fd9faf1ca0ae377ca805da7536662b84f5ee18dd3ae38fcb095a7543e55f9069ae92c8cf54ae44e97b558d35e2545c66601ed2149cbc32bd6df199a2be7cf0da8b2ff137e0d23e776bc87248425013876d3a3cc31a83b424b752bd0346437f24b532978005d8f5b1b0be1a37a2489c32a18a9ad3118e3f9d30eb299bffae18e1f0677c2a5c185e62519093fe6bc2b7339299ea50a587989f726ca6443a75dd5bb936f6367c6355d80fae53ff529d740b2e5576e3eefdf1fdbfc69c3c8d8ac750512635de63e054bee1d3b689bc1b2bc3d2601e42a00b5c89066d173d4ae7ffedfd2274e5cf6d868fbe640aedb69b8246142f00b32d459974287537ddd5373460dcbc92f5cfdd7a3ed6020822ae922d947893752ca1983d0d32977374c384ac8f5ab566859019b7351526b9f13e932037a55bb052d9deb3b3c23317e0784fdc51a64f2159bfea3b069cf5caf02ee2c3c1a6b6b427bb16165713e8802d95b5c8ed77953690e994bd38c9ae113fedaf6ee7fc2b96c032ceafc2a530ad0422e84546b9c6ad8ef6ea02fa508abddd1805c38a7b42e9b7c971b1b636865ebec06ed754bb404cd6b4e6cc8cb77bd4a0c43410d5cd5ef8fe853a66d49b3b9e06cb141236cdbfdd5761601dc54d1250b86c660e0f898fe62526fdd9acf0eab60a3bbbb2151970461f28f10b31689594bea646c4b03ee197d63bdef4e5a7c22716b3bb9494a83b78ecd81b338b80ac6c09c43485b1b09ba41c74343832c78f0520c1d659ac9eb1502094141e82fb9e5e620970ebc0655514c43c294a7714cbf9a499d277daf089f556398a01589a77494bec8bfb60a108f3813b55368672b88c1af40f6b3c8b513f7c70c3e0efce85228b8b9ec67ba0393f9f7305024d8e2da6a26cf85613d14f249170ce1000089df4c9c260df7f8292aa2ecb5d5bac97656d59aa248caedea2d198e51ce87baece338716d114b458de02d65c9ff808ca5b5b73723b4d1e962d9ac2d98176544dc9984cf8554d07820ef3dd0861cfe57b478328046380de589adad94ee44743ffac73bb7361feca5d56f07cf8ce75080e261282ae30350d7882679b15cab9e7e53ddf93310b33f7390ae5d318bb53f387e6af5d0ef4f947fc9cb8e7e38b52c7f8d772ece6156b38d88796ea19df02c53723b44df7c76315a0de9462f27287e682d2b4cda1a68fe00d7e48c51ee981be44e1ca940fb5190c12655edb4a83c3a4f33e48a015692df4f0b3d61656e362aca657b5ae8c12db5a0db3db1e45135ee918b66918f40e53c4f83e9da0cddfe63f736ae751ab3837a30ae3220d8e8e311487093a7b90c7e7e40dd54ca750e19452f9193aa892aa6a6229ab493dadae988b1724f7898ee69c36d3eb7364c4adbeca811cfe2065873e78c2b6dfdf1595f7a7831c07e03cda82e4f86f76438dfb2b07c13638ce7b509cfa71b88b5102b39a203b423202088e1c2103319cb32c13c1e546ff8612fa194c95a7808ab767c265a1bd5fa0efed5c8ec1701876a00ec8",
"01001ecb68176f215d04326300100000dbcf30292d14b54e9ee2d115ee5b8ebc7fad3e882d4fcdd0c14c6b917c11cb4c6a9f410b52a33ae97c2ac77c7a2b122b8955e09af3c5c595f1b2e79ca57cfe44c44e069610773b9bc9ba223d7f6b383e3adddd03fb88a8476028e30979c2ef321ffc97c5c132bcf9ac5b410bbb5ec6cefca3c7209202a14c5ae922b6b157b0a80249d13ffe5b996af0bc8e54ba576d148372494303e7ead0602b05b9c8fc97d48508a028a04d63a1fd28b0edfcd5c51715f63188b53eefede98a76912dca98518551a8856567307a56a702cbfcc115ea0c755b418bc2c7b57721239b82f09fb24328a4b0ce0f109bcb2a64e04b8aadb1f8487585425acdf8fc4ec8ea93cfcec5ac098bb29d42ddef6e46b03f34a5de28316726699b7cb5195c33e5c48abe87d591d63f9991c84c30819d186d6e0e95fd83c8dff07aa669c4430989bcaccfeacb9bcadbdb4d8f1964dbeb9687745656edd30b21c66cc0a1d742a78717d134a19a7f02d285a4973b1a198c00cfdff4676608dc4f3e817e3463c3b4e2c80d3e8d4fbac541a58a2fb7ad6939f607f8144eff6c8b0adc28ee5609ea158987519892fb",
"21001ecb6817f2805d044fd700100000dbd03029",
"410277ef0b1fb1f60000000000040000c233000000080000000000000000",
}
for _, pkt := range packets {
pkt, err := hex.DecodeString(pkt)
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.UTP(context.TODO(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
}
}
func TestSniffUDPTracker(t *testing.T) {
t.Parallel()
connectPackets := []string{
"00000417271019800000000078e90560",
"00000417271019800000000022c5d64d",
"000004172710198000000000b3863541",
}
for _, pkt := range connectPackets {
pkt, err := hex.DecodeString(pkt)
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.UDPTracker(context.TODO(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
}
}

View File

@@ -17,18 +17,17 @@ import (
mDNS "github.com/miekg/dns" mDNS "github.com/miekg/dns"
) )
func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.InboundContext, error) { func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
var length uint16 var length uint16
err := binary.Read(reader, binary.BigEndian, &length) err := binary.Read(reader, binary.BigEndian, &length)
if err != nil { if err != nil {
return nil, err return os.ErrInvalid
} }
if length == 0 { if length == 0 {
return nil, os.ErrInvalid return os.ErrInvalid
} }
buffer := buf.NewSize(int(length)) buffer := buf.NewSize(int(length))
defer buffer.Release() defer buffer.Release()
readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100) readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100)
var readTask task.Group var readTask task.Group
readTask.Append0(func(ctx context.Context) error { readTask.Append0(func(ctx context.Context) error {
@@ -37,19 +36,20 @@ func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.
err = readTask.Run(readCtx) err = readTask.Run(readCtx)
cancel() cancel()
if err != nil { if err != nil {
return nil, err return err
} }
return DomainNameQuery(readCtx, buffer.Bytes()) return DomainNameQuery(readCtx, metadata, buffer.Bytes())
} }
func DomainNameQuery(ctx context.Context, packet []byte) (*adapter.InboundContext, error) { func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
var msg mDNS.Msg var msg mDNS.Msg
err := msg.Unpack(packet) err := msg.Unpack(packet)
if err != nil { if err != nil {
return nil, err return err
} }
if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) { if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) {
return nil, os.ErrInvalid return os.ErrInvalid
} }
return &adapter.InboundContext{Protocol: C.ProtocolDNS}, nil metadata.Protocol = C.ProtocolDNS
return nil
} }

32
common/sniff/dtls.go Normal file
View File

@@ -0,0 +1,32 @@
package sniff
import (
"context"
"os"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
)
func DTLSRecord(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
const fixedHeaderSize = 13
if len(packet) < fixedHeaderSize {
return os.ErrInvalid
}
contentType := packet[0]
switch contentType {
case 20, 21, 22, 23, 25:
default:
return os.ErrInvalid
}
versionMajor := packet[1]
if versionMajor != 0xfe {
return os.ErrInvalid
}
versionMinor := packet[2]
if versionMinor != 0xff && versionMinor != 0xfd {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolDTLS
return nil
}

33
common/sniff/dtls_test.go Normal file
View File

@@ -0,0 +1,33 @@
package sniff_test
import (
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffDTLSClientHello(t *testing.T) {
t.Parallel()
packet, err := hex.DecodeString("16fefd0000000000000000007e010000720000000000000072fefd668a43523798e064bd806d0c87660de9c611a59bbdfc3892c4e072d94f2cafc40000000cc02bc02fc00ac014c02cc0300100003c000d0010000e0403050306030401050106010807ff01000100000a00080006001d00170018000b00020100000e000900060008000700010000170000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.DTLSRecord(context.Background(), &metadata, packet)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolDTLS)
}
func TestSniffDTLSClientApplicationData(t *testing.T) {
t.Parallel()
packet, err := hex.DecodeString("17fefd000100000000000100440001000000000001a4f682b77ecadd10f3f3a2f78d90566212366ff8209fd77314f5a49352f9bb9bd12f4daba0b4736ae29e46b9714d3b424b3e6d0234736619b5aa0d3f")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.DTLSRecord(context.Background(), &metadata, packet)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolDTLS)
}

View File

@@ -11,10 +11,12 @@ import (
"github.com/sagernet/sing/protocol/http" "github.com/sagernet/sing/protocol/http"
) )
func HTTPHost(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) { func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
request, err := http.ReadRequest(std_bufio.NewReader(reader)) request, err := http.ReadRequest(std_bufio.NewReader(reader))
if err != nil { if err != nil {
return nil, err return err
} }
return &adapter.InboundContext{Protocol: C.ProtocolHTTP, Domain: M.ParseSocksaddr(request.Host).AddrString()}, nil metadata.Protocol = C.ProtocolHTTP
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
return nil
} }

View File

@@ -5,6 +5,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff" "github.com/sagernet/sing-box/common/sniff"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -13,7 +14,8 @@ import (
func TestSniffHTTP1(t *testing.T) { func TestSniffHTTP1(t *testing.T) {
t.Parallel() t.Parallel()
pkt := "GET / HTTP/1.1\r\nHost: www.google.com\r\nAccept: */*\r\n\r\n" pkt := "GET / HTTP/1.1\r\nHost: www.google.com\r\nAccept: */*\r\n\r\n"
metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt)) var metadata adapter.InboundContext
err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, metadata.Domain, "www.google.com") require.Equal(t, metadata.Domain, "www.google.com")
} }
@@ -21,7 +23,8 @@ func TestSniffHTTP1(t *testing.T) {
func TestSniffHTTP1WithPort(t *testing.T) { func TestSniffHTTP1WithPort(t *testing.T) {
t.Parallel() t.Parallel()
pkt := "GET / HTTP/1.1\r\nHost: www.gov.cn:8080\r\nAccept: */*\r\n\r\n" pkt := "GET / HTTP/1.1\r\nHost: www.gov.cn:8080\r\nAccept: */*\r\n\r\n"
metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt)) var metadata adapter.InboundContext
err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, metadata.Domain, "www.gov.cn") require.Equal(t, metadata.Domain, "www.gov.cn")
} }

View File

@@ -5,95 +5,99 @@ import (
"context" "context"
"crypto" "crypto"
"crypto/aes" "crypto/aes"
"crypto/tls"
"encoding/binary" "encoding/binary"
"io" "io"
"os" "os"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/ja3"
"github.com/sagernet/sing-box/common/sniff/internal/qtls" "github.com/sagernet/sing-box/common/sniff/internal/qtls"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
) )
func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContext, error) { var ErrClientHelloFragmented = E.New("need more packet for chromium QUIC connection")
reader := bytes.NewReader(packet)
func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
reader := bytes.NewReader(packet)
typeByte, err := reader.ReadByte() typeByte, err := reader.ReadByte()
if err != nil { if err != nil {
return nil, err return err
} }
if typeByte&0x40 == 0 { if typeByte&0x40 == 0 {
return nil, E.New("bad type byte") return E.New("bad type byte")
} }
var versionNumber uint32 var versionNumber uint32
err = binary.Read(reader, binary.BigEndian, &versionNumber) err = binary.Read(reader, binary.BigEndian, &versionNumber)
if err != nil { if err != nil {
return nil, err return err
} }
if versionNumber != qtls.VersionDraft29 && versionNumber != qtls.Version1 && versionNumber != qtls.Version2 { if versionNumber != qtls.VersionDraft29 && versionNumber != qtls.Version1 && versionNumber != qtls.Version2 {
return nil, E.New("bad version") return E.New("bad version")
} }
packetType := (typeByte & 0x30) >> 4 packetType := (typeByte & 0x30) >> 4
if packetType == 0 && versionNumber == qtls.Version2 || packetType == 2 && versionNumber != qtls.Version2 || packetType > 2 { if packetType == 0 && versionNumber == qtls.Version2 || packetType == 2 && versionNumber != qtls.Version2 || packetType > 2 {
return nil, E.New("bad packet type") return E.New("bad packet type")
} }
destConnIDLen, err := reader.ReadByte() destConnIDLen, err := reader.ReadByte()
if err != nil { if err != nil {
return nil, err return err
} }
if destConnIDLen == 0 || destConnIDLen > 20 { if destConnIDLen == 0 || destConnIDLen > 20 {
return nil, E.New("bad destination connection id length") return E.New("bad destination connection id length")
} }
destConnID := make([]byte, destConnIDLen) destConnID := make([]byte, destConnIDLen)
_, err = io.ReadFull(reader, destConnID) _, err = io.ReadFull(reader, destConnID)
if err != nil { if err != nil {
return nil, err return err
} }
srcConnIDLen, err := reader.ReadByte() srcConnIDLen, err := reader.ReadByte()
if err != nil { if err != nil {
return nil, err return err
} }
_, err = io.CopyN(io.Discard, reader, int64(srcConnIDLen)) _, err = io.CopyN(io.Discard, reader, int64(srcConnIDLen))
if err != nil { if err != nil {
return nil, err return err
} }
tokenLen, err := qtls.ReadUvarint(reader) tokenLen, err := qtls.ReadUvarint(reader)
if err != nil { if err != nil {
return nil, err return err
} }
_, err = io.CopyN(io.Discard, reader, int64(tokenLen)) _, err = io.CopyN(io.Discard, reader, int64(tokenLen))
if err != nil { if err != nil {
return nil, err return err
} }
packetLen, err := qtls.ReadUvarint(reader) packetLen, err := qtls.ReadUvarint(reader)
if err != nil { if err != nil {
return nil, err return err
} }
hdrLen := int(reader.Size()) - reader.Len() hdrLen := int(reader.Size()) - reader.Len()
if hdrLen+int(packetLen) > len(packet) { if hdrLen+int(packetLen) > len(packet) {
return nil, os.ErrInvalid return os.ErrInvalid
} }
_, err = io.CopyN(io.Discard, reader, 4) _, err = io.CopyN(io.Discard, reader, 4)
if err != nil { if err != nil {
return nil, err return err
} }
pnBytes := make([]byte, aes.BlockSize) pnBytes := make([]byte, aes.BlockSize)
_, err = io.ReadFull(reader, pnBytes) _, err = io.ReadFull(reader, pnBytes)
if err != nil { if err != nil {
return nil, err return err
} }
var salt []byte var salt []byte
@@ -117,7 +121,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
hpKey := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, hkdfHeaderProtectionLabel, 16) hpKey := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, hkdfHeaderProtectionLabel, 16)
block, err := aes.NewCipher(hpKey) block, err := aes.NewCipher(hpKey)
if err != nil { if err != nil {
return nil, err return err
} }
mask := make([]byte, aes.BlockSize) mask := make([]byte, aes.BlockSize)
block.Encrypt(mask, pnBytes) block.Encrypt(mask, pnBytes)
@@ -129,7 +133,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
} }
packetNumberLength := newPacket[0]&0x3 + 1 packetNumberLength := newPacket[0]&0x3 + 1
if hdrLen+int(packetNumberLength) > int(packetLen)+hdrLen { if hdrLen+int(packetNumberLength) > int(packetLen)+hdrLen {
return nil, os.ErrInvalid return os.ErrInvalid
} }
var packetNumber uint32 var packetNumber uint32
switch packetNumberLength { switch packetNumberLength {
@@ -142,7 +146,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
case 4: case 4:
packetNumber = binary.BigEndian.Uint32(newPacket[hdrLen:]) packetNumber = binary.BigEndian.Uint32(newPacket[hdrLen:])
default: default:
return nil, E.New("bad packet number length") return E.New("bad packet number length")
} }
extHdrLen := hdrLen + int(packetNumberLength) extHdrLen := hdrLen + int(packetNumberLength)
copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:]) copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:])
@@ -166,138 +170,208 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber)) binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber))
decrypted, err := cipher.Open(newPacket[extHdrLen:extHdrLen], nonce, data, newPacket[:extHdrLen]) decrypted, err := cipher.Open(newPacket[extHdrLen:extHdrLen], nonce, data, newPacket[:extHdrLen])
if err != nil { if err != nil {
return nil, err return err
} }
var frameType byte var frameType byte
var frameLen uint64 var fragments []qCryptoFragment
var fragments []struct {
offset uint64
length uint64
payload []byte
}
decryptedReader := bytes.NewReader(decrypted) decryptedReader := bytes.NewReader(decrypted)
const (
frameTypePadding = 0x00
frameTypePing = 0x01
frameTypeAck = 0x02
frameTypeAck2 = 0x03
frameTypeCrypto = 0x06
frameTypeConnectionClose = 0x1c
)
var frameTypeList []uint8
for { for {
frameType, err = decryptedReader.ReadByte() frameType, err = decryptedReader.ReadByte()
if err == io.EOF { if err == io.EOF {
break break
} }
frameTypeList = append(frameTypeList, frameType)
switch frameType { switch frameType {
case 0x00: // PADDING case frameTypePadding:
continue continue
case 0x01: // PING case frameTypePing:
continue continue
case 0x02, 0x03: // ACK case frameTypeAck, frameTypeAck2:
_, err = qtls.ReadUvarint(decryptedReader) // Largest Acknowledged _, err = qtls.ReadUvarint(decryptedReader) // Largest Acknowledged
if err != nil { if err != nil {
return nil, err return err
} }
_, err = qtls.ReadUvarint(decryptedReader) // ACK Delay _, err = qtls.ReadUvarint(decryptedReader) // ACK Delay
if err != nil { if err != nil {
return nil, err return err
} }
ackRangeCount, err := qtls.ReadUvarint(decryptedReader) // ACK Range Count ackRangeCount, err := qtls.ReadUvarint(decryptedReader) // ACK Range Count
if err != nil { if err != nil {
return nil, err return err
} }
_, err = qtls.ReadUvarint(decryptedReader) // First ACK Range _, err = qtls.ReadUvarint(decryptedReader) // First ACK Range
if err != nil { if err != nil {
return nil, err return err
} }
for i := 0; i < int(ackRangeCount); i++ { for i := 0; i < int(ackRangeCount); i++ {
_, err = qtls.ReadUvarint(decryptedReader) // Gap _, err = qtls.ReadUvarint(decryptedReader) // Gap
if err != nil { if err != nil {
return nil, err return err
} }
_, err = qtls.ReadUvarint(decryptedReader) // ACK Range Length _, err = qtls.ReadUvarint(decryptedReader) // ACK Range Length
if err != nil { if err != nil {
return nil, err return err
} }
} }
if frameType == 0x03 { if frameType == 0x03 {
_, err = qtls.ReadUvarint(decryptedReader) // ECT0 Count _, err = qtls.ReadUvarint(decryptedReader) // ECT0 Count
if err != nil { if err != nil {
return nil, err return err
} }
_, err = qtls.ReadUvarint(decryptedReader) // ECT1 Count _, err = qtls.ReadUvarint(decryptedReader) // ECT1 Count
if err != nil { if err != nil {
return nil, err return err
} }
_, err = qtls.ReadUvarint(decryptedReader) // ECN-CE Count _, err = qtls.ReadUvarint(decryptedReader) // ECN-CE Count
if err != nil { if err != nil {
return nil, err return err
} }
} }
case 0x06: // CRYPTO case frameTypeCrypto:
var offset uint64 var offset uint64
offset, err = qtls.ReadUvarint(decryptedReader) offset, err = qtls.ReadUvarint(decryptedReader)
if err != nil { if err != nil {
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err return err
} }
var length uint64 var length uint64
length, err = qtls.ReadUvarint(decryptedReader) length, err = qtls.ReadUvarint(decryptedReader)
if err != nil { if err != nil {
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err return err
} }
index := len(decrypted) - decryptedReader.Len() index := len(decrypted) - decryptedReader.Len()
fragments = append(fragments, struct { fragments = append(fragments, qCryptoFragment{offset, length, decrypted[index : index+int(length)]})
offset uint64
length uint64
payload []byte
}{offset, length, decrypted[index : index+int(length)]})
frameLen += length
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent) _, err = decryptedReader.Seek(int64(length), io.SeekCurrent)
if err != nil { if err != nil {
return nil, err return err
} }
case 0x1c: // CONNECTION_CLOSE case frameTypeConnectionClose:
_, err = qtls.ReadUvarint(decryptedReader) // Error Code _, err = qtls.ReadUvarint(decryptedReader) // Error Code
if err != nil { if err != nil {
return nil, err return err
} }
_, err = qtls.ReadUvarint(decryptedReader) // Frame Type _, err = qtls.ReadUvarint(decryptedReader) // Frame Type
if err != nil { if err != nil {
return nil, err return err
} }
var length uint64 var length uint64
length, err = qtls.ReadUvarint(decryptedReader) // Reason Phrase Length length, err = qtls.ReadUvarint(decryptedReader) // Reason Phrase Length
if err != nil { if err != nil {
return nil, err return err
} }
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent) // Reason Phrase _, err = decryptedReader.Seek(int64(length), io.SeekCurrent) // Reason Phrase
if err != nil { if err != nil {
return nil, err return err
} }
default: default:
return nil, os.ErrInvalid return os.ErrInvalid
} }
} }
tlsHdr := make([]byte, 5) if metadata.SniffContext != nil {
tlsHdr[0] = 0x16 fragments = append(fragments, metadata.SniffContext.([]qCryptoFragment)...)
binary.BigEndian.PutUint16(tlsHdr[1:], uint16(0x0303)) metadata.SniffContext = nil
binary.BigEndian.PutUint16(tlsHdr[3:], uint16(frameLen)) }
var frameLen uint64
for _, fragment := range fragments {
frameLen += fragment.length
}
buffer := buf.NewSize(5 + int(frameLen))
defer buffer.Release()
buffer.WriteByte(0x16)
binary.Write(buffer, binary.BigEndian, uint16(0x0303))
binary.Write(buffer, binary.BigEndian, uint16(frameLen))
var index uint64 var index uint64
var length int var length int
var readers []io.Reader
readers = append(readers, bytes.NewReader(tlsHdr))
find: find:
for { for {
for _, fragment := range fragments { for _, fragment := range fragments {
if fragment.offset == index { if fragment.offset == index {
readers = append(readers, bytes.NewReader(fragment.payload)) buffer.Write(fragment.payload)
index = fragment.offset + fragment.length index = fragment.offset + fragment.length
length++ length++
continue find continue find
} }
} }
if length == len(fragments) { break
break
}
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, E.New("bad fragments")
}
metadata, err := TLSClientHello(ctx, io.MultiReader(readers...))
if err != nil {
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
} }
metadata.Protocol = C.ProtocolQUIC metadata.Protocol = C.ProtocolQUIC
return metadata, nil fingerprint, err := ja3.Compute(buffer.Bytes())
if err != nil {
metadata.Protocol = C.ProtocolQUIC
metadata.Client = C.ClientChromium
metadata.SniffContext = fragments
return ErrClientHelloFragmented
}
metadata.Domain = fingerprint.ServerName
for metadata.Client == "" {
if len(frameTypeList) == 1 {
metadata.Client = C.ClientFirefox
break
}
if frameTypeList[0] == frameTypeCrypto && isZero(frameTypeList[1:]) {
if len(fingerprint.Versions) == 2 && fingerprint.Versions[0]&ja3.GreaseBitmask == 0x0A0A &&
len(fingerprint.EllipticCurves) == 5 && fingerprint.EllipticCurves[0]&ja3.GreaseBitmask == 0x0A0A {
metadata.Client = C.ClientSafari
break
}
if len(fingerprint.CipherSuites) == 1 && fingerprint.CipherSuites[0] == tls.TLS_AES_256_GCM_SHA384 &&
len(fingerprint.EllipticCurves) == 1 && fingerprint.EllipticCurves[0] == uint16(tls.X25519) &&
len(fingerprint.SignatureAlgorithms) == 1 && fingerprint.SignatureAlgorithms[0] == uint16(tls.ECDSAWithP256AndSHA256) {
metadata.Client = C.ClientSafari
break
}
}
if frameTypeList[len(frameTypeList)-1] == frameTypeCrypto && isZero(frameTypeList[:len(frameTypeList)-1]) {
metadata.Client = C.ClientQUICGo
break
}
if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 {
if maybeUQUIC(fingerprint) {
metadata.Client = C.ClientQUICGo
} else {
metadata.Client = C.ClientChromium
}
break
}
metadata.Client = C.ClientUnknown
//nolint:staticcheck
break
}
return nil
}
func isZero(slices []uint8) bool {
for _, slice := range slices {
if slice != 0 {
return false
}
}
return true
}
func count(slices []uint8, value uint8) int {
var times int
for _, slice := range slices {
if slice == value {
times++
}
}
return times
}
type qCryptoFragment struct {
offset uint64
length uint64
payload []byte
} }

View File

@@ -0,0 +1,24 @@
package sniff
import (
"crypto/tls"
"github.com/sagernet/sing-box/common/ja3"
)
// Chromium sends separate client hello packets, but UQUIC has not yet implemented this behavior
// The cronet without this behavior does not have version 115
var uQUICChrome115 = &ja3.ClientHello{
Version: tls.VersionTLS12,
CipherSuites: []uint16{4865, 4866, 4867},
Extensions: []uint16{0, 10, 13, 16, 27, 43, 45, 51, 57, 17513},
EllipticCurves: []uint16{29, 23, 24},
SignatureAlgorithms: []uint16{1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537, 513},
}
func maybeUQUIC(fingerprint *ja3.ClientHello) bool {
if uQUICChrome115.Equals(fingerprint, true) {
return true
}
return false
}

View File

@@ -5,31 +5,69 @@ import (
"encoding/hex" "encoding/hex"
"testing" "testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff" "github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestSniffQUICv1(t *testing.T) { func TestSniffQUICChromium(t *testing.T) {
t.Parallel() t.Parallel()
pkt, err := hex.DecodeString("cc0000000108d2dc7bad02241f5003796e71004215a71bfcb05159416c724be418537389acdd9a4047306283dcb4d7a9cad5cc06322042d204da67a8dbaa328ab476bb428b48fd001501863afd203f8d4ef085629d664f1a734a65969a47e4a63d4e01a21f18c1d90db0c027180906dc135f9ae421bb8617314c8d54c175fef3d3383d310d0916ebcbd6eed9329befbbb109d8fd4af1d2cf9d6adce8e6c1260a7f8256e273e326da0aa7cc148d76e7a08489dc9d52ade89c027cbc3491ada46417c2c04e2ca768e9a7dd6aa00c594e48b678927325da796817693499bb727050cb3baf3d3291a397c3a8d868e8ec7b8f7295e347455c9dadbe2252ae917ac793d958c7fb8a3d2cdb34e3891eb4286f18617556ff7216dd60256aa5b1d11ff4753459fc5f9dedf11d483a26a0835dc6cd50e1c1f54f86e8f1e502821183cd874f6447a74e818bf3445c7795acf4559d1c1fac474911d2ead5c8d23e4aa4f67afb66efe305a30a0b5d825679b31ddc186cbea936535795c7e8c378c87b8c5adc065154d15bae8f85ac8fec2da40c3aa623b682a065440831555011d7647cde44446a0fb4cf5892f2c088ae1920643094be72e3c499fe8d265caf939e8ab607a5b9317917d2a32a812e8a0e6a2f84721bbb5984ffd242838f705d13f4cfb249bc6a5c80d58ac2595edf56648ec3fe21d787573c253a79805252d6d81e26d367d4ff29ef66b5fe8992086af7bada8cad10b82a7c0dc406c5b6d0c5ec3c583e767f759ce08cad6c3c8f91e5a8") pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620")
require.NoError(t, err) require.NoError(t, err)
metadata, err := sniff.QUICClientHello(context.Background(), pkt) var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientChromium)
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, metadata.Domain, "cloudflare-quic.com") err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, metadata.Domain, "google.com")
} }
func TestSniffQUICFragment(t *testing.T) { func TestSniffUQUICChrome115(t *testing.T) {
t.Parallel() t.Parallel()
pkt, err := hex.DecodeString("cc00000001082e3d5d1b64040c55000044d0ccea69e773f6631c1d18b04ae9ee75fcfc34ef74fa62533c93534338a86f101a05d70e0697fb483063fa85db1c59ccfbda5c35234931d8524d8aac37eaaad649470a67794cd754b23c98695238b8363452333bc8c4858376b4166e001da2006e35cf98a91e11a56419b2786775284942d0f7163982f7c248867d12dd374957481dbc564013ff785e1916195eef671f725908f761099d992d69231336ba81d9e25fe2fa3a6eff4318a6ccf10176fc841a1b315f7b35c5b292266fc869d76ca533e7d14e86d82db2e22eacd350977e47d2e012d8a5891c5aaf2a0f4c2b2dae897c161e5b68cbb4dee952472bdc1e21504b8f02534ec4366ce3f8bf86efc78e0232778fbd554457567112abdcafcf6d4d8fcf35083c25d9495679614aba21696e338c62b585046cc55ba8c09c844361d889a47c3ea703b4e23545a9ab2c0bb369693a9ddfb5daffa85cf80fdd6ad66738664e5b0a551729b4955cff7255afcb04dee88c2f072c9de7400947a1bd9327ac5d012a33000ada021d4c03d249fb017d6ac9200b2f9436beab8183ddfbe2d8aee31ffb7df9e1cc181c1af80c39a89965d18ed12da8e3ebe2ae1fbe4b348f83ba19e3e3d1c9b22bcf03ab6ad9b30fe180623faa291ebad83bcd71d7b57f2f5e2f3b8e81d24fb70b2f2159239e8f21ffafef2747aba47d97ab4081e603c018b10678cf99cab1fb42156a14486fa435153979d7279fd22cd40af7088bfc7eff41af2f4b3c0c8864d0040d74dff427f7bffdb8c278474ea00311326cf4925471a8cf596cb92119f19e0f789490ba9cb77b98015a987d93e0324cf1a38b55109f00c3e6ddc5180fb107bf468323afec9bb49fd6a86418569789d66cafe3b8253c2aebb3af3782c1c54dd560487d031d28e6a6e23e159581bb1d47efc4da3fe1d169f9ffb0ca9ba61af0a38a92fde5bc5e6ec026e8378a6315a7b95abf1d2da790a391306ce74d0baf8e2ce648ca74c487f2c0a76a28a80cdf5bd34316eb607684fe7e6d9e83824a00e07660d0b90e3cddd61ebf10748263474afa88c300549e64ce2e90560bb1a12dee7e9484f729a8a4ee7c5651adb5194b3b3ae38e501567c7dbf36e7bb37a2c20b74655f47f2d9af18e52e9d4c9c9eee8e63745779b8f0b06f3a09d846ba62eb978ad77c85de1ee2fee3fbb4c2d283c73e1ccba56a4658e48a2665d200f7f9342f8e84c2ba490094a4f94feec89e42d2f654f564c2beb2997bafa1fc2c68ad8e160b63587d49abc31b834878d52acfb05fb73d0e059b206162e3c90b40c4bc08407ffcb3c08431895b691a3fea923f1f3b48db75d3e6b91fd319ffe4d486e0e14bd5c6affc838dee63d9e0b80f169b5e6c02c7321dcb20deb2b8e707b60e345a308d505bbf26a93d8f18b39d62632e9a77cbe48b3b32eb8819d6311a49820d40f5acbf0273c91c36b2269a03e72ee64df3dfb10ddefe73c64ef60870b2b77bd99dea655f5fe791b538a929a14d99f6d69685d72431ea5f0f4b27a044f2f575ab474fcc3857895934de1ca2581798eaef2c17fe5aaf2e6add97fa32997c7026f15c1b1ad0e6043ae506027a7c0242546fdc851cca39a204e56879f2cef838be8ec66e0f2292f8c862e06f810eb9b80c7a467ce6e90155206352c7f82b1173ba3b98d35bb72c259a60db20dd1a43fe6d7aef0265e6eaa5caafd9b64b448ff745a2046acbdb65cf2a5007809808a4828dc99097feedc734c236260c584") pkt, err := hex.DecodeString("cb0000000108181e17c387120abc000044d0705b6a3ef9ee37a8d3949a7d393ed078243c2ee2c3627fad1c3f107c117f4f071131ad61848068fcbbe5c65803c147f7f8ec5e2cd77b77beea23ba779d936dccac540f8396400e3190ea35cc2942af4171a04cb14272491920f90124959f44e80143678c0b52f5d31af319aaa589db2f940f004562724d0af40f737e1bb0002a071e6a1dbc9f52c64f070806a5010abed0298053634d9c9126bd7949ae5087998ade762c0ad06691d99c0875a38c601fc1ee77bfc3b8c11381829f2c9bdd022f4499c43ff1d6aee1a0d296861461dda217d22c568b276016ef3929e59d2f7d7ddf7809920fb7dc805641608949f3f8466ab3d37149aac501f0b107d808f3add4acfc657e4a82e2b88e97a6c74a00c419548760ab3414ba13915c78a1ca79dceee8d59fbe299f20b671ac44823218368b2a026baa55170cf549519ac21dbb6d31d248bd339438a4e663bcdca1fe3ae3f045a5dc19b122e9db9d7af9757076666dda4e9ace1c67def77fa14786f0cab3ebf7a270ea6e2b37838318c95779f80c3b8471948d0046c3614b3a13477c939a39a7855d85d13522a45ae0765739cd5eedef87237e824a929983ace27640c6495dbf5a72fa0b96893dc5d28f3988249a57bdb458d460b4a57043de3da750a76b6e5d2259247ca27cd864ea18f0d09aa62ab6eb7c014fb43179b2a1963d170b756cce83eeaebff78a828d025c811848e16ff862a8080d093478cd2208c8ab0803178325bc0d9d6bb25e62fa50c4ad15cf80916da6578796932036c72e43eb480d1e423ed812ac75a97722f8416529b82ba8ee2219c535012282bb17066bd53e78b87a71abdb7ebdb2a7c2766ff8397962e87d0f85485b64b4ee81cc84f99c47f33f2b0872716441992773f59186e38d32dbf5609a6fda94cb928cd25f5a7a3ab736b5a4236b6d5409ab18892c6a4d3480fc2350abfdf0bab1cedb55bdf0760fdb703e6688f4de596254eed4ed3e67eb03d0717b8e15b31e735214e588c87ae36bc6c310e1894b4c15143e4ccf287b2dbc707a946bf9671ae3c574f9486b2c82eec784bba4cbc76113cbe0f97ac8c13cfa38f2925ab9d06887a612ce48280a91d7e074e6caf898d88e2bbf71360899abf48a03f9a70cf2891199f2d63b116f4871af0ebb4f4906792f66cc21d1609f189138532875c129a68c73e7bcd3b5d8100beac1d8ac4b20d94a59ac8df5a5af58a9acb20413eadf97189f5f19ff889155f0c4d37514ec184eb6903967ff38a41fc087abb0f2cad3761d6e3f95f92a09a72f5c065b16e188088b87460241f27ecdb1bc6ece92c8d36b2d68b58d0fb4d4b3c928c579ade8ae5a995833aadd297c30a37f7bc35440fc97070e1b198e0fac00157452177d16d2803b4239997452b4ad3a951173bdec47a033fd7f8a7942accaa9aaa905b3c5a2175e7c3e07c48bf25331727fd69cd1e64d74d8c9d4a6f8f4491adb7bc911505cb19877083d8f21a12475e313fccf57877ff3556318e81ed9145dd9427f2b65275440893035f417481f721c69215af8ae103530cd0a1d35bf2cb5a27628f8d44d7c6f5ec12ce79d0a8333e0eb48771115d0a191304e46b8db19bbe5c40f1c346dde98e76ff5e21ff38d2c34e60cb07766ed529dd6d2cbacd7fbf1ed8a0e6e40decad0ca5021e91552be87c156d3ae2fffef41c65b14ba6d488f2c3227a1ab11ffce0e2dc47723a69da27a67a7f26e1cb13a7103af9b87a8db8e18ea")
require.NoError(t, err) require.NoError(t, err)
metadata, err := sniff.QUICClientHello(context.Background(), pkt) var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, metadata.Domain, "cloudflare-quic.com") require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientQUICGo)
require.Equal(t, metadata.Domain, "www.google.com")
}
func TestSniffQUICFirefox(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("c8000000010867f174d7ebfe1b0803cd9c20004286de068f7963cf1736349ee6ebe0ddcd3e4cd0041a51ced3f7ce9eea1fb595458e74bdb4b792b16449bd8cae71419862c4fcbe766eaec7d1af65cd298e1dd46f8bd94a77ab4ca28c54b8e9773de3f02d7cb2463c9f7dcacfb311f024b0266ec6ab7bfb615b4148333fb4d4ece7c4cd90029ca30c2cbae2216b428499ec873fa125797e71c5a5da85087760ad37ca610020f71b76e82651c47576e20bf33cf676cb2d400b8c09d3c8cb4e21c47d2b21f6b68732bef30c8cefd5c723fc23eb29e6f7f65a5e52aad9055c1fb3d8b1811f0380b38d7e2eee8eb37dd5bd5d4ca4b66540175d916289d88a9df7c161964d713999c5057d27edb298ef5164352568b0d4bac3c15d90456e8fd460e41b81d0ec1b1e94b87d3333cc6908b018e0914ae1f214d73e75398da3d55a0106161d3a75897b4eb66e98c59010fae75f0d367d38be48c3a5c58bc8a30773c3fff50690ac9d487822f85d4f5713d626baa92d36e858dd21259cf814bce0b90d18da88a1ade40113e5a088cdb304a2558879152a8cf15c1839e056378aa41acba6fcb9974dee54bd50b5d4eb2c475654e06c0ec06b7f18f4462c808684843a1071041b9bfb2688324e0120144944416e30e83eedbbbcbc275b1f53762d3db18f0998ce54f0e1c512946b4098f07781d49264fa148f4c8220a3b02e73d7f15554aa370aafeff73cb75c52c494edf90f0261abfdd32a4d670f729de50266162687aa8efe14b8506f313b058b02aaaab5825428f5f4510b8e49451fdcb7b5a4af4b59c831afcb89fb4f64dba78e3b38387e87e9e8cdaa1f3b700a87c7d442388863b8950296e5773b38f308d62f52548c0bbf308e40540747cca5bf99b1345bc0d70b8f0e69a83b85a8d69f795b87f93e2bfccf52b529afea4ff6fd456957000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientFirefox)
require.Equal(t, metadata.Domain, "www.google.com")
}
func TestSniffQUICSafari(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("c70000000108e4e75af2e223198a0000449ef2d83cb4473a62765eba67424cd4a5817315cbf55a9e8daaca360904b0bae60b1629cfeba11e2dfbbf5ea4c588cb134e31af36fd7a409fb0fcc0187e9b56037ac37964ed20a8c1ca19fd6cfd53398324b3d0c71537294f769db208fa998b6811234a4a7eb3b5eceb457ae92e3a2d98f7c110702db8064b5c29fa3298eb1d0529fd445a84a5fd6ff8709be90f8af4f94998d8a8f2953bb05ad08c80668eca784c6aec959114e68e5b827e7c41c79f2277c716a967e7fcc8d1b77442e6cb18329dbedb34b473516b468cba5fc20659e655fbe37f36408289b9a475fcee091bd82828d3be00367e9e5cec9423bb97854abdada1d7562a3777756eb3bddef826ddc1ef46137cb01bb504a54d410d9bcb74cd5f959050c84edf343fa6a49708c228a758ee7adbbadf260b2f1984911489712e2cb364a3d6520badba4b7e539b9c163eeddfd96c0abb0de151e47496bb9750be76ee17ccdb61d35d2c6795174037d6f9d282c3f36c4d9a90b64f3b6ddd0cf4d9ed8e6f7805e25928fa04b087e63ae02761df30720cc01dfc32b64c575c8a66ef82e9a17400ff80cd8609b93ba16d668f4aa734e71c4a5d145f14ee1151bec970214e0ff83fc3e1e85d8694f2975f9155c57c18b7b69bb6a36832a9435f1f4b346a7be188f3a75f9ad2cc6ad0a3d26d6fa7d4c1179bd49bd5989d15ba43ff602890107db96484695086627356750d7b2b3b714ba65d564654e8f60ac10f5b6d3bfb507e8eaa31bab1da2d676195046d165c7f8b32829c9f9b68d97b2af7ac04a1369357e4b65de2b2f24eaf27cc8d95e05db001adebe726f927a94e43e62ce671e6e306e16f05aafcbe6c49080e80286d7939f375023d110a5ad9069364ae928ca480454a9dcddd61bc48b7efeb716a5bd6c7cd39c486ceb20c738af6abf22ba1ddd8b4a3b781fc2f251173409e1aadccbd7514e97106d0ebfc3af6e59445f74cd733a1ba99b10fce3fb4e9f7c88f5e25b567f5ba2b8dabacd375e7faf7634bfa178cbe51aee63032c5126b196ea47b02385fc3062a000fb7e4b4d0d12e74579f8830ede20d10829496032b2cc56743287f9a9b4d5091877a82fea44deb2cffac8a379f78a151d99e28cbc74d732c083bf06d50584e3f18f254e71a48d6ababaf6fff6f425e9be001510dfbe6a32a27792c00ada036b62ddb90c706d7b882c76a7072f5dd11c69a1f49d4ba183cb0b57545419fa27b9b9706098848935ae9c9e8fbe9fac165d1339128b991a73d20e7795e8d6a8c6adfbf20bf13ada43f2aef3ba78c14697910507132623f721387dce60c4707225b84d9782d469a5d9eaa099f35d6a590ef142ddef766495cf3337815ceef5ff2b3ed352637e72b5c23a2a8ff7d7440236a19b981d47f8e519a0431ebfbc0b78d8a36798b4c060c0c6793499f1e2e818862560a5b501c8d02ba1517be1941da2af5b174e0189c62978d878eb0f9c9db3a9221c28fb94645cf6e85ff2eea8c65ba3083a7382b131b83102dd67aa5453ad7375a4eb8c69fc479fbd29dab8924f801d253f2c997120b705c6e5217fb74702e2f1038917dd5fb0eeb7ae1bf7a668fc7d50c034b4cd5a057a8482e6bc9c921297f44e76967265623a167cd9883eb6e64bc77856dc333bd605d7df3bed0e5cecb5a99fe8b62873d58530f")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientSafari)
require.Equal(t, metadata.Domain, "www.google.com")
} }
func FuzzSniffQUIC(f *testing.F) { func FuzzSniffQUIC(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) { f.Fuzz(func(t *testing.T, data []byte) {
sniff.QUICClientHello(context.Background(), data) var metadata adapter.InboundContext
err := sniff.QUICClientHello(context.Background(), &metadata, data)
require.Error(t, err)
}) })
} }

90
common/sniff/rdp.go Normal file
View File

@@ -0,0 +1,90 @@
package sniff
import (
"context"
"encoding/binary"
"io"
"os"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common/rw"
)
func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
var tpktVersion uint8
err := binary.Read(reader, binary.BigEndian, &tpktVersion)
if err != nil {
return err
}
if tpktVersion != 0x03 {
return os.ErrInvalid
}
var tpktReserved uint8
err = binary.Read(reader, binary.BigEndian, &tpktReserved)
if err != nil {
return err
}
if tpktReserved != 0x00 {
return os.ErrInvalid
}
var tpktLength uint16
err = binary.Read(reader, binary.BigEndian, &tpktLength)
if err != nil {
return err
}
if tpktLength != 19 {
return os.ErrInvalid
}
var cotpLength uint8
err = binary.Read(reader, binary.BigEndian, &cotpLength)
if err != nil {
return err
}
if cotpLength != 14 {
return os.ErrInvalid
}
var cotpTpduType uint8
err = binary.Read(reader, binary.BigEndian, &cotpTpduType)
if err != nil {
return err
}
if cotpTpduType != 0xE0 {
return os.ErrInvalid
}
err = rw.SkipN(reader, 5)
if err != nil {
return err
}
var rdpType uint8
err = binary.Read(reader, binary.BigEndian, &rdpType)
if err != nil {
return err
}
if rdpType != 0x01 {
return os.ErrInvalid
}
var rdpFlags uint8
err = binary.Read(reader, binary.BigEndian, &rdpFlags)
if err != nil {
return err
}
var rdpLength uint8
err = binary.Read(reader, binary.BigEndian, &rdpLength)
if err != nil {
return err
}
if rdpLength != 8 {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolRDP
return nil
}

25
common/sniff/rdp_test.go Normal file
View File

@@ -0,0 +1,25 @@
package sniff_test
import (
"bytes"
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffRDP(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("030000130ee00000000000010008000b000000010008000b000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.RDP(context.TODO(), &metadata, bytes.NewReader(pkt))
require.NoError(t, err)
require.Equal(t, C.ProtocolRDP, metadata.Protocol)
}

View File

@@ -14,49 +14,65 @@ import (
) )
type ( type (
StreamSniffer = func(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) StreamSniffer = func(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error
PacketSniffer = func(ctx context.Context, packet []byte) (*adapter.InboundContext, error) PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error
) )
func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) (*adapter.InboundContext, error) { func Skip(metadata adapter.InboundContext) bool {
// skip server first protocols
switch metadata.Destination.Port {
case 25, 465, 587:
// SMTP
return true
case 143, 993:
// IMAP
return true
case 110, 995:
// POP3
return true
}
return false
}
func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error {
if timeout == 0 { if timeout == 0 {
timeout = C.ReadPayloadTimeout timeout = C.ReadPayloadTimeout
} }
deadline := time.Now().Add(timeout) deadline := time.Now().Add(timeout)
var errors []error var errors []error
for i := 0; ; i++ {
for i := 0; i < 3; i++ {
err := conn.SetReadDeadline(deadline) err := conn.SetReadDeadline(deadline)
if err != nil { if err != nil {
return nil, E.Cause(err, "set read deadline") return E.Cause(err, "set read deadline")
} }
_, err = buffer.ReadOnceFrom(conn) _, err = buffer.ReadOnceFrom(conn)
err = E.Errors(err, conn.SetReadDeadline(time.Time{})) _ = conn.SetReadDeadline(time.Time{})
if err != nil { if err != nil {
if i > 0 { if i > 0 {
break break
} }
return nil, E.Cause(err, "read payload") return E.Cause(err, "read payload")
} }
errors = nil
for _, sniffer := range sniffers { for _, sniffer := range sniffers {
metadata, err := sniffer(ctx, bytes.NewReader(buffer.Bytes())) err = sniffer(ctx, metadata, bytes.NewReader(buffer.Bytes()))
if metadata != nil { if err == nil {
return metadata, nil return nil
} }
errors = append(errors, err) errors = append(errors, err)
} }
} }
return nil, E.Errors(errors...) return E.Errors(errors...)
} }
func PeekPacket(ctx context.Context, packet []byte, sniffers ...PacketSniffer) (*adapter.InboundContext, error) { func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error {
var errors []error var errors []error
for _, sniffer := range sniffers { for _, sniffer := range sniffers {
metadata, err := sniffer(ctx, packet) err := sniffer(ctx, metadata, packet)
if metadata != nil { if err == nil {
return metadata, nil return nil
} }
errors = append(errors, err) errors = append(errors, err)
} }
return nil, E.Errors(errors...) return E.Errors(errors...)
} }

26
common/sniff/ssh.go Normal file
View File

@@ -0,0 +1,26 @@
package sniff
import (
"bufio"
"context"
"io"
"os"
"strings"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
)
func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
scanner := bufio.NewScanner(reader)
if !scanner.Scan() {
return os.ErrInvalid
}
fistLine := scanner.Text()
if !strings.HasPrefix(fistLine, "SSH-2.0-") {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolSSH
metadata.Client = fistLine[8:]
return nil
}

26
common/sniff/ssh_test.go Normal file
View File

@@ -0,0 +1,26 @@
package sniff_test
import (
"bytes"
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffSSH(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("5353482d322e302d64726f70626561720d0a000001a40a1492892570d1223aef61b0d647972c8bd30000009f637572766532353531392d7368613235362c637572766532353531392d736861323536406c69627373682e6f72672c6469666669652d68656c6c6d616e2d67726f757031342d7368613235362c6469666669652d68656c6c6d616e2d67726f757031342d736861312c6b6578677565737332406d6174742e7563632e61736e2e61752c6b65782d7374726963742d732d763030406f70656e7373682e636f6d000000207373682d656432353531392c7273612d736861322d3235362c7373682d7273610000003363686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733235362d6374720000003363686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733235362d63747200000017686d61632d736861312c686d61632d736861322d32353600000017686d61632d736861312c686d61632d736861322d323536000000046e6f6e65000000046e6f6e65000000000000000000000000002aa6ed090585b7d635b6")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt))
require.NoError(t, err)
require.Equal(t, C.ProtocolSSH, metadata.Protocol)
require.Equal(t, "dropbear", metadata.Client)
}

View File

@@ -9,16 +9,17 @@ import (
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
) )
func STUNMessage(ctx context.Context, packet []byte) (*adapter.InboundContext, error) { func STUNMessage(_ context.Context, metadata *adapter.InboundContext, packet []byte) error {
pLen := len(packet) pLen := len(packet)
if pLen < 20 { if pLen < 20 {
return nil, os.ErrInvalid return os.ErrInvalid
} }
if binary.BigEndian.Uint32(packet[4:8]) != 0x2112A442 { if binary.BigEndian.Uint32(packet[4:8]) != 0x2112A442 {
return nil, os.ErrInvalid return os.ErrInvalid
} }
if len(packet) < 20+int(binary.BigEndian.Uint16(packet[2:4])) { if len(packet) < 20+int(binary.BigEndian.Uint16(packet[2:4])) {
return nil, os.ErrInvalid return os.ErrInvalid
} }
return &adapter.InboundContext{Protocol: C.ProtocolSTUN}, nil metadata.Protocol = C.ProtocolSTUN
return nil
} }

View File

@@ -5,6 +5,7 @@ import (
"encoding/hex" "encoding/hex"
"testing" "testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff" "github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
@@ -15,14 +16,16 @@ func TestSniffSTUN(t *testing.T) {
t.Parallel() t.Parallel()
packet, err := hex.DecodeString("000100002112a44224b1a025d0c180c484341306") packet, err := hex.DecodeString("000100002112a44224b1a025d0c180c484341306")
require.NoError(t, err) require.NoError(t, err)
metadata, err := sniff.STUNMessage(context.Background(), packet) var metadata adapter.InboundContext
err = sniff.STUNMessage(context.Background(), &metadata, packet)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolSTUN) require.Equal(t, metadata.Protocol, C.ProtocolSTUN)
} }
func FuzzSniffSTUN(f *testing.F) { func FuzzSniffSTUN(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) { f.Fuzz(func(t *testing.T, data []byte) {
if _, err := sniff.STUNMessage(context.Background(), data); err == nil { var metadata adapter.InboundContext
if err := sniff.STUNMessage(context.Background(), &metadata, data); err == nil {
t.Fail() t.Fail()
} }
}) })

View File

@@ -10,7 +10,7 @@ import (
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
) )
func TLSClientHello(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) { func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
var clientHello *tls.ClientHelloInfo var clientHello *tls.ClientHelloInfo
err := tls.Server(bufio.NewReadOnlyConn(reader), &tls.Config{ err := tls.Server(bufio.NewReadOnlyConn(reader), &tls.Config{
GetConfigForClient: func(argHello *tls.ClientHelloInfo) (*tls.Config, error) { GetConfigForClient: func(argHello *tls.ClientHelloInfo) (*tls.Config, error) {
@@ -19,7 +19,9 @@ func TLSClientHello(ctx context.Context, reader io.Reader) (*adapter.InboundCont
}, },
}).HandshakeContext(ctx) }).HandshakeContext(ctx)
if clientHello != nil { if clientHello != nil {
return &adapter.InboundContext{Protocol: C.ProtocolTLS, Domain: clientHello.ServerName}, nil metadata.Protocol = C.ProtocolTLS
metadata.Domain = clientHello.ServerName
return nil
} }
return nil, err return err
} }

View File

@@ -1,6 +1,7 @@
package srs package srs
import ( import (
"bufio"
"compress/zlib" "compress/zlib"
"encoding/binary" "encoding/binary"
"io" "io"
@@ -11,7 +12,7 @@ import (
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/domain" "github.com/sagernet/sing/common/domain"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/varbin"
"go4.org/netipx" "go4.org/netipx"
) )
@@ -35,38 +36,42 @@ const (
ruleItemPackageName ruleItemPackageName
ruleItemWIFISSID ruleItemWIFISSID
ruleItemWIFIBSSID ruleItemWIFIBSSID
ruleItemAdGuardDomain
ruleItemProcessPathRegex
ruleItemFinal uint8 = 0xFF ruleItemFinal uint8 = 0xFF
) )
func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) { func Read(reader io.Reader, recover bool) (ruleSetCompat option.PlainRuleSetCompat, err error) {
var magicBytes [3]byte var magicBytes [3]byte
_, err = io.ReadFull(reader, magicBytes[:]) _, err = io.ReadFull(reader, magicBytes[:])
if err != nil { if err != nil {
return return
} }
if magicBytes != MagicBytes { if magicBytes != MagicBytes {
err = E.New("invalid sing-box rule set file") err = E.New("invalid sing-box rule-set file")
return return
} }
var version uint8 var version uint8
err = binary.Read(reader, binary.BigEndian, &version) err = binary.Read(reader, binary.BigEndian, &version)
if err != nil { if err != nil {
return ruleSet, err return ruleSetCompat, err
} }
if version != 1 { if version > C.RuleSetVersionCurrent {
return ruleSet, E.New("unsupported version: ", version) return ruleSetCompat, E.New("unsupported version: ", version)
} }
zReader, err := zlib.NewReader(reader) compressReader, err := zlib.NewReader(reader)
if err != nil { if err != nil {
return return
} }
length, err := rw.ReadUVariant(zReader) bReader := bufio.NewReader(compressReader)
length, err := binary.ReadUvarint(bReader)
if err != nil { if err != nil {
return return
} }
ruleSet.Rules = make([]option.HeadlessRule, length) ruleSetCompat.Version = version
ruleSetCompat.Options.Rules = make([]option.HeadlessRule, length)
for i := uint64(0); i < length; i++ { for i := uint64(0); i < length; i++ {
ruleSet.Rules[i], err = readRule(zReader, recovery) ruleSetCompat.Options.Rules[i], err = readRule(bReader, recover)
if err != nil { if err != nil {
err = E.Cause(err, "read rule[", i, "]") err = E.Cause(err, "read rule[", i, "]")
return return
@@ -75,33 +80,38 @@ func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err err
return return
} }
func Write(writer io.Writer, ruleSet option.PlainRuleSet) error { func Write(writer io.Writer, ruleSet option.PlainRuleSet, generateVersion uint8) error {
_, err := writer.Write(MagicBytes[:]) _, err := writer.Write(MagicBytes[:])
if err != nil { if err != nil {
return err return err
} }
err = binary.Write(writer, binary.BigEndian, uint8(1)) err = binary.Write(writer, binary.BigEndian, generateVersion)
if err != nil { if err != nil {
return err return err
} }
zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression) compressWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression)
if err != nil { if err != nil {
return err return err
} }
err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules))) bWriter := bufio.NewWriter(compressWriter)
_, err = varbin.WriteUvarint(bWriter, uint64(len(ruleSet.Rules)))
if err != nil { if err != nil {
return err return err
} }
for _, rule := range ruleSet.Rules { for _, rule := range ruleSet.Rules {
err = writeRule(zWriter, rule) err = writeRule(bWriter, rule, generateVersion)
if err != nil { if err != nil {
return err return err
} }
} }
return zWriter.Close() err = bWriter.Flush()
if err != nil {
return err
}
return compressWriter.Close()
} }
func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) { func readRule(reader varbin.Reader, recover bool) (rule option.HeadlessRule, err error) {
var ruleType uint8 var ruleType uint8
err = binary.Read(reader, binary.BigEndian, &ruleType) err = binary.Read(reader, binary.BigEndian, &ruleType)
if err != nil { if err != nil {
@@ -110,28 +120,28 @@ func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err er
switch ruleType { switch ruleType {
case 0: case 0:
rule.Type = C.RuleTypeDefault rule.Type = C.RuleTypeDefault
rule.DefaultOptions, err = readDefaultRule(reader, recovery) rule.DefaultOptions, err = readDefaultRule(reader, recover)
case 1: case 1:
rule.Type = C.RuleTypeLogical rule.Type = C.RuleTypeLogical
rule.LogicalOptions, err = readLogicalRule(reader, recovery) rule.LogicalOptions, err = readLogicalRule(reader, recover)
default: default:
err = E.New("unknown rule type: ", ruleType) err = E.New("unknown rule type: ", ruleType)
} }
return return
} }
func writeRule(writer io.Writer, rule option.HeadlessRule) error { func writeRule(writer varbin.Writer, rule option.HeadlessRule, generateVersion uint8) error {
switch rule.Type { switch rule.Type {
case C.RuleTypeDefault: case C.RuleTypeDefault:
return writeDefaultRule(writer, rule.DefaultOptions) return writeDefaultRule(writer, rule.DefaultOptions, generateVersion)
case C.RuleTypeLogical: case C.RuleTypeLogical:
return writeLogicalRule(writer, rule.LogicalOptions) return writeLogicalRule(writer, rule.LogicalOptions, generateVersion)
default: default:
panic("unknown rule type: " + rule.Type) panic("unknown rule type: " + rule.Type)
} }
} }
func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) { func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHeadlessRule, err error) {
var lastItemType uint8 var lastItemType uint8
for { for {
var itemType uint8 var itemType uint8
@@ -158,6 +168,9 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
return return
} }
rule.DomainMatcher = matcher rule.DomainMatcher = matcher
if recover {
rule.Domain, rule.DomainSuffix = matcher.Dump()
}
case ruleItemDomainKeyword: case ruleItemDomainKeyword:
rule.DomainKeyword, err = readRuleItemString(reader) rule.DomainKeyword, err = readRuleItemString(reader)
case ruleItemDomainRegex: case ruleItemDomainRegex:
@@ -167,7 +180,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
if err != nil { if err != nil {
return return
} }
if recovery { if recover {
rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String) rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String)
} }
case ruleItemIPCIDR: case ruleItemIPCIDR:
@@ -175,7 +188,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
if err != nil { if err != nil {
return return
} }
if recovery { if recover {
rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String) rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String)
} }
case ruleItemSourcePort: case ruleItemSourcePort:
@@ -190,12 +203,25 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
rule.ProcessName, err = readRuleItemString(reader) rule.ProcessName, err = readRuleItemString(reader)
case ruleItemProcessPath: case ruleItemProcessPath:
rule.ProcessPath, err = readRuleItemString(reader) rule.ProcessPath, err = readRuleItemString(reader)
case ruleItemProcessPathRegex:
rule.ProcessPathRegex, err = readRuleItemString(reader)
case ruleItemPackageName: case ruleItemPackageName:
rule.PackageName, err = readRuleItemString(reader) rule.PackageName, err = readRuleItemString(reader)
case ruleItemWIFISSID: case ruleItemWIFISSID:
rule.WIFISSID, err = readRuleItemString(reader) rule.WIFISSID, err = readRuleItemString(reader)
case ruleItemWIFIBSSID: case ruleItemWIFIBSSID:
rule.WIFIBSSID, err = readRuleItemString(reader) rule.WIFIBSSID, err = readRuleItemString(reader)
case ruleItemAdGuardDomain:
if recover {
err = E.New("unable to decompile binary AdGuard rules to rule-set")
return
}
var matcher *domain.AdGuardMatcher
matcher, err = domain.ReadAdGuardMatcher(reader)
if err != nil {
return
}
rule.AdGuardDomainMatcher = matcher
case ruleItemFinal: case ruleItemFinal:
err = binary.Read(reader, binary.BigEndian, &rule.Invert) err = binary.Read(reader, binary.BigEndian, &rule.Invert)
return return
@@ -209,7 +235,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
} }
} }
func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error { func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, generateVersion uint8) error {
err := binary.Write(writer, binary.BigEndian, uint8(0)) err := binary.Write(writer, binary.BigEndian, uint8(0))
if err != nil { if err != nil {
return err return err
@@ -233,7 +259,7 @@ func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
if err != nil { if err != nil {
return err return err
} }
err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer) err = domain.NewMatcher(rule.Domain, rule.DomainSuffix, generateVersion == C.RuleSetVersion1).Write(writer)
if err != nil { if err != nil {
return err return err
} }
@@ -298,6 +324,12 @@ func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
return err return err
} }
} }
if len(rule.ProcessPathRegex) > 0 {
err = writeRuleItemString(writer, ruleItemProcessPathRegex, rule.ProcessPathRegex)
if err != nil {
return err
}
}
if len(rule.PackageName) > 0 { if len(rule.PackageName) > 0 {
err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName) err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName)
if err != nil { if err != nil {
@@ -316,6 +348,19 @@ func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
return err return err
} }
} }
if len(rule.AdGuardDomain) > 0 {
if generateVersion < C.RuleSetVersion2 {
return E.New("AdGuard rule items is only supported in version 2 or later")
}
err = binary.Write(writer, binary.BigEndian, ruleItemAdGuardDomain)
if err != nil {
return err
}
err = domain.NewAdGuardMatcher(rule.AdGuardDomain).Write(writer)
if err != nil {
return err
}
}
err = binary.Write(writer, binary.BigEndian, ruleItemFinal) err = binary.Write(writer, binary.BigEndian, ruleItemFinal)
if err != nil { if err != nil {
return err return err
@@ -327,73 +372,31 @@ func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
return nil return nil
} }
func readRuleItemString(reader io.Reader) ([]string, error) { func readRuleItemString(reader varbin.Reader) ([]string, error) {
length, err := rw.ReadUVariant(reader) return varbin.ReadValue[[]string](reader, binary.BigEndian)
if err != nil {
return nil, err
}
value := make([]string, length)
for i := uint64(0); i < length; i++ {
value[i], err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
}
return value, nil
} }
func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error { func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) error {
err := binary.Write(writer, binary.BigEndian, itemType) err := writer.WriteByte(itemType)
if err != nil { if err != nil {
return err return err
} }
err = rw.WriteUVariant(writer, uint64(len(value))) return varbin.Write(writer, binary.BigEndian, value)
}
func readRuleItemUint16(reader varbin.Reader) ([]uint16, error) {
return varbin.ReadValue[[]uint16](reader, binary.BigEndian)
}
func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) error {
err := writer.WriteByte(itemType)
if err != nil { if err != nil {
return err return err
} }
for _, item := range value { return varbin.Write(writer, binary.BigEndian, value)
err = rw.WriteVString(writer, item)
if err != nil {
return err
}
}
return nil
} }
func readRuleItemUint16(reader io.Reader) ([]uint16, error) { func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []string) error {
length, err := rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
value := make([]uint16, length)
for i := uint64(0); i < length; i++ {
err = binary.Read(reader, binary.BigEndian, &value[i])
if err != nil {
return nil, err
}
}
return value, nil
}
func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error {
err := binary.Write(writer, binary.BigEndian, itemType)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(value)))
if err != nil {
return err
}
for _, item := range value {
err = binary.Write(writer, binary.BigEndian, item)
if err != nil {
return err
}
}
return nil
}
func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error {
var builder netipx.IPSetBuilder var builder netipx.IPSetBuilder
for i, prefixString := range value { for i, prefixString := range value {
prefix, err := netip.ParsePrefix(prefixString) prefix, err := netip.ParsePrefix(prefixString)
@@ -419,9 +422,8 @@ func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error {
return writeIPSet(writer, ipSet) return writeIPSet(writer, ipSet)
} }
func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) { func readLogicalRule(reader varbin.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) {
var mode uint8 mode, err := reader.ReadByte()
err = binary.Read(reader, binary.BigEndian, &mode)
if err != nil { if err != nil {
return return
} }
@@ -434,7 +436,7 @@ func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.Logica
err = E.New("unknown logical mode: ", mode) err = E.New("unknown logical mode: ", mode)
return return
} }
length, err := rw.ReadUVariant(reader) length, err := binary.ReadUvarint(reader)
if err != nil { if err != nil {
return return
} }
@@ -453,7 +455,7 @@ func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.Logica
return return
} }
func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error { func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule, generateVersion uint8) error {
err := binary.Write(writer, binary.BigEndian, uint8(1)) err := binary.Write(writer, binary.BigEndian, uint8(1))
if err != nil { if err != nil {
return err return err
@@ -469,12 +471,12 @@ func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule)
if err != nil { if err != nil {
return err return err
} }
err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules))) _, err = varbin.WriteUvarint(writer, uint64(len(logicalRule.Rules)))
if err != nil { if err != nil {
return err return err
} }
for _, rule := range logicalRule.Rules { for _, rule := range logicalRule.Rules {
err = writeRule(writer, rule) err = writeRule(writer, rule, generateVersion)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -2,11 +2,13 @@ package srs
import ( import (
"encoding/binary" "encoding/binary"
"io"
"net/netip" "net/netip"
"os"
"unsafe" "unsafe"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/varbin"
"go4.org/netipx" "go4.org/netipx"
) )
@@ -20,94 +22,57 @@ type myIPRange struct {
to netip.Addr to netip.Addr
} }
func readIPSet(reader io.Reader) (*netipx.IPSet, error) { type myIPRangeData struct {
var version uint8 From []byte
err := binary.Read(reader, binary.BigEndian, &version) To []byte
}
func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
version, err := reader.ReadByte()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if version != 1 {
return nil, os.ErrInvalid
}
// WTF why using uint64 here
var length uint64 var length uint64
err = binary.Read(reader, binary.BigEndian, &length) err = binary.Read(reader, binary.BigEndian, &length)
if err != nil { if err != nil {
return nil, err return nil, err
} }
mySet := &myIPSet{ ranges := make([]myIPRangeData, length)
rr: make([]myIPRange, length), err = varbin.Read(reader, binary.BigEndian, &ranges)
if err != nil {
return nil, err
} }
for i := uint64(0); i < length; i++ { mySet := &myIPSet{
var ( rr: make([]myIPRange, len(ranges)),
fromLen uint64 }
toLen uint64 for i, rangeData := range ranges {
fromAddr netip.Addr mySet.rr[i].from = M.AddrFromIP(rangeData.From)
toAddr netip.Addr mySet.rr[i].to = M.AddrFromIP(rangeData.To)
)
fromLen, err = rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
fromBytes := make([]byte, fromLen)
_, err = io.ReadFull(reader, fromBytes)
if err != nil {
return nil, err
}
err = fromAddr.UnmarshalBinary(fromBytes)
if err != nil {
return nil, err
}
toLen, err = rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
toBytes := make([]byte, toLen)
_, err = io.ReadFull(reader, toBytes)
if err != nil {
return nil, err
}
err = toAddr.UnmarshalBinary(toBytes)
if err != nil {
return nil, err
}
mySet.rr[i] = myIPRange{fromAddr, toAddr}
} }
return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil
} }
func writeIPSet(writer io.Writer, set *netipx.IPSet) error { func writeIPSet(writer varbin.Writer, set *netipx.IPSet) error {
err := binary.Write(writer, binary.BigEndian, uint8(1)) err := writer.WriteByte(1)
if err != nil { if err != nil {
return err return err
} }
mySet := (*myIPSet)(unsafe.Pointer(set)) dataList := common.Map((*myIPSet)(unsafe.Pointer(set)).rr, func(rr myIPRange) myIPRangeData {
err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr))) return myIPRangeData{
From: rr.from.AsSlice(),
To: rr.to.AsSlice(),
}
})
err = binary.Write(writer, binary.BigEndian, uint64(len(dataList)))
if err != nil { if err != nil {
return err return err
} }
for _, rr := range mySet.rr { for _, data := range dataList {
var ( err = varbin.Write(writer, binary.BigEndian, data)
fromBinary []byte
toBinary []byte
)
fromBinary, err = rr.from.MarshalBinary()
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(fromBinary)))
if err != nil {
return err
}
_, err = writer.Write(fromBinary)
if err != nil {
return err
}
toBinary, err = rr.to.MarshalBinary()
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(toBinary)))
if err != nil {
return err
}
_, err = writer.Write(toBinary)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -27,11 +27,10 @@ func (c *echClientConfig) DialEarly(ctx context.Context, conn net.PacketConn, ad
return quic.DialEarly(ctx, conn, addr, c.config, config) return quic.DialEarly(ctx, conn, addr, c.config, config)
} }
func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config, enableDatagrams bool) http.RoundTripper { func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config) http.RoundTripper {
return &http3.RoundTripper{ return &http3.RoundTripper{
TLSClientConfig: c.config, TLSClientConfig: c.config,
QuicConfig: quicConfig, QUICConfig: quicConfig,
EnableDatagrams: enableDatagrams,
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
quicConn, err := quic.DialEarly(ctx, conn, serverAddr.UDPAddr(), tlsCfg, cfg) quicConn, err := quic.DialEarly(ctx, conn, serverAddr.UDPAddr(), tlsCfg, cfg)
if err != nil { if err != nil {

View File

@@ -11,12 +11,11 @@ import (
"strings" "strings"
cftls "github.com/sagernet/cloudflare-tls" cftls "github.com/sagernet/cloudflare-tls"
"github.com/sagernet/fswatch"
"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" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
"github.com/fsnotify/fsnotify"
) )
type echServerConfig struct { type echServerConfig struct {
@@ -26,9 +25,8 @@ type echServerConfig struct {
key []byte key []byte
certificatePath string certificatePath string
keyPath string keyPath string
watcher *fsnotify.Watcher
echKeyPath string echKeyPath string
echWatcher *fsnotify.Watcher watcher *fswatch.Watcher
} }
func (c *echServerConfig) ServerName() string { func (c *echServerConfig) ServerName() string {
@@ -66,146 +64,88 @@ func (c *echServerConfig) Clone() Config {
} }
func (c *echServerConfig) Start() error { func (c *echServerConfig) Start() error {
if c.certificatePath != "" && c.keyPath != "" { err := c.startWatcher()
err := c.startWatcher() if err != nil {
if err != nil { c.logger.Warn("create credentials watcher: ", err)
c.logger.Warn("create fsnotify watcher: ", err)
}
}
if c.echKeyPath != "" {
err := c.startECHWatcher()
if err != nil {
c.logger.Warn("create fsnotify watcher: ", err)
}
} }
return nil return nil
} }
func (c *echServerConfig) startWatcher() error { func (c *echServerConfig) startWatcher() error {
watcher, err := fsnotify.NewWatcher() var watchPath []string
if c.certificatePath != "" {
watchPath = append(watchPath, c.certificatePath)
}
if c.keyPath != "" {
watchPath = append(watchPath, c.keyPath)
}
if c.echKeyPath != "" {
watchPath = append(watchPath, c.echKeyPath)
}
if len(watchPath) == 0 {
return nil
}
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath,
Callback: func(path string) {
err := c.credentialsUpdated(path)
if err != nil {
c.logger.Error(E.Cause(err, "reload credentials from ", path))
}
},
})
if err != nil { if err != nil {
return err return err
} }
if c.certificatePath != "" { err = watcher.Start()
err = watcher.Add(c.certificatePath) if err != nil {
if err != nil { return err
return err
}
}
if c.keyPath != "" {
err = watcher.Add(c.keyPath)
if err != nil {
return err
}
} }
c.watcher = watcher c.watcher = watcher
go c.loopUpdate()
return nil return nil
} }
func (c *echServerConfig) loopUpdate() { func (c *echServerConfig) credentialsUpdated(path string) error {
for { if path == c.certificatePath || path == c.keyPath {
select { if path == c.certificatePath {
case event, ok := <-c.watcher.Events: certificate, err := os.ReadFile(c.certificatePath)
if !ok {
return
}
if event.Op&fsnotify.Write != fsnotify.Write {
continue
}
err := c.reloadKeyPair()
if err != nil { if err != nil {
c.logger.Error(E.Cause(err, "reload TLS key pair")) return err
} }
case err, ok := <-c.watcher.Errors: c.certificate = certificate
if !ok { } else {
return key, err := os.ReadFile(c.keyPath)
}
c.logger.Error(E.Cause(err, "fsnotify error"))
}
}
}
func (c *echServerConfig) reloadKeyPair() error {
if c.certificatePath != "" {
certificate, err := os.ReadFile(c.certificatePath)
if err != nil {
return E.Cause(err, "reload certificate from ", c.certificatePath)
}
c.certificate = certificate
}
if c.keyPath != "" {
key, err := os.ReadFile(c.keyPath)
if err != nil {
return E.Cause(err, "reload key from ", c.keyPath)
}
c.key = key
}
keyPair, err := cftls.X509KeyPair(c.certificate, c.key)
if err != nil {
return E.Cause(err, "reload key pair")
}
c.config.Certificates = []cftls.Certificate{keyPair}
c.logger.Info("reloaded TLS certificate")
return nil
}
func (c *echServerConfig) startECHWatcher() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
err = watcher.Add(c.echKeyPath)
if err != nil {
return err
}
c.echWatcher = watcher
go c.loopECHUpdate()
return nil
}
func (c *echServerConfig) loopECHUpdate() {
for {
select {
case event, ok := <-c.echWatcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write != fsnotify.Write {
continue
}
err := c.reloadECHKey()
if err != nil { if err != nil {
c.logger.Error(E.Cause(err, "reload ECH key")) return err
} }
case err, ok := <-c.echWatcher.Errors: c.key = key
if !ok {
return
}
c.logger.Error(E.Cause(err, "fsnotify error"))
} }
keyPair, err := cftls.X509KeyPair(c.certificate, c.key)
if err != nil {
return E.Cause(err, "parse key pair")
}
c.config.Certificates = []cftls.Certificate{keyPair}
c.logger.Info("reloaded TLS certificate")
} else {
echKeyContent, err := os.ReadFile(c.echKeyPath)
if err != nil {
return err
}
block, rest := pem.Decode(echKeyContent)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
if err != nil {
return E.Cause(err, "create ECH key set")
}
c.config.ServerECHProvider = echKeySet
c.logger.Info("reloaded ECH keys")
} }
}
func (c *echServerConfig) reloadECHKey() error {
echKeyContent, err := os.ReadFile(c.echKeyPath)
if err != nil {
return err
}
block, rest := pem.Decode(echKeyContent)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
if err != nil {
return E.Cause(err, "create ECH key set")
}
c.config.ServerECHProvider = echKeySet
c.logger.Info("reloaded ECH keys")
return nil return nil
} }
@@ -213,12 +153,7 @@ func (c *echServerConfig) Close() error {
var err error var err error
if c.watcher != nil { if c.watcher != nil {
err = E.Append(err, c.watcher.Close(), func(err error) error { err = E.Append(err, c.watcher.Close(), func(err error) error {
return E.Cause(err, "close certificate watcher") return E.Cause(err, "close credentials watcher")
})
}
if c.echWatcher != nil {
err = E.Append(err, c.echWatcher.Close(), func(err error) error {
return E.Cause(err, "close ECH key watcher")
}) })
} }
return err return err
@@ -301,7 +236,7 @@ func NewECHServer(ctx context.Context, logger log.Logger, options option.Inbound
var echKey []byte var echKey []byte
if len(options.ECH.Key) > 0 { if len(options.ECH.Key) > 0 {
echKey = []byte(strings.Join(options.ECH.Key, "\n")) echKey = []byte(strings.Join(options.ECH.Key, "\n"))
} else if options.KeyPath != "" { } else if options.ECH.KeyPath != "" {
content, err := os.ReadFile(options.ECH.KeyPath) content, err := os.ReadFile(options.ECH.KeyPath)
if err != nil { if err != nil {
return nil, E.Cause(err, "read ECH key") return nil, E.Cause(err, "read ECH key")

View File

@@ -7,14 +7,13 @@ import (
"os" "os"
"strings" "strings"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
"github.com/fsnotify/fsnotify"
) )
var errInsecureUnused = E.New("tls: insecure unused") var errInsecureUnused = E.New("tls: insecure unused")
@@ -27,7 +26,7 @@ type STDServerConfig struct {
key []byte key []byte
certificatePath string certificatePath string
keyPath string keyPath string
watcher *fsnotify.Watcher watcher *fswatch.Watcher
} }
func (c *STDServerConfig) ServerName() string { func (c *STDServerConfig) ServerName() string {
@@ -88,59 +87,41 @@ func (c *STDServerConfig) Start() error {
} }
func (c *STDServerConfig) startWatcher() error { func (c *STDServerConfig) startWatcher() error {
watcher, err := fsnotify.NewWatcher() var watchPath []string
if c.certificatePath != "" {
watchPath = append(watchPath, c.certificatePath)
}
if c.keyPath != "" {
watchPath = append(watchPath, c.keyPath)
}
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath,
Callback: func(path string) {
err := c.certificateUpdated(path)
if err != nil {
c.logger.Error(err)
}
},
})
if err != nil { if err != nil {
return err return err
} }
if c.certificatePath != "" { err = watcher.Start()
err = watcher.Add(c.certificatePath) if err != nil {
if err != nil { return err
return err
}
}
if c.keyPath != "" {
err = watcher.Add(c.keyPath)
if err != nil {
return err
}
} }
c.watcher = watcher c.watcher = watcher
go c.loopUpdate()
return nil return nil
} }
func (c *STDServerConfig) loopUpdate() { func (c *STDServerConfig) certificateUpdated(path string) error {
for { if path == c.certificatePath {
select {
case event, ok := <-c.watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write != fsnotify.Write {
continue
}
err := c.reloadKeyPair()
if err != nil {
c.logger.Error(E.Cause(err, "reload TLS key pair"))
}
case err, ok := <-c.watcher.Errors:
if !ok {
return
}
c.logger.Error(E.Cause(err, "fsnotify error"))
}
}
}
func (c *STDServerConfig) reloadKeyPair() error {
if c.certificatePath != "" {
certificate, err := os.ReadFile(c.certificatePath) certificate, err := os.ReadFile(c.certificatePath)
if err != nil { if err != nil {
return E.Cause(err, "reload certificate from ", c.certificatePath) return E.Cause(err, "reload certificate from ", c.certificatePath)
} }
c.certificate = certificate c.certificate = certificate
} } else if path == c.keyPath {
if c.keyPath != "" {
key, err := os.ReadFile(c.keyPath) key, err := os.ReadFile(c.keyPath)
if err != nil { if err != nil {
return E.Cause(err, "reload key from ", c.keyPath) return E.Cause(err, "reload key from ", c.keyPath)

View File

@@ -217,18 +217,10 @@ 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":
fallthrough
case "chrome", "": case "chrome", "":
return utls.HelloChrome_Auto, nil return utls.HelloChrome_Auto, nil
case "chrome_psk":
return utls.HelloChrome_100_PSK, nil
case "chrome_psk_shuffle":
return utls.HelloChrome_112_PSK_Shuf, nil
case "chrome_padding_psk_shuffle":
return utls.HelloChrome_114_Padding_PSK_Shuf, nil
case "chrome_pq":
return utls.HelloChrome_115_PQ, nil
case "chrome_pq_psk":
return utls.HelloChrome_115_PQ_PSK, nil
case "firefox": case "firefox":
return utls.HelloFirefox_Auto, nil return utls.HelloFirefox_Auto, nil
case "edge": case "edge":

View File

@@ -8,6 +8,7 @@ import (
"sync" "sync"
"time" "time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
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"
@@ -113,6 +114,7 @@ func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err e
CheckRedirect: func(req *http.Request, via []*http.Request) error { CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse return http.ErrUseLastResponse
}, },
Timeout: C.TCPTimeout,
} }
defer client.CloseIdleConnections() defer client.CloseIdleConnections()
resp, err := client.Do(req.WithContext(ctx)) resp, err := client.Do(req.WithContext(ctx))

View File

@@ -13,14 +13,14 @@ var resourcePaths []string
func FindPath(name string) (string, bool) { func FindPath(name string) (string, bool) {
name = os.ExpandEnv(name) name = os.ExpandEnv(name)
if rw.FileExists(name) { if rw.IsFile(name) {
return name, true return name, true
} }
for _, dir := range resourcePaths { for _, dir := range resourcePaths {
if path := filepath.Join(dir, dirName, name); rw.FileExists(path) { if path := filepath.Join(dir, dirName, name); rw.IsFile(path) {
return path, true return path, true
} }
if path := filepath.Join(dir, name); rw.FileExists(path) { if path := filepath.Join(dir, name); rw.IsFile(path) {
return path, true return path, true
} }
} }

View File

@@ -1,9 +1,21 @@
package constant package constant
const ( const (
ProtocolTLS = "tls" ProtocolTLS = "tls"
ProtocolHTTP = "http" ProtocolHTTP = "http"
ProtocolQUIC = "quic" ProtocolQUIC = "quic"
ProtocolDNS = "dns" ProtocolDNS = "dns"
ProtocolSTUN = "stun" ProtocolSTUN = "stun"
ProtocolBitTorrent = "bittorrent"
ProtocolDTLS = "dtls"
ProtocolSSH = "ssh"
ProtocolRDP = "rdp"
)
const (
ClientChromium = "chromium"
ClientSafari = "safari"
ClientFirefox = "firefox"
ClientQUICGo = "quic-go"
ClientUnknown = "unknown"
) )

View File

@@ -32,6 +32,12 @@ const (
func ProxyDisplayName(proxyType string) string { func ProxyDisplayName(proxyType string) string {
switch proxyType { switch proxyType {
case TypeTun:
return "TUN"
case TypeRedirect:
return "Redirect"
case TypeTProxy:
return "TProxy"
case TypeDirect: case TypeDirect:
return "Direct" return "Direct"
case TypeBlock: case TypeBlock:
@@ -42,6 +48,8 @@ func ProxyDisplayName(proxyType string) string {
return "SOCKS" return "SOCKS"
case TypeHTTP: case TypeHTTP:
return "HTTP" return "HTTP"
case TypeMixed:
return "Mixed"
case TypeShadowsocks: case TypeShadowsocks:
return "Shadowsocks" return "Shadowsocks"
case TypeVMess: case TypeVMess:

5
constant/quic.go Normal file
View File

@@ -0,0 +1,5 @@
//go:build with_quic
package constant
const WithQUIC = true

5
constant/quic_stub.go Normal file
View File

@@ -0,0 +1,5 @@
//go:build !with_quic
package constant
const WithQUIC = false

View File

@@ -11,9 +11,15 @@ const (
) )
const ( const (
RuleSetTypeInline = "inline"
RuleSetTypeLocal = "local" RuleSetTypeLocal = "local"
RuleSetTypeRemote = "remote" RuleSetTypeRemote = "remote"
RuleSetVersion1 = 1
RuleSetFormatSource = "source" RuleSetFormatSource = "source"
RuleSetFormatBinary = "binary" RuleSetFormatBinary = "binary"
) )
const (
RuleSetVersion1 = 1 + iota
RuleSetVersion2
RuleSetVersionCurrent = RuleSetVersion2
)

View File

@@ -3,15 +3,19 @@ package constant
import "time" import "time"
const ( const (
TCPTimeout = 5 * time.Second TCPKeepAliveInitial = 10 * time.Minute
ReadPayloadTimeout = 300 * time.Millisecond TCPKeepAliveInterval = 75 * time.Second
DNSTimeout = 10 * time.Second TCPConnectTimeout = 5 * time.Second
QUICTimeout = 30 * time.Second TCPTimeout = 15 * time.Second
STUNTimeout = 15 * time.Second ReadPayloadTimeout = 300 * time.Millisecond
UDPTimeout = 5 * time.Minute DNSTimeout = 10 * time.Second
DefaultURLTestInterval = 3 * time.Minute QUICTimeout = 30 * time.Second
DefaultURLTestIdleTimeout = 30 * time.Minute STUNTimeout = 15 * time.Second
DefaultStartTimeout = 10 * time.Second UDPTimeout = 5 * time.Minute
DefaultStopTimeout = 5 * time.Second DefaultURLTestInterval = 3 * time.Minute
DefaultStopFatalTimeout = 10 * time.Second DefaultURLTestIdleTimeout = 30 * time.Minute
StartTimeout = 10 * time.Second
StopTimeout = 5 * time.Second
FatalStopTimeout = 10 * time.Second
FakeIPMetadataSaveInterval = 10 * time.Second
) )

View File

@@ -1,5 +1,3 @@
//go:build go1.19
package box package box
import ( import (

View File

@@ -1,36 +0,0 @@
//go:build !go1.19
package box
import (
"runtime/debug"
"github.com/sagernet/sing-box/common/conntrack"
"github.com/sagernet/sing-box/option"
)
func applyDebugOptions(options option.DebugOptions) {
applyDebugListenOption(options)
if options.GCPercent != nil {
debug.SetGCPercent(*options.GCPercent)
}
if options.MaxStack != nil {
debug.SetMaxStack(*options.MaxStack)
}
if options.MaxThreads != nil {
debug.SetMaxThreads(*options.MaxThreads)
}
if options.PanicOnFault != nil {
debug.SetPanicOnFault(*options.PanicOnFault)
}
if options.TraceBack != "" {
debug.SetTraceback(options.TraceBack)
}
if options.MemoryLimit != 0 {
// debug.SetMemoryLimit(int64(options.MemoryLimit))
conntrack.MemoryLimit = uint64(options.MemoryLimit)
}
if options.OOMKiller != nil {
conntrack.KillerEnabled = *options.OOMKiller
}
}

View File

@@ -46,7 +46,7 @@ func applyDebugListenOption(options option.DebugOptions) {
encoder := json.NewEncoder(writer) encoder := json.NewEncoder(writer)
encoder.SetIndent("", " ") encoder.SetIndent("", " ")
encoder.Encode(memObject) encoder.Encode(&memObject)
}) })
r.Route("/pprof", func(r chi.Router) { r.Route("/pprof", func(r chi.Router) {
r.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { r.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {

View File

@@ -2,10 +2,578 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
### 1.10.2
* Add deprecated warnings
* Fix proxying websocket connections in HTTP/mixed inbounds
* Fixes and improvements
### 1.10.1
* Fixes and improvements
### 1.10.0
Important changes since 1.9:
* Introducing auto-redirect **1**
* Add AdGuard DNS Filter support **2**
* TUN address fields are merged **3**
* Add custom options for `auto-route` and `auto-redirect` **4**
* Drop support for go1.18 and go1.19 **5**
* Add tailing comma support in JSON configuration
* Improve sniffers **6**
* Add new `inline` rule-set type **7**
* Add access control options for Clash API **8**
* Add `rule_set_ip_cidr_accept_empty` DNS address filter rule item **9**
* Add auto reload support for local rule-set
* Update fsnotify usages **10**
* Add IP address support for `rule-set match` command
* Add `rule-set decompile` command
* Add `process_path_regex` rule item
* Update uTLS to v1.6.7 **11**
* Optimize memory usages of rule-sets **12**
**1**:
The new auto-redirect feature allows TUN to automatically
configure connection redirection to improve proxy performance.
When auto-redirect is enabled, new route address set options will allow you to
automatically configure destination IP CIDR rules from a specified rule set to the firewall.
Specified or unspecified destinations will bypass the sing-box routes to get better performance
(for example, keep hardware offloading of direct traffics on the router).
See [TUN](/configuration/inbound/tun).
**2**:
The new feature allows you to use AdGuard DNS Filter lists in a sing-box without AdGuard Home.
See [AdGuard DNS Filter](/configuration/rule-set/adguard/).
**3**:
See [Migration](/migration/#tun-address-fields-are-merged).
**4**:
See [iproute2_table_index](/configuration/inbound/tun/#iproute2_table_index),
[iproute2_rule_index](/configuration/inbound/tun/#iproute2_rule_index),
[auto_redirect_input_mark](/configuration/inbound/tun/#auto_redirect_input_mark) and
[auto_redirect_output_mark](/configuration/inbound/tun/#auto_redirect_output_mark).
**5**:
Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile.
**6**:
BitTorrent, DTLS, RDP, SSH sniffers are added.
Now the QUIC sniffer can correctly extract the server name from Chromium requests and
can identify common QUIC clients, including
Chromium, Safari, Firefox, quic-go (including uquic disguised as Chrome).
**7**:
The new [rule-set](/configuration/rule-set/) type inline (which also becomes the default type)
allows you to write headless rules directly without creating a rule-set file.
**8**:
With the new access control options, not only can you allow Clash dashboards
to access the Clash API on your local network,
you can also manually limit the websites that can access the API instead of allowing everyone.
See [Clash API](/configuration/experimental/clash-api/).
**9**:
See [DNS Rule](/configuration/dns/rule/#rule_set_ip_cidr_accept_empty).
**10**:
sing-box now uses fsnotify correctly and will not cancel watching
if the target file is deleted or recreated via rename (e.g. `mv`).
This affects all path options that support reload, including
`tls.certificate_path`, `tls.key_path`, `tls.ech.key_path` and `rule_set.path`.
**11**:
Some legacy chrome fingerprints have been removed and will fallback to chrome,
see [utls](/configuration/shared/tls#utls).
**12**:
See [Source Format](/configuration/rule-set/source-format/#version).
### 1.9.7
* Fixes and improvements
#### 1.10.0-beta.11
* Update uTLS to v1.6.7 **1**
**1**:
Some legacy chrome fingerprints have been removed and will fallback to chrome,
see [utls](/configuration/shared/tls#utls).
#### 1.10.0-beta.10
* Add `process_path_regex` rule item
* Fixes and improvements
_The macOS standalone versions of sing-box (>=1.9.5/<1.10.0-beta.11) now silently fail and require manual granting of
the **Full Disk Access** permission to system extension to start, probably due to Apple's changed security policy. We
will prompt users about this in feature versions._
### 1.9.6
* Fixes and improvements
### 1.9.5
* Update quic-go to v0.47.0
* Fix direct dialer not resolving domain
* Fix no error return when empty DNS cache retrieved
* Fix build with go1.23
* Fix stream sniffer
* Fix bad redirect in clash-api
* Fix wireguard events chan leak
* Fix cached conn eats up read deadlines
* Fix disconnected interface selected as default in windows
* Update Bundle Identifiers for Apple platform clients **1**
**1**:
See [Migration](/migration/#bundle-identifier-updates-in-apple-platform-clients).
We are still working on getting all sing-box apps back on the App Store, which should be completed within a week
(SFI on the App Store and others on TestFlight are already available).
#### 1.10.0-beta.8
* Fixes and improvements
_With the help of a netizen, we are in the process of getting sing-box apps back on the App Store, which should be
completed within a month (TestFlight is already available)._
#### 1.10.0-beta.7
* Update quic-go to v0.47.0
* Fixes and improvements
#### 1.10.0-beta.6
* Add RDP sniffer
* Fixes and improvements
#### 1.10.0-beta.5
* Add PNA support for [Clash API](/configuration/experimental/clash-api/)
* Fixes and improvements
#### 1.10.0-beta.3
* Add SSH sniffer
* Fixes and improvements
#### 1.10.0-beta.2
* Build with go1.23
* Fixes and improvements
### 1.9.4
* Update quic-go to v0.46.0
* Update Hysteria2 BBR congestion control
* Filter HTTPS ipv4hint/ipv6hint with domain strategy
* Fix crash on Android when using process rules
* Fix non-IP queries accepted by address filter rules
* Fix UDP server for shadowsocks AEAD multi-user inbounds
* Fix default next protos for v2ray QUIC transport
* Fix default end value of port range configuration options
* Fix reset v2ray transports
* Fix panic caused by rule-set generation of duplicate keys for `domain_suffix`
* Fix UDP connnection leak when sniffing
* Fixes and improvements
_Due to problems with our Apple developer account,
sing-box apps on Apple platforms are temporarily unavailable for download or update.
If your company or organization is willing to help us return to the App Store,
please [contact us](mailto:contact@sagernet.org)._
#### 1.10.0-alpha.29
* Update quic-go to v0.46.0
* Fixes and improvements
#### 1.10.0-alpha.25
* Add AdGuard DNS Filter support **1**
**1**:
The new feature allows you to use AdGuard DNS Filter lists in a sing-box without AdGuard Home.
See [AdGuard DNS Filter](/configuration/rule-set/adguard/).
#### 1.10.0-alpha.23
* Add Chromium support for QUIC sniffer
* Add client type detect support for QUIC sniffer **1**
* Fixes and improvements
**1**:
Now the QUIC sniffer can correctly extract the server name from Chromium requests and
can identify common QUIC clients, including
Chromium, Safari, Firefox, quic-go (including uquic disguised as Chrome).
See [Protocol Sniff](/configuration/route/sniff/) and [Route Rule](/configuration/route/rule/#client).
#### 1.10.0-alpha.22
* Optimize memory usages of rule-sets **1**
* Fixes and improvements
**1**:
See [Source Format](/configuration/rule-set/source-format/#version).
#### 1.10.0-alpha.20
* Add DTLS sniffer
* Fixes and improvements
#### 1.10.0-alpha.19
* Add `rule-set decompile` command
* Add IP address support for `rule-set match` command
* Fixes and improvements
#### 1.10.0-alpha.18
* Add new `inline` rule-set type **1**
* Add auto reload support for local rule-set
* Update fsnotify usages **2**
* Fixes and improvements
**1**:
The new [rule-set](/configuration/rule-set/) type inline (which also becomes the default type)
allows you to write headless rules directly without creating a rule-set file.
**2**:
sing-box now uses fsnotify correctly and will not cancel watching
if the target file is deleted or recreated via rename (e.g. `mv`).
This affects all path options that support reload, including
`tls.certificate_path`, `tls.key_path`, `tls.ech.key_path` and `rule_set.path`.
#### 1.10.0-alpha.17
* Some chaotic changes **1**
* `rule_set_ipcidr_match_source` rule items are renamed **2**
* Add `rule_set_ip_cidr_accept_empty` DNS address filter rule item **3**
* Update quic-go to v0.45.1
* Fixes and improvements
**1**:
Something may be broken, please actively report problems with this version.
**2**:
`rule_set_ipcidr_match_source` route and DNS rule items are renamed to
`rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0.
**3**:
See [DNS Rule](/configuration/dns/rule/#rule_set_ip_cidr_accept_empty).
#### 1.10.0-alpha.16
* Add custom options for `auto-route` and `auto-redirect` **1**
* Fixes and improvements
**1**:
See [iproute2_table_index](/configuration/inbound/tun/#iproute2_table_index),
[iproute2_rule_index](/configuration/inbound/tun/#iproute2_rule_index),
[auto_redirect_input_mark](/configuration/inbound/tun/#auto_redirect_input_mark) and
[auto_redirect_output_mark](/configuration/inbound/tun/#auto_redirect_output_mark).
#### 1.10.0-alpha.13
* TUN address fields are merged **1**
* Add route address set support for auto-redirect **2**
**1**:
See [Migration](/migration/#tun-address-fields-are-merged).
**2**:
The new feature will allow you to configure the destination IP CIDR rules
in the specified rule-sets to the firewall automatically.
Specified or unspecified destinations will bypass the sing-box routes to get better performance
(for example, keep hardware offloading of direct traffics on the router).
See [route_address_set](/configuration/inbound/tun/#route_address_set)
and [route_exclude_address_set](/configuration/inbound/tun/#route_exclude_address_set).
#### 1.10.0-alpha.12
* Fix auto-redirect not configuring nftables forward chain correctly
* Fixes and improvements
### 1.9.3
* Fixes and improvements
#### 1.10.0-alpha.10
* Fixes and improvements
### 1.9.2
* Fixes and improvements
#### 1.10.0-alpha.8
* Drop support for go1.18 and go1.19 **1**
* Update quic-go to v0.45.0
* Update Hysteria2 BBR congestion control
* Fixes and improvements
**1**:
Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile.
### 1.9.1
* Fixes and improvements
#### 1.10.0-alpha.7
* Fixes and improvements
#### 1.10.0-alpha.5
* Improve auto-redirect **1**
**1**:
nftables support and DNS hijacking has been added.
Tun inbounds with `auto_route` and `auto_redirect` now works as expected on routers **without intervention**.
#### 1.10.0-alpha.4
* Fix auto-redirect **1**
* Improve auto-route on linux **2**
**1**:
Tun inbounds with `auto_route` and `auto_redirect` now works as expected on routers.
**2**:
Tun inbounds with `auto_route` and `strict_route` now works as expected on routers and servers,
but the usages of [exclude_interface](/configuration/inbound/tun/#exclude_interface) need to be updated.
#### 1.10.0-alpha.2
* Move auto-redirect to Tun **1**
* Fixes and improvements
**1**:
Linux support are added.
See [Tun](/configuration/inbound/tun/#auto_redirect).
#### 1.10.0-alpha.1
* Add tailing comma support in JSON configuration
* Add simple auto-redirect for Android **1**
* Add BitTorrent sniffer **2**
**1**:
It allows you to use redirect inbound in the sing-box Android client
and automatically configures IPv4 TCP redirection via su.
This may alleviate the symptoms of some OCD patients who think that
redirect can effectively save power compared to the system HTTP Proxy.
See [Redirect](/configuration/inbound/redirect/).
**2**:
See [Protocol Sniff](/configuration/route/sniff/).
### 1.9.0
* Fixes and improvements
Important changes since 1.8:
* `domain_suffix` behavior update **1**
* `process_path` format update on Windows **2**
* Add address filter DNS rule items **3**
* Add support for `client-subnet` DNS options **4**
* Add rejected DNS response cache support **5**
* Add `bypass_domain` and `search_domain` platform HTTP proxy options **6**
* Fix missing `rule_set_ipcidr_match_source` item in DNS rules **7**
* Handle Windows power events
* Always disable cache for fake-ip DNS transport if `dns.independent_cache` disabled
* Improve DNS truncate behavior
* Update Hysteria protocol
* Update quic-go to v0.43.1
* Update gVisor to 20240422.0
* Mitigating TunnelVision attacks **8**
**1**:
See [Migration](/migration/#domain_suffix-behavior-update).
**2**:
See [Migration](/migration/#process_path-format-update-on-windows).
**3**:
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
if using this method.
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
**4**:
See [DNS](/configuration/dns), [DNS Server](/configuration/dns/server) and [DNS Rules](/configuration/dns/rule).
Since this feature makes the scenario mentioned in `alpha.1` no longer leak DNS requests,
the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) has been updated.
**5**:
The new feature allows you to cache the check results of
[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration.
**6**:
See [TUN](/configuration/inbound/tun) inbound.
**7**:
See [DNS Rule](/configuration/dns/rule/).
**8**:
See [TunnelVision](/manual/misc/tunnelvision).
#### 1.9.0-rc.22
* Fixes and improvements
#### 1.9.0-rc.20
* Prioritize `*_route_address` in linux auto-route
* Fix `*_route_address` in darwin auto-route
#### 1.8.14
* Fix hysteria2 panic
* Fixes and improvements
#### 1.9.0-rc.18
* Add custom prefix support in EDNS0 client subnet options
* Fix hysteria2 crash
* Fix `store_rdrc` corrupted
* Update quic-go to v0.43.1
* Fixes and improvements
#### 1.9.0-rc.16
* Mitigating TunnelVision attacks **1**
* Fixes and improvements
**1**:
See [TunnelVision](/manual/misc/tunnelvision).
#### 1.9.0-rc.15
* Fixes and improvements
#### 1.8.13
* Fix fake-ip mapping
* Fixes and improvements
#### 1.9.0-rc.14
* Fixes and improvements
#### 1.9.0-rc.13
* Update Hysteria protocol
* Update quic-go to v0.43.0
* Update gVisor to 20240422.0
* Fixes and improvements
#### 1.8.12
* Now we have official APT and DNF repositories **1**
* Fix packet MTU for QUIC protocols
* Fixes and improvements
**1**:
Including stable and beta versions, see https://sing-box.sagernet.org/installation/package-manager/
#### 1.9.0-rc.11
* Fixes and improvements
#### 1.8.11
* Fixes and improvements
#### 1.8.10 #### 1.8.10
* Fixes and improvements * Fixes and improvements
#### 1.9.0-beta.17
* Update `quic-go` to v0.42.0
* Fixes and improvements
#### 1.9.0-beta.16
* Fixes and improvements
_Our Testflight distribution has been temporarily blocked by Apple (possibly due to too many beta versions)
and you cannot join the test, install or update the sing-box beta app right now.
Please wait patiently for processing._
#### 1.9.0-beta.14
* Update gVisor to 20240212.0-65-g71212d503
* Fixes and improvements
#### 1.8.9 #### 1.8.9
* Fixes and improvements * Fixes and improvements
@@ -14,14 +582,125 @@ icon: material/alert-decagram
* Fixes and improvements * Fixes and improvements
#### 1.9.0-beta.7
* Fixes and improvements
#### 1.9.0-beta.6
* Fix address filter DNS rule items **1**
* Fix DNS outbound responding with wrong data
* Fixes and improvements
**1**:
Fixed an issue where address filter DNS rule was incorrectly rejected under certain circumstances.
If you have enabled `store_rdrc` to save results, consider clearing the cache file.
#### 1.8.7 #### 1.8.7
* Fixes and improvements * Fixes and improvements
#### 1.9.0-alpha.15
* Fixes and improvements
#### 1.9.0-alpha.14
* Improve DNS truncate behavior
* Fixes and improvements
#### 1.9.0-alpha.13
* Fixes and improvements
#### 1.8.6 #### 1.8.6
* Fixes and improvements * Fixes and improvements
#### 1.9.0-alpha.12
* Handle Windows power events
* Always disable cache for fake-ip DNS transport if `dns.independent_cache` disabled
* Fixes and improvements
#### 1.9.0-alpha.11
* Fix missing `rule_set_ipcidr_match_source` item in DNS rules **1**
* Fixes and improvements
**1**:
See [DNS Rule](/configuration/dns/rule/).
#### 1.9.0-alpha.10
* Add `bypass_domain` and `search_domain` platform HTTP proxy options **1**
* Fixes and improvements
**1**:
See [TUN](/configuration/inbound/tun) inbound.
#### 1.9.0-alpha.8
* Add rejected DNS response cache support **1**
* Fixes and improvements
**1**:
The new feature allows you to cache the check results of
[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration.
#### 1.9.0-alpha.7
* Update gVisor to 20240206.0
* Fixes and improvements
#### 1.9.0-alpha.6
* Fixes and improvements
#### 1.9.0-alpha.3
* Update `quic-go` to v0.41.0
* Fixes and improvements
#### 1.9.0-alpha.2
* Add support for `client-subnet` DNS options **1**
* Fixes and improvements
**1**:
See [DNS](/configuration/dns), [DNS Server](/configuration/dns/server) and [DNS Rules](/configuration/dns/rule).
Since this feature makes the scenario mentioned in `alpha.1` no longer leak DNS requests,
the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) has been updated.
#### 1.9.0-alpha.1
* `domain_suffix` behavior update **1**
* `process_path` format update on Windows **2**
* Add address filter DNS rule items **3**
**1**:
See [Migration](/migration/#domain_suffix-behavior-update).
**2**:
See [Migration](/migration/#process_path-format-update-on-windows).
**3**:
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
if using this method.
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
#### 1.8.5 #### 1.8.5
* Fixes and improvements * Fixes and improvements
@@ -38,14 +717,14 @@ icon: material/alert-decagram
* Fixes and improvements * Fixes and improvements
#### 1.8.0 ### 1.8.0
* Fixes and improvements * Fixes and improvements
Important changes since 1.7: Important changes since 1.7:
* Migrate cache file from Clash API to independent options **1** * Migrate cache file from Clash API to independent options **1**
* Introducing [Rule Set](/configuration/rule-set/) **2** * Introducing [rule-set](/configuration/rule-set/) **2**
* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3** * Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3**
* Allow nested logical rules **4** * Allow nested logical rules **4**
* Independent `source_ip_is_private` and `ip_is_private` rules **5** * Independent `source_ip_is_private` and `ip_is_private` rules **5**
@@ -65,7 +744,7 @@ See [Cache File](/configuration/experimental/cache-file/) and
**2**: **2**:
Rule set is independent collections of rules that can be compiled into binaries to improve performance. rule-set is independent collections of rules that can be compiled into binaries to improve performance.
Compared to legacy GeoIP and Geosite resources, Compared to legacy GeoIP and Geosite resources,
it can include more types of rules, load faster, it can include more types of rules, load faster,
use less memory, and update automatically. use less memory, and update automatically.
@@ -73,16 +752,16 @@ use less memory, and update automatically.
See [Route#rule_set](/configuration/route/#rule_set), See [Route#rule_set](/configuration/route/#rule_set),
[Route Rule](/configuration/route/rule/), [Route Rule](/configuration/route/rule/),
[DNS Rule](/configuration/dns/rule/), [DNS Rule](/configuration/dns/rule/),
[Rule Set](/configuration/rule-set/), [rule-set](/configuration/rule-set/),
[Source Format](/configuration/rule-set/source-format/) and [Source Format](/configuration/rule-set/source-format/) and
[Headless Rule](/configuration/rule-set/headless-rule/). [Headless Rule](/configuration/rule-set/headless-rule/).
For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and For GEO resources migration, see [Migrate GeoIP to rule-sets](/migration/#migrate-geoip-to-rule-sets) and
[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets). [Migrate Geosite to rule-sets](/migration/#migrate-geosite-to-rule-sets).
**3**: **3**:
New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets. New commands manage GeoIP, Geosite and rule-set resources, and help you migrate GEO resources to rule-sets.
**4**: **4**:
@@ -279,7 +958,7 @@ This change is intended to break incorrect usage and essentially requires no act
**1**: **1**:
Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule sets, Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule-sets,
rather than completely following the AND logic. rather than completely following the AND logic.
#### 1.8.0-alpha.5 #### 1.8.0-alpha.5
@@ -295,7 +974,7 @@ Since GeoIP was deprecated, we made this rule independent, see [Migration](/migr
#### 1.8.0-alpha.1 #### 1.8.0-alpha.1
* Migrate cache file from Clash API to independent options **1** * Migrate cache file from Clash API to independent options **1**
* Introducing [Rule Set](/configuration/rule-set/) **2** * Introducing [rule-set](/configuration/rule-set/) **2**
* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3** * Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3**
* Allow nested logical rules **4** * Allow nested logical rules **4**
@@ -306,7 +985,7 @@ See [Cache File](/configuration/experimental/cache-file/) and
**2**: **2**:
Rule set is independent collections of rules that can be compiled into binaries to improve performance. rule-set is independent collections of rules that can be compiled into binaries to improve performance.
Compared to legacy GeoIP and Geosite resources, Compared to legacy GeoIP and Geosite resources,
it can include more types of rules, load faster, it can include more types of rules, load faster,
use less memory, and update automatically. use less memory, and update automatically.
@@ -314,22 +993,22 @@ use less memory, and update automatically.
See [Route#rule_set](/configuration/route/#rule_set), See [Route#rule_set](/configuration/route/#rule_set),
[Route Rule](/configuration/route/rule/), [Route Rule](/configuration/route/rule/),
[DNS Rule](/configuration/dns/rule/), [DNS Rule](/configuration/dns/rule/),
[Rule Set](/configuration/rule-set/), [rule-set](/configuration/rule-set/),
[Source Format](/configuration/rule-set/source-format/) and [Source Format](/configuration/rule-set/source-format/) and
[Headless Rule](/configuration/rule-set/headless-rule/). [Headless Rule](/configuration/rule-set/headless-rule/).
For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and For GEO resources migration, see [Migrate GeoIP to rule-sets](/migration/#migrate-geoip-to-rule-sets) and
[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets). [Migrate Geosite to rule-sets](/migration/#migrate-geosite-to-rule-sets).
**3**: **3**:
New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets. New commands manage GeoIP, Geosite and rule-set resources, and help you migrate GEO resources to rule-sets.
**4**: **4**:
Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules. Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules.
#### 1.7.0 ### 1.7.0
* Fixes and improvements * Fixes and improvements
@@ -371,7 +1050,7 @@ see [TCP Brutal](/configuration/shared/tcp-brutal/) for details.
**5**: **5**:
Only supported in graphical clients on Android and iOS. Only supported in graphical clients on Android and Apple platforms.
#### 1.7.0-rc.3 #### 1.7.0-rc.3
@@ -408,7 +1087,7 @@ Only supported in graphical clients on Android and iOS.
**1**: **1**:
Only supported in graphical clients on Android and iOS. Only supported in graphical clients on Android and Apple platforms.
#### 1.7.0-beta.3 #### 1.7.0-beta.3
@@ -489,7 +1168,7 @@ Introduced in V2Ray 5.10.0.
The new HTTPUpgrade transport has better performance than WebSocket and is better suited for CDN abuse. The new HTTPUpgrade transport has better performance than WebSocket and is better suited for CDN abuse.
#### 1.6.0 ### 1.6.0
* Fixes and improvements * Fixes and improvements
@@ -668,7 +1347,7 @@ introduce new issues.
None of the existing Golang BBR congestion control implementations have been reviewed or unit tested. None of the existing Golang BBR congestion control implementations have been reviewed or unit tested.
This update is intended to address the multi-send defects of the old implementation and may introduce new issues. This update is intended to address the multi-send defects of the old implementation and may introduce new issues.
#### 1.5.0 ### 1.5.0
* Fixes and improvements * Fixes and improvements
@@ -862,7 +1541,7 @@ All inbounds and outbounds are supported, including `Naiveproxy`, `Hysteria`, `T
* Fixes and improvements * Fixes and improvements
#### 1.4.0 ### 1.4.0
* Fix bugs and update dependencies * Fix bugs and update dependencies
@@ -1004,7 +1683,7 @@ The old testflight link and app are no longer valid.
* Fixes and improvements * Fixes and improvements
#### 1.3.0 ### 1.3.0
* Fix bugs and update dependencies * Fix bugs and update dependencies
@@ -1196,7 +1875,7 @@ to `domain` rule.
* Flush DNS cache for macOS when tun start/close * Flush DNS cache for macOS when tun start/close
* Fix tun's DNS hijacking compatibility with systemd-resolved * Fix tun's DNS hijacking compatibility with systemd-resolved
#### 1.2.0 ### 1.2.0
* Fix bugs and update dependencies * Fix bugs and update dependencies

View File

@@ -40,6 +40,7 @@ SFA provides an unprivileged TUN implementation through Android VpnService.
|-----------------------|------------------|-----------------------------------| |-----------------------|------------------|-----------------------------------|
| `process_name` | :material-close: | No permission | | `process_name` | :material-close: | No permission |
| `process_path` | :material-close: | No permission | | `process_path` | :material-close: | No permission |
| `process_path_regex` | :material-close: | No permission |
| `package_name` | :material-check: | / | | `package_name` | :material-check: | / |
| `user` | :material-close: | Use `package_name` instead | | `user` | :material-close: | Use `package_name` instead |
| `user_id` | :material-close: | Use `package_name` instead | | `user_id` | :material-close: | Use `package_name` instead |

View File

@@ -42,6 +42,7 @@ SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension
|-----------------------|------------------|-----------------------| |-----------------------|------------------|-----------------------|
| `process_name` | :material-close: | No permission | | `process_name` | :material-close: | No permission |
| `process_path` | :material-close: | No permission | | `process_path` | :material-close: | No permission |
| `process_path_regex` | :material-close: | No permission |
| `package_name` | :material-close: | / | | `package_name` | :material-close: | / |
| `user` | :material-close: | No permission | | `user` | :material-close: | No permission |
| `user_id` | :material-close: | No permission | | `user_id` | :material-close: | No permission |

View File

@@ -14,8 +14,13 @@ platform-specific function implementation, such as TUN transparent proxy impleme
## :material-download: Download ## :material-download: Download
* [App Store](https://apps.apple.com/us/app/sing-box/id6451272673) * [App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)
* [TestFlight (Beta)](https://testflight.apple.com/join/AcqO44FH) * TestFlight (Beta)
TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai)
(one-time sponsorships are accepted).
Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot)
or sending us your Apple ID [via email](mailto:contact@sagernet.org).
## :material-file-download: Download (macOS standalone version) ## :material-file-download: Download (macOS standalone version)

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