Compare commits

..

407 Commits

Author SHA1 Message Date
世界
d03b4eaf0a
documentation: Bump version 2025-08-28 12:12:00 +08:00
世界
cc3e2fb8fb
Fix local DNS server crash 2025-08-28 12:12:00 +08:00
世界
7cb1cacd89
Add proxy support for ICMP echo request 2025-08-28 12:12:00 +08:00
世界
aa47149382
Fix resolve using resolved 2025-08-28 12:12:00 +08:00
世界
57c6c2b40e
documentation: Update behavior of local DNS server on darwin 2025-08-28 12:11:59 +08:00
世界
ea822eedfc
Stop using DHCP on iOS and tvOS
We do not have the `com.apple.developer.networking.multicast` entitlement and are unable to obtain it for non-technical reasons.
2025-08-28 12:11:59 +08:00
世界
2284fc465c
Remove use of ldflags -checklinkname=0 on darwin 2025-08-28 12:11:59 +08:00
世界
b4d4a38cd4
Fix local DNS server on darwin
We mistakenly believed that `libresolv`'s `search` function worked correctly in NetworkExtension, but it seems only `getaddrinfo` does.

This commit changes the behavior of the `local` DNS server in NetworkExtension to prefer DHCP, falling back to `getaddrinfo` if DHCP servers are unavailable.

It's worth noting that `prefer_go` does not disable DHCP since it respects Dial Fields, but `getaddrinfo` does the opposite. The new behavior only applies to NetworkExtension, not to all scenarios (primarily command-line binaries) as it did previously.

