Compare commits

..

201 Commits

Author SHA1 Message Date
Sergei Maklagin
9dc526ea1f Resolve conflicts 2025-08-15 12:57:47 +03:00
Sergei Maklagin
93eb435e26 Resolve conflicts 2025-08-15 12:56:52 +03: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: 9533031891 ("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
Sergei Maklagin
e22416f0d9 Merge branch 'extended' of https://github.com/shtorm-7/sing-box-extended into extended 2025-07-14 13:27:54 +03:00
Sergei Maklagin
89497dbfd5 Update dependencies 2025-07-13 21:48:31 +03:00
Sergei Maklagin
8388abbb77 Resolve conflicts 2025-07-13 21:44:23 +03:00
Sergei Maklagin
180b7c4134 Fix tunnel client 2025-07-13 21:41:41 +03:00
Sergei Maklagin
deda2cca5e Fix xhttp transport 2025-07-13 21:40:34 +03: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
Shtorm
691ecd45a9 Update README.md
Signed-off-by: Shtorm <108103062+shtorm-7@users.noreply.github.com>
2025-07-06 21:16:54 +03:00
Sergei Maklagin
209b89a4a3 Add tunnel 2025-07-06 18:31:06 +03: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
Sergei Maklagin
765111a552 Merge tag 'v1.11.14' into HEAD 2025-06-19 20:18:51 +03: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
Sergei Maklagin
824eac453b Add new examples 2025-06-15 22:23:42 +03:00
Sergei Maklagin
85a1a8a53b Format examples 2025-06-15 22:23:25 +03:00
Sergei Maklagin
ae9e7aa5f4 Fix Range 2025-06-15 22:21:58 +03:00
Sergei Maklagin
52b71c6f00 Add sdns transport 2025-06-15 20:47:05 +03:00
Sergei Maklagin
5d04673783 Fix XHTTP Fqdn 2025-06-15 19:41:22 +03:00
Sergei Maklagin
307c429715 Fix WARP endpoint error 2025-06-15 19:40:42 +03:00
Sergei Maklagin
6801b58d96 Fix interrupt_exist_connections 2025-06-15 19:32:14 +03:00
Sergei Maklagin
6272596ebc Add unified delay 2025-06-15 19:24:09 +03:00
Sergei Maklagin
7c4c2d5ca8 Add mieru protocol 2025-06-15 18:04:27 +03: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
Sergei Maklagin
6768c77fa0 Merge tag 'v1.11.13' into HEAD 2025-06-08 22:43:21 +03:00
Sergei Maklagin
0dff811977 Add examples 2025-06-08 22:38:32 +03:00
Sergei Maklagin
e1a58d12fe Resolve conflicts 2025-06-08 19:54:17 +03:00
Sergei Maklagin
4e74a4108d refactor: WARP 2025-06-08 19:51:17 +03:00
Sergei Maklagin
0238c54261 Add xhttp transport 2025-06-08 19:35:59 +03: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
Sergei Maklagin
5c911c97d8 Added WARP endpoint 2025-06-01 22:06:34 +03:00
Sergei Maklagin
2cfc8092ad Fix endpoint manager locks 2025-05-29 22:48:15 +03: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
Sergei Maklagin
26bd698462 Integrate AmneziaWG 2025-05-24 23:54:15 +03:00
201 changed files with 9443 additions and 889 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -108,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:
@@ -175,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:
@@ -225,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.6
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.6
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.7
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.7
docs:
venv/bin/mkdocs serve

View File

@@ -1,12 +1,20 @@
# sing-box
# sing-box-extended
The universal proxy platform.
Sing-box with extended features.
[![Packaging status](https://repology.org/badge/vertical-allrepos/sing-box.svg)](https://repology.org/project/sing-box/versions)
## Features
## Documentation
* Amnezia
* WARP
* Tunneling
* Mieru
* XHTTP
* SDNS (DNSCrypt)
* Unified delay
https://sing-box.sagernet.org
## Examples
https://github.com/shtorm-7/sing-box-extended/tree/extended/examples
## License
@@ -28,4 +36,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
In addition, no derivative work may use the name or imply association
with this application without prior consent.
```
```

View File

@@ -35,7 +35,6 @@ func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Ma
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
defer m.access.Unlock()
if m.started && m.stage >= stage {
panic("already started")
}
@@ -43,9 +42,12 @@ func (m *Manager) Start(stage adapter.StartStage) error {
m.stage = stage
if stage == adapter.StartStateStart {
// started with outbound manager
m.access.Unlock()
return nil
}
for _, endpoint := range m.endpoints {
endpoints := m.endpoints
m.access.Unlock()
for _, endpoint := range endpoints {
err := adapter.LegacyStart(endpoint, stage)
if err != nil {
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")

View File

@@ -44,6 +44,8 @@ type CacheFile interface {
StoreRDRC() bool
RDRCStore
StoreWARPConfig() bool
LoadMode() string
StoreMode(mode string) error
LoadSelected(group string) string
@@ -52,6 +54,8 @@ type CacheFile interface {
StoreGroupExpand(group string, expand bool) error
LoadRuleSet(tag string) *SavedBinary
SaveRuleSet(tag string, set *SavedBinary) error
LoadWARPConfig(tag string) *SavedBinary
SaveWARPConfig(tag string, set *SavedBinary) error
}
type SavedBinary struct {

View File

@@ -42,22 +42,24 @@ type InboundManager interface {
}
type InboundContext struct {
Inbound string
InboundType string
IPVersion uint8
Network string
Source M.Socksaddr
Destination M.Socksaddr
User string
Outbound string
Inbound string
InboundType string
IPVersion uint8
Network string
Source M.Socksaddr
Destination M.Socksaddr
TunnelSource string
TunnelDestination string
User string
Outbound string
// sniffer
Protocol string
Domain string
Client string
SniffContext any
PacketSniffError error
Protocol string
Domain string
Client string
SniffContext any
SniffError error
// cache

6
box.go
View File

@@ -17,6 +17,7 @@ import (
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/taskmonitor"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/common/urltest"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport/local"
@@ -139,6 +140,9 @@ func New(options Options) (*Box, error) {
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
needV2RayAPI = true
}
if experimentalOptions.UnifiedDelay != nil && experimentalOptions.UnifiedDelay.Enabled {
ctx = urltest.ContextWithIsUnifiedDelay(ctx)
}
platformInterface := service.FromContext[platform.Interface](ctx)
var defaultLogWriter io.Writer
if platformInterface != nil {
@@ -498,7 +502,7 @@ func (s *Box) Close() error {
close(s.done)
}
err := common.Close(
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
s.service, s.endpoint, s.inbound, s.outbound, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
)
for _, lifecycleService := range s.internalService {
err = E.Append(err, lifecycleService.Close(), func(err error) error {

View File

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

View File

@@ -16,15 +16,17 @@ import (
)
var (
debugEnabled bool
target string
platform string
debugEnabled bool
target string
platform string
withTailscale bool
)
func init() {
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
flag.StringVar(&target, "target", "android", "target platform")
flag.StringVar(&platform, "platform", "", "specify platform")
flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS")
}
func main() {
@@ -44,8 +46,9 @@ var (
sharedFlags []string
debugFlags []string
sharedTags []string
iosTags []string
darwinTags []string
memcTags []string
notMemcTags []string
debugTags []string
)
@@ -60,8 +63,9 @@ func init() {
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack")
iosTags = append(iosTags, "with_dhcp", "with_low_memory")
darwinTags = append(darwinTags, "with_dhcp")
memcTags = append(memcTags, "with_tailscale")
notMemcTags = append(notMemcTags, "with_low_memory")
debugTags = append(debugTags, "debug")
}
@@ -108,6 +112,8 @@ func buildAndroid() {
args = append(args, debugFlags...)
}
args = append(args, "-ldflags", "-checklinkname=0")
tags := append(sharedTags, memcTags...)
if debugEnabled {
tags = append(tags, debugTags...)
@@ -151,7 +157,10 @@ func buildApple() {
"-v",
"-target", bindTarget,
"-libname=box",
"-tags-macos=" + strings.Join(memcTags, ","),
"-tags-not-macos=with_low_memory",
}
if !withTailscale {
args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
}
if !debugEnabled {
@@ -160,7 +169,10 @@ func buildApple() {
args = append(args, debugFlags...)
}
tags := append(sharedTags, iosTags...)
tags := append(sharedTags, darwinTags...)
if withTailscale {
tags = append(tags, memcTags...)
}
if debugEnabled {
tags = append(tags, debugTags...)
}

View File

@@ -0,0 +1,286 @@
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")
for _, flag := range flags {
sudoArgs = append(sudoArgs, flag)
}
}
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

@@ -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
}

74
common/cloudflare/api.go Normal file
View File

@@ -0,0 +1,74 @@
package cloudflare
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/tidwall/gjson"
)
type CloudflareApi struct {
client http.Client
}
func NewCloudflareApi(opts ...CloudflareApiOption) *CloudflareApi {
api := &CloudflareApi{http.Client{Timeout: 30 * time.Second}}
for _, opt := range opts {
opt(api)
}
return api
}
func (api *CloudflareApi) CreateProfile(ctx context.Context, publicKey string) (*CloudflareProfile, error) {
request, err := http.NewRequest("POST", "https://api.cloudflareclient.com/v0i1909051800/reg", strings.NewReader(
fmt.Sprintf(
"{\"install_id\":\"\",\"tos\":\"%s\",\"key\":\"%s\",\"fcm_token\":\"\",\"type\":\"ios\",\"locale\":\"en_US\"}",
time.Now().Format("2006-01-02T15:04:05.000Z"),
publicKey,
),
))
if err != nil {
return nil, err
}
response, err := api.client.Do(request.WithContext(ctx))
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != 200 {
return nil, fmt.Errorf("status code is not 200")
}
content, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
profile := new(CloudflareProfile)
return profile, json.NewDecoder(strings.NewReader(gjson.Get(string(content), "result").Raw)).Decode(profile)
}
func (api *CloudflareApi) GetProfile(ctx context.Context, authToken string, id string) (*CloudflareProfile, error) {
request, err := http.NewRequest("GET", "https://api.cloudflareclient.com/v0i1909051800/reg/"+id, nil)
if err != nil {
return nil, err
}
request.Header.Set("Authorization", "Bearer "+authToken)
response, err := api.client.Do(request.WithContext(ctx))
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != 200 {
return nil, fmt.Errorf("status code is not 200")
}
content, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
profile := new(CloudflareProfile)
return profile, json.NewDecoder(strings.NewReader(gjson.Get(string(content), "result").Raw)).Decode(profile)
}

View File

@@ -0,0 +1,17 @@
package cloudflare
import (
"context"
"net"
"net/http"
)
type CloudflareApiOption func(api *CloudflareApi)
func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) CloudflareApiOption {
return func(api *CloudflareApi) {
api.client.Transport = &http.Transport{
DialContext: dialContext,
}
}
}

View File

@@ -0,0 +1,64 @@
package cloudflare
import "time"
type CloudflareProfile struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Key string `json:"key"`
Account struct {
ID string `json:"id"`
AccountType string `json:"account_type"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
PremiumData int `json:"premium_data"`
Quota int `json:"quota"`
Usage int `json:"usage"`
WARPPlus bool `json:"warp_plus"`
ReferralCount int `json:"referral_count"`
ReferralRenewalCountdown int `json:"referral_renewal_countdown"`
Role string `json:"role"`
License string `json:"license"`
TTL time.Time `json:"ttl"`
} `json:"account"`
Config struct {
ClientID string `json:"client_id"`
Interface struct {
Addresses struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
} `json:"addresses"`
} `json:"interface"`
Peers []struct {
PublicKey string `json:"public_key"`
Endpoint struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
Host string `json:"host"`
Ports []int `json:"ports"`
} `json:"endpoint"`
} `json:"peers"`
Services struct {
HTTPProxy string `json:"http_proxy"`
} `json:"services"`
Metrics struct {
Ping int `json:"ping"`
Report int `json:"report"`
} `json:"metrics"`
} `json:"config"`
Token string `json:"token"`
WARPEnabled bool `json:"warp_enabled"`
WaitlistEnabled bool `json:"waitlist_enabled"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Tos time.Time `json:"tos"`
Place int `json:"place"`
Locale string `json:"locale"`
Enabled bool `json:"enabled"`
InstallID string `json:"install_id"`
FcmToken string `json:"fcm_token"`
Policy struct {
TunnelProtocol string `json:"tunnel_protocol"`
} `json:"policy"`
}

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

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

View File

@@ -111,7 +111,7 @@ func NewWithOptions(options Options) (N.Dialer, error) {
dnsQueryOptions.Transport = dnsTransport.Default()
} else if options.NewDialer {
return nil, E.New("missing domain resolver for domain server address")
} else if !options.DirectOutbound {
} else {
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
}
}

View File

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

View File

@@ -3,6 +3,7 @@ package interrupt
import (
"net"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
)
@@ -73,3 +74,32 @@ func (c *PacketConn) WriterReplaceable() bool {
func (c *PacketConn) Upstream() any {
return c.PacketConn
}
type SingPacketConn struct {
N.PacketConn
group *Group
element *list.Element[*groupConnItem]
}
/*func (c *SingPacketConn) MarkAsInternal() {
c.element.Value.internal = true
}*/
func (c *SingPacketConn) Close() error {
c.group.access.Lock()
defer c.group.access.Unlock()
c.group.connections.Remove(c.element)
return c.PacketConn.Close()
}
func (c *SingPacketConn) ReaderReplaceable() bool {
return true
}
func (c *SingPacketConn) WriterReplaceable() bool {
return true
}
func (c *SingPacketConn) Upstream() any {
return c.PacketConn
}

View File

@@ -5,6 +5,7 @@ import (
"net"
"sync"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
)
@@ -36,6 +37,13 @@ func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool) net.PacketCo
return &PacketConn{PacketConn: conn, group: g, element: item}
}
func (g *Group) NewSingPacketConn(conn N.PacketConn, isExternal bool) N.PacketConn {
g.access.Lock()
defer g.access.Unlock()
item := g.connections.PushBack(&groupConnItem{conn, isExternal})
return &SingPacketConn{PacketConn: conn, group: g, element: item}
}
func (g *Group) Interrupt(interruptExternalConnections bool) {
g.access.Lock()
defer g.access.Unlock()

View File

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

View File

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

@@ -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,24 +92,34 @@ 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)
}
// xsocket_n.so_last_pid
pid := readNativeUint32(buf[so+68 : so+72])
return getExecPathFromPID(pid)
// udp packet connection may be not equal with srcIP
if network == N.NetworkUDP && srcIP.IsUnspecified() && isIPv4 == srcIsIPv4 {
pid := readNativeUint32(buf[so+68 : so+72])
fallbackUDPProcess, _ = getExecPathFromPID(pid)
}
}
if network == N.NetworkUDP && len(fallbackUDPProcess) > 0 {
return fallbackUDPProcess, nil
}
return "", ErrNotFound

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,155 +0,0 @@
package tls
import (
"bytes"
"encoding/binary"
"encoding/pem"
E "github.com/sagernet/sing/common/exceptions"
"github.com/cloudflare/circl/hpke"
"github.com/cloudflare/circl/kem"
)
func ECHKeygenDefault(serverName string) (configPem string, keyPem string, err error) {
cipherSuites := []echCipherSuite{
{
kdf: hpke.KDF_HKDF_SHA256,
aead: hpke.AEAD_AES128GCM,
}, {
kdf: hpke.KDF_HKDF_SHA256,
aead: hpke.AEAD_ChaCha20Poly1305,
},
}
keyConfig := []myECHKeyConfig{
{id: 0, kem: hpke.KEM_X25519_HKDF_SHA256},
}
keyPairs, err := echKeygen(0xfe0d, serverName, keyConfig, cipherSuites)
if err != nil {
return
}
var configBuffer bytes.Buffer
var totalLen uint16
for _, keyPair := range keyPairs {
totalLen += uint16(len(keyPair.rawConf))
}
binary.Write(&configBuffer, binary.BigEndian, totalLen)
for _, keyPair := range keyPairs {
configBuffer.Write(keyPair.rawConf)
}
var keyBuffer bytes.Buffer
for _, keyPair := range keyPairs {
keyBuffer.Write(keyPair.rawKey)
}
configPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer.Bytes()}))
keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer.Bytes()}))
return
}
type echKeyConfigPair struct {
id uint8
rawKey []byte
conf myECHKeyConfig
rawConf []byte
}
type echCipherSuite struct {
kdf hpke.KDF
aead hpke.AEAD
}
type myECHKeyConfig struct {
id uint8
kem hpke.KEM
seed []byte
}
func echKeygen(version uint16, serverName string, conf []myECHKeyConfig, suite []echCipherSuite) ([]echKeyConfigPair, error) {
be := binary.BigEndian
// prepare for future update
if version != 0xfe0d {
return nil, E.New("unsupported ECH version", version)
}
suiteBuf := make([]byte, 0, len(suite)*4+2)
suiteBuf = be.AppendUint16(suiteBuf, uint16(len(suite))*4)
for _, s := range suite {
if !s.kdf.IsValid() || !s.aead.IsValid() {
return nil, E.New("invalid HPKE cipher suite")
}
suiteBuf = be.AppendUint16(suiteBuf, uint16(s.kdf))
suiteBuf = be.AppendUint16(suiteBuf, uint16(s.aead))
}
pairs := []echKeyConfigPair{}
for _, c := range conf {
pair := echKeyConfigPair{}
pair.id = c.id
pair.conf = c
if !c.kem.IsValid() {
return nil, E.New("invalid HPKE KEM")
}
kpGenerator := c.kem.Scheme().GenerateKeyPair
if len(c.seed) > 0 {
kpGenerator = func() (kem.PublicKey, kem.PrivateKey, error) {
pub, sec := c.kem.Scheme().DeriveKeyPair(c.seed)
return pub, sec, nil
}
if len(c.seed) < c.kem.Scheme().PrivateKeySize() {
return nil, E.New("HPKE KEM seed too short")
}
}
pub, sec, err := kpGenerator()
if err != nil {
return nil, E.Cause(err, "generate ECH config key pair")
}
b := []byte{}
b = be.AppendUint16(b, version)
b = be.AppendUint16(b, 0) // length field
// contents
// key config
b = append(b, c.id)
b = be.AppendUint16(b, uint16(c.kem))
pubBuf, err := pub.MarshalBinary()
if err != nil {
return nil, E.Cause(err, "serialize ECH public key")
}
b = be.AppendUint16(b, uint16(len(pubBuf)))
b = append(b, pubBuf...)
b = append(b, suiteBuf...)
// end key config
// max name len, not supported
b = append(b, 0)
// server name
b = append(b, byte(len(serverName)))
b = append(b, []byte(serverName)...)
// extensions, not supported
b = be.AppendUint16(b, 0)
be.PutUint16(b[2:], uint16(len(b)-4))
pair.rawConf = b
secBuf, err := sec.MarshalBinary()
if err != nil {
return nil, E.Cause(err, "serialize ECH private key")
}
sk := []byte{}
sk = be.AppendUint16(sk, uint16(len(secBuf)))
sk = append(sk, secBuf...)
sk = be.AppendUint16(sk, uint16(len(b)))
sk = append(sk, b...)
pair.rawKey = sk
pairs = append(pairs, pair)
}
return pairs, nil
}

81
common/tls/ech_shared.go Normal file
View File

@@ -0,0 +1,81 @@
package tls
import (
"crypto/ecdh"
"crypto/rand"
"encoding/pem"
"golang.org/x/crypto/cryptobyte"
)
type ECHCapableConfig interface {
Config
ECHConfigList() []byte
SetECHConfigList([]byte)
}
func ECHKeygenDefault(publicName string) (configPem string, keyPem string, err error) {
echKey, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return
}
echConfig, err := marshalECHConfig(0, echKey.PublicKey().Bytes(), publicName, 0)
if err != nil {
return
}
configBuilder := cryptobyte.NewBuilder(nil)
configBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echConfig)
})
configBytes, err := configBuilder.Bytes()
if err != nil {
return
}
keyBuilder := cryptobyte.NewBuilder(nil)
keyBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echKey.Bytes())
})
keyBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echConfig)
})
keyBytes, err := keyBuilder.Bytes()
if err != nil {
return
}
configPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBytes}))
keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBytes}))
return
}
func marshalECHConfig(id uint8, pubKey []byte, publicName string, maxNameLen uint8) ([]byte, error) {
const extensionEncryptedClientHello = 0xfe0d
const DHKEM_X25519_HKDF_SHA256 = 0x0020
const KDF_HKDF_SHA256 = 0x0001
builder := cryptobyte.NewBuilder(nil)
builder.AddUint16(extensionEncryptedClientHello)
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddUint8(id)
builder.AddUint16(DHKEM_X25519_HKDF_SHA256) // The only DHKEM we support
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(pubKey)
})
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
const (
AEAD_AES_128_GCM = 0x0001
AEAD_AES_256_GCM = 0x0002
AEAD_ChaCha20Poly1305 = 0x0003
)
for _, aeadID := range []uint16{AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_ChaCha20Poly1305} {
builder.AddUint16(KDF_HKDF_SHA256) // The only KDF we support
builder.AddUint16(aeadID)
}
})
builder.AddUint8(maxNameLen)
builder.AddUint8LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes([]byte(publicName))
})
builder.AddUint16(0) // extensions
})
return builder.Bytes()
}