In addition, this commit also improves the DHCP DNS server to use the same robust query logic as `local`.
2025-08-28 12:11:59 +08:00
世界
feeace6ee2
Fix legacy DNS config 2025-08-28 12:11:59 +08:00
世界
af2446c150
Fix rule-set format 2025-08-28 12:11:59 +08:00
世界
6010e6cb67
documentation: Remove outdated icons 2025-08-28 12:11:58 +08:00
世界
f3cc2fdb73
documentation: Improve local DNS server 2025-08-28 12:11:58 +08:00
世界
71f7c7ec43
Use libresolv in local DNS server on darwin 2025-08-28 12:11:58 +08:00
世界
9e33df85ab
Use resolved in local DNS server if available 2025-08-28 12:11:57 +08:00
xchacha20-poly1305
e2065120d4
Fix rule set version 2025-08-28 12:11:57 +08:00
世界
fecafd3cb1
documentation: Add preferred_by route rule item 2025-08-28 12:11:57 +08:00
世界
c5f6e22ddb
Add preferred_by route rule item 2025-08-28 12:11:57 +08:00
世界
cd7cabcbae
documentation: Add interface address rule items 2025-08-28 12:11:57 +08:00
世界
7b9b6642d6
Add interface address rule items 2025-08-28 12:11:56 +08:00
neletor
331ba48a6a
Add support for ech retry configs 2025-08-28 12:11:56 +08:00
Zephyruso
1612e98bc6
Add /dns/flush-clash meta api 2025-08-28 12:11:56 +08:00
世界
980e96250b
Bump version 2025-08-28 12:11:30 +08:00
世界
963bc4b647
Enforce Tailscale NoLogsNoSupport 2025-08-28 10:30:13 +08:00
世界
031f25c1c1
Deprecate common/atomic 2025-08-25 19:49:12 +08:00
世界
b40f642fa4
Bump version 2025-08-21 09:43:47 +08:00
世界
22782ca6fc
Fix outbound start 2025-08-21 09:41:31 +08:00
世界
1468d83895
Make realityClientConnWrapper replaceable 2025-08-20 16:26:27 +08:00
世界
97f0dc8a60
Bump version 2025-08-20 09:20:41 +08:00
dyhkwong
ee02532ab5
Fix tlsfragment fallback writeAndWaitAck 2025-08-20 09:20:41 +08:00
世界
f1dd0dba78
Make ReadWaitConn reader replaceable 2025-08-20 09:18:03 +08:00
wwqgtxx
f4ed684146
Update cast using in sing-vmess 2025-08-20 08:45:09 +08:00
wwqgtxx
83f02d0bfb
Make utlsConnWrapper replaceable 2025-08-20 08:45:09 +08:00
wwqgtxx
52fa5f20a3
Make realityConnWrapper replaceable 2025-08-20 08:45:09 +08:00
世界
f462ce5615
Update tfo-go 2025-08-19 21:56:05 +08:00
世界
cef3e538ba
Fix failed DNS responses being incorrectly rejected 2025-08-19 11:14:46 +08:00
世界
acda4ce985
Fix bind_interface not working with auto_redirect 2025-08-17 14:48:01 +08:00
世界
354ece2bdf
Fix resolved service 2025-08-16 00:09:29 +08:00
世界
de10bb00a9
Fix ssm-api 2025-08-15 15:05:37 +08:00
世界
fdc181106d
Fix atomic pointer usages 2025-08-15 15:05:34 +08:00
renovate[bot]
8752b631bd
[dependencies] Update golang Docker tag to v1.25 (#3276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-15 12:45:43 +08:00
世界
378e39f70c
Update golangci-lint to v2 2025-08-13 23:37:40 +08:00
renovate[bot]
043a2e7a07
[dependencies] Update github-actions 2025-08-13 22:12:19 +08:00
世界
7e190e92ca
Fix build with Go 1.25 2025-08-13 22:08:35 +08:00
世界
5eb318ba06
Update Go to 1.25 2025-08-13 22:08:35 +08:00
世界
4a209f1afb
Fix h2mux close check 2025-08-13 21:04:01 +08:00
世界
c0ac3c748c
Reduce default MTU for android 2025-08-13 11:48:44 +08:00
世界
a65d3e040a
platform: Fix context 2025-08-13 11:26:32 +08:00
世界
2358efe44a
release: Fix android build 2025-08-11 22:11:14 +08:00
世界
09d3b8f2c2
release: Fix repo 2025-08-11 22:11:14 +08:00
yu
531de77124
documentation: Fix tun address format 2025-08-11 22:11:13 +08:00
Kismet
44981fd803
documentation: Fix typo 2025-08-11 22:11:13 +08:00
世界
4fb5ac292b
Bump version 2025-08-10 20:06:28 +08:00
Sentsuki
0e23a3d7c2
documentation: Fix Rcode's migration guide
Signed-off-by: Sentsuki <52487960+Sentsuki@users.noreply.github.com>
2025-08-10 20:06:28 +08:00
Oleksandr Redko
76ee64ae50
Simplify slice to array conversion 2025-08-10 20:06:28 +08:00
Me0wo
e1dbcccab5
documentation: Fix typo
Signed-off-by: Me0wo <152751263+Sn0wo2@users.noreply.github.com>
2025-08-10 20:06:28 +08:00
Youfu Zhang
fba802effd
Fix libresolv initialization
Fixes: 9533031891c0 ("Update libresolv usage")

Signed-off-by: Youfu Zhang <zhangyoufu@gmail.com>
2025-08-10 20:06:28 +08:00
世界
9495b56772
Update Go to 1.24.6 2025-08-08 17:07:56 +08:00
世界
a8434b176f
Fix SyscallVectorisedWriter 2025-08-08 16:08:47 +08:00
世界
ef0004400d
Fix legacy domain resolver deprecated warning incorrectly suppressed for direct outbound 2025-08-07 13:56:35 +08:00
世界
0a63049845
android: Add workaround for tailscale pidfd crash 2025-08-07 12:54:19 +08:00
世界
2dcb86941f
Bump version 2025-08-04 08:54:00 +08:00
世界
5c6eb89cfb
Fix udp listener write back 2025-08-04 08:54:00 +08:00
世界
5b92eeb3bf
Fix auto redirect panic 2025-08-01 16:50:54 +08:00
wwqgtxx
3518ce083b
Fix packetaddr panic 2025-08-01 16:50:54 +08:00
世界
f13c54afc1
Fix vless write 2025-07-28 08:01:52 +08:00
世界
3388efe65a
Fix ssm-api 2025-07-28 08:01:52 +08:00
世界
a11384b286
Fix time service wrapper 2025-07-24 09:21:08 +08:00
dyhkwong
9dd9fb27cd
Fix disable_sni nil time func 2025-07-24 09:21:08 +08:00
世界
0f2035149c
Remove dependency on circl 2025-07-23 11:26:06 +08:00
世界
cba364204a
Fix VectorisedReadWaiter on windows 2025-07-23 11:26:06 +08:00
世界
4e17788549
Update dependencies 2025-07-23 11:26:06 +08:00
世界
18a6719893
Fix IndexTLSServerName 2025-07-23 11:26:06 +08:00
dyhkwong
687343f6ca
Fix disable_sni not working with custom RootCAs 2025-07-22 19:20:09 +08:00
世界
e061538c30
Update Go to 1.24.5 2025-07-21 10:10:02 +08:00
世界
a6375c7530
Fix data corruption in direct copy 2025-07-21 10:09:59 +08:00
世界
45fa18a2e3
Fix vision crash 2025-07-20 18:31:05 +08:00
世界
534cccce91
Fix DNS upgrade 2025-07-18 21:46:03 +08:00
世界
72dbcd3ad4
Improve darwin tun performance 2025-07-18 21:23:04 +08:00
世界
5533094984
Fix UDP DNS buffer size 2025-07-18 12:20:33 +08:00
世界
ae2ecd6002
Increase default mtu to 65535 2025-07-12 14:48:04 +08:00
世界
0098a2adc5
Improve direct copy 2025-07-12 14:48:04 +08:00
世界
c0dd4a3f07
Fix DNS reject check 2025-07-08 13:14:46 +08:00
世界
497ddb5829
Improve copy 2025-07-08 13:14:46 +08:00
世界
811ff93549
Increase default mtu under network extension to 4064 2025-07-08 13:14:46 +08:00
世界
96df69bcdc
release: Fix publish testflight 2025-07-08 13:14:46 +08:00
世界
6cfa2b8b86
Improve darwin tun performance 2025-07-08 13:14:46 +08:00
世界
eea1e701b7
Improve nftables rules for openwrt 2025-07-08 13:14:46 +08:00
世界
455e5de74d
Fixed DoH server recover from conn freezes 2025-07-08 13:14:45 +08:00
世界
9533031891
Update libresolv usage 2025-07-08 13:14:45 +08:00
yu
80f8ea6849
documentation: Update client configuration manual 2025-07-08 13:14:45 +08:00
yanwo
50eadb00c7
documentation: Fix typo
Signed-off-by: yanwo <ogilvy@gmail.com>
2025-07-08 13:14:45 +08:00
anytinz
d4012bd0b2
documentation: Fix wrong SideStore loopback ip 2025-07-08 13:14:45 +08:00
世界
a902e9f9f6
Revert "release: Add IPA build"
After testing, it seems that since extensions are not handled correctly, it cannot be installed by SideStore.
2025-07-08 13:14:45 +08:00
世界
da3ba573d8
release: Add IPA build 2025-07-08 13:14:45 +08:00
世界
bea9048cfe
Add API to dump AdGuard rules 2025-07-08 13:14:44 +08:00
Sukka
fc0f5ed83a
Improve AdGuard rule-set parser 2025-07-08 13:14:44 +08:00
Restia-Ashbell
c0588c30d7
Add ECH support for uTLS 2025-07-08 13:14:44 +08:00
世界
24c940c51c
Improve TLS fragments 2025-07-08 13:14:44 +08:00
世界
407ee08d8a
Add cache support for ssm-api 2025-07-08 13:14:44 +08:00
世界
756585fb2a
Fix service will not be closed 2025-07-08 13:14:44 +08:00
世界
5662784afb
Add loopback address support for tun 2025-07-08 13:14:44 +08:00
世界
3801901726
Fix tproxy listener 2025-07-08 13:14:43 +08:00
世界
7d58174f1f
Fix systemd package 2025-07-08 13:14:43 +08:00
世界
d339f85087
Fix missing home for derp service 2025-07-08 13:14:43 +08:00
Zero Clover
b6a114f7f4
documentation: Fix services 2025-07-08 13:14:43 +08:00
世界
e586ef070e
Fix dns.client_subnet ignored 2025-07-08 13:14:43 +08:00
世界
71a76e9ecb
documentation: Minor fixes 2025-07-08 13:14:42 +08:00
世界
1d66474022
Fix tailscale forward 2025-07-08 13:14:42 +08:00
世界
3934e53476
Minor fixes 2025-07-08 13:14:42 +08:00
世界
0146fbfc40
Add SSM API service 2025-07-08 13:14:42 +08:00
世界
6ee3117755
Add resolved service and DNS server 2025-07-08 13:14:41 +08:00
世界
e2440a569e
Add DERP service 2025-07-08 13:14:41 +08:00
世界
7a1eee78df
Add service component type 2025-07-08 13:14:41 +08:00
世界
e3c8c0705f
Fix tproxy tcp control 2025-07-08 13:14:40 +08:00
愚者
886d427337
release: Fix build tags for android
Signed-off-by: 愚者 <11926619+FansChou@users.noreply.github.com>
2025-07-08 13:14:40 +08:00
世界
d5432b4c27
prevent creation of bind and mark controls on unsupported platforms 2025-07-08 13:14:40 +08:00
PuerNya
42064fe7ec
documentation: Fix description of reject DNS action behavior 2025-07-08 13:14:40 +08:00
Restia-Ashbell
7cee76f9a6
Fix TLS record fragment 2025-07-08 13:14:39 +08:00
世界
ed5b2f2997
Add missing accept_routes option for Tailscale 2025-07-08 13:14:39 +08:00
世界
3b480de38a
Add TLS record fragment support 2025-07-08 13:14:38 +08:00
世界
f990630ccc
Fix set edns0 client subnet 2025-07-08 13:14:38 +08:00
世界
d33614d6a0
Update minor dependencies 2025-07-08 13:14:38 +08:00
世界
b3866bcea0
Update certmagic and providers 2025-07-08 13:14:38 +08:00
世界
26ec73c71b
Update protobuf and grpc 2025-07-08 13:14:38 +08:00
世界
c3403c5413
Add control options for listeners 2025-07-08 13:14:38 +08:00
世界
3b6ddcae37
Update quic-go to v0.52.0 2025-07-08 13:14:19 +08:00
世界
dbdcce20a8
Update utls to v1.7.2 2025-07-08 13:12:35 +08:00
世界
e7ef1b2368
Handle EDNS version downgrade 2025-07-08 13:12:35 +08:00
世界
ce32d1c2c3
documentation: Fix anytls padding scheme description 2025-07-08 13:12:34 +08:00
安容
596b66f397
Report invalid DNS address early 2025-07-08 13:12:34 +08:00
世界
d4fd43cf6f
Fix wireguard listen_port 2025-07-08 13:12:34 +08:00
世界
6c377f16e7
clash-api: Add more meta api 2025-07-08 13:12:34 +08:00
世界
349db7baec
Fix DNS lookup 2025-07-08 13:12:33 +08:00
世界
1f3097da00
Fix fetch ECH configs 2025-07-08 13:12:33 +08:00
reletor
0b4b5e6f0f
documentation: Minor fixes 2025-07-08 13:12:33 +08:00
caelansar
245273e6c1
Fix callback deletion in UDP transport 2025-07-08 13:12:32 +08:00
世界
54a0004de6
documentation: Try to make the play review happy 2025-07-08 13:12:32 +08:00
世界
6a211f6ed6
Fix missing handling of legacy domain_strategy options 2025-07-08 13:12:32 +08:00
世界
aadb44ebd6
Improve local DNS server 2025-07-08 13:12:32 +08:00
anytls
9b0db6ab15
Update anytls
Co-authored-by: anytls <anytls>
2025-07-08 13:12:31 +08:00
世界
5b363c347f
Fix DNS dialer 2025-07-08 13:12:31 +08:00
世界
cdea3f63d4
release: Skip override version for iOS 2025-07-08 13:12:31 +08:00
iikira
40a6260f6e
Fix UDP DNS server crash
Signed-off-by: iikira <i2@mail.iikira.com>
2025-07-08 13:12:31 +08:00
ReleTor
a5e47f4e0f
Fix fetch ECH configs 2025-07-08 13:12:30 +08:00
世界
ac7bc587cb
Allow direct outbounds without domain_resolver 2025-07-08 13:12:30 +08:00
世界
4e11a3585a
Fix Tailscale dialer 2025-07-08 13:12:30 +08:00
dyhkwong
63d3e9f6e5
Fix DNS over QUIC stream close 2025-07-08 13:12:30 +08:00
anytls
d115e36ed8
Update anytls
Co-authored-by: anytls <anytls>
2025-07-08 13:12:30 +08:00
Rambling2076
af56b1a950
Fix missing with_tailscale in Dockerfile
Signed-off-by: Rambling2076 <Rambling2076@proton.me>
2025-07-08 13:12:29 +08:00
世界
f9999a76fe
Fail when default DNS server not found 2025-07-08 13:12:28 +08:00
世界
42eb3841a1
Update gVisor to 20250319.0 2025-07-08 13:12:28 +08:00
世界
fb622ccbdf
Explicitly reject detour to empty direct outbounds 2025-07-08 13:12:28 +08:00
世界
d2dc3ddf72
Add netns support 2025-07-08 13:12:28 +08:00
世界
e8499452f8
Add wildcard name support for predefined records 2025-07-08 13:12:27 +08:00
世界
e0a6b31c03
Remove map usage in options 2025-07-08 13:12:27 +08:00
世界
7c923209ad
Fix unhandled DNS loop 2025-07-08 13:12:27 +08:00
世界
bca2bd2fa1
Add wildcard-sni support for shadow-tls inbound 2025-07-08 13:12:26 +08:00
k9982874
fa99ca2757
Add ntp protocol sniffing 2025-07-08 13:12:26 +08:00
世界
7073f2a272
option: Fix marshal legacy DNS options 2025-07-08 13:12:26 +08:00
世界
390e30ae7b
Make domain_resolver optional when only one DNS server is configured 2025-07-08 13:12:26 +08:00
世界
23cf8c49e0
Fix DNS lookup context pollution 2025-07-08 13:12:25 +08:00
世界
b17a024f6c
Fix http3 DNS server connecting to wrong address 2025-07-08 13:12:25 +08:00
Restia-Ashbell
1ed21085bb
documentation: Fix typo 2025-07-08 13:12:25 +08:00
anytls
56409ff269
Update sing-anytls
Co-authored-by: anytls <anytls>
2025-07-08 13:12:24 +08:00
k9982874
0c523980ff
Fix hosts DNS server 2025-07-08 13:12:24 +08:00
世界
32873d06bc
Fix UDP DNS server crash 2025-07-08 13:12:24 +08:00
世界
4accaccf77
documentation: Fix missing ip_accept_any DNS rule option 2025-07-08 13:12:23 +08:00
世界
ff416aacaf
Fix anytls dialer usage 2025-07-08 13:12:23 +08:00
世界
b97947e8ac
Move predefined DNS server to rule action 2025-07-08 13:12:23 +08:00
世界
dfcd9fb8c3
Fix domain resolver on direct outbound 2025-07-08 13:12:22 +08:00
Zephyruso
803811568e
Fix missing AnyTLS display name 2025-07-08 13:12:22 +08:00
anytls
50b0bd5c39
Update sing-anytls
Co-authored-by: anytls <anytls>
2025-07-08 13:12:22 +08:00
Estel
2d02b2b1cf
documentation: Fix typo
Signed-off-by: Estel <callmebedrockdigger@gmail.com>
2025-07-08 13:12:22 +08:00
TargetLocked
456fbecf16
Fix parsing legacy DNS options 2025-07-08 13:12:21 +08:00
世界
668923c392
Fix DNS fallback 2025-07-08 13:12:21 +08:00
世界
c51e9cbe06
documentation: Fix missing hosts DNS server 2025-07-08 13:12:20 +08:00
anytls
60b451e6cf
Add MinIdleSession option to AnyTLS outbound
Co-authored-by: anytls <anytls>
2025-07-08 13:12:20 +08:00
ReleTor
3e35390d8f
documentation: Minor fixes 2025-07-08 13:12:20 +08:00
libtry486
f2dad289fb
documentation: Fix typo
fix typo

Signed-off-by: libtry486 <89328481+libtry486@users.noreply.github.com>
2025-07-08 13:12:20 +08:00
Alireza Ahmadi
b4a8fa59f5
Fix Outbound deadlock 2025-07-08 13:12:19 +08:00
世界
73de2a7d07
documentation: Fix AnyTLS doc 2025-07-08 13:12:19 +08:00
anytls
1699a7ce33
Add AnyTLS protocol 2025-07-08 13:12:19 +08:00
世界
7743c6e881
Migrate to stdlib ECH support 2025-07-08 13:12:19 +08:00
世界
9a5f69f435
Add fallback local DNS server for iOS 2025-07-08 13:12:18 +08:00
世界
5c4211e849
Get darwin local DNS server from libresolv 2025-07-08 13:12:18 +08:00
世界
c1189e2a7b
Improve resolve action 2025-07-08 13:12:18 +08:00
世界
f18889369f
Add back port hopping to hysteria 1 2025-07-08 13:12:17 +08:00
xchacha20-poly1305
91c7b638e8
Remove single quotes of raw Moziila certs 2025-07-08 13:12:17 +08:00
世界
6f793a0273
Add Tailscale endpoint 2025-07-08 13:12:16 +08:00
世界
0f6c417c3c
Build legacy binaries with latest Go 2025-07-08 13:12:16 +08:00
世界
c830e9a634
documentation: Remove outdated icons 2025-07-08 13:12:16 +08:00
世界
e809623ec9
documentation: Certificate store 2025-07-08 13:12:16 +08:00
世界
061276902b
documentation: TLS fragment 2025-07-08 13:12:15 +08:00
世界
fa6f7d396e
documentation: Outbound domain resolver 2025-07-08 13:12:15 +08:00
世界
23666a9230
documentation: Refactor DNS 2025-07-08 13:12:15 +08:00
世界
17576e9f66
Add certificate store 2025-07-08 13:12:14 +08:00
世界
90ec9c8bcb
Add TLS fragment support 2025-07-08 13:12:14 +08:00
世界
988ac62a1b
refactor: Outbound domain resolver 2025-07-08 13:12:14 +08:00
世界
3016338e34
refactor: DNS 2025-07-08 13:12:14 +08:00
世界
bc35aca017
Bump version 2025-07-08 13:11:13 +08:00
世界
281d52a1ea
Fix hy2 server crash 2025-07-08 13:11:13 +08:00
世界
b8502759b5
Fix DNS reject check 2025-07-07 13:57:37 +08:00
世界
6f804adf39
Fix v2rayhttp crash 2025-07-03 21:48:10 +08:00
Kyson
36db31c55a
documentation: Fix typo
Co-authored-by: chenqixin <chenqixin@bytedance.com>
2025-06-29 18:54:05 +08:00
世界
4dbbf59c82
Fix logger for acme 2025-06-29 18:44:40 +08:00
世界
832eb4458d
release: Fix xcode version 2025-06-29 18:44:40 +08:00
dyhkwong
2cf989d306
Fix inbound with v2ray transport missing InboundOptions 2025-06-25 13:20:00 +08:00
世界
7d3ee29bd0
Also skip duplicate sniff for TCP 2025-06-21 12:57:27 +08:00
世界
cba0e46aba
Fix log for rejected connections 2025-06-21 12:57:26 +08:00
世界
9b8ab3e61e
Bump version 2025-06-19 11:57:44 +08:00
dyhkwong
47f18e823a
Fix: macOS udp find process should use unspecified fallback
be8d63ba8f
2025-06-18 08:34:59 +08:00
世界
2d1b824b62
Fix gLazyConn race 2025-06-17 14:24:11 +08:00
世界
d511698f3f
Fix slowOpenConn 2025-06-12 08:05:04 +08:00
世界
cb435ea232
Fix default network strategy 2025-06-12 08:05:04 +08:00
世界
43a9016c83
Fix leak in hijack-dns 2025-06-06 14:28:09 +08:00
世界
255068fd40
Bump version 2025-06-04 23:32:10 +08:00
世界
098a00b025
Fix v2ray websocket transport 2025-06-04 23:23:36 +08:00
世界
dba0b5276b
Bump version 2025-06-04 20:06:38 +08:00
Sentsuki
78ae935468
documentation: Fix typo
Signed-off-by: Sentsuki <52487960+Sentsuki@users.noreply.github.com>
2025-06-04 20:06:38 +08:00
Mahdi
3ea5f76470
Fix nil logger at v2rayhttp server 2025-06-04 20:06:20 +08:00
世界
b4d294c05e
Fix TUIC read buffer 2025-06-04 20:03:51 +08:00
世界
83cf5f5c6a
Fix ws closed error message 2025-05-27 14:30:07 +08:00
世界
e7b3a8eebe
Fix vmess read request 2025-05-27 14:11:05 +08:00
世界
ee3a42a67e
Fix none method read buffer 2025-05-27 14:03:48 +08:00
世界
50227c0f5f
Fix sniff action 2025-05-26 18:24:35 +08:00
世界
bc5eb1e1a5
Fix RoutePacketConnectionEx 2025-05-24 08:14:43 +08:00
世界
995267a042
Remove wrong ALPNs in DOH/DOH3 2025-05-24 08:00:13 +08:00
世界
41226a6075
Fix interface finder 2025-05-23 10:57:38 +08:00
世界
81d32181ce
Fix update route address set 2025-05-20 19:46:54 +08:00
世界
c5ecca3938
Bump version 2025-05-18 16:48:44 +08:00
世界
900888731c
Fix DNS reject response 2025-05-13 18:05:31 +08:00
世界
13e648e4b1
Fix set edns0 subnet 2025-05-07 15:12:17 +08:00
世界
aff12ff671
Bump version 2025-05-05 09:37:47 +08:00
世界
101fb88255
Fix allocator put 2025-05-05 09:37:44 +08:00
世界
8b489354e4
Undeprecate the block outbound 2025-05-04 18:45:53 +08:00
世界
7dea6eb7a6
Fix missing read waiter for cancelers 2025-05-04 18:14:21 +08:00
世界
af1bfe4e3e
Make rule_set.format optional 2025-05-04 18:14:21 +08:00
世界
d574e9eb52
Update smux to v1.5.34 2025-04-30 19:39:15 +08:00
世界
2d7df1e1f2
Fix hysteria bytes format 2025-04-29 20:45:19 +08:00
世界
1c0ffcf5b1
Fix counter position in auto redirect dnat rules 2025-04-28 11:20:23 +08:00
世界
348cc39975
Bump version 2025-04-27 21:33:31 +08:00
世界
987899f94a
Fix usages of wireguard listener 2025-04-27 21:29:23 +08:00
世界
d8b2d5142f
Fix panic on some stupid input 2025-04-25 16:03:58 +08:00
世界
134802d1ee
Fix ssh outbound 2025-04-25 16:03:57 +08:00
世界
e5e81b4de1
Fix wireguard listening 2025-04-25 16:03:57 +08:00
世界
300c961efa
option: Fix listable again and again 2025-04-25 16:03:57 +08:00
世界
7c7f512405
option: Fix omitempty reject method 2025-04-25 16:03:57 +08:00
世界
03e8d029c2
release: Fix apt-get install 2025-04-25 16:03:57 +08:00
世界
787b5f1931
Fix set wireguard reserved on Linux 2025-04-25 16:03:57 +08:00
世界
56a7624618
Fix vmess working with zero uuids 2025-04-25 16:03:57 +08:00
世界
3a84acf122
Fix hysteria1 server panic 2025-04-25 16:03:57 +08:00
世界
f600e02e47
Fix DNS crash 2025-04-25 16:03:57 +08:00
世界
e6d19de58a
Fix overriding address 2025-04-22 14:55:44 +08:00
dyhkwong
f2bbf6b2aa
Fix sniffer errors override each others
* Fix sniffer errors override each others

* Do not return ErrNeedMoreData if header is not expected
2025-04-22 14:44:55 +08:00
dyhkwong
c54d50fd36
Fix websocket detour
Signed-off-by: trimgop <20010323+trimgop@users.noreply.github.com>
Co-authored-by: trimgop <20010323+trimgop@users.noreply.github.com>
2025-04-22 14:44:34 +08:00
世界
6a051054db
release: Fix packages 2025-04-19 19:12:01 +08:00
世界
49498f6439
Bump version 2025-04-18 08:54:40 +08:00
世界
144a890c71
release: Add openwrt packages 2025-04-18 08:54:40 +08:00
世界
afb4993445
Fix urltest outbound 2025-04-18 08:54:40 +08:00
世界
4c9455b944
Fix wireguard endpoint 2025-04-18 08:54:40 +08:00
世界
5fdc051a08
Fix override_port in direct inbound 2025-04-16 17:04:13 +08:00
世界
cb68a40c43
documentation: Update actual behaviors of auto_redirect and strict_route 2025-04-12 13:06:16 +08:00
纳西妲 · Nahida
023218e6e7
Fix build will fail when use space to split each tag 2025-04-12 13:06:16 +08:00
世界
2a24b94b8d
Minor fixes 2025-04-12 13:06:15 +08:00
世界
c6531cf184
Fix NTP service 2025-04-12 13:06:15 +08:00
世界
d4fa0ed349
Improve auto redirect 2025-04-12 13:06:10 +08:00
世界
10874d2dc4
Bump version 2025-04-08 14:34:09 +08:00
Fei1Yang
5adaf1ac75
Mark config file as noreplace for rpm 2025-04-08 14:21:08 +08:00
世界
9668ea69b8
Fix windows process searcher 2025-04-08 14:16:27 +08:00
testing
ae9bc7acf1
documentation: Fix typo
Signed-off-by: testing <58134720+testing765@users.noreply.github.com>
2025-04-08 14:16:23 +08:00
世界
594ee480a2
option: Fix listable 2025-04-08 14:16:23 +08:00
世界
a15b5a2463
Fix no_drop not work 2025-04-08 14:16:23 +08:00
Mahdi
991e755789
Fix conn copy 2025-04-08 14:16:22 +08:00
世界
97d41ffde8
Improve pause management 2025-04-08 14:16:22 +08:00
世界
24af0766ac
Fix uTP sniffer 2025-04-08 14:16:22 +08:00
世界
af17eaa537
Improve sniffer 2025-04-08 14:16:22 +08:00
世界
3adc10a797
Fix hysteria2 close 2025-04-08 14:16:22 +08:00
xchacha20-poly1305
5eeef6b28e
Fix multiple trackers 2025-04-08 14:16:22 +08:00
世界
f4c29840c3
Fix DNS sniffer 2025-03-31 20:45:04 +08:00
世界
47fc3ebda4
Add duplicate tag check 2025-03-29 23:10:22 +08:00
世界
9774a659b0
Fix DoQ / truncate DNS message 2025-03-29 17:41:22 +08:00
世界
2e4a6de4e7
release: Fix read tag 2025-03-27 20:30:57 +08:00
世界
a530e424e9
Bump version 2025-03-27 18:17:39 +08:00
世界
0bfd487ee9
Fix udpnat2 handler again 2025-03-27 18:17:39 +08:00
世界
6aae834493
release: Fix workflow 2025-03-27 18:17:39 +08:00
世界
f56131f38e
Make linter happy 2025-03-24 20:38:42 +08:00
世界
273a11d550
Fix crash on udpnat2 handler 2025-03-24 18:14:32 +08:00
世界
ae8ce75e41
Fix websocket crash 2025-03-24 17:44:14 +08:00
世界
d6d94b689f
release: Replace goreleaser build with scripts 2025-03-24 13:48:37 +08:00
世界
30d785f1ee
release: Use fake goreleaser key 2025-03-21 22:25:51 +08:00
世界
db5ec3cdfc
Fix connectionCopyEarly 2025-03-21 10:51:16 +08:00
世界
9aca54d039
Fix socks5 UDP 2025-03-16 14:46:44 +08:00
世界
d55d5009c2
Fix processing multiple sniffs 2025-03-16 09:21:54 +08:00
世界
4f3ee61104
Fix copy early conn 2025-03-15 08:09:04 +08:00
世界
96eb98c00a
Fix httpupgrade crash 2025-03-14 17:17:28 +08:00
世界
68ce9577c6
Fix context in v2ray http transports 2025-03-14 17:07:17 +08:00
世界
3ae036e997
Downgrade goreleaser to stable since nfpm fixed 2025-03-13 18:53:19 +08:00
世界
5da2d1d470
release: Fix goreleaser version 2025-03-12 16:15:50 +08:00
世界
8e2baf40f1
Bump version 2025-03-11 20:18:34 +08:00
世界
c24c40dfee
platform: Fix android start 2025-03-11 20:18:34 +08:00
世界
32e52ce1ed
Fix udp nat for fakeip 2025-03-11 19:09:27 +08:00
世界
ed46438359
release: Use nightly goreleaser to fix rpm bug 2025-03-11 13:29:08 +08:00
世界
0b5490d5a3
Fix resolve domain for WireGuard 2025-03-11 12:02:25 +08:00
Tal Rasha
2d73ef511d
Fix grpclite memory leak
Co-authored-by: talrasha007 <talrasha007@gmail.om>
2025-03-10 14:48:02 +08:00
Mahdi
63e6c85f6f
Fix shadowsocks UoT 2025-03-10 14:47:59 +08:00
世界
8946a6d2d0
release: Use latest goreleaser 2025-03-09 15:27:04 +08:00
世界
d3132645fb
documentation: Fix description of the UoT protocol 2025-03-09 15:26:42 +08:00
世界
373f158fe0
Fix download external ui with query params 2025-03-09 15:26:36 +08:00
世界
ce36835fab
Fix override destination 2025-03-09 15:25:06 +08:00
世界
619fa671d7
Skip binding to the default interface as it will fail on some Android devices 2025-02-26 07:25:35 +08:00
世界
eb07c7a79e
Bump version 2025-02-24 07:27:55 +08:00
Gavin Luo
7eb3535094
release: Fix systemd permissions 2025-02-24 07:27:55 +08:00
世界
93b68312cf
platform: Add update WIFI state func 2025-02-23 08:35:30 +08:00
世界
97ce666e43
Fix http.FileServer short write 2025-02-23 08:35:30 +08:00
世界
4000e1e66d
release: Fix update android version 2025-02-23 08:35:30 +08:00
世界
270740e859
Fix crash on route address set update 2025-02-23 08:35:30 +08:00
世界
6cad142cfe
Bump Go to go1.24 2025-02-23 08:35:30 +08:00
世界
093013687c
Fix sniff QUIC hidden in three or more packets 2025-02-18 18:14:59 +08:00
世界
ff31c469a0
Override version 2025-02-11 15:55:15 +08:00
世界
fbe390268c
Bump version 2025-02-11 01:32:14 +08:00
世界
07ac01dcb7
platform: Update NDK to r28 2025-02-11 01:32:14 +08:00
ReleTor
badfdb62cd
documentation: Fixes 2025-02-11 01:32:14 +08:00
printfer
986a410b30
documentation: Fix migration links 2025-02-11 01:32:14 +08:00
世界
9db2d58545
Fix override address 2025-02-11 01:32:14 +08:00
世界
4eed46ac59
Fix respond ICMP echo 2025-02-10 15:12:10 +08:00
世界
abc38d1dab
Fix udpnat2 crash 2025-02-10 15:11:26 +08:00
世界
8d6c4f1289
release: Skip testflight when another build in review 2025-02-06 12:02:47 +08:00
世界
a2d40eb8b8
Fix override UDP destination 2025-02-06 11:20:35 +08:00
世界
17b502bb4b
Update dependencies 2025-02-06 09:08:52 +08:00
世界
a0d4421085
Update quic-go to v0.49.0 2025-02-06 08:50:21 +08:00
世界
0d443072d1
Fix panic in auto-redirect initialize 2025-02-06 08:49:25 +08:00
世界
c9fb99b799
Fix missing ENOTCONN in IsClosed check 2025-02-06 08:48:49 +08:00
世界
92d245ad04
Bump version 2025-02-05 09:59:52 +08:00
世界
0908627297
Fix crash on remote rule-set stop 2025-02-05 08:58:10 +08:00
世界
7f79458b4f
Minor updates 2025-02-01 19:49:33 +08:00
世界
9b4c11ba95
Fix rule-set not closed 2025-02-01 19:49:33 +08:00
世界
27c31eac5d
Fix local rule-set not updated 2025-02-01 19:42:21 +08:00
世界
bab8dc0b82
Fix missing handshake for early conn 2025-01-31 12:57:35 +08:00
世界
d09d2fb665
Bump version 2025-01-30 15:09:54 +08:00
世界
e64cf3b7df
Do not set address sets to routes on Apple platforms
Network Extension was observed to stop for unknown reasons
2025-01-27 13:40:39 +08:00
世界
9b73222314
documentation: Bump version 2025-01-27 10:53:14 +08:00
世界
3923b57abf
release: Update NDK to r28-beta3 2025-01-27 10:53:14 +08:00
世界
4807e64609
documentation: Fix typo 2025-01-27 10:04:07 +08:00
世界
eeb37d89f1
Fix rule-set upgrade command 2025-01-27 09:50:27 +08:00
世界
08c1ec4b7e
Fix legacy routes 2025-01-27 09:50:27 +08:00
HystericalDragon
6b4cf67add
Fix endpoints not close 2025-01-27 09:50:27 +08:00
世界
e65926fd08
Fix tests 2025-01-13 15:14:30 +08:00
世界
f2ec319fe1
Fix system time 2025-01-13 15:14:30 +08:00
世界
32377a61b7
Add port hopping for hysteria2 2025-01-13 15:14:30 +08:00
世界
7aac801ccd
tun: Set address sets to routes 2025-01-13 15:14:30 +08:00
世界
96fdf59ee4
Fix default dialer on legacy xiaomi systems 2025-01-13 15:14:30 +08:00
世界
50b8f3ab94
Add rule-set merge command 2025-01-13 15:14:30 +08:00
世界
ff7aaf977b
Fix DNS match 2025-01-13 15:14:30 +08:00
世界
9a1efbe54d
Fix domain strategy 2025-01-13 15:14:30 +08:00
世界
906c21f458
Fix time service 2025-01-13 15:14:30 +08:00
世界
d5e7af7a7e
Fix socks5 UDP implementation 2025-01-13 15:14:30 +08:00
世界
4d41f03bd5
clash-api: Fix missing endpoints 2025-01-13 15:14:30 +08:00
世界
30704a15a7
hysteria2: Add more masquerade options 2025-01-13 15:14:30 +08:00
世界
83889178ed
Improve timeouts 2025-01-13 15:14:30 +08:00
世界
1d2720bf5e
Add UDP timeout route option 2025-01-13 15:14:30 +08:00
世界
c4b6d0eadb
Make GSO adaptive 2025-01-13 15:14:30 +08:00
世界
0c66888691
Fix lint 2025-01-13 15:14:30 +08:00
世界
68781387fe
refactor: WireGuard endpoint 2025-01-13 15:14:30 +08:00
世界
fd299a0961
refactor: connection manager 2025-01-13 15:14:30 +08:00
世界
285a82050c
documentation: Fix typo 2025-01-13 15:14:30 +08:00
世界
2dbb8c55c9
Add override destination to route options 2025-01-13 15:14:30 +08:00
世界
effcf39469
Add dns.cache_capacity 2025-01-13 15:14:30 +08:00
世界
9db9484863
Refactor multi networks strategy 2025-01-13 15:14:30 +08:00
世界
ca813f461b
documentation: Remove unused titles 2025-01-13 15:14:30 +08:00
世界
bb46cdb2b3
Add multi network dialing 2025-01-13 15:14:30 +08:00
世界
dcb10c21a1
documentation: Merge route options to route actions 2025-01-13 15:14:29 +08:00
世界
05ea0ca00e
Add network_[type/is_expensive/is_constrained] rule items 2025-01-13 15:14:29 +08:00
世界
c098f282b1
Merge route options to route actions 2025-01-13 15:14:29 +08:00
世界
ecf82d197c
refactor: Platform Interfaces 2025-01-13 15:14:29 +08:00
世界
9afe75586a
refactor: Extract services form router 2025-01-13 15:14:29 +08:00
世界
a1be455202
refactor: Modular network manager 2025-01-13 15:14:29 +08:00
世界
19fb214226
refactor: Modular inbound/outbound manager 2025-01-13 15:14:29 +08:00
世界
28ec898a8c
documentation: Add rule action 2025-01-13 15:14:29 +08:00
世界
467b1bbeeb
documentation: Update the scheduled removal time of deprecated features 2025-01-13 15:14:29 +08:00
世界
02ab8ce806
documentation: Remove outdated icons 2025-01-13 15:14:29 +08:00
世界
ce69e620e9
Migrate bad options to library 2025-01-13 15:14:29 +08:00
世界
1133cf3ef5
Implement udp connect 2025-01-13 15:14:29 +08:00
世界
59a607e303
Implement new deprecated warnings 2025-01-13 15:14:29 +08:00
世界
313be3d7a4
Improve rule actions 2025-01-13 15:14:29 +08:00
世界
4fe40fcee0
Remove unused reject methods 2025-01-13 15:14:29 +08:00
世界
e233fd4fe5
refactor: Modular inbounds/outbounds 2025-01-13 15:14:29 +08:00
世界
9f7683818f
Implement dns-hijack 2025-01-13 15:14:29 +08:00
世界
179e3cb2f5
Implement resolve(server) 2025-01-13 15:14:29 +08:00
世界
41b960552d
Implement TCP and ICMP rejects 2025-01-13 15:14:29 +08:00
世界
8304295c48
Crazy sekai overturns the small pond 2025-01-13 15:14:29 +08:00
世界
253b41936e
Bump version 2025-01-11 20:58:00 +08:00
世界
ce5b4b06b5
auto-redirect: Fix fetch interfaces 2025-01-07 21:24:52 +08:00
世界
50f5006c43
Fix leak in reality server 2025-01-07 17:27:10 +08:00
世界
e42ff22c2e
quic: Fix source not unwrapped 2025-01-07 17:27:10 +08:00
世界
578571b972
Bump version 2025-01-02 15:21:13 +08:00
世界
935beca45d
Fix check interface 2025-01-01 12:19:56 +08:00
世界
3e246f1173
Fix vmess mux leak 2025-01-01 12:18:56 +08:00
世界
1bc27a32c2
Fix linter configuration 2025-01-01 12:18:56 +08:00
世界
bc2e3960e4
Enable fix stack for Android 7 and 9 2024-12-28 15:06:57 +08:00
世界
9c4ab0bf33
Bump version 2024-12-21 17:20:56 +08:00
世界
27bdef34c7
release: Fix create app store version 2024-12-21 17:20:56 +08:00
世界
3c00099ed4
Fix process rule check 2024-12-21 17:15:41 +08:00
世界
2babf07f9a
Fix wireguard 2024-12-21 16:07:19 +08:00
世界
4795ed712b
release: Fix android version 2024-12-21 16:07:19 +08:00
世界
d4cd564dbe
release: Fix check tag 2024-12-21 16:07:19 +08:00
世界
1676e13d3e
Bump version 2024-12-17 13:43:17 +08:00
世界
50576084c6
release: Add publish testflight 2024-12-17 13:43:17 +08:00
470 changed files with 28594 additions and 6792 deletions

30
.fpm_openwrt Normal file
View File

@ -0,0 +1,30 @@
-s dir
--name sing-box
--category net
--license GPL-3.0-or-later
--description "The universal proxy platform."
--url "https://sing-box.sagernet.org/"
--maintainer "nekohasekai <contact-git@sekai.icu>"
--no-deb-generate-changes
--config-files /etc/config/sing-box
--config-files /etc/sing-box/config.json
--depends ca-bundle
--depends kmod-inet-diag
--depends kmod-tun
--depends firewall4
--before-remove release/config/openwrt.prerm
release/config/config.json=/etc/sing-box/config.json
release/config/openwrt.conf=/etc/config/sing-box
release/config/openwrt.init=/etc/init.d/sing-box
release/config/openwrt.keep=/lib/upgrade/keep.d/sing-box
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
LICENSE=/usr/share/licenses/sing-box/LICENSE

25
.fpm_systemd Normal file
View File

@ -0,0 +1,25 @@
-s dir
--name sing-box
--category net
--license GPL-3.0-or-later
--description "The universal proxy platform."
--url "https://sing-box.sagernet.org/"
--maintainer "nekohasekai <contact-git@sekai.icu>"
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
--no-deb-generate-changes
--config-files /etc/sing-box/config.json
--after-install release/config/sing-box.postinst
release/config/config.json=/etc/sing-box/config.json
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
LICENSE=/usr/share/licenses/sing-box/LICENSE

28
.github/deb2ipk.sh vendored Executable file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# mod from https://gist.github.com/pldubouilh/c5703052986bfdd404005951dee54683
set -e -o pipefail
PROJECT=$(dirname "$0")/../..
TMP_PATH=`mktemp -d`
cp $2 $TMP_PATH
pushd $TMP_PATH
DEB_NAME=`ls *.deb`
ar x $DEB_NAME
mkdir control
pushd control
tar xf ../control.tar.gz
rm md5sums
sed "s/Architecture:\\ \w*/Architecture:\\ $1/g" ./control -i
cat control
tar czf ../control.tar.gz ./*
popd
DEB_NAME=${DEB_NAME%.deb}
tar czf $DEB_NAME.ipk control.tar.gz data.tar.gz debian-binary
popd
cp $TMP_PATH/$DEB_NAME.ipk $3
rm -r $TMP_PATH

25
.github/setup_legacy_go.sh vendored Executable file
View File

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

View File

@ -40,13 +40,13 @@ jobs:
version: ${{ steps.outputs.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.25.0
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@ -55,7 +55,7 @@ jobs:
- name: Calculate version
if: github.event_name != 'workflow_dispatch'
run: |-
go run -v ./cmd/internal/read_tag --nightly
go run -v ./cmd/internal/read_tag --ci --nightly
- name: Set outputs
id: outputs
run: |-
@ -69,140 +69,222 @@ jobs:
strategy:
matrix:
include:
- name: linux_386
goos: linux
goarch: 386
- name: linux_amd64
goos: linux
goarch: amd64
- name: linux_arm64
goos: linux
goarch: arm64
- name: linux_arm
goos: linux
goarch: arm
goarm: 6
- name: linux_arm_v7
goos: linux
goarch: arm
goarm: 7
- name: linux_s390x
goos: linux
goarch: s390x
- name: linux_riscv64
goos: linux
goarch: riscv64
- name: linux_mips64le
goos: linux
goarch: mips64le
- name: windows_amd64
goos: windows
goarch: amd64
require_legacy_go: true
- name: windows_386
goos: windows
goarch: 386
require_legacy_go: true
- name: windows_arm64
goos: windows
goarch: arm64
- name: darwin_arm64
goos: darwin
goarch: arm64
- name: darwin_amd64
goos: darwin
goarch: amd64
require_legacy_go: true
- name: android_arm64
goos: android
goarch: arm64
- name: android_arm
goos: android
goarch: arm
goarm: 7
- name: android_amd64
goos: android
goarch: amd64
- name: android_386
goos: android
goarch: 386
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
- { os: linux, arch: "386", go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
- { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" }
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" }
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" }
- { os: linux, arch: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" }
- { os: linux, arch: mipsle, gomips: hardfloat, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc_24kf" }
- { os: linux, arch: mipsle, gomips: softfloat, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
- { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" }
- { os: linux, arch: mips64le, gomips: hardfloat, debian: mips64el, rpm: mips64el }
- { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" }
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
- { os: windows, arch: amd64 }
- { os: windows, arch: amd64, legacy_go123: true, legacy_name: "windows-7" }
- { os: windows, arch: "386" }
- { os: windows, arch: "386", legacy_go123: true, legacy_name: "windows-7" }
- { os: windows, arch: arm64 }
- { os: darwin, arch: amd64 }
- { os: darwin, arch: arm64 }
- { os: darwin, arch: amd64, legacy_go124: true, legacy_name: "macos-11" }
- { os: android, arch: arm64, ndk: "aarch64-linux-android21" }
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" }
- { os: android, arch: amd64, ndk: "x86_64-linux-android21" }
- { os: android, arch: "386", ndk: "i686-linux-android21" }
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
if: ${{ ! (matrix.legacy_go123 || matrix.legacy_go124) }}
uses: actions/setup-go@v5
with:
go-version: ^1.23
- name: Cache legacy Go
if: matrix.require_legacy_go
go-version: ^1.25.0
- name: Setup Go 1.24
if: matrix.legacy_go124
uses: actions/setup-go@v5
with:
go-version: ~1.24.6
- name: Cache Go 1.23
if: matrix.legacy_go123
id: cache-legacy-go
uses: actions/cache@v4
with:
path: |
~/go/go1.20.14
key: go120
- name: Setup legacy Go
if: matrix.require_legacy_go == 'true' && steps.cache-legacy-go.outputs.cache-hit != 'true'
~/go/go_legacy
key: go_legacy_12312
- name: Setup Go 1.23
if: matrix.legacy_go123 && steps.cache-legacy-go.outputs.cache-hit != 'true'
run: |-
wget https://dl.google.com/go/go1.20.14.linux-amd64.tar.gz
tar -xzf go1.20.14.linux-amd64.tar.gz
mv go $HOME/go/go1.20.14
.github/setup_legacy_go.sh
- name: Setup Go 1.23
if: matrix.legacy_go123
run: |-
echo "PATH=$HOME/go/go_legacy/bin:$PATH" >> $GITHUB_ENV
echo "GOROOT=$HOME/go/go_legacy" >> $GITHUB_ENV
- name: Setup Android NDK
if: matrix.goos == 'android'
if: matrix.os == 'android'
uses: nttld/setup-ndk@v1
with:
ndk-version: r28-beta2
ndk-version: r28
local-cache: true
- name: Setup Goreleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: latest
install-only: true
- name: Extract signing key
run: |-
mkdir -p $HOME/.gnupg
cat > $HOME/.gnupg/sagernet.key <<EOF
${{ secrets.GPG_KEY }}
EOF
echo "HOME=$HOME" >> "$GITHUB_ENV"
- name: Set tag
run: |-
git tag v${{ needs.calculate_version.outputs.version }}
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build
if: matrix.goos != 'android'
run: |-
goreleaser release --clean --split
if: matrix.os != 'android'
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
./cmd/sing-box
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOPATH: ${{ env.HOME }}/go
CGO_ENABLED: "0"
GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.arch }}
GO386: ${{ matrix.go386 }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
GOMIPS64: ${{ matrix.gomips }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
- name: Build Android
if: matrix.goos == 'android'
run: |-
if: matrix.os == 'android'
run: |
set -xeuo pipefail
go install -v ./cmd/internal/build
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build goreleaser release --clean --split
export CC='${{ matrix.ndk }}-clang'
export CXX="${CC}++"
mkdir -p dist
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
./cmd/sing-box
env:
BUILD_GOOS: ${{ matrix.goos }}
BUILD_GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm }}
CGO_ENABLED: "1"
BUILD_GOOS: ${{ matrix.os }}
BUILD_GOARCH: ${{ matrix.arch }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
- name: Set name
run: |-
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}"
if [[ -n "${{ matrix.goarm }}" ]]; then
DIR_NAME="${DIR_NAME}v${{ matrix.goarm }}"
elif [[ -n "${{ matrix.go386 }}" && "${{ matrix.go386 }}" != 'sse2' ]]; then
DIR_NAME="${DIR_NAME}-${{ matrix.go386 }}"
elif [[ -n "${{ matrix.gomips }}" && "${{ matrix.gomips }}" != 'hardfloat' ]]; then
DIR_NAME="${DIR_NAME}-${{ matrix.gomips }}"
elif [[ -n "${{ matrix.legacy_name }}" ]]; then
DIR_NAME="${DIR_NAME}-legacy-${{ matrix.legacy_name }}"
fi
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
PKG_VERSION="${PKG_VERSION//-/\~}"
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
- name: Package DEB
if: matrix.debian != ''
run: |
set -xeuo pipefail
sudo gem install fpm
sudo apt-get update
sudo apt-get install -y debsigs
cp .fpm_systemd .fpm
fpm -t deb \
-v "$PKG_VERSION" \
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.debian }}.deb" \
--architecture ${{ matrix.debian }} \
dist/sing-box=/usr/bin/sing-box
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
rm -rf $HOME/.gnupg
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
${{ secrets.GPG_KEY }}
EOF
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
- name: Package RPM
if: matrix.rpm != ''
run: |-
set -xeuo pipefail
sudo gem install fpm
cp .fpm_systemd .fpm
fpm -t rpm \
-v "$PKG_VERSION" \
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.rpm }}.rpm" \
--architecture ${{ matrix.rpm }} \
dist/sing-box=/usr/bin/sing-box
cat > $HOME/.rpmmacros <<EOF
%_gpg_name ${{ secrets.GPG_KEY_ID }}
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
EOF
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
${{ secrets.GPG_KEY }}
EOF
rpmsign --addsign dist/*.rpm
- name: Package Pacman
if: matrix.pacman != ''
run: |-
set -xeuo pipefail
sudo gem install fpm
sudo apt-get update
sudo apt-get install -y libarchive-tools
cp .fpm_systemd .fpm
fpm -t pacman \
-v "$PKG_VERSION" \
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \
--architecture ${{ matrix.pacman }} \
dist/sing-box=/usr/bin/sing-box
- name: Package OpenWrt
if: matrix.openwrt != ''
run: |-
set -xeuo pipefail
sudo gem install fpm
cp .fpm_openwrt .fpm
fpm -t deb \
-v "$PKG_VERSION" \
-p "dist/openwrt.deb" \
--architecture all \
dist/sing-box=/usr/bin/sing-box
for architecture in ${{ matrix.openwrt }}; do
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk"
done
rm "dist/openwrt.deb"
- name: Archive
run: |
set -xeuo pipefail
cd dist
mkdir -p "${DIR_NAME}"
cp ../LICENSE "${DIR_NAME}"
if [ '${{ matrix.os }}' = 'windows' ]; then
cp sing-box "${DIR_NAME}/sing-box.exe"
zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
else
cp sing-box "${DIR_NAME}"
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
fi
rm -r "${DIR_NAME}"
- name: Cleanup
run: rm dist/sing-box
- name: Upload artifact
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.name }}
path: 'dist'
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_name && format('-legacy-{0}', matrix.legacy_name) }}
path: "dist"
build_android:
name: Build Android
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
@ -211,26 +293,27 @@ jobs:
- calculate_version
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
submodules: 'recursive'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.25.0
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
with:
ndk-version: r28-beta2
ndk-version: r28
- name: Setup OpenJDK
run: |-
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
/usr/lib/jvm/java-17-openjdk-amd64/bin/java --version
- name: Set tag
run: |-
git tag v${{ needs.calculate_version.outputs.version }}
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
- name: Build library
run: |-
make lib_install
@ -254,9 +337,16 @@ jobs:
with:
path: ~/.gradle
key: gradle-${{ hashFiles('**/*.gradle') }}
- name: Build
- name: Update version
if: github.event_name == 'workflow_dispatch'
run: |-
go run -v ./cmd/internal/update_android_version --ci
- name: Update nightly version
if: github.event_name != 'workflow_dispatch'
run: |-
go run -v ./cmd/internal/update_android_version --ci --nightly
- name: Build
run: |-
mkdir clients/android/app/libs
cp libbox.aar clients/android/app/libs
cd clients/android
@ -266,13 +356,11 @@ jobs:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
- name: Prepare upload
if: github.event_name == 'workflow_dispatch'
run: |-
mkdir -p dist/release
cp clients/android/app/build/outputs/apk/play/release/*.apk dist/release
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist/release
mkdir -p dist
cp clients/android/app/build/outputs/apk/play/release/*.apk dist
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist
- name: Upload artifact
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
with:
name: binary-android-apks
@ -285,26 +373,27 @@ jobs:
- calculate_version
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
submodules: 'recursive'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.25.0
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
with:
ndk-version: r28-beta2
ndk-version: r28
- name: Setup OpenJDK
run: |-
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
/usr/lib/jvm/java-17-openjdk-amd64/bin/java --version
- name: Set tag
run: |-
git tag v${{ needs.calculate_version.outputs.version }}
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
- name: Build library
run: |-
make lib_install
@ -381,7 +470,7 @@ jobs:
steps:
- name: Checkout
if: matrix.if
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
submodules: 'recursive'
@ -389,19 +478,20 @@ jobs:
if: matrix.if
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.25.0
- name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.1.app
sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Setup Xcode beta
if: matrix.if && github.ref == 'refs/heads/dev-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.2.app
sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Set tag
if: matrix.if
run: |-
git tag v${{ needs.calculate_version.outputs.version }}
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
- name: Checkout main branch
if: matrix.if && github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
@ -465,10 +555,13 @@ jobs:
MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version)
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION"
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV"
- name: Update version
if: matrix.if && matrix.name != 'iOS'
run: |-
go run -v ./cmd/internal/update_apple_version --ci
- name: Build
if: matrix.if
run: |-
go run -v ./cmd/internal/update_apple_version --ci
cd clients/apple
xcodebuild archive \
-scheme "${{ matrix.scheme }}" \
@ -491,6 +584,10 @@ jobs:
-authenticationKeyPath $ASC_KEY_PATH \
-authenticationKeyID $ASC_KEY_ID \
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
- name: Publish to TestFlight
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/dev-next'
run: |-
go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }}
- name: Build image
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
run: |-
@ -513,9 +610,9 @@ jobs:
zip -r SFM.dSYMs.zip dSYMs
popd
mkdir -p dist/release
cp clients/apple/SFM.dmg "dist/release/SFM-${VERSION}-universal.dmg"
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/release/SFM-${VERSION}-universal.dSYMs.zip"
mkdir -p dist
cp clients/apple/SFM.dmg "dist/SFM-${VERSION}-universal.dmg"
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/SFM-${VERSION}-universal.dSYMs.zip"
- name: Upload image
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
@ -524,7 +621,7 @@ jobs:
path: 'dist'
upload:
name: Upload builds
if: always() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')
if: "!failure() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')"
runs-on: ubuntu-latest
needs:
- calculate_version
@ -533,15 +630,9 @@ jobs:
- build_apple
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Goreleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: latest
install-only: true
- name: Cache ghr
uses: actions/cache@v4
id: cache-ghr
@ -558,25 +649,25 @@ jobs:
go install -v .
- name: Set tag
run: |-
git tag v${{ needs.calculate_version.outputs.version }}
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
- name: Download builds
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
path: dist
merge-multiple: true
- name: Merge builds
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
run: |-
goreleaser continue --merge --skip publish
mkdir -p dist/release
mv dist/*/sing-box*{tar.gz,zip,deb,rpm,_amd64.pkg.tar.zst,_arm64.pkg.tar.zst} dist/release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
- name: Upload builds
if: ${{ env.PUBLISHED == 'false' }}
run: |-
export PATH="$PATH:$HOME/go/bin"
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Replace builds
if: ${{ env.PUBLISHED != 'false' }}
run: |-
export PATH="$PATH:$HOME/go/bin"
ghr --replace -p 5 "v${VERSION}" dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -39,7 +39,7 @@ jobs:
echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: ${{ steps.ref.outputs.ref }}
fetch-depth: 0
@ -107,7 +107,7 @@ jobs:
echo "latest=$latest"
echo "latest=$latest" >> $GITHUB_OUTPUT
- name: Download digests
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
path: /tmp/digests
pattern: digests-*

View File

@ -22,16 +22,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ~1.24.6
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v8
with:
version: latest
args: --timeout=30m
install-mode: binary
verify: false

View File

@ -1,38 +1,189 @@
name: Release to Linux repository
name: Build Linux Packages
on:
workflow_dispatch:
inputs:
version:
description: "Version name"
required: true
type: string
forceBeta:
description: "Force beta"
required: false
type: boolean
default: false
release:
types:
- published
jobs:
build:
calculate_version:
name: Calculate version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.outputs.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
- name: Extract signing key
go-version: ^1.25.0
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
mkdir -p $HOME/.gnupg
cat > $HOME/.gnupg/sagernet.key <<EOF
echo "version=${{ inputs.version }}"
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
- name: Calculate version
if: github.event_name != 'workflow_dispatch'
run: |-
go run -v ./cmd/internal/read_tag --ci --nightly
- name: Set outputs
id: outputs
run: |-
echo "version=$version" >> "$GITHUB_OUTPUT"
build:
name: Build binary
runs-on: ubuntu-latest
needs:
- calculate_version
strategy:
matrix:
include:
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 }
- { os: linux, arch: "386", debian: i386, rpm: i386 }
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl }
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64 }
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64 }
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.0
- name: Setup Android NDK
if: matrix.os == 'android'
uses: nttld/setup-ndk@v1
with:
ndk-version: r28
local-cache: true
- name: Set tag
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
./cmd/sing-box
env:
CGO_ENABLED: "0"
GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set mtime
run: |-
TZ=UTC touch -t '197001010000' dist/sing-box
- name: Set name
if: (! contains(needs.calculate_version.outputs.version, '-')) && !inputs.forceBeta
run: |-
echo "NAME=sing-box" >> "$GITHUB_ENV"
- name: Set beta name
if: contains(needs.calculate_version.outputs.version, '-') || inputs.forceBeta
run: |-
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
- name: Set version
run: |-
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
PKG_VERSION="${PKG_VERSION//-/\~}"
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
- name: Package DEB
if: matrix.debian != ''
run: |
set -xeuo pipefail
sudo gem install fpm
sudo apt-get install -y debsigs
cp .fpm_systemd .fpm
fpm -t deb \
--name "${NAME}" \
-v "$PKG_VERSION" \
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
--architecture ${{ matrix.debian }} \
dist/sing-box=/usr/bin/sing-box
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
rm -rf $HOME/.gnupg
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
${{ secrets.GPG_KEY }}
EOF
echo "HOME=$HOME" >> "$GITHUB_ENV"
- name: Publish release
uses: goreleaser/goreleaser-action@v6
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
- name: Package RPM
if: matrix.rpm != ''
run: |-
set -xeuo pipefail
sudo gem install fpm
cp .fpm_systemd .fpm
fpm -t rpm \
--name "${NAME}" \
-v "$PKG_VERSION" \
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
--architecture ${{ matrix.rpm }} \
dist/sing-box=/usr/bin/sing-box
cat > $HOME/.rpmmacros <<EOF
%_gpg_name ${{ secrets.GPG_KEY_ID }}
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
EOF
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
${{ secrets.GPG_KEY }}
EOF
rpmsign --addsign dist/*.rpm
- name: Cleanup
run: rm dist/sing-box
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
distribution: goreleaser-pro
version: latest
args: release -f .goreleaser.fury.yaml --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.legacy_go && '-legacy' || '' }}
path: "dist"
upload:
name: Upload builds
runs-on: ubuntu-latest
needs:
- calculate_version
- build
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Set tag
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
- name: Download builds
uses: actions/download-artifact@v5
with:
path: dist
merge-multiple: true
- name: Publish packages
run: |-
ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/

View File

@ -1,28 +1,59 @@
version: "2"
run:
go: "1.24"
build-tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_utls
- with_acme
- with_clash_api
linters:
disable-all: true
default: none
enable:
- gofumpt
- govet
- gci
- staticcheck
- paralleltest
- ineffassign
linters-settings:
- paralleltest
- staticcheck
settings:
staticcheck:
checks:
- all
- -S1000
- -S1008
- -S1017
- -ST1003
- -QF1001
- -QF1003
- -QF1008
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- transport/simple-obfs
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofumpt
settings:
gci:
custom-order: true
sections:
- standard
- prefix(github.com/sagernet/)
- default
staticcheck:
checks:
- all
- -SA1003
run:
go: "1.23"
issues:
exclude-dirs:
custom-order: true
exclusions:
generated: lax
paths:
- transport/simple-obfs
- third_party$
- builtin$
- examples$

View File

@ -6,17 +6,18 @@ builds:
- -v
- -trimpath
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:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_ech
- with_utls
- with_reality_server
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
targets:
@ -48,12 +49,18 @@ nfpms:
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: config
type: "config|noreplace"
- src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service
- src: release/config/sing-box.sysusers
dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash

View File

@ -16,13 +16,13 @@ builds:
- with_quic
- with_dhcp
- with_wireguard
- with_ech
- with_utls
- with_reality_server
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
- GOTOOLCHAIN=local
targets:
- linux_386
- linux_amd64_v1
@ -46,21 +46,21 @@ builds:
- with_dhcp
- with_wireguard
- with_utls
- with_reality_server
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
- GOROOT={{ .Env.GOPATH }}/go1.20.14
gobinary: "{{ .Env.GOPATH }}/go1.20.14/bin/go"
- GOROOT={{ .Env.GOPATH }}/go_legacy
tool: "{{ .Env.GOPATH }}/go_legacy/bin/go"
targets:
- windows_amd64_v1
- windows_386
- darwin_amd64_v1
- id: android
<<: *template
env:
- CGO_ENABLED=1
- GOTOOLCHAIN=local
overrides:
- goos: android
goarch: arm
@ -95,10 +95,12 @@ archives:
builds:
- main
- android
format: tar.gz
formats:
- tar.gz
format_overrides:
- goos: windows
format: zip
formats:
- zip
wrap_in_directory: true
files:
- LICENSE
@ -128,12 +130,18 @@ nfpms:
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: config
type: "config|noreplace"
- src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service
- src: release/config/sing-box.sysusers
dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash

View File

@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
COPY . /go/src/github.com/sagernet/sing-box
WORKDIR /go/src/github.com/sagernet/sing-box
@ -13,7 +13,7 @@ RUN set -ex \
&& export COMMIT=$(git rev-parse --short HEAD) \
&& export VERSION=$(go run ./cmd/internal/read_tag) \
&& go build -v -trimpath -tags \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale" \
-o /go/bin/sing-box \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
./cmd/sing-box

View File

@ -1,34 +1,29 @@
NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD)
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
TAGS_GO121 = with_ech
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 ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale
GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
MAIN_PARAMS = $(PARAMS) -tags $(TAGS)
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid= -checklinkname=0"
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
MAIN = ./cmd/sing-box
PREFIX ?= $(shell go env GOPATH)
.PHONY: test release docs build
build:
export GOTOOLCHAIN=local && \
go build $(MAIN_PARAMS) $(MAIN)
ci_build_go120:
go build $(PARAMS) $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN)
ci_build:
go build $(PARAMS) $(MAIN)
export GOTOOLCHAIN=local && \
go build $(PARAMS) $(MAIN) && \
go build $(MAIN_PARAMS) $(MAIN)
generate_completions:
go run -v --tags generate,generate_completions $(MAIN)
go run -v --tags "$(TAGS),generate,generate_completions" $(MAIN)
install:
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
@ -50,7 +45,7 @@ lint:
GOOS=freebsd golangci-lint run ./...
lint_install:
go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
proto:
@go run ./cmd/internal/protogen
@ -61,6 +56,9 @@ proto_install:
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
update_certificates:
go run ./cmd/internal/update_certificates
release:
go run ./cmd/internal/build goreleaser release --clean --skip publish
mkdir dist/release
@ -110,6 +108,16 @@ upload_ios_app_store:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
export_ios_ipa:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Export.plist -allowProvisioningUpdates -exportPath build/SFI && \
cp build/SFI/sing-box.ipa dist/SFI.ipa
upload_ios_ipa:
cd dist && \
cp SFI.ipa "SFI-${VERSION}.ipa" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFI-${VERSION}.ipa"
release_ios: build_ios upload_ios_app_store
build_macos:
@ -177,6 +185,16 @@ upload_tvos_app_store:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
export_tvos_ipa:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Export.plist -allowProvisioningUpdates -exportPath build/SFT && \
cp build/SFT/sing-box.ipa dist/SFT.ipa
upload_tvos_ipa:
cd dist && \
cp SFT.ipa "SFT-${VERSION}.ipa" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFT-${VERSION}.ipa"
release_tvos: build_tvos upload_tvos_app_store
update_apple_version:
@ -227,8 +245,8 @@ lib:
go run ./cmd/internal/build_libbox -target ios
lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.4
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.4
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.8
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.8
docs:
venv/bin/mkdocs serve

21
adapter/certificate.go Normal file
View File

@ -0,0 +1,21 @@
package adapter
import (
"context"
"crypto/x509"
"github.com/sagernet/sing/service"
)
type CertificateStore interface {
LifecycleService
Pool() *x509.CertPool
}
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
store := service.FromContext[CertificateStore](ctx)
if store == nil {
return nil
}
return store.Pool()
}

94
adapter/dns.go Normal file
View File

@ -0,0 +1,94 @@
package adapter
import (
"context"
"net/netip"
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/logger"
"github.com/sagernet/sing/service"
"github.com/miekg/dns"
)
type DNSRouter interface {
Lifecycle
Exchange(ctx context.Context, message *dns.Msg, options DNSQueryOptions) (*dns.Msg, error)
Lookup(ctx context.Context, domain string, options DNSQueryOptions) ([]netip.Addr, error)
ClearCache()
LookupReverseMapping(ip netip.Addr) (string, bool)
ResetNetwork()
}
type DNSClient interface {
Start()
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
ClearCache()
}
type DNSQueryOptions struct {
Transport DNSTransport
Strategy C.DomainStrategy
LookupStrategy C.DomainStrategy
DisableCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
}
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
if options == nil {
return &DNSQueryOptions{}, nil
}
transportManager := service.FromContext[DNSTransportManager](ctx)
transport, loaded := transportManager.Transport(options.Server)
if !loaded {
return nil, E.New("domain resolver not found: " + options.Server)
}
return &DNSQueryOptions{
Transport: transport,
Strategy: C.DomainStrategy(options.Strategy),
DisableCache: options.DisableCache,
RewriteTTL: options.RewriteTTL,
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
}, nil
}
type RDRCStore interface {
LoadRDRC(transportName string, qName string, qType uint16) (rejected bool)
SaveRDRC(transportName string, qName string, qType uint16) error
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
}
type DNSTransport interface {
Lifecycle
Type() string
Tag() string
Dependencies() []string
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
}
type LegacyDNSTransport interface {
LegacyStrategy() C.DomainStrategy
LegacyClientSubnet() netip.Prefix
}
type DNSTransportRegistry interface {
option.DNSTransportOptionsRegistry
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
}
type DNSTransportManager interface {
Lifecycle
Transports() []DNSTransport
Transport(tag string) (DNSTransport, bool)
Default() DNSTransport
FakeIP() FakeIPTransport
Remove(tag string) error
Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error
}

View File

@ -6,8 +6,6 @@ import (
"encoding/binary"
"time"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common/varbin"
)
@ -16,7 +14,20 @@ type ClashServer interface {
ConnectionTracker
Mode() string
ModeList() []string
HistoryStorage() *urltest.HistoryStorage
HistoryStorage() URLTestHistoryStorage
}
type URLTestHistory struct {
Time time.Time `json:"time"`
Delay uint16 `json:"delay"`
}
type URLTestHistoryStorage interface {
SetHook(hook chan<- struct{})
LoadURLTestHistory(tag string) *URLTestHistory
DeleteURLTestHistory(tag string)
StoreURLTestHistory(tag string, history *URLTestHistory)
Close() error
}
type V2RayServer interface {
@ -31,7 +42,7 @@ type CacheFile interface {
FakeIPStorage
StoreRDRC() bool
dns.RDRCStore
RDRCStore
LoadMode() string
StoreMode(mode string) error
@ -39,17 +50,17 @@ type CacheFile interface {
StoreSelected(group string, selected string) error
LoadGroupExpand(group string) (isExpand bool, loaded bool)
StoreGroupExpand(group string, expand bool) error
LoadRuleSet(tag string) *SavedRuleSet
SaveRuleSet(tag string, set *SavedRuleSet) error
LoadRuleSet(tag string) *SavedBinary
SaveRuleSet(tag string, set *SavedBinary) error
}
type SavedRuleSet struct {
type SavedBinary struct {
Content []byte
LastUpdated time.Time
LastEtag string
}
func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
func (s *SavedBinary) MarshalBinary() ([]byte, error) {
var buffer bytes.Buffer
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
if err != nil {
@ -70,7 +81,7 @@ func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
return buffer.Bytes(), nil
}
func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
func (s *SavedBinary) UnmarshalBinary(data []byte) error {
reader := bytes.NewReader(data)
var version uint8
err := binary.Read(reader, binary.BigEndian, &version)

View File

@ -3,12 +3,11 @@ package adapter
import (
"net/netip"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common/logger"
)
type FakeIPStore interface {
Service
SimpleLifecycle
Contains(address netip.Addr) bool
Create(domain string, isIPv6 bool) (netip.Addr, error)
Lookup(address netip.Addr) (string, bool)
@ -27,6 +26,6 @@ type FakeIPStorage interface {
}
type FakeIPTransport interface {
dns.Transport
DNSTransport
Store() FakeIPStore
}

View File

@ -57,6 +57,7 @@ type InboundContext struct {
Domain string
Client string
SniffContext any
SniffError error
// cache
@ -71,14 +72,15 @@ type InboundContext struct {
UDPDisableDomainUnmapping bool
UDPConnect bool
UDPTimeout time.Duration
TLSFragment bool
TLSFragmentFallbackDelay time.Duration
TLSRecordFragment bool
NetworkStrategy *C.NetworkStrategy
NetworkType []C.InterfaceType
FallbackNetworkType []C.InterfaceType
FallbackDelay time.Duration
DNSServer string
DestinationAddresses []netip.Addr
SourceGeoIPCode string
GeoIPCode string
@ -133,8 +135,7 @@ func ExtendContext(ctx context.Context) (context.Context, *InboundContext) {
func OverrideContext(ctx context.Context) context.Context {
if metadata := ContextFrom(ctx); metadata != nil {
var newMetadata InboundContext
newMetadata = *metadata
newMetadata := *metadata
return WithContext(ctx, &newMetadata)
}
return ctx

View File

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

View File

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

View File

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

View File

@ -20,20 +20,24 @@ type NetworkManager interface {
DefaultOptions() NetworkOptions
RegisterAutoRedirectOutputMark(mark uint32) error
AutoRedirectOutputMark() uint32
AutoRedirectOutputMarkFunc() control.Func
NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager
WIFIState() WIFIState
ResetNetwork()
UpdateWIFIState()
}
type NetworkOptions struct {
BindInterface string
RoutingMark uint32
DomainResolver string
DomainResolveOptions DNSQueryOptions
NetworkStrategy *C.NetworkStrategy
NetworkType []C.InterfaceType
FallbackNetworkType []C.InterfaceType
FallbackDelay time.Duration
BindInterface string
RoutingMark uint32
}
type InterfaceUpdateListener interface {

View File

@ -2,9 +2,12 @@ package adapter
import (
"context"
"net/netip"
"time"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
N "github.com/sagernet/sing/common/network"
)
@ -18,6 +21,17 @@ type Outbound interface {
N.Dialer
}
type OutboundWithPreferredRoutes interface {
Outbound
PreferredDomain(domain string) bool
PreferredAddress(address netip.Addr) bool
}
type DirectRouteOutbound interface {
Outbound
NewDirectRouteConnection(metadata InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
}
type OutboundRegistry interface {
option.OutboundOptionsRegistry
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)

View File

@ -23,14 +23,14 @@ type Manager struct {
registry adapter.OutboundRegistry
endpoint adapter.EndpointManager
defaultTag string
access sync.Mutex
access sync.RWMutex
started bool
stage adapter.StartStage
outbounds []adapter.Outbound
outboundByTag map[string]adapter.Outbound
dependByTag map[string][]string
defaultOutbound adapter.Outbound
defaultOutboundFallback adapter.Outbound
defaultOutboundFallback func() (adapter.Outbound, error)
}
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
@ -44,7 +44,7 @@ func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry,
}
}
func (m *Manager) Initialize(defaultOutboundFallback adapter.Outbound) {
func (m *Manager) Initialize(defaultOutboundFallback func() (adapter.Outbound, error)) {
m.defaultOutboundFallback = defaultOutboundFallback
}
@ -55,18 +55,31 @@ func (m *Manager) Start(stage adapter.StartStage) error {
}
m.started = true
m.stage = stage
outbounds := m.outbounds
m.access.Unlock()
if stage == adapter.StartStateStart {
if m.defaultTag != "" && m.defaultOutbound == nil {
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
if !loaded {
m.access.Unlock()
return E.New("default outbound not found: ", m.defaultTag)
}
m.defaultOutbound = defaultEndpoint
}
if m.defaultOutbound == nil {
directOutbound, err := m.defaultOutboundFallback()
if err != nil {
m.access.Unlock()
return E.Cause(err, "create direct outbound for fallback")
}
m.outbounds = append(m.outbounds, directOutbound)
m.outboundByTag[directOutbound.Tag()] = directOutbound
m.defaultOutbound = directOutbound
}
outbounds := m.outbounds
m.access.Unlock()
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
} else {
outbounds := m.outbounds
m.access.Unlock()
for _, outbound := range outbounds {
err := adapter.LegacyStart(outbound, stage)
if err != nil {
@ -169,15 +182,15 @@ func (m *Manager) Close() error {
}
func (m *Manager) Outbounds() []adapter.Outbound {
m.access.Lock()
defer m.access.Unlock()
m.access.RLock()
defer m.access.RUnlock()
return m.outbounds
}
func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
m.access.Lock()
m.access.RLock()
outbound, found := m.outboundByTag[tag]
m.access.Unlock()
m.access.RUnlock()
if found {
return outbound, true
}
@ -185,20 +198,16 @@ func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
}
func (m *Manager) Default() adapter.Outbound {
m.access.Lock()
defer m.access.Unlock()
if m.defaultOutbound != nil {
m.access.RLock()
defer m.access.RUnlock()
return m.defaultOutbound
} else {
return m.defaultOutboundFallback
}
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
defer m.access.Unlock()
outbound, found := m.outboundByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.outboundByTag, tag)
@ -232,7 +241,6 @@ func (m *Manager) Remove(tag string) error {
})
}
}
m.access.Unlock()
if started {
return common.Close(outbound)
}
@ -247,8 +255,6 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(outbound, stage)
@ -257,6 +263,8 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
}
}
}
m.access.Lock()
defer m.access.Unlock()
if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
if m.started {
err = common.Close(existsOutbound)

View File

@ -2,44 +2,31 @@ package adapter
import (
"context"
"crypto/tls"
"net"
"net/http"
"net/netip"
"sync"
"time"
"github.com/sagernet/sing-box/common/geoip"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing-tun"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/common/x/list"
mdns "github.com/miekg/dns"
"go4.org/netipx"
)
type Router interface {
Lifecycle
FakeIPStore() FakeIPStore
ConnectionRouter
PreMatch(metadata InboundContext) error
PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
ConnectionRouterEx
GeoIPReader() *geoip.Reader
LoadGeosite(code string) (Rule, error)
RuleSet(tag string) (RuleSet, bool)
NeedWIFIState() bool
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
ClearDNSCache()
Rules() []Rule
SetTracker(tracker ConnectionTracker)
AppendTracker(tracker ConnectionTracker)
ResetNetwork()
}
@ -83,12 +70,14 @@ type RuleSetMetadata struct {
ContainsIPCIDRRule bool
}
type HTTPStartContext struct {
ctx context.Context
access sync.Mutex
httpClientCache map[string]*http.Client
}
func NewHTTPStartContext() *HTTPStartContext {
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
return &HTTPStartContext{
ctx: ctx,
httpClientCache: make(map[string]*http.Client),
}
}
@ -106,6 +95,10 @@ func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Clie
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
TLSClientConfig: &tls.Config{
Time: ntp.TimeFuncFromContext(c.ctx),
RootCAs: RootPoolFromContext(c.ctx),
},
},
}
c.httpClientCache[detour] = httpClient

View File

@ -11,9 +11,8 @@ type HeadlessRule interface {
type Rule interface {
HeadlessRule
Service
SimpleLifecycle
Type() string
UpdateGeosite() error
Action() RuleAction
}

View File

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

View File

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

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

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

View File

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

18
adapter/ssm.go Normal file
View File

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

View File

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

174
box.go
View File

@ -12,10 +12,14 @@ import (
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound"
boxService "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/common/certificate"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/taskmonitor"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport/local"
"github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/cachefile"
"github.com/sagernet/sing-box/experimental/libbox/platform"
@ -31,7 +35,7 @@ import (
"github.com/sagernet/sing/service/pause"
)
var _ adapter.Service = (*Box)(nil)
var _ adapter.SimpleLifecycle = (*Box)(nil)
type Box struct {
createdAt time.Time
@ -41,9 +45,12 @@ type Box struct {
endpoint *endpoint.Manager
inbound *inbound.Manager
outbound *outbound.Manager
service *boxService.Manager
dnsTransport *dns.TransportManager
dnsRouter *dns.Router
connection *route.ConnectionManager
router *route.Router
services []adapter.LifecycleService
internalService []adapter.LifecycleService
done chan struct{}
}
@ -58,6 +65,8 @@ func Context(
inboundRegistry adapter.InboundRegistry,
outboundRegistry adapter.OutboundRegistry,
endpointRegistry adapter.EndpointRegistry,
dnsTransportRegistry adapter.DNSTransportRegistry,
serviceRegistry adapter.ServiceRegistry,
) context.Context {
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
@ -74,6 +83,14 @@ func Context(
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
}
if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil {
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
}
if service.FromContext[adapter.ServiceRegistry](ctx) == nil {
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
}
return ctx
}
@ -88,6 +105,8 @@ func New(options Options) (*Box, error) {
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
if endpointRegistry == nil {
return nil, E.New("missing endpoint registry in context")
@ -98,6 +117,12 @@ func New(options Options) (*Box, error) {
if outboundRegistry == nil {
return nil, E.New("missing outbound registry in context")
}
if dnsTransportRegistry == nil {
return nil, E.New("missing DNS transport registry in context")
}
if serviceRegistry == nil {
return nil, E.New("missing service registry in context")
}
ctx = pause.WithDefaultManager(ctx)
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
@ -131,14 +156,34 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "create log factory")
}
var internalServices []adapter.LifecycleService
certificateOptions := common.PtrValueOrDefault(options.Certificate)
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
len(certificateOptions.Certificate) > 0 ||
len(certificateOptions.CertificatePath) > 0 ||
len(certificateOptions.CertificateDirectoryPath) > 0 {
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
if err != nil {
return nil, err
}
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
internalServices = append(internalServices, certificateStore)
}
routeOptions := common.PtrValueOrDefault(options.Route)
dnsOptions := common.PtrValueOrDefault(options.DNS)
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
if err != nil {
return nil, E.Cause(err, "initialize network manager")
@ -146,18 +191,40 @@ func New(options Options) (*Box, error) {
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
router, err := route.NewRouter(ctx, logFactory, routeOptions, common.PtrValueOrDefault(options.DNS))
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
service.MustRegister[adapter.Router](ctx, router)
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
if err != nil {
return nil, E.Cause(err, "initialize router")
}
ntpOptions := common.PtrValueOrDefault(options.NTP)
var timeService *tls.TimeServiceWrapper
if ntpOptions.Enabled {
timeService = new(tls.TimeServiceWrapper)
service.MustRegister[ntp.TimeService](ctx, timeService)
}
for i, transportOptions := range dnsOptions.Servers {
var tag string
if transportOptions.Tag != "" {
tag = transportOptions.Tag
} else {
tag = F.ToString(i)
}
err = dnsTransportManager.Create(
ctx,
logFactory.NewLogger(F.ToString("dns/", transportOptions.Type, "[", tag, "]")),
tag,
transportOptions.Type,
transportOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize DNS server[", i, "]")
}
}
err = dnsRouter.Initialize(dnsOptions.Rules)
if err != nil {
return nil, E.Cause(err, "initialize dns router")
}
for i, endpointOptions := range options.Endpoints {
var tag string
if endpointOptions.Tag != "" {
@ -165,7 +232,15 @@ func New(options Options) (*Box, error) {
} else {
tag = F.ToString(i)
}
err = endpointManager.Create(ctx,
endpointCtx := ctx
if tag != "" {
// TODO: remove this
endpointCtx = adapter.WithContext(endpointCtx, &adapter.InboundContext{
Outbound: tag,
})
}
err = endpointManager.Create(
endpointCtx,
router,
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
tag,
@ -173,7 +248,7 @@ func New(options Options) (*Box, error) {
endpointOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize inbound[", i, "]")
return nil, E.Cause(err, "initialize endpoint[", i, "]")
}
}
for i, inboundOptions := range options.Inbounds {
@ -183,7 +258,8 @@ func New(options Options) (*Box, error) {
} else {
tag = F.ToString(i)
}
err = inboundManager.Create(ctx,
err = inboundManager.Create(
ctx,
router,
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
tag,
@ -220,26 +296,51 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize outbound[", i, "]")
}
}
outboundManager.Initialize(common.Must1(
direct.NewOutbound(
for i, serviceOptions := range options.Services {
var tag string
if serviceOptions.Tag != "" {
tag = serviceOptions.Tag
} else {
tag = F.ToString(i)
}
err = serviceManager.Create(
ctx,
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
tag,
serviceOptions.Type,
serviceOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize service[", i, "]")
}
}
outboundManager.Initialize(func() (adapter.Outbound, error) {
return direct.NewOutbound(
ctx,
router,
logFactory.NewLogger("outbound/direct"),
"direct",
option.DirectOutboundOptions{},
),
))
)
})
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
return local.NewTransport(
ctx,
logFactory.NewLogger("dns/local"),
"local",
option.LocalDNSServerOptions{},
)
})
if platformInterface != nil {
err = platformInterface.Initialize(networkManager)
if err != nil {
return nil, E.Cause(err, "initialize platform interface")
}
}
var services []adapter.LifecycleService
if needCacheFile {
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
services = append(services, cacheFile)
internalServices = append(internalServices, cacheFile)
}
if needClashAPI {
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
@ -248,9 +349,9 @@ func New(options Options) (*Box, error) {
if err != nil {
return nil, E.Cause(err, "create clash-server")
}
router.SetTracker(clashServer)
router.AppendTracker(clashServer)
service.MustRegister[adapter.ClashServer](ctx, clashServer)
services = append(services, clashServer)
internalServices = append(internalServices, clashServer)
}
if needV2RayAPI {
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
@ -258,13 +359,13 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "create v2ray-server")
}
if v2rayServer.StatsService() != nil {
router.SetTracker(v2rayServer.StatsService())
services = append(services, v2rayServer)
router.AppendTracker(v2rayServer.StatsService())
internalServices = append(internalServices, v2rayServer)
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
}
}
if ntpOptions.Enabled {
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions)
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain())
if err != nil {
return nil, E.Cause(err, "create NTP service")
}
@ -277,19 +378,22 @@ func New(options Options) (*Box, error) {
WriteToSystem: ntpOptions.WriteToSystem,
})
timeService.TimeService = ntpService
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
}
return &Box{
network: networkManager,
endpoint: endpointManager,
inbound: inboundManager,
outbound: outboundManager,
dnsTransport: dnsTransportManager,
service: serviceManager,
dnsRouter: dnsRouter,
connection: connectionManager,
router: router,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
services: services,
internalService: internalServices,
done: make(chan struct{}),
}, nil
}
@ -340,15 +444,15 @@ func (s *Box) preStart() error {
if err != nil {
return E.Cause(err, "start logger")
}
err = adapter.StartNamed(adapter.StartStateInitialize, s.services) // cache-file clash-api v2ray-api
err = adapter.StartNamed(adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateInitialize, s.network, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateStart, s.outbound, s.network, s.connection, s.router)
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
if err != nil {
return err
}
@ -360,31 +464,27 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.StartNamed(adapter.StartStateStart, s.services)
err = adapter.StartNamed(adapter.StartStateStart, s.internalService)
if err != nil {
return err
}
err = s.inbound.Start(adapter.StartStateStart)
err = adapter.Start(adapter.StartStateStart, s.inbound, s.endpoint, s.service)
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateStart, s.endpoint)
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
if err != nil {
return err
}
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.connection, s.router, s.inbound, s.endpoint)
err = adapter.StartNamed(adapter.StartStatePostStart, s.internalService)
if err != nil {
return err
}
err = adapter.StartNamed(adapter.StartStatePostStart, s.services)
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateStarted, s.network, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
if err != nil {
return err
}
err = adapter.StartNamed(adapter.StartStateStarted, s.services)
err = adapter.StartNamed(adapter.StartStateStarted, s.internalService)
if err != nil {
return err
}
@ -399,9 +499,9 @@ func (s *Box) Close() error {
close(s.done)
}
err := common.Close(
s.inbound, s.outbound, s.router, s.connection, s.network,
s.service, s.endpoint, s.inbound, s.outbound, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
)
for _, lifecycleService := range s.services {
for _, lifecycleService := range s.internalService {
err = E.Append(err, lifecycleService.Close(), func(err error) error {
return E.Cause(err, "close ", lifecycleService.Name())
})

@ -1 +1 @@
Subproject commit 13fe81af5fc74e09b844fa70ab9d1a4affa45adc
Subproject commit 9d71ede01a1a51bb459847bb5d4444b2846c77de

@ -1 +1 @@
Subproject commit 835108e41108a03770e008e4ad5bb9843d79e029
Subproject commit c5734677bdfcba5d2d4faf10c4f10475077a82ab

View File

@ -1,30 +0,0 @@
package main
import (
"context"
"fmt"
_ "unsafe"
"github.com/cidertool/asc-go/asc"
)
type Client struct {
*asc.Client
}
func (c *Client) UpdateBuildForAppStoreVersion(ctx context.Context, id string, buildID *string) (*asc.Response, error) {
linkage := newRelationshipDeclaration(buildID, "builds")
url := fmt.Sprintf("appStoreVersions/%s/relationships/build", id)
return c.patch(ctx, url, newRequestBody(linkage), nil)
}
func newRelationshipDeclaration(id *string, relationshipType string) *asc.RelationshipData {
if id == nil {
return nil
}
return &asc.RelationshipData{
ID: *id,
Type: relationshipType,
}
}

View File

@ -1,140 +0,0 @@
package main
import (
"context"
"net/http"
"net/url"
"reflect"
_ "unsafe"
"github.com/cidertool/asc-go/asc"
"github.com/google/go-querystring/query"
)
func (c *Client) newRequest(ctx context.Context, method string, path string, body *requestBody, options ...requestOption) (*http.Request, error) {
return clientNewRequest(c.Client, ctx, method, path, body, options...)
}
//go:linkname clientNewRequest github.com/cidertool/asc-go/asc.(*Client).newRequest
func clientNewRequest(c *asc.Client, ctx context.Context, method string, path string, body *requestBody, options ...requestOption) (*http.Request, error)
func (c *Client) do(ctx context.Context, req *http.Request, v interface{}) (*asc.Response, error) {
return clientDo(c.Client, ctx, req, v)
}
//go:linkname clientDo github.com/cidertool/asc-go/asc.(*Client).do
func clientDo(c *asc.Client, ctx context.Context, req *http.Request, v interface{}) (*asc.Response, error)
// get sends a GET request to the API as configured.
func (c *Client) get(ctx context.Context, url string, query interface{}, v interface{}, options ...requestOption) (*asc.Response, error) {
var err error
if query != nil {
url, err = appendingQueryOptions(url, query)
if err != nil {
return nil, err
}
}
req, err := c.newRequest(ctx, "GET", url, nil, options...)
if err != nil {
return nil, err
}
resp, err := c.do(ctx, req, v)
if err != nil {
return resp, err
}
return resp, err
}
// post sends a POST request to the API as configured.
func (c *Client) post(ctx context.Context, url string, body *requestBody, v interface{}) (*asc.Response, error) {
req, err := c.newRequest(ctx, "POST", url, body, withContentType("application/json"))
if err != nil {
return nil, err
}
resp, err := c.do(ctx, req, v)
if err != nil {
return resp, err
}
return resp, err
}
// patch sends a PATCH request to the API as configured.
func (c *Client) patch(ctx context.Context, url string, body *requestBody, v interface{}) (*asc.Response, error) {
req, err := c.newRequest(ctx, "PATCH", url, body, withContentType("application/json"))
if err != nil {
return nil, err
}
resp, err := c.do(ctx, req, v)
if err != nil {
return resp, err
}
return resp, err
}
// delete sends a DELETE request to the API as configured.
func (c *Client) delete(ctx context.Context, url string, body *requestBody) (*asc.Response, error) {
req, err := c.newRequest(ctx, "DELETE", url, body, withContentType("application/json"))
if err != nil {
return nil, err
}
return c.do(ctx, req, nil)
}
// request is a common structure for a request body sent to the API.
type requestBody struct {
Data interface{} `json:"data"`
Included interface{} `json:"included,omitempty"`
}
func newRequestBody(data interface{}) *requestBody {
return newRequestBodyWithIncluded(data, nil)
}
func newRequestBodyWithIncluded(data interface{}, included interface{}) *requestBody {
return &requestBody{Data: data, Included: included}
}
type requestOption func(*http.Request)
func withAccept(typ string) requestOption {
return func(req *http.Request) {
req.Header.Set("Accept", typ)
}
}
func withContentType(typ string) requestOption {
return func(req *http.Request) {
req.Header.Set("Content-Type", typ)
}
}
// AddOptions adds the parameters in opt as URL query parameters to s. opt
// must be a struct whose fields may contain "url" tags.
func appendingQueryOptions(s string, opt interface{}) (string, error) {
v := reflect.ValueOf(opt)
if v.Kind() == reflect.Ptr && v.IsNil() {
return s, nil
}
u, err := url.Parse(s)
if err != nil {
return s, err
}
qs, err := query.Values(opt)
if err != nil {
return s, err
}
u.RawQuery = qs.Encode()
return u.String(), nil
}

View File

@ -5,15 +5,15 @@ import (
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/sagernet/asc-go/asc"
"github.com/sagernet/sing-box/cmd/internal/build_shared"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/cidertool/asc-go/asc"
)
func main() {
@ -54,20 +54,20 @@ const (
groupID = "5c5f3b78-b7a0-40c0-bcad-e6ef87bbefda"
)
func createClient() *Client {
func createClient(expireDuration time.Duration) *asc.Client {
privateKey, err := os.ReadFile(os.Getenv("ASC_KEY_PATH"))
if err != nil {
log.Fatal(err)
}
tokenConfig, err := asc.NewTokenConfig(os.Getenv("ASC_KEY_ID"), os.Getenv("ASC_KEY_ISSUER_ID"), time.Minute, privateKey)
tokenConfig, err := asc.NewTokenConfig(os.Getenv("ASC_KEY_ID"), os.Getenv("ASC_KEY_ISSUER_ID"), expireDuration, privateKey)
if err != nil {
log.Fatal(err)
}
return &Client{asc.NewClient(tokenConfig.Client())}
return asc.NewClient(tokenConfig.Client())
}
func fetchMacOSVersion(ctx context.Context) error {
client := createClient()
client := createClient(time.Minute)
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{"MAC_OS"},
})
@ -105,21 +105,38 @@ func publishTestflight(ctx context.Context) error {
return err
}
tag := tagVersion.VersionString()
client := createClient()
client := createClient(20 * time.Minute)
log.Info(tag, " list build IDs")
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
if err != nil {
return err
}
buildIDS := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
buildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
return it.ID
})
for _, platform := range []asc.Platform{
var platforms []asc.Platform
if len(os.Args) == 3 {
switch os.Args[2] {
case "ios":
platforms = []asc.Platform{asc.PlatformIOS}
case "macos":
platforms = []asc.Platform{asc.PlatformMACOS}
case "tvos":
platforms = []asc.Platform{asc.PlatformTVOS}
default:
return E.New("unknown platform: ", os.Args[2])
}
} else {
platforms = []asc.Platform{
asc.PlatformIOS,
asc.PlatformMACOS,
asc.PlatformTVOS,
} {
}
}
for _, platform := range platforms {
log.Info(string(platform), " list builds")
for {
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
@ -127,8 +144,19 @@ func publishTestflight(ctx context.Context) error {
if err != nil {
return err
}
build := builds.Data[0]
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute {
log.Info(string(platform), " ", tag, " waiting for process")
time.Sleep(15 * time.Second)
continue
}
if *build.Attributes.ProcessingState != "VALID" {
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
time.Sleep(15 * time.Second)
continue
}
log.Info(string(platform), " ", tag, " list localizations")
localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, builds.Data[0].ID, nil)
localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil)
if err != nil {
return err
}
@ -141,35 +169,41 @@ func publishTestflight(ctx context.Context) error {
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
log.Info(string(platform), " ", tag, " update localization")
_, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(
F.ToString("sing-box ", tag),
F.ToString("sing-box ", tagVersion.String()),
))
if err != nil {
return err
}
}
if !common.Contains(buildIDS, builds.Data[0].ID) {
log.Info(string(platform), " ", tag, " publish")
_, err = client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{builds.Data[0].ID})
if err != nil {
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
if response != nil && (response.StatusCode == http.StatusUnprocessableEntity || response.StatusCode == http.StatusNotFound) {
log.Info("waiting for process")
time.Sleep(15 * time.Second)
continue
} else if err != nil {
return err
}
}
log.Info(string(platform), " ", tag, " list submissions")
betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{
FilterBuild: []string{builds.Data[0].ID},
FilterBuild: []string{build.ID},
})
if err != nil {
return err
}
if len(betaSubmissions.Data) == 0 {
log.Info(string(platform), " ", tag, " create submission")
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, builds.Data[0].ID)
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
if err != nil {
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
log.Error(err)
break
}
return err
}
continue
}
break
}
}
return nil
}
@ -187,12 +221,15 @@ func cancelAppStore(ctx context.Context, platform string) error {
if err != nil {
return err
}
client := createClient()
client := createClient(time.Minute)
for {
log.Info(platform, " list versions")
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
versions, response, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
})
if err != nil {
if isRetryable(response) {
continue
} else if err != nil {
return err
}
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
@ -201,12 +238,14 @@ func cancelAppStore(ctx context.Context, platform string) error {
if version.ID == "" {
return nil
}
log.Info(string(platform), " ", tag, " get submission")
log.Info(platform, " ", tag, " get submission")
submission, response, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
if response != nil && response.StatusCode == http.StatusNotFound {
return nil
}
if err != nil {
if isRetryable(response) {
continue
} else if err != nil {
return err
}
log.Info(platform, " ", tag, " delete submission")
@ -215,6 +254,7 @@ func cancelAppStore(ctx context.Context, platform string) error {
return err
}
return nil
}
}
func prepareAppStore(ctx context.Context) error {
@ -222,7 +262,7 @@ func prepareAppStore(ctx context.Context) error {
if err != nil {
return err
}
client := createClient()
client := createClient(time.Minute)
for _, platform := range []asc.Platform{
asc.PlatformIOS,
asc.PlatformMACOS,
@ -291,7 +331,7 @@ func prepareAppStore(ctx context.Context) error {
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
}
log.Info(string(platform), " ", tag, " update build")
response, err = client.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID)
response, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID)
if err != nil {
return err
}
@ -324,7 +364,7 @@ func prepareAppStore(ctx context.Context) error {
if localization.ID == "" {
log.Info(string(platform), " ", tag, " no en-US localization found")
}
if localization.Attributes.WhatsNew == nil && *localization.Attributes.WhatsNew == "" {
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
log.Info(string(platform), " ", tag, " update localization")
_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
PromotionalText: common.Ptr("Yet another distribution for sing-box, the universal proxy platform."),
@ -343,16 +383,14 @@ func prepareAppStore(ctx context.Context) error {
case http.StatusInternalServerError:
continue
default:
response.Write(os.Stderr)
log.Info(string(platform), " ", tag, " unexpected response: ", response.Status)
return err
}
}
switch response.StatusCode {
case http.StatusCreated:
break fixSubmit
default:
response.Write(os.Stderr)
log.Info(string(platform), " ", tag, " unexpected response: ", response.Status)
return err
}
}
}
@ -364,7 +402,7 @@ func publishAppStore(ctx context.Context) error {
if err != nil {
return err
}
client := createClient()
client := createClient(time.Minute)
for _, platform := range []asc.Platform{
asc.PlatformIOS,
asc.PlatformMACOS,
@ -398,3 +436,15 @@ func publishAppStore(ctx context.Context) error {
}
return nil
}
func isRetryable(response *asc.Response) bool {
if response == nil {
return false
}
switch response.StatusCode {
case http.StatusInternalServerError, http.StatusUnprocessableEntity:
return true
default:
return false
}
}

View File

@ -19,12 +19,14 @@ var (
debugEnabled bool
target string
platform string
withTailscale bool
)
func init() {
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
flag.StringVar(&target, "target", "android", "target platform")
flag.StringVar(&platform, "platform", "", "specify platform")
flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS")
}
func main() {
@ -44,7 +46,9 @@ var (
sharedFlags []string
debugFlags []string
sharedTags []string
iosTags []string
macOSTags []string
memcTags []string
notMemcTags []string
debugTags []string
)
@ -58,8 +62,10 @@ func init() {
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api")
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack")
macOSTags = append(macOSTags, "with_dhcp")
memcTags = append(memcTags, "with_tailscale")
notMemcTags = append(notMemcTags, "with_low_memory")
debugTags = append(debugTags, "debug")
}
@ -99,18 +105,21 @@ func buildAndroid() {
"-javapkg=io.nekohasekai",
"-libname=box",
}
if !debugEnabled {
sharedFlags[3] = sharedFlags[3] + " -checklinkname=0"
args = append(args, sharedFlags...)
} else {
debugFlags[1] = debugFlags[1] + " -checklinkname=0"
args = append(args, debugFlags...)
}
args = append(args, "-tags")
if !debugEnabled {
args = append(args, strings.Join(sharedTags, ","))
} else {
args = append(args, strings.Join(append(sharedTags, debugTags...), ","))
tags := append(sharedTags, memcTags...)
if debugEnabled {
tags = append(tags, debugTags...)
}
args = append(args, "-tags", strings.Join(tags, ","))
args = append(args, "./experimental/libbox")
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
@ -148,20 +157,29 @@ func buildApple() {
"-v",
"-target", bindTarget,
"-libname=box",
"-tags-not-macos=with_low_memory",
}
if !withTailscale {
args = append(args, "-tags-macos="+strings.Join(append(macOSTags, memcTags...), ","))
} else {
args = append(args, "-tags-macos="+strings.Join(macOSTags, ","))
}
if !debugEnabled {
args = append(args, sharedFlags...)
} else {
args = append(args, debugFlags...)
}
tags := append(sharedTags, iosTags...)
args = append(args, "-tags")
if !debugEnabled {
args = append(args, strings.Join(tags, ","))
} else {
args = append(args, strings.Join(append(tags, debugTags...), ","))
tags := sharedTags
if withTailscale {
tags = append(tags, memcTags...)
}
if debugEnabled {
tags = append(tags, debugTags...)
}
args = append(args, "-tags", strings.Join(tags, ","))
args = append(args, "./experimental/libbox")
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)

View File

@ -48,7 +48,7 @@ func FindSDK() {
}
func findNDK() bool {
const fixedVersion = "28.0.12674087"
const fixedVersion = "28.0.13004108"
const versionFile = "source.properties"
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
androidNDKPath = fixedPath

View File

@ -5,40 +5,49 @@ import (
"os"
"github.com/sagernet/sing-box/cmd/internal/build_shared"
"github.com/sagernet/sing-box/common/badversion"
"github.com/sagernet/sing-box/log"
)
var nightly bool
var (
flagRunInCI bool
flagRunNightly bool
)
func init() {
flag.BoolVar(&nightly, "nightly", false, "Print nightly tag")
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
}
func main() {
flag.Parse()
if nightly {
version, err := build_shared.ReadTagVersionRev()
var (
versionStr string
err error
)
if flagRunNightly {
var version badversion.Version
version, err = build_shared.ReadTagVersion()
if err == nil {
versionStr = version.String()
}
} else {
versionStr, err = build_shared.ReadTag()
}
if flagRunInCI {
if err != nil {
log.Fatal(err)
}
var versionStr string
if version.PreReleaseIdentifier != "" {
versionStr = version.VersionString() + "-nightly"
} else {
version.Patch++
versionStr = version.VersionString() + "-nightly"
}
err = setGitHubEnv("version", versionStr)
if err != nil {
log.Fatal(err)
}
} else {
tag, err := build_shared.ReadTag()
if err != nil {
log.Error(err)
os.Stdout.WriteString("unknown\n")
} else {
os.Stdout.WriteString(tag + "\n")
os.Stdout.WriteString(versionStr + "\n")
}
}
}

View File

@ -0,0 +1,284 @@
package main
import (
"context"
"fmt"
"io"
"net/netip"
"os"
"os/exec"
"strings"
"syscall"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/shell"
)
var iperf3Path string
func main() {
err := main0()
if err != nil {
log.Fatal(err)
}
}
func main0() error {
err := shell.Exec("sudo", "ls").Run()
if err != nil {
return err
}
results, err := runTests()
if err != nil {
return err
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
return encoder.Encode(results)
}
func runTests() ([]TestResult, error) {
boxPaths := []string{
os.ExpandEnv("$HOME/Downloads/sing-box-1.11.15-darwin-arm64/sing-box"),
//"/Users/sekai/Downloads/sing-box-1.11.15-linux-arm64/sing-box",
"./sing-box",
}
stacks := []string{
"gvisor",
"system",
}
mtus := []int{
1500,
4064,
// 16384,
// 32768,
// 49152,
65535,
}
flagList := [][]string{
{},
}
var results []TestResult
for _, boxPath := range boxPaths {
for _, stack := range stacks {
for _, mtu := range mtus {
if strings.HasPrefix(boxPath, ".") {
for _, flags := range flagList {
result, err := testOnce(boxPath, stack, mtu, false, flags)
if err != nil {
return nil, err
}
results = append(results, *result)
}
} else {
result, err := testOnce(boxPath, stack, mtu, false, nil)
if err != nil {
return nil, err
}
results = append(results, *result)
}
}
}
}
return results, nil
}
type TestResult struct {
BoxPath string `json:"box_path"`
Stack string `json:"stack"`
MTU int `json:"mtu"`
Flags []string `json:"flags"`
MultiThread bool `json:"multi_thread"`
UploadSpeed string `json:"upload_speed"`
DownloadSpeed string `json:"download_speed"`
}
func testOnce(boxPath string, stackName string, mtu int, multiThread bool, flags []string) (result *TestResult, err error) {
testAddress := netip.MustParseAddr("1.1.1.1")
testConfig := option.Options{
Inbounds: []option.Inbound{
{
Type: C.TypeTun,
Options: &option.TunInboundOptions{
Address: []netip.Prefix{netip.MustParsePrefix("172.18.0.1/30")},
AutoRoute: true,
MTU: uint32(mtu),
Stack: stackName,
RouteAddress: []netip.Prefix{netip.PrefixFrom(testAddress, testAddress.BitLen())},
},
},
},
Route: &option.RouteOptions{
Rules: []option.Rule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
RawDefaultRule: option.RawDefaultRule{
IPCIDR: []string{testAddress.String()},
},
RuleAction: option.RuleAction{
Action: C.RuleActionTypeRouteOptions,
RouteOptionsOptions: option.RouteOptionsActionOptions{
OverrideAddress: "127.0.0.1",
},
},
},
},
},
AutoDetectInterface: true,
},
}
ctx := include.Context(context.Background())
tempConfig, err := os.CreateTemp("", "tun-bench-*.json")
if err != nil {
return
}
defer os.Remove(tempConfig.Name())
encoder := json.NewEncoderContext(ctx, tempConfig)
encoder.SetIndent("", " ")
err = encoder.Encode(testConfig)
if err != nil {
return nil, E.Cause(err, "encode test config")
}
tempConfig.Close()
var sudoArgs []string
if len(flags) > 0 {
sudoArgs = append(sudoArgs, "env")
sudoArgs = append(sudoArgs, flags...)
}
sudoArgs = append(sudoArgs, boxPath, "run", "-c", tempConfig.Name())
boxProcess := shell.Exec("sudo", sudoArgs...)
boxProcess.Stdout = &stderrWriter{}
boxProcess.Stderr = io.Discard
err = boxProcess.Start()
if err != nil {
return
}
if C.IsDarwin {
iperf3Path, err = exec.LookPath("iperf3-darwin")
} else {
iperf3Path, err = exec.LookPath("iperf3")
}
if err != nil {
return
}
serverProcess := shell.Exec(iperf3Path, "-s")
serverProcess.Stdout = io.Discard
serverProcess.Stderr = io.Discard
err = serverProcess.Start()
if err != nil {
return nil, E.Cause(err, "start iperf3 server")
}
time.Sleep(time.Second)
args := []string{"-c", testAddress.String()}
if multiThread {
args = append(args, "-P", "10")
}
uploadProcess := shell.Exec(iperf3Path, args...)
output, err := uploadProcess.Read()
if err != nil {
boxProcess.Process.Signal(syscall.SIGKILL)
serverProcess.Process.Signal(syscall.SIGKILL)
println(output)
return
}
uploadResult := common.SubstringBeforeLast(output, "iperf Done.")
uploadResult = common.SubstringBeforeLast(uploadResult, "sender")
uploadResult = common.SubstringBeforeLast(uploadResult, "bits/sec")
uploadResult = common.SubstringAfterLast(uploadResult, "Bytes")
uploadResult = strings.ReplaceAll(uploadResult, " ", "")
result = &TestResult{
BoxPath: boxPath,
Stack: stackName,
MTU: mtu,
Flags: flags,
MultiThread: multiThread,
UploadSpeed: uploadResult,
}
downloadProcess := shell.Exec(iperf3Path, append(args, "-R")...)
output, err = downloadProcess.Read()
if err != nil {
boxProcess.Process.Signal(syscall.SIGKILL)
serverProcess.Process.Signal(syscall.SIGKILL)
println(output)
return
}
downloadResult := common.SubstringBeforeLast(output, "iperf Done.")
downloadResult = common.SubstringBeforeLast(downloadResult, "receiver")
downloadResult = common.SubstringBeforeLast(downloadResult, "bits/sec")
downloadResult = common.SubstringAfterLast(downloadResult, "Bytes")
downloadResult = strings.ReplaceAll(downloadResult, " ", "")
result.DownloadSpeed = downloadResult
printArgs := []any{boxPath, stackName, mtu, "upload", uploadResult, "download", downloadResult}
if len(flags) > 0 {
printArgs = append(printArgs, "flags", strings.Join(flags, " "))
}
if multiThread {
printArgs = append(printArgs, "(-P 10)")
}
fmt.Println(printArgs...)
err = boxProcess.Process.Signal(syscall.SIGTERM)
if err != nil {
return
}
err = serverProcess.Process.Signal(syscall.SIGTERM)
if err != nil {
return
}
boxDone := make(chan struct{})
go func() {
boxProcess.Cmd.Wait()
close(boxDone)
}()
serverDone := make(chan struct{})
go func() {
serverProcess.Process.Wait()
close(serverDone)
}()
select {
case <-boxDone:
case <-time.After(2 * time.Second):
boxProcess.Process.Kill()
case <-time.After(4 * time.Second):
println("box process did not close!")
os.Exit(1)
}
select {
case <-serverDone:
case <-time.After(2 * time.Second):
serverProcess.Process.Kill()
case <-time.After(4 * time.Second):
println("server process did not close!")
os.Exit(1)
}
return
}
type stderrWriter struct{}
func (w *stderrWriter) Write(p []byte) (n int, err error) {
return os.Stderr.Write(p)
}

View File

@ -13,10 +13,14 @@ import (
"github.com/sagernet/sing/common"
)
var flagRunInCI bool
var (
flagRunInCI bool
flagRunNightly bool
)
func init() {
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
}
func main() {
@ -46,21 +50,23 @@ func main() {
switch propPair[0] {
case "VERSION_NAME":
if propPair[1] != newVersion {
log.Info("updated version from ", propPair[1], " to ", newVersion)
versionUpdated = true
propPair[1] = newVersion
log.Info("updated version to ", newVersion)
}
case "GO_VERSION":
if propPair[1] != runtime.Version() {
log.Info("updated Go version from ", propPair[1], " to ", runtime.Version())
goVersionUpdated = true
propPair[1] = runtime.Version()
log.Info("updated Go version to ", runtime.Version())
}
}
}
if !(versionUpdated || goVersionUpdated) {
log.Info("version not changed")
return
} else if flagRunInCI && !flagRunNightly {
log.Fatal("version changed, commit changes first.")
}
for _, propPair := range propsList {
switch propPair[0] {

View File

@ -0,0 +1,71 @@
package main
import (
"encoding/csv"
"io"
"net/http"
"os"
"strings"
"github.com/sagernet/sing-box/log"
"golang.org/x/exp/slices"
)
func main() {
err := updateMozillaIncludedRootCAs()
if err != nil {
log.Error(err)
}
}
func updateMozillaIncludedRootCAs() error {
response, err := http.Get("https://ccadb.my.salesforce-sites.com/mozilla/IncludedCACertificateReportPEMCSV")
if err != nil {
return err
}
defer response.Body.Close()
reader := csv.NewReader(response.Body)
header, err := reader.Read()
if err != nil {
return err
}
geoIndex := slices.Index(header, "Geographic Focus")
nameIndex := slices.Index(header, "Common Name or Certificate Name")
certIndex := slices.Index(header, "PEM Info")
generated := strings.Builder{}
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
package certificate
import "crypto/x509"
var mozillaIncluded *x509.CertPool
func init() {
mozillaIncluded = x509.NewCertPool()
`)
for {
record, err := reader.Read()
if err == io.EOF {
break
} else if err != nil {
return err
}
if record[geoIndex] == "China" {
continue
}
generated.WriteString("\n // ")
generated.WriteString(record[nameIndex])
generated.WriteString("\n")
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
cert := record[certIndex]
// Remove single quotes
cert = cert[1 : len(cert)-1]
generated.WriteString(cert)
generated.WriteString("`))\n")
}
generated.WriteString("}\n")
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
}

View File

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

View File

@ -9,8 +9,6 @@ import (
"github.com/spf13/cobra"
)
var pqSignatureSchemesEnabled bool
var commandGenerateECHKeyPair = &cobra.Command{
Use: "ech-keypair <plain_server_name>",
Short: "Generate TLS ECH key pair",
@ -24,12 +22,11 @@ var commandGenerateECHKeyPair = &cobra.Command{
}
func init() {
commandGenerateECHKeyPair.Flags().BoolVar(&pqSignatureSchemesEnabled, "pq-signature-schemes-enabled", false, "Enable PQ signature schemes")
commandGenerate.AddCommand(commandGenerateECHKeyPair)
}
func generateECHKeyPair(serverName string) error {
configPem, keyPem, err := tls.ECHKeygenDefault(serverName, pqSignatureSchemesEnabled)
configPem, keyPem, err := tls.ECHKeygenDefault(serverName)
if err != nil {
return err
}

View File

@ -30,7 +30,7 @@ func init() {
}
func generateTLSKeyPair(serverName string) error {
privateKeyPem, publicKeyPem, err := tls.GenerateKeyPair(time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
privateKeyPem, publicKeyPem, err := tls.GenerateCertificate(nil, nil, time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
if err != nil {
return err
}

View File

@ -18,7 +18,7 @@ import (
)
var commandMerge = &cobra.Command{
Use: "merge <output>",
Use: "merge <output-path>",
Short: "Merge configurations",
Run: func(cmd *cobra.Command, args []string) {
err := merge(args[0])

View File

@ -6,8 +6,10 @@ import (
"strings"
"github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing/common/json"
"github.com/spf13/cobra"
@ -69,7 +71,7 @@ func compileRuleSet(sourcePath string) error {
if err != nil {
return err
}
err = srs.Write(outputFile, plainRuleSet.Options, plainRuleSet.Version)
err = srs.Write(outputFile, plainRuleSet.Options, downgradeRuleSetVersion(plainRuleSet.Version, plainRuleSet.Options))
if err != nil {
outputFile.Close()
os.Remove(outputPath)
@ -78,3 +80,18 @@ func compileRuleSet(sourcePath string) error {
outputFile.Close()
return nil
}
func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 {
if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
len(rule.DefaultInterfaceAddress) > 0
}) {
version = C.RuleSetVersion3
}
if version == C.RuleSetVersion3 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
return len(rule.NetworkType) > 0 || rule.NetworkIsExpensive || rule.NetworkIsConstrained
}) {
version = C.RuleSetVersion2
}
return version
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,162 @@
package main
import (
"bytes"
"io"
"os"
"path/filepath"
"sort"
"strings"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/rw"
"github.com/spf13/cobra"
)
var (
ruleSetPaths []string
ruleSetDirectories []string
)
var commandRuleSetMerge = &cobra.Command{
Use: "merge <output-path>",
Short: "Merge rule-set source files",
Run: func(cmd *cobra.Command, args []string) {
err := mergeRuleSet(args[0])
if err != nil {
log.Fatal(err)
}
},
Args: cobra.ExactArgs(1),
}
func init() {
commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetPaths, "config", "c", nil, "set input rule-set file path")
commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetDirectories, "config-directory", "C", nil, "set input rule-set directory path")
commandRuleSet.AddCommand(commandRuleSetMerge)
}
type RuleSetEntry struct {
content []byte
path string
options option.PlainRuleSetCompat
}
func readRuleSetAt(path string) (*RuleSetEntry, error) {
var (
configContent []byte
err error
)
if path == "stdin" {
configContent, err = io.ReadAll(os.Stdin)
} else {
configContent, err = os.ReadFile(path)
}
if err != nil {
return nil, E.Cause(err, "read config at ", path)
}
options, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, configContent)
if err != nil {
return nil, E.Cause(err, "decode config at ", path)
}
return &RuleSetEntry{
content: configContent,
path: path,
options: options,
}, nil
}
func readRuleSet() ([]*RuleSetEntry, error) {
var optionsList []*RuleSetEntry
for _, path := range ruleSetPaths {
optionsEntry, err := readRuleSetAt(path)
if err != nil {
return nil, err
}
optionsList = append(optionsList, optionsEntry)
}
for _, directory := range ruleSetDirectories {
entries, err := os.ReadDir(directory)
if err != nil {
return nil, E.Cause(err, "read rule-set directory at ", directory)
}
for _, entry := range entries {
if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() {
continue
}
optionsEntry, err := readRuleSetAt(filepath.Join(directory, entry.Name()))
if err != nil {
return nil, err
}
optionsList = append(optionsList, optionsEntry)
}
}
sort.Slice(optionsList, func(i, j int) bool {
return optionsList[i].path < optionsList[j].path
})
return optionsList, nil
}
func readRuleSetAndMerge() (option.PlainRuleSetCompat, error) {
optionsList, err := readRuleSet()
if err != nil {
return option.PlainRuleSetCompat{}, err
}
if len(optionsList) == 1 {
return optionsList[0].options, nil
}
var optionVersion uint8
for _, options := range optionsList {
if optionVersion < options.options.Version {
optionVersion = options.options.Version
}
}
var mergedMessage json.RawMessage
for _, options := range optionsList {
mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false)
if err != nil {
return option.PlainRuleSetCompat{}, E.Cause(err, "merge config at ", options.path)
}
}
mergedOptions, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, mergedMessage)
if err != nil {
return option.PlainRuleSetCompat{}, E.Cause(err, "unmarshal merged config")
}
mergedOptions.Version = optionVersion
return mergedOptions, nil
}
func mergeRuleSet(outputPath string) error {
mergedOptions, err := readRuleSetAndMerge()
if err != nil {
return err
}
buffer := new(bytes.Buffer)
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
err = encoder.Encode(mergedOptions)
if err != nil {
return E.Cause(err, "encode config")
}
if existsContent, err := os.ReadFile(outputPath); err != nil {
if string(existsContent) == buffer.String() {
return nil
}
}
err = rw.MkdirParent(outputPath)
if err != nil {
return err
}
err = os.WriteFile(outputPath, buffer.Bytes(), 0o644)
if err != nil {
return err
}
outputPath, _ = filepath.Abs(outputPath)
os.Stderr.WriteString(outputPath + "\n")
return nil
}

View File

@ -61,14 +61,15 @@ func upgradeRuleSet(sourcePath string) error {
log.Info("already up-to-date")
return nil
}
plainRuleSet, err := plainRuleSetCompat.Upgrade()
plainRuleSetCompat.Options, err = plainRuleSetCompat.Upgrade()
if err != nil {
return err
}
plainRuleSetCompat.Version = C.RuleSetVersionCurrent
buffer := new(bytes.Buffer)
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
err = encoder.Encode(plainRuleSet)
err = encoder.Encode(plainRuleSetCompat)
if err != nil {
return E.Cause(err, "encode config")
}

View File

@ -21,7 +21,7 @@ func initializeHTTP3Client(instance *box.Box) error {
return err
}
http3Client = &http.Client{
Transport: &http3.RoundTripper{
Transport: &http3.Transport{
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)

View File

@ -4,7 +4,6 @@ import (
"context"
"os"
"github.com/sagernet/sing-box/common/settings"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
@ -58,7 +57,7 @@ func syncTime() error {
return err
}
if commandSyncTimeWrite {
err = settings.SetSystemTime(response.Time)
err = ntp.SetSystemTime(response.Time)
if err != nil {
return E.Cause(err, "write time to system")
}

View File

@ -128,6 +128,10 @@ func (c *ReadWaitConn) Upstream() any {
return c.Conn
}
func (c *ReadWaitConn) ReaderReplaceable() bool {
return true
}
var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error)
func init() {

View File

@ -1,31 +0,0 @@
//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

@ -6,26 +6,31 @@ import (
"net"
_ "unsafe"
"github.com/sagernet/sing/common"
"github.com/sagernet/utls"
"github.com/metacubex/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
}
switch tlsConn := conn.(type) {
case *tls.UConn:
return true, func() error {
return utlsReadRecord(tlsConn.Conn)
}, func() error {
return utlsHandlePostHandshakeMessage(tlsConn.Conn)
}
case *tls.Conn:
return true, func() error {
return utlsReadRecord(tlsConn)
}, func() error {
return utlsHandlePostHandshakeMessage(tlsConn)
}
}
return
})
}
//go:linkname utlsReadRecord github.com/sagernet/utls.(*Conn).readRecord
//go:linkname utlsReadRecord github.com/metacubex/utls.(*Conn).readRecord
func utlsReadRecord(c *tls.Conn) error
//go:linkname utlsHandlePostHandshakeMessage github.com/sagernet/utls.(*Conn).handlePostHandshakeMessage
//go:linkname utlsHandlePostHandshakeMessage github.com/metacubex/utls.(*Conn).handlePostHandshakeMessage
func utlsHandlePostHandshakeMessage(c *tls.Conn) error

File diff suppressed because it is too large Load Diff

185
common/certificate/store.go Normal file
View File

@ -0,0 +1,185 @@
package certificate
import (
"context"
"crypto/x509"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
)
var _ adapter.CertificateStore = (*Store)(nil)
type Store struct {
systemPool *x509.CertPool
currentPool *x509.CertPool
certificate string
certificatePaths []string
certificateDirectoryPaths []string
watcher *fswatch.Watcher
}
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
var systemPool *x509.CertPool
switch options.Store {
case C.CertificateStoreSystem, "":
systemPool = x509.NewCertPool()
platformInterface := service.FromContext[platform.Interface](ctx)
var systemValid bool
if platformInterface != nil {
for _, cert := range platformInterface.SystemCertificates() {
if systemPool.AppendCertsFromPEM([]byte(cert)) {
systemValid = true
}
}
}
if !systemValid {
certPool, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
systemPool = certPool
}
case C.CertificateStoreMozilla:
systemPool = mozillaIncluded
case C.CertificateStoreNone:
systemPool = nil
default:
return nil, E.New("unknown certificate store: ", options.Store)
}
store := &Store{
systemPool: systemPool,
certificate: strings.Join(options.Certificate, "\n"),
certificatePaths: options.CertificatePath,
certificateDirectoryPaths: options.CertificateDirectoryPath,
}
var watchPaths []string
for _, target := range options.CertificatePath {
watchPaths = append(watchPaths, target)
}
for _, target := range options.CertificateDirectoryPath {
watchPaths = append(watchPaths, target)
}
if len(watchPaths) > 0 {
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPaths,
Logger: logger,
Callback: func(_ string) {
err := store.update()
if err != nil {
logger.Error(E.Cause(err, "reload certificates"))
}
},
})
if err != nil {
return nil, E.Cause(err, "fswatch: create fsnotify watcher")
}
store.watcher = watcher
}
err := store.update()
if err != nil {
return nil, E.Cause(err, "initializing certificate store")
}
return store, nil
}
func (s *Store) Name() string {
return "certificate"
}
func (s *Store) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
if s.watcher != nil {
return s.watcher.Start()
}
return nil
}
func (s *Store) Close() error {
if s.watcher != nil {
return s.watcher.Close()
}
return nil
}
func (s *Store) Pool() *x509.CertPool {
return s.currentPool
}
func (s *Store) update() error {
var currentPool *x509.CertPool
if s.systemPool == nil {
currentPool = x509.NewCertPool()
} else {
currentPool = s.systemPool.Clone()
}
if s.certificate != "" {
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
return E.New("invalid certificate PEM strings")
}
}
for _, path := range s.certificatePaths {
pemContent, err := os.ReadFile(path)
if err != nil {
return err
}
if !currentPool.AppendCertsFromPEM(pemContent) {
return E.New("invalid certificate PEM file: ", path)
}
}
var firstErr error
for _, directoryPath := range s.certificateDirectoryPaths {
directoryEntries, err := readUniqueDirectoryEntries(directoryPath)
if err != nil {
if firstErr == nil && !os.IsNotExist(err) {
firstErr = E.Cause(err, "invalid certificate directory: ", directoryPath)
}
continue
}
for _, directoryEntry := range directoryEntries {
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
if err == nil {
currentPool.AppendCertsFromPEM(pemContent)
}
}
}
if firstErr != nil {
return firstErr
}
s.currentPool = currentPool
return nil
}
func readUniqueDirectoryEntries(dir string) ([]fs.DirEntry, error) {
files, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
uniq := files[:0]
for _, f := range files {
if !isSameDirSymlink(f, dir) {
uniq = append(uniq, f)
}
}
return uniq, nil
}
func isSameDirSymlink(f fs.DirEntry, dir string) bool {
if f.Type()&fs.ModeSymlink == 0 {
return false
}
target, err := os.Readlink(filepath.Join(dir, f.Name()))
return err == nil && !strings.Contains(target, "/")
}

View File

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

View File

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

View File

@ -2,17 +2,19 @@ package dialer
import (
"context"
"errors"
"net"
"net/netip"
"syscall"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/conntrack"
"github.com/sagernet/sing-box/common/listener"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
@ -33,13 +35,14 @@ type DefaultDialer struct {
udpListener net.ListenConfig
udpAddr4 string
udpAddr6 string
isWireGuardListener bool
netns string
networkManager adapter.NetworkManager
networkStrategy *C.NetworkStrategy
defaultNetworkStrategy bool
networkType []C.InterfaceType
fallbackNetworkType []C.InterfaceType
networkFallbackDelay time.Duration
networkLastFallback atomic.TypedValue[time.Time]
networkLastFallback common.TypedValue[time.Time]
}
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
@ -51,6 +54,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
listener net.ListenConfig
interfaceFinder control.InterfaceFinder
networkStrategy *C.NetworkStrategy
defaultNetworkStrategy bool
networkType []C.InterfaceType
fallbackNetworkType []C.InterfaceType
networkFallbackDelay time.Duration
@ -61,25 +65,22 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
interfaceFinder = control.NewDefaultInterfaceFinder()
}
if options.BindInterface != "" {
if !(C.IsLinux || C.IsDarwin || C.IsWindows) {
return nil, E.New("`bind_interface` is only supported on Linux, macOS and Windows")
}
bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1)
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
}
if options.RoutingMark > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(uint32(options.RoutingMark)))
listener.Control = control.Append(listener.Control, control.RoutingMark(uint32(options.RoutingMark)))
if !C.IsLinux {
return nil, E.New("`routing_mark` is only supported on Linux")
}
if networkManager != nil {
autoRedirectOutputMark := networkManager.AutoRedirectOutputMark()
if autoRedirectOutputMark > 0 {
if options.RoutingMark > 0 {
return nil, E.New("`routing_mark` is conflict with `tun.auto_redirect` with `tun.route_[_exclude]_address_set")
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
}
dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
}
}
if options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil || options.TCPFastOpen {
disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil
if disableDefaultBind || options.TCPFastOpen {
if options.NetworkStrategy != nil || len(options.NetworkType) > 0 && options.FallbackNetworkType == nil && options.FallbackDelay == 0 {
return nil, E.New("`network_strategy` is conflict with `bind_interface`, `inet4_bind_address`, `inet6_bind_address` and `tcp_fast_open`")
}
@ -87,6 +88,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
if networkManager != nil {
defaultOptions := networkManager.DefaultOptions()
if !disableDefaultBind {
if defaultOptions.BindInterface != "" {
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
dialer.Control = control.Append(dialer.Control, bindFunc)
@ -94,9 +96,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
} else if networkManager.AutoDetectInterface() {
if platformInterface != nil {
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
if networkStrategy == nil {
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
}
networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 {
@ -108,6 +107,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
networkFallbackDelay = defaultOptions.FallbackDelay
}
if networkStrategy == nil {
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
defaultNetworkStrategy = true
}
bindFunc := networkManager.ProtectFunc()
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
@ -118,10 +121,16 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
}
}
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(defaultOptions.RoutingMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(defaultOptions.RoutingMark))
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
}
}
}
if networkManager != nil {
markFunc := networkManager.AutoRedirectOutputMarkFunc()
dialer.Control = control.Append(dialer.Control, markFunc)
listener.Control = control.Append(listener.Control, markFunc)
}
if options.ReuseAddr {
listener.Control = control.Append(listener.Control, control.ReuseAddr())
}
@ -175,11 +184,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
}
setMultiPathTCP(&dialer4)
}
if options.IsWireGuardListener {
for _, controlFn := range WgControlFns {
listener.Control = control.Append(listener.Control, controlFn)
}
}
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
if err != nil {
return nil, err
@ -196,33 +200,54 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
udpListener: listener,
udpAddr4: udpAddr4,
udpAddr6: udpAddr6,
isWireGuardListener: options.IsWireGuardListener,
netns: options.NetNs,
networkManager: networkManager,
networkStrategy: networkStrategy,
defaultNetworkStrategy: defaultNetworkStrategy,
networkType: networkType,
fallbackNetworkType: fallbackNetworkType,
networkFallbackDelay: networkFallbackDelay,
}, nil
}
func setMarkWrapper(networkManager adapter.NetworkManager, mark uint32, isDefault bool) control.Func {
if networkManager == nil {
return control.RoutingMark(mark)
}
return func(network, address string, conn syscall.RawConn) error {
if networkManager.AutoRedirectOutputMark() != 0 {
if isDefault {
return E.New("`route.default_mark` is conflict with `tun.auto_redirect`")
} else {
return E.New("`routing_mark` is conflict with `tun.auto_redirect`")
}
}
return control.RoutingMark(mark)(network, address, conn)
}
}
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
if !address.IsValid() {
return nil, E.New("invalid address")
} else if address.IsFqdn() {
return nil, E.New("domain not resolved")
}
if d.networkStrategy == nil {
return trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) {
switch N.NetworkName(network) {
case N.NetworkUDP:
if !address.IsIPv6() {
return trackConn(d.udpDialer4.DialContext(ctx, network, address.String()))
return d.udpDialer4.DialContext(ctx, network, address.String())
} else {
return trackConn(d.udpDialer6.DialContext(ctx, network, address.String()))
return d.udpDialer6.DialContext(ctx, network, address.String())
}
}
if !address.IsIPv6() {
return trackConn(DialSlowContext(&d.dialer4, ctx, network, address))
return DialSlowContext(&d.dialer4, ctx, network, address)
} else {
return trackConn(DialSlowContext(&d.dialer6, ctx, network, address))
return DialSlowContext(&d.dialer6, ctx, network, address)
}
}))
} else {
return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
}
@ -250,7 +275,7 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
} else {
dialer = d.udpDialer4
}
fastFallback := time.Now().Sub(d.networkLastFallback.Load()) < C.TCPTimeout
fastFallback := time.Since(d.networkLastFallback.Load()) < C.TCPTimeout
var (
conn net.Conn
isPrimary bool
@ -262,8 +287,14 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay, d.networkLastFallback.Store)
}
if err != nil {
// bind interface failed on legacy xiaomi systems
if d.defaultNetworkStrategy && errors.Is(err, syscall.EPERM) {
d.networkStrategy = nil
return d.DialContext(ctx, network, address)
} else {
return nil, err
}
}
if !fastFallback && !isPrimary {
d.networkLastFallback.Store(time.Now())
}
@ -272,18 +303,28 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if d.networkStrategy == nil {
return trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) {
if destination.IsIPv6() {
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6))
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4))
return d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4)
} else {
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4))
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4)
}
}))
} else {
return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
}
}
func (d *DefaultDialer) DialerForICMPDestination(destination netip.Addr) net.Dialer {
if !destination.Is6() {
return dialerFromTCPDialer(d.dialer6)
} else {
return dialerFromTCPDialer(d.dialer4)
}
}
func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
if strategy == nil {
strategy = d.networkStrategy
@ -300,18 +341,35 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina
if fallbackDelay == 0 {
fallbackDelay = d.networkFallbackDelay
}
if !d.networkManager.AutoDetectInterface() {
return nil, E.New("`route.auto_detect_interface` is require by `network_strategy`")
}
network := N.NetworkUDP
if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
network += "4"
}
return trackPacketConn(d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", *strategy, interfaceType, fallbackInterfaceType, fallbackDelay))
packetConn, err := d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", *strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
if err != nil {
// bind interface failed on legacy xiaomi systems
if d.defaultNetworkStrategy && errors.Is(err, syscall.EPERM) {
d.networkStrategy = nil
return d.ListenPacket(ctx, destination)
} else {
return nil, err
}
}
return trackPacketConn(packetConn, nil)
}
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
return d.udpListener.ListenPacket(context.Background(), network, address)
udpListener := d.udpListener
udpListener.Control = control.Append(udpListener.Control, func(network, address string, conn syscall.RawConn) error {
for _, wgControlFn := range WgControlFns {
err := wgControlFn(network, address, conn)
if err != nil {
return err
}
}
return nil
})
return udpListener.ListenPacket(context.Background(), network, address)
}
func trackConn(conn net.Conn, err error) (net.Conn, error) {

View File

@ -18,6 +18,7 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
return nil, false, E.New("no available network interface")
}
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
if fallbackDelay == 0 {
fallbackDelay = N.DefaultFallbackDelay
}
@ -31,11 +32,13 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di
results := make(chan dialResult) // unbuffered
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
perNetDialer := dialer
if defaultInterface == nil || iif.Index != defaultInterface.Index {
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
}
conn, err := perNetDialer.DialContext(ctx, network, addr)
if err != nil {
select {
case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Name, ")"), primary: primary}:
case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")"), primary: primary}:
case <-returned:
}
} else {
@ -89,6 +92,7 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
return nil, false, E.New("no available network interface")
}
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
if fallbackDelay == 0 {
fallbackDelay = N.DefaultFallbackDelay
}
@ -103,11 +107,13 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d
results := make(chan dialResult) // unbuffered
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
perNetDialer := dialer
if defaultInterface == nil || iif.Index != defaultInterface.Index {
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
}
conn, err := perNetDialer.DialContext(ctx, network, addr)
if err != nil {
select {
case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Name, ")"), primary: primary}:
case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")"), primary: primary}:
case <-returned:
}
} else {
@ -149,24 +155,29 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
return nil, E.New("no available network interface")
}
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
var errors []error
for _, primaryInterface := range primaryInterfaces {
perNetListener := listener
if defaultInterface == nil || primaryInterface.Index != defaultInterface.Index {
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, primaryInterface.Name, primaryInterface.Index))
}
conn, err := perNetListener.ListenPacket(ctx, network, addr)
if err == nil {
return conn, nil
}
errors = append(errors, E.Cause(err, "listen ", primaryInterface.Name, " (", primaryInterface.Name, ")"))
errors = append(errors, E.Cause(err, "listen ", primaryInterface.Name, " (", primaryInterface.Index, ")"))
}
for _, fallbackInterface := range fallbackInterfaces {
perNetListener := listener
if defaultInterface == nil || fallbackInterface.Index != defaultInterface.Index {
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, fallbackInterface.Name, fallbackInterface.Index))
}
conn, err := perNetListener.ListenPacket(ctx, network, addr)
if err == nil {
return conn, nil
}
errors = append(errors, E.Cause(err, "listen ", fallbackInterface.Name, " (", fallbackInterface.Name, ")"))
errors = append(errors, E.Cause(err, "listen ", fallbackInterface.Name, " (", fallbackInterface.Index, ")"))
}
return nil, E.Errors(errors...)
}
@ -177,11 +188,15 @@ func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkS
case C.NetworkStrategyDefault:
if len(interfaceType) == 0 {
defaultIf := networkManager.InterfaceMonitor().DefaultInterface()
if defaultIf != nil {
for _, iif := range interfaces {
if iif.Index == defaultIf.Index {
primaryInterfaces = append(primaryInterfaces, iif)
}
}
} else {
primaryInterfaces = interfaces
}
} else {
primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool {
return common.Contains(interfaceType, it.Type)
@ -198,12 +213,16 @@ func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkS
case C.NetworkStrategyFallback:
if len(interfaceType) == 0 {
defaultIf := networkManager.InterfaceMonitor().DefaultInterface()
if defaultIf != nil {
for _, iif := range interfaces {
if iif.Index == defaultIf.Index {
primaryInterfaces = append(primaryInterfaces, iif)
break
}
}
} else {
primaryInterfaces = interfaces
}
} else {
primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool {
return common.Contains(interfaceType, it.Type)

View File

@ -6,37 +6,61 @@ import (
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type DirectDialer interface {
IsEmpty() bool
}
type DetourDialer struct {
outboundManager adapter.OutboundManager
detour string
legacyDNSDialer bool
dialer N.Dialer
initOnce sync.Once
initErr error
}
func NewDetour(outboundManager adapter.OutboundManager, detour string) N.Dialer {
return &DetourDialer{outboundManager: outboundManager, detour: detour}
func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNSDialer bool) N.Dialer {
return &DetourDialer{
outboundManager: outboundManager,
detour: detour,
legacyDNSDialer: legacyDNSDialer,
}
}
func (d *DetourDialer) Start() error {
_, err := d.Dialer()
return err
func InitializeDetour(dialer N.Dialer) error {
detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
if !isDetour {
return nil
}
return common.Error(detourDialer.Dialer())
}
func (d *DetourDialer) Dialer() (N.Dialer, error) {
d.initOnce.Do(func() {
var loaded bool
d.dialer, loaded = d.outboundManager.Outbound(d.detour)
d.initOnce.Do(d.init)
return d.dialer, d.initErr
}
func (d *DetourDialer) init() {
dialer, loaded := d.outboundManager.Outbound(d.detour)
if !loaded {
d.initErr = E.New("outbound detour not found: ", d.detour)
return
}
})
return d.dialer, d.initErr
if !d.legacyDNSDialer {
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
if directDialer.IsEmpty() {
d.initErr = E.New("detour to an empty direct outbound makes no sense")
return
}
}
}
d.dialer = dialer
}
func (d *DetourDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {

View File

@ -8,66 +8,131 @@ import (
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-dns"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) {
if options.IsWireGuardListener {
return NewDefault(ctx, options)
}
type Options struct {
Context context.Context
Options option.DialerOptions
RemoteIsDomain bool
DirectResolver bool
ResolverOnDetour bool
NewDialer bool
LegacyDNSDialer bool
DirectOutbound bool
}
// TODO: merge with NewWithOptions
func New(ctx context.Context, options option.DialerOptions, remoteIsDomain bool) (N.Dialer, error) {
return NewWithOptions(Options{
Context: ctx,
Options: options,
RemoteIsDomain: remoteIsDomain,
})
}
func NewWithOptions(options Options) (N.Dialer, error) {
dialOptions := options.Options
var (
dialer N.Dialer
err error
)
if options.Detour == "" {
dialer, err = NewDefault(ctx, options)
if err != nil {
return nil, err
}
} else {
outboundManager := service.FromContext[adapter.OutboundManager](ctx)
if dialOptions.Detour != "" {
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
if outboundManager == nil {
return nil, E.New("missing outbound manager")
}
dialer = NewDetour(outboundManager, options.Detour)
}
if options.Detour == "" {
router := service.FromContext[adapter.Router](ctx)
if router != nil {
dialer = NewResolveDialer(
router,
dialer,
options.Detour == "" && !options.TCPFastOpen,
dns.DomainStrategy(options.DomainStrategy),
time.Duration(options.FallbackDelay))
}
}
return dialer, nil
}
func NewDirect(ctx context.Context, options option.DialerOptions) (ParallelInterfaceDialer, error) {
if options.Detour != "" {
return nil, E.New("`detour` is not supported in direct context")
}
if options.IsWireGuardListener {
return NewDefault(ctx, options)
}
dialer, err := NewDefault(ctx, options)
dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
} else {
dialer, err = NewDefault(options.Context, dialOptions)
if err != nil {
return nil, err
}
return NewResolveParallelInterfaceDialer(
service.FromContext[adapter.Router](ctx),
}
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
var defaultOptions adapter.NetworkOptions
if networkManager != nil {
defaultOptions = networkManager.DefaultOptions()
}
var (
server string
dnsQueryOptions adapter.DNSQueryOptions
resolveFallbackDelay time.Duration
)
if dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "" {
var transport adapter.DNSTransport
if !options.DirectResolver {
var loaded bool
transport, loaded = dnsTransport.Transport(dialOptions.DomainResolver.Server)
if !loaded {
return nil, E.New("domain resolver not found: " + dialOptions.DomainResolver.Server)
}
}
var strategy C.DomainStrategy
if dialOptions.DomainResolver.Strategy != option.DomainStrategy(C.DomainStrategyAsIS) {
strategy = C.DomainStrategy(dialOptions.DomainResolver.Strategy)
} else if
//nolint:staticcheck
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
//nolint:staticcheck
strategy = C.DomainStrategy(dialOptions.DomainStrategy)
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
}
server = dialOptions.DomainResolver.Server
dnsQueryOptions = adapter.DNSQueryOptions{
Transport: transport,
Strategy: strategy,
DisableCache: dialOptions.DomainResolver.DisableCache,
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
}
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else if options.DirectResolver {
return nil, E.New("missing domain resolver for domain server address")
} else {
if defaultOptions.DomainResolver != "" {
dnsQueryOptions = defaultOptions.DomainResolveOptions
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
if !loaded {
return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
}
dnsQueryOptions.Transport = transport
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else {
transports := dnsTransport.Transports()
if len(transports) < 2 {
dnsQueryOptions.Transport = dnsTransport.Default()
} else if options.NewDialer {
return nil, E.New("missing domain resolver for domain server address")
} else {
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
}
}
if
//nolint:staticcheck
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
//nolint:staticcheck
dnsQueryOptions.Strategy = C.DomainStrategy(dialOptions.DomainStrategy)
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
}
}
dialer = NewResolveDialer(
options.Context,
dialer,
true,
dns.DomainStrategy(options.DomainStrategy),
time.Duration(options.FallbackDelay),
), nil
dialOptions.Detour == "" && !dialOptions.TCPFastOpen,
server,
dnsQueryOptions,
resolveFallbackDelay,
)
}
return dialer, nil
}
type ParallelInterfaceDialer interface {

View File

@ -3,16 +3,17 @@ package dialer
import (
"context"
"net"
"net/netip"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
var (
@ -20,21 +21,51 @@ var (
_ ParallelInterfaceDialer = (*resolveParallelNetworkDialer)(nil)
)
type ResolveDialer interface {
N.Dialer
QueryOptions() adapter.DNSQueryOptions
}
type ParallelInterfaceResolveDialer interface {
ParallelInterfaceDialer
QueryOptions() adapter.DNSQueryOptions
}
type resolveDialer struct {
transport adapter.DNSTransportManager
router adapter.DNSRouter
dialer N.Dialer
parallel bool
router adapter.Router
strategy dns.DomainStrategy
server string
initOnce sync.Once
initErr error
queryOptions adapter.DNSQueryOptions
fallbackDelay time.Duration
}
func NewResolveDialer(router adapter.Router, dialer N.Dialer, parallel bool, strategy dns.DomainStrategy, fallbackDelay time.Duration) N.Dialer {
func NewResolveDialer(ctx context.Context, dialer N.Dialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ResolveDialer {
if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel {
return &resolveParallelNetworkDialer{
resolveDialer{
transport: service.FromContext[adapter.DNSTransportManager](ctx),
router: service.FromContext[adapter.DNSRouter](ctx),
dialer: dialer,
parallel: parallel,
server: server,
queryOptions: queryOptions,
fallbackDelay: fallbackDelay,
},
parallelDialer,
}
}
return &resolveDialer{
dialer,
parallel,
router,
strategy,
fallbackDelay,
transport: service.FromContext[adapter.DNSTransportManager](ctx),
router: service.FromContext[adapter.DNSRouter](ctx),
dialer: dialer,
parallel: parallel,
server: server,
queryOptions: queryOptions,
fallbackDelay: fallbackDelay,
}
}
@ -43,59 +74,53 @@ type resolveParallelNetworkDialer struct {
dialer ParallelInterfaceDialer
}
func NewResolveParallelInterfaceDialer(router adapter.Router, dialer ParallelInterfaceDialer, parallel bool, strategy dns.DomainStrategy, fallbackDelay time.Duration) ParallelInterfaceDialer {
return &resolveParallelNetworkDialer{
resolveDialer{
dialer,
parallel,
router,
strategy,
fallbackDelay,
},
dialer,
func (d *resolveDialer) initialize() error {
d.initOnce.Do(d.initServer)
return d.initErr
}
func (d *resolveDialer) initServer() {
if d.server == "" {
return
}
transport, loaded := d.transport.Transport(d.server)
if !loaded {
d.initErr = E.New("domain resolver not found: " + d.server)
return
}
d.queryOptions.Transport = transport
}
func (d *resolveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
err := d.initialize()
if err != nil {
return nil, err
}
if !destination.IsFqdn() {
return d.dialer.DialContext(ctx, network, destination)
}
ctx, metadata := adapter.ExtendContext(ctx)
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
metadata.Destination = destination
metadata.Domain = ""
var addresses []netip.Addr
var err error
if d.strategy == dns.DomainStrategyAsIS {
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
} else {
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
}
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
if err != nil {
return nil, err
}
if d.parallel {
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, d.fallbackDelay)
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
} else {
return N.DialSerial(ctx, d.dialer, network, destination, addresses)
}
}
func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
err := d.initialize()
if err != nil {
return nil, err
}
if !destination.IsFqdn() {
return d.dialer.ListenPacket(ctx, destination)
}
ctx, metadata := adapter.ExtendContext(ctx)
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
metadata.Destination = destination
metadata.Domain = ""
var addresses []netip.Addr
var err error
if d.strategy == dns.DomainStrategyAsIS {
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
} else {
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
}
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
if err != nil {
return nil, err
}
@ -106,21 +131,24 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
}
func (d *resolveDialer) QueryOptions() adapter.DNSQueryOptions {
return d.queryOptions
}
func (d *resolveDialer) Upstream() any {
return d.dialer
}
func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
err := d.initialize()
if err != nil {
return nil, err
}
if !destination.IsFqdn() {
return d.dialer.DialContext(ctx, network, destination)
}
ctx, metadata := adapter.ExtendContext(ctx)
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
metadata.Destination = destination
metadata.Domain = ""
var addresses []netip.Addr
var err error
if d.strategy == dns.DomainStrategyAsIS {
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
} else {
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
}
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
if err != nil {
return nil, err
}
@ -128,30 +156,28 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
fallbackDelay = d.fallbackDelay
}
if d.parallel {
return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
} else {
return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
}
}
func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
err := d.initialize()
if err != nil {
return nil, err
}
if !destination.IsFqdn() {
return d.dialer.ListenPacket(ctx, destination)
}
ctx, metadata := adapter.ExtendContext(ctx)
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
metadata.Destination = destination
metadata.Domain = ""
var addresses []netip.Addr
var err error
if d.strategy == dns.DomainStrategyAsIS {
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
} else {
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
}
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
if err != nil {
return nil, err
}
if fallbackDelay == 0 {
fallbackDelay = d.fallbackDelay
}
conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
if err != nil {
return nil, err
@ -159,6 +185,10 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
}
func (d *resolveDialer) Upstream() any {
func (d *resolveParallelNetworkDialer) QueryOptions() adapter.DNSQueryOptions {
return d.queryOptions
}
func (d *resolveParallelNetworkDialer) Upstream() any {
return d.dialer
}

View File

@ -7,24 +7,27 @@ import (
"github.com/sagernet/sing-box/adapter"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
type DefaultOutboundDialer struct {
outboundManager adapter.OutboundManager
outbound adapter.OutboundManager
}
func NewDefaultOutbound(outboundManager adapter.OutboundManager) N.Dialer {
return &DefaultOutboundDialer{outboundManager: outboundManager}
func NewDefaultOutbound(ctx context.Context) N.Dialer {
return &DefaultOutboundDialer{
outbound: service.FromContext[adapter.OutboundManager](ctx),
}
}
func (d *DefaultOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
return d.outboundManager.Default().DialContext(ctx, network, destination)
return d.outbound.Default().DialContext(ctx, network, destination)
}
func (d *DefaultOutboundDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return d.outboundManager.Default().ListenPacket(ctx, destination)
return d.outbound.Default().ListenPacket(ctx, destination)
}
func (d *DefaultOutboundDialer) Upstream() any {
return d.outboundManager.Default()
return d.outbound.Default()
}

View File

@ -8,11 +8,11 @@ import (
"net"
"os"
"sync"
"sync/atomic"
"time"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
@ -24,9 +24,11 @@ type slowOpenConn struct {
ctx context.Context
network string
destination M.Socksaddr
conn net.Conn
conn atomic.Pointer[net.TCPConn]
create chan struct{}
done chan struct{}
access sync.Mutex
closeOnce sync.Once
err error
}
@ -45,26 +47,30 @@ func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, des
network: network,
destination: destination,
create: make(chan struct{}),
done: make(chan struct{}),
}, nil
}
func (c *slowOpenConn) Read(b []byte) (n int, err error) {
if c.conn == nil {
conn := c.conn.Load()
if conn != nil {
return conn.Read(b)
}
select {
case <-c.create:
if c.err != nil {
return 0, c.err
}
case <-c.ctx.Done():
return 0, c.ctx.Err()
return c.conn.Load().Read(b)
case <-c.done:
return 0, os.ErrClosed
}
}
return c.conn.Read(b)
}
func (c *slowOpenConn) Write(b []byte) (n int, err error) {
if c.conn != nil {
return c.conn.Write(b)
tcpConn := c.conn.Load()
if tcpConn != nil {
return tcpConn.Write(b)
}
c.access.Lock()
defer c.access.Unlock()
@ -73,13 +79,16 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
if c.err != nil {
return 0, c.err
}
return c.conn.Write(b)
return c.conn.Load().Write(b)
case <-c.done:
return 0, os.ErrClosed
default:
}
c.conn, err = c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
conn, err := c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
if err != nil {
c.conn = nil
c.err = E.Cause(err, "dial tcp fast open")
c.err = err
} else {
c.conn.Store(conn.(*net.TCPConn))
}
n = len(b)
close(c.create)
@ -87,74 +96,87 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
}
func (c *slowOpenConn) Close() error {
return common.Close(c.conn)
c.closeOnce.Do(func() {
close(c.done)
conn := c.conn.Load()
if conn != nil {
conn.Close()
}
})
return nil
}
func (c *slowOpenConn) LocalAddr() net.Addr {
if c.conn == nil {
conn := c.conn.Load()
if conn == nil {
return M.Socksaddr{}
}
return c.conn.LocalAddr()
return conn.LocalAddr()
}
func (c *slowOpenConn) RemoteAddr() net.Addr {
if c.conn == nil {
conn := c.conn.Load()
if conn == nil {
return M.Socksaddr{}
}
return c.conn.RemoteAddr()
return conn.RemoteAddr()
}
func (c *slowOpenConn) SetDeadline(t time.Time) error {
if c.conn == nil {
conn := c.conn.Load()
if conn == nil {
return os.ErrInvalid
}
return c.conn.SetDeadline(t)
return conn.SetDeadline(t)
}
func (c *slowOpenConn) SetReadDeadline(t time.Time) error {
if c.conn == nil {
conn := c.conn.Load()
if conn == nil {
return os.ErrInvalid
}
return c.conn.SetReadDeadline(t)
return conn.SetReadDeadline(t)
}
func (c *slowOpenConn) SetWriteDeadline(t time.Time) error {
if c.conn == nil {
conn := c.conn.Load()
if conn == nil {
return os.ErrInvalid
}
return c.conn.SetWriteDeadline(t)
return conn.SetWriteDeadline(t)
}
func (c *slowOpenConn) Upstream() any {
return c.conn
return common.PtrOrNil(c.conn.Load())
}
func (c *slowOpenConn) ReaderReplaceable() bool {
return c.conn != nil
return c.conn.Load() != nil
}
func (c *slowOpenConn) WriterReplaceable() bool {
return c.conn != nil
return c.conn.Load() != nil
}
func (c *slowOpenConn) LazyHeadroom() bool {
return c.conn == nil
return c.conn.Load() == nil
}
func (c *slowOpenConn) NeedHandshake() bool {
return c.conn == nil
return c.conn.Load() == nil
}
func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
if c.conn == nil {
conn := c.conn.Load()
if conn == nil {
select {
case <-c.create:
if c.err != nil {
return 0, c.err
}
case <-c.ctx.Done():
return 0, c.ctx.Err()
case <-c.done:
return 0, c.err
}
}
return bufio.Copy(w, c.conn)
return bufio.Copy(w, c.conn.Load())
}

View File

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

View File

@ -4,6 +4,8 @@ import (
"context"
"net"
"net/netip"
"runtime"
"strings"
"sync/atomic"
"github.com/sagernet/sing-box/adapter"
@ -14,6 +16,8 @@ import (
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/vishvananda/netns"
)
type Listener struct {
@ -28,6 +32,7 @@ type Listener struct {
disablePacketOutput bool
setSystemProxy bool
systemProxySOCKS bool
tproxy bool
tcpListener net.Listener
systemProxy settings.SystemProxy
@ -50,6 +55,7 @@ type Options struct {
DisablePacketOutput bool
SetSystemProxy bool
SystemProxySOCKS bool
TProxy bool
}
func New(
@ -67,6 +73,7 @@ func New(
disablePacketOutput: options.DisablePacketOutput,
setSystemProxy: options.SetSystemProxy,
systemProxySOCKS: options.SystemProxySOCKS,
tproxy: options.TProxy,
}
}
@ -135,3 +142,30 @@ func (l *Listener) UDPConn() *net.UDPConn {
func (l *Listener) ListenOptions() option.ListenOptions {
return l.listenOptions
}
func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (T, error) {
if nameOrPath != "" {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
currentNs, err := netns.Get()
if err != nil {
return common.DefaultValue[T](), E.Cause(err, "get current netns")
}
defer netns.Set(currentNs)
var targetNs netns.NsHandle
if strings.HasPrefix(nameOrPath, "/") {
targetNs, err = netns.GetFromPath(nameOrPath)
} else {
targetNs, err = netns.GetFromName(nameOrPath)
}
if err != nil {
return common.DefaultValue[T](), E.Cause(err, "get netns ", nameOrPath)
}
defer targetNs.Close()
err = netns.Set(targetNs)
if err != nil {
return common.DefaultValue[T](), E.Cause(err, "set netns to ", nameOrPath)
}
}
return block()
}

View File

@ -3,23 +3,39 @@ package listener
import (
"net"
"net/netip"
"syscall"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/redir"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
"github.com/metacubex/tfo-go"
)
func (l *Listener) ListenTCP() (net.Listener, error) {
//nolint:staticcheck
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
}
var err error
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
var tcpListener net.Listener
var listenConfig net.ListenConfig
if l.listenOptions.BindInterface != "" {
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
}
if l.listenOptions.RoutingMark != 0 {
listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
}
if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
}
if l.listenOptions.TCPKeepAlive >= 0 {
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
if keepIdle == 0 {
@ -37,20 +53,26 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
}
setMultiPathTCP(&listenConfig)
}
if l.tproxy {
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
return control.Raw(conn, func(fd uintptr) error {
return redir.TProxy(fd, !M.ParseSocksaddr(address).IsIPv4(), false)
})
})
}
tcpListener, err := ListenNetworkNamespace[net.Listener](l.listenOptions.NetNs, func() (net.Listener, error) {
if l.listenOptions.TCPFastOpen {
var tfoConfig tfo.ListenConfig
tfoConfig.ListenConfig = listenConfig
tcpListener, err = tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
return tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
} else {
tcpListener, err = listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
return listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
}
})
if err != nil {
return nil, err
}
if err == nil {
l.logger.Info("tcp server started at ", tcpListener.Addr())
}
//nolint:staticcheck
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
}
l.tcpListener = tcpListener
return tcpListener, err
}

View File

@ -1,20 +1,34 @@
package listener
import (
"context"
"net"
"net/netip"
"os"
"syscall"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/redir"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
func (l *Listener) ListenUDP() (net.PacketConn, error) {
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
var lc net.ListenConfig
var listenConfig net.ListenConfig
if l.listenOptions.BindInterface != "" {
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
}
if l.listenOptions.RoutingMark != 0 {
listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
}
if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
}
var udpFragment bool
if l.listenOptions.UDPFragment != nil {
udpFragment = *l.listenOptions.UDPFragment
@ -22,9 +36,18 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
udpFragment = l.listenOptions.UDPFragmentDefault
}
if !udpFragment {
lc.Control = control.Append(lc.Control, control.DisableUDPFragment())
listenConfig.Control = control.Append(listenConfig.Control, control.DisableUDPFragment())
}
udpConn, err := lc.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
if l.tproxy {
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
return control.Raw(conn, func(fd uintptr) error {
return redir.TProxy(fd, !M.ParseSocksaddr(address).IsIPv4(), true)
})
})
}
udpConn, err := ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
return listenConfig.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
})
if err != nil {
return nil, err
}
@ -34,6 +57,36 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
return udpConn, err
}
func (l *Listener) DialContext(dialer net.Dialer, ctx context.Context, network string, address string) (net.Conn, error) {
return ListenNetworkNamespace[net.Conn](l.listenOptions.NetNs, func() (net.Conn, error) {
if l.listenOptions.BindInterface != "" {
dialer.Control = control.Append(dialer.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
}
if l.listenOptions.RoutingMark != 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
}
if l.listenOptions.ReuseAddr {
dialer.Control = control.Append(dialer.Control, control.ReuseAddr())
}
return dialer.DialContext(ctx, network, address)
})
}
func (l *Listener) ListenPacket(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.PacketConn, error) {
return ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
if l.listenOptions.BindInterface != "" {
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
}
if l.listenOptions.RoutingMark != 0 {
listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
}
if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
}
return listenConfig.ListenPacket(ctx, network, address)
})
}
func (l *Listener) UDPAddr() M.Socksaddr {
return l.udpAddr
}
@ -111,9 +164,8 @@ func (l *Listener) loopUDPOut() {
if l.shutdown.Load() && E.IsClosed(err) {
return
}
l.udpConn.Close()
l.logger.Error("udp listener write back: ", destination, ": ", err)
return
continue
}
continue
case <-l.packetOutboundClosed:

View File

@ -23,6 +23,7 @@ type Config struct {
}
type Info struct {
ProcessID uint32
ProcessPath string
PackageName string
User string

View File

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

View File

@ -2,14 +2,11 @@ package process
import (
"context"
"fmt"
"net/netip"
"os"
"syscall"
"unsafe"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/winiphlpapi"
"golang.org/x/sys/windows"
)
@ -26,209 +23,39 @@ func NewSearcher(_ Config) (Searcher, error) {
return &windowsSearcher{}, nil
}
var (
modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
procGetExtendedTcpTable = modiphlpapi.NewProc("GetExtendedTcpTable")
procGetExtendedUdpTable = modiphlpapi.NewProc("GetExtendedUdpTable")
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
procQueryFullProcessImageNameW = modkernel32.NewProc("QueryFullProcessImageNameW")
)
func initWin32API() error {
err := modiphlpapi.Load()
if err != nil {
return E.Cause(err, "load iphlpapi.dll")
}
err = procGetExtendedTcpTable.Find()
if err != nil {
return E.Cause(err, "load iphlpapi::GetExtendedTcpTable")
}
err = procGetExtendedUdpTable.Find()
if err != nil {
return E.Cause(err, "load iphlpapi::GetExtendedUdpTable")
}
err = modkernel32.Load()
if err != nil {
return E.Cause(err, "load kernel32.dll")
}
err = procQueryFullProcessImageNameW.Find()
if err != nil {
return E.Cause(err, "load kernel32::QueryFullProcessImageNameW")
}
return nil
return winiphlpapi.LoadExtendedTable()
}
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
pid, err := winiphlpapi.FindPid(network, source)
if err != nil {
return nil, err
}
return &Info{ProcessPath: processName, UserId: -1}, nil
}
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
family := windows.AF_INET
if ip.Is6() {
family = windows.AF_INET6
}
const (
tcpTablePidConn = 4
udpTablePid = 1
)
var class int
var fn uintptr
switch network {
case N.NetworkTCP:
fn = procGetExtendedTcpTable.Addr()
class = tcpTablePidConn
case N.NetworkUDP:
fn = procGetExtendedUdpTable.Addr()
class = udpTablePid
default:
return "", os.ErrInvalid
}
buf, err := getTransportTable(fn, family, class)
path, err := getProcessPath(pid)
if err != nil {
return "", err
return &Info{ProcessID: pid, UserId: -1}, err
}
s := newSearcher(family == windows.AF_INET, network == N.NetworkTCP)
pid, err := s.Search(buf, ip, uint16(srcPort))
if err != nil {
return "", err
}
return getExecPathFromPID(pid)
return &Info{ProcessID: pid, ProcessPath: path, UserId: -1}, nil
}
type searcher struct {
itemSize int
port int
ip int
ipSize int
pid int
tcpState int
}
func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error) {
n := int(readNativeUint32(b[:4]))
itemSize := s.itemSize
for i := 0; i < n; i++ {
row := b[4+itemSize*i : 4+itemSize*(i+1)]
if s.tcpState >= 0 {
tcpState := readNativeUint32(row[s.tcpState : s.tcpState+4])
// MIB_TCP_STATE_ESTAB, only check established connections for TCP
if tcpState != 5 {
continue
}
}
// according to MSDN, only the lower 16 bits of dwLocalPort are used and the port number is in network endian.
// this field can be illustrated as follows depends on different machine endianess:
// little endian: [ MSB LSB 0 0 ] interpret as native uint32 is ((LSB<<8)|MSB)
// big endian: [ 0 0 MSB LSB ] interpret as native uint32 is ((MSB<<8)|LSB)
// so we need an syscall.Ntohs on the lower 16 bits after read the port as native uint32
srcPort := syscall.Ntohs(uint16(readNativeUint32(row[s.port : s.port+4])))
if srcPort != port {
continue
}
srcIP, _ := netip.AddrFromSlice(row[s.ip : s.ip+s.ipSize])
// windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto
if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) {
continue
}
pid := readNativeUint32(row[s.pid : s.pid+4])
return pid, nil
}
return 0, ErrNotFound
}
func newSearcher(isV4, isTCP bool) *searcher {
var itemSize, port, ip, ipSize, pid int
tcpState := -1
switch {
case isV4 && isTCP:
// struct MIB_TCPROW_OWNER_PID
itemSize, port, ip, ipSize, pid, tcpState = 24, 8, 4, 4, 20, 0
case isV4 && !isTCP:
// struct MIB_UDPROW_OWNER_PID
itemSize, port, ip, ipSize, pid = 12, 4, 0, 4, 8
case !isV4 && isTCP:
// struct MIB_TCP6ROW_OWNER_PID
itemSize, port, ip, ipSize, pid, tcpState = 56, 20, 0, 16, 52, 48
case !isV4 && !isTCP:
// struct MIB_UDP6ROW_OWNER_PID
itemSize, port, ip, ipSize, pid = 28, 20, 0, 16, 24
}
return &searcher{
itemSize: itemSize,
port: port,
ip: ip,
ipSize: ipSize,
pid: pid,
tcpState: tcpState,
}
}
func getTransportTable(fn uintptr, family int, class int) ([]byte, error) {
for size, buf := uint32(8), make([]byte, 8); ; {
ptr := unsafe.Pointer(&buf[0])
err, _, _ := syscall.SyscallN(fn, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0)
switch err {
case 0:
return buf, nil
case uintptr(syscall.ERROR_INSUFFICIENT_BUFFER):
buf = make([]byte, size)
default:
return nil, fmt.Errorf("syscall error: %d", err)
}
}
}
func readNativeUint32(b []byte) uint32 {
return *(*uint32)(unsafe.Pointer(&b[0]))
}
func getExecPathFromPID(pid uint32) (string, error) {
// kernel process starts with a colon in order to distinguish with normal processes
func getProcessPath(pid uint32) (string, error) {
switch pid {
case 0:
// reserved pid for system idle process
return ":System Idle Process", nil
case 4:
// reserved pid for windows kernel image
return ":System", nil
}
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
if err != nil {
return "", err
}
defer windows.CloseHandle(h)
defer windows.CloseHandle(handle)
size := uint32(syscall.MAX_LONG_PATH)
buf := make([]uint16, syscall.MAX_LONG_PATH)
size := uint32(len(buf))
r1, _, err := syscall.SyscallN(
procQueryFullProcessImageNameW.Addr(),
uintptr(h),
uintptr(0),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
)
if r1 == 0 {
err = windows.QueryFullProcessImageName(handle, 0, &buf[0], &size)
if err != nil {
return "", err
}
return syscall.UTF16ToString(buf[:size]), nil
return windows.UTF16ToString(buf[:size]), nil
}

View File

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

View File

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

View File

@ -1,12 +0,0 @@
//go:build !(windows || linux || darwin)
package settings
import (
"os"
"time"
)
func SetSystemTime(nowTime time.Time) error {
return os.ErrInvalid
}

View File

@ -1,14 +0,0 @@
//go:build linux || darwin
package settings
import (
"time"
"golang.org/x/sys/unix"
)
func SetSystemTime(nowTime time.Time) error {
timeVal := unix.NsecToTimeval(nowTime.UnixNano())
return unix.Settimeofday(&timeVal)
}

View File

@ -1,32 +0,0 @@
package settings
import (
"time"
"unsafe"
"golang.org/x/sys/windows"
)
func SetSystemTime(nowTime time.Time) error {
var systemTime windows.Systemtime
systemTime.Year = uint16(nowTime.Year())
systemTime.Month = uint16(nowTime.Month())
systemTime.Day = uint16(nowTime.Day())
systemTime.Hour = uint16(nowTime.Hour())
systemTime.Minute = uint16(nowTime.Minute())
systemTime.Second = uint16(nowTime.Second())
systemTime.Milliseconds = uint16(nowTime.UnixMilli() - nowTime.Unix()*1000)
dllKernel32 := windows.NewLazySystemDLL("kernel32.dll")
proc := dllKernel32.NewProc("SetSystemTime")
_, _, err := proc.Call(
uintptr(unsafe.Pointer(&systemTime)),
)
if err != nil && err.Error() != "The operation completed successfully." {
return err
}
return nil
}

View File

@ -9,6 +9,7 @@ import (
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
)
const (
@ -23,21 +24,26 @@ func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.R
var first byte
err := binary.Read(reader, binary.BigEndian, &first)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if first != 19 {
return os.ErrInvalid
}
const header = "BitTorrent protocol"
var protocol [19]byte
_, err = reader.Read(protocol[:])
if err != nil {
return err
}
if string(protocol[:]) != "BitTorrent protocol" {
var n int
n, err = reader.Read(protocol[:])
if string(protocol[:n]) != header[:n] {
return os.ErrInvalid
}
if err != nil {
return E.Cause1(ErrNeedMoreData, err)
}
if n < 19 {
return ErrNeedMoreData
}
metadata.Protocol = C.ProtocolBitTorrent
return nil
@ -67,7 +73,9 @@ func UTP(_ context.Context, metadata *adapter.InboundContext, packet []byte) err
if err != nil {
return err
}
if extension > 0x04 {
return os.ErrInvalid
}
var length byte
err = binary.Read(reader, binary.BigEndian, &length)
if err != nil {

View File

@ -32,6 +32,27 @@ func TestSniffBittorrent(t *testing.T) {
}
}
func TestSniffIncompleteBittorrent(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("13426974546f7272656e74")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt))
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
}
func TestSniffNotBittorrent(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("13426974546f7272656e75")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt))
require.NotEmpty(t, err)
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
}
func TestSniffUTP(t *testing.T) {
t.Parallel()
@ -71,3 +92,19 @@ func TestSniffUDPTracker(t *testing.T) {
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
}
}
func TestSniffNotUTP(t *testing.T) {
t.Parallel()
packets := []string{
"0102736470696e674958d580121500000000000079aaed6717a39c27b07c0c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
}
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.Error(t, err)
}
}

View File

@ -5,14 +5,11 @@ import (
"encoding/binary"
"io"
"os"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/task"
E "github.com/sagernet/sing/common/exceptions"
mDNS "github.com/miekg/dns"
)
@ -21,35 +18,40 @@ func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundCon
var length uint16
err := binary.Read(reader, binary.BigEndian, &length)
if err != nil {
return os.ErrInvalid
return E.Cause1(ErrNeedMoreData, err)
}
if length == 0 {
if length < 12 {
return os.ErrInvalid
}
buffer := buf.NewSize(int(length))
defer buffer.Release()
readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100)
var readTask task.Group
readTask.Append0(func(ctx context.Context) error {
return common.Error(buffer.ReadFullFrom(reader, buffer.FreeLen()))
})
err = readTask.Run(readCtx)
cancel()
if err != nil {
return err
var n int
n, err = buffer.ReadFullFrom(reader, buffer.FreeLen())
packet := buffer.Bytes()
if n > 2 && packet[2]&0x80 != 0 { // QR
return os.ErrInvalid
}
return DomainNameQuery(readCtx, metadata, buffer.Bytes())
if n > 5 && packet[4] == 0 && packet[5] == 0 { // QDCOUNT
return os.ErrInvalid
}
for i := 6; i < 10; i++ {
// ANCOUNT, NSCOUNT
if n > i && packet[i] != 0 {
return os.ErrInvalid
}
}
if err != nil {
return E.Cause1(ErrNeedMoreData, err)
}
return DomainNameQuery(readCtx, metadata, packet)
}
func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
var msg mDNS.Msg
err := msg.Unpack(packet)
if err != nil {
if err != nil || msg.Response || len(msg.Question) == 0 || len(msg.Answer) > 0 || len(msg.Ns) > 0 {
return err
}
if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolDNS
return nil
}

53
common/sniff/dns_test.go Normal file
View File

@ -0,0 +1,53 @@
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 TestSniffDNS(t *testing.T) {
t.Parallel()
query, err := hex.DecodeString("740701000001000000000000012a06676f6f676c6503636f6d0000010001")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.DomainNameQuery(context.TODO(), &metadata, query)
require.NoError(t, err)
require.Equal(t, C.ProtocolDNS, metadata.Protocol)
}
func TestSniffStreamDNS(t *testing.T) {
t.Parallel()
query, err := hex.DecodeString("001e740701000001000000000000012a06676f6f676c6503636f6d0000010001")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
require.NoError(t, err)
require.Equal(t, C.ProtocolDNS, metadata.Protocol)
}
func TestSniffIncompleteStreamDNS(t *testing.T) {
t.Parallel()
query, err := hex.DecodeString("001e740701000001000000000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
}
func TestSniffNotStreamDNS(t *testing.T) {
t.Parallel()
query, err := hex.DecodeString("001e740701000000000000000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
require.NotEmpty(t, err)
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
}

View File

@ -3,10 +3,12 @@ package sniff
import (
std_bufio "bufio"
"context"
"errors"
"io"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/protocol/http"
)
@ -14,8 +16,12 @@ import (
func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
request, err := http.ReadRequest(std_bufio.NewReader(reader))
if err != nil {
if errors.Is(err, io.ErrUnexpectedEOF) {
return E.Cause1(ErrNeedMoreData, err)
} else {
return err
}
}
metadata.Protocol = C.ProtocolHTTP
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
return nil

58
common/sniff/ntp.go Normal file
View File

@ -0,0 +1,58 @@
package sniff
import (
"context"
"encoding/binary"
"os"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
)
func NTP(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
// NTP packets must be at least 48 bytes long (standard NTP header size).
pLen := len(packet)
if pLen < 48 {
return os.ErrInvalid
}
// Check the LI (Leap Indicator) and Version Number (VN) in the first byte.
// We'll primarily focus on ensuring the version is valid for NTP.
// Many NTP versions are used, but let's check for generally accepted ones (3 & 4 for IPv4, plus potential extensions/customizations)
firstByte := packet[0]
li := (firstByte >> 6) & 0x03 // Extract LI
vn := (firstByte >> 3) & 0x07 // Extract VN
mode := firstByte & 0x07 // Extract Mode
// Leap Indicator should be a valid value (0-3).
if li > 3 {
return os.ErrInvalid
}
// Version Check (common NTP versions are 3 and 4)
if vn != 3 && vn != 4 {
return os.ErrInvalid
}
// Check the Mode field for a client request (Mode 3). This validates it *is* a request.
if mode != 3 {
return os.ErrInvalid
}
// Check Root Delay and Root Dispersion. While not strictly *required* for a request,
// we can check if they appear to be reasonable values (not excessively large).
rootDelay := binary.BigEndian.Uint32(packet[4:8])
rootDispersion := binary.BigEndian.Uint32(packet[8:12])
// Check for unreasonably large root delay and dispersion. NTP RFC specifies max values of approximately 16 seconds.
// Convert to milliseconds for easy comparison. Each unit is 1/2^16 seconds.
if float64(rootDelay)/65536.0 > 16.0 {
return os.ErrInvalid
}
if float64(rootDispersion)/65536.0 > 16.0 {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolNTP
return nil
}

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

@ -0,0 +1,33 @@
package sniff_test
import (
"context"
"encoding/hex"
"os"
"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 TestSniffNTP(t *testing.T) {
t.Parallel()
packet, err := hex.DecodeString("1b0006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.NTP(context.Background(), &metadata, packet)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolNTP)
}
func TestSniffNTPFailed(t *testing.T) {
t.Parallel()
packet, err := hex.DecodeString("400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.NTP(context.Background(), &metadata, packet)
require.ErrorIs(t, err, os.ErrInvalid)
}

View File

@ -20,8 +20,6 @@ import (
"golang.org/x/crypto/hkdf"
)
var ErrClientHelloFragmented = E.New("need more packet for chromium QUIC connection")
func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
reader := bytes.NewReader(packet)
typeByte, err := reader.ReadByte()
@ -308,7 +306,7 @@ find:
metadata.Protocol = C.ProtocolQUIC
metadata.Client = C.ClientChromium
metadata.SniffContext = fragments
return ErrClientHelloFragmented
return E.Cause1(ErrNeedMoreData, err)
}
metadata.Domain = fingerprint.ServerName
for metadata.Client == "" {

View File

@ -17,8 +17,5 @@ var uQUICChrome115 = &ja3.ClientHello{
}
func maybeUQUIC(fingerprint *ja3.ClientHello) bool {
if uQUICChrome115.Equals(fingerprint, true) {
return true
}
return false
return !uQUICChrome115.Equals(fingerprint, true)
}

View File

@ -12,6 +12,26 @@ import (
"github.com/stretchr/testify/require"
)
func TestSniffQUICChromeNew(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("ca0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489ad89c322f75f9a383c90d126a0b21104cb519c2bb32e6a134e86896452e942b26c519b8c7ac9e4c99fae5e1f65cf08fb98443b30e4567932e8fb0789820d8f33037b59ac8113530258c9467dfb52489396dae01f099d28b234efa107fa411f2a1ffa2abe74988e03d662d4296024e95ce0fe1671724937157f77b84990478a2d4060676cf0827b4e8c600654111750414dafa0cccb332f3020c2922a015f445df5edc9c7d2d1ceea9fddcc9ff821c9183aa39a70da20fcc057579e1051c1c899148d6cf9d08b4919822082d040d1ce03ca4f216be6cb7ef03db6df0993ef1ccce5c8c648980554f41704526e1809d2545739f5872e75ec797db1c99f5682e2eda9363cb32aa367b7b363c782ddbacf874183cc15c8a2db068dd4093eebdd096ad33832a7939deb0a872279744f5a56dc001ba62fac973bf680f3b362bdd336add4dd102f462b773bf70bfce1921070a802a92025273a177186d1a643081b42175eb789ccddadb71033ef4feacbf6fd282ab622cf61669d73cda559e411c6ccdd8f003443b6933b7729b7a357aa4aa2fba0f365f829a4d497afb5dc2648a53bc9f3e786d955069d0a4781088a5463747dfe9958ea19ea444eae947ec6a67640955f710f93640084f3fbb8ad259b68dbc0ee0b7fab2d81bffd83ed8a6d33522dbfef43bec0a0fb4bdf1cb712dc4ced0680c0687fa240fd157baa232b1c84e14adce6421cf9270f9b3972f98fc67b344b8a4f1fb551e26f7f76d484ed9f8197f231dc5d9a44cc0ddce73d7f810a620851f4e97eb5037ab5135d7c3be5b80cc32d19910b8387aca64c93c02dc3e35238b78e6aff470722078982e58802844932b6041446bfdcc97ba640cbb86721bcd0f40f27b77aa6287ce5674ec1720134b9302875482c3269787e004b9edb483d44f326eef38c0e83cb46af96488c2e696bc2524567fb29c1e8edcd5a73615496d172d46a9d29e0505c0018b7bbb00165eca0389e09c4b1d73b6cc4a2f735a720650134a2e98e8105e20695cf231b92586237dfe0f99c897414e51c21627496276535f07abb53fb2b554376fe520fa45a3e944fd91dfe7a72aead08842b6b63d8edf861fb911954c83bd9a896eb9da4af5eff646455069d747facd4e77c254096843bff7c3e9031dbdf8dc37ea45f1122922fcbc322ec1378f3c7c1af0da62e1052e6210f1b23073f93a82d90e14cb20bc4501d487a1c848674d57a7c269b13590b3a99d8b8b4f6d0dfbd1d2cbbe7a32c0d5c84ae7ec438b0b19f3862d8fabaa828d06c7e3c6967405cd56a1ae90f38633e2ee0e3ecfca3df399fe12f029e0860a1a30da010300d0c94f0bf56091d00011488c1429928b21c739ebf50ba8be91116315d3173f6d2c56735722478c4d74392ba84d1727036b3d64e8c2263b0f33cb8086be587ca6b3940259c06afa2683868856529303ae12e91d7ca874568be7f2bfaa0656dfab0ed31ed90eaea10fb7f3433ec59a334abe6211d547fa0c825ac45d3691e749d15432008de83e9f6d98f368359137ae803d9189b3386f800c7c0cf4b615d1983cf82d9981a8105b60a80fe66c9b0d439b5ba153dd19e9e7483a01cf3b02b4597540b38e658d4eb8455e030b2bf2690bdd78c23f16fe5")
require.NoError(t, err)
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.ErrNeedMoreData)
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
require.NoError(t, err)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
pkt, err = hex.DecodeString("c20000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489e2ff30c43a5f63beb2e4501ce7754085bcbe838003a0b4bccb53863c0766df7eac073c2bdc170772b157997945acdc2ab2e84750cc9aa0ffa0fdc023da7fc565a14f87f7c563dbc9183dd226aab79957d263f66e64b85a1b15a24516bd2c7c04eea4fa0a34ef9849c21585db2e4adb7c05e265c4f38d8ffe4cbed0f3b0e68f3693bf1f726c3fb135b8e32a5d22931d7c55fc2ff4b9a354933ab14544df3cdaf3e3217dfb8d7feb3465dc34df6320ea486f12e5b2d609aaa5f4515c20c86fc440f8087be0ee3d339835746ae2573c2afdee6bb6ef7e9eb541feae9209391b2902cfb0bdaccd9da8d290714638b7da588d4a656ca6eabba78b7363922d6037cf060b161a42019d4feb4156459103cffdeefd0e63114af2b0e0c39e70ebc7fecb8dd1ebb8d60b2137f509bb7dcef5f1d3e06ab1d391466652d57440a410fb4f58a6ce1fb62feb453241f64e110709f59a3d9ebdac94f811337d0e4a80fd6b56b2a70cd6eebbf98e1661291da6bf5beb8b8afc376dfd20eb76afe709e8e8f28e0ef82105954e346546ad25973df43f4acddbec0ffd9b215f62abebebf71305b5ea993560316f69430bf5afe50420340622f802b5830f3bcebffff04980c75a59d28902879e5d51a4fb21062a4ae13c42297075b21d54ee04303879c1157e7470c1451673c98a2f3921f2f3e8f6acfe85b01caaca66b59e5ebffbfe68e5e9ab17e9a1b857eb409df91cb76767fc1814fd3c522a9b117edd0b02526e469cb4afb291a4dcc74c79b47ec6e7ce558c597129366f83ec306b11d2598c705fd4ee9ee99df6b7039bef13b08fc6f26853ad213829d24f895747d45a47414f931c583fb6c3e4f6c27d0c2b81a5f3cee390ec6314e1fec637e8d28b675e97caafdfbf8c25d34a635083a7553d219dd80dbb39087d74c6ad6192ca6f48a3ff8d47db41b2a492c63fcd780012780931dae0a325f9dcbd772d09a700f132c4bc1d9809b25b9751b694eb72a8ba4db7208d2b1bab63e1845208e4f841ea30218a559db98751589716b6d059ca673378f5fe7c7d8a1c82e14a561c47313bbcc278412ba86ffb2b87ec308eab9df696f5b4b54f8e361731bf232820a02a35fda7e5d4bf01b8f005ad299a055116e7b23c181f15a66442cf6032ca477bccc55b79d424eb4f245847bd81a581dc369dd20b1a4892733bde3c38e492c0039f69f2b947a4dc251a49ee7ccc0f36b3b75a555fa1d126db75f94dab60f52f6b15a877a0c380b59f82d35c570bc5f8051e9ef87db51f52383d47b50829b7f9e947ccc67aa280566aa48b4a85c1c7eca6f542789d8abcc050f1aa3cc221b6859656a21454aa21c7bfb9d12115f61c3ed46263ade68a8d3679fa62a659a5da7817406bd16618fccf33ed208ada1b03584e8b485d3cb6ed80a0774e60b6cd55aff64169ea998cf8235997049515abac58e0169ca07fb1c8c4c8b2803ba9d27b44c045d0a1cac86e5e188195c68001f53eb44851b6d821fc01ccbb41e27f38e6ddd66540c2d62ed6e0d551e22c0f26b60078c74a6302a1ed3d9e8fc0861257a63f6ac4e759fd54bff088becd28e30944a6c15db4fc8ae6244346869add946d9d92c430d737e042fa18b28a8ed64d1e8987ad9061cdc1335f")
require.NoError(t, err)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, "www.google.com", metadata.Domain)
}
func TestSniffQUICChromium(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620")
@ -20,7 +40,7 @@ func TestSniffQUICChromium(t *testing.T) {
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)
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
require.NoError(t, err)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)

View File

@ -8,6 +8,7 @@ import (
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
)
@ -15,7 +16,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var tpktVersion uint8
err := binary.Read(reader, binary.BigEndian, &tpktVersion)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if tpktVersion != 0x03 {
return os.ErrInvalid
@ -24,7 +25,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var tpktReserved uint8
err = binary.Read(reader, binary.BigEndian, &tpktReserved)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if tpktReserved != 0x00 {
return os.ErrInvalid
@ -33,7 +34,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var tpktLength uint16
err = binary.Read(reader, binary.BigEndian, &tpktLength)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if tpktLength != 19 {
@ -43,7 +44,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var cotpLength uint8
err = binary.Read(reader, binary.BigEndian, &cotpLength)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if cotpLength != 14 {
@ -53,7 +54,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var cotpTpduType uint8
err = binary.Read(reader, binary.BigEndian, &cotpTpduType)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if cotpTpduType != 0xE0 {
return os.ErrInvalid
@ -61,13 +62,13 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
err = rw.SkipN(reader, 5)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
var rdpType uint8
err = binary.Read(reader, binary.BigEndian, &rdpType)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if rdpType != 0x01 {
return os.ErrInvalid
@ -75,12 +76,12 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var rdpFlags uint8
err = binary.Read(reader, binary.BigEndian, &rdpFlags)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
var rdpLength uint8
err = binary.Read(reader, binary.BigEndian, &rdpLength)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if rdpLength != 8 {
return os.ErrInvalid

View File

@ -3,12 +3,14 @@ package sniff
import (
"bytes"
"context"
"errors"
"io"
"net"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
)
@ -18,6 +20,8 @@ type (
PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error
)
var ErrNeedMoreData = E.New("need more data")
func Skip(metadata *adapter.InboundContext) bool {
// skip server first protocols
switch metadata.Destination.Port {
@ -34,12 +38,12 @@ func Skip(metadata *adapter.InboundContext) bool {
return false
}
func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error {
func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffers []*buf.Buffer, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error {
if timeout == 0 {
timeout = C.ReadPayloadTimeout
}
deadline := time.Now().Add(timeout)
var errors []error
var sniffError error
for i := 0; ; i++ {
err := conn.SetReadDeadline(deadline)
if err != nil {
@ -53,26 +57,32 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.
}
return E.Cause(err, "read payload")
}
errors = nil
sniffError = nil
for _, sniffer := range sniffers {
err = sniffer(ctx, metadata, bytes.NewReader(buffer.Bytes()))
reader := io.MultiReader(common.Map(append(buffers, buffer), func(it *buf.Buffer) io.Reader {
return bytes.NewReader(it.Bytes())
})...)
err = sniffer(ctx, metadata, reader)
if err == nil {
return nil
}
errors = append(errors, err)
sniffError = E.Errors(sniffError, err)
}
if !errors.Is(sniffError, ErrNeedMoreData) {
break
}
}
return E.Errors(errors...)
return sniffError
}
func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error {
var errors []error
var sniffError []error
for _, sniffer := range sniffers {
err := sniffer(ctx, metadata, packet)
if err == nil {
return nil
}
errors = append(errors, err)
sniffError = append(sniffError, err)
}
return E.Errors(errors...)
return E.Errors(sniffError...)
}

View File

@ -5,22 +5,27 @@ import (
"context"
"io"
"os"
"strings"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
)
func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
scanner := bufio.NewScanner(reader)
if !scanner.Scan() {
const sshPrefix = "SSH-2.0-"
bReader := bufio.NewReader(reader)
prefix, err := bReader.Peek(len(sshPrefix))
if string(prefix[:]) != sshPrefix[:len(prefix)] {
return os.ErrInvalid
}
fistLine := scanner.Text()
if !strings.HasPrefix(fistLine, "SSH-2.0-") {
return os.ErrInvalid
if err != nil {
return E.Cause1(ErrNeedMoreData, err)
}
fistLine, _, err := bReader.ReadLine()
if err != nil {
return err
}
metadata.Protocol = C.ProtocolSSH
metadata.Client = fistLine[8:]
metadata.Client = string(fistLine)[8:]
return nil
}

View File

@ -24,3 +24,24 @@ func TestSniffSSH(t *testing.T) {
require.Equal(t, C.ProtocolSSH, metadata.Protocol)
require.Equal(t, "dropbear", metadata.Client)
}
func TestSniffIncompleteSSH(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("5353482d322e30")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt))
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
}
func TestSniffNotSSH(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("5353482d322e31")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt))
require.NotEmpty(t, err)
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
}

View File

@ -3,11 +3,13 @@ package sniff
import (
"context"
"crypto/tls"
"errors"
"io"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
)
func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
@ -23,5 +25,9 @@ func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reade
metadata.Domain = clientHello.ServerName
return nil
}
if errors.Is(err, io.ErrUnexpectedEOF) {
return E.Cause1(ErrNeedMoreData, err)
} else {
return err
}
}

View File

@ -12,6 +12,8 @@ import (
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/domain"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
"github.com/sagernet/sing/common/varbin"
"go4.org/netipx"
@ -41,6 +43,8 @@ const (
ruleItemNetworkType
ruleItemNetworkIsExpensive
ruleItemNetworkIsConstrained
ruleItemNetworkInterfaceAddress
ruleItemDefaultInterfaceAddress
ruleItemFinal uint8 = 0xFF
)
@ -215,22 +219,66 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
case ruleItemWIFIBSSID:
rule.WIFIBSSID, err = readRuleItemString(reader)
case ruleItemAdGuardDomain:
if recover {
err = E.New("unable to decompile binary AdGuard rules to rule-set")
return
}
var matcher *domain.AdGuardMatcher
matcher, err = domain.ReadAdGuardMatcher(reader)
if err != nil {
return
}
rule.AdGuardDomainMatcher = matcher
if recover {
rule.AdGuardDomain = matcher.Dump()
}
case ruleItemNetworkType:
rule.NetworkType, err = readRuleItemUint8[option.InterfaceType](reader)
case ruleItemNetworkIsExpensive:
rule.NetworkIsExpensive = true
case ruleItemNetworkIsConstrained:
rule.NetworkIsConstrained = true
case ruleItemNetworkInterfaceAddress:
rule.NetworkInterfaceAddress = new(badjson.TypedMap[option.InterfaceType, badoption.Listable[*badoption.Prefixable]])
var size uint64
size, err = binary.ReadUvarint(reader)
if err != nil {
return
}
for i := uint64(0); i < size; i++ {
var key uint8
err = binary.Read(reader, binary.BigEndian, &key)
if err != nil {
return
}
var value []*badoption.Prefixable
var prefixCount uint64
prefixCount, err = binary.ReadUvarint(reader)
if err != nil {
return
}
for j := uint64(0); j < prefixCount; j++ {
var prefix netip.Prefix
prefix, err = readPrefix(reader)
if err != nil {
return
}
value = append(value, common.Ptr(badoption.Prefixable(prefix)))
}
rule.NetworkInterfaceAddress.Put(option.InterfaceType(key), value)
}
case ruleItemDefaultInterfaceAddress:
var value []*badoption.Prefixable
var prefixCount uint64
prefixCount, err = binary.ReadUvarint(reader)
if err != nil {
return
}
for j := uint64(0); j < prefixCount; j++ {
var prefix netip.Prefix
prefix, err = readPrefix(reader)
if err != nil {
return
}
value = append(value, common.Ptr(badoption.Prefixable(prefix)))
}
rule.DefaultInterfaceAddress = value
case ruleItemFinal:
err = binary.Read(reader, binary.BigEndian, &rule.Invert)
return
@ -347,7 +395,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
}
if len(rule.NetworkType) > 0 {
if generateVersion < C.RuleSetVersion3 {
return E.New("network_type rule item is only supported in version 3 or later")
return E.New("`network_type` rule item is only supported in version 3 or later")
}
err = writeRuleItemUint8(writer, ruleItemNetworkType, rule.NetworkType)
if err != nil {
@ -355,17 +403,71 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
}
}
if rule.NetworkIsExpensive {
if generateVersion < C.RuleSetVersion3 {
return E.New("`network_is_expensive` rule item is only supported in version 3 or later")
}
err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsExpensive)
if err != nil {
return err
}
}
if rule.NetworkIsConstrained {
if generateVersion < C.RuleSetVersion3 {
return E.New("`network_is_constrained` rule item is only supported in version 3 or later")
}
err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsConstrained)
if err != nil {
return err
}
}
if rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 {
if generateVersion < C.RuleSetVersion4 {
return E.New("`network_interface_address` rule item is only supported in version 4 or later")
}
err = writer.WriteByte(ruleItemNetworkInterfaceAddress)
if err != nil {
return err
}
_, err = varbin.WriteUvarint(writer, uint64(rule.NetworkInterfaceAddress.Size()))
if err != nil {
return err
}
for _, entry := range rule.NetworkInterfaceAddress.Entries() {
err = binary.Write(writer, binary.BigEndian, uint8(entry.Key.Build()))
if err != nil {
return err
}
_, err = varbin.WriteUvarint(writer, uint64(len(entry.Value)))
if err != nil {
return err
}
for _, rawPrefix := range entry.Value {
err = writePrefix(writer, rawPrefix.Build(netip.Prefix{}))
if err != nil {
return err
}
}
}
}
if len(rule.DefaultInterfaceAddress) > 0 {
if generateVersion < C.RuleSetVersion4 {
return E.New("`default_interface_address` rule item is only supported in version 4 or later")
}
err = writer.WriteByte(ruleItemDefaultInterfaceAddress)
if err != nil {
return err
}
_, err = varbin.WriteUvarint(writer, uint64(len(rule.DefaultInterfaceAddress)))
if err != nil {
return err
}
for _, rawPrefix := range rule.DefaultInterfaceAddress {
err = writePrefix(writer, rawPrefix.Build(netip.Prefix{}))
if err != nil {
return err
}
}
}
if len(rule.WIFISSID) > 0 {
err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID)
if err != nil {

33
common/srs/ip_cidr.go Normal file
View File

@ -0,0 +1,33 @@
package srs
import (
"encoding/binary"
"net/netip"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/varbin"
)
func readPrefix(reader varbin.Reader) (netip.Prefix, error) {
addrSlice, err := varbin.ReadValue[[]byte](reader, binary.BigEndian)
if err != nil {
return netip.Prefix{}, err
}
prefixBits, err := varbin.ReadValue[uint8](reader, binary.BigEndian)
if err != nil {
return netip.Prefix{}, err
}
return netip.PrefixFrom(M.AddrFromIP(addrSlice), int(prefixBits)), nil
}
func writePrefix(writer varbin.Writer, prefix netip.Prefix) error {
err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice())
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, uint8(prefix.Bits()))
if err != nil {
return err
}
return nil
}

View File

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

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