View File

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

View File

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

View File

@@ -7,43 +7,60 @@ import (
"net"
"os"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tlsfragment"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
)
type STDClientConfig struct {
config *tls.Config
ctx context.Context
config *tls.Config
fragment bool
fragmentFallbackDelay time.Duration
recordFragment bool
}
func (s *STDClientConfig) ServerName() string {
return s.config.ServerName
func (c *STDClientConfig) ServerName() string {
return c.config.ServerName
}
func (s *STDClientConfig) SetServerName(serverName string) {
s.config.ServerName = serverName
func (c *STDClientConfig) SetServerName(serverName string) {
c.config.ServerName = serverName
}
func (s *STDClientConfig) NextProtos() []string {
return s.config.NextProtos
func (c *STDClientConfig) NextProtos() []string {
return c.config.NextProtos
}
func (s *STDClientConfig) SetNextProtos(nextProto []string) {
s.config.NextProtos = nextProto
func (c *STDClientConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto
}
func (s *STDClientConfig) Config() (*STDConfig, error) {
return s.config, nil
func (c *STDClientConfig) Config() (*STDConfig, error) {
return c.config, nil
}
func (s *STDClientConfig) Client(conn net.Conn) (Conn, error) {
return tls.Client(conn, s.config), nil
func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) {
if c.recordFragment {
conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay)
}
return tls.Client(conn, c.config), nil
}
func (s *STDClientConfig) Clone() Config {
return &STDClientConfig{s.config.Clone()}
func (c *STDClientConfig) Clone() Config {
return &STDClientConfig{c.ctx, c.config.Clone(), c.fragment, c.fragmentFallbackDelay, c.recordFragment}
}
func (c *STDClientConfig) ECHConfigList() []byte {
return c.config.EncryptedClientHelloConfigList
}
func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte) {
c.config.EncryptedClientHelloConfigList = EncryptedClientHelloConfigList
}
func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
@@ -60,9 +77,7 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
var tlsConfig tls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if options.DisableSNI {
tlsConfig.ServerName = "127.0.0.1"
} else {
if !options.DisableSNI {
tlsConfig.ServerName = serverName
}
if options.Insecure {
@@ -71,12 +86,16 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
verifyOptions := x509.VerifyOptions{
Roots: tlsConfig.RootCAs,
DNSName: serverName,
Intermediates: x509.NewCertPool(),
}
for _, cert := range state.PeerCertificates[1:] {
verifyOptions.Intermediates.AddCert(cert)
}
if tlsConfig.Time != nil {
verifyOptions.CurrentTime = tlsConfig.Time()
}
_, err := state.PeerCertificates[0].Verify(verifyOptions)
return err
}
@@ -127,8 +146,10 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
}
tlsConfig.RootCAs = certPool
}
stdConfig := &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
if options.ECH != nil && options.ECH.Enabled {
return parseECHClientConfig(ctx, options, &tlsConfig)
return parseECHClientConfig(ctx, stdConfig, options)
} else {
return stdConfig, nil
}
return &STDClientConfig{&tlsConfig}, nil
}

View File

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

View File

@@ -11,10 +11,13 @@ type TimeServiceWrapper struct {
}
func (w *TimeServiceWrapper) TimeFunc() func() time.Time {
if w.TimeService == nil {
return nil
return func() time.Time {
if w.TimeService != nil {
return w.TimeService.TimeFunc()()
} else {
return time.Now()
}
}
return w.TimeService.TimeFunc()
}
func (w *TimeServiceWrapper) Upstream() any {

View File

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

View File

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

View File

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

View File

@@ -22,13 +22,13 @@ const (
tls13 uint16 = 0x0304
)
type myServerName struct {
type MyServerName struct {
Index int
Length int
ServerName string
}
func indexTLSServerName(payload []byte) *myServerName {
func IndexTLSServerName(payload []byte) *MyServerName {
if len(payload) < recordLayerHeaderLen || payload[0] != contentType {
return nil
}
@@ -36,47 +36,48 @@ func indexTLSServerName(payload []byte) *myServerName {
if len(payload) < recordLayerHeaderLen+int(segmentLen) {
return nil
}
serverName := indexTLSServerNameFromHandshake(payload[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)])
serverName := indexTLSServerNameFromHandshake(payload[recordLayerHeaderLen:])
if serverName == nil {
return nil
}
serverName.Length += recordLayerHeaderLen
serverName.Index += recordLayerHeaderLen
return serverName
}
func indexTLSServerNameFromHandshake(hs []byte) *myServerName {
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen {
func indexTLSServerNameFromHandshake(handshake []byte) *MyServerName {
if len(handshake) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen {
return nil
}
if hs[0] != handshakeType {
if handshake[0] != handshakeType {
return nil
}
handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3])
if len(hs[4:]) != int(handshakeLen) {
handshakeLen := uint32(handshake[1])<<16 | uint32(handshake[2])<<8 | uint32(handshake[3])
if len(handshake[4:]) != int(handshakeLen) {
return nil
}
tlsVersion := uint16(hs[4])<<8 | uint16(hs[5])
tlsVersion := uint16(handshake[4])<<8 | uint16(handshake[5])
if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 {
return nil
}
sessionIDLen := hs[38]
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) {
sessionIDLen := handshake[38]
currentIndex := handshakeHeaderLen + randomDataLen + sessionIDHeaderLen + int(sessionIDLen)
if len(handshake) < currentIndex {
return nil
}
cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):]
if len(cs) < cipherSuiteHeaderLen {
cipherSuites := handshake[currentIndex:]
if len(cipherSuites) < cipherSuiteHeaderLen {
return nil
}
csLen := uint16(cs[0])<<8 | uint16(cs[1])
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen {
csLen := uint16(cipherSuites[0])<<8 | uint16(cipherSuites[1])
if len(cipherSuites) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen {
return nil
}
compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)])
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) {
compressMethodLen := uint16(cipherSuites[cipherSuiteHeaderLen+int(csLen)])
currentIndex += cipherSuiteHeaderLen + int(csLen) + compressMethodHeaderLen + int(compressMethodLen)
if len(handshake) < currentIndex {
return nil
}
currentIndex := cipherSuiteHeaderLen + int(csLen) + compressMethodHeaderLen + int(compressMethodLen)
serverName := indexTLSServerNameFromExtensions(cs[currentIndex:])
serverName := indexTLSServerNameFromExtensions(handshake[currentIndex:])
if serverName == nil {
return nil
}
@@ -84,7 +85,7 @@ func indexTLSServerNameFromHandshake(hs []byte) *myServerName {
return serverName
}
func indexTLSServerNameFromExtensions(exs []byte) *myServerName {
func indexTLSServerNameFromExtensions(exs []byte) *MyServerName {
if len(exs) == 0 {
return nil
}
@@ -118,7 +119,8 @@ func indexTLSServerNameFromExtensions(exs []byte) *myServerName {
}
sniLen := uint16(sex[3])<<8 | uint16(sex[4])
sex = sex[sniExtensionHeaderLen:]
return &myServerName{
return &MyServerName{
Index: currentIndex + extensionHeaderLen + sniExtensionHeaderLen,
Length: int(sniLen),
ServerName: string(sex),

View File

@@ -0,0 +1,20 @@
package tf_test
import (
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/common/tlsfragment"
"github.com/stretchr/testify/require"
)
func TestIndexTLSServerName(t *testing.T) {
t.Parallel()
payload, err := hex.DecodeString("16030105f8010005f403036e35de7389a679c54029cf452611f2211c70d9ac3897271de589ab6155f8e4ab20637d225f1ef969ad87ed78bfb9d171300bcb1703b6f314ccefb964f79b7d0961002a0a0a130213031301c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a01000581baba00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000e000c3a3a11ec001d001700180019000b000201000010000e000c02683208687474702f312e31000500050100000000000d00160014040308040401050308050805050108060601020100120000003304ef04ed3a3a00010011ec04c0aeb2250c092a3463161cccb29d9183331a424964248579507ed23a180b0ceab2a5f5d9ce41547e497a89055471ea572867ba3a1fc3c9e45025274a20f60c6b60e62476b6afed0403af59ab83660ef4112ae20386a602010d0a5d454c0ed34c84ed4423e750213e6a2baab1bf9c4367a6007ab40a33d95220c2dcaa44f257024a5626b545db0510f4311b1a60714154909c6a61fdfca011fb2626d657aeb6070bf078508babe3b584555013e34acc56198ed4663742b3155a664a9901794c4586820a7dc162c01827291f3792e1237f801a8d1ef096013c181c4a58d2f6859ba75022d18cc4418bd4f351d5c18f83a58857d05af860c4b9ac018a5b63f17184e591532c6bc2cf2215d4a282c8a8a4f6f7aee110422c8bc9ebd3b1d609c568523aaae555db320e6c269473d87af38c256cbb9febc20aea6380c32a8916f7a373c8b1e37554e3260bf6621f6b804ee80b3c516b1d01985bf4c603b6daa9a5991de6a7a29f3a7122b8afb843a7660110fce62b43c615f5bcc2db688ba012649c0952b0a2c031e732d2b454c6b2968683cb8d244be2c9a7fa163222979eaf92722b92b862d81a3d94450c2b60c318421ebb4307c42d1f0473592a5c30e42039cc68cda9721e61aa63f49def17c15221680ed444896340133bbee67556f56b9f9d78a4df715f926a12add0cc9c862e46ea8b7316ae468282c18601b2771c9c9322f982228cf93effaacd3f80cbd12bce5fc36f56e2a3caf91e578a5fae00c9b23a8ed1a66764f4433c3628a70b8f0a6196adc60a4cb4226f07ba4c6b363fe9065563bfc1347452946386bab488686e837ab979c64f9047417fca635fe1bb4f074f256cc8af837c7b455e280426547755af90a61640169ef180aea3a77e662bb6dac1b6c3696027129b1a5edf495314e9c7f4b6110e16378ec893fa24642330a40aba1a85326101acb97c620fd8d71389e69eaed7bdb01bbe1fd428d66191150c7b2cd1ad4257391676a82ba8ce07fb2667c3b289f159003a7c7bc31d361b7b7f49a802961739d950dfcc0fa1c7abce5abdd2245101da391151490862028110465950b9e9c03d08a90998ab83267838d2e74a0593bc81f74cdf734519a05b351c0e5488c68dd810e6e9142ccc1e2f4a7f464297eb340e27acc6b9d64e12e38cce8492b3d939140b5a9e149a75597f10a23874c84323a07cdd657274378f887c85c4259b9c04cd33ba58ed630ef2a744f8e19dd34843dff331d2a6be7e2332c599289cd248a611c73d7481cd4a9bd43449a3836f14b2af18a1739e17999e4c67e85cc5bcecabb14185e5bcaff3c96098f03dc5aba819f29587758f49f940585354a2a780830528d68ccd166920dadcaa25cab5fc1907272a826aba3f08bc6b88757776812ecb6c7cec69a223ec0a13a7b62a2349a0f63ed7a27a3b15ba21d71fe6864ec6e089ae17cadd433fa3138f7ee24353c11365818f8fc34f43a05542d18efaac24bfccc1f748a0cc1a67ad379468b76fd34973dba785f5c91d618333cd810fe0700d1bbc8422029782628070a624c52c5309a4a64d625b11f8033ab28df34a1add297517fcc06b92b6817b3c5144438cf260867c57bde68c8c4b82e6a135ef676a52fbae5708002a404e6189a60e2836de565ad1b29e3819e5ed49f6810bcb28e1bd6de57306f94b79d9dae1cc4624d2a068499beef81cd5fe4b76dcbfff2a2008001d002001976128c6d5a934533f28b9914d2480aab2a8c1ab03d212529ce8b27640a716002d00020101002b000706caca03040303001b00030200015a5a000100")
require.NoError(t, err)
serverName := tf.IndexTLSServerName(payload)
require.NotNil(t, serverName)
require.Equal(t, serverName.ServerName, string(payload[serverName.Index:serverName.Index+serverName.Length]))
require.Equal(t, "github.com", serverName.ServerName)
}

13
common/urltest/context.go Normal file
View File

@@ -0,0 +1,13 @@
package urltest
import "context"
type contextKeyIsUnifiedDelay struct{}
func ContextWithIsUnifiedDelay(ctx context.Context) context.Context {
return context.WithValue(ctx, contextKeyIsUnifiedDelay{}, true)
}
func IsUnifiedDelayFromContext(ctx context.Context) bool {
return ctx.Value(contextKeyIsUnifiedDelay{}) != nil
}

View File

@@ -126,6 +126,15 @@ func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err e
return
}
resp.Body.Close()
if IsUnifiedDelayFromContext(ctx) {
second := time.Now()
resp, err = client.Do(req)
if err != nil {
return
}
resp.Body.Close()
start = second
}
t = uint16(time.Since(start) / time.Millisecond)
return
}

337
common/xray/buf/buffer.go Normal file
View File

@@ -0,0 +1,337 @@
package buf
import (
"io"
"github.com/sagernet/sing-box/common/xray/bytespool"
"github.com/sagernet/sing-box/common/xray/net"
E "github.com/sagernet/sing/common/exceptions"
)
const (
// Size of a regular buffer.
Size = 8192
)
var zero = [Size * 10]byte{0}
var pool = bytespool.GetPool(Size)
// ownership represents the data owner of the buffer.
type ownership uint8
const (
managed ownership = iota
unmanaged
bytespools
)
// Buffer is a recyclable allocation of a byte array. Buffer.Release() recycles
// the buffer into an internal buffer pool, in order to recreate a buffer more
// quickly.
type Buffer struct {
v []byte
start int32
end int32
ownership ownership
UDP *net.Destination
}
// New creates a Buffer with 0 length and 8K capacity, managed.
func New() *Buffer {
buf := pool.Get().([]byte)
if cap(buf) >= Size {
buf = buf[:Size]
} else {
buf = make([]byte, Size)
}
return &Buffer{
v: buf,
}
}
// NewExisted creates a standard size Buffer with an existed bytearray, managed.
func NewExisted(b []byte) *Buffer {
if cap(b) < Size {
panic("Invalid buffer")
}
oLen := len(b)
if oLen < Size {
b = b[:Size]
}
return &Buffer{
v: b,
end: int32(oLen),
}
}
// FromBytes creates a Buffer with an existed bytearray, unmanaged.
func FromBytes(b []byte) *Buffer {
return &Buffer{
v: b,
end: int32(len(b)),
ownership: unmanaged,
}
}
// StackNew creates a new Buffer object on stack, managed.
// This method is for buffers that is released in the same function.
func StackNew() Buffer {
buf := pool.Get().([]byte)
if cap(buf) >= Size {
buf = buf[:Size]
} else {
buf = make([]byte, Size)
}
return Buffer{
v: buf,
}
}
// NewWithSize creates a Buffer with 0 length and capacity with at least the given size, bytespool's.
func NewWithSize(size int32) *Buffer {
return &Buffer{
v: bytespool.Alloc(size),
ownership: bytespools,
}
}
// Release recycles the buffer into an internal buffer pool.
func (b *Buffer) Release() {
if b == nil || b.v == nil || b.ownership == unmanaged {
return
}
p := b.v
b.v = nil
b.Clear()
switch b.ownership {
case managed:
if cap(p) == Size {
pool.Put(p)
}
case bytespools:
bytespool.Free(p)
}
b.UDP = nil
}
// Clear clears the content of the buffer, results an empty buffer with
// Len() = 0.
func (b *Buffer) Clear() {
b.start = 0
b.end = 0
}
// Byte returns the bytes at index.
func (b *Buffer) Byte(index int32) byte {
return b.v[b.start+index]
}
// SetByte sets the byte value at index.
func (b *Buffer) SetByte(index int32, value byte) {
b.v[b.start+index] = value
}
// Bytes returns the content bytes of this Buffer.
func (b *Buffer) Bytes() []byte {
return b.v[b.start:b.end]
}
// Extend increases the buffer size by n bytes, and returns the extended part.
// It panics if result size is larger than buf.Size.
func (b *Buffer) Extend(n int32) []byte {
end := b.end + n
if end > int32(len(b.v)) {
panic("extending out of bound")
}
ext := b.v[b.end:end]
b.end = end
copy(ext, zero[:])
return ext
}
// BytesRange returns a slice of this buffer with given from and to boundary.
func (b *Buffer) BytesRange(from, to int32) []byte {
if from < 0 {
from += b.Len()
}
if to < 0 {
to += b.Len()
}
return b.v[b.start+from : b.start+to]
}
// BytesFrom returns a slice of this Buffer starting from the given position.
func (b *Buffer) BytesFrom(from int32) []byte {
if from < 0 {
from += b.Len()
}
return b.v[b.start+from : b.end]
}
// BytesTo returns a slice of this Buffer from start to the given position.
func (b *Buffer) BytesTo(to int32) []byte {
if to < 0 {
to += b.Len()
}
if to < 0 {
to = 0
}
return b.v[b.start : b.start+to]
}
// Check makes sure that 0 <= b.start <= b.end.
func (b *Buffer) Check() {
if b.start < 0 {
b.start = 0
}
if b.end < 0 {
b.end = 0
}
if b.start > b.end {
b.start = b.end
}
}
// Resize cuts the buffer at the given position.
func (b *Buffer) Resize(from, to int32) {
oldEnd := b.end
if from < 0 {
from += b.Len()
}
if to < 0 {
to += b.Len()
}
if to < from {
panic("Invalid slice")
}
b.end = b.start + to
b.start += from
b.Check()
if b.end > oldEnd {
copy(b.v[oldEnd:b.end], zero[:])
}
}
// Advance cuts the buffer at the given position.
func (b *Buffer) Advance(from int32) {
if from < 0 {
from += b.Len()
}
b.start += from
b.Check()
}
// Len returns the length of the buffer content.
func (b *Buffer) Len() int32 {
if b == nil {
return 0
}
return b.end - b.start
}
// Cap returns the capacity of the buffer content.
func (b *Buffer) Cap() int32 {
if b == nil {
return 0
}
return int32(len(b.v))
}
// IsEmpty returns true if the buffer is empty.
func (b *Buffer) IsEmpty() bool {
return b.Len() == 0
}
// IsFull returns true if the buffer has no more room to grow.
func (b *Buffer) IsFull() bool {
return b != nil && b.end == int32(len(b.v))
}
// Write implements Write method in io.Writer.
func (b *Buffer) Write(data []byte) (int, error) {
nBytes := copy(b.v[b.end:], data)
b.end += int32(nBytes)
return nBytes, nil
}
// WriteByte writes a single byte into the buffer.
func (b *Buffer) WriteByte(v byte) error {
if b.IsFull() {
return E.New("buffer full")
}
b.v[b.end] = v
b.end++
return nil
}
// WriteString implements io.StringWriter.
func (b *Buffer) WriteString(s string) (int, error) {
return b.Write([]byte(s))
}
// ReadByte implements io.ByteReader
func (b *Buffer) ReadByte() (byte, error) {
if b.start == b.end {
return 0, io.EOF
}
nb := b.v[b.start]
b.start++
return nb, nil
}
// ReadBytes implements bufio.Reader.ReadBytes
func (b *Buffer) ReadBytes(length int32) ([]byte, error) {
if b.end-b.start < length {
return nil, io.EOF
}
nb := b.v[b.start : b.start+length]
b.start += length
return nb, nil
}
// Read implements io.Reader.Read().
func (b *Buffer) Read(data []byte) (int, error) {
if b.Len() == 0 {
return 0, io.EOF
}
nBytes := copy(data, b.v[b.start:b.end])
if int32(nBytes) == b.Len() {
b.Clear()
} else {
b.start += int32(nBytes)
}
return nBytes, nil
}
// ReadFrom implements io.ReaderFrom.
func (b *Buffer) ReadFrom(reader io.Reader) (int64, error) {
n, err := reader.Read(b.v[b.end:])
b.end += int32(n)
return int64(n), err
}
// ReadFullFrom reads exact size of bytes from given reader, or until error occurs.
func (b *Buffer) ReadFullFrom(reader io.Reader, size int32) (int64, error) {
end := b.end + size
if end > int32(len(b.v)) {
v := end
return 0, E.New("out of bound: ", v)
}
n, err := io.ReadFull(reader, b.v[b.end:end])
b.end += int32(n)
return int64(n), err
}
// String returns the string form of this Buffer.
func (b *Buffer) String() string {
return string(b.Bytes())
}

124
common/xray/buf/copy.go Normal file
View File

@@ -0,0 +1,124 @@
package buf
import (
"io"
"time"
"github.com/sagernet/sing-box/common/xray/errors"
"github.com/sagernet/sing-box/common/xray/signal"
E "github.com/sagernet/sing/common/exceptions"
)
type dataHandler func(MultiBuffer)
type copyHandler struct {
onData []dataHandler
}
// SizeCounter is for counting bytes copied by Copy().
type SizeCounter struct {
Size int64
}
// CopyOption is an option for copying data.
type CopyOption func(*copyHandler)
// UpdateActivity is a CopyOption to update activity on each data copy operation.
func UpdateActivity(timer signal.ActivityUpdater) CopyOption {
return func(handler *copyHandler) {
handler.onData = append(handler.onData, func(MultiBuffer) {
timer.Update()
})
}
}
// CountSize is a CopyOption that sums the total size of data copied into the given SizeCounter.
func CountSize(sc *SizeCounter) CopyOption {
return func(handler *copyHandler) {
handler.onData = append(handler.onData, func(b MultiBuffer) {
sc.Size += int64(b.Len())
})
}
}
type readError struct {
error
}
func (e readError) Error() string {
return e.error.Error()
}
func (e readError) Unwrap() error {
return e.error
}
// IsReadError returns true if the error in Copy() comes from reading.
func IsReadError(err error) bool {
_, ok := err.(readError)
return ok
}
type writeError struct {
error
}
func (e writeError) Error() string {
return e.error.Error()
}
func (e writeError) Unwrap() error {
return e.error
}
// IsWriteError returns true if the error in Copy() comes from writing.
func IsWriteError(err error) bool {
_, ok := err.(writeError)
return ok
}
func copyInternal(reader Reader, writer Writer, handler *copyHandler) error {
for {
buffer, err := reader.ReadMultiBuffer()
if !buffer.IsEmpty() {
for _, handler := range handler.onData {
handler(buffer)
}
if werr := writer.WriteMultiBuffer(buffer); werr != nil {
return writeError{werr}
}
}
if err != nil {
return readError{err}
}
}
}
// Copy dumps all payload from reader to writer or stops when an error occurs. It returns nil when EOF.
func Copy(reader Reader, writer Writer, options ...CopyOption) error {
var handler copyHandler
for _, option := range options {
option(&handler)
}
err := copyInternal(reader, writer, &handler)
if err != nil && errors.Cause(err) != io.EOF {
return err
}
return nil
}
var ErrNotTimeoutReader = E.New("not a TimeoutReader")
func CopyOnceTimeout(reader Reader, writer Writer, timeout time.Duration) error {
timeoutReader, ok := reader.(TimeoutReader)
if !ok {
return ErrNotTimeoutReader
}
mb, err := timeoutReader.ReadMultiBufferTimeout(timeout)
if err != nil {
return err
}
return writer.WriteMultiBuffer(mb)
}

126
common/xray/buf/io.go Normal file
View File

@@ -0,0 +1,126 @@
package buf
import (
"io"
"net"
"syscall"
"time"
"github.com/sagernet/sing-box/common/xray/stat"
"github.com/sagernet/sing-box/common/xray/stats"
E "github.com/sagernet/sing/common/exceptions"
)
// Reader extends io.Reader with MultiBuffer.
type Reader interface {
// ReadMultiBuffer reads content from underlying reader, and put it into a MultiBuffer.
ReadMultiBuffer() (MultiBuffer, error)
}
// ErrReadTimeout is an error that happens with IO timeout.
var ErrReadTimeout = E.New("IO timeout")
// TimeoutReader is a reader that returns error if Read() operation takes longer than the given timeout.
type TimeoutReader interface {
ReadMultiBufferTimeout(time.Duration) (MultiBuffer, error)
}
// Writer extends io.Writer with MultiBuffer.
type Writer interface {
// WriteMultiBuffer writes a MultiBuffer into underlying writer.
WriteMultiBuffer(MultiBuffer) error
}
// WriteAllBytes ensures all bytes are written into the given writer.
func WriteAllBytes(writer io.Writer, payload []byte, c stats.Counter) error {
wc := 0
defer func() {
if c != nil {
c.Add(int64(wc))
}
}()
for len(payload) > 0 {
n, err := writer.Write(payload)
wc += n
if err != nil {
return err
}
payload = payload[n:]
}
return nil
}
func isPacketReader(reader io.Reader) bool {
_, ok := reader.(net.PacketConn)
return ok
}
// NewReader creates a new Reader.
// The Reader instance doesn't take the ownership of reader.
func NewReader(reader io.Reader) Reader {
if mr, ok := reader.(Reader); ok {
return mr
}
if isPacketReader(reader) {
return &PacketReader{
Reader: reader,
}
}
return &SingleReader{
Reader: reader,
}
}
// NewPacketReader creates a new PacketReader based on the given reader.
func NewPacketReader(reader io.Reader) Reader {
if mr, ok := reader.(Reader); ok {
return mr
}
return &PacketReader{
Reader: reader,
}
}
func isPacketWriter(writer io.Writer) bool {
if _, ok := writer.(net.PacketConn); ok {
return true
}
// If the writer doesn't implement syscall.Conn, it is probably not a TCP connection.
if _, ok := writer.(syscall.Conn); !ok {
return true
}
return false
}
// NewWriter creates a new Writer.
func NewWriter(writer io.Writer) Writer {
if mw, ok := writer.(Writer); ok {
return mw
}
iConn := writer
if statConn, ok := writer.(*stat.CounterConnection); ok {
iConn = statConn.Connection
}
if isPacketWriter(iConn) {
return &SequentialWriter{
Writer: writer,
}
}
var counter stats.Counter
if statConn, ok := writer.(*stat.CounterConnection); ok {
counter = statConn.WriteCounter
}
return &BufferToBytesWriter{
Writer: iConn,
counter: counter,
}
}

View File

@@ -0,0 +1,310 @@
package buf
import (
"io"
"github.com/sagernet/sing-box/common/xray"
"github.com/sagernet/sing-box/common/xray/errors"
"github.com/sagernet/sing-box/common/xray/serial"
)
// ReadAllToBytes reads all content from the reader into a byte array, until EOF.
func ReadAllToBytes(reader io.Reader) ([]byte, error) {
mb, err := ReadFrom(reader)
if err != nil {
return nil, err
}
if mb.Len() == 0 {
return nil, nil
}
b := make([]byte, mb.Len())
mb, _ = SplitBytes(mb, b)
ReleaseMulti(mb)
return b, nil
}
// MultiBuffer is a list of Buffers. The order of Buffer matters.
type MultiBuffer []*Buffer
// MergeMulti merges content from src to dest, and returns the new address of dest and src
func MergeMulti(dest MultiBuffer, src MultiBuffer) (MultiBuffer, MultiBuffer) {
dest = append(dest, src...)
for idx := range src {
src[idx] = nil
}
return dest, src[:0]
}
// MergeBytes merges the given bytes into MultiBuffer and return the new address of the merged MultiBuffer.
func MergeBytes(dest MultiBuffer, src []byte) MultiBuffer {
n := len(dest)
if n > 0 && !(dest)[n-1].IsFull() {
nBytes, _ := (dest)[n-1].Write(src)
src = src[nBytes:]
}
for len(src) > 0 {
b := New()
nBytes, _ := b.Write(src)
src = src[nBytes:]
dest = append(dest, b)
}
return dest
}
// ReleaseMulti releases all content of the MultiBuffer, and returns an empty MultiBuffer.
func ReleaseMulti(mb MultiBuffer) MultiBuffer {
for i := range mb {
mb[i].Release()
mb[i] = nil
}
return mb[:0]
}
// Copy copied the beginning part of the MultiBuffer into the given byte array.
func (mb MultiBuffer) Copy(b []byte) int {
total := 0
for _, bb := range mb {
nBytes := copy(b[total:], bb.Bytes())
total += nBytes
if int32(nBytes) < bb.Len() {
break
}
}
return total
}
// ReadFrom reads all content from reader until EOF.
func ReadFrom(reader io.Reader) (MultiBuffer, error) {
mb := make(MultiBuffer, 0, 16)
for {
b := New()
_, err := b.ReadFullFrom(reader, Size)
if b.IsEmpty() {
b.Release()
} else {
mb = append(mb, b)
}
if err != nil {
if errors.Cause(err) == io.EOF || errors.Cause(err) == io.ErrUnexpectedEOF {
return mb, nil
}
return mb, err
}
}
}
// SplitBytes splits the given amount of bytes from the beginning of the MultiBuffer.
// It returns the new address of MultiBuffer leftover, and number of bytes written into the input byte slice.
func SplitBytes(mb MultiBuffer, b []byte) (MultiBuffer, int) {
totalBytes := 0
endIndex := -1
for i := range mb {
pBuffer := mb[i]
nBytes, _ := pBuffer.Read(b)
totalBytes += nBytes
b = b[nBytes:]
if !pBuffer.IsEmpty() {
endIndex = i
break
}
pBuffer.Release()
mb[i] = nil
}
if endIndex == -1 {
mb = mb[:0]
} else {
mb = mb[endIndex:]
}
return mb, totalBytes
}
// SplitFirstBytes splits the first buffer from MultiBuffer, and then copy its content into the given slice.
func SplitFirstBytes(mb MultiBuffer, p []byte) (MultiBuffer, int) {
mb, b := SplitFirst(mb)
if b == nil {
return mb, 0
}
n := copy(p, b.Bytes())
b.Release()
return mb, n
}
// Compact returns another MultiBuffer by merging all content of the given one together.
func Compact(mb MultiBuffer) MultiBuffer {
if len(mb) == 0 {
return mb
}
mb2 := make(MultiBuffer, 0, len(mb))
last := mb[0]
for i := 1; i < len(mb); i++ {
curr := mb[i]
if last.Len()+curr.Len() > Size {
mb2 = append(mb2, last)
last = curr
} else {
common.Must2(last.ReadFrom(curr))
curr.Release()
}
}
mb2 = append(mb2, last)
return mb2
}
// SplitFirst splits the first Buffer from the beginning of the MultiBuffer.
func SplitFirst(mb MultiBuffer) (MultiBuffer, *Buffer) {
if len(mb) == 0 {
return mb, nil
}
b := mb[0]
mb[0] = nil
mb = mb[1:]
return mb, b
}
// SplitSize splits the beginning of the MultiBuffer into another one, for at most size bytes.
func SplitSize(mb MultiBuffer, size int32) (MultiBuffer, MultiBuffer) {
if len(mb) == 0 {
return mb, nil
}
if mb[0].Len() > size {
b := New()
copy(b.Extend(size), mb[0].BytesTo(size))
mb[0].Advance(size)
return mb, MultiBuffer{b}
}
totalBytes := int32(0)
var r MultiBuffer
endIndex := -1
for i := range mb {
if totalBytes+mb[i].Len() > size {
endIndex = i
break
}
totalBytes += mb[i].Len()
r = append(r, mb[i])
mb[i] = nil
}
if endIndex == -1 {
// To reuse mb array
mb = mb[:0]
} else {
mb = mb[endIndex:]
}
return mb, r
}
// SplitMulti splits the beginning of the MultiBuffer into first one, the index i and after into second one
func SplitMulti(mb MultiBuffer, i int) (MultiBuffer, MultiBuffer) {
mb2 := make(MultiBuffer, 0, len(mb))
if i < len(mb) && i >= 0 {
mb2 = append(mb2, mb[i:]...)
for j := i; j < len(mb); j++ {
mb[j] = nil
}
mb = mb[:i]
}
return mb, mb2
}
// WriteMultiBuffer writes all buffers from the MultiBuffer to the Writer one by one, and return error if any, with leftover MultiBuffer.
func WriteMultiBuffer(writer io.Writer, mb MultiBuffer) (MultiBuffer, error) {
for {
mb2, b := SplitFirst(mb)
mb = mb2
if b == nil {
break
}
_, err := writer.Write(b.Bytes())
b.Release()
if err != nil {
return mb, err
}
}
return nil, nil
}
// Len returns the total number of bytes in the MultiBuffer.
func (mb MultiBuffer) Len() int32 {
if mb == nil {
return 0
}
size := int32(0)
for _, b := range mb {
size += b.Len()
}
return size
}
// IsEmpty returns true if the MultiBuffer has no content.
func (mb MultiBuffer) IsEmpty() bool {
for _, b := range mb {
if !b.IsEmpty() {
return false
}
}
return true
}
// String returns the content of the MultiBuffer in string.
func (mb MultiBuffer) String() string {
v := make([]interface{}, len(mb))
for i, b := range mb {
v[i] = b
}
return serial.Concat(v...)
}
// MultiBufferContainer is a ReadWriteCloser wrapper over MultiBuffer.
type MultiBufferContainer struct {
MultiBuffer
}
// Read implements io.Reader.
func (c *MultiBufferContainer) Read(b []byte) (int, error) {
if c.MultiBuffer.IsEmpty() {
return 0, io.EOF
}
mb, nBytes := SplitBytes(c.MultiBuffer, b)
c.MultiBuffer = mb
return nBytes, nil
}
// ReadMultiBuffer implements Reader.
func (c *MultiBufferContainer) ReadMultiBuffer() (MultiBuffer, error) {
mb := c.MultiBuffer
c.MultiBuffer = nil
return mb, nil
}
// Write implements io.Writer.
func (c *MultiBufferContainer) Write(b []byte) (int, error) {
c.MultiBuffer = MergeBytes(c.MultiBuffer, b)
return len(b), nil
}
// WriteMultiBuffer implements Writer.
func (c *MultiBufferContainer) WriteMultiBuffer(b MultiBuffer) error {
mb, _ := MergeMulti(c.MultiBuffer, b)
c.MultiBuffer = mb
return nil
}
// Close implements io.Closer.
func (c *MultiBufferContainer) Close() error {
c.MultiBuffer = ReleaseMulti(c.MultiBuffer)
return nil
}

View File

@@ -0,0 +1,38 @@
package buf
import (
"github.com/sagernet/sing-box/common/xray/net"
)
type EndpointOverrideReader struct {
Reader
Dest net.Address
OriginalDest net.Address
}
func (r *EndpointOverrideReader) ReadMultiBuffer() (MultiBuffer, error) {
mb, err := r.Reader.ReadMultiBuffer()
if err == nil {
for _, b := range mb {
if b.UDP != nil && b.UDP.Address == r.OriginalDest {
b.UDP.Address = r.Dest
}
}
}
return mb, err
}
type EndpointOverrideWriter struct {
Writer
Dest net.Address
OriginalDest net.Address
}
func (w *EndpointOverrideWriter) WriteMultiBuffer(mb MultiBuffer) error {
for _, b := range mb {
if b.UDP != nil && b.UDP.Address == w.Dest {
b.UDP.Address = w.OriginalDest
}
}
return w.Writer.WriteMultiBuffer(mb)
}

175
common/xray/buf/reader.go Normal file
View File

@@ -0,0 +1,175 @@
package buf
import (
"io"
"github.com/sagernet/sing-box/common/xray"
"github.com/sagernet/sing-box/common/xray/errors"
E "github.com/sagernet/sing/common/exceptions"
)
func readOneUDP(r io.Reader) (*Buffer, error) {
b := New()
for i := 0; i < 64; i++ {
_, err := b.ReadFrom(r)
if !b.IsEmpty() {
return b, nil
}
if err != nil {
b.Release()
return nil, err
}
}
b.Release()
return nil, E.New("Reader returns too many empty payloads.")
}
// ReadBuffer reads a Buffer from the given reader.
func ReadBuffer(r io.Reader) (*Buffer, error) {
b := New()
n, err := b.ReadFrom(r)
if n > 0 {
return b, err
}
b.Release()
return nil, err
}
// BufferedReader is a Reader that keeps its internal buffer.
type BufferedReader struct {
// Reader is the underlying reader to be read from
Reader Reader
// Buffer is the internal buffer to be read from first
Buffer MultiBuffer
// Splitter is a function to read bytes from MultiBuffer
Splitter func(MultiBuffer, []byte) (MultiBuffer, int)
}
// BufferedBytes returns the number of bytes that is cached in this reader.
func (r *BufferedReader) BufferedBytes() int32 {
return r.Buffer.Len()
}
// ReadByte implements io.ByteReader.
func (r *BufferedReader) ReadByte() (byte, error) {
var b [1]byte
_, err := r.Read(b[:])
return b[0], err
}
// Read implements io.Reader. It reads from internal buffer first (if available) and then reads from the underlying reader.
func (r *BufferedReader) Read(b []byte) (int, error) {
spliter := r.Splitter
if spliter == nil {
spliter = SplitBytes
}
if !r.Buffer.IsEmpty() {
buffer, nBytes := spliter(r.Buffer, b)
r.Buffer = buffer
if r.Buffer.IsEmpty() {
r.Buffer = nil
}
return nBytes, nil
}
mb, err := r.Reader.ReadMultiBuffer()
if err != nil {
return 0, err
}
mb, nBytes := spliter(mb, b)
if !mb.IsEmpty() {
r.Buffer = mb
}
return nBytes, nil
}
// ReadMultiBuffer implements Reader.
func (r *BufferedReader) ReadMultiBuffer() (MultiBuffer, error) {
if !r.Buffer.IsEmpty() {
mb := r.Buffer
r.Buffer = nil
return mb, nil
}
return r.Reader.ReadMultiBuffer()
}
// ReadAtMost returns a MultiBuffer with at most size.
func (r *BufferedReader) ReadAtMost(size int32) (MultiBuffer, error) {
if r.Buffer.IsEmpty() {
mb, err := r.Reader.ReadMultiBuffer()
if mb.IsEmpty() && err != nil {
return nil, err
}
r.Buffer = mb
}
rb, mb := SplitSize(r.Buffer, size)
r.Buffer = rb
if r.Buffer.IsEmpty() {
r.Buffer = nil
}
return mb, nil
}
func (r *BufferedReader) writeToInternal(writer io.Writer) (int64, error) {
mbWriter := NewWriter(writer)
var sc SizeCounter
if r.Buffer != nil {
sc.Size = int64(r.Buffer.Len())
if err := mbWriter.WriteMultiBuffer(r.Buffer); err != nil {
return 0, err
}
r.Buffer = nil
}
err := Copy(r.Reader, mbWriter, CountSize(&sc))
return sc.Size, err
}
// WriteTo implements io.WriterTo.
func (r *BufferedReader) WriteTo(writer io.Writer) (int64, error) {
nBytes, err := r.writeToInternal(writer)
if errors.Cause(err) == io.EOF {
return nBytes, nil
}
return nBytes, err
}
// Interrupt implements common.Interruptible.
func (r *BufferedReader) Interrupt() {
common.Interrupt(r.Reader)
}
// Close implements io.Closer.
func (r *BufferedReader) Close() error {
return common.Close(r.Reader)
}
// SingleReader is a Reader that read one Buffer every time.
type SingleReader struct {
io.Reader
}
// ReadMultiBuffer implements Reader.
func (r *SingleReader) ReadMultiBuffer() (MultiBuffer, error) {
b, err := ReadBuffer(r.Reader)
return MultiBuffer{b}, err
}
// PacketReader is a Reader that read one Buffer every time.
type PacketReader struct {
io.Reader
}
// ReadMultiBuffer implements Reader.
func (r *PacketReader) ReadMultiBuffer() (MultiBuffer, error) {
b, err := readOneUDP(r.Reader)
if err != nil {
return nil, err
}
return MultiBuffer{b}, nil
}

270
common/xray/buf/writer.go Normal file
View File

@@ -0,0 +1,270 @@
package buf
import (
"io"
"net"
"sync"
"github.com/sagernet/sing-box/common/xray"
"github.com/sagernet/sing-box/common/xray/errors"
"github.com/sagernet/sing-box/common/xray/stats"
)
// BufferToBytesWriter is a Writer that writes alloc.Buffer into underlying writer.
type BufferToBytesWriter struct {
io.Writer
counter stats.Counter
cache [][]byte
}
// WriteMultiBuffer implements Writer. This method takes ownership of the given buffer.
func (w *BufferToBytesWriter) WriteMultiBuffer(mb MultiBuffer) error {
defer ReleaseMulti(mb)
size := mb.Len()
if size == 0 {
return nil
}
if len(mb) == 1 {
return WriteAllBytes(w.Writer, mb[0].Bytes(), w.counter)
}
if cap(w.cache) < len(mb) {
w.cache = make([][]byte, 0, len(mb))
}
bs := w.cache
for _, b := range mb {
bs = append(bs, b.Bytes())
}
defer func() {
for idx := range bs {
bs[idx] = nil
}
}()
nb := net.Buffers(bs)
wc := int64(0)
defer func() {
if w.counter != nil {
w.counter.Add(wc)
}
}()
for size > 0 {
n, err := nb.WriteTo(w.Writer)
wc += n
if err != nil {
return err
}
size -= int32(n)
}
return nil
}
// ReadFrom implements io.ReaderFrom.
func (w *BufferToBytesWriter) ReadFrom(reader io.Reader) (int64, error) {
var sc SizeCounter
err := Copy(NewReader(reader), w, CountSize(&sc))
return sc.Size, err
}
// BufferedWriter is a Writer with internal buffer.
type BufferedWriter struct {
sync.Mutex
writer Writer
buffer *Buffer
buffered bool
}
// NewBufferedWriter creates a new BufferedWriter.
func NewBufferedWriter(writer Writer) *BufferedWriter {
return &BufferedWriter{
writer: writer,
buffer: New(),
buffered: true,
}
}
// WriteByte implements io.ByteWriter.
func (w *BufferedWriter) WriteByte(c byte) error {
return common.Error2(w.Write([]byte{c}))
}
// Write implements io.Writer.
func (w *BufferedWriter) Write(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
w.Lock()
defer w.Unlock()
if !w.buffered {
if writer, ok := w.writer.(io.Writer); ok {
return writer.Write(b)
}
}
totalBytes := 0
for len(b) > 0 {
if w.buffer == nil {
w.buffer = New()
}
nBytes, err := w.buffer.Write(b)
totalBytes += nBytes
if err != nil {
return totalBytes, err
}
if !w.buffered || w.buffer.IsFull() {
if err := w.flushInternal(); err != nil {
return totalBytes, err
}
}
b = b[nBytes:]
}
return totalBytes, nil
}
// WriteMultiBuffer implements Writer. It takes ownership of the given MultiBuffer.
func (w *BufferedWriter) WriteMultiBuffer(b MultiBuffer) error {
if b.IsEmpty() {
return nil
}
w.Lock()
defer w.Unlock()
if !w.buffered {
return w.writer.WriteMultiBuffer(b)
}
reader := MultiBufferContainer{
MultiBuffer: b,
}
defer reader.Close()
for !reader.MultiBuffer.IsEmpty() {
if w.buffer == nil {
w.buffer = New()
}
common.Must2(w.buffer.ReadFrom(&reader))
if w.buffer.IsFull() {
if err := w.flushInternal(); err != nil {
return err
}
}
}
return nil
}
// Flush flushes buffered content into underlying writer.
func (w *BufferedWriter) Flush() error {
w.Lock()
defer w.Unlock()
return w.flushInternal()
}
func (w *BufferedWriter) flushInternal() error {
if w.buffer.IsEmpty() {
return nil
}
b := w.buffer
w.buffer = nil
if writer, ok := w.writer.(io.Writer); ok {
err := WriteAllBytes(writer, b.Bytes(), nil)
b.Release()
return err
}
return w.writer.WriteMultiBuffer(MultiBuffer{b})
}
// SetBuffered sets whether the internal buffer is used. If set to false, Flush() will be called to clear the buffer.
func (w *BufferedWriter) SetBuffered(f bool) error {
w.Lock()
defer w.Unlock()
w.buffered = f
if !f {
return w.flushInternal()
}
return nil
}
// ReadFrom implements io.ReaderFrom.
func (w *BufferedWriter) ReadFrom(reader io.Reader) (int64, error) {
if err := w.SetBuffered(false); err != nil {
return 0, err
}
var sc SizeCounter
err := Copy(NewReader(reader), w, CountSize(&sc))
return sc.Size, err
}
// Close implements io.Closable.
func (w *BufferedWriter) Close() error {
if err := w.Flush(); err != nil {
return err
}
return common.Close(w.writer)
}
// SequentialWriter is a Writer that writes MultiBuffer sequentially into the underlying io.Writer.
type SequentialWriter struct {
io.Writer
}
// WriteMultiBuffer implements Writer.
func (w *SequentialWriter) WriteMultiBuffer(mb MultiBuffer) error {
mb, err := WriteMultiBuffer(w.Writer, mb)
ReleaseMulti(mb)
return err
}
type noOpWriter byte
func (noOpWriter) WriteMultiBuffer(b MultiBuffer) error {
ReleaseMulti(b)
return nil
}
func (noOpWriter) Write(b []byte) (int, error) {
return len(b), nil
}
func (noOpWriter) ReadFrom(reader io.Reader) (int64, error) {
b := New()
defer b.Release()
totalBytes := int64(0)
for {
b.Clear()
_, err := b.ReadFrom(reader)
totalBytes += int64(b.Len())
if err != nil {
if errors.Cause(err) == io.EOF {
return totalBytes, nil
}
return totalBytes, err
}
}
}
var (
// Discard is a Writer that swallows all contents written in.
Discard Writer = noOpWriter(0)
// DiscardBytes is an io.Writer that swallows all contents written in.
DiscardBytes io.Writer = noOpWriter(0)
)

View File

@@ -0,0 +1,72 @@
package bytespool
import "sync"
func createAllocFunc(size int32) func() interface{} {
return func() interface{} {
return make([]byte, size)
}
}
// The following parameters controls the size of buffer pools.
// There are numPools pools. Starting from 2k size, the size of each pool is sizeMulti of the previous one.
// Package buf is guaranteed to not use buffers larger than the largest pool.
// Other packets may use larger buffers.
const (
numPools = 4
sizeMulti = 4
)
var (
pool [numPools]sync.Pool
poolSize [numPools]int32
)
func init() {
size := int32(2048)
for i := 0; i < numPools; i++ {
pool[i] = sync.Pool{
New: createAllocFunc(size),
}
poolSize[i] = size
size *= sizeMulti
}
}
// GetPool returns a sync.Pool that generates bytes array with at least the given size.
// It may return nil if no such pool exists.
//
// xray:api:stable
func GetPool(size int32) *sync.Pool {
for idx, ps := range poolSize {
if size <= ps {
return &pool[idx]
}
}
return nil
}
// Alloc returns a byte slice with at least the given size. Minimum size of returned slice is 2048.
//
// xray:api:stable
func Alloc(size int32) []byte {
pool := GetPool(size)
if pool != nil {
return pool.Get().([]byte)
}
return make([]byte, size)
}
// Free puts a byte slice into the internal pool.
//
// xray:api:stable
func Free(b []byte) {
size := int32(cap(b))
b = b[0:cap(b)]
for i := numPools - 1; i >= 0; i-- {
if size >= poolSize[i] {
pool[i].Put(b)
return
}
}
}

19
common/xray/common.go Normal file
View File

@@ -0,0 +1,19 @@
package common
// Must panics if err is not nil.
func Must(err error) {
if err != nil {
panic(err)
}
}
// Must2 panics if the second parameter is not nil, otherwise returns the first parameter.
func Must2(v interface{}, err error) interface{} {
Must(err)
return v
}
// Error2 returns the err from the 2nd parameter.
func Error2(v interface{}, err error) error {
return err
}

View File

@@ -0,0 +1,14 @@
package crypto
import (
"crypto/rand"
"math/big"
)
func RandBetween(from int64, to int64) int64 {
if from == to {
return from
}
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
return from + bigInt.Int64()
}

View File

@@ -0,0 +1,25 @@
package errors
type hasInnerError interface {
// Unwrap returns the underlying error of this one.
Unwrap() error
}
func Cause(err error) error {
if err == nil {
return nil
}
L:
for {
switch inner := err.(type) {
case hasInnerError:
if inner.Unwrap() == nil {
break L
}
err = inner.Unwrap()
default:
break L
}
}
return err
}

52
common/xray/interfaces.go Normal file
View File

@@ -0,0 +1,52 @@
package common
// Closable is the interface for objects that can release its resources.
//
// xray:api:beta
type Closable interface {
// Close release all resources used by this object, including goroutines.
Close() error
}
// Interruptible is an interface for objects that can be stopped before its completion.
//
// xray:api:beta
type Interruptible interface {
Interrupt()
}
// Close closes the obj if it is a Closable.
//
// xray:api:beta
func Close(obj interface{}) error {
if c, ok := obj.(Closable); ok {
return c.Close()
}
return nil
}
// Interrupt calls Interrupt() if object implements Interruptible interface, or Close() if the object implements Closable interface.
//
// xray:api:beta
func Interrupt(obj interface{}) error {
if c, ok := obj.(Interruptible); ok {
c.Interrupt()
return nil
}
return Close(obj)
}
// Runnable is the interface for objects that can start to work and stop on demand.
type Runnable interface {
// Start starts the runnable object. Upon the method returning nil, the object begins to function properly.
Start() error
Closable
}
// HasType is the interface for objects that knows its type.
type HasType interface {
// Type returns the type of the object.
// Usually it returns (*Type)(nil) of the object.
Type() interface{}
}

View File

@@ -0,0 +1,62 @@
package badoption
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/sagernet/sing-box/common/xray/crypto"
E "github.com/sagernet/sing/common/exceptions"
)
type Range struct {
From int32 `json:"from"`
To int32 `json:"to"`
}
func (c *Range) Build() *Range {
return (*Range)(c)
}
func (c *Range) MarshalJSON() ([]byte, error) {
return json.Marshal(fmt.Sprintf("%d-%d", c.From, c.To))
}
func (c *Range) UnmarshalJSON(content []byte) error {
var rangeValue struct {
From int32 `json:"from"`
To int32 `json:"to"`
}
var stringValue string
err := json.Unmarshal(content, &stringValue)
if err == nil {
parts := strings.Split(stringValue, "-")
if len(parts) != 2 {
return E.New("invalid length of range parts")
}
from, err := strconv.ParseInt(parts[0], 10, 32)
if err != nil {
return err
}
to, err := strconv.ParseInt(parts[1], 10, 32)
if err != nil {
return err
}
rangeValue.From, rangeValue.To = int32(from), int32(to)
} else {
err := json.Unmarshal(content, &rangeValue)
if err != nil {
return err
}
}
if rangeValue.From > rangeValue.To {
return E.New("invalid range")
}
*c = Range{rangeValue.From, rangeValue.To}
return nil
}
func (c Range) Rand() int32 {
return int32(crypto.RandBetween(int64(c.From), int64(c.To)))
}

181
common/xray/net/address.go Normal file
View File

@@ -0,0 +1,181 @@
package net
import (
"bytes"
"net"
"strings"
)
var (
// LocalHostIP is a constant value for localhost IP in IPv4.
LocalHostIP = IPAddress([]byte{127, 0, 0, 1})
// AnyIP is a constant value for any IP in IPv4.
AnyIP = IPAddress([]byte{0, 0, 0, 0})
// LocalHostDomain is a constant value for localhost domain.
LocalHostDomain = DomainAddress("localhost")
// LocalHostIPv6 is a constant value for localhost IP in IPv6.
LocalHostIPv6 = IPAddress([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
// AnyIPv6 is a constant value for any IP in IPv6.
AnyIPv6 = IPAddress([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
)
// AddressFamily is the type of address.
type AddressFamily byte
const (
// AddressFamilyIPv4 represents address as IPv4
AddressFamilyIPv4 = AddressFamily(0)
// AddressFamilyIPv6 represents address as IPv6
AddressFamilyIPv6 = AddressFamily(1)
// AddressFamilyDomain represents address as Domain
AddressFamilyDomain = AddressFamily(2)
)
// IsIPv4 returns true if current AddressFamily is IPv4.
func (af AddressFamily) IsIPv4() bool {
return af == AddressFamilyIPv4
}
// IsIPv6 returns true if current AddressFamily is IPv6.
func (af AddressFamily) IsIPv6() bool {
return af == AddressFamilyIPv6
}
// IsIP returns true if current AddressFamily is IPv6 or IPv4.
func (af AddressFamily) IsIP() bool {
return af == AddressFamilyIPv4 || af == AddressFamilyIPv6
}
// IsDomain returns true if current AddressFamily is Domain.
func (af AddressFamily) IsDomain() bool {
return af == AddressFamilyDomain
}
// Address represents a network address to be communicated with. It may be an IP address or domain
// address, not both. This interface doesn't resolve IP address for a given domain.
type Address interface {
IP() net.IP // IP of this Address
Domain() string // Domain of this Address
Family() AddressFamily
String() string // String representation of this Address
}
func isAlphaNum(c byte) bool {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
// ParseAddress parses a string into an Address. The return value will be an IPAddress when
// the string is in the form of IPv4 or IPv6 address, or a DomainAddress otherwise.
func ParseAddress(addr string) Address {
// Handle IPv6 address in form as "[2001:4860:0:2001::68]"
lenAddr := len(addr)
if lenAddr > 0 && addr[0] == '[' && addr[lenAddr-1] == ']' {
addr = addr[1 : lenAddr-1]
lenAddr -= 2
}
if lenAddr > 0 && (!isAlphaNum(addr[0]) || !isAlphaNum(addr[len(addr)-1])) {
addr = strings.TrimSpace(addr)
}
ip := net.ParseIP(addr)
if ip != nil {
return IPAddress(ip)
}
return DomainAddress(addr)
}
var bytes0 = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
// IPAddress creates an Address with given IP.
func IPAddress(ip []byte) Address {
switch len(ip) {
case net.IPv4len:
var addr ipv4Address = [4]byte{ip[0], ip[1], ip[2], ip[3]}
return addr
case net.IPv6len:
if bytes.Equal(ip[:10], bytes0) && ip[10] == 0xff && ip[11] == 0xff {
return IPAddress(ip[12:16])
}
var addr ipv6Address = [16]byte{
ip[0], ip[1], ip[2], ip[3],
ip[4], ip[5], ip[6], ip[7],
ip[8], ip[9], ip[10], ip[11],
ip[12], ip[13], ip[14], ip[15],
}
return addr
default:
return nil
}
}
// DomainAddress creates an Address with given domain.
// This is an internal function that forcibly converts a string to domain.
// It's mainly used in test files and mux.
// Unless you have a specific reason, use net.ParseAddress instead,
// as this function does not check whether the input is an IP address.
// Otherwise, you will get strange results like domain: 1.1.1.1
func DomainAddress(domain string) Address {
return domainAddress(domain)
}
type ipv4Address [4]byte
func (a ipv4Address) IP() net.IP {
return net.IP(a[:])
}
func (ipv4Address) Domain() string {
panic("Calling Domain() on an IPv4Address.")
}
func (ipv4Address) Family() AddressFamily {
return AddressFamilyIPv4
}
func (a ipv4Address) String() string {
return a.IP().String()
}
type ipv6Address [16]byte
func (a ipv6Address) IP() net.IP {
return net.IP(a[:])
}
func (ipv6Address) Domain() string {
panic("Calling Domain() on an IPv6Address.")
}
func (ipv6Address) Family() AddressFamily {
return AddressFamilyIPv6
}
func (a ipv6Address) String() string {
return "[" + a.IP().String() + "]"
}
type domainAddress string
func (domainAddress) IP() net.IP {
panic("Calling IP() on a DomainAddress.")
}
func (a domainAddress) Domain() string {
return string(a)
}
func (domainAddress) Family() AddressFamily {
return AddressFamilyDomain
}
func (a domainAddress) String() string {
return a.Domain()
}

View File

@@ -0,0 +1,146 @@
package net
import (
"net"
"strings"
)
// Destination represents a network destination including address and protocol (tcp / udp).
type Destination struct {
Address Address
Port Port
Network Network
}
// DestinationFromAddr generates a Destination from a net address.
func DestinationFromAddr(addr net.Addr) Destination {
switch addr := addr.(type) {
case *net.TCPAddr:
return TCPDestination(IPAddress(addr.IP), Port(addr.Port))
case *net.UDPAddr:
return UDPDestination(IPAddress(addr.IP), Port(addr.Port))
case *net.UnixAddr:
return UnixDestination(DomainAddress(addr.Name))
default:
panic("Net: Unknown address type.")
}
}
// ParseDestination converts a destination from its string presentation.
func ParseDestination(dest string) (Destination, error) {
d := Destination{
Address: AnyIP,
Port: Port(0),
}
if strings.HasPrefix(dest, "tcp:") {
d.Network = Network_TCP
dest = dest[4:]
} else if strings.HasPrefix(dest, "udp:") {
d.Network = Network_UDP
dest = dest[4:]
} else if strings.HasPrefix(dest, "unix:") {
d = UnixDestination(DomainAddress(dest[5:]))
return d, nil
}
hstr, pstr, err := SplitHostPort(dest)
if err != nil {
return d, err
}
if len(hstr) > 0 {
d.Address = ParseAddress(hstr)
}
if len(pstr) > 0 {
port, err := PortFromString(pstr)
if err != nil {
return d, err
}
d.Port = port
}
return d, nil
}
// TCPDestination creates a TCP destination with given address
func TCPDestination(address Address, port Port) Destination {
return Destination{
Network: Network_TCP,
Address: address,
Port: port,
}
}
// UDPDestination creates a UDP destination with given address
func UDPDestination(address Address, port Port) Destination {
return Destination{
Network: Network_UDP,
Address: address,
Port: port,
}
}
// UnixDestination creates a Unix destination with given address
func UnixDestination(address Address) Destination {
return Destination{
Network: Network_UNIX,
Address: address,
}
}
// NetAddr returns the network address in this Destination in string form.
func (d Destination) NetAddr() string {
addr := ""
if d.Network == Network_TCP || d.Network == Network_UDP {
addr = d.Address.String() + ":" + d.Port.String()
} else if d.Network == Network_UNIX {
addr = d.Address.String()
}
return addr
}
// RawNetAddr converts a net.Addr from its Destination presentation.
func (d Destination) RawNetAddr() net.Addr {
var addr net.Addr
switch d.Network {
case Network_TCP:
if d.Address.Family().IsIP() {
addr = &net.TCPAddr{
IP: d.Address.IP(),
Port: int(d.Port),
}
}
case Network_UDP:
if d.Address.Family().IsIP() {
addr = &net.UDPAddr{
IP: d.Address.IP(),
Port: int(d.Port),
}
}
case Network_UNIX:
if d.Address.Family().IsDomain() {
addr = &net.UnixAddr{
Name: d.Address.String(),
Net: d.Network.SystemString(),
}
}
}
return addr
}
// String returns the strings form of this Destination.
func (d Destination) String() string {
prefix := "unknown:"
switch d.Network {
case Network_TCP:
prefix = "tcp:"
case Network_UDP:
prefix = "udp:"
case Network_UNIX:
prefix = "unix:"
}
return prefix + d.NetAddr()
}
// IsValid returns true if this Destination is valid.
func (d Destination) IsValid() bool {
return d.Network != Network_Unknown
}

13
common/xray/net/net.go Normal file
View File

@@ -0,0 +1,13 @@
package net
import "time"
// defines the maximum time an idle TCP session can survive in the tunnel, so
// it should be consistent across HTTP versions and with other transports.
const ConnIdleTimeout = 300 * time.Second
// consistent with quic-go
const QuicgoH3KeepAlivePeriod = 10 * time.Second
// consistent with chrome
const ChromeH2KeepAlivePeriod = 45 * time.Second

View File

@@ -0,0 +1,33 @@
package net
type Network int32
const (
Network_Unknown Network = 0
Network_TCP Network = 2
Network_UDP Network = 3
Network_UNIX Network = 4
)
func (n Network) SystemString() string {
switch n {
case Network_TCP:
return "tcp"
case Network_UDP:
return "udp"
case Network_UNIX:
return "unix"
default:
return "unknown"
}
}
// HasNetwork returns true if the network list has a certain network.
func HasNetwork(list []Network, network Network) bool {
for _, value := range list {
if value == network {
return true
}
}
return false
}

55
common/xray/net/port.go Normal file
View File

@@ -0,0 +1,55 @@
package net
import (
"encoding/binary"
"strconv"
E "github.com/sagernet/sing/common/exceptions"
)
// Port represents a network port in TCP and UDP protocol.
type Port uint16
// PortFromBytes converts a byte array to a Port, assuming bytes are in big endian order.
// @unsafe Caller must ensure that the byte array has at least 2 elements.
func PortFromBytes(port []byte) Port {
return Port(binary.BigEndian.Uint16(port))
}
// PortFromInt converts an integer to a Port.
// @error when the integer is not positive or larger then 65535
func PortFromInt(val uint32) (Port, error) {
if val > 65535 {
return Port(0), E.New("invalid port range: ", val)
}
return Port(val), nil
}
// PortFromString converts a string to a Port.
// @error when the string is not an integer or the integral value is a not a valid Port.
func PortFromString(s string) (Port, error) {
val, err := strconv.ParseUint(s, 10, 32)
if err != nil {
return Port(0), E.New("invalid port range: ", s)
}
return PortFromInt(uint32(val))
}
// Value return the corresponding uint16 value of a Port.
func (p Port) Value() uint16 {
return uint16(p)
}
// String returns the string presentation of a Port.
func (p Port) String() string {
return strconv.Itoa(int(p))
}
type MemoryPortRange struct {
From Port
To Port
}
func (r MemoryPortRange) Contains(port Port) bool {
return r.From <= port && port <= r.To
}

84
common/xray/net/system.go Normal file
View File

@@ -0,0 +1,84 @@
package net
import "net"
// DialTCP is an alias of net.DialTCP.
var (
DialTCP = net.DialTCP
DialUDP = net.DialUDP
DialUnix = net.DialUnix
Dial = net.Dial
)
type ListenConfig = net.ListenConfig
var (
Listen = net.Listen
ListenTCP = net.ListenTCP
ListenUDP = net.ListenUDP
ListenUnix = net.ListenUnix
)
var LookupIP = net.LookupIP
var FileConn = net.FileConn
// ParseIP is an alias of net.ParseIP
var ParseIP = net.ParseIP
var SplitHostPort = net.SplitHostPort
var CIDRMask = net.CIDRMask
type (
Addr = net.Addr
Conn = net.Conn
PacketConn = net.PacketConn
)
type (
TCPAddr = net.TCPAddr
TCPConn = net.TCPConn
)
type (
UDPAddr = net.UDPAddr
UDPConn = net.UDPConn
)
type (
UnixAddr = net.UnixAddr
UnixConn = net.UnixConn
)
// IP is an alias for net.IP.
type (
IP = net.IP
IPMask = net.IPMask
IPNet = net.IPNet
)
const (
IPv4len = net.IPv4len
IPv6len = net.IPv6len
)
type (
Error = net.Error
AddrError = net.AddrError
)
type (
Dialer = net.Dialer
Listener = net.Listener
TCPListener = net.TCPListener
UnixListener = net.UnixListener
)
var (
ResolveTCPAddr = net.ResolveTCPAddr
ResolveUDPAddr = net.ResolveUDPAddr
ResolveUnixAddr = net.ResolveUnixAddr
)
type Resolver = net.Resolver

215
common/xray/pipe/impl.go Normal file
View File

@@ -0,0 +1,215 @@
package pipe
import (
"errors"
"io"
"runtime"
"sync"
"time"
"github.com/sagernet/sing-box/common/xray"
"github.com/sagernet/sing-box/common/xray/buf"
"github.com/sagernet/sing-box/common/xray/signal"
"github.com/sagernet/sing-box/common/xray/signal/done"
)
type state byte
const (
open state = iota
closed
errord
)
type pipeOption struct {
limit int32 // maximum buffer size in bytes
discardOverflow bool
}
func (o *pipeOption) isFull(curSize int32) bool {
return o.limit >= 0 && curSize > o.limit
}
type pipe struct {
sync.Mutex
data buf.MultiBuffer
readSignal *signal.Notifier
writeSignal *signal.Notifier
done *done.Instance
errChan chan error
option pipeOption
state state
}
var (
errBufferFull = errors.New("buffer full")
errSlowDown = errors.New("slow down")
)
func (p *pipe) Len() int32 {
data := p.data
if data == nil {
return 0
}
return data.Len()
}
func (p *pipe) getState(forRead bool) error {
switch p.state {
case open:
if !forRead && p.option.isFull(p.data.Len()) {
return errBufferFull
}
return nil
case closed:
if !forRead {
return io.ErrClosedPipe
}
if !p.data.IsEmpty() {
return nil
}
return io.EOF
case errord:
return io.ErrClosedPipe
default:
panic("impossible case")
}
}
func (p *pipe) readMultiBufferInternal() (buf.MultiBuffer, error) {
p.Lock()
defer p.Unlock()
if err := p.getState(true); err != nil {
return nil, err
}
data := p.data
p.data = nil
return data, nil
}
func (p *pipe) ReadMultiBuffer() (buf.MultiBuffer, error) {
for {
data, err := p.readMultiBufferInternal()
if data != nil || err != nil {
p.writeSignal.Signal()
return data, err
}
select {
case <-p.readSignal.Wait():
case <-p.done.Wait():
case err = <-p.errChan:
return nil, err
}
}
}
func (p *pipe) ReadMultiBufferTimeout(d time.Duration) (buf.MultiBuffer, error) {
timer := time.NewTimer(d)
defer timer.Stop()
for {
data, err := p.readMultiBufferInternal()
if data != nil || err != nil {
p.writeSignal.Signal()
return data, err
}
select {
case <-p.readSignal.Wait():
case <-p.done.Wait():
case <-timer.C:
return nil, buf.ErrReadTimeout
}
}
}
func (p *pipe) writeMultiBufferInternal(mb buf.MultiBuffer) error {
p.Lock()
defer p.Unlock()
if err := p.getState(false); err != nil {
return err
}
if p.data == nil {
p.data = mb
return nil
}
p.data, _ = buf.MergeMulti(p.data, mb)
return errSlowDown
}
func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error {
if mb.IsEmpty() {
return nil
}
for {
err := p.writeMultiBufferInternal(mb)
if err == nil {
p.readSignal.Signal()
return nil
}
if err == errSlowDown {
p.readSignal.Signal()
// Yield current goroutine. Hopefully the reading counterpart can pick up the payload.
runtime.Gosched()
return nil
}
if err == errBufferFull && p.option.discardOverflow {
buf.ReleaseMulti(mb)
return nil
}
if err != errBufferFull {
buf.ReleaseMulti(mb)
p.readSignal.Signal()
return err
}
select {
case <-p.writeSignal.Wait():
case <-p.done.Wait():
return io.ErrClosedPipe
}
}
}
func (p *pipe) Close() error {
p.Lock()
defer p.Unlock()
if p.state == closed || p.state == errord {
return nil
}
p.state = closed
common.Must(p.done.Close())
return nil
}
// Interrupt implements common.Interruptible.
func (p *pipe) Interrupt() {
p.Lock()
defer p.Unlock()
if p.state == closed || p.state == errord {
return
}
p.state = errord
if !p.data.IsEmpty() {
buf.ReleaseMulti(p.data)
p.data = nil
}
common.Must(p.done.Close())
}

53
common/xray/pipe/pipe.go Normal file
View File

@@ -0,0 +1,53 @@
package pipe
import (
"github.com/sagernet/sing-box/common/xray/signal"
"github.com/sagernet/sing-box/common/xray/signal/done"
)
// Option for creating new Pipes.
type Option func(*pipeOption)
// WithoutSizeLimit returns an Option for Pipe to have no size limit.
func WithoutSizeLimit() Option {
return func(opt *pipeOption) {
opt.limit = -1
}
}
// WithSizeLimit returns an Option for Pipe to have the given size limit.
func WithSizeLimit(limit int32) Option {
return func(opt *pipeOption) {
opt.limit = limit
}
}
// DiscardOverflow returns an Option for Pipe to discard writes if full.
func DiscardOverflow() Option {
return func(opt *pipeOption) {
opt.discardOverflow = true
}
}
// New creates a new Reader and Writer that connects to each other.
func New(opts ...Option) (*Reader, *Writer) {
p := &pipe{
readSignal: signal.NewNotifier(),
writeSignal: signal.NewNotifier(),
done: done.New(),
errChan: make(chan error, 1),
option: pipeOption{
limit: -1,
},
}
for _, opt := range opts {
opt(&(p.option))
}
return &Reader{
pipe: p,
}, &Writer{
pipe: p,
}
}

View File

@@ -0,0 +1,41 @@
package pipe
import (
"time"
"github.com/sagernet/sing-box/common/xray/buf"
)
// Reader is a buf.Reader that reads content from a pipe.
type Reader struct {
pipe *pipe
}
// ReadMultiBuffer implements buf.Reader.
func (r *Reader) ReadMultiBuffer() (buf.MultiBuffer, error) {
return r.pipe.ReadMultiBuffer()
}
// ReadMultiBufferTimeout reads content from a pipe within the given duration, or returns buf.ErrTimeout otherwise.
func (r *Reader) ReadMultiBufferTimeout(d time.Duration) (buf.MultiBuffer, error) {
return r.pipe.ReadMultiBufferTimeout(d)
}
// Interrupt implements common.Interruptible.
func (r *Reader) Interrupt() {
r.pipe.Interrupt()
}
// ReturnAnError makes ReadMultiBuffer return an error, only once.
func (r *Reader) ReturnAnError(err error) {
r.pipe.errChan <- err
}
// Recover catches an error set by ReturnAnError, if exists.
func (r *Reader) Recover() (err error) {
select {
case err = <-r.pipe.errChan:
default:
}
return
}

View File

@@ -0,0 +1,29 @@
package pipe
import (
"github.com/sagernet/sing-box/common/xray/buf"
)
// Writer is a buf.Writer that writes data into a pipe.
type Writer struct {
pipe *pipe
}
// WriteMultiBuffer implements buf.Writer.
func (w *Writer) WriteMultiBuffer(mb buf.MultiBuffer) error {
return w.pipe.WriteMultiBuffer(mb)
}
// Close implements io.Closer. After the pipe is closed, writing to the pipe will return io.ErrClosedPipe, while reading will return io.EOF.
func (w *Writer) Close() error {
return w.pipe.Close()
}
func (w *Writer) Len() int32 {
return w.pipe.Len()
}
// Interrupt implements common.Interruptible.
func (w *Writer) Interrupt() {
w.pipe.Interrupt()
}

View File

@@ -0,0 +1,29 @@
package serial
import (
"encoding/binary"
"io"
)
// ReadUint16 reads first two bytes from the reader, and then converts them to an uint16 value.
func ReadUint16(reader io.Reader) (uint16, error) {
var b [2]byte
if _, err := io.ReadFull(reader, b[:]); err != nil {
return 0, err
}
return binary.BigEndian.Uint16(b[:]), nil
}
// WriteUint16 writes an uint16 value into writer.
func WriteUint16(writer io.Writer, value uint16) (int, error) {
var b [2]byte
binary.BigEndian.PutUint16(b[:], value)
return writer.Write(b[:])
}
// WriteUint64 writes an uint64 value into writer.
func WriteUint64(writer io.Writer, value uint64) (int, error) {
var b [8]byte
binary.BigEndian.PutUint64(b[:], value)
return writer.Write(b[:])
}

View File

@@ -0,0 +1,35 @@
package serial
import (
"fmt"
"strings"
)
// ToString serializes an arbitrary value into string.
func ToString(v interface{}) string {
if v == nil {
return ""
}
switch value := v.(type) {
case string:
return value
case *string:
return *value
case fmt.Stringer:
return value.String()
case error:
return value.Error()
default:
return fmt.Sprintf("%+v", value)
}
}
// Concat concatenates all input into a single string.
func Concat(v ...interface{}) string {
builder := strings.Builder{}
for _, value := range v {
builder.WriteString(ToString(value))
}
return builder.String()
}

View File

@@ -0,0 +1,49 @@
package done
import (
"sync"
)
// Instance is a utility for notifications of something being done.
type Instance struct {
access sync.Mutex
c chan struct{}
closed bool
}
// New returns a new Done.
func New() *Instance {
return &Instance{
c: make(chan struct{}),
}
}
// Done returns true if Close() is called.
func (d *Instance) Done() bool {
select {
case <-d.Wait():
return true
default:
return false
}
}
// Wait returns a channel for waiting for done.
func (d *Instance) Wait() <-chan struct{} {
return d.c
}
// Close marks this Done 'done'. This method may be called multiple times. All calls after first call will have no effect on its status.
func (d *Instance) Close() error {
d.access.Lock()
defer d.access.Unlock()
if d.closed {
return nil
}
d.closed = true
close(d.c)
return nil
}

View File

@@ -0,0 +1,26 @@
package signal
// Notifier is a utility for notifying changes. The change producer may notify changes multiple time, and the consumer may get notified asynchronously.
type Notifier struct {
c chan struct{}
}
// NewNotifier creates a new Notifier.
func NewNotifier() *Notifier {
return &Notifier{
c: make(chan struct{}, 1),
}
}
// Signal signals a change, usually by producer. This method never blocks.
func (n *Notifier) Signal() {
select {
case n.c <- struct{}{}:
default:
}
}
// Wait returns a channel for waiting for changes. The returned channel never gets closed.
func (n *Notifier) Wait() <-chan struct{} {
return n.c
}

View File

@@ -0,0 +1,105 @@
package pubsub
import (
"errors"
"sync"
"time"
"github.com/sagernet/sing-box/common/xray"
"github.com/sagernet/sing-box/common/xray/signal/done"
"github.com/sagernet/sing-box/common/xray/task"
)
type Subscriber struct {
buffer chan interface{}
done *done.Instance
}
func (s *Subscriber) push(msg interface{}) {
select {
case s.buffer <- msg:
default:
}
}
func (s *Subscriber) Wait() <-chan interface{} {
return s.buffer
}
func (s *Subscriber) Close() error {
return s.done.Close()
}
func (s *Subscriber) IsClosed() bool {
return s.done.Done()
}
type Service struct {
sync.RWMutex
subs map[string][]*Subscriber
ctask *task.Periodic
}
func NewService() *Service {
s := &Service{
subs: make(map[string][]*Subscriber),
}
s.ctask = &task.Periodic{
Execute: s.Cleanup,
Interval: time.Second * 30,
}
return s
}
// Cleanup cleans up internal caches of subscribers.
// Visible for testing only.
func (s *Service) Cleanup() error {
s.Lock()
defer s.Unlock()
if len(s.subs) == 0 {
return errors.New("nothing to do")
}
for name, subs := range s.subs {
newSub := make([]*Subscriber, 0, len(s.subs))
for _, sub := range subs {
if !sub.IsClosed() {
newSub = append(newSub, sub)
}
}
if len(newSub) == 0 {
delete(s.subs, name)
} else {
s.subs[name] = newSub
}
}
if len(s.subs) == 0 {
s.subs = make(map[string][]*Subscriber)
}
return nil
}
func (s *Service) Subscribe(name string) *Subscriber {
sub := &Subscriber{
buffer: make(chan interface{}, 16),
done: done.New(),
}
s.Lock()
s.subs[name] = append(s.subs[name], sub)
s.Unlock()
common.Must(s.ctask.Start())
return sub
}
func (s *Service) Publish(name string, message interface{}) {
s.RLock()
defer s.RUnlock()
for _, sub := range s.subs[name] {
if !sub.IsClosed() {
sub.push(message)
}
}
}

View File

@@ -0,0 +1,27 @@
package semaphore
// Instance is an implementation of semaphore.
type Instance struct {
token chan struct{}
}
// New create a new Semaphore with n permits.
func New(n int) *Instance {
s := &Instance{
token: make(chan struct{}, n),
}
for i := 0; i < n; i++ {
s.token <- struct{}{}
}
return s
}
// Wait returns a channel for acquiring a permit.
func (s *Instance) Wait() <-chan struct{} {
return s.token
}
// Signal releases a permit into the semaphore.
func (s *Instance) Signal() {
s.token <- struct{}{}
}

View File

@@ -0,0 +1,82 @@
package signal
import (
"context"
"sync"
"time"
"github.com/sagernet/sing-box/common/xray"
"github.com/sagernet/sing-box/common/xray/task"
)
type ActivityUpdater interface {
Update()
}
type ActivityTimer struct {
sync.RWMutex
updated chan struct{}
checkTask *task.Periodic
onTimeout func()
}
func (t *ActivityTimer) Update() {
select {
case t.updated <- struct{}{}:
default:
}
}
func (t *ActivityTimer) check() error {
select {
case <-t.updated:
default:
t.finish()
}
return nil
}
func (t *ActivityTimer) finish() {
t.Lock()
defer t.Unlock()
if t.onTimeout != nil {
t.onTimeout()
t.onTimeout = nil
}
if t.checkTask != nil {
t.checkTask.Close()
t.checkTask = nil
}
}
func (t *ActivityTimer) SetTimeout(timeout time.Duration) {
if timeout == 0 {
t.finish()
return
}
checkTask := &task.Periodic{
Interval: timeout,
Execute: t.check,
}
t.Lock()
if t.checkTask != nil {
t.checkTask.Close()
}
t.checkTask = checkTask
t.Unlock()
t.Update()
common.Must(checkTask.Start())
}
func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer {
timer := &ActivityTimer{
updated: make(chan struct{}, 1),
onTimeout: cancel,
}
timer.SetTimeout(timeout)
return timer
}

View File

@@ -0,0 +1,34 @@
package stat
import (
"net"
"github.com/sagernet/sing-box/common/xray/stats"
)
type Connection interface {
net.Conn
}
type CounterConnection struct {
Connection
ReadCounter stats.Counter
WriteCounter stats.Counter
}
func (c *CounterConnection) Read(b []byte) (int, error) {
nBytes, err := c.Connection.Read(b)
if c.ReadCounter != nil {
c.ReadCounter.Add(int64(nBytes))
}
return nBytes, err
}
func (c *CounterConnection) Write(b []byte) (int, error) {
nBytes, err := c.Connection.Write(b)
if c.WriteCounter != nil {
c.WriteCounter.Add(int64(nBytes))
}
return nBytes, err
}

View File

@@ -0,0 +1,13 @@
package stats
// Counter is the interface for stats counters.
//
// xray:api:stable
type Counter interface {
// Value is the current value of the counter.
Value() int64
// Set sets a new value to the counter, and returns the previous one.
Set(int64) int64
// Add adds a value to the current counter value, and returns the previous value.
Add(int64) int64
}

View File

@@ -0,0 +1,10 @@
package task
import "github.com/sagernet/sing-box/common/xray"
// Close returns a func() that closes v.
func Close(v interface{}) func() error {
return func() error {
return common.Close(v)
}
}

View File

@@ -0,0 +1,85 @@
package task
import (
"sync"
"time"
)
// Periodic is a task that runs periodically.
type Periodic struct {
// Interval of the task being run
Interval time.Duration
// Execute is the task function
Execute func() error
access sync.Mutex
timer *time.Timer
running bool
}
func (t *Periodic) hasClosed() bool {
t.access.Lock()
defer t.access.Unlock()
return !t.running
}
func (t *Periodic) checkedExecute() error {
if t.hasClosed() {
return nil
}
if err := t.Execute(); err != nil {
t.access.Lock()
t.running = false
t.access.Unlock()
return err
}
t.access.Lock()
defer t.access.Unlock()
if !t.running {
return nil
}
t.timer = time.AfterFunc(t.Interval, func() {
t.checkedExecute()
})
return nil
}
// Start implements common.Runnable.
func (t *Periodic) Start() error {
t.access.Lock()
if t.running {
t.access.Unlock()
return nil
}
t.running = true
t.access.Unlock()
if err := t.checkedExecute(); err != nil {
t.access.Lock()
t.running = false
t.access.Unlock()
return err
}
return nil
}
// Close implements common.Closable.
func (t *Periodic) Close() error {
t.access.Lock()
defer t.access.Unlock()
t.running = false
if t.timer != nil {
t.timer.Stop()
t.timer = nil
}
return nil
}

64
common/xray/task/task.go Normal file
View File

@@ -0,0 +1,64 @@
package task
import (
"context"
"github.com/sagernet/sing-box/common/xray/signal/semaphore"
)
// OnSuccess executes g() after f() returns nil.
func OnSuccess(f func() error, g func() error) func() error {
return func() error {
if err := f(); err != nil {
return err
}
return g()
}
}
// Run executes a list of tasks in parallel, returns the first error encountered or nil if all tasks pass.
func Run(ctx context.Context, tasks ...func() error) error {
n := len(tasks)
s := semaphore.New(n)
done := make(chan error, 1)
for _, task := range tasks {
<-s.Wait()
go func(f func() error) {
err := f()
if err == nil {
s.Signal()
return
}
select {
case done <- err:
default:
}
}(task)
}
/*
if altctx := ctx.Value("altctx"); altctx != nil {
ctx = altctx.(context.Context)
}
*/
for i := 0; i < n; i++ {
select {
case err := <-done:
return err
case <-ctx.Done():
return ctx.Err()
case <-s.Wait():
}
}
/*
if cancel := ctx.Value("cancel"); cancel != nil {
cancel.(context.CancelFunc)()
}
*/
return nil
}

101
common/xray/uuid/uuid.go Normal file
View File

@@ -0,0 +1,101 @@
package uuid
import (
"bytes"
"crypto/rand"
"crypto/sha1"
"encoding/hex"
"github.com/sagernet/sing-box/common/xray"
E "github.com/sagernet/sing/common/exceptions"
)
var byteGroups = []int{8, 4, 4, 4, 12}
type UUID [16]byte
// String returns the string representation of this UUID.
func (u *UUID) String() string {
bytes := u.Bytes()
result := hex.EncodeToString(bytes[0 : byteGroups[0]/2])
start := byteGroups[0] / 2
for i := 1; i < len(byteGroups); i++ {
nBytes := byteGroups[i] / 2
result += "-"
result += hex.EncodeToString(bytes[start : start+nBytes])
start += nBytes
}
return result
}
// Bytes returns the bytes representation of this UUID.
func (u *UUID) Bytes() []byte {
return u[:]
}
// Equals returns true if this UUID equals another UUID by value.
func (u *UUID) Equals(another *UUID) bool {
if u == nil && another == nil {
return true
}
if u == nil || another == nil {
return false
}
return bytes.Equal(u.Bytes(), another.Bytes())
}
// New creates a UUID with random value.
func New() UUID {
var uuid UUID
common.Must2(rand.Read(uuid.Bytes()))
uuid[6] = (uuid[6] & 0x0f) | (4 << 4)
uuid[8] = (uuid[8]&(0xff>>2) | (0x02 << 6))
return uuid
}
// ParseBytes converts a UUID in byte form to object.
func ParseBytes(b []byte) (UUID, error) {
var uuid UUID
if len(b) != 16 {
return uuid, E.New("invalid UUID: ", b)
}
copy(uuid[:], b)
return uuid, nil
}
// ParseString converts a UUID in string form to object.
func ParseString(str string) (UUID, error) {
var uuid UUID
text := []byte(str)
if l := len(text); l < 32 || l > 36 {
if l == 0 || l > 30 {
return uuid, E.New("invalid UUID: ", str)
}
h := sha1.New()
h.Write(uuid[:])
h.Write(text)
u := h.Sum(nil)[:16]
u[6] = (u[6] & 0x0f) | (5 << 4)
u[8] = (u[8]&(0xff>>2) | (0x02 << 6))
copy(uuid[:], u)
return uuid, nil
}
b := uuid.Bytes()
for _, byteGroup := range byteGroups {
if text[0] == '-' {
text = text[1:]
}
if _, err := hex.Decode(b[:byteGroup/2], text[:byteGroup]); err != nil {
return uuid, err
}
text = text[byteGroup:]
b = b[byteGroup/2:]
}
return uuid, nil
}

View File

@@ -28,6 +28,7 @@ const (
DNSTypeFakeIP = "fakeip"
DNSTypeDHCP = "dhcp"
DNSTypeTailscale = "tailscale"
DNSTypeSDNS = "sdns"
)
const (

View File

@@ -15,15 +15,19 @@ const (
TypeTrojan = "trojan"
TypeNaive = "naive"
TypeWireGuard = "wireguard"
TypeWARP = "warp"
TypeHysteria = "hysteria"
TypeTor = "tor"
TypeSSH = "ssh"
TypeShadowTLS = "shadowtls"
TypeMieru = "mieru"
TypeAnyTLS = "anytls"
TypeShadowsocksR = "shadowsocksr"
TypeVLESS = "vless"
TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2"
TypeTunnelClient = "tunnel_client"
TypeTunnelServer = "tunnel_server"
TypeTailscale = "tailscale"
TypeDERP = "derp"
TypeResolved = "resolved"
@@ -65,6 +69,8 @@ func ProxyDisplayName(proxyType string) string {
return "Naive"
case TypeWireGuard:
return "WireGuard"
case TypeWARP:
return "WARP"
case TypeHysteria:
return "Hysteria"
case TypeTor:
@@ -81,12 +87,18 @@ func ProxyDisplayName(proxyType string) string {
return "TUIC"
case TypeHysteria2:
return "Hysteria2"
case TypeMieru:
return "Mieru"
case TypeAnyTLS:
return "AnyTLS"
case TypeSelector:
return "Selector"
case TypeURLTest:
return "URLTest"
case TypeTunnelClient:
return "Tunnel Client"
case TypeTunnelServer:
return "Tunnel Server"
default:
return "Unknown"
}

View File

@@ -6,4 +6,5 @@ const (
V2RayTransportTypeQUIC = "quic"
V2RayTransportTypeGRPC = "grpc"
V2RayTransportTypeHTTPUpgrade = "httpupgrade"
V2RayTransportTypeXHTTP = "xhttp"
)

20
constant/warp.go Normal file
View File

@@ -0,0 +1,20 @@
package constant
type WARPConfig struct {
PrivateKey string `json:"private_key"`
Interface struct {
Addresses struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
} `json:"addresses"`
} `json:"interface"`
Peers []struct {
PublicKey string `json:"public_key"`
Endpoint struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
Host string `json:"host"`
Ports []int `json:"ports"`
} `json:"endpoint"`
} `json:"peers"`
}

View File

@@ -34,6 +34,7 @@ type Client struct {
disableCache bool
disableExpire bool
independentCache bool
clientSubnet netip.Prefix
rdrc adapter.RDRCStore
initRDRCFunc func() adapter.RDRCStore
logger logger.ContextLogger
@@ -47,6 +48,7 @@ type ClientOptions struct {
DisableExpire bool
IndependentCache bool
CacheCapacity uint32
ClientSubnet netip.Prefix
RDRC func() adapter.RDRCStore
Logger logger.ContextLogger
}
@@ -57,6 +59,7 @@ func NewClient(options ClientOptions) *Client {
disableCache: options.DisableCache,
disableExpire: options.DisableExpire,
independentCache: options.IndependentCache,
clientSubnet: options.ClientSubnet,
initRDRCFunc: options.RDRC,
logger: options.Logger,
}
@@ -104,8 +107,12 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return &responseMessage, nil
}
question := message.Question[0]
if options.ClientSubnet.IsValid() {
message = SetClientSubnet(message, options.ClientSubnet)
clientSubnet := options.ClientSubnet
if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet
}
if clientSubnet.IsValid() {
message = SetClientSubnet(message, clientSubnet)
}
isSimpleRequest := len(message.Question) == 1 &&
len(message.Ns) == 0 &&
@@ -188,8 +195,13 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
}
}*/
if responseChecker != nil {
addr, addrErr := MessageToAddresses(response)
if addrErr != nil || !responseChecker(addr) {
var rejected bool
if !(response.Rcode == dns.RcodeSuccess || response.Rcode == dns.RcodeNameError) {
rejected = true
} else {
rejected = !responseChecker(MessageToAddresses(response))
}
if rejected {
if c.rdrc != nil {
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
}
@@ -413,7 +425,10 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran
if err != nil {
return nil, err
}
return MessageToAddresses(response)
if response.Rcode != dns.RcodeSuccess {
return nil, RcodeError(response.Rcode)
}
return MessageToAddresses(response), nil
}
func (c *Client) questionCache(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) {
@@ -421,7 +436,10 @@ func (c *Client) questionCache(question dns.Question, transport adapter.DNSTrans
if response == nil {
return nil, ErrNotCached
}
return MessageToAddresses(response)
if response.Rcode != dns.RcodeSuccess {
return nil, RcodeError(response.Rcode)
}
return MessageToAddresses(response), nil
}
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int) {
@@ -498,10 +516,7 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
}
}
func MessageToAddresses(response *dns.Msg) ([]netip.Addr, error) {
if response.Rcode != dns.RcodeSuccess {
return nil, RcodeError(response.Rcode)
}
func MessageToAddresses(response *dns.Msg) []netip.Addr {
addresses := make([]netip.Addr, 0, len(response.Answer))
for _, rawAnswer := range response.Answer {
switch answer := rawAnswer.(type) {
@@ -517,7 +532,7 @@ func MessageToAddresses(response *dns.Msg) ([]netip.Addr, error) {
}
}
}
return addresses, nil
return addresses
}
func wrapError(err error) error {

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ package local
import (
"context"
"math/rand"
"net/netip"
"time"
"github.com/sagernet/sing-box/adapter"
@@ -91,9 +90,9 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
startRacer := func(ctx context.Context, fqdn string) {
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
if err == nil {
var addresses []netip.Addr
addresses, err = dns.MessageToAddresses(response)
if err == nil && len(addresses) == 0 {
if response.Rcode != mDNS.RcodeSuccess {
err = dns.RcodeError(response.Rcode)
} else if len(dns.MessageToAddresses(response)) == 0 {
err = E.New(fqdn, ": empty result")
}
}

View File

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

View File

@@ -0,0 +1,13 @@
package local
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestDNSReadConfig(t *testing.T) {
t.Parallel()
require.NoError(t, dnsReadConfig(context.Background(), "/etc/resolv.conf").err)
}

71
dns/transport/sdns.go Normal file
View File

@@ -0,0 +1,71 @@
package transport
import (
"context"
"net"
"sync"
"time"
"github.com/ameshkov/dnscrypt/v2"
mDNS "github.com/miekg/dns"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
)
var _ adapter.DNSTransport = (*SDNSTransport)(nil)
func RegisterSDNS(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.SDNSDNSServerOptions](registry, C.DNSTypeSDNS, NewSDNSTransport)
}
type SDNSTransport struct {
dns.TransportAdapter
client *dnscrypt.Client
name string
stamp string
mtx sync.Mutex
}
func NewSDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.SDNSDNSServerOptions) (adapter.DNSTransport, error) {
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
if err != nil {
return nil, err
}
return &SDNSTransport{
client: &dnscrypt.Client{
Net: "udp",
Timeout: 10 * time.Second,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return transportDialer.DialContext(ctx, N.NetworkName(network), M.ParseSocksaddr(addr))
},
},
stamp: options.Stamp,
}, err
}
func (t *SDNSTransport) Name() string {
return t.name
}
func (t *SDNSTransport) Start(adapter.StartStage) error {
return nil
}
func (t *SDNSTransport) Close() error {
return nil
}
func (t *SDNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
resolverInfo, err := t.client.Dial(t.stamp)
if err != nil {
return nil, err
}
return t.client.Exchange(message, resolverInfo)
}

View File

@@ -60,7 +60,7 @@ func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer
logger: logger,
dialer: dialer,
serverAddr: serverAddr,
udpSize: 512,
udpSize: 2048,
tcpTransport: &TCPTransport{
dialer: dialer,
serverAddr: serverAddr,
@@ -97,15 +97,19 @@ func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
}
func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
conn, err := t.open(ctx)
if err != nil {
return nil, err
}
t.access.Lock()
if edns0Opt := message.IsEdns0(); edns0Opt != nil {
if udpSize := int(edns0Opt.UDPSize()); udpSize > t.udpSize {
t.udpSize = udpSize
close(t.done)
t.done = make(chan struct{})
}
}
t.access.Unlock()
conn, err := t.open(ctx)
if err != nil {
return nil, err
}
buffer := buf.NewSize(1 + message.Len())
defer buffer.Release()
exMessage := *message

View File

@@ -2,10 +2,234 @@
icon: material/alert-decagram
---
#### 1.12.0-beta.18
#### 1.12.1
* Fixes and improvements
#### 1.12.0
* Refactor DNS servers **1**
* Add domain resolver options**2**
* Add TLS fragment/record fragment support to route options and outbound TLS options **3**
* Add certificate options **4**
* Add Tailscale endpoint and DNS server **5**
* Drop support for go1.22 **6**
* Add AnyTLS protocol **7**
* Migrate to stdlib ECH implementation **8**
* Add NTP sniffer **9**
* Add wildcard SNI support for ShadowTLS inbound **10**
* Improve `auto_redirect` **11**
* Add control options for listeners **12**
* Add DERP service **13**
* Add Resolved service and DNS server **14**
* Add SSM API service **15**
* Add loopback address support for tun **16**
* Improve tun performance on Apple platforms **17**
* Update quic-go to v0.52.0
* Update gVisor to 20250319.0
* Update the status of graphical clients in stores **18**
**1**:
DNS servers are refactored for better performance and scalability.
See [DNS server](/configuration/dns/server/).
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-servers).
Compatibility for old formats will be removed in sing-box 1.14.0.
**2**:
Legacy `outbound` DNS rules are deprecated
and can be replaced by the new `domain_resolver` option.
See [Dial Fields](/configuration/shared/dial/#domain_resolver) and
[Route](/configuration/route/#default_domain_resolver).
For migration,
see [Migrate outbound DNS rule items to domain resolver](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver).
**3**:
See [Route Action](/configuration/route/rule_action/#tls_fragment) and [TLS](/configuration/shared/tls/).
**4**:
New certificate options allow you to manage the default list of trusted X509 CA certificates.
For the system certificate list, fixed Go not reading Android trusted certificates correctly.
You can also use the Mozilla Included List instead, or add trusted certificates yourself.
See [Certificate](/configuration/certificate/).
**5**:
See [Tailscale](/configuration/endpoint/tailscale/).
**6**:
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go).
**7**:
The new AnyTLS protocol claims to mitigate TLS proxy traffic characteristics and comes with a new multiplexing scheme.
See [AnyTLS Inbound](/configuration/inbound/anytls/) and [AnyTLS Outbound](/configuration/outbound/anytls/).
**8**:
See [TLS](/configuration/shared/tls).
The build tag `with_ech` is no longer needed and has been removed.
**9**:
See [Protocol Sniff](/configuration/route/sniff/).
**10**:
See [ShadowTLS](/configuration/inbound/shadowtls/#wildcard_sni).
**11**:
Now `auto_redirect` fixes compatibility issues between tun and Docker bridge networks,
see [Tun](/configuration/inbound/tun/#auto_redirect).
**12**:
You can now set `bind_interface`, `routing_mark` and `reuse_addr` in Listen Fields.
See [Listen Fields](/configuration/shared/listen/).
**13**:
DERP service is a Tailscale DERP server, similar to [derper](https://pkg.go.dev/tailscale.com/cmd/derper).
See [DERP Service](/configuration/service/derp/).
**14**:
Resolved service is a fake systemd-resolved DBUS service to receive DNS settings from other programs
(e.g. NetworkManager) and provide DNS resolution.
See [Resolved Service](/configuration/service/resolved/) and [Resolved DNS Server](/configuration/dns/server/resolved/).
**15**:
SSM API service is a RESTful API server for managing Shadowsocks servers.
See [SSM API Service](/configuration/service/ssm-api/).
**16**:
TUN now implements SideStore's StosVPN.
See [Tun](/configuration/inbound/tun/#loopback_address).
**17**:
We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack.
The following data was tested using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/internal/tun_bench/main.go) on M4 MacBook pro.
| Version | Stack | MTU | Upload | Download |
|-------------|--------|-------|--------|----------|
| 1.11.15 | gvisor | 1500 | 852M | 2.57G |
| 1.12.0-rc.4 | gvisor | 1500 | 2.90G | 4.68G |
| 1.11.15 | gvisor | 4064 | 2.31G | 6.34G |
| 1.12.0-rc.4 | gvisor | 4064 | 7.54G | 12.2G |
| 1.11.15 | gvisor | 65535 | 27.6G | 18.1G |
| 1.12.0-rc.4 | gvisor | 65535 | 39.8G | 34.7G |
| 1.11.15 | system | 1500 | 664M | 706M |
| 1.12.0-rc.4 | system | 1500 | 2.44G | 2.51G |
| 1.11.15 | system | 4064 | 1.88G | 1.94G |
| 1.12.0-rc.4 | system | 4064 | 6.45G | 6.27G |
| 1.11.15 | system | 65535 | 26.2G | 17.4G |
| 1.12.0-rc.4 | system | 65535 | 17.6G | 21.0G |
**18**:
We continue to experience issues updating our sing-box apps on the App Store and Play Store.
Until we rewrite and resubmit the apps, they are considered irrecoverable.
Therefore, after this release, we will not be repeating this notice unless there is new information.
### 1.11.15
* Fixes and improvements
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we
violated the rules (TestFlight users are not affected)._
#### 1.12.0-beta.32
* Improve tun performance on Apple platforms **1**
* Fixes and improvements
**1**:
We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack.
### 1.11.14
* Fixes and improvements
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we
violated the rules (TestFlight users are not affected)._
#### 1.12.0-beta.24
* Allow `tls_fragment` and `tls_record_fragment` to be enabled together **1**
* Also add fragment options for TLS client configuration **2**
* Fixes and improvements
**1**:
For debugging only, it is recommended to disable if record fragmentation works.
See [Route Action](/configuration/route/rule_action/#tls_fragment).
**2**:
See [TLS](/configuration/shared/tls/).
#### 1.12.0-beta.23
* Add loopback address support for tun **1**
* Add cache support for ssm-api **2**
* Fixes and improvements
**1**:
TUN now implements SideStore's StosVPN.
See [Tun](/configuration/inbound/tun/#loopback_address).
**2**:
See [SSM API Service](/configuration/service/ssm-api/#cache_path).
#### 1.12.0-beta.21
* Fix missing `home` option for DERP service **1**
* Fixes and improvements
**1**:
You can now choose what the DERP home page shows, just like with derper's `-home` flag.
See [DERP](/configuration/service/derp/#home).
### 1.11.13
* Fixes and improvements
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we
violated the rules (TestFlight users are not affected)._
#### 1.12.0-beta.17
* Update quic-go to v0.52.0

View File

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

View File

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

View File

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

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