mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-13 02:27:19 +10:00
Compare commits
484 Commits
dev-ts
...
copilot/im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfb4ef2deb | ||
|
|
9ec1549a23 | ||
|
|
76d447d7d9 | ||
|
|
ab3caabde1 | ||
|
|
1338c1e1a9 | ||
|
|
822e140eed | ||
|
|
c726237522 | ||
|
|
466925f636 | ||
|
|
517e1503e9 | ||
|
|
b5f62c1b88 | ||
|
|
6bdfa2d99e | ||
|
|
1662802db8 | ||
|
|
c09a10074b | ||
|
|
269e4a40f7 | ||
|
|
6a1640fe71 | ||
|
|
8cf6842a86 | ||
|
|
2e6af5185c | ||
|
|
9c36b00526 | ||
|
|
9d0510f384 | ||
|
|
6a0ceb7839 | ||
|
|
13ed7fce55 | ||
|
|
788fd491d4 | ||
|
|
c651b9b14c | ||
|
|
07a7530df0 | ||
|
|
362edd2731 | ||
|
|
42e842a7da | ||
|
|
63121f18c0 | ||
|
|
b13643bd42 | ||
|
|
9078f20860 | ||
|
|
a31ecd79c3 | ||
|
|
4f4d18bfaf | ||
|
|
af2ea8f8b9 | ||
|
|
5452980523 | ||
|
|
1f421c04ac | ||
|
|
159610556d | ||
|
|
16de8b394f | ||
|
|
52a0134af0 | ||
|
|
d1bee96553 | ||
|
|
0572e6c436 | ||
|
|
7c1ad09c4c | ||
|
|
89b8212ebb | ||
|
|
fb765bc0da | ||
|
|
83cb784de4 | ||
|
|
73df4a7665 | ||
|
|
044057168c | ||
|
|
4407105f11 | ||
|
|
fdeea12514 | ||
|
|
2747a00ba2 | ||
|
|
48e76038d0 | ||
|
|
6421252d44 | ||
|
|
216c4c8bd4 | ||
|
|
5841d410a1 | ||
|
|
63c8207d7a | ||
|
|
54ed58499d | ||
|
|
b1bdc18c85 | ||
|
|
a38030cc0b | ||
|
|
4626aa2cb0 | ||
|
|
5a40b673a4 | ||
|
|
541f63fee4 | ||
|
|
5de6f4a14f | ||
|
|
5658830077 | ||
|
|
0e50edc009 | ||
|
|
444f454810 | ||
|
|
d0e1fd6c7e | ||
|
|
17b4d1e010 | ||
|
|
06791470c9 | ||
|
|
ef14c8ca0e | ||
|
|
36dc883c7c | ||
|
|
6557bd7029 | ||
|
|
41b30c91d9 | ||
|
|
0f767d5ce1 | ||
|
|
328a6de797 | ||
|
|
886be6414d | ||
|
|
9362d3cab3 | ||
|
|
ced2e39dbf | ||
|
|
2159d8877b | ||
|
|
cb7dba3eff | ||
|
|
d9d7f7880d | ||
|
|
a031aaf2c0 | ||
|
|
4bca951773 | ||
|
|
140735dbde | ||
|
|
714a68bba1 | ||
|
|
573c6179ab | ||
|
|
510bf05e36 | ||
|
|
ae852e0be4 | ||
|
|
1955002ed8 | ||
|
|
44559fb7b9 | ||
|
|
0977c5cf73 | ||
|
|
07697bf931 | ||
|
|
5d1d1a1456 | ||
|
|
146383499e | ||
|
|
e81a76fdf9 | ||
|
|
de13137418 | ||
|
|
e42b818c2a | ||
|
|
fcde0c94e0 | ||
|
|
1af83e997d | ||
|
|
59ee7be72a | ||
|
|
c331ee3d5c | ||
|
|
36babe4bef | ||
|
|
c5f2cea802 | ||
|
|
8a200bf913 | ||
|
|
f16468e74f | ||
|
|
79c0b9f51d | ||
|
|
f98a3a4f65 | ||
|
|
b14cecaeb2 | ||
|
|
2594745ef8 | ||
|
|
cc3041322e | ||
|
|
f352f84483 | ||
|
|
cbf48e9b8c | ||
|
|
0ef7e8eca2 | ||
|
|
1a18e43a88 | ||
|
|
6849288d6d | ||
|
|
2edfed7d91 | ||
|
|
30c069f5b7 | ||
|
|
649163cb7b | ||
|
|
980e96250b | ||
|
|
963bc4b647 | ||
|
|
031f25c1c1 | ||
|
|
b40f642fa4 | ||
|
|
22782ca6fc | ||
|
|
1468d83895 | ||
|
|
97f0dc8a60 | ||
|
|
ee02532ab5 | ||
|
|
f1dd0dba78 | ||
|
|
f4ed684146 | ||
|
|
83f02d0bfb | ||
|
|
52fa5f20a3 | ||
|
|
f462ce5615 | ||
|
|
cef3e538ba | ||
|
|
acda4ce985 | ||
|
|
354ece2bdf | ||
|
|
de10bb00a9 | ||
|
|
fdc181106d | ||
|
|
8752b631bd | ||
|
|
378e39f70c | ||
|
|
043a2e7a07 | ||
|
|
7e190e92ca | ||
|
|
5eb318ba06 | ||
|
|
4a209f1afb | ||
|
|
c0ac3c748c | ||
|
|
a65d3e040a | ||
|
|
2358efe44a | ||
|
|
09d3b8f2c2 | ||
|
|
531de77124 | ||
|
|
44981fd803 | ||
|
|
4fb5ac292b | ||
|
|
0e23a3d7c2 | ||
|
|
76ee64ae50 | ||
|
|
e1dbcccab5 | ||
|
|
fba802effd | ||
|
|
9495b56772 | ||
|
|
a8434b176f | ||
|
|
ef0004400d | ||
|
|
0a63049845 | ||
|
|
2dcb86941f | ||
|
|
5c6eb89cfb | ||
|
|
5b92eeb3bf | ||
|
|
3518ce083b | ||
|
|
f13c54afc1 | ||
|
|
3388efe65a | ||
|
|
a11384b286 | ||
|
|
9dd9fb27cd | ||
|
|
0f2035149c | ||
|
|
cba364204a | ||
|
|
4e17788549 | ||
|
|
18a6719893 | ||
|
|
687343f6ca | ||
|
|
e061538c30 | ||
|
|
a6375c7530 | ||
|
|
45fa18a2e3 | ||
|
|
534cccce91 | ||
|
|
72dbcd3ad4 | ||
|
|
5533094984 | ||
|
|
ae2ecd6002 | ||
|
|
0098a2adc5 | ||
|
|
c0dd4a3f07 | ||
|
|
497ddb5829 | ||
|
|
811ff93549 | ||
|
|
96df69bcdc | ||
|
|
6cfa2b8b86 | ||
|
|
eea1e701b7 | ||
|
|
455e5de74d | ||
|
|
9533031891 | ||
|
|
80f8ea6849 | ||
|
|
50eadb00c7 | ||
|
|
d4012bd0b2 | ||
|
|
a902e9f9f6 | ||
|
|
da3ba573d8 | ||
|
|
bea9048cfe | ||
|
|
fc0f5ed83a | ||
|
|
c0588c30d7 | ||
|
|
24c940c51c | ||
|
|
407ee08d8a | ||
|
|
756585fb2a | ||
|
|
5662784afb | ||
|
|
3801901726 | ||
|
|
7d58174f1f | ||
|
|
d339f85087 | ||
|
|
b6a114f7f4 | ||
|
|
e586ef070e | ||
|
|
71a76e9ecb | ||
|
|
1d66474022 | ||
|
|
3934e53476 | ||
|
|
0146fbfc40 | ||
|
|
6ee3117755 | ||
|
|
e2440a569e | ||
|
|
7a1eee78df | ||
|
|
e3c8c0705f | ||
|
|
886d427337 | ||
|
|
d5432b4c27 | ||
|
|
42064fe7ec | ||
|
|
7cee76f9a6 | ||
|
|
ed5b2f2997 | ||
|
|
3b480de38a | ||
|
|
f990630ccc | ||
|
|
d33614d6a0 | ||
|
|
b3866bcea0 | ||
|
|
26ec73c71b | ||
|
|
c3403c5413 | ||
|
|
3b6ddcae37 | ||
|
|
dbdcce20a8 | ||
|
|
e7ef1b2368 | ||
|
|
ce32d1c2c3 | ||
|
|
596b66f397 | ||
|
|
d4fd43cf6f | ||
|
|
6c377f16e7 | ||
|
|
349db7baec | ||
|
|
1f3097da00 | ||
|
|
0b4b5e6f0f | ||
|
|
245273e6c1 | ||
|
|
54a0004de6 | ||
|
|
6a211f6ed6 | ||
|
|
aadb44ebd6 | ||
|
|
9b0db6ab15 | ||
|
|
5b363c347f | ||
|
|
cdea3f63d4 | ||
|
|
40a6260f6e | ||
|
|
a5e47f4e0f | ||
|
|
ac7bc587cb | ||
|
|
4e11a3585a | ||
|
|
63d3e9f6e5 | ||
|
|
d115e36ed8 | ||
|
|
af56b1a950 | ||
|
|
f9999a76fe | ||
|
|
42eb3841a1 | ||
|
|
fb622ccbdf | ||
|
|
d2dc3ddf72 | ||
|
|
e8499452f8 | ||
|
|
e0a6b31c03 | ||
|
|
7c923209ad | ||
|
|
bca2bd2fa1 | ||
|
|
fa99ca2757 | ||
|
|
7073f2a272 | ||
|
|
390e30ae7b | ||
|
|
23cf8c49e0 | ||
|
|
b17a024f6c | ||
|
|
1ed21085bb | ||
|
|
56409ff269 | ||
|
|
0c523980ff | ||
|
|
32873d06bc | ||
|
|
4accaccf77 | ||
|
|
ff416aacaf | ||
|
|
b97947e8ac | ||
|
|
dfcd9fb8c3 | ||
|
|
803811568e | ||
|
|
50b0bd5c39 | ||
|
|
2d02b2b1cf | ||
|
|
456fbecf16 | ||
|
|
668923c392 | ||
|
|
c51e9cbe06 | ||
|
|
60b451e6cf | ||
|
|
3e35390d8f | ||
|
|
f2dad289fb | ||
|
|
b4a8fa59f5 | ||
|
|
73de2a7d07 | ||
|
|
1699a7ce33 | ||
|
|
7743c6e881 | ||
|
|
9a5f69f435 | ||
|
|
5c4211e849 | ||
|
|
c1189e2a7b | ||
|
|
f18889369f | ||
|
|
91c7b638e8 | ||
|
|
6f793a0273 | ||
|
|
0f6c417c3c | ||
|
|
c830e9a634 | ||
|
|
e809623ec9 | ||
|
|
061276902b | ||
|
|
fa6f7d396e | ||
|
|
23666a9230 | ||
|
|
17576e9f66 | ||
|
|
90ec9c8bcb | ||
|
|
988ac62a1b | ||
|
|
3016338e34 | ||
|
|
bc35aca017 | ||
|
|
281d52a1ea | ||
|
|
b8502759b5 | ||
|
|
6f804adf39 | ||
|
|
36db31c55a | ||
|
|
4dbbf59c82 | ||
|
|
832eb4458d | ||
|
|
2cf989d306 | ||
|
|
7d3ee29bd0 | ||
|
|
cba0e46aba | ||
|
|
9b8ab3e61e | ||
|
|
47f18e823a | ||
|
|
2d1b824b62 | ||
|
|
d511698f3f | ||
|
|
cb435ea232 | ||
|
|
43a9016c83 | ||
|
|
255068fd40 | ||
|
|
098a00b025 | ||
|
|
dba0b5276b | ||
|
|
78ae935468 | ||
|
|
3ea5f76470 | ||
|
|
b4d294c05e | ||
|
|
83cf5f5c6a | ||
|
|
e7b3a8eebe | ||
|
|
ee3a42a67e | ||
|
|
50227c0f5f | ||
|
|
bc5eb1e1a5 | ||
|
|
995267a042 | ||
|
|
41226a6075 | ||
|
|
81d32181ce | ||
|
|
c5ecca3938 | ||
|
|
900888731c | ||
|
|
13e648e4b1 | ||
|
|
aff12ff671 | ||
|
|
101fb88255 | ||
|
|
8b489354e4 | ||
|
|
7dea6eb7a6 | ||
|
|
af1bfe4e3e | ||
|
|
d574e9eb52 | ||
|
|
2d7df1e1f2 | ||
|
|
1c0ffcf5b1 | ||
|
|
348cc39975 | ||
|
|
987899f94a | ||
|
|
d8b2d5142f | ||
|
|
134802d1ee | ||
|
|
e5e81b4de1 | ||
|
|
300c961efa | ||
|
|
7c7f512405 | ||
|
|
03e8d029c2 | ||
|
|
787b5f1931 | ||
|
|
56a7624618 | ||
|
|
3a84acf122 | ||
|
|
f600e02e47 | ||
|
|
e6d19de58a | ||
|
|
f2bbf6b2aa | ||
|
|
c54d50fd36 | ||
|
|
6a051054db | ||
|
|
49498f6439 | ||
|
|
144a890c71 | ||
|
|
afb4993445 | ||
|
|
4c9455b944 | ||
|
|
5fdc051a08 | ||
|
|
cb68a40c43 | ||
|
|
023218e6e7 | ||
|
|
2a24b94b8d | ||
|
|
c6531cf184 | ||
|
|
d4fa0ed349 | ||
|
|
10874d2dc4 | ||
|
|
5adaf1ac75 | ||
|
|
9668ea69b8 | ||
|
|
ae9bc7acf1 | ||
|
|
594ee480a2 | ||
|
|
a15b5a2463 | ||
|
|
991e755789 | ||
|
|
97d41ffde8 | ||
|
|
24af0766ac | ||
|
|
af17eaa537 | ||
|
|
3adc10a797 | ||
|
|
5eeef6b28e | ||
|
|
f4c29840c3 | ||
|
|
47fc3ebda4 | ||
|
|
9774a659b0 | ||
|
|
2e4a6de4e7 | ||
|
|
a530e424e9 | ||
|
|
0bfd487ee9 | ||
|
|
6aae834493 | ||
|
|
f56131f38e | ||
|
|
273a11d550 | ||
|
|
ae8ce75e41 | ||
|
|
d6d94b689f | ||
|
|
30d785f1ee | ||
|
|
db5ec3cdfc | ||
|
|
9aca54d039 | ||
|
|
d55d5009c2 | ||
|
|
4f3ee61104 | ||
|
|
96eb98c00a | ||
|
|
68ce9577c6 | ||
|
|
3ae036e997 | ||
|
|
5da2d1d470 | ||
|
|
8e2baf40f1 | ||
|
|
c24c40dfee | ||
|
|
32e52ce1ed | ||
|
|
ed46438359 | ||
|
|
0b5490d5a3 | ||
|
|
2d73ef511d | ||
|
|
63e6c85f6f | ||
|
|
8946a6d2d0 | ||
|
|
d3132645fb | ||
|
|
373f158fe0 | ||
|
|
ce36835fab | ||
|
|
619fa671d7 | ||
|
|
eb07c7a79e | ||
|
|
7eb3535094 | ||
|
|
93b68312cf | ||
|
|
97ce666e43 | ||
|
|
4000e1e66d | ||
|
|
270740e859 | ||
|
|
6cad142cfe | ||
|
|
093013687c | ||
|
|
ff31c469a0 | ||
|
|
fbe390268c | ||
|
|
07ac01dcb7 | ||
|
|
badfdb62cd | ||
|
|
986a410b30 | ||
|
|
9db2d58545 | ||
|
|
4eed46ac59 | ||
|
|
abc38d1dab | ||
|
|
8d6c4f1289 | ||
|
|
a2d40eb8b8 | ||
|
|
17b502bb4b | ||
|
|
a0d4421085 | ||
|
|
0d443072d1 | ||
|
|
c9fb99b799 | ||
|
|
92d245ad04 | ||
|
|
0908627297 | ||
|
|
7f79458b4f | ||
|
|
9b4c11ba95 | ||
|
|
27c31eac5d | ||
|
|
bab8dc0b82 | ||
|
|
d09d2fb665 | ||
|
|
e64cf3b7df | ||
|
|
9b73222314 | ||
|
|
3923b57abf | ||
|
|
4807e64609 | ||
|
|
eeb37d89f1 | ||
|
|
08c1ec4b7e | ||
|
|
6b4cf67add | ||
|
|
e65926fd08 | ||
|
|
f2ec319fe1 | ||
|
|
32377a61b7 | ||
|
|
7aac801ccd | ||
|
|
96fdf59ee4 | ||
|
|
50b8f3ab94 | ||
|
|
ff7aaf977b | ||
|
|
9a1efbe54d | ||
|
|
906c21f458 | ||
|
|
d5e7af7a7e | ||
|
|
4d41f03bd5 | ||
|
|
30704a15a7 | ||
|
|
83889178ed | ||
|
|
1d2720bf5e | ||
|
|
c4b6d0eadb | ||
|
|
0c66888691 | ||
|
|
68781387fe | ||
|
|
fd299a0961 | ||
|
|
285a82050c | ||
|
|
2dbb8c55c9 | ||
|
|
effcf39469 | ||
|
|
9db9484863 | ||
|
|
ca813f461b | ||
|
|
bb46cdb2b3 | ||
|
|
dcb10c21a1 | ||
|
|
05ea0ca00e | ||
|
|
c098f282b1 | ||
|
|
ecf82d197c | ||
|
|
9afe75586a | ||
|
|
a1be455202 | ||
|
|
19fb214226 | ||
|
|
28ec898a8c | ||
|
|
467b1bbeeb | ||
|
|
02ab8ce806 | ||
|
|
ce69e620e9 | ||
|
|
1133cf3ef5 | ||
|
|
59a607e303 | ||
|
|
313be3d7a4 | ||
|
|
4fe40fcee0 | ||
|
|
e233fd4fe5 | ||
|
|
9f7683818f | ||
|
|
179e3cb2f5 | ||
|
|
41b960552d | ||
|
|
8304295c48 |
30
.fpm_openwrt
Normal file
30
.fpm_openwrt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
-s dir
|
||||||
|
--name sing-box
|
||||||
|
--category net
|
||||||
|
--license GPL-3.0-or-later
|
||||||
|
--description "The universal proxy platform."
|
||||||
|
--url "https://sing-box.sagernet.org/"
|
||||||
|
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||||
|
--no-deb-generate-changes
|
||||||
|
|
||||||
|
--config-files /etc/config/sing-box
|
||||||
|
--config-files /etc/sing-box/config.json
|
||||||
|
|
||||||
|
--depends ca-bundle
|
||||||
|
--depends kmod-inet-diag
|
||||||
|
--depends kmod-tun
|
||||||
|
--depends firewall4
|
||||||
|
|
||||||
|
--before-remove release/config/openwrt.prerm
|
||||||
|
|
||||||
|
release/config/config.json=/etc/sing-box/config.json
|
||||||
|
|
||||||
|
release/config/openwrt.conf=/etc/config/sing-box
|
||||||
|
release/config/openwrt.init=/etc/init.d/sing-box
|
||||||
|
release/config/openwrt.keep=/lib/upgrade/keep.d/sing-box
|
||||||
|
|
||||||
|
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
|
||||||
|
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
|
||||||
|
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
|
||||||
|
|
||||||
|
LICENSE=/usr/share/licenses/sing-box/LICENSE
|
||||||
25
.fpm_systemd
Normal file
25
.fpm_systemd
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
-s dir
|
||||||
|
--name sing-box
|
||||||
|
--category net
|
||||||
|
--license GPL-3.0-or-later
|
||||||
|
--description "The universal proxy platform."
|
||||||
|
--url "https://sing-box.sagernet.org/"
|
||||||
|
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||||
|
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
|
||||||
|
--no-deb-generate-changes
|
||||||
|
--config-files /etc/sing-box/config.json
|
||||||
|
--after-install release/config/sing-box.postinst
|
||||||
|
|
||||||
|
release/config/config.json=/etc/sing-box/config.json
|
||||||
|
|
||||||
|
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
|
||||||
|
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
|
||||||
|
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
|
||||||
|
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
|
||||||
|
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
||||||
|
|
||||||
|
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
|
||||||
|
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
|
||||||
|
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
|
||||||
|
|
||||||
|
LICENSE=/usr/share/licenses/sing-box/LICENSE
|
||||||
28
.github/deb2ipk.sh
vendored
Executable file
28
.github/deb2ipk.sh
vendored
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# mod from https://gist.github.com/pldubouilh/c5703052986bfdd404005951dee54683
|
||||||
|
|
||||||
|
set -e -o pipefail
|
||||||
|
|
||||||
|
PROJECT=$(dirname "$0")/../..
|
||||||
|
TMP_PATH=`mktemp -d`
|
||||||
|
cp $2 $TMP_PATH
|
||||||
|
pushd $TMP_PATH
|
||||||
|
|
||||||
|
DEB_NAME=`ls *.deb`
|
||||||
|
ar x $DEB_NAME
|
||||||
|
|
||||||
|
mkdir control
|
||||||
|
pushd control
|
||||||
|
tar xf ../control.tar.gz
|
||||||
|
rm md5sums
|
||||||
|
sed "s/Architecture:\\ \w*/Architecture:\\ $1/g" ./control -i
|
||||||
|
cat control
|
||||||
|
tar czf ../control.tar.gz ./*
|
||||||
|
popd
|
||||||
|
|
||||||
|
DEB_NAME=${DEB_NAME%.deb}
|
||||||
|
tar czf $DEB_NAME.ipk control.tar.gz data.tar.gz debian-binary
|
||||||
|
popd
|
||||||
|
|
||||||
|
cp $TMP_PATH/$DEB_NAME.ipk $3
|
||||||
|
rm -r $TMP_PATH
|
||||||
27
.github/setup_go_for_windows7.sh
vendored
Executable file
27
.github/setup_go_for_windows7.sh
vendored
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
VERSION="1.25.4"
|
||||||
|
|
||||||
|
mkdir -p $HOME/go
|
||||||
|
cd $HOME/go
|
||||||
|
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
|
||||||
|
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
|
||||||
|
mv go go_win7
|
||||||
|
cd go_win7
|
||||||
|
|
||||||
|
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||||
|
# this patch file only works on golang1.25.x
|
||||||
|
# that means after golang1.26 release it must be changed
|
||||||
|
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
|
||||||
|
# revert:
|
||||||
|
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
||||||
|
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||||
|
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
||||||
|
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
||||||
|
|
||||||
|
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
|
||||||
|
|
||||||
|
curl https://github.com/MetaCubeX/go/commit/8cb5472d94c34b88733a81091bd328e70ee565a4.diff | patch --verbose -p 1
|
||||||
|
curl https://github.com/MetaCubeX/go/commit/6788c4c6f9fafb56729bad6b660f7ee2272d699f.diff | patch --verbose -p 1
|
||||||
|
curl https://github.com/MetaCubeX/go/commit/a5b2168bb836ed9d6601c626f95e56c07923f906.diff | patch --verbose -p 1
|
||||||
|
curl https://github.com/MetaCubeX/go/commit/f56f1e23507e646c85243a71bde7b9629b2f970c.diff | patch --verbose -p 1
|
||||||
11
.github/setup_musl_cross.sh
vendored
Executable file
11
.github/setup_musl_cross.sh
vendored
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -xeuo pipefail
|
||||||
|
|
||||||
|
TARGET="$1"
|
||||||
|
|
||||||
|
# Download musl-cross toolchain from musl.cc
|
||||||
|
cd "$HOME"
|
||||||
|
wget -q "https://musl.cc/${TARGET}-cross.tgz"
|
||||||
|
mkdir -p musl-cross
|
||||||
|
tar -xf "${TARGET}-cross.tgz" -C musl-cross --strip-components=1
|
||||||
|
rm "${TARGET}-cross.tgz"
|
||||||
7
.github/update_cronet.sh
vendored
Executable file
7
.github/update_cronet.sh
vendored
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
PROJECTS=$(dirname "$0")/../..
|
||||||
|
git -C $PROJECTS/cronet-go fetch origin go
|
||||||
|
go get -x github.com/sagernet/cronet-go/all@$(git -C $PROJECTS/cronet-go rev-parse origin/go)
|
||||||
|
|
||||||
|
go mod tidy
|
||||||
603
.github/workflows/build.yml
vendored
603
.github/workflows/build.yml
vendored
@@ -40,22 +40,22 @@ jobs:
|
|||||||
version: ${{ steps.outputs.outputs.version }}
|
version: ${{ steps.outputs.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.23
|
go-version: ^1.25.4
|
||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
echo "version=${{ inputs.version }}"
|
echo "version=${{ inputs.version }}"
|
||||||
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||||
- name: Calculate version
|
- name: Calculate version
|
||||||
if: github.event_name != 'workflow_dispatch'
|
if: github.event_name != 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
go run -v ./cmd/internal/read_tag --nightly
|
go run -v ./cmd/internal/read_tag --ci --nightly
|
||||||
- name: Set outputs
|
- name: Set outputs
|
||||||
id: outputs
|
id: outputs
|
||||||
run: |-
|
run: |-
|
||||||
@@ -69,141 +69,414 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- name: linux_386
|
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
|
||||||
goos: linux
|
- { os: linux, arch: "386", go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
|
||||||
goarch: 386
|
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
|
||||||
- name: linux_amd64
|
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
|
||||||
goos: linux
|
- { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" }
|
||||||
goarch: amd64
|
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" }
|
||||||
- name: linux_arm64
|
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" }
|
||||||
goos: linux
|
- { os: linux, arch: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" }
|
||||||
goarch: arm64
|
- { os: linux, arch: mipsle, gomips: hardfloat, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc_24kf" }
|
||||||
- name: linux_arm
|
- { os: linux, arch: mipsle, gomips: softfloat, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
|
||||||
goos: linux
|
- { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" }
|
||||||
goarch: arm
|
- { os: linux, arch: mips64le, gomips: hardfloat, debian: mips64el, rpm: mips64el }
|
||||||
goarm: 6
|
- { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" }
|
||||||
- name: linux_arm_v7
|
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||||
goos: linux
|
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||||
goarch: arm
|
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
|
||||||
goarm: 7
|
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
|
||||||
- name: linux_s390x
|
|
||||||
goos: linux
|
- { os: windows, arch: amd64 }
|
||||||
goarch: s390x
|
- { os: windows, arch: amd64, legacy_win7: true, legacy_name: "windows-7" }
|
||||||
- name: linux_riscv64
|
- { os: windows, arch: "386" }
|
||||||
goos: linux
|
- { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" }
|
||||||
goarch: riscv64
|
- { os: windows, arch: arm64 }
|
||||||
- name: linux_mips64le
|
|
||||||
goos: linux
|
- { os: android, arch: arm64, ndk: "aarch64-linux-android21" }
|
||||||
goarch: mips64le
|
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" }
|
||||||
- name: windows_amd64
|
- { os: android, arch: amd64, ndk: "x86_64-linux-android21" }
|
||||||
goos: windows
|
- { os: android, arch: "386", ndk: "i686-linux-android21" }
|
||||||
goarch: amd64
|
|
||||||
require_legacy_go: true
|
|
||||||
- name: windows_386
|
|
||||||
goos: windows
|
|
||||||
goarch: 386
|
|
||||||
require_legacy_go: true
|
|
||||||
- name: windows_arm64
|
|
||||||
goos: windows
|
|
||||||
goarch: arm64
|
|
||||||
- name: darwin_arm64
|
|
||||||
goos: darwin
|
|
||||||
goarch: arm64
|
|
||||||
- name: darwin_amd64
|
|
||||||
goos: darwin
|
|
||||||
goarch: amd64
|
|
||||||
require_legacy_go: true
|
|
||||||
- name: android_arm64
|
|
||||||
goos: android
|
|
||||||
goarch: arm64
|
|
||||||
- name: android_arm
|
|
||||||
goos: android
|
|
||||||
goarch: arm
|
|
||||||
goarm: 7
|
|
||||||
- name: android_amd64
|
|
||||||
goos: android
|
|
||||||
goarch: amd64
|
|
||||||
- name: android_386
|
|
||||||
goos: android
|
|
||||||
goarch: 386
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Go
|
||||||
|
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ^1.25.4
|
||||||
|
- name: Setup Go 1.24
|
||||||
|
if: matrix.legacy_go124
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ~1.24.10
|
||||||
|
- name: Cache Go for Windows 7
|
||||||
|
if: matrix.legacy_win7
|
||||||
|
id: cache-go-for-windows7
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/go_win7
|
||||||
|
key: go_win7_1254
|
||||||
|
- name: Setup Go for Windows 7
|
||||||
|
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
|
||||||
|
run: |-
|
||||||
|
.github/setup_go_for_windows7.sh
|
||||||
|
- name: Setup Go for Windows 7
|
||||||
|
if: matrix.legacy_win7
|
||||||
|
run: |-
|
||||||
|
echo "PATH=$HOME/go/go_win7/bin:$PATH" >> $GITHUB_ENV
|
||||||
|
echo "GOROOT=$HOME/go/go_win7" >> $GITHUB_ENV
|
||||||
|
- name: Setup Android NDK
|
||||||
|
if: matrix.os == 'android'
|
||||||
|
uses: nttld/setup-ndk@v1
|
||||||
|
with:
|
||||||
|
ndk-version: r28
|
||||||
|
local-cache: true
|
||||||
|
- name: Set tag
|
||||||
|
run: |-
|
||||||
|
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||||
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
|
- name: Set build tags
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,badlinkname,tfogo_checklinkname0'
|
||||||
|
if [[ "${{ matrix.os }}" == "android" ]]; then
|
||||||
|
TAGS="${TAGS},with_naive_outbound"
|
||||||
|
fi
|
||||||
|
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||||
|
- name: Build
|
||||||
|
if: matrix.os != 'android'
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
mkdir -p dist
|
||||||
|
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \
|
||||||
|
./cmd/sing-box
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
GOOS: ${{ matrix.os }}
|
||||||
|
GOARCH: ${{ matrix.arch }}
|
||||||
|
GO386: ${{ matrix.go386 }}
|
||||||
|
GOARM: ${{ matrix.goarm }}
|
||||||
|
GOMIPS: ${{ matrix.gomips }}
|
||||||
|
GOMIPS64: ${{ matrix.gomips }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Build Android
|
||||||
|
if: matrix.os == 'android'
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
go install -v ./cmd/internal/build
|
||||||
|
export CC='${{ matrix.ndk }}-clang'
|
||||||
|
export CXX="${CC}++"
|
||||||
|
mkdir -p dist
|
||||||
|
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \
|
||||||
|
./cmd/sing-box
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: "1"
|
||||||
|
BUILD_GOOS: ${{ matrix.os }}
|
||||||
|
BUILD_GOARCH: ${{ matrix.arch }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Set name
|
||||||
|
run: |-
|
||||||
|
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}"
|
||||||
|
if [[ -n "${{ matrix.goarm }}" ]]; then
|
||||||
|
DIR_NAME="${DIR_NAME}v${{ matrix.goarm }}"
|
||||||
|
elif [[ -n "${{ matrix.go386 }}" && "${{ matrix.go386 }}" != 'sse2' ]]; then
|
||||||
|
DIR_NAME="${DIR_NAME}-${{ matrix.go386 }}"
|
||||||
|
elif [[ -n "${{ matrix.gomips }}" && "${{ matrix.gomips }}" != 'hardfloat' ]]; then
|
||||||
|
DIR_NAME="${DIR_NAME}-${{ matrix.gomips }}"
|
||||||
|
elif [[ -n "${{ matrix.legacy_name }}" ]]; then
|
||||||
|
DIR_NAME="${DIR_NAME}-legacy-${{ matrix.legacy_name }}"
|
||||||
|
fi
|
||||||
|
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
|
||||||
|
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||||
|
PKG_VERSION="${PKG_VERSION//-/\~}"
|
||||||
|
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
|
||||||
|
- name: Package DEB
|
||||||
|
if: matrix.debian != ''
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y debsigs
|
||||||
|
cp .fpm_systemd .fpm
|
||||||
|
fpm -t deb \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.debian }}.deb" \
|
||||||
|
--architecture ${{ matrix.debian }} \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
||||||
|
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
|
||||||
|
rm -rf $HOME/.gnupg
|
||||||
|
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||||
|
${{ secrets.GPG_KEY }}
|
||||||
|
EOF
|
||||||
|
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
|
||||||
|
- name: Package RPM
|
||||||
|
if: matrix.rpm != ''
|
||||||
|
run: |-
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
cp .fpm_systemd .fpm
|
||||||
|
fpm -t rpm \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.rpm }}.rpm" \
|
||||||
|
--architecture ${{ matrix.rpm }} \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
cat > $HOME/.rpmmacros <<EOF
|
||||||
|
%_gpg_name ${{ secrets.GPG_KEY_ID }}
|
||||||
|
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
|
||||||
|
EOF
|
||||||
|
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||||
|
${{ secrets.GPG_KEY }}
|
||||||
|
EOF
|
||||||
|
rpmsign --addsign dist/*.rpm
|
||||||
|
- name: Package Pacman
|
||||||
|
if: matrix.pacman != ''
|
||||||
|
run: |-
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libarchive-tools
|
||||||
|
cp .fpm_systemd .fpm
|
||||||
|
fpm -t pacman \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \
|
||||||
|
--architecture ${{ matrix.pacman }} \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
- name: Package OpenWrt
|
||||||
|
if: matrix.openwrt != ''
|
||||||
|
run: |-
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
cp .fpm_openwrt .fpm
|
||||||
|
fpm -t deb \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/openwrt.deb" \
|
||||||
|
--architecture all \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
for architecture in ${{ matrix.openwrt }}; do
|
||||||
|
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk"
|
||||||
|
done
|
||||||
|
rm "dist/openwrt.deb"
|
||||||
|
- name: Archive
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
cd dist
|
||||||
|
mkdir -p "${DIR_NAME}"
|
||||||
|
cp ../LICENSE "${DIR_NAME}"
|
||||||
|
if [ '${{ matrix.os }}' = 'windows' ]; then
|
||||||
|
cp sing-box "${DIR_NAME}/sing-box.exe"
|
||||||
|
zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
|
||||||
|
else
|
||||||
|
cp sing-box "${DIR_NAME}"
|
||||||
|
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
|
||||||
|
fi
|
||||||
|
rm -r "${DIR_NAME}"
|
||||||
|
- name: Cleanup
|
||||||
|
run: rm dist/sing-box
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_name && format('-legacy-{0}', matrix.legacy_name) }}
|
||||||
|
path: "dist"
|
||||||
|
build_darwin:
|
||||||
|
name: Build Darwin binaries
|
||||||
|
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
|
||||||
|
runs-on: macos-latest
|
||||||
|
needs:
|
||||||
|
- calculate_version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { arch: amd64 }
|
||||||
|
- { arch: arm64 }
|
||||||
|
- { arch: amd64, legacy_go124: true, legacy_name: "macos-11" }
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Go
|
||||||
|
if: ${{ ! matrix.legacy_go124 }}
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ^1.25.3
|
||||||
|
- name: Setup Go 1.24
|
||||||
|
if: matrix.legacy_go124
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ~1.24.6
|
||||||
|
- name: Set tag
|
||||||
|
run: |-
|
||||||
|
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||||
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
|
- name: Set build tags
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_naive_outbound,badlinkname,tfogo_checklinkname0'
|
||||||
|
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
mkdir -p dist
|
||||||
|
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \
|
||||||
|
./cmd/sing-box
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: "1"
|
||||||
|
GOOS: darwin
|
||||||
|
GOARCH: ${{ matrix.arch }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Set name
|
||||||
|
run: |-
|
||||||
|
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-darwin-${{ matrix.arch }}"
|
||||||
|
if [[ -n "${{ matrix.legacy_name }}" ]]; then
|
||||||
|
DIR_NAME="${DIR_NAME}-legacy-${{ matrix.legacy_name }}"
|
||||||
|
fi
|
||||||
|
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
|
||||||
|
- name: Archive
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
cd dist
|
||||||
|
mkdir -p "${DIR_NAME}"
|
||||||
|
cp ../LICENSE "${DIR_NAME}"
|
||||||
|
cp sing-box "${DIR_NAME}"
|
||||||
|
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
|
||||||
|
rm -r "${DIR_NAME}"
|
||||||
|
- name: Cleanup
|
||||||
|
run: rm dist/sing-box
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: binary-darwin_${{ matrix.arch }}${{ matrix.legacy_name && format('-legacy-{0}', matrix.legacy_name) }}
|
||||||
|
path: "dist"
|
||||||
|
build_naive_linux:
|
||||||
|
name: Build Linux with naive outbound
|
||||||
|
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- calculate_version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
# Linux glibc (dynamic linking with Debian Bullseye sysroot)
|
||||||
|
- { arch: amd64, sysroot_arch: amd64, sysroot_sha: "36a164623d03f525e3dfb783a5e9b8a00e98e1ddd2b5cff4e449bd016dd27e50", cc_target: "x86_64-linux-gnu", suffix: "-naive" }
|
||||||
|
- { arch: arm64, sysroot_arch: arm64, sysroot_sha: "2f915d821eec27515c0c6d21b69898e23762908d8d7ccc1aa2a8f5f25e8b7e18", cc_target: "aarch64-linux-gnu", suffix: "-naive" }
|
||||||
|
- { arch: "386", sysroot_arch: i386, sysroot_sha: "63f0e5128b84f7b0421956a4a40affa472be8da0e58caf27e9acbc84072daee7", cc_target: "i686-linux-gnu", suffix: "-naive" }
|
||||||
|
- { arch: arm, goarm: "7", sysroot_arch: armhf, sysroot_sha: "47b3a0b161ca011b2b33d4fc1ef6ef269b8208a0b7e4c900700c345acdfd1814", cc_target: "arm-linux-gnueabihf", suffix: "-naive" }
|
||||||
|
# Linux musl (static linking)
|
||||||
|
- { arch: amd64, musl: true, cc_target: "x86_64-linux-musl", suffix: "-naive-musl" }
|
||||||
|
- { arch: arm64, musl: true, cc_target: "aarch64-linux-musl", suffix: "-naive-musl" }
|
||||||
|
- { arch: "386", musl: true, cc_target: "i686-linux-musl", suffix: "-naive-musl" }
|
||||||
|
- { arch: arm, goarm: "7", musl: true, cc_target: "arm-linux-musleabihf", suffix: "-naive-musl" }
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.23
|
go-version: ^1.25.4
|
||||||
- name: Cache legacy Go
|
|
||||||
if: matrix.require_legacy_go
|
|
||||||
id: cache-legacy-go
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/go/go1.20.14
|
|
||||||
key: go120
|
|
||||||
- name: Setup legacy Go
|
|
||||||
if: matrix.require_legacy_go == 'true' && steps.cache-legacy-go.outputs.cache-hit != 'true'
|
|
||||||
run: |-
|
|
||||||
wget https://dl.google.com/go/go1.20.14.linux-amd64.tar.gz
|
|
||||||
tar -xzf go1.20.14.linux-amd64.tar.gz
|
|
||||||
mv go $HOME/go/go1.20.14
|
|
||||||
- name: Setup Android NDK
|
|
||||||
if: matrix.goos == 'android'
|
|
||||||
uses: nttld/setup-ndk@v1
|
|
||||||
with:
|
|
||||||
ndk-version: r28-beta2
|
|
||||||
local-cache: true
|
|
||||||
- name: Setup Goreleaser
|
|
||||||
uses: goreleaser/goreleaser-action@v6
|
|
||||||
with:
|
|
||||||
distribution: goreleaser-pro
|
|
||||||
version: latest
|
|
||||||
install-only: true
|
|
||||||
- name: Extract signing key
|
|
||||||
run: |-
|
|
||||||
mkdir -p $HOME/.gnupg
|
|
||||||
cat > $HOME/.gnupg/sagernet.key <<EOF
|
|
||||||
${{ secrets.GPG_KEY }}
|
|
||||||
EOF
|
|
||||||
echo "HOME=$HOME" >> "$GITHUB_ENV"
|
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
run: |-
|
run: |-
|
||||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
- name: Build
|
- name: Download sysroot (glibc)
|
||||||
if: matrix.goos != 'android'
|
if: ${{ ! matrix.musl }}
|
||||||
run: |-
|
run: |
|
||||||
goreleaser release --clean --split
|
set -xeuo pipefail
|
||||||
|
wget -q "https://commondatastorage.googleapis.com/chrome-linux-sysroot/${{ matrix.sysroot_sha }}" -O sysroot.tar.xz
|
||||||
|
mkdir -p /tmp/sysroot
|
||||||
|
tar -xf sysroot.tar.xz -C /tmp/sysroot
|
||||||
|
- name: Install cross compiler (glibc)
|
||||||
|
if: ${{ ! matrix.musl }}
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y clang lld
|
||||||
|
if [[ "${{ matrix.arch }}" == "arm64" ]]; then
|
||||||
|
sudo apt-get install -y libc6-dev-arm64-cross
|
||||||
|
elif [[ "${{ matrix.arch }}" == "386" ]]; then
|
||||||
|
sudo apt-get install -y libc6-dev-i386-cross
|
||||||
|
elif [[ "${{ matrix.arch }}" == "arm" ]]; then
|
||||||
|
sudo apt-get install -y libc6-dev-armhf-cross
|
||||||
|
fi
|
||||||
|
- name: Install musl cross compiler
|
||||||
|
if: matrix.musl
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
.github/setup_musl_cross.sh "${{ matrix.cc_target }}"
|
||||||
|
echo "PATH=$HOME/musl-cross/bin:$PATH" >> $GITHUB_ENV
|
||||||
|
- name: Set build tags
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_naive_outbound,badlinkname,tfogo_checklinkname0'
|
||||||
|
if [[ "${{ matrix.musl }}" == "true" ]]; then
|
||||||
|
TAGS="${TAGS},with_musl"
|
||||||
|
fi
|
||||||
|
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||||
|
- name: Build (glibc)
|
||||||
|
if: ${{ ! matrix.musl }}
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
mkdir -p dist
|
||||||
|
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0 -linkmode=external -extldflags "-fuse-ld=lld --sysroot=/tmp/sysroot"' \
|
||||||
|
./cmd/sing-box
|
||||||
env:
|
env:
|
||||||
GOOS: ${{ matrix.goos }}
|
CGO_ENABLED: "1"
|
||||||
GOARCH: ${{ matrix.goarch }}
|
GOOS: linux
|
||||||
GOPATH: ${{ env.HOME }}/go
|
GOARCH: ${{ matrix.arch }}
|
||||||
GOARM: ${{ matrix.goarm }}
|
GOARM: ${{ matrix.goarm }}
|
||||||
|
CC: "clang --target=${{ matrix.cc_target }} --sysroot=/tmp/sysroot"
|
||||||
|
CXX: "clang++ --target=${{ matrix.cc_target }} --sysroot=/tmp/sysroot"
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
- name: Build (musl)
|
||||||
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
if: matrix.musl
|
||||||
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
run: |
|
||||||
- name: Build Android
|
set -xeuo pipefail
|
||||||
if: matrix.goos == 'android'
|
mkdir -p dist
|
||||||
run: |-
|
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
go install -v ./cmd/internal/build
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0 -linkmode=external -extldflags "-static"' \
|
||||||
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build goreleaser release --clean --split
|
./cmd/sing-box
|
||||||
env:
|
env:
|
||||||
BUILD_GOOS: ${{ matrix.goos }}
|
CGO_ENABLED: "1"
|
||||||
BUILD_GOARCH: ${{ matrix.goarch }}
|
GOOS: linux
|
||||||
|
GOARCH: ${{ matrix.arch }}
|
||||||
GOARM: ${{ matrix.goarm }}
|
GOARM: ${{ matrix.goarm }}
|
||||||
|
CC: "${{ matrix.cc_target }}-gcc"
|
||||||
|
CXX: "${{ matrix.cc_target }}-g++"
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
- name: Set name
|
||||||
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
run: |-
|
||||||
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-linux-${{ matrix.arch }}"
|
||||||
|
if [[ -n "${{ matrix.goarm }}" ]]; then
|
||||||
|
DIR_NAME="${DIR_NAME}v${{ matrix.goarm }}"
|
||||||
|
fi
|
||||||
|
DIR_NAME="${DIR_NAME}${{ matrix.suffix }}"
|
||||||
|
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
|
||||||
|
- name: Archive
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
cd dist
|
||||||
|
mkdir -p "${DIR_NAME}"
|
||||||
|
cp ../LICENSE "${DIR_NAME}"
|
||||||
|
cp sing-box "${DIR_NAME}"
|
||||||
|
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
|
||||||
|
rm -r "${DIR_NAME}"
|
||||||
|
- name: Cleanup
|
||||||
|
run: rm dist/sing-box
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
if: github.event_name == 'workflow_dispatch'
|
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: binary-${{ matrix.name }}
|
name: binary-linux_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.suffix }}
|
||||||
path: 'dist'
|
path: "dist"
|
||||||
build_android:
|
build_android:
|
||||||
name: Build Android
|
name: Build Android
|
||||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
|
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
|
||||||
@@ -212,19 +485,19 @@ jobs:
|
|||||||
- calculate_version
|
- calculate_version
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.23
|
go-version: ^1.25.4
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
with:
|
with:
|
||||||
ndk-version: r28-beta2
|
ndk-version: r28
|
||||||
- name: Setup OpenJDK
|
- name: Setup OpenJDK
|
||||||
run: |-
|
run: |-
|
||||||
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
||||||
@@ -256,9 +529,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: ~/.gradle
|
path: ~/.gradle
|
||||||
key: gradle-${{ hashFiles('**/*.gradle') }}
|
key: gradle-${{ hashFiles('**/*.gradle') }}
|
||||||
- name: Build
|
- name: Update version
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
go run -v ./cmd/internal/update_android_version --ci
|
go run -v ./cmd/internal/update_android_version --ci
|
||||||
|
- name: Update nightly version
|
||||||
|
if: github.event_name != 'workflow_dispatch'
|
||||||
|
run: |-
|
||||||
|
go run -v ./cmd/internal/update_android_version --ci --nightly
|
||||||
|
- name: Build
|
||||||
|
run: |-
|
||||||
mkdir clients/android/app/libs
|
mkdir clients/android/app/libs
|
||||||
cp libbox.aar clients/android/app/libs
|
cp libbox.aar clients/android/app/libs
|
||||||
cd clients/android
|
cd clients/android
|
||||||
@@ -268,13 +548,11 @@ jobs:
|
|||||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
||||||
- name: Prepare upload
|
- name: Prepare upload
|
||||||
if: github.event_name == 'workflow_dispatch'
|
|
||||||
run: |-
|
run: |-
|
||||||
mkdir -p dist/release
|
mkdir -p dist
|
||||||
cp clients/android/app/build/outputs/apk/play/release/*.apk dist/release
|
cp clients/android/app/build/outputs/apk/play/release/*.apk dist
|
||||||
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist/release
|
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
if: github.event_name == 'workflow_dispatch'
|
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: binary-android-apks
|
name: binary-android-apks
|
||||||
@@ -287,19 +565,19 @@ jobs:
|
|||||||
- calculate_version
|
- calculate_version
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.23
|
go-version: ^1.25.4
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
with:
|
with:
|
||||||
ndk-version: r28-beta2
|
ndk-version: r28
|
||||||
- name: Setup OpenJDK
|
- name: Setup OpenJDK
|
||||||
run: |-
|
run: |-
|
||||||
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
||||||
@@ -346,7 +624,8 @@ jobs:
|
|||||||
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
|
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
|
||||||
build_apple:
|
build_apple:
|
||||||
name: Build Apple clients
|
name: Build Apple clients
|
||||||
runs-on: macos-15
|
runs-on: macos-26
|
||||||
|
if: false
|
||||||
needs:
|
needs:
|
||||||
- calculate_version
|
- calculate_version
|
||||||
strategy:
|
strategy:
|
||||||
@@ -384,7 +663,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
if: matrix.if
|
if: matrix.if
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
@@ -392,15 +671,7 @@ jobs:
|
|||||||
if: matrix.if
|
if: matrix.if
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.23
|
go-version: ^1.25.4
|
||||||
- name: Setup Xcode stable
|
|
||||||
if: matrix.if && github.ref == 'refs/heads/main-next'
|
|
||||||
run: |-
|
|
||||||
sudo xcode-select -s /Applications/Xcode_16.2.app
|
|
||||||
- name: Setup Xcode beta
|
|
||||||
if: matrix.if && github.ref == 'refs/heads/dev-next'
|
|
||||||
run: |-
|
|
||||||
sudo xcode-select -s /Applications/Xcode_16.2.app
|
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
if: matrix.if
|
if: matrix.if
|
||||||
run: |-
|
run: |-
|
||||||
@@ -432,19 +703,19 @@ jobs:
|
|||||||
|
|
||||||
PROFILES_ZIP_PATH=$RUNNER_TEMP/Profiles.zip
|
PROFILES_ZIP_PATH=$RUNNER_TEMP/Profiles.zip
|
||||||
echo -n "$PROVISIONING_PROFILES" | base64 --decode -o $PROFILES_ZIP_PATH
|
echo -n "$PROVISIONING_PROFILES" | base64 --decode -o $PROFILES_ZIP_PATH
|
||||||
|
|
||||||
PROFILES_PATH="$HOME/Library/MobileDevice/Provisioning Profiles"
|
PROFILES_PATH="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||||
mkdir -p "$PROFILES_PATH"
|
mkdir -p "$PROFILES_PATH"
|
||||||
unzip $PROFILES_ZIP_PATH -d "$PROFILES_PATH"
|
unzip $PROFILES_ZIP_PATH -d "$PROFILES_PATH"
|
||||||
|
|
||||||
ASC_KEY_PATH=$RUNNER_TEMP/Key.p12
|
ASC_KEY_PATH=$RUNNER_TEMP/Key.p12
|
||||||
echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH
|
echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH
|
||||||
|
|
||||||
xcrun notarytool store-credentials "notarytool-password" \
|
xcrun notarytool store-credentials "notarytool-password" \
|
||||||
--key $ASC_KEY_PATH \
|
--key $ASC_KEY_PATH \
|
||||||
--key-id $ASC_KEY_ID \
|
--key-id $ASC_KEY_ID \
|
||||||
--issuer $ASC_KEY_ISSUER_ID
|
--issuer $ASC_KEY_ISSUER_ID
|
||||||
|
|
||||||
echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV"
|
echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV"
|
||||||
echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV"
|
echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV"
|
||||||
echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV"
|
echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV"
|
||||||
@@ -469,10 +740,13 @@ jobs:
|
|||||||
MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version)
|
MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version)
|
||||||
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION"
|
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION"
|
||||||
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV"
|
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV"
|
||||||
|
- name: Update version
|
||||||
|
if: matrix.if && matrix.name != 'iOS'
|
||||||
|
run: |-
|
||||||
|
go run -v ./cmd/internal/update_apple_version --ci
|
||||||
- name: Build
|
- name: Build
|
||||||
if: matrix.if
|
if: matrix.if
|
||||||
run: |-
|
run: |-
|
||||||
go run -v ./cmd/internal/update_apple_version --ci
|
|
||||||
cd clients/apple
|
cd clients/apple
|
||||||
xcodebuild archive \
|
xcodebuild archive \
|
||||||
-scheme "${{ matrix.scheme }}" \
|
-scheme "${{ matrix.scheme }}" \
|
||||||
@@ -520,10 +794,10 @@ jobs:
|
|||||||
cd "${{ matrix.archive }}"
|
cd "${{ matrix.archive }}"
|
||||||
zip -r SFM.dSYMs.zip dSYMs
|
zip -r SFM.dSYMs.zip dSYMs
|
||||||
popd
|
popd
|
||||||
|
|
||||||
mkdir -p dist/release
|
mkdir -p dist
|
||||||
cp clients/apple/SFM.dmg "dist/release/SFM-${VERSION}-universal.dmg"
|
cp clients/apple/SFM.dmg "dist/SFM-${VERSION}-universal.dmg"
|
||||||
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/release/SFM-${VERSION}-universal.dSYMs.zip"
|
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/SFM-${VERSION}-universal.dSYMs.zip"
|
||||||
- name: Upload image
|
- name: Upload image
|
||||||
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -532,24 +806,20 @@ jobs:
|
|||||||
path: 'dist'
|
path: 'dist'
|
||||||
upload:
|
upload:
|
||||||
name: Upload builds
|
name: Upload builds
|
||||||
if: always() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')
|
if: "!failure() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- calculate_version
|
- calculate_version
|
||||||
- build
|
- build
|
||||||
|
- build_darwin
|
||||||
|
- build_naive_linux
|
||||||
- build_android
|
- build_android
|
||||||
- build_apple
|
- build_apple
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Goreleaser
|
|
||||||
uses: goreleaser/goreleaser-action@v6
|
|
||||||
with:
|
|
||||||
distribution: goreleaser-pro
|
|
||||||
version: latest
|
|
||||||
install-only: true
|
|
||||||
- name: Cache ghr
|
- name: Cache ghr
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
id: cache-ghr
|
id: cache-ghr
|
||||||
@@ -570,30 +840,21 @@ jobs:
|
|||||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||||
- name: Download builds
|
- name: Download builds
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: dist
|
path: dist
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- name: Merge builds
|
|
||||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
|
|
||||||
run: |-
|
|
||||||
goreleaser continue --merge --skip publish
|
|
||||||
mkdir -p dist/release
|
|
||||||
mv dist/*/sing-box*{tar.gz,zip,deb,rpm,_amd64.pkg.tar.zst,_arm64.pkg.tar.zst} dist/release
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
|
||||||
- name: Upload builds
|
- name: Upload builds
|
||||||
if: ${{ env.PUBLISHED == 'false' }}
|
if: ${{ env.PUBLISHED == 'false' }}
|
||||||
run: |-
|
run: |-
|
||||||
export PATH="$PATH:$HOME/go/bin"
|
export PATH="$PATH:$HOME/go/bin"
|
||||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Replace builds
|
- name: Replace builds
|
||||||
if: ${{ env.PUBLISHED != 'false' }}
|
if: ${{ env.PUBLISHED != 'false' }}
|
||||||
run: |-
|
run: |-
|
||||||
export PATH="$PATH:$HOME/go/bin"
|
export PATH="$PATH:$HOME/go/bin"
|
||||||
ghr --replace -p 5 "v${VERSION}" dist/release
|
ghr --replace -p 5 "v${VERSION}" dist
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
|||||||
echo "ref=$ref"
|
echo "ref=$ref"
|
||||||
echo "ref=$ref" >> $GITHUB_OUTPUT
|
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
with:
|
with:
|
||||||
ref: ${{ steps.ref.outputs.ref }}
|
ref: ${{ steps.ref.outputs.ref }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
echo "latest=$latest"
|
echo "latest=$latest"
|
||||||
echo "latest=$latest" >> $GITHUB_OUTPUT
|
echo "latest=$latest" >> $GITHUB_OUTPUT
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
|
|||||||
11
.github/workflows/lint.yml
vendored
11
.github/workflows/lint.yml
vendored
@@ -22,16 +22,17 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.23
|
go-version: ^1.25
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v6
|
uses: golangci/golangci-lint-action@v8
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: v2.4.0
|
||||||
args: --timeout=30m
|
args: --timeout=30m
|
||||||
install-mode: binary
|
install-mode: binary
|
||||||
|
verify: false
|
||||||
|
|||||||
189
.github/workflows/linux.yml
vendored
189
.github/workflows/linux.yml
vendored
@@ -1,38 +1,189 @@
|
|||||||
name: Release to Linux repository
|
name: Build Linux Packages
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Version name"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
forceBeta:
|
||||||
|
description: "Force beta"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- published
|
- published
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
calculate_version:
|
||||||
|
name: Calculate version
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.outputs.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.23
|
go-version: ^1.25.4
|
||||||
- name: Extract signing key
|
- name: Check input version
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
mkdir -p $HOME/.gnupg
|
echo "version=${{ inputs.version }}"
|
||||||
cat > $HOME/.gnupg/sagernet.key <<EOF
|
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||||
|
- name: Calculate version
|
||||||
|
if: github.event_name != 'workflow_dispatch'
|
||||||
|
run: |-
|
||||||
|
go run -v ./cmd/internal/read_tag --ci --nightly
|
||||||
|
- name: Set outputs
|
||||||
|
id: outputs
|
||||||
|
run: |-
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
build:
|
||||||
|
name: Build binary
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- calculate_version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 }
|
||||||
|
- { os: linux, arch: "386", debian: i386, rpm: i386 }
|
||||||
|
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
|
||||||
|
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl }
|
||||||
|
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64 }
|
||||||
|
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
|
||||||
|
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
|
||||||
|
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||||
|
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||||
|
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64 }
|
||||||
|
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ^1.25.4
|
||||||
|
- name: Setup Android NDK
|
||||||
|
if: matrix.os == 'android'
|
||||||
|
uses: nttld/setup-ndk@v1
|
||||||
|
with:
|
||||||
|
ndk-version: r28
|
||||||
|
local-cache: true
|
||||||
|
- name: Set tag
|
||||||
|
run: |-
|
||||||
|
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||||
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
|
- name: Set build tags
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,badlinkname,tfogo_checklinkname0'
|
||||||
|
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
mkdir -p dist
|
||||||
|
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \
|
||||||
|
./cmd/sing-box
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
GOOS: ${{ matrix.os }}
|
||||||
|
GOARCH: ${{ matrix.arch }}
|
||||||
|
GOARM: ${{ matrix.goarm }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Set mtime
|
||||||
|
run: |-
|
||||||
|
TZ=UTC touch -t '197001010000' dist/sing-box
|
||||||
|
- name: Set name
|
||||||
|
if: (! contains(needs.calculate_version.outputs.version, '-')) && !inputs.forceBeta
|
||||||
|
run: |-
|
||||||
|
echo "NAME=sing-box" >> "$GITHUB_ENV"
|
||||||
|
- name: Set beta name
|
||||||
|
if: contains(needs.calculate_version.outputs.version, '-') || inputs.forceBeta
|
||||||
|
run: |-
|
||||||
|
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
|
||||||
|
- name: Set version
|
||||||
|
run: |-
|
||||||
|
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||||
|
PKG_VERSION="${PKG_VERSION//-/\~}"
|
||||||
|
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
|
||||||
|
- name: Package DEB
|
||||||
|
if: matrix.debian != ''
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
sudo apt-get install -y debsigs
|
||||||
|
cp .fpm_systemd .fpm
|
||||||
|
fpm -t deb \
|
||||||
|
--name "${NAME}" \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
|
||||||
|
--architecture ${{ matrix.debian }} \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
||||||
|
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
|
||||||
|
rm -rf $HOME/.gnupg
|
||||||
|
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||||
${{ secrets.GPG_KEY }}
|
${{ secrets.GPG_KEY }}
|
||||||
EOF
|
EOF
|
||||||
echo "HOME=$HOME" >> "$GITHUB_ENV"
|
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
|
||||||
- name: Publish release
|
- name: Package RPM
|
||||||
uses: goreleaser/goreleaser-action@v6
|
if: matrix.rpm != ''
|
||||||
|
run: |-
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
cp .fpm_systemd .fpm
|
||||||
|
fpm -t rpm \
|
||||||
|
--name "${NAME}" \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
|
||||||
|
--architecture ${{ matrix.rpm }} \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
cat > $HOME/.rpmmacros <<EOF
|
||||||
|
%_gpg_name ${{ secrets.GPG_KEY_ID }}
|
||||||
|
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
|
||||||
|
EOF
|
||||||
|
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||||
|
${{ secrets.GPG_KEY }}
|
||||||
|
EOF
|
||||||
|
rpmsign --addsign dist/*.rpm
|
||||||
|
- name: Cleanup
|
||||||
|
run: rm dist/sing-box
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser-pro
|
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.legacy_go && '-legacy' || '' }}
|
||||||
version: latest
|
path: "dist"
|
||||||
args: release -f .goreleaser.fury.yaml --clean
|
upload:
|
||||||
env:
|
name: Upload builds
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
runs-on: ubuntu-latest
|
||||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
needs:
|
||||||
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
|
- calculate_version
|
||||||
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
- build
|
||||||
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set tag
|
||||||
|
run: |-
|
||||||
|
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||||
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
|
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||||
|
- name: Download builds
|
||||||
|
uses: actions/download-artifact@v5
|
||||||
|
with:
|
||||||
|
path: dist
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Publish packages
|
||||||
|
run: |-
|
||||||
|
ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -15,4 +15,6 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
/config.d/
|
/config.d/
|
||||||
/venv/
|
/venv/
|
||||||
|
CLAUDE.md
|
||||||
|
AGENTS.md
|
||||||
|
/.claude/
|
||||||
|
|||||||
@@ -1,39 +1,59 @@
|
|||||||
linters:
|
version: "2"
|
||||||
disable-all: true
|
|
||||||
enable:
|
|
||||||
- gofumpt
|
|
||||||
- govet
|
|
||||||
- gci
|
|
||||||
- staticcheck
|
|
||||||
- paralleltest
|
|
||||||
- ineffassign
|
|
||||||
|
|
||||||
linters-settings:
|
|
||||||
gci:
|
|
||||||
custom-order: true
|
|
||||||
sections:
|
|
||||||
- standard
|
|
||||||
- prefix(github.com/sagernet/)
|
|
||||||
- default
|
|
||||||
staticcheck:
|
|
||||||
checks:
|
|
||||||
- all
|
|
||||||
- -SA1003
|
|
||||||
|
|
||||||
run:
|
run:
|
||||||
go: "1.23"
|
go: "1.25"
|
||||||
build-tags:
|
build-tags:
|
||||||
- with_gvisor
|
- with_gvisor
|
||||||
- with_quic
|
- with_quic
|
||||||
- with_dhcp
|
- with_dhcp
|
||||||
- with_wireguard
|
- with_wireguard
|
||||||
- with_ech
|
|
||||||
- with_utls
|
- with_utls
|
||||||
- with_reality_server
|
|
||||||
- with_acme
|
- with_acme
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
- badlinkname
|
linters:
|
||||||
|
default: none
|
||||||
issues:
|
enable:
|
||||||
exclude-dirs:
|
- govet
|
||||||
- transport/simple-obfs
|
- ineffassign
|
||||||
|
- paralleltest
|
||||||
|
- staticcheck
|
||||||
|
settings:
|
||||||
|
staticcheck:
|
||||||
|
checks:
|
||||||
|
- all
|
||||||
|
- -S1000
|
||||||
|
- -S1008
|
||||||
|
- -S1017
|
||||||
|
- -ST1003
|
||||||
|
- -QF1001
|
||||||
|
- -QF1003
|
||||||
|
- -QF1008
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
paths:
|
||||||
|
- transport/simple-obfs
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gci
|
||||||
|
- gofumpt
|
||||||
|
settings:
|
||||||
|
gci:
|
||||||
|
sections:
|
||||||
|
- standard
|
||||||
|
- prefix(github.com/sagernet/)
|
||||||
|
- default
|
||||||
|
custom-order: true
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- transport/simple-obfs
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
project_name: sing-box
|
|
||||||
builds:
|
|
||||||
- id: main
|
|
||||||
main: ./cmd/sing-box
|
|
||||||
flags:
|
|
||||||
- -v
|
|
||||||
- -trimpath
|
|
||||||
ldflags:
|
|
||||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
|
||||||
- -s
|
|
||||||
- -buildid=
|
|
||||||
- -checklinkname=0
|
|
||||||
tags:
|
|
||||||
- with_gvisor
|
|
||||||
- with_quic
|
|
||||||
- with_dhcp
|
|
||||||
- with_wireguard
|
|
||||||
- with_ech
|
|
||||||
- with_utls
|
|
||||||
- with_reality_server
|
|
||||||
- with_acme
|
|
||||||
- with_clash_api
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
targets:
|
|
||||||
- linux_386
|
|
||||||
- linux_amd64_v1
|
|
||||||
- linux_arm64
|
|
||||||
- linux_arm_7
|
|
||||||
- linux_s390x
|
|
||||||
- linux_riscv64
|
|
||||||
- linux_mips64le
|
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
|
||||||
snapshot:
|
|
||||||
name_template: "{{ .Version }}.{{ .ShortCommit }}"
|
|
||||||
nfpms:
|
|
||||||
- &template
|
|
||||||
id: package
|
|
||||||
package_name: sing-box
|
|
||||||
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
|
||||||
builds:
|
|
||||||
- main
|
|
||||||
homepage: https://sing-box.sagernet.org/
|
|
||||||
maintainer: nekohasekai <contact-git@sekai.icu>
|
|
||||||
description: The universal proxy platform.
|
|
||||||
license: GPLv3 or later
|
|
||||||
formats:
|
|
||||||
- deb
|
|
||||||
- rpm
|
|
||||||
priority: extra
|
|
||||||
contents:
|
|
||||||
- src: release/config/config.json
|
|
||||||
dst: /etc/sing-box/config.json
|
|
||||||
type: config
|
|
||||||
|
|
||||||
- src: release/config/sing-box.service
|
|
||||||
dst: /usr/lib/systemd/system/sing-box.service
|
|
||||||
- src: release/config/sing-box@.service
|
|
||||||
dst: /usr/lib/systemd/system/sing-box@.service
|
|
||||||
|
|
||||||
- src: release/completions/sing-box.bash
|
|
||||||
dst: /usr/share/bash-completion/completions/sing-box.bash
|
|
||||||
- src: release/completions/sing-box.fish
|
|
||||||
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
|
|
||||||
- src: release/completions/sing-box.zsh
|
|
||||||
dst: /usr/share/zsh/site-functions/_sing-box
|
|
||||||
|
|
||||||
- src: LICENSE
|
|
||||||
dst: /usr/share/licenses/sing-box/LICENSE
|
|
||||||
deb:
|
|
||||||
signature:
|
|
||||||
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
|
||||||
fields:
|
|
||||||
Bugs: https://github.com/SagerNet/sing-box/issues
|
|
||||||
rpm:
|
|
||||||
signature:
|
|
||||||
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
|
||||||
conflicts:
|
|
||||||
- sing-box-beta
|
|
||||||
- id: package_beta
|
|
||||||
<<: *template
|
|
||||||
package_name: sing-box-beta
|
|
||||||
file_name_template: '{{ .ProjectName }}-beta_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
|
||||||
formats:
|
|
||||||
- deb
|
|
||||||
- rpm
|
|
||||||
conflicts:
|
|
||||||
- sing-box
|
|
||||||
release:
|
|
||||||
disable: true
|
|
||||||
furies:
|
|
||||||
- account: sagernet
|
|
||||||
ids:
|
|
||||||
- package
|
|
||||||
disable: "{{ not (not .Prerelease) }}"
|
|
||||||
- account: sagernet
|
|
||||||
ids:
|
|
||||||
- package_beta
|
|
||||||
disable: "{{ not .Prerelease }}"
|
|
||||||
206
.goreleaser.yaml
206
.goreleaser.yaml
@@ -1,206 +0,0 @@
|
|||||||
version: 2
|
|
||||||
project_name: sing-box
|
|
||||||
builds:
|
|
||||||
- &template
|
|
||||||
id: main
|
|
||||||
main: ./cmd/sing-box
|
|
||||||
flags:
|
|
||||||
- -v
|
|
||||||
- -trimpath
|
|
||||||
ldflags:
|
|
||||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
|
||||||
- -s
|
|
||||||
- -buildid=
|
|
||||||
- -checklinkname=0
|
|
||||||
tags:
|
|
||||||
- with_gvisor
|
|
||||||
- with_quic
|
|
||||||
- with_dhcp
|
|
||||||
- with_wireguard
|
|
||||||
- with_ech
|
|
||||||
- with_utls
|
|
||||||
- with_reality_server
|
|
||||||
- with_acme
|
|
||||||
- with_clash_api
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
targets:
|
|
||||||
- linux_386
|
|
||||||
- linux_amd64_v1
|
|
||||||
- linux_arm64
|
|
||||||
- linux_arm_6
|
|
||||||
- linux_arm_7
|
|
||||||
- linux_s390x
|
|
||||||
- linux_riscv64
|
|
||||||
- linux_mips64le
|
|
||||||
- windows_amd64_v1
|
|
||||||
- windows_386
|
|
||||||
- windows_arm64
|
|
||||||
- darwin_amd64_v1
|
|
||||||
- darwin_arm64
|
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
|
||||||
- id: legacy
|
|
||||||
<<: *template
|
|
||||||
tags:
|
|
||||||
- with_gvisor
|
|
||||||
- with_quic
|
|
||||||
- with_dhcp
|
|
||||||
- with_wireguard
|
|
||||||
- with_utls
|
|
||||||
- with_reality_server
|
|
||||||
- with_acme
|
|
||||||
- with_clash_api
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
- GOROOT={{ .Env.GOPATH }}/go1.20.14
|
|
||||||
gobinary: "{{ .Env.GOPATH }}/go1.20.14/bin/go"
|
|
||||||
targets:
|
|
||||||
- windows_amd64_v1
|
|
||||||
- windows_386
|
|
||||||
- darwin_amd64_v1
|
|
||||||
- id: android
|
|
||||||
<<: *template
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=1
|
|
||||||
overrides:
|
|
||||||
- goos: android
|
|
||||||
goarch: arm
|
|
||||||
goarm: 7
|
|
||||||
env:
|
|
||||||
- CC=armv7a-linux-androideabi21-clang
|
|
||||||
- CXX=armv7a-linux-androideabi21-clang++
|
|
||||||
- goos: android
|
|
||||||
goarch: arm64
|
|
||||||
env:
|
|
||||||
- CC=aarch64-linux-android21-clang
|
|
||||||
- CXX=aarch64-linux-android21-clang++
|
|
||||||
- goos: android
|
|
||||||
goarch: 386
|
|
||||||
env:
|
|
||||||
- CC=i686-linux-android21-clang
|
|
||||||
- CXX=i686-linux-android21-clang++
|
|
||||||
- goos: android
|
|
||||||
goarch: amd64
|
|
||||||
goamd64: v1
|
|
||||||
env:
|
|
||||||
- CC=x86_64-linux-android21-clang
|
|
||||||
- CXX=x86_64-linux-android21-clang++
|
|
||||||
targets:
|
|
||||||
- android_arm_7
|
|
||||||
- android_arm64
|
|
||||||
- android_386
|
|
||||||
- android_amd64
|
|
||||||
archives:
|
|
||||||
- &template
|
|
||||||
id: archive
|
|
||||||
builds:
|
|
||||||
- main
|
|
||||||
- android
|
|
||||||
format: tar.gz
|
|
||||||
format_overrides:
|
|
||||||
- goos: windows
|
|
||||||
format: zip
|
|
||||||
wrap_in_directory: true
|
|
||||||
files:
|
|
||||||
- LICENSE
|
|
||||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
|
||||||
- id: archive-legacy
|
|
||||||
<<: *template
|
|
||||||
builds:
|
|
||||||
- legacy
|
|
||||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
|
|
||||||
nfpms:
|
|
||||||
- id: package
|
|
||||||
package_name: sing-box
|
|
||||||
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
|
||||||
builds:
|
|
||||||
- main
|
|
||||||
homepage: https://sing-box.sagernet.org/
|
|
||||||
maintainer: nekohasekai <contact-git@sekai.icu>
|
|
||||||
description: The universal proxy platform.
|
|
||||||
license: GPLv3 or later
|
|
||||||
formats:
|
|
||||||
- deb
|
|
||||||
- rpm
|
|
||||||
- archlinux
|
|
||||||
# - apk
|
|
||||||
# - ipk
|
|
||||||
priority: extra
|
|
||||||
contents:
|
|
||||||
- src: release/config/config.json
|
|
||||||
dst: /etc/sing-box/config.json
|
|
||||||
type: config
|
|
||||||
|
|
||||||
- src: release/config/sing-box.service
|
|
||||||
dst: /usr/lib/systemd/system/sing-box.service
|
|
||||||
- src: release/config/sing-box@.service
|
|
||||||
dst: /usr/lib/systemd/system/sing-box@.service
|
|
||||||
|
|
||||||
- src: release/completions/sing-box.bash
|
|
||||||
dst: /usr/share/bash-completion/completions/sing-box.bash
|
|
||||||
- src: release/completions/sing-box.fish
|
|
||||||
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
|
|
||||||
- src: release/completions/sing-box.zsh
|
|
||||||
dst: /usr/share/zsh/site-functions/_sing-box
|
|
||||||
|
|
||||||
- src: LICENSE
|
|
||||||
dst: /usr/share/licenses/sing-box/LICENSE
|
|
||||||
deb:
|
|
||||||
signature:
|
|
||||||
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
|
||||||
fields:
|
|
||||||
Bugs: https://github.com/SagerNet/sing-box/issues
|
|
||||||
rpm:
|
|
||||||
signature:
|
|
||||||
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
|
||||||
overrides:
|
|
||||||
apk:
|
|
||||||
contents:
|
|
||||||
- src: release/config/config.json
|
|
||||||
dst: /etc/sing-box/config.json
|
|
||||||
type: config
|
|
||||||
|
|
||||||
- src: release/config/sing-box.initd
|
|
||||||
dst: /etc/init.d/sing-box
|
|
||||||
|
|
||||||
- src: release/completions/sing-box.bash
|
|
||||||
dst: /usr/share/bash-completion/completions/sing-box.bash
|
|
||||||
- src: release/completions/sing-box.fish
|
|
||||||
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
|
|
||||||
- src: release/completions/sing-box.zsh
|
|
||||||
dst: /usr/share/zsh/site-functions/_sing-box
|
|
||||||
|
|
||||||
- src: LICENSE
|
|
||||||
dst: /usr/share/licenses/sing-box/LICENSE
|
|
||||||
ipk:
|
|
||||||
contents:
|
|
||||||
- src: release/config/config.json
|
|
||||||
dst: /etc/sing-box/config.json
|
|
||||||
type: config
|
|
||||||
|
|
||||||
- src: release/config/openwrt.init
|
|
||||||
dst: /etc/init.d/sing-box
|
|
||||||
- src: release/config/openwrt.conf
|
|
||||||
dst: /etc/config/sing-box
|
|
||||||
source:
|
|
||||||
enabled: false
|
|
||||||
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
|
|
||||||
prefix_template: '{{ .ProjectName }}-{{ .Version }}/'
|
|
||||||
checksum:
|
|
||||||
disable: true
|
|
||||||
name_template: '{{ .ProjectName }}-{{ .Version }}.checksum'
|
|
||||||
signs:
|
|
||||||
- artifacts: checksum
|
|
||||||
release:
|
|
||||||
github:
|
|
||||||
owner: SagerNet
|
|
||||||
name: sing-box
|
|
||||||
draft: true
|
|
||||||
prerelease: auto
|
|
||||||
mode: replace
|
|
||||||
ids:
|
|
||||||
- archive
|
|
||||||
- package
|
|
||||||
skip_upload: true
|
|
||||||
partial:
|
|
||||||
by: target
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder
|
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
|
||||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||||
COPY . /go/src/github.com/sagernet/sing-box
|
COPY . /go/src/github.com/sagernet/sing-box
|
||||||
WORKDIR /go/src/github.com/sagernet/sing-box
|
WORKDIR /go/src/github.com/sagernet/sing-box
|
||||||
@@ -13,15 +13,13 @@ RUN set -ex \
|
|||||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||||
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||||
&& go build -v -trimpath -tags \
|
&& go build -v -trimpath -tags \
|
||||||
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \
|
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,badlinkname,tfogo_checklinkname0" \
|
||||||
-o /go/bin/sing-box \
|
-o /go/bin/sing-box \
|
||||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid= -checklinkname=0" \
|
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid= -checklinkname=0" \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
FROM --platform=$TARGETPLATFORM alpine AS dist
|
FROM --platform=$TARGETPLATFORM alpine AS dist
|
||||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||||
RUN set -ex \
|
RUN set -ex \
|
||||||
&& apk upgrade \
|
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables
|
||||||
&& apk add bash tzdata ca-certificates nftables \
|
|
||||||
&& rm -rf /var/cache/apk/*
|
|
||||||
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
||||||
ENTRYPOINT ["sing-box"]
|
ENTRYPOINT ["sing-box"]
|
||||||
|
|||||||
55
Makefile
55
Makefile
@@ -1,35 +1,33 @@
|
|||||||
NAME = sing-box
|
NAME = sing-box
|
||||||
COMMIT = $(shell git rev-parse --short HEAD)
|
COMMIT = $(shell git rev-parse --short HEAD)
|
||||||
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
|
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,badlinkname,tfogo_checklinkname0
|
||||||
TAGS_GO121 = with_ech
|
|
||||||
TAGS_GO123 = with_tailscale,badlinkname
|
|
||||||
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121),$(TAGS_GO123)
|
|
||||||
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
|
|
||||||
|
|
||||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||||
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
|
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
|
||||||
|
|
||||||
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid= -checklinkname=0"
|
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid= -checklinkname=0"
|
||||||
MAIN_PARAMS = $(PARAMS) -tags $(TAGS)
|
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
|
||||||
MAIN = ./cmd/sing-box
|
MAIN = ./cmd/sing-box
|
||||||
PREFIX ?= $(shell go env GOPATH)
|
PREFIX ?= $(shell go env GOPATH)
|
||||||
|
|
||||||
.PHONY: test release docs build
|
.PHONY: test release docs build
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
export GOTOOLCHAIN=local && \
|
||||||
go build $(MAIN_PARAMS) $(MAIN)
|
go build $(MAIN_PARAMS) $(MAIN)
|
||||||
|
|
||||||
ci_build_go120:
|
race:
|
||||||
go build $(PARAMS) $(MAIN)
|
export GOTOOLCHAIN=local && \
|
||||||
go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN)
|
go build -race $(MAIN_PARAMS) $(MAIN)
|
||||||
|
|
||||||
ci_build:
|
ci_build:
|
||||||
go build $(PARAMS) $(MAIN)
|
export GOTOOLCHAIN=local && \
|
||||||
|
go build $(PARAMS) $(MAIN) && \
|
||||||
go build $(MAIN_PARAMS) $(MAIN)
|
go build $(MAIN_PARAMS) $(MAIN)
|
||||||
|
|
||||||
generate_completions:
|
generate_completions:
|
||||||
go run -v --tags $(TAGS),generate,generate_completions $(MAIN)
|
go run -v --tags "$(TAGS),generate,generate_completions" $(MAIN)
|
||||||
|
|
||||||
install:
|
install:
|
||||||
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
|
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
|
||||||
@@ -40,7 +38,7 @@ fmt:
|
|||||||
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
|
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
|
||||||
|
|
||||||
fmt_install:
|
fmt_install:
|
||||||
go install -v mvdan.cc/gofumpt@latest
|
go install -v mvdan.cc/gofumpt@v0.8.0
|
||||||
go install -v github.com/daixiang0/gci@latest
|
go install -v github.com/daixiang0/gci@latest
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@@ -51,7 +49,7 @@ lint:
|
|||||||
GOOS=freebsd golangci-lint run ./...
|
GOOS=freebsd golangci-lint run ./...
|
||||||
|
|
||||||
lint_install:
|
lint_install:
|
||||||
go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0
|
||||||
|
|
||||||
proto:
|
proto:
|
||||||
@go run ./cmd/internal/protogen
|
@go run ./cmd/internal/protogen
|
||||||
@@ -62,6 +60,9 @@ proto_install:
|
|||||||
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||||
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||||
|
|
||||||
|
update_certificates:
|
||||||
|
go run ./cmd/internal/update_certificates
|
||||||
|
|
||||||
release:
|
release:
|
||||||
go run ./cmd/internal/build goreleaser release --clean --skip publish
|
go run ./cmd/internal/build goreleaser release --clean --skip publish
|
||||||
mkdir dist/release
|
mkdir dist/release
|
||||||
@@ -111,6 +112,16 @@ upload_ios_app_store:
|
|||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
||||||
|
|
||||||
|
export_ios_ipa:
|
||||||
|
cd ../sing-box-for-apple && \
|
||||||
|
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Export.plist -allowProvisioningUpdates -exportPath build/SFI && \
|
||||||
|
cp build/SFI/sing-box.ipa dist/SFI.ipa
|
||||||
|
|
||||||
|
upload_ios_ipa:
|
||||||
|
cd dist && \
|
||||||
|
cp SFI.ipa "SFI-${VERSION}.ipa" && \
|
||||||
|
ghr --replace --draft --prerelease "v${VERSION}" "SFI-${VERSION}.ipa"
|
||||||
|
|
||||||
release_ios: build_ios upload_ios_app_store
|
release_ios: build_ios upload_ios_app_store
|
||||||
|
|
||||||
build_macos:
|
build_macos:
|
||||||
@@ -178,6 +189,16 @@ upload_tvos_app_store:
|
|||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
||||||
|
|
||||||
|
export_tvos_ipa:
|
||||||
|
cd ../sing-box-for-apple && \
|
||||||
|
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Export.plist -allowProvisioningUpdates -exportPath build/SFT && \
|
||||||
|
cp build/SFT/sing-box.ipa dist/SFT.ipa
|
||||||
|
|
||||||
|
upload_tvos_ipa:
|
||||||
|
cd dist && \
|
||||||
|
cp SFT.ipa "SFT-${VERSION}.ipa" && \
|
||||||
|
ghr --replace --draft --prerelease "v${VERSION}" "SFT-${VERSION}.ipa"
|
||||||
|
|
||||||
release_tvos: build_tvos upload_tvos_app_store
|
release_tvos: build_tvos upload_tvos_app_store
|
||||||
|
|
||||||
update_apple_version:
|
update_apple_version:
|
||||||
@@ -228,8 +249,8 @@ lib:
|
|||||||
go run ./cmd/internal/build_libbox -target ios
|
go run ./cmd/internal/build_libbox -target ios
|
||||||
|
|
||||||
lib_install:
|
lib_install:
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.4
|
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.8
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.4
|
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.8
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
venv/bin/mkdocs serve
|
venv/bin/mkdocs serve
|
||||||
@@ -248,4 +269,4 @@ clean:
|
|||||||
update:
|
update:
|
||||||
git fetch
|
git fetch
|
||||||
git reset FETCH_HEAD --hard
|
git reset FETCH_HEAD --hard
|
||||||
git clean -fdx
|
git clean -fdx
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
> Sponsored by [Warp](https://go.warp.dev/sing-box), built for coding with multiple AI agents
|
||||||
|
|
||||||
|
<a href="https://go.warp.dev/sing-box">
|
||||||
|
<img alt="Warp sponsorship" width="400" src="https://github.com/warpdotdev/brand-assets/raw/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# sing-box
|
# sing-box
|
||||||
|
|
||||||
The universal proxy platform.
|
The universal proxy platform.
|
||||||
|
|||||||
21
adapter/certificate.go
Normal file
21
adapter/certificate.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CertificateStore interface {
|
||||||
|
LifecycleService
|
||||||
|
Pool() *x509.CertPool
|
||||||
|
}
|
||||||
|
|
||||||
|
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
||||||
|
store := service.FromContext[CertificateStore](ctx)
|
||||||
|
if store == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return store.Pool()
|
||||||
|
}
|
||||||
@@ -7,7 +7,9 @@ import (
|
|||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
@@ -31,11 +33,30 @@ type DNSClient interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DNSQueryOptions struct {
|
type DNSQueryOptions struct {
|
||||||
Transport DNSTransport
|
Transport DNSTransport
|
||||||
Strategy C.DomainStrategy
|
Strategy C.DomainStrategy
|
||||||
DisableCache bool
|
LookupStrategy C.DomainStrategy
|
||||||
RewriteTTL *uint32
|
DisableCache bool
|
||||||
ClientSubnet netip.Prefix
|
RewriteTTL *uint32
|
||||||
|
ClientSubnet netip.Prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
||||||
|
if options == nil {
|
||||||
|
return &DNSQueryOptions{}, nil
|
||||||
|
}
|
||||||
|
transportManager := service.FromContext[DNSTransportManager](ctx)
|
||||||
|
transport, loaded := transportManager.Transport(options.Server)
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("domain resolver not found: " + options.Server)
|
||||||
|
}
|
||||||
|
return &DNSQueryOptions{
|
||||||
|
Transport: transport,
|
||||||
|
Strategy: C.DomainStrategy(options.Strategy),
|
||||||
|
DisableCache: options.DisableCache,
|
||||||
|
RewriteTTL: options.RewriteTTL,
|
||||||
|
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type RDRCStore interface {
|
type RDRCStore interface {
|
||||||
@@ -45,10 +66,10 @@ type RDRCStore interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DNSTransport interface {
|
type DNSTransport interface {
|
||||||
|
Lifecycle
|
||||||
Type() string
|
Type() string
|
||||||
Tag() string
|
Tag() string
|
||||||
Dependencies() []string
|
Dependencies() []string
|
||||||
Reset()
|
|
||||||
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/urltest"
|
|
||||||
"github.com/sagernet/sing/common/varbin"
|
"github.com/sagernet/sing/common/varbin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,7 +14,20 @@ type ClashServer interface {
|
|||||||
ConnectionTracker
|
ConnectionTracker
|
||||||
Mode() string
|
Mode() string
|
||||||
ModeList() []string
|
ModeList() []string
|
||||||
HistoryStorage() *urltest.HistoryStorage
|
HistoryStorage() URLTestHistoryStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
type URLTestHistory struct {
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
Delay uint16 `json:"delay"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type URLTestHistoryStorage interface {
|
||||||
|
SetHook(hook chan<- struct{})
|
||||||
|
LoadURLTestHistory(tag string) *URLTestHistory
|
||||||
|
DeleteURLTestHistory(tag string)
|
||||||
|
StoreURLTestHistory(tag string, history *URLTestHistory)
|
||||||
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type V2RayServer interface {
|
type V2RayServer interface {
|
||||||
@@ -38,17 +50,17 @@ type CacheFile interface {
|
|||||||
StoreSelected(group string, selected string) error
|
StoreSelected(group string, selected string) error
|
||||||
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
||||||
StoreGroupExpand(group string, expand bool) error
|
StoreGroupExpand(group string, expand bool) error
|
||||||
LoadRuleSet(tag string) *SavedRuleSet
|
LoadRuleSet(tag string) *SavedBinary
|
||||||
SaveRuleSet(tag string, set *SavedRuleSet) error
|
SaveRuleSet(tag string, set *SavedBinary) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type SavedRuleSet struct {
|
type SavedBinary struct {
|
||||||
Content []byte
|
Content []byte
|
||||||
LastUpdated time.Time
|
LastUpdated time.Time
|
||||||
LastEtag string
|
LastEtag string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
|
func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
|
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -69,7 +81,7 @@ func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
|
|||||||
return buffer.Bytes(), nil
|
return buffer.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
|
func (s *SavedBinary) UnmarshalBinary(data []byte) error {
|
||||||
reader := bytes.NewReader(data)
|
reader := bytes.NewReader(data)
|
||||||
var version uint8
|
var version uint8
|
||||||
err := binary.Read(reader, binary.BigEndian, &version)
|
err := binary.Read(reader, binary.BigEndian, &version)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type FakeIPStore interface {
|
type FakeIPStore interface {
|
||||||
Service
|
SimpleLifecycle
|
||||||
Contains(address netip.Addr) bool
|
Contains(address netip.Addr) bool
|
||||||
Create(domain string, isIPv6 bool) (netip.Addr, error)
|
Create(domain string, isIPv6 bool) (netip.Addr, error)
|
||||||
Lookup(address netip.Addr) (string, bool)
|
Lookup(address netip.Addr) (string, bool)
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ type InboundContext struct {
|
|||||||
Domain string
|
Domain string
|
||||||
Client string
|
Client string
|
||||||
SniffContext any
|
SniffContext any
|
||||||
|
SnifferNames []string
|
||||||
|
SniffError error
|
||||||
|
|
||||||
// cache
|
// cache
|
||||||
|
|
||||||
@@ -71,6 +73,9 @@ type InboundContext struct {
|
|||||||
UDPDisableDomainUnmapping bool
|
UDPDisableDomainUnmapping bool
|
||||||
UDPConnect bool
|
UDPConnect bool
|
||||||
UDPTimeout time.Duration
|
UDPTimeout time.Duration
|
||||||
|
TLSFragment bool
|
||||||
|
TLSFragmentFallbackDelay time.Duration
|
||||||
|
TLSRecordFragment bool
|
||||||
|
|
||||||
NetworkStrategy *C.NetworkStrategy
|
NetworkStrategy *C.NetworkStrategy
|
||||||
NetworkType []C.InterfaceType
|
NetworkType []C.InterfaceType
|
||||||
@@ -131,8 +136,7 @@ func ExtendContext(ctx context.Context) (context.Context, *InboundContext) {
|
|||||||
|
|
||||||
func OverrideContext(ctx context.Context) context.Context {
|
func OverrideContext(ctx context.Context) context.Context {
|
||||||
if metadata := ContextFrom(ctx); metadata != nil {
|
if metadata := ContextFrom(ctx); metadata != nil {
|
||||||
var newMetadata InboundContext
|
newMetadata := *metadata
|
||||||
newMetadata = *metadata
|
|
||||||
return WithContext(ctx, &newMetadata)
|
return WithContext(ctx, &newMetadata)
|
||||||
}
|
}
|
||||||
return ctx
|
return ctx
|
||||||
|
|||||||
@@ -37,13 +37,14 @@ func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endp
|
|||||||
|
|
||||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||||
m.access.Lock()
|
m.access.Lock()
|
||||||
defer m.access.Unlock()
|
|
||||||
if m.started && m.stage >= stage {
|
if m.started && m.stage >= stage {
|
||||||
panic("already started")
|
panic("already started")
|
||||||
}
|
}
|
||||||
m.started = true
|
m.started = true
|
||||||
m.stage = stage
|
m.stage = stage
|
||||||
for _, inbound := range m.inbounds {
|
inbounds := m.inbounds
|
||||||
|
m.access.Unlock()
|
||||||
|
for _, inbound := range inbounds {
|
||||||
err := adapter.LegacyStart(inbound, stage)
|
err := adapter.LegacyStart(inbound, stage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")
|
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ package adapter
|
|||||||
|
|
||||||
import E "github.com/sagernet/sing/common/exceptions"
|
import E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
type SimpleLifecycle interface {
|
||||||
|
Start() error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
type StartStage uint8
|
type StartStage uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ func LegacyStart(starter any, stage StartStage) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type lifecycleServiceWrapper struct {
|
type lifecycleServiceWrapper struct {
|
||||||
Service
|
SimpleLifecycle
|
||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLifecycleService(service Service, name string) LifecycleService {
|
func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService {
|
||||||
return &lifecycleServiceWrapper{
|
return &lifecycleServiceWrapper{
|
||||||
Service: service,
|
SimpleLifecycle: service,
|
||||||
name: name,
|
name: name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,9 +44,9 @@ func (l *lifecycleServiceWrapper) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
|
func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
|
||||||
return LegacyStart(l.Service, stage)
|
return LegacyStart(l.SimpleLifecycle, stage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *lifecycleServiceWrapper) Close() error {
|
func (l *lifecycleServiceWrapper) Close() error {
|
||||||
return l.Service.Close()
|
return l.SimpleLifecycle.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
type NetworkManager interface {
|
type NetworkManager interface {
|
||||||
Lifecycle
|
Lifecycle
|
||||||
|
Initialize(ruleSets []RuleSet)
|
||||||
InterfaceFinder() control.InterfaceFinder
|
InterfaceFinder() control.InterfaceFinder
|
||||||
UpdateInterfaces() error
|
UpdateInterfaces() error
|
||||||
DefaultNetworkInterface() *NetworkInterface
|
DefaultNetworkInterface() *NetworkInterface
|
||||||
@@ -20,20 +21,25 @@ type NetworkManager interface {
|
|||||||
DefaultOptions() NetworkOptions
|
DefaultOptions() NetworkOptions
|
||||||
RegisterAutoRedirectOutputMark(mark uint32) error
|
RegisterAutoRedirectOutputMark(mark uint32) error
|
||||||
AutoRedirectOutputMark() uint32
|
AutoRedirectOutputMark() uint32
|
||||||
|
AutoRedirectOutputMarkFunc() control.Func
|
||||||
NetworkMonitor() tun.NetworkUpdateMonitor
|
NetworkMonitor() tun.NetworkUpdateMonitor
|
||||||
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
||||||
PackageManager() tun.PackageManager
|
PackageManager() tun.PackageManager
|
||||||
|
NeedWIFIState() bool
|
||||||
WIFIState() WIFIState
|
WIFIState() WIFIState
|
||||||
|
UpdateWIFIState()
|
||||||
ResetNetwork()
|
ResetNetwork()
|
||||||
}
|
}
|
||||||
|
|
||||||
type NetworkOptions struct {
|
type NetworkOptions struct {
|
||||||
NetworkStrategy *C.NetworkStrategy
|
BindInterface string
|
||||||
NetworkType []C.InterfaceType
|
RoutingMark uint32
|
||||||
FallbackNetworkType []C.InterfaceType
|
DomainResolver string
|
||||||
FallbackDelay time.Duration
|
DomainResolveOptions DNSQueryOptions
|
||||||
BindInterface string
|
NetworkStrategy *C.NetworkStrategy
|
||||||
RoutingMark uint32
|
NetworkType []C.InterfaceType
|
||||||
|
FallbackNetworkType []C.InterfaceType
|
||||||
|
FallbackDelay time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterfaceUpdateListener interface {
|
type InterfaceUpdateListener interface {
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-tun"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +21,17 @@ type Outbound interface {
|
|||||||
N.Dialer
|
N.Dialer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OutboundWithPreferredRoutes interface {
|
||||||
|
Outbound
|
||||||
|
PreferredDomain(domain string) bool
|
||||||
|
PreferredAddress(address netip.Addr) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type DirectRouteOutbound interface {
|
||||||
|
Outbound
|
||||||
|
NewDirectRouteConnection(metadata InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
|
||||||
|
}
|
||||||
|
|
||||||
type OutboundRegistry interface {
|
type OutboundRegistry interface {
|
||||||
option.OutboundOptionsRegistry
|
option.OutboundOptionsRegistry
|
||||||
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
|
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type Manager struct {
|
|||||||
outboundByTag map[string]adapter.Outbound
|
outboundByTag map[string]adapter.Outbound
|
||||||
dependByTag map[string][]string
|
dependByTag map[string][]string
|
||||||
defaultOutbound adapter.Outbound
|
defaultOutbound adapter.Outbound
|
||||||
defaultOutboundFallback adapter.Outbound
|
defaultOutboundFallback func() (adapter.Outbound, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
|
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
|
||||||
@@ -44,7 +44,7 @@ func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Initialize(defaultOutboundFallback adapter.Outbound) {
|
func (m *Manager) Initialize(defaultOutboundFallback func() (adapter.Outbound, error)) {
|
||||||
m.defaultOutboundFallback = defaultOutboundFallback
|
m.defaultOutboundFallback = defaultOutboundFallback
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,18 +55,31 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
|||||||
}
|
}
|
||||||
m.started = true
|
m.started = true
|
||||||
m.stage = stage
|
m.stage = stage
|
||||||
outbounds := m.outbounds
|
|
||||||
m.access.Unlock()
|
|
||||||
if stage == adapter.StartStateStart {
|
if stage == adapter.StartStateStart {
|
||||||
if m.defaultTag != "" && m.defaultOutbound == nil {
|
if m.defaultTag != "" && m.defaultOutbound == nil {
|
||||||
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
|
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
|
||||||
if !loaded {
|
if !loaded {
|
||||||
|
m.access.Unlock()
|
||||||
return E.New("default outbound not found: ", m.defaultTag)
|
return E.New("default outbound not found: ", m.defaultTag)
|
||||||
}
|
}
|
||||||
m.defaultOutbound = defaultEndpoint
|
m.defaultOutbound = defaultEndpoint
|
||||||
}
|
}
|
||||||
|
if m.defaultOutbound == nil {
|
||||||
|
directOutbound, err := m.defaultOutboundFallback()
|
||||||
|
if err != nil {
|
||||||
|
m.access.Unlock()
|
||||||
|
return E.Cause(err, "create direct outbound for fallback")
|
||||||
|
}
|
||||||
|
m.outbounds = append(m.outbounds, directOutbound)
|
||||||
|
m.outboundByTag[directOutbound.Tag()] = directOutbound
|
||||||
|
m.defaultOutbound = directOutbound
|
||||||
|
}
|
||||||
|
outbounds := m.outbounds
|
||||||
|
m.access.Unlock()
|
||||||
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
|
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
|
||||||
} else {
|
} else {
|
||||||
|
outbounds := m.outbounds
|
||||||
|
m.access.Unlock()
|
||||||
for _, outbound := range outbounds {
|
for _, outbound := range outbounds {
|
||||||
err := adapter.LegacyStart(outbound, stage)
|
err := adapter.LegacyStart(outbound, stage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -187,11 +200,7 @@ func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
|
|||||||
func (m *Manager) Default() adapter.Outbound {
|
func (m *Manager) Default() adapter.Outbound {
|
||||||
m.access.RLock()
|
m.access.RLock()
|
||||||
defer m.access.RUnlock()
|
defer m.access.RUnlock()
|
||||||
if m.defaultOutbound != nil {
|
return m.defaultOutbound
|
||||||
return m.defaultOutbound
|
|
||||||
} else {
|
|
||||||
return m.defaultOutboundFallback
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Remove(tag string) error {
|
func (m *Manager) Remove(tag string) error {
|
||||||
@@ -246,8 +255,6 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
m.access.Lock()
|
|
||||||
defer m.access.Unlock()
|
|
||||||
if m.started {
|
if m.started {
|
||||||
for _, stage := range adapter.ListStartStages {
|
for _, stage := range adapter.ListStartStages {
|
||||||
err = adapter.LegacyStart(outbound, stage)
|
err = adapter.LegacyStart(outbound, stage)
|
||||||
@@ -256,6 +263,8 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
|
if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
|
||||||
if m.started {
|
if m.started {
|
||||||
err = common.Close(existsOutbound)
|
err = common.Close(existsOutbound)
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-tun"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
|
||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
@@ -17,12 +21,11 @@ import (
|
|||||||
type Router interface {
|
type Router interface {
|
||||||
Lifecycle
|
Lifecycle
|
||||||
ConnectionRouter
|
ConnectionRouter
|
||||||
PreMatch(metadata InboundContext) error
|
PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
|
||||||
ConnectionRouterEx
|
ConnectionRouterEx
|
||||||
RuleSet(tag string) (RuleSet, bool)
|
RuleSet(tag string) (RuleSet, bool)
|
||||||
NeedWIFIState() bool
|
|
||||||
Rules() []Rule
|
Rules() []Rule
|
||||||
SetTracker(tracker ConnectionTracker)
|
AppendTracker(tracker ConnectionTracker)
|
||||||
ResetNetwork()
|
ResetNetwork()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,12 +69,14 @@ type RuleSetMetadata struct {
|
|||||||
ContainsIPCIDRRule bool
|
ContainsIPCIDRRule bool
|
||||||
}
|
}
|
||||||
type HTTPStartContext struct {
|
type HTTPStartContext struct {
|
||||||
|
ctx context.Context
|
||||||
access sync.Mutex
|
access sync.Mutex
|
||||||
httpClientCache map[string]*http.Client
|
httpClientCache map[string]*http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPStartContext() *HTTPStartContext {
|
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
|
||||||
return &HTTPStartContext{
|
return &HTTPStartContext{
|
||||||
|
ctx: ctx,
|
||||||
httpClientCache: make(map[string]*http.Client),
|
httpClientCache: make(map[string]*http.Client),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,6 +94,10 @@ func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Clie
|
|||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||||
},
|
},
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
Time: ntp.TimeFuncFromContext(c.ctx),
|
||||||
|
RootCAs: RootPoolFromContext(c.ctx),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
c.httpClientCache[detour] = httpClient
|
c.httpClientCache[detour] = httpClient
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ type HeadlessRule interface {
|
|||||||
|
|
||||||
type Rule interface {
|
type Rule interface {
|
||||||
HeadlessRule
|
HeadlessRule
|
||||||
Service
|
SimpleLifecycle
|
||||||
Type() string
|
Type() string
|
||||||
Action() RuleAction
|
Action() RuleAction
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,27 @@
|
|||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
Start() error
|
Lifecycle
|
||||||
Close() error
|
Type() string
|
||||||
|
Tag() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceRegistry interface {
|
||||||
|
option.ServiceOptionsRegistry
|
||||||
|
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) (Service, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceManager interface {
|
||||||
|
Lifecycle
|
||||||
|
Services() []Service
|
||||||
|
Get(tag string) (Service, bool)
|
||||||
|
Remove(tag string) error
|
||||||
|
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error
|
||||||
}
|
}
|
||||||
|
|||||||
21
adapter/service/adapter.go
Normal file
21
adapter/service/adapter.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
type Adapter struct {
|
||||||
|
serviceType string
|
||||||
|
serviceTag string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdapter(serviceType string, serviceTag string) Adapter {
|
||||||
|
return Adapter{
|
||||||
|
serviceType: serviceType,
|
||||||
|
serviceTag: serviceTag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Type() string {
|
||||||
|
return a.serviceType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Tag() string {
|
||||||
|
return a.serviceTag
|
||||||
|
}
|
||||||
144
adapter/service/manager.go
Normal file
144
adapter/service/manager.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.ServiceManager = (*Manager)(nil)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
logger log.ContextLogger
|
||||||
|
registry adapter.ServiceRegistry
|
||||||
|
access sync.Mutex
|
||||||
|
started bool
|
||||||
|
stage adapter.StartStage
|
||||||
|
services []adapter.Service
|
||||||
|
serviceByTag map[string]adapter.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager {
|
||||||
|
return &Manager{
|
||||||
|
logger: logger,
|
||||||
|
registry: registry,
|
||||||
|
serviceByTag: make(map[string]adapter.Service),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||||
|
m.access.Lock()
|
||||||
|
if m.started && m.stage >= stage {
|
||||||
|
panic("already started")
|
||||||
|
}
|
||||||
|
m.started = true
|
||||||
|
m.stage = stage
|
||||||
|
services := m.services
|
||||||
|
m.access.Unlock()
|
||||||
|
for _, service := range services {
|
||||||
|
err := adapter.LegacyStart(service, stage)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Close() error {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
if !m.started {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.started = false
|
||||||
|
services := m.services
|
||||||
|
m.services = nil
|
||||||
|
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||||
|
var err error
|
||||||
|
for _, service := range services {
|
||||||
|
monitor.Start("close service/", service.Type(), "[", service.Tag(), "]")
|
||||||
|
err = E.Append(err, service.Close(), func(err error) error {
|
||||||
|
return E.Cause(err, "close service/", service.Type(), "[", service.Tag(), "]")
|
||||||
|
})
|
||||||
|
monitor.Finish()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Services() []adapter.Service {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
return m.services
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Get(tag string) (adapter.Service, bool) {
|
||||||
|
m.access.Lock()
|
||||||
|
service, found := m.serviceByTag[tag]
|
||||||
|
m.access.Unlock()
|
||||||
|
return service, found
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Remove(tag string) error {
|
||||||
|
m.access.Lock()
|
||||||
|
service, found := m.serviceByTag[tag]
|
||||||
|
if !found {
|
||||||
|
m.access.Unlock()
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
delete(m.serviceByTag, tag)
|
||||||
|
index := common.Index(m.services, func(it adapter.Service) bool {
|
||||||
|
return it == service
|
||||||
|
})
|
||||||
|
if index == -1 {
|
||||||
|
panic("invalid service index")
|
||||||
|
}
|
||||||
|
m.services = append(m.services[:index], m.services[index+1:]...)
|
||||||
|
started := m.started
|
||||||
|
m.access.Unlock()
|
||||||
|
if started {
|
||||||
|
return service.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error {
|
||||||
|
service, err := m.registry.Create(ctx, logger, tag, serviceType, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
if m.started {
|
||||||
|
for _, stage := range adapter.ListStartStages {
|
||||||
|
err = adapter.LegacyStart(service, stage)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if existsService, loaded := m.serviceByTag[tag]; loaded {
|
||||||
|
if m.started {
|
||||||
|
err = existsService.Close()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "close service/", existsService.Type(), "[", existsService.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existsIndex := common.Index(m.services, func(it adapter.Service) bool {
|
||||||
|
return it == existsService
|
||||||
|
})
|
||||||
|
if existsIndex == -1 {
|
||||||
|
panic("invalid service index")
|
||||||
|
}
|
||||||
|
m.services = append(m.services[:existsIndex], m.services[existsIndex+1:]...)
|
||||||
|
}
|
||||||
|
m.services = append(m.services, service)
|
||||||
|
m.serviceByTag[tag] = service
|
||||||
|
return nil
|
||||||
|
}
|
||||||
72
adapter/service/registry.go
Normal file
72
adapter/service/registry.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.Service, error)
|
||||||
|
|
||||||
|
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
|
||||||
|
registry.register(outboundType, func() any {
|
||||||
|
return new(Options)
|
||||||
|
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.Service, error) {
|
||||||
|
var options *Options
|
||||||
|
if rawOptions != nil {
|
||||||
|
options = rawOptions.(*Options)
|
||||||
|
}
|
||||||
|
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ adapter.ServiceRegistry = (*Registry)(nil)
|
||||||
|
|
||||||
|
type (
|
||||||
|
optionsConstructorFunc func() any
|
||||||
|
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.Service, error)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Registry struct {
|
||||||
|
access sync.Mutex
|
||||||
|
optionsType map[string]optionsConstructorFunc
|
||||||
|
constructor map[string]constructorFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistry() *Registry {
|
||||||
|
return &Registry{
|
||||||
|
optionsType: make(map[string]optionsConstructorFunc),
|
||||||
|
constructor: make(map[string]constructorFunc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
optionsConstructor, loaded := m.optionsType[outboundType]
|
||||||
|
if !loaded {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return optionsConstructor(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Service, error) {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
constructor, loaded := m.constructor[outboundType]
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("outbound type not found: " + outboundType)
|
||||||
|
}
|
||||||
|
return constructor(ctx, logger, tag, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
m.optionsType[outboundType] = optionsConstructor
|
||||||
|
m.constructor[outboundType] = constructor
|
||||||
|
}
|
||||||
18
adapter/ssm.go
Normal file
18
adapter/ssm.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ManagedSSMServer interface {
|
||||||
|
Inbound
|
||||||
|
SetTracker(tracker SSMTracker)
|
||||||
|
UpdateUsers(users []string, uPSKs []string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type SSMTracker interface {
|
||||||
|
TrackConnection(conn net.Conn, metadata InboundContext) net.Conn
|
||||||
|
TrackPacketConnection(conn N.PacketConn, metadata InboundContext) N.PacketConn
|
||||||
|
}
|
||||||
@@ -3,6 +3,6 @@ package adapter
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type TimeService interface {
|
type TimeService interface {
|
||||||
Service
|
SimpleLifecycle
|
||||||
TimeFunc() func() time.Time
|
TimeFunc() func() time.Time
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func NewUpstreamContextHandlerEx(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
myMetadata := ContextFrom(ctx)
|
_, myMetadata := ExtendContext(ctx)
|
||||||
if source.IsValid() {
|
if source.IsValid() {
|
||||||
myMetadata.Source = source
|
myMetadata.Source = source
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
myMetadata := ContextFrom(ctx)
|
_, myMetadata := ExtendContext(ctx)
|
||||||
if source.IsValid() {
|
if source.IsValid() {
|
||||||
myMetadata.Source = source
|
myMetadata.Source = source
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ type routeContextHandlerWrapperEx struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
metadata := ContextFrom(ctx)
|
_, metadata := ExtendContext(ctx)
|
||||||
if source.IsValid() {
|
if source.IsValid() {
|
||||||
metadata.Source = source
|
metadata.Source = source
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
metadata := ContextFrom(ctx)
|
_, metadata := ExtendContext(ctx)
|
||||||
if source.IsValid() {
|
if source.IsValid() {
|
||||||
metadata.Source = source
|
metadata.Source = source
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
|
|||||||
// Deprecated: removed
|
// Deprecated: removed
|
||||||
func UpstreamMetadata(metadata InboundContext) M.Metadata {
|
func UpstreamMetadata(metadata InboundContext) M.Metadata {
|
||||||
return M.Metadata{
|
return M.Metadata{
|
||||||
Source: metadata.Source,
|
Source: metadata.Source.Unwrap(),
|
||||||
Destination: metadata.Destination,
|
Destination: metadata.Destination.Unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
171
box.go
171
box.go
@@ -12,6 +12,8 @@ import (
|
|||||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||||
"github.com/sagernet/sing-box/adapter/inbound"
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
"github.com/sagernet/sing-box/adapter/outbound"
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
|
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||||
|
"github.com/sagernet/sing-box/common/certificate"
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
@@ -33,22 +35,23 @@ import (
|
|||||||
"github.com/sagernet/sing/service/pause"
|
"github.com/sagernet/sing/service/pause"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.Service = (*Box)(nil)
|
var _ adapter.SimpleLifecycle = (*Box)(nil)
|
||||||
|
|
||||||
type Box struct {
|
type Box struct {
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
logFactory log.Factory
|
logFactory log.Factory
|
||||||
logger log.ContextLogger
|
logger log.ContextLogger
|
||||||
network *route.NetworkManager
|
network *route.NetworkManager
|
||||||
endpoint *endpoint.Manager
|
endpoint *endpoint.Manager
|
||||||
inbound *inbound.Manager
|
inbound *inbound.Manager
|
||||||
outbound *outbound.Manager
|
outbound *outbound.Manager
|
||||||
dnsTransport *dns.TransportManager
|
service *boxService.Manager
|
||||||
dnsRouter *dns.Router
|
dnsTransport *dns.TransportManager
|
||||||
connection *route.ConnectionManager
|
dnsRouter *dns.Router
|
||||||
router *route.Router
|
connection *route.ConnectionManager
|
||||||
services []adapter.LifecycleService
|
router *route.Router
|
||||||
done chan struct{}
|
internalService []adapter.LifecycleService
|
||||||
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
@@ -63,6 +66,7 @@ func Context(
|
|||||||
outboundRegistry adapter.OutboundRegistry,
|
outboundRegistry adapter.OutboundRegistry,
|
||||||
endpointRegistry adapter.EndpointRegistry,
|
endpointRegistry adapter.EndpointRegistry,
|
||||||
dnsTransportRegistry adapter.DNSTransportRegistry,
|
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||||
|
serviceRegistry adapter.ServiceRegistry,
|
||||||
) context.Context {
|
) context.Context {
|
||||||
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
||||||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
||||||
@@ -83,6 +87,10 @@ func Context(
|
|||||||
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
|
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
|
||||||
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
|
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
|
||||||
}
|
}
|
||||||
|
if service.FromContext[adapter.ServiceRegistry](ctx) == nil {
|
||||||
|
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
|
||||||
|
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
|
||||||
|
}
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +106,7 @@ func New(options Options) (*Box, error) {
|
|||||||
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
||||||
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||||
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
||||||
|
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
|
||||||
|
|
||||||
if endpointRegistry == nil {
|
if endpointRegistry == nil {
|
||||||
return nil, E.New("missing endpoint registry in context")
|
return nil, E.New("missing endpoint registry in context")
|
||||||
@@ -108,6 +117,12 @@ func New(options Options) (*Box, error) {
|
|||||||
if outboundRegistry == nil {
|
if outboundRegistry == nil {
|
||||||
return nil, E.New("missing outbound registry in context")
|
return nil, E.New("missing outbound registry in context")
|
||||||
}
|
}
|
||||||
|
if dnsTransportRegistry == nil {
|
||||||
|
return nil, E.New("missing DNS transport registry in context")
|
||||||
|
}
|
||||||
|
if serviceRegistry == nil {
|
||||||
|
return nil, E.New("missing service registry in context")
|
||||||
|
}
|
||||||
|
|
||||||
ctx = pause.WithDefaultManager(ctx)
|
ctx = pause.WithDefaultManager(ctx)
|
||||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||||
@@ -141,19 +156,35 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "create log factory")
|
return nil, E.Cause(err, "create log factory")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var internalServices []adapter.LifecycleService
|
||||||
|
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||||
|
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||||
|
len(certificateOptions.Certificate) > 0 ||
|
||||||
|
len(certificateOptions.CertificatePath) > 0 ||
|
||||||
|
len(certificateOptions.CertificateDirectoryPath) > 0 {
|
||||||
|
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||||
|
internalServices = append(internalServices, certificateStore)
|
||||||
|
}
|
||||||
|
|
||||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||||
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
||||||
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
||||||
|
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
|
||||||
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||||
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||||
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||||
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||||
|
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
|
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize network manager")
|
return nil, E.Cause(err, "initialize network manager")
|
||||||
}
|
}
|
||||||
@@ -187,7 +218,7 @@ func New(options Options) (*Box, error) {
|
|||||||
transportOptions.Options,
|
transportOptions.Options,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
return nil, E.Cause(err, "initialize DNS server[", i, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = dnsRouter.Initialize(dnsOptions.Rules)
|
err = dnsRouter.Initialize(dnsOptions.Rules)
|
||||||
@@ -201,8 +232,15 @@ func New(options Options) (*Box, error) {
|
|||||||
} else {
|
} else {
|
||||||
tag = F.ToString(i)
|
tag = F.ToString(i)
|
||||||
}
|
}
|
||||||
|
endpointCtx := ctx
|
||||||
|
if tag != "" {
|
||||||
|
// TODO: remove this
|
||||||
|
endpointCtx = adapter.WithContext(endpointCtx, &adapter.InboundContext{
|
||||||
|
Outbound: tag,
|
||||||
|
})
|
||||||
|
}
|
||||||
err = endpointManager.Create(
|
err = endpointManager.Create(
|
||||||
ctx,
|
endpointCtx,
|
||||||
router,
|
router,
|
||||||
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
|
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
|
||||||
tag,
|
tag,
|
||||||
@@ -210,7 +248,7 @@ func New(options Options) (*Box, error) {
|
|||||||
endpointOptions.Options,
|
endpointOptions.Options,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
return nil, E.Cause(err, "initialize endpoint[", i, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, inboundOptions := range options.Inbounds {
|
for i, inboundOptions := range options.Inbounds {
|
||||||
@@ -258,33 +296,51 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
outboundManager.Initialize(common.Must1(
|
for i, serviceOptions := range options.Services {
|
||||||
direct.NewOutbound(
|
var tag string
|
||||||
|
if serviceOptions.Tag != "" {
|
||||||
|
tag = serviceOptions.Tag
|
||||||
|
} else {
|
||||||
|
tag = F.ToString(i)
|
||||||
|
}
|
||||||
|
err = serviceManager.Create(
|
||||||
|
ctx,
|
||||||
|
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
||||||
|
tag,
|
||||||
|
serviceOptions.Type,
|
||||||
|
serviceOptions.Options,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize service[", i, "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outboundManager.Initialize(func() (adapter.Outbound, error) {
|
||||||
|
return direct.NewOutbound(
|
||||||
ctx,
|
ctx,
|
||||||
router,
|
router,
|
||||||
logFactory.NewLogger("outbound/direct"),
|
logFactory.NewLogger("outbound/direct"),
|
||||||
"direct",
|
"direct",
|
||||||
option.DirectOutboundOptions{},
|
option.DirectOutboundOptions{},
|
||||||
),
|
)
|
||||||
))
|
})
|
||||||
dnsTransportManager.Initialize(common.Must1(
|
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
|
||||||
local.NewTransport(
|
return local.NewTransport(
|
||||||
ctx,
|
ctx,
|
||||||
logFactory.NewLogger("dns/local"),
|
logFactory.NewLogger("dns/local"),
|
||||||
"local",
|
"local",
|
||||||
option.LocalDNSServerOptions{},
|
option.LocalDNSServerOptions{},
|
||||||
)))
|
)
|
||||||
|
})
|
||||||
if platformInterface != nil {
|
if platformInterface != nil {
|
||||||
err = platformInterface.Initialize(networkManager)
|
err = platformInterface.Initialize(networkManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize platform interface")
|
return nil, E.Cause(err, "initialize platform interface")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var services []adapter.LifecycleService
|
|
||||||
if needCacheFile {
|
if needCacheFile {
|
||||||
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||||
services = append(services, cacheFile)
|
internalServices = append(internalServices, cacheFile)
|
||||||
}
|
}
|
||||||
if needClashAPI {
|
if needClashAPI {
|
||||||
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
||||||
@@ -293,9 +349,9 @@ func New(options Options) (*Box, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "create clash-server")
|
return nil, E.Cause(err, "create clash-server")
|
||||||
}
|
}
|
||||||
router.SetTracker(clashServer)
|
router.AppendTracker(clashServer)
|
||||||
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
||||||
services = append(services, clashServer)
|
internalServices = append(internalServices, clashServer)
|
||||||
}
|
}
|
||||||
if needV2RayAPI {
|
if needV2RayAPI {
|
||||||
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
|
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
|
||||||
@@ -303,8 +359,8 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "create v2ray-server")
|
return nil, E.Cause(err, "create v2ray-server")
|
||||||
}
|
}
|
||||||
if v2rayServer.StatsService() != nil {
|
if v2rayServer.StatsService() != nil {
|
||||||
router.SetTracker(v2rayServer.StatsService())
|
router.AppendTracker(v2rayServer.StatsService())
|
||||||
services = append(services, v2rayServer)
|
internalServices = append(internalServices, v2rayServer)
|
||||||
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,22 +378,23 @@ func New(options Options) (*Box, error) {
|
|||||||
WriteToSystem: ntpOptions.WriteToSystem,
|
WriteToSystem: ntpOptions.WriteToSystem,
|
||||||
})
|
})
|
||||||
timeService.TimeService = ntpService
|
timeService.TimeService = ntpService
|
||||||
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
|
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||||
}
|
}
|
||||||
return &Box{
|
return &Box{
|
||||||
network: networkManager,
|
network: networkManager,
|
||||||
endpoint: endpointManager,
|
endpoint: endpointManager,
|
||||||
inbound: inboundManager,
|
inbound: inboundManager,
|
||||||
outbound: outboundManager,
|
outbound: outboundManager,
|
||||||
dnsTransport: dnsTransportManager,
|
dnsTransport: dnsTransportManager,
|
||||||
dnsRouter: dnsRouter,
|
service: serviceManager,
|
||||||
connection: connectionManager,
|
dnsRouter: dnsRouter,
|
||||||
router: router,
|
connection: connectionManager,
|
||||||
createdAt: createdAt,
|
router: router,
|
||||||
logFactory: logFactory,
|
createdAt: createdAt,
|
||||||
logger: logFactory.Logger(),
|
logFactory: logFactory,
|
||||||
services: services,
|
logger: logFactory.Logger(),
|
||||||
done: make(chan struct{}),
|
internalService: internalServices,
|
||||||
|
done: make(chan struct{}),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,11 +444,11 @@ func (s *Box) preStart() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "start logger")
|
return E.Cause(err, "start logger")
|
||||||
}
|
}
|
||||||
err = adapter.StartNamed(adapter.StartStateInitialize, s.services) // cache-file clash-api v2ray-api
|
err = adapter.StartNamed(adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -407,31 +464,27 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.StartNamed(adapter.StartStateStart, s.services)
|
err = adapter.StartNamed(adapter.StartStateStart, s.internalService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = s.inbound.Start(adapter.StartStateStart)
|
err = adapter.Start(adapter.StartStateStart, s.inbound, s.endpoint, s.service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateStart, s.endpoint)
|
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint)
|
err = adapter.StartNamed(adapter.StartStatePostStart, s.internalService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.StartNamed(adapter.StartStatePostStart, s.services)
|
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
err = adapter.StartNamed(adapter.StartStateStarted, s.internalService)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = adapter.StartNamed(adapter.StartStateStarted, s.services)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -446,9 +499,9 @@ func (s *Box) Close() error {
|
|||||||
close(s.done)
|
close(s.done)
|
||||||
}
|
}
|
||||||
err := common.Close(
|
err := common.Close(
|
||||||
s.inbound, s.outbound, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
s.service, s.endpoint, s.inbound, s.outbound, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
||||||
)
|
)
|
||||||
for _, lifecycleService := range s.services {
|
for _, lifecycleService := range s.internalService {
|
||||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||||
return E.Cause(err, "close ", lifecycleService.Name())
|
return E.Cause(err, "close ", lifecycleService.Name())
|
||||||
})
|
})
|
||||||
|
|||||||
Submodule clients/android updated: b17fb6d857...3b2c371905
Submodule clients/apple updated: 64a4614aca...84d8cf1757
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/asc-go/asc"
|
"github.com/sagernet/asc-go/asc"
|
||||||
@@ -104,7 +105,7 @@ func publishTestflight(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tag := tagVersion.VersionString()
|
tag := tagVersion.VersionString()
|
||||||
client := createClient(10 * time.Minute)
|
client := createClient(20 * time.Minute)
|
||||||
|
|
||||||
log.Info(tag, " list build IDs")
|
log.Info(tag, " list build IDs")
|
||||||
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
|
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
|
||||||
@@ -133,6 +134,7 @@ func publishTestflight(ctx context.Context) error {
|
|||||||
asc.PlatformTVOS,
|
asc.PlatformTVOS,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
waitingForProcess := false
|
||||||
for _, platform := range platforms {
|
for _, platform := range platforms {
|
||||||
log.Info(string(platform), " list builds")
|
log.Info(string(platform), " list builds")
|
||||||
for {
|
for {
|
||||||
@@ -144,12 +146,13 @@ func publishTestflight(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
build := builds.Data[0]
|
build := builds.Data[0]
|
||||||
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 5*time.Minute {
|
if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) {
|
||||||
log.Info(string(platform), " ", tag, " waiting for process")
|
log.Info(string(platform), " ", tag, " waiting for process")
|
||||||
time.Sleep(15 * time.Second)
|
time.Sleep(15 * time.Second)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if *build.Attributes.ProcessingState != "VALID" {
|
if *build.Attributes.ProcessingState != "VALID" {
|
||||||
|
waitingForProcess = true
|
||||||
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
|
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
|
||||||
time.Sleep(15 * time.Second)
|
time.Sleep(15 * time.Second)
|
||||||
continue
|
continue
|
||||||
@@ -176,7 +179,7 @@ func publishTestflight(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
log.Info(string(platform), " ", tag, " publish")
|
log.Info(string(platform), " ", tag, " publish")
|
||||||
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
|
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")
|
log.Info("waiting for process")
|
||||||
time.Sleep(15 * time.Second)
|
time.Sleep(15 * time.Second)
|
||||||
continue
|
continue
|
||||||
@@ -194,6 +197,10 @@ func publishTestflight(ctx context.Context) error {
|
|||||||
log.Info(string(platform), " ", tag, " create submission")
|
log.Info(string(platform), " ", tag, " create submission")
|
||||||
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
|
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
|
||||||
|
log.Error(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
debugEnabled bool
|
debugEnabled bool
|
||||||
target string
|
target string
|
||||||
platform string
|
platform string
|
||||||
|
withTailscale bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
|
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
|
||||||
flag.StringVar(&target, "target", "android", "target platform")
|
flag.StringVar(&target, "target", "android", "target platform")
|
||||||
flag.StringVar(&platform, "platform", "", "specify platform")
|
flag.StringVar(&platform, "platform", "", "specify platform")
|
||||||
|
flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -44,7 +46,9 @@ var (
|
|||||||
sharedFlags []string
|
sharedFlags []string
|
||||||
debugFlags []string
|
debugFlags []string
|
||||||
sharedTags []string
|
sharedTags []string
|
||||||
iosTags []string
|
macOSTags []string
|
||||||
|
memcTags []string
|
||||||
|
notMemcTags []string
|
||||||
debugTags []string
|
debugTags []string
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,11 +59,13 @@ func init() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
currentTag = "unknown"
|
currentTag = "unknown"
|
||||||
}
|
}
|
||||||
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid= -checklinkname=0")
|
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid= -checklinkname=0")
|
||||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -checklinkname=0")
|
||||||
|
|
||||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api", "with_tailscale")
|
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack", "badlinkname", "tfogo_checklinkname0")
|
||||||
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
macOSTags = append(macOSTags, "with_dhcp")
|
||||||
|
memcTags = append(memcTags, "with_tailscale")
|
||||||
|
notMemcTags = append(notMemcTags, "with_low_memory")
|
||||||
debugTags = append(debugTags, "debug")
|
debugTags = append(debugTags, "debug")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,18 +105,19 @@ func buildAndroid() {
|
|||||||
"-javapkg=io.nekohasekai",
|
"-javapkg=io.nekohasekai",
|
||||||
"-libname=box",
|
"-libname=box",
|
||||||
}
|
}
|
||||||
|
|
||||||
if !debugEnabled {
|
if !debugEnabled {
|
||||||
args = append(args, sharedFlags...)
|
args = append(args, sharedFlags...)
|
||||||
} else {
|
} else {
|
||||||
args = append(args, debugFlags...)
|
args = append(args, debugFlags...)
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, "-tags")
|
tags := append(sharedTags, memcTags...)
|
||||||
if !debugEnabled {
|
if debugEnabled {
|
||||||
args = append(args, strings.Join(sharedTags, ","))
|
tags = append(tags, debugTags...)
|
||||||
} else {
|
|
||||||
args = append(args, strings.Join(append(sharedTags, debugTags...), ","))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args = append(args, "-tags", strings.Join(tags, ","))
|
||||||
args = append(args, "./experimental/libbox")
|
args = append(args, "./experimental/libbox")
|
||||||
|
|
||||||
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
||||||
@@ -148,20 +155,29 @@ func buildApple() {
|
|||||||
"-v",
|
"-v",
|
||||||
"-target", bindTarget,
|
"-target", bindTarget,
|
||||||
"-libname=box",
|
"-libname=box",
|
||||||
|
"-tags-not-macos=with_low_memory",
|
||||||
}
|
}
|
||||||
|
if !withTailscale {
|
||||||
|
args = append(args, "-tags-macos="+strings.Join(append(macOSTags, memcTags...), ","))
|
||||||
|
} else {
|
||||||
|
args = append(args, "-tags-macos="+strings.Join(macOSTags, ","))
|
||||||
|
}
|
||||||
|
|
||||||
if !debugEnabled {
|
if !debugEnabled {
|
||||||
args = append(args, sharedFlags...)
|
args = append(args, sharedFlags...)
|
||||||
} else {
|
} else {
|
||||||
args = append(args, debugFlags...)
|
args = append(args, debugFlags...)
|
||||||
}
|
}
|
||||||
|
|
||||||
tags := append(sharedTags, iosTags...)
|
tags := sharedTags
|
||||||
args = append(args, "-tags")
|
if withTailscale {
|
||||||
if !debugEnabled {
|
tags = append(tags, memcTags...)
|
||||||
args = append(args, strings.Join(tags, ","))
|
|
||||||
} else {
|
|
||||||
args = append(args, strings.Join(append(tags, debugTags...), ","))
|
|
||||||
}
|
}
|
||||||
|
if debugEnabled {
|
||||||
|
tags = append(tags, debugTags...)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, "-tags", strings.Join(tags, ","))
|
||||||
args = append(args, "./experimental/libbox")
|
args = append(args, "./experimental/libbox")
|
||||||
|
|
||||||
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func FindSDK() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findNDK() bool {
|
func findNDK() bool {
|
||||||
const fixedVersion = "28.0.12674087"
|
const fixedVersion = "28.0.13004108"
|
||||||
const versionFile = "source.properties"
|
const versionFile = "source.properties"
|
||||||
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
|
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
|
||||||
androidNDKPath = fixedPath
|
androidNDKPath = fixedPath
|
||||||
|
|||||||
@@ -5,40 +5,49 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||||
|
"github.com/sagernet/sing-box/common/badversion"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var nightly bool
|
var (
|
||||||
|
flagRunInCI bool
|
||||||
|
flagRunNightly bool
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&nightly, "nightly", false, "Print nightly tag")
|
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
||||||
|
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if nightly {
|
var (
|
||||||
version, err := build_shared.ReadTagVersionRev()
|
versionStr string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if flagRunNightly {
|
||||||
|
var version badversion.Version
|
||||||
|
version, err = build_shared.ReadTagVersion()
|
||||||
|
if err == nil {
|
||||||
|
versionStr = version.String()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
versionStr, err = build_shared.ReadTag()
|
||||||
|
}
|
||||||
|
if flagRunInCI {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
var versionStr string
|
|
||||||
if version.PreReleaseIdentifier != "" {
|
|
||||||
versionStr = version.VersionString() + "-nightly"
|
|
||||||
} else {
|
|
||||||
version.Patch++
|
|
||||||
versionStr = version.VersionString() + "-nightly"
|
|
||||||
}
|
|
||||||
err = setGitHubEnv("version", versionStr)
|
err = setGitHubEnv("version", versionStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tag, err := build_shared.ReadTag()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
os.Stdout.WriteString("unknown\n")
|
os.Stdout.WriteString("unknown\n")
|
||||||
} else {
|
} else {
|
||||||
os.Stdout.WriteString(tag + "\n")
|
os.Stdout.WriteString(versionStr + "\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
284
cmd/internal/tun_bench/main.go
Normal file
284
cmd/internal/tun_bench/main.go
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/include"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
"github.com/sagernet/sing/common/shell"
|
||||||
|
)
|
||||||
|
|
||||||
|
var iperf3Path string
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := main0()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main0() error {
|
||||||
|
err := shell.Exec("sudo", "ls").Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
results, err := runTests()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
encoder := json.NewEncoder(os.Stdout)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
return encoder.Encode(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTests() ([]TestResult, error) {
|
||||||
|
boxPaths := []string{
|
||||||
|
os.ExpandEnv("$HOME/Downloads/sing-box-1.11.15-darwin-arm64/sing-box"),
|
||||||
|
//"/Users/sekai/Downloads/sing-box-1.11.15-linux-arm64/sing-box",
|
||||||
|
"./sing-box",
|
||||||
|
}
|
||||||
|
stacks := []string{
|
||||||
|
"gvisor",
|
||||||
|
"system",
|
||||||
|
}
|
||||||
|
mtus := []int{
|
||||||
|
1500,
|
||||||
|
4064,
|
||||||
|
// 16384,
|
||||||
|
// 32768,
|
||||||
|
// 49152,
|
||||||
|
65535,
|
||||||
|
}
|
||||||
|
flagList := [][]string{
|
||||||
|
{},
|
||||||
|
}
|
||||||
|
var results []TestResult
|
||||||
|
for _, boxPath := range boxPaths {
|
||||||
|
for _, stack := range stacks {
|
||||||
|
for _, mtu := range mtus {
|
||||||
|
if strings.HasPrefix(boxPath, ".") {
|
||||||
|
for _, flags := range flagList {
|
||||||
|
result, err := testOnce(boxPath, stack, mtu, false, flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results = append(results, *result)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result, err := testOnce(boxPath, stack, mtu, false, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results = append(results, *result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestResult struct {
|
||||||
|
BoxPath string `json:"box_path"`
|
||||||
|
Stack string `json:"stack"`
|
||||||
|
MTU int `json:"mtu"`
|
||||||
|
Flags []string `json:"flags"`
|
||||||
|
MultiThread bool `json:"multi_thread"`
|
||||||
|
UploadSpeed string `json:"upload_speed"`
|
||||||
|
DownloadSpeed string `json:"download_speed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOnce(boxPath string, stackName string, mtu int, multiThread bool, flags []string) (result *TestResult, err error) {
|
||||||
|
testAddress := netip.MustParseAddr("1.1.1.1")
|
||||||
|
testConfig := option.Options{
|
||||||
|
Inbounds: []option.Inbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeTun,
|
||||||
|
Options: &option.TunInboundOptions{
|
||||||
|
Address: []netip.Prefix{netip.MustParsePrefix("172.18.0.1/30")},
|
||||||
|
AutoRoute: true,
|
||||||
|
MTU: uint32(mtu),
|
||||||
|
Stack: stackName,
|
||||||
|
RouteAddress: []netip.Prefix{netip.PrefixFrom(testAddress, testAddress.BitLen())},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Route: &option.RouteOptions{
|
||||||
|
Rules: []option.Rule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
RawDefaultRule: option.RawDefaultRule{
|
||||||
|
IPCIDR: []string{testAddress.String()},
|
||||||
|
},
|
||||||
|
RuleAction: option.RuleAction{
|
||||||
|
Action: C.RuleActionTypeRouteOptions,
|
||||||
|
RouteOptionsOptions: option.RouteOptionsActionOptions{
|
||||||
|
OverrideAddress: "127.0.0.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AutoDetectInterface: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx := include.Context(context.Background())
|
||||||
|
tempConfig, err := os.CreateTemp("", "tun-bench-*.json")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.Remove(tempConfig.Name())
|
||||||
|
encoder := json.NewEncoderContext(ctx, tempConfig)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
err = encoder.Encode(testConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "encode test config")
|
||||||
|
}
|
||||||
|
tempConfig.Close()
|
||||||
|
var sudoArgs []string
|
||||||
|
if len(flags) > 0 {
|
||||||
|
sudoArgs = append(sudoArgs, "env")
|
||||||
|
sudoArgs = append(sudoArgs, flags...)
|
||||||
|
}
|
||||||
|
sudoArgs = append(sudoArgs, boxPath, "run", "-c", tempConfig.Name())
|
||||||
|
boxProcess := shell.Exec("sudo", sudoArgs...)
|
||||||
|
boxProcess.Stdout = &stderrWriter{}
|
||||||
|
boxProcess.Stderr = io.Discard
|
||||||
|
err = boxProcess.Start()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if C.IsDarwin {
|
||||||
|
iperf3Path, err = exec.LookPath("iperf3-darwin")
|
||||||
|
} else {
|
||||||
|
iperf3Path, err = exec.LookPath("iperf3")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverProcess := shell.Exec(iperf3Path, "-s")
|
||||||
|
serverProcess.Stdout = io.Discard
|
||||||
|
serverProcess.Stderr = io.Discard
|
||||||
|
err = serverProcess.Start()
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "start iperf3 server")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
args := []string{"-c", testAddress.String()}
|
||||||
|
if multiThread {
|
||||||
|
args = append(args, "-P", "10")
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadProcess := shell.Exec(iperf3Path, args...)
|
||||||
|
output, err := uploadProcess.Read()
|
||||||
|
if err != nil {
|
||||||
|
boxProcess.Process.Signal(syscall.SIGKILL)
|
||||||
|
serverProcess.Process.Signal(syscall.SIGKILL)
|
||||||
|
println(output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadResult := common.SubstringBeforeLast(output, "iperf Done.")
|
||||||
|
uploadResult = common.SubstringBeforeLast(uploadResult, "sender")
|
||||||
|
uploadResult = common.SubstringBeforeLast(uploadResult, "bits/sec")
|
||||||
|
uploadResult = common.SubstringAfterLast(uploadResult, "Bytes")
|
||||||
|
uploadResult = strings.ReplaceAll(uploadResult, " ", "")
|
||||||
|
|
||||||
|
result = &TestResult{
|
||||||
|
BoxPath: boxPath,
|
||||||
|
Stack: stackName,
|
||||||
|
MTU: mtu,
|
||||||
|
Flags: flags,
|
||||||
|
MultiThread: multiThread,
|
||||||
|
UploadSpeed: uploadResult,
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadProcess := shell.Exec(iperf3Path, append(args, "-R")...)
|
||||||
|
output, err = downloadProcess.Read()
|
||||||
|
if err != nil {
|
||||||
|
boxProcess.Process.Signal(syscall.SIGKILL)
|
||||||
|
serverProcess.Process.Signal(syscall.SIGKILL)
|
||||||
|
println(output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadResult := common.SubstringBeforeLast(output, "iperf Done.")
|
||||||
|
downloadResult = common.SubstringBeforeLast(downloadResult, "receiver")
|
||||||
|
downloadResult = common.SubstringBeforeLast(downloadResult, "bits/sec")
|
||||||
|
downloadResult = common.SubstringAfterLast(downloadResult, "Bytes")
|
||||||
|
downloadResult = strings.ReplaceAll(downloadResult, " ", "")
|
||||||
|
|
||||||
|
result.DownloadSpeed = downloadResult
|
||||||
|
|
||||||
|
printArgs := []any{boxPath, stackName, mtu, "upload", uploadResult, "download", downloadResult}
|
||||||
|
if len(flags) > 0 {
|
||||||
|
printArgs = append(printArgs, "flags", strings.Join(flags, " "))
|
||||||
|
}
|
||||||
|
if multiThread {
|
||||||
|
printArgs = append(printArgs, "(-P 10)")
|
||||||
|
}
|
||||||
|
fmt.Println(printArgs...)
|
||||||
|
err = boxProcess.Process.Signal(syscall.SIGTERM)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = serverProcess.Process.Signal(syscall.SIGTERM)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
boxDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
boxProcess.Cmd.Wait()
|
||||||
|
close(boxDone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
serverProcess.Process.Wait()
|
||||||
|
close(serverDone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-boxDone:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
boxProcess.Process.Kill()
|
||||||
|
case <-time.After(4 * time.Second):
|
||||||
|
println("box process did not close!")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-serverDone:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
serverProcess.Process.Kill()
|
||||||
|
case <-time.After(4 * time.Second):
|
||||||
|
println("server process did not close!")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type stderrWriter struct{}
|
||||||
|
|
||||||
|
func (w *stderrWriter) Write(p []byte) (n int, err error) {
|
||||||
|
return os.Stderr.Write(p)
|
||||||
|
}
|
||||||
@@ -13,10 +13,14 @@ import (
|
|||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
var flagRunInCI bool
|
var (
|
||||||
|
flagRunInCI bool
|
||||||
|
flagRunNightly bool
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
||||||
|
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -46,21 +50,23 @@ func main() {
|
|||||||
switch propPair[0] {
|
switch propPair[0] {
|
||||||
case "VERSION_NAME":
|
case "VERSION_NAME":
|
||||||
if propPair[1] != newVersion {
|
if propPair[1] != newVersion {
|
||||||
|
log.Info("updated version from ", propPair[1], " to ", newVersion)
|
||||||
versionUpdated = true
|
versionUpdated = true
|
||||||
propPair[1] = newVersion
|
propPair[1] = newVersion
|
||||||
log.Info("updated version to ", newVersion)
|
|
||||||
}
|
}
|
||||||
case "GO_VERSION":
|
case "GO_VERSION":
|
||||||
if propPair[1] != runtime.Version() {
|
if propPair[1] != runtime.Version() {
|
||||||
|
log.Info("updated Go version from ", propPair[1], " to ", runtime.Version())
|
||||||
goVersionUpdated = true
|
goVersionUpdated = true
|
||||||
propPair[1] = runtime.Version()
|
propPair[1] = runtime.Version()
|
||||||
log.Info("updated Go version to ", runtime.Version())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !(versionUpdated || goVersionUpdated) {
|
if !(versionUpdated || goVersionUpdated) {
|
||||||
log.Info("version not changed")
|
log.Info("version not changed")
|
||||||
return
|
return
|
||||||
|
} else if flagRunInCI && !flagRunNightly {
|
||||||
|
log.Fatal("version changed, commit changes first.")
|
||||||
}
|
}
|
||||||
for _, propPair := range propsList {
|
for _, propPair := range propsList {
|
||||||
switch propPair[0] {
|
switch propPair[0] {
|
||||||
|
|||||||
71
cmd/internal/update_certificates/main.go
Normal file
71
cmd/internal/update_certificates/main.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := updateMozillaIncludedRootCAs()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMozillaIncludedRootCAs() error {
|
||||||
|
response, err := http.Get("https://ccadb.my.salesforce-sites.com/mozilla/IncludedCACertificateReportPEMCSV")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
reader := csv.NewReader(response.Body)
|
||||||
|
header, err := reader.Read()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
geoIndex := slices.Index(header, "Geographic Focus")
|
||||||
|
nameIndex := slices.Index(header, "Common Name or Certificate Name")
|
||||||
|
certIndex := slices.Index(header, "PEM Info")
|
||||||
|
|
||||||
|
generated := strings.Builder{}
|
||||||
|
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||||
|
|
||||||
|
package certificate
|
||||||
|
|
||||||
|
import "crypto/x509"
|
||||||
|
|
||||||
|
var mozillaIncluded *x509.CertPool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
mozillaIncluded = x509.NewCertPool()
|
||||||
|
`)
|
||||||
|
for {
|
||||||
|
record, err := reader.Read()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if record[geoIndex] == "China" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
generated.WriteString("\n // ")
|
||||||
|
generated.WriteString(record[nameIndex])
|
||||||
|
generated.WriteString("\n")
|
||||||
|
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
|
||||||
|
cert := record[certIndex]
|
||||||
|
// Remove single quotes
|
||||||
|
cert = cert[1 : len(cert)-1]
|
||||||
|
generated.WriteString(cert)
|
||||||
|
generated.WriteString("`))\n")
|
||||||
|
}
|
||||||
|
generated.WriteString("}\n")
|
||||||
|
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box"
|
|
||||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||||
"github.com/sagernet/sing-box/include"
|
"github.com/sagernet/sing-box/include"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
@@ -68,6 +67,5 @@ func preRun(cmd *cobra.Command, args []string) {
|
|||||||
if len(configPaths) == 0 && len(configDirectories) == 0 {
|
if len(configPaths) == 0 && len(configDirectories) == 0 {
|
||||||
configPaths = append(configPaths, "config.json")
|
configPaths = append(configPaths, "config.json")
|
||||||
}
|
}
|
||||||
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
|
globalCtx = include.Context(service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger())))
|
||||||
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var pqSignatureSchemesEnabled bool
|
|
||||||
|
|
||||||
var commandGenerateECHKeyPair = &cobra.Command{
|
var commandGenerateECHKeyPair = &cobra.Command{
|
||||||
Use: "ech-keypair <plain_server_name>",
|
Use: "ech-keypair <plain_server_name>",
|
||||||
Short: "Generate TLS ECH key pair",
|
Short: "Generate TLS ECH key pair",
|
||||||
@@ -24,12 +22,11 @@ var commandGenerateECHKeyPair = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
commandGenerateECHKeyPair.Flags().BoolVar(&pqSignatureSchemesEnabled, "pq-signature-schemes-enabled", false, "Enable PQ signature schemes")
|
|
||||||
commandGenerate.AddCommand(commandGenerateECHKeyPair)
|
commandGenerate.AddCommand(commandGenerateECHKeyPair)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateECHKeyPair(serverName string) error {
|
func generateECHKeyPair(serverName string) error {
|
||||||
configPem, keyPem, err := tls.ECHKeygenDefault(serverName, pqSignatureSchemesEnabled)
|
configPem, keyPem, err := tls.ECHKeygenDefault(serverName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateTLSKeyPair(serverName string) error {
|
func generateTLSKeyPair(serverName string) error {
|
||||||
privateKeyPem, publicKeyPem, err := tls.GenerateKeyPair(time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
|
privateKeyPem, publicKeyPem, err := tls.GenerateCertificate(nil, nil, time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/srs"
|
"github.com/sagernet/sing-box/common/srs"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-box/route/rule"
|
||||||
"github.com/sagernet/sing/common/json"
|
"github.com/sagernet/sing/common/json"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -69,7 +71,7 @@ func compileRuleSet(sourcePath string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = srs.Write(outputFile, plainRuleSet.Options, plainRuleSet.Version)
|
err = srs.Write(outputFile, plainRuleSet.Options, downgradeRuleSetVersion(plainRuleSet.Version, plainRuleSet.Options))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
outputFile.Close()
|
outputFile.Close()
|
||||||
os.Remove(outputPath)
|
os.Remove(outputPath)
|
||||||
@@ -78,3 +80,18 @@ func compileRuleSet(sourcePath string) error {
|
|||||||
outputFile.Close()
|
outputFile.Close()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 {
|
||||||
|
if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
||||||
|
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
|
||||||
|
len(rule.DefaultInterfaceAddress) > 0
|
||||||
|
}) {
|
||||||
|
version = C.RuleSetVersion3
|
||||||
|
}
|
||||||
|
if version == C.RuleSetVersion3 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
||||||
|
return len(rule.NetworkType) > 0 || rule.NetworkIsExpensive || rule.NetworkIsConstrained
|
||||||
|
}) {
|
||||||
|
version = C.RuleSetVersion2
|
||||||
|
}
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/cmd/sing-box/internal/convertor/adguard"
|
"github.com/sagernet/sing-box/common/convertor/adguard"
|
||||||
"github.com/sagernet/sing-box/common/srs"
|
"github.com/sagernet/sing-box/common/srs"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
@@ -54,7 +54,7 @@ func convertRuleSet(sourcePath string) error {
|
|||||||
var rules []option.HeadlessRule
|
var rules []option.HeadlessRule
|
||||||
switch flagRuleSetConvertType {
|
switch flagRuleSetConvertType {
|
||||||
case "adguard":
|
case "adguard":
|
||||||
rules, err = adguard.Convert(reader)
|
rules, err = adguard.ToOptions(reader, log.StdLogger())
|
||||||
case "":
|
case "":
|
||||||
return E.New("source type is required")
|
return E.New("source type is required")
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/srs"
|
"github.com/sagernet/sing-box/common/srs"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/json"
|
"github.com/sagernet/sing/common/json"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -50,6 +53,11 @@ func decompileRuleSet(sourcePath string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if hasRule(ruleSet.Options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
||||||
|
return len(rule.AdGuardDomain) > 0
|
||||||
|
}) {
|
||||||
|
return E.New("unable to decompile binary AdGuard rules to rule-set.")
|
||||||
|
}
|
||||||
var outputPath string
|
var outputPath string
|
||||||
if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput {
|
if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput {
|
||||||
if strings.HasSuffix(sourcePath, ".srs") {
|
if strings.HasSuffix(sourcePath, ".srs") {
|
||||||
@@ -75,3 +83,19 @@ func decompileRuleSet(sourcePath string) error {
|
|||||||
outputFile.Close()
|
outputFile.Close()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool {
|
||||||
|
for _, rule := range rules {
|
||||||
|
switch rule.Type {
|
||||||
|
case C.RuleTypeDefault:
|
||||||
|
if cond(rule.DefaultOptions) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case C.RuleTypeLogical:
|
||||||
|
if hasRule(rule.LogicalOptions.Rules, cond) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/srs"
|
"github.com/sagernet/sing-box/common/srs"
|
||||||
@@ -56,6 +57,14 @@ func ruleSetMatch(sourcePath string, domain string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "read rule-set")
|
return E.Cause(err, "read rule-set")
|
||||||
}
|
}
|
||||||
|
if flagRuleSetMatchFormat == "" {
|
||||||
|
switch filepath.Ext(sourcePath) {
|
||||||
|
case ".json":
|
||||||
|
flagRuleSetMatchFormat = C.RuleSetFormatSource
|
||||||
|
case ".srs":
|
||||||
|
flagRuleSetMatchFormat = C.RuleSetFormatBinary
|
||||||
|
}
|
||||||
|
}
|
||||||
var ruleSet option.PlainRuleSetCompat
|
var ruleSet option.PlainRuleSetCompat
|
||||||
switch flagRuleSetMatchFormat {
|
switch flagRuleSetMatchFormat {
|
||||||
case C.RuleSetFormatSource:
|
case C.RuleSetFormatSource:
|
||||||
|
|||||||
@@ -61,14 +61,15 @@ func upgradeRuleSet(sourcePath string) error {
|
|||||||
log.Info("already up-to-date")
|
log.Info("already up-to-date")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
plainRuleSet, err := plainRuleSetCompat.Upgrade()
|
plainRuleSetCompat.Options, err = plainRuleSetCompat.Upgrade()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
plainRuleSetCompat.Version = C.RuleSetVersionCurrent
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
encoder := json.NewEncoder(buffer)
|
encoder := json.NewEncoder(buffer)
|
||||||
encoder.SetIndent("", " ")
|
encoder.SetIndent("", " ")
|
||||||
err = encoder.Encode(plainRuleSet)
|
err = encoder.Encode(plainRuleSetCompat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "encode config")
|
return E.Cause(err, "encode config")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func initializeHTTP3Client(instance *box.Box) error {
|
|||||||
}
|
}
|
||||||
http3Client = &http.Client{
|
http3Client = &http.Client{
|
||||||
Transport: &http3.Transport{
|
Transport: &http3.Transport{
|
||||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
||||||
destination := M.ParseSocksaddr(addr)
|
destination := M.ParseSocksaddr(addr)
|
||||||
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
|
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
|
||||||
if dErr != nil {
|
if dErr != nil {
|
||||||
|
|||||||
176
common/badtls/raw_conn.go
Normal file
176
common/badtls/raw_conn.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
//go:build go1.25 && badlinkname
|
||||||
|
|
||||||
|
package badtls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"sync/atomic"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RawConn struct {
|
||||||
|
pointer unsafe.Pointer
|
||||||
|
methods *Methods
|
||||||
|
|
||||||
|
IsClient *bool
|
||||||
|
IsHandshakeComplete *atomic.Bool
|
||||||
|
Vers *uint16
|
||||||
|
CipherSuite *uint16
|
||||||
|
|
||||||
|
RawInput *bytes.Buffer
|
||||||
|
Input *bytes.Reader
|
||||||
|
Hand *bytes.Buffer
|
||||||
|
|
||||||
|
CloseNotifySent *bool
|
||||||
|
CloseNotifyErr *error
|
||||||
|
|
||||||
|
In *RawHalfConn
|
||||||
|
Out *RawHalfConn
|
||||||
|
|
||||||
|
BytesSent *int64
|
||||||
|
PacketsSent *int64
|
||||||
|
|
||||||
|
ActiveCall *atomic.Int32
|
||||||
|
Tmp *[16]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRawConn(rawTLSConn tls.Conn) (*RawConn, error) {
|
||||||
|
var (
|
||||||
|
pointer unsafe.Pointer
|
||||||
|
methods *Methods
|
||||||
|
loaded bool
|
||||||
|
)
|
||||||
|
for _, tlsCreator := range methodRegistry {
|
||||||
|
pointer, methods, loaded = tlsCreator(rawTLSConn)
|
||||||
|
if loaded {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !loaded {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := &RawConn{
|
||||||
|
pointer: pointer,
|
||||||
|
methods: methods,
|
||||||
|
}
|
||||||
|
|
||||||
|
rawConn := reflect.Indirect(reflect.ValueOf(rawTLSConn))
|
||||||
|
|
||||||
|
rawIsClient := rawConn.FieldByName("isClient")
|
||||||
|
if !rawIsClient.IsValid() || rawIsClient.Kind() != reflect.Bool {
|
||||||
|
return nil, E.New("invalid Conn.isClient")
|
||||||
|
}
|
||||||
|
conn.IsClient = (*bool)(unsafe.Pointer(rawIsClient.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawIsHandshakeComplete := rawConn.FieldByName("isHandshakeComplete")
|
||||||
|
if !rawIsHandshakeComplete.IsValid() || rawIsHandshakeComplete.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("invalid Conn.isHandshakeComplete")
|
||||||
|
}
|
||||||
|
conn.IsHandshakeComplete = (*atomic.Bool)(unsafe.Pointer(rawIsHandshakeComplete.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawVers := rawConn.FieldByName("vers")
|
||||||
|
if !rawVers.IsValid() || rawVers.Kind() != reflect.Uint16 {
|
||||||
|
return nil, E.New("invalid Conn.vers")
|
||||||
|
}
|
||||||
|
conn.Vers = (*uint16)(unsafe.Pointer(rawVers.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawCipherSuite := rawConn.FieldByName("cipherSuite")
|
||||||
|
if !rawCipherSuite.IsValid() || rawCipherSuite.Kind() != reflect.Uint16 {
|
||||||
|
return nil, E.New("invalid Conn.cipherSuite")
|
||||||
|
}
|
||||||
|
conn.CipherSuite = (*uint16)(unsafe.Pointer(rawCipherSuite.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawRawInput := rawConn.FieldByName("rawInput")
|
||||||
|
if !rawRawInput.IsValid() || rawRawInput.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("invalid Conn.rawInput")
|
||||||
|
}
|
||||||
|
conn.RawInput = (*bytes.Buffer)(unsafe.Pointer(rawRawInput.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawInput := rawConn.FieldByName("input")
|
||||||
|
if !rawInput.IsValid() || rawInput.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("invalid Conn.input")
|
||||||
|
}
|
||||||
|
conn.Input = (*bytes.Reader)(unsafe.Pointer(rawInput.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawHand := rawConn.FieldByName("hand")
|
||||||
|
if !rawHand.IsValid() || rawHand.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("invalid Conn.hand")
|
||||||
|
}
|
||||||
|
conn.Hand = (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawCloseNotifySent := rawConn.FieldByName("closeNotifySent")
|
||||||
|
if !rawCloseNotifySent.IsValid() || rawCloseNotifySent.Kind() != reflect.Bool {
|
||||||
|
return nil, E.New("invalid Conn.closeNotifySent")
|
||||||
|
}
|
||||||
|
conn.CloseNotifySent = (*bool)(unsafe.Pointer(rawCloseNotifySent.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawCloseNotifyErr := rawConn.FieldByName("closeNotifyErr")
|
||||||
|
if !rawCloseNotifyErr.IsValid() || rawCloseNotifyErr.Kind() != reflect.Interface {
|
||||||
|
return nil, E.New("invalid Conn.closeNotifyErr")
|
||||||
|
}
|
||||||
|
conn.CloseNotifyErr = (*error)(unsafe.Pointer(rawCloseNotifyErr.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawIn := rawConn.FieldByName("in")
|
||||||
|
if !rawIn.IsValid() || rawIn.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("invalid Conn.in")
|
||||||
|
}
|
||||||
|
halfIn, err := NewRawHalfConn(rawIn, methods)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "invalid Conn.in")
|
||||||
|
}
|
||||||
|
conn.In = halfIn
|
||||||
|
|
||||||
|
rawOut := rawConn.FieldByName("out")
|
||||||
|
if !rawOut.IsValid() || rawOut.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("invalid Conn.out")
|
||||||
|
}
|
||||||
|
halfOut, err := NewRawHalfConn(rawOut, methods)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "invalid Conn.out")
|
||||||
|
}
|
||||||
|
conn.Out = halfOut
|
||||||
|
|
||||||
|
rawBytesSent := rawConn.FieldByName("bytesSent")
|
||||||
|
if !rawBytesSent.IsValid() || rawBytesSent.Kind() != reflect.Int64 {
|
||||||
|
return nil, E.New("invalid Conn.bytesSent")
|
||||||
|
}
|
||||||
|
conn.BytesSent = (*int64)(unsafe.Pointer(rawBytesSent.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawPacketsSent := rawConn.FieldByName("packetsSent")
|
||||||
|
if !rawPacketsSent.IsValid() || rawPacketsSent.Kind() != reflect.Int64 {
|
||||||
|
return nil, E.New("invalid Conn.packetsSent")
|
||||||
|
}
|
||||||
|
conn.PacketsSent = (*int64)(unsafe.Pointer(rawPacketsSent.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawActiveCall := rawConn.FieldByName("activeCall")
|
||||||
|
if !rawActiveCall.IsValid() || rawActiveCall.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("invalid Conn.activeCall")
|
||||||
|
}
|
||||||
|
conn.ActiveCall = (*atomic.Int32)(unsafe.Pointer(rawActiveCall.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawTmp := rawConn.FieldByName("tmp")
|
||||||
|
if !rawTmp.IsValid() || rawTmp.Kind() != reflect.Array || rawTmp.Len() != 16 || rawTmp.Type().Elem().Kind() != reflect.Uint8 {
|
||||||
|
return nil, E.New("invalid Conn.tmp")
|
||||||
|
}
|
||||||
|
conn.Tmp = (*[16]byte)(unsafe.Pointer(rawTmp.UnsafeAddr()))
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RawConn) ReadRecord() error {
|
||||||
|
return c.methods.readRecord(c.pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RawConn) HandlePostHandshakeMessage() error {
|
||||||
|
return c.methods.handlePostHandshakeMessage(c.pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RawConn) WriteRecordLocked(typ uint16, data []byte) (int, error) {
|
||||||
|
return c.methods.writeRecordLocked(c.pointer, typ, data)
|
||||||
|
}
|
||||||
121
common/badtls/raw_half_conn.go
Normal file
121
common/badtls/raw_half_conn.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
//go:build go1.25 && badlinkname
|
||||||
|
|
||||||
|
package badtls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hash"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RawHalfConn struct {
|
||||||
|
pointer unsafe.Pointer
|
||||||
|
methods *Methods
|
||||||
|
*sync.Mutex
|
||||||
|
Err *error
|
||||||
|
Version *uint16
|
||||||
|
Cipher *any
|
||||||
|
Seq *[8]byte
|
||||||
|
ScratchBuf *[13]byte
|
||||||
|
TrafficSecret *[]byte
|
||||||
|
Mac *hash.Hash
|
||||||
|
RawKey *[]byte
|
||||||
|
RawIV *[]byte
|
||||||
|
RawMac *[]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRawHalfConn(rawHalfConn reflect.Value, methods *Methods) (*RawHalfConn, error) {
|
||||||
|
halfConn := &RawHalfConn{
|
||||||
|
pointer: (unsafe.Pointer)(rawHalfConn.UnsafeAddr()),
|
||||||
|
methods: methods,
|
||||||
|
}
|
||||||
|
|
||||||
|
rawMutex := rawHalfConn.FieldByName("Mutex")
|
||||||
|
if !rawMutex.IsValid() || rawMutex.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("badtls: invalid halfConn.Mutex")
|
||||||
|
}
|
||||||
|
halfConn.Mutex = (*sync.Mutex)(unsafe.Pointer(rawMutex.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawErr := rawHalfConn.FieldByName("err")
|
||||||
|
if !rawErr.IsValid() || rawErr.Kind() != reflect.Interface {
|
||||||
|
return nil, E.New("badtls: invalid halfConn.err")
|
||||||
|
}
|
||||||
|
halfConn.Err = (*error)(unsafe.Pointer(rawErr.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawVersion := rawHalfConn.FieldByName("version")
|
||||||
|
if !rawVersion.IsValid() || rawVersion.Kind() != reflect.Uint16 {
|
||||||
|
return nil, E.New("badtls: invalid halfConn.version")
|
||||||
|
}
|
||||||
|
halfConn.Version = (*uint16)(unsafe.Pointer(rawVersion.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawCipher := rawHalfConn.FieldByName("cipher")
|
||||||
|
if !rawCipher.IsValid() || rawCipher.Kind() != reflect.Interface {
|
||||||
|
return nil, E.New("badtls: invalid halfConn.cipher")
|
||||||
|
}
|
||||||
|
halfConn.Cipher = (*any)(unsafe.Pointer(rawCipher.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawSeq := rawHalfConn.FieldByName("seq")
|
||||||
|
if !rawSeq.IsValid() || rawSeq.Kind() != reflect.Array || rawSeq.Len() != 8 || rawSeq.Type().Elem().Kind() != reflect.Uint8 {
|
||||||
|
return nil, E.New("badtls: invalid halfConn.seq")
|
||||||
|
}
|
||||||
|
halfConn.Seq = (*[8]byte)(unsafe.Pointer(rawSeq.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawScratchBuf := rawHalfConn.FieldByName("scratchBuf")
|
||||||
|
if !rawScratchBuf.IsValid() || rawScratchBuf.Kind() != reflect.Array || rawScratchBuf.Len() != 13 || rawScratchBuf.Type().Elem().Kind() != reflect.Uint8 {
|
||||||
|
return nil, E.New("badtls: invalid halfConn.scratchBuf")
|
||||||
|
}
|
||||||
|
halfConn.ScratchBuf = (*[13]byte)(unsafe.Pointer(rawScratchBuf.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawTrafficSecret := rawHalfConn.FieldByName("trafficSecret")
|
||||||
|
if !rawTrafficSecret.IsValid() || rawTrafficSecret.Kind() != reflect.Slice || rawTrafficSecret.Type().Elem().Kind() != reflect.Uint8 {
|
||||||
|
return nil, E.New("badtls: invalid halfConn.trafficSecret")
|
||||||
|
}
|
||||||
|
halfConn.TrafficSecret = (*[]byte)(unsafe.Pointer(rawTrafficSecret.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawMac := rawHalfConn.FieldByName("mac")
|
||||||
|
if !rawMac.IsValid() || rawMac.Kind() != reflect.Interface {
|
||||||
|
return nil, E.New("badtls: invalid halfConn.mac")
|
||||||
|
}
|
||||||
|
halfConn.Mac = (*hash.Hash)(unsafe.Pointer(rawMac.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawKey := rawHalfConn.FieldByName("rawKey")
|
||||||
|
if rawKey.IsValid() {
|
||||||
|
if /*!rawKey.IsValid() || */ rawKey.Kind() != reflect.Slice || rawKey.Type().Elem().Kind() != reflect.Uint8 {
|
||||||
|
return nil, E.New("badtls: invalid halfConn.rawKey")
|
||||||
|
}
|
||||||
|
halfConn.RawKey = (*[]byte)(unsafe.Pointer(rawKey.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawIV := rawHalfConn.FieldByName("rawIV")
|
||||||
|
if !rawIV.IsValid() || rawIV.Kind() != reflect.Slice || rawIV.Type().Elem().Kind() != reflect.Uint8 {
|
||||||
|
return nil, E.New("badtls: invalid halfConn.rawIV")
|
||||||
|
}
|
||||||
|
halfConn.RawIV = (*[]byte)(unsafe.Pointer(rawIV.UnsafeAddr()))
|
||||||
|
|
||||||
|
rawMAC := rawHalfConn.FieldByName("rawMac")
|
||||||
|
if !rawMAC.IsValid() || rawMAC.Kind() != reflect.Slice || rawMAC.Type().Elem().Kind() != reflect.Uint8 {
|
||||||
|
return nil, E.New("badtls: invalid halfConn.rawMac")
|
||||||
|
}
|
||||||
|
halfConn.RawMac = (*[]byte)(unsafe.Pointer(rawMAC.UnsafeAddr()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return halfConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *RawHalfConn) Decrypt(record []byte) ([]byte, uint8, error) {
|
||||||
|
return hc.methods.decrypt(hc.pointer, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *RawHalfConn) SetErrorLocked(err error) error {
|
||||||
|
return hc.methods.setErrorLocked(hc.pointer, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *RawHalfConn) SetTrafficSecret(suite unsafe.Pointer, level int, secret []byte) {
|
||||||
|
hc.methods.setTrafficSecret(hc.pointer, suite, level, secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *RawHalfConn) ExplicitNonceLen() int {
|
||||||
|
return hc.methods.explicitNonceLen(hc.pointer)
|
||||||
|
}
|
||||||
@@ -1,18 +1,9 @@
|
|||||||
//go:build go1.21 && !without_badtls
|
//go:build go1.25 && badlinkname
|
||||||
|
|
||||||
package badtls
|
package badtls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"sync"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/tls"
|
"github.com/sagernet/sing/common/tls"
|
||||||
)
|
)
|
||||||
@@ -21,63 +12,21 @@ var _ N.ReadWaiter = (*ReadWaitConn)(nil)
|
|||||||
|
|
||||||
type ReadWaitConn struct {
|
type ReadWaitConn struct {
|
||||||
tls.Conn
|
tls.Conn
|
||||||
halfAccess *sync.Mutex
|
rawConn *RawConn
|
||||||
rawInput *bytes.Buffer
|
readWaitOptions N.ReadWaitOptions
|
||||||
input *bytes.Reader
|
|
||||||
hand *bytes.Buffer
|
|
||||||
readWaitOptions N.ReadWaitOptions
|
|
||||||
tlsReadRecord func() error
|
|
||||||
tlsHandlePostHandshakeMessage func() error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) {
|
func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) {
|
||||||
var (
|
if _, isReadWaitConn := conn.(N.ReadWaiter); isReadWaitConn {
|
||||||
loaded bool
|
return conn, nil
|
||||||
tlsReadRecord func() error
|
|
||||||
tlsHandlePostHandshakeMessage func() error
|
|
||||||
)
|
|
||||||
for _, tlsCreator := range tlsRegistry {
|
|
||||||
loaded, tlsReadRecord, tlsHandlePostHandshakeMessage = tlsCreator(conn)
|
|
||||||
if loaded {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !loaded {
|
rawConn, err := NewRawConn(conn)
|
||||||
return nil, os.ErrInvalid
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
rawConn := reflect.Indirect(reflect.ValueOf(conn))
|
|
||||||
rawHalfConn := rawConn.FieldByName("in")
|
|
||||||
if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: invalid half conn")
|
|
||||||
}
|
|
||||||
rawHalfMutex := rawHalfConn.FieldByName("Mutex")
|
|
||||||
if !rawHalfMutex.IsValid() || rawHalfMutex.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: invalid half mutex")
|
|
||||||
}
|
|
||||||
halfAccess := (*sync.Mutex)(unsafe.Pointer(rawHalfMutex.UnsafeAddr()))
|
|
||||||
rawRawInput := rawConn.FieldByName("rawInput")
|
|
||||||
if !rawRawInput.IsValid() || rawRawInput.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: invalid raw input")
|
|
||||||
}
|
|
||||||
rawInput := (*bytes.Buffer)(unsafe.Pointer(rawRawInput.UnsafeAddr()))
|
|
||||||
rawInput0 := rawConn.FieldByName("input")
|
|
||||||
if !rawInput0.IsValid() || rawInput0.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: invalid input")
|
|
||||||
}
|
|
||||||
input := (*bytes.Reader)(unsafe.Pointer(rawInput0.UnsafeAddr()))
|
|
||||||
rawHand := rawConn.FieldByName("hand")
|
|
||||||
if !rawHand.IsValid() || rawHand.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: invalid hand")
|
|
||||||
}
|
|
||||||
hand := (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr()))
|
|
||||||
return &ReadWaitConn{
|
return &ReadWaitConn{
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
halfAccess: halfAccess,
|
rawConn: rawConn,
|
||||||
rawInput: rawInput,
|
|
||||||
input: input,
|
|
||||||
hand: hand,
|
|
||||||
tlsReadRecord: tlsReadRecord,
|
|
||||||
tlsHandlePostHandshakeMessage: tlsHandlePostHandshakeMessage,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,36 +36,36 @@ func (c *ReadWaitConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
|
func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
|
||||||
err = c.HandshakeContext(context.Background())
|
//err = c.HandshakeContext(context.Background())
|
||||||
if err != nil {
|
//if err != nil {
|
||||||
return
|
// return
|
||||||
}
|
//}
|
||||||
c.halfAccess.Lock()
|
c.rawConn.In.Lock()
|
||||||
defer c.halfAccess.Unlock()
|
defer c.rawConn.In.Unlock()
|
||||||
for c.input.Len() == 0 {
|
for c.rawConn.Input.Len() == 0 {
|
||||||
err = c.tlsReadRecord()
|
err = c.rawConn.ReadRecord()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for c.hand.Len() > 0 {
|
for c.rawConn.Hand.Len() > 0 {
|
||||||
err = c.tlsHandlePostHandshakeMessage()
|
err = c.rawConn.HandlePostHandshakeMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buffer = c.readWaitOptions.NewBuffer()
|
buffer = c.readWaitOptions.NewBuffer()
|
||||||
n, err := c.input.Read(buffer.FreeBytes())
|
n, err := c.rawConn.Input.Read(buffer.FreeBytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
buffer.Release()
|
buffer.Release()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
buffer.Truncate(n)
|
buffer.Truncate(n)
|
||||||
|
|
||||||
if n != 0 && c.input.Len() == 0 && c.rawInput.Len() > 0 &&
|
if n != 0 && c.rawConn.Input.Len() == 0 && c.rawConn.Input.Len() > 0 &&
|
||||||
// recordType(c.rawInput.Bytes()[0]) == recordTypeAlert {
|
// recordType(c.RawInput.Bytes()[0]) == recordTypeAlert {
|
||||||
c.rawInput.Bytes()[0] == 21 {
|
c.rawConn.RawInput.Bytes()[0] == 21 {
|
||||||
_ = c.tlsReadRecord()
|
_ = c.rawConn.ReadRecord()
|
||||||
// return n, err // will be io.EOF on closeNotify
|
// return n, err // will be io.EOF on closeNotify
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,24 +77,6 @@ func (c *ReadWaitConn) Upstream() any {
|
|||||||
return c.Conn
|
return c.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error)
|
func (c *ReadWaitConn) ReaderReplaceable() bool {
|
||||||
|
return true
|
||||||
func init() {
|
|
||||||
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
|
|
||||||
tlsConn, loaded := conn.(*tls.STDConn)
|
|
||||||
if !loaded {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return true, func() error {
|
|
||||||
return stdTLSReadRecord(tlsConn)
|
|
||||||
}, func() error {
|
|
||||||
return stdTLSHandlePostHandshakeMessage(tlsConn)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:linkname stdTLSReadRecord crypto/tls.(*Conn).readRecord
|
|
||||||
func stdTLSReadRecord(c *tls.STDConn) error
|
|
||||||
|
|
||||||
//go:linkname stdTLSHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage
|
|
||||||
func stdTLSHandlePostHandshakeMessage(c *tls.STDConn) error
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
//go:build go1.21 && !without_badtls && with_ech
|
|
||||||
|
|
||||||
package badtls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
_ "unsafe"
|
|
||||||
|
|
||||||
"github.com/sagernet/cloudflare-tls"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
|
|
||||||
tlsConn, loaded := common.Cast[*tls.Conn](conn)
|
|
||||||
if !loaded {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return true, func() error {
|
|
||||||
return echReadRecord(tlsConn)
|
|
||||||
}, func() error {
|
|
||||||
return echHandlePostHandshakeMessage(tlsConn)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:linkname echReadRecord github.com/sagernet/cloudflare-tls.(*Conn).readRecord
|
|
||||||
func echReadRecord(c *tls.Conn) error
|
|
||||||
|
|
||||||
//go:linkname echHandlePostHandshakeMessage github.com/sagernet/cloudflare-tls.(*Conn).handlePostHandshakeMessage
|
|
||||||
func echHandlePostHandshakeMessage(c *tls.Conn) error
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !go1.21 || without_badtls
|
//go:build !go1.25 || !badlinkname
|
||||||
|
|
||||||
package badtls
|
package badtls
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
//go:build go1.21 && !without_badtls && with_utls
|
|
||||||
|
|
||||||
package badtls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
_ "unsafe"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/utls"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
|
|
||||||
tlsConn, loaded := common.Cast[*tls.UConn](conn)
|
|
||||||
if !loaded {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return true, func() error {
|
|
||||||
return utlsReadRecord(tlsConn.Conn)
|
|
||||||
}, func() error {
|
|
||||||
return utlsHandlePostHandshakeMessage(tlsConn.Conn)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:linkname utlsReadRecord github.com/sagernet/utls.(*Conn).readRecord
|
|
||||||
func utlsReadRecord(c *tls.Conn) error
|
|
||||||
|
|
||||||
//go:linkname utlsHandlePostHandshakeMessage github.com/sagernet/utls.(*Conn).handlePostHandshakeMessage
|
|
||||||
func utlsHandlePostHandshakeMessage(c *tls.Conn) error
|
|
||||||
62
common/badtls/registry.go
Normal file
62
common/badtls/registry.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
//go:build go1.25 && badlinkname
|
||||||
|
|
||||||
|
package badtls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Methods struct {
|
||||||
|
readRecord func(c unsafe.Pointer) error
|
||||||
|
handlePostHandshakeMessage func(c unsafe.Pointer) error
|
||||||
|
writeRecordLocked func(c unsafe.Pointer, typ uint16, data []byte) (int, error)
|
||||||
|
|
||||||
|
setErrorLocked func(hc unsafe.Pointer, err error) error
|
||||||
|
decrypt func(hc unsafe.Pointer, record []byte) ([]byte, uint8, error)
|
||||||
|
setTrafficSecret func(hc unsafe.Pointer, suite unsafe.Pointer, level int, secret []byte)
|
||||||
|
explicitNonceLen func(hc unsafe.Pointer) int
|
||||||
|
}
|
||||||
|
|
||||||
|
var methodRegistry []func(conn net.Conn) (unsafe.Pointer, *Methods, bool)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
methodRegistry = append(methodRegistry, func(conn net.Conn) (unsafe.Pointer, *Methods, bool) {
|
||||||
|
tlsConn, loaded := conn.(*tls.Conn)
|
||||||
|
if !loaded {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
return unsafe.Pointer(tlsConn), &Methods{
|
||||||
|
readRecord: stdTLSReadRecord,
|
||||||
|
handlePostHandshakeMessage: stdTLSHandlePostHandshakeMessage,
|
||||||
|
writeRecordLocked: stdWriteRecordLocked,
|
||||||
|
|
||||||
|
setErrorLocked: stdSetErrorLocked,
|
||||||
|
decrypt: stdDecrypt,
|
||||||
|
setTrafficSecret: stdSetTrafficSecret,
|
||||||
|
explicitNonceLen: stdExplicitNonceLen,
|
||||||
|
}, true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:linkname stdTLSReadRecord crypto/tls.(*Conn).readRecord
|
||||||
|
func stdTLSReadRecord(c unsafe.Pointer) error
|
||||||
|
|
||||||
|
//go:linkname stdTLSHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage
|
||||||
|
func stdTLSHandlePostHandshakeMessage(c unsafe.Pointer) error
|
||||||
|
|
||||||
|
//go:linkname stdWriteRecordLocked crypto/tls.(*Conn).writeRecordLocked
|
||||||
|
func stdWriteRecordLocked(c unsafe.Pointer, typ uint16, data []byte) (int, error)
|
||||||
|
|
||||||
|
//go:linkname stdSetErrorLocked crypto/tls.(*halfConn).setErrorLocked
|
||||||
|
func stdSetErrorLocked(hc unsafe.Pointer, err error) error
|
||||||
|
|
||||||
|
//go:linkname stdDecrypt crypto/tls.(*halfConn).decrypt
|
||||||
|
func stdDecrypt(hc unsafe.Pointer, record []byte) ([]byte, uint8, error)
|
||||||
|
|
||||||
|
//go:linkname stdSetTrafficSecret crypto/tls.(*halfConn).setTrafficSecret
|
||||||
|
func stdSetTrafficSecret(hc unsafe.Pointer, suite unsafe.Pointer, level int, secret []byte)
|
||||||
|
|
||||||
|
//go:linkname stdExplicitNonceLen crypto/tls.(*halfConn).explicitNonceLen
|
||||||
|
func stdExplicitNonceLen(hc unsafe.Pointer) int
|
||||||
56
common/badtls/registry_utls.go
Normal file
56
common/badtls/registry_utls.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
//go:build go1.25 && badlinkname
|
||||||
|
|
||||||
|
package badtls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
"github.com/metacubex/utls"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
methodRegistry = append(methodRegistry, func(conn net.Conn) (unsafe.Pointer, *Methods, bool) {
|
||||||
|
var pointer unsafe.Pointer
|
||||||
|
if uConn, loaded := N.CastReader[*tls.Conn](conn); loaded {
|
||||||
|
pointer = unsafe.Pointer(uConn)
|
||||||
|
} else if uConn, loaded := N.CastReader[*tls.UConn](conn); loaded {
|
||||||
|
pointer = unsafe.Pointer(uConn.Conn)
|
||||||
|
} else {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
return pointer, &Methods{
|
||||||
|
readRecord: utlsReadRecord,
|
||||||
|
handlePostHandshakeMessage: utlsHandlePostHandshakeMessage,
|
||||||
|
writeRecordLocked: utlsWriteRecordLocked,
|
||||||
|
|
||||||
|
setErrorLocked: utlsSetErrorLocked,
|
||||||
|
decrypt: utlsDecrypt,
|
||||||
|
setTrafficSecret: utlsSetTrafficSecret,
|
||||||
|
explicitNonceLen: utlsExplicitNonceLen,
|
||||||
|
}, true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:linkname utlsReadRecord github.com/metacubex/utls.(*Conn).readRecord
|
||||||
|
func utlsReadRecord(c unsafe.Pointer) error
|
||||||
|
|
||||||
|
//go:linkname utlsHandlePostHandshakeMessage github.com/metacubex/utls.(*Conn).handlePostHandshakeMessage
|
||||||
|
func utlsHandlePostHandshakeMessage(c unsafe.Pointer) error
|
||||||
|
|
||||||
|
//go:linkname utlsWriteRecordLocked github.com/metacubex/utls.(*Conn).writeRecordLocked
|
||||||
|
func utlsWriteRecordLocked(hc unsafe.Pointer, typ uint16, data []byte) (int, error)
|
||||||
|
|
||||||
|
//go:linkname utlsSetErrorLocked github.com/metacubex/utls.(*halfConn).setErrorLocked
|
||||||
|
func utlsSetErrorLocked(hc unsafe.Pointer, err error) error
|
||||||
|
|
||||||
|
//go:linkname utlsDecrypt github.com/metacubex/utls.(*halfConn).decrypt
|
||||||
|
func utlsDecrypt(hc unsafe.Pointer, record []byte) ([]byte, uint8, error)
|
||||||
|
|
||||||
|
//go:linkname utlsSetTrafficSecret github.com/metacubex/utls.(*halfConn).setTrafficSecret
|
||||||
|
func utlsSetTrafficSecret(hc unsafe.Pointer, suite unsafe.Pointer, level int, secret []byte)
|
||||||
|
|
||||||
|
//go:linkname utlsExplicitNonceLen github.com/metacubex/utls.(*halfConn).explicitNonceLen
|
||||||
|
func utlsExplicitNonceLen(hc unsafe.Pointer) int
|
||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Version struct {
|
type Version struct {
|
||||||
@@ -16,7 +18,19 @@ type Version struct {
|
|||||||
PreReleaseVersion int
|
PreReleaseVersion int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v Version) After(anotherVersion Version) bool {
|
func (v Version) LessThan(anotherVersion Version) bool {
|
||||||
|
return !v.GreaterThanOrEqual(anotherVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) LessThanOrEqual(anotherVersion Version) bool {
|
||||||
|
return v == anotherVersion || anotherVersion.GreaterThan(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) GreaterThanOrEqual(anotherVersion Version) bool {
|
||||||
|
return v == anotherVersion || v.GreaterThan(anotherVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) GreaterThan(anotherVersion Version) bool {
|
||||||
if v.Major > anotherVersion.Major {
|
if v.Major > anotherVersion.Major {
|
||||||
return true
|
return true
|
||||||
} else if v.Major < anotherVersion.Major {
|
} else if v.Major < anotherVersion.Major {
|
||||||
@@ -44,19 +58,29 @@ func (v Version) After(anotherVersion Version) bool {
|
|||||||
} else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion {
|
} else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} else if v.PreReleaseIdentifier == "rc" && anotherVersion.PreReleaseIdentifier == "beta" {
|
}
|
||||||
|
preReleaseIdentifier := parsePreReleaseIdentifier(v.PreReleaseIdentifier)
|
||||||
|
anotherPreReleaseIdentifier := parsePreReleaseIdentifier(anotherVersion.PreReleaseIdentifier)
|
||||||
|
if preReleaseIdentifier < anotherPreReleaseIdentifier {
|
||||||
return true
|
return true
|
||||||
} else if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "rc" {
|
} else if preReleaseIdentifier > anotherPreReleaseIdentifier {
|
||||||
return false
|
|
||||||
} else if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "alpha" {
|
|
||||||
return true
|
|
||||||
} else if v.PreReleaseIdentifier == "alpha" && anotherVersion.PreReleaseIdentifier == "beta" {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parsePreReleaseIdentifier(identifier string) int {
|
||||||
|
if strings.HasPrefix(identifier, "rc") {
|
||||||
|
return 1
|
||||||
|
} else if strings.HasPrefix(identifier, "beta") {
|
||||||
|
return 2
|
||||||
|
} else if strings.HasPrefix(identifier, "alpha") {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func (v Version) VersionString() string {
|
func (v Version) VersionString() string {
|
||||||
return F.ToString(v.Major, ".", v.Minor, ".", v.Patch)
|
return F.ToString(v.Major, ".", v.Minor, ".", v.Patch)
|
||||||
}
|
}
|
||||||
@@ -83,6 +107,10 @@ func (v Version) BadString() string {
|
|||||||
return version
|
return version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsValid(versionName string) bool {
|
||||||
|
return semver.IsValid("v" + versionName)
|
||||||
|
}
|
||||||
|
|
||||||
func Parse(versionName string) (version Version) {
|
func Parse(versionName string) (version Version) {
|
||||||
if strings.HasPrefix(versionName, "v") {
|
if strings.HasPrefix(versionName, "v") {
|
||||||
versionName = versionName[1:]
|
versionName = versionName[1:]
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ func TestCompareVersion(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
require.Equal(t, "1.3.0-beta.1", Parse("v1.3.0-beta1").String())
|
require.Equal(t, "1.3.0-beta.1", Parse("v1.3.0-beta1").String())
|
||||||
require.Equal(t, "1.3-beta1", Parse("v1.3.0-beta.1").BadString())
|
require.Equal(t, "1.3-beta1", Parse("v1.3.0-beta.1").BadString())
|
||||||
require.True(t, Parse("1.3.0").After(Parse("1.3-beta1")))
|
require.True(t, Parse("1.3.0").GreaterThan(Parse("1.3-beta1")))
|
||||||
require.True(t, Parse("1.3.0").After(Parse("1.3.0-beta1")))
|
require.True(t, Parse("1.3.0").GreaterThan(Parse("1.3.0-beta1")))
|
||||||
require.True(t, Parse("1.3.0-beta1").After(Parse("1.3.0-alpha1")))
|
require.True(t, Parse("1.3.0-beta1").GreaterThan(Parse("1.3.0-alpha1")))
|
||||||
require.True(t, Parse("1.3.1").After(Parse("1.3.0")))
|
require.True(t, Parse("1.3.1").GreaterThan(Parse("1.3.0")))
|
||||||
require.True(t, Parse("1.4").After(Parse("1.3")))
|
require.True(t, Parse("1.4").GreaterThan(Parse("1.3")))
|
||||||
}
|
}
|
||||||
|
|||||||
4394
common/certificate/mozilla.go
Normal file
4394
common/certificate/mozilla.go
Normal file
File diff suppressed because it is too large
Load Diff
191
common/certificate/store.go
Normal file
191
common/certificate/store.go
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
package certificate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/fswatch"
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.CertificateStore = (*Store)(nil)
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
access sync.RWMutex
|
||||||
|
systemPool *x509.CertPool
|
||||||
|
currentPool *x509.CertPool
|
||||||
|
certificate string
|
||||||
|
certificatePaths []string
|
||||||
|
certificateDirectoryPaths []string
|
||||||
|
watcher *fswatch.Watcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
|
||||||
|
var systemPool *x509.CertPool
|
||||||
|
switch options.Store {
|
||||||
|
case C.CertificateStoreSystem, "":
|
||||||
|
systemPool = x509.NewCertPool()
|
||||||
|
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||||
|
var systemValid bool
|
||||||
|
if platformInterface != nil {
|
||||||
|
for _, cert := range platformInterface.SystemCertificates() {
|
||||||
|
if systemPool.AppendCertsFromPEM([]byte(cert)) {
|
||||||
|
systemValid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !systemValid {
|
||||||
|
certPool, err := x509.SystemCertPool()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
systemPool = certPool
|
||||||
|
}
|
||||||
|
case C.CertificateStoreMozilla:
|
||||||
|
systemPool = mozillaIncluded
|
||||||
|
case C.CertificateStoreNone:
|
||||||
|
systemPool = nil
|
||||||
|
default:
|
||||||
|
return nil, E.New("unknown certificate store: ", options.Store)
|
||||||
|
}
|
||||||
|
store := &Store{
|
||||||
|
systemPool: systemPool,
|
||||||
|
certificate: strings.Join(options.Certificate, "\n"),
|
||||||
|
certificatePaths: options.CertificatePath,
|
||||||
|
certificateDirectoryPaths: options.CertificateDirectoryPath,
|
||||||
|
}
|
||||||
|
var watchPaths []string
|
||||||
|
for _, target := range options.CertificatePath {
|
||||||
|
watchPaths = append(watchPaths, target)
|
||||||
|
}
|
||||||
|
for _, target := range options.CertificateDirectoryPath {
|
||||||
|
watchPaths = append(watchPaths, target)
|
||||||
|
}
|
||||||
|
if len(watchPaths) > 0 {
|
||||||
|
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
||||||
|
Path: watchPaths,
|
||||||
|
Logger: logger,
|
||||||
|
Callback: func(_ string) {
|
||||||
|
err := store.update()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(E.Cause(err, "reload certificates"))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "fswatch: create fsnotify watcher")
|
||||||
|
}
|
||||||
|
store.watcher = watcher
|
||||||
|
}
|
||||||
|
err := store.update()
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initializing certificate store")
|
||||||
|
}
|
||||||
|
return store, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Name() string {
|
||||||
|
return "certificate"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Start(stage adapter.StartStage) error {
|
||||||
|
if stage != adapter.StartStateStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if s.watcher != nil {
|
||||||
|
return s.watcher.Start()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Close() error {
|
||||||
|
if s.watcher != nil {
|
||||||
|
return s.watcher.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Pool() *x509.CertPool {
|
||||||
|
s.access.RLock()
|
||||||
|
defer s.access.RUnlock()
|
||||||
|
return s.currentPool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) update() error {
|
||||||
|
s.access.Lock()
|
||||||
|
defer s.access.Unlock()
|
||||||
|
var currentPool *x509.CertPool
|
||||||
|
if s.systemPool == nil {
|
||||||
|
currentPool = x509.NewCertPool()
|
||||||
|
} else {
|
||||||
|
currentPool = s.systemPool.Clone()
|
||||||
|
}
|
||||||
|
if s.certificate != "" {
|
||||||
|
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
||||||
|
return E.New("invalid certificate PEM strings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, path := range s.certificatePaths {
|
||||||
|
pemContent, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !currentPool.AppendCertsFromPEM(pemContent) {
|
||||||
|
return E.New("invalid certificate PEM file: ", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var firstErr error
|
||||||
|
for _, directoryPath := range s.certificateDirectoryPaths {
|
||||||
|
directoryEntries, err := readUniqueDirectoryEntries(directoryPath)
|
||||||
|
if err != nil {
|
||||||
|
if firstErr == nil && !os.IsNotExist(err) {
|
||||||
|
firstErr = E.Cause(err, "invalid certificate directory: ", directoryPath)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, directoryEntry := range directoryEntries {
|
||||||
|
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
|
||||||
|
if err == nil {
|
||||||
|
currentPool.AppendCertsFromPEM(pemContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if firstErr != nil {
|
||||||
|
return firstErr
|
||||||
|
}
|
||||||
|
s.currentPool = currentPool
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readUniqueDirectoryEntries(dir string) ([]fs.DirEntry, error) {
|
||||||
|
files, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
uniq := files[:0]
|
||||||
|
for _, f := range files {
|
||||||
|
if !isSameDirSymlink(f, dir) {
|
||||||
|
uniq = append(uniq, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSameDirSymlink(f fs.DirEntry, dir string) bool {
|
||||||
|
if f.Type()&fs.ModeSymlink == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
target, err := os.Readlink(filepath.Join(dir, f.Name()))
|
||||||
|
return err == nil && !strings.Contains(target, "/")
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package adguard
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
@@ -9,10 +10,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ type agdguardRuleLine struct {
|
|||||||
isImportant bool
|
isImportant bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func Convert(reader io.Reader) ([]option.HeadlessRule, error) {
|
func ToOptions(reader io.Reader, logger logger.Logger) ([]option.HeadlessRule, error) {
|
||||||
scanner := bufio.NewScanner(reader)
|
scanner := bufio.NewScanner(reader)
|
||||||
var (
|
var (
|
||||||
ruleLines []agdguardRuleLine
|
ruleLines []agdguardRuleLine
|
||||||
@@ -36,7 +37,10 @@ func Convert(reader io.Reader) ([]option.HeadlessRule, error) {
|
|||||||
parseLine:
|
parseLine:
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
ruleLine := scanner.Text()
|
ruleLine := scanner.Text()
|
||||||
if ruleLine == "" || ruleLine[0] == '!' || ruleLine[0] == '#' {
|
if ruleLine == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(ruleLine, "!") || strings.HasPrefix(ruleLine, "#") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
originRuleLine := ruleLine
|
originRuleLine := ruleLine
|
||||||
@@ -92,7 +96,7 @@ parseLine:
|
|||||||
}
|
}
|
||||||
if !ignored {
|
if !ignored {
|
||||||
ignoredLines++
|
ignoredLines++
|
||||||
log.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", ruleLine)
|
logger.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", originRuleLine)
|
||||||
continue parseLine
|
continue parseLine
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,27 +124,35 @@ parseLine:
|
|||||||
ruleLine = ruleLine[1 : len(ruleLine)-1]
|
ruleLine = ruleLine[1 : len(ruleLine)-1]
|
||||||
if ignoreIPCIDRRegexp(ruleLine) {
|
if ignoreIPCIDRRegexp(ruleLine) {
|
||||||
ignoredLines++
|
ignoredLines++
|
||||||
log.Debug("ignored unsupported rule with IPCIDR regexp: ", ruleLine)
|
logger.Debug("ignored unsupported rule with IPCIDR regexp: ", originRuleLine)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
isRegexp = true
|
isRegexp = true
|
||||||
} else {
|
} else {
|
||||||
if strings.Contains(ruleLine, "://") {
|
if strings.Contains(ruleLine, "://") {
|
||||||
ruleLine = common.SubstringAfter(ruleLine, "://")
|
ruleLine = common.SubstringAfter(ruleLine, "://")
|
||||||
|
isSuffix = true
|
||||||
}
|
}
|
||||||
if strings.Contains(ruleLine, "/") {
|
if strings.Contains(ruleLine, "/") {
|
||||||
ignoredLines++
|
ignoredLines++
|
||||||
log.Debug("ignored unsupported rule with path: ", ruleLine)
|
logger.Debug("ignored unsupported rule with path: ", originRuleLine)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if strings.Contains(ruleLine, "##") {
|
if strings.Contains(ruleLine, "?") || strings.Contains(ruleLine, "&") {
|
||||||
ignoredLines++
|
ignoredLines++
|
||||||
log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
|
logger.Debug("ignored unsupported rule with query: ", originRuleLine)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if strings.Contains(ruleLine, "#$#") {
|
if strings.Contains(ruleLine, "[") || strings.Contains(ruleLine, "]") ||
|
||||||
|
strings.Contains(ruleLine, "(") || strings.Contains(ruleLine, ")") ||
|
||||||
|
strings.Contains(ruleLine, "!") || strings.Contains(ruleLine, "#") {
|
||||||
ignoredLines++
|
ignoredLines++
|
||||||
log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
|
logger.Debug("ignored unsupported cosmetic filter: ", originRuleLine)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(ruleLine, "~") {
|
||||||
|
ignoredLines++
|
||||||
|
logger.Debug("ignored unsupported rule modifier: ", originRuleLine)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var domainCheck string
|
var domainCheck string
|
||||||
@@ -151,7 +163,7 @@ parseLine:
|
|||||||
}
|
}
|
||||||
if ruleLine == "" {
|
if ruleLine == "" {
|
||||||
ignoredLines++
|
ignoredLines++
|
||||||
log.Debug("ignored unsupported rule with empty domain", originRuleLine)
|
logger.Debug("ignored unsupported rule with empty domain", originRuleLine)
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
domainCheck = strings.ReplaceAll(domainCheck, "*", "x")
|
domainCheck = strings.ReplaceAll(domainCheck, "*", "x")
|
||||||
@@ -159,13 +171,13 @@ parseLine:
|
|||||||
_, ipErr := parseADGuardIPCIDRLine(ruleLine)
|
_, ipErr := parseADGuardIPCIDRLine(ruleLine)
|
||||||
if ipErr == nil {
|
if ipErr == nil {
|
||||||
ignoredLines++
|
ignoredLines++
|
||||||
log.Debug("ignored unsupported rule with IPCIDR: ", ruleLine)
|
logger.Debug("ignored unsupported rule with IPCIDR: ", originRuleLine)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if M.ParseSocksaddr(domainCheck).Port != 0 {
|
if M.ParseSocksaddr(domainCheck).Port != 0 {
|
||||||
log.Debug("ignored unsupported rule with port: ", ruleLine)
|
logger.Debug("ignored unsupported rule with port: ", originRuleLine)
|
||||||
} else {
|
} else {
|
||||||
log.Debug("ignored unsupported rule with invalid domain: ", ruleLine)
|
logger.Debug("ignored unsupported rule with invalid domain: ", originRuleLine)
|
||||||
}
|
}
|
||||||
ignoredLines++
|
ignoredLines++
|
||||||
continue
|
continue
|
||||||
@@ -283,10 +295,112 @@ parseLine:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Info("parsed rules: ", len(ruleLines), "/", len(ruleLines)+ignoredLines)
|
if ignoredLines > 0 {
|
||||||
|
logger.Info("parsed rules: ", len(ruleLines), "/", len(ruleLines)+ignoredLines)
|
||||||
|
}
|
||||||
return []option.HeadlessRule{currentRule}, nil
|
return []option.HeadlessRule{currentRule}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ErrInvalid = E.New("invalid binary AdGuard rule-set")
|
||||||
|
|
||||||
|
func FromOptions(rules []option.HeadlessRule) ([]byte, error) {
|
||||||
|
if len(rules) != 1 {
|
||||||
|
return nil, ErrInvalid
|
||||||
|
}
|
||||||
|
rule := rules[0]
|
||||||
|
var (
|
||||||
|
importantDomain []string
|
||||||
|
importantDomainRegex []string
|
||||||
|
importantExcludeDomain []string
|
||||||
|
importantExcludeDomainRegex []string
|
||||||
|
domain []string
|
||||||
|
domainRegex []string
|
||||||
|
excludeDomain []string
|
||||||
|
excludeDomainRegex []string
|
||||||
|
)
|
||||||
|
parse:
|
||||||
|
for {
|
||||||
|
switch rule.Type {
|
||||||
|
case C.RuleTypeLogical:
|
||||||
|
if !(len(rule.LogicalOptions.Rules) == 2 && rule.LogicalOptions.Rules[0].Type == C.RuleTypeDefault) {
|
||||||
|
return nil, ErrInvalid
|
||||||
|
}
|
||||||
|
if rule.LogicalOptions.Mode == C.LogicalTypeAnd && rule.LogicalOptions.Rules[0].DefaultOptions.Invert {
|
||||||
|
if len(importantExcludeDomain) == 0 && len(importantExcludeDomainRegex) == 0 {
|
||||||
|
importantExcludeDomain = rule.LogicalOptions.Rules[0].DefaultOptions.AdGuardDomain
|
||||||
|
importantExcludeDomainRegex = rule.LogicalOptions.Rules[0].DefaultOptions.DomainRegex
|
||||||
|
if len(importantExcludeDomain)+len(importantExcludeDomainRegex) == 0 {
|
||||||
|
return nil, ErrInvalid
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
excludeDomain = rule.LogicalOptions.Rules[0].DefaultOptions.AdGuardDomain
|
||||||
|
excludeDomainRegex = rule.LogicalOptions.Rules[0].DefaultOptions.DomainRegex
|
||||||
|
if len(excludeDomain)+len(excludeDomainRegex) == 0 {
|
||||||
|
return nil, ErrInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if rule.LogicalOptions.Mode == C.LogicalTypeOr && !rule.LogicalOptions.Rules[0].DefaultOptions.Invert {
|
||||||
|
importantDomain = rule.LogicalOptions.Rules[0].DefaultOptions.AdGuardDomain
|
||||||
|
importantDomainRegex = rule.LogicalOptions.Rules[0].DefaultOptions.DomainRegex
|
||||||
|
if len(importantDomain)+len(importantDomainRegex) == 0 {
|
||||||
|
return nil, ErrInvalid
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, ErrInvalid
|
||||||
|
}
|
||||||
|
rule = rule.LogicalOptions.Rules[1]
|
||||||
|
case C.RuleTypeDefault:
|
||||||
|
domain = rule.DefaultOptions.AdGuardDomain
|
||||||
|
domainRegex = rule.DefaultOptions.DomainRegex
|
||||||
|
if len(domain)+len(domainRegex) == 0 {
|
||||||
|
return nil, ErrInvalid
|
||||||
|
}
|
||||||
|
break parse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var output bytes.Buffer
|
||||||
|
for _, ruleLine := range importantDomain {
|
||||||
|
output.WriteString(ruleLine)
|
||||||
|
output.WriteString("$important\n")
|
||||||
|
}
|
||||||
|
for _, ruleLine := range importantDomainRegex {
|
||||||
|
output.WriteString("/")
|
||||||
|
output.WriteString(ruleLine)
|
||||||
|
output.WriteString("/$important\n")
|
||||||
|
|
||||||
|
}
|
||||||
|
for _, ruleLine := range importantExcludeDomain {
|
||||||
|
output.WriteString("@@")
|
||||||
|
output.WriteString(ruleLine)
|
||||||
|
output.WriteString("$important\n")
|
||||||
|
}
|
||||||
|
for _, ruleLine := range importantExcludeDomainRegex {
|
||||||
|
output.WriteString("@@/")
|
||||||
|
output.WriteString(ruleLine)
|
||||||
|
output.WriteString("/$important\n")
|
||||||
|
}
|
||||||
|
for _, ruleLine := range domain {
|
||||||
|
output.WriteString(ruleLine)
|
||||||
|
output.WriteString("\n")
|
||||||
|
}
|
||||||
|
for _, ruleLine := range domainRegex {
|
||||||
|
output.WriteString("/")
|
||||||
|
output.WriteString(ruleLine)
|
||||||
|
output.WriteString("/\n")
|
||||||
|
}
|
||||||
|
for _, ruleLine := range excludeDomain {
|
||||||
|
output.WriteString("@@")
|
||||||
|
output.WriteString(ruleLine)
|
||||||
|
output.WriteString("\n")
|
||||||
|
}
|
||||||
|
for _, ruleLine := range excludeDomainRegex {
|
||||||
|
output.WriteString("@@/")
|
||||||
|
output.WriteString(ruleLine)
|
||||||
|
output.WriteString("/\n")
|
||||||
|
}
|
||||||
|
return output.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func ignoreIPCIDRRegexp(ruleLine string) bool {
|
func ignoreIPCIDRRegexp(ruleLine string) bool {
|
||||||
if strings.HasPrefix(ruleLine, "(http?:\\/\\/)") {
|
if strings.HasPrefix(ruleLine, "(http?:\\/\\/)") {
|
||||||
ruleLine = ruleLine[12:]
|
ruleLine = ruleLine[12:]
|
||||||
@@ -294,11 +408,9 @@ func ignoreIPCIDRRegexp(ruleLine string) bool {
|
|||||||
ruleLine = ruleLine[13:]
|
ruleLine = ruleLine[13:]
|
||||||
} else if strings.HasPrefix(ruleLine, "^") {
|
} else if strings.HasPrefix(ruleLine, "^") {
|
||||||
ruleLine = ruleLine[1:]
|
ruleLine = ruleLine[1:]
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
_, parseErr := strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8)
|
return common.Error(strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8)) == nil ||
|
||||||
return parseErr == nil
|
common.Error(strconv.ParseUint(common.SubstringBefore(ruleLine, "."), 10, 8)) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAdGuardHostLine(ruleLine string) (string, error) {
|
func parseAdGuardHostLine(ruleLine string) (string, error) {
|
||||||
@@ -342,5 +454,5 @@ func parseADGuardIPCIDRLine(ruleLine string) (netip.Prefix, error) {
|
|||||||
for len(ruleParts) < 4 {
|
for len(ruleParts) < 4 {
|
||||||
ruleParts = append(ruleParts, 0)
|
ruleParts = append(ruleParts, 0)
|
||||||
}
|
}
|
||||||
return netip.PrefixFrom(netip.AddrFrom4(*(*[4]byte)(ruleParts)), bitLen), nil
|
return netip.PrefixFrom(netip.AddrFrom4([4]byte(ruleParts)), bitLen), nil
|
||||||
}
|
}
|
||||||
@@ -7,13 +7,15 @@ import (
|
|||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/route/rule"
|
"github.com/sagernet/sing-box/route/rule"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConverter(t *testing.T) {
|
func TestConverter(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
rules, err := Convert(strings.NewReader(`
|
ruleString := `||sagernet.org^$important
|
||||||
|
@@|sing-box.sagernet.org^$important
|
||||||
||example.org^
|
||example.org^
|
||||||
|example.com^
|
|example.com^
|
||||||
example.net^
|
example.net^
|
||||||
@@ -21,10 +23,9 @@ example.net^
|
|||||||
||example.edu.tw^
|
||example.edu.tw^
|
||||||
|example.gov
|
|example.gov
|
||||||
example.arpa
|
example.arpa
|
||||||
@@|sagernet.example.org|
|
@@|sagernet.example.org^
|
||||||
||sagernet.org^$important
|
`
|
||||||
@@|sing-box.sagernet.org^$important
|
rules, err := ToOptions(strings.NewReader(ruleString), logger.NOP())
|
||||||
`))
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, rules, 1)
|
require.Len(t, rules, 1)
|
||||||
rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
|
rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
|
||||||
@@ -75,15 +76,18 @@ example.arpa
|
|||||||
Domain: domain,
|
Domain: domain,
|
||||||
}), domain)
|
}), domain)
|
||||||
}
|
}
|
||||||
|
ruleFromOptions, err := FromOptions(rules)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ruleString, string(ruleFromOptions))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHosts(t *testing.T) {
|
func TestHosts(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
rules, err := Convert(strings.NewReader(`
|
rules, err := ToOptions(strings.NewReader(`
|
||||||
127.0.0.1 localhost
|
127.0.0.1 localhost
|
||||||
::1 localhost #[IPv6]
|
::1 localhost #[IPv6]
|
||||||
0.0.0.0 google.com
|
0.0.0.0 google.com
|
||||||
`))
|
`), logger.NOP())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, rules, 1)
|
require.Len(t, rules, 1)
|
||||||
rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
|
rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
|
||||||
@@ -110,10 +114,10 @@ func TestHosts(t *testing.T) {
|
|||||||
|
|
||||||
func TestSimpleHosts(t *testing.T) {
|
func TestSimpleHosts(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
rules, err := Convert(strings.NewReader(`
|
rules, err := ToOptions(strings.NewReader(`
|
||||||
example.com
|
example.com
|
||||||
www.example.org
|
www.example.org
|
||||||
`))
|
`), logger.NOP())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, rules, 1)
|
require.Len(t, rules, 1)
|
||||||
rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
|
rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
|
||||||
@@ -10,16 +10,18 @@ import (
|
|||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/conntrack"
|
"github.com/sagernet/sing-box/common/conntrack"
|
||||||
|
"github.com/sagernet/sing-box/common/listener"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/atomic"
|
|
||||||
"github.com/sagernet/sing/common/control"
|
"github.com/sagernet/sing/common/control"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
|
"github.com/database64128/tfo-go/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -28,21 +30,21 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DefaultDialer struct {
|
type DefaultDialer struct {
|
||||||
dialer4 tcpDialer
|
dialer4 tfo.Dialer
|
||||||
dialer6 tcpDialer
|
dialer6 tfo.Dialer
|
||||||
udpDialer4 net.Dialer
|
udpDialer4 net.Dialer
|
||||||
udpDialer6 net.Dialer
|
udpDialer6 net.Dialer
|
||||||
udpListener net.ListenConfig
|
udpListener net.ListenConfig
|
||||||
udpAddr4 string
|
udpAddr4 string
|
||||||
udpAddr6 string
|
udpAddr6 string
|
||||||
isWireGuardListener bool
|
netns string
|
||||||
networkManager adapter.NetworkManager
|
networkManager adapter.NetworkManager
|
||||||
networkStrategy *C.NetworkStrategy
|
networkStrategy *C.NetworkStrategy
|
||||||
defaultNetworkStrategy bool
|
defaultNetworkStrategy bool
|
||||||
networkType []C.InterfaceType
|
networkType []C.InterfaceType
|
||||||
fallbackNetworkType []C.InterfaceType
|
fallbackNetworkType []C.InterfaceType
|
||||||
networkFallbackDelay time.Duration
|
networkFallbackDelay time.Duration
|
||||||
networkLastFallback atomic.TypedValue[time.Time]
|
networkLastFallback common.TypedValue[time.Time]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
|
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
|
||||||
@@ -65,23 +67,19 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
interfaceFinder = control.NewDefaultInterfaceFinder()
|
interfaceFinder = control.NewDefaultInterfaceFinder()
|
||||||
}
|
}
|
||||||
if options.BindInterface != "" {
|
if options.BindInterface != "" {
|
||||||
|
if !(C.IsLinux || C.IsDarwin || C.IsWindows) {
|
||||||
|
return nil, E.New("`bind_interface` is only supported on Linux, macOS and Windows")
|
||||||
|
}
|
||||||
bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1)
|
bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1)
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
}
|
}
|
||||||
if options.RoutingMark > 0 {
|
if options.RoutingMark > 0 {
|
||||||
dialer.Control = control.Append(dialer.Control, control.RoutingMark(uint32(options.RoutingMark)))
|
if !C.IsLinux {
|
||||||
listener.Control = control.Append(listener.Control, control.RoutingMark(uint32(options.RoutingMark)))
|
return nil, E.New("`routing_mark` is only supported on Linux")
|
||||||
}
|
|
||||||
if networkManager != nil {
|
|
||||||
autoRedirectOutputMark := networkManager.AutoRedirectOutputMark()
|
|
||||||
if autoRedirectOutputMark > 0 {
|
|
||||||
if options.RoutingMark > 0 {
|
|
||||||
return nil, E.New("`routing_mark` is conflict with `tun.auto_redirect` with `tun.route_[_exclude]_address_set")
|
|
||||||
}
|
|
||||||
dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark))
|
|
||||||
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
|
|
||||||
}
|
}
|
||||||
|
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
|
||||||
|
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
|
||||||
}
|
}
|
||||||
disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil
|
disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil
|
||||||
if disableDefaultBind || options.TCPFastOpen {
|
if disableDefaultBind || options.TCPFastOpen {
|
||||||
@@ -92,44 +90,47 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
|
|
||||||
if networkManager != nil {
|
if networkManager != nil {
|
||||||
defaultOptions := networkManager.DefaultOptions()
|
defaultOptions := networkManager.DefaultOptions()
|
||||||
if !disableDefaultBind {
|
if defaultOptions.BindInterface != "" {
|
||||||
if defaultOptions.BindInterface != "" {
|
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
||||||
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
|
} else if networkManager.AutoDetectInterface() && !disableDefaultBind {
|
||||||
|
if platformInterface != nil {
|
||||||
|
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
|
||||||
|
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 {
|
||||||
|
networkStrategy = defaultOptions.NetworkStrategy
|
||||||
|
networkType = defaultOptions.NetworkType
|
||||||
|
fallbackNetworkType = defaultOptions.FallbackNetworkType
|
||||||
|
}
|
||||||
|
networkFallbackDelay = time.Duration(options.FallbackDelay)
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
bindFunc := networkManager.AutoDetectInterfaceFunc()
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
} else if 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 {
|
|
||||||
networkStrategy = defaultOptions.NetworkStrategy
|
|
||||||
networkType = defaultOptions.NetworkType
|
|
||||||
fallbackNetworkType = defaultOptions.FallbackNetworkType
|
|
||||||
}
|
|
||||||
networkFallbackDelay = time.Duration(options.FallbackDelay)
|
|
||||||
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
|
|
||||||
networkFallbackDelay = defaultOptions.FallbackDelay
|
|
||||||
}
|
|
||||||
bindFunc := networkManager.ProtectFunc()
|
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
|
||||||
} else {
|
|
||||||
bindFunc := networkManager.AutoDetectInterfaceFunc()
|
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
|
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
|
||||||
dialer.Control = control.Append(dialer.Control, control.RoutingMark(defaultOptions.RoutingMark))
|
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
|
||||||
listener.Control = control.Append(listener.Control, control.RoutingMark(defaultOptions.RoutingMark))
|
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if networkManager != nil {
|
||||||
|
markFunc := networkManager.AutoRedirectOutputMarkFunc()
|
||||||
|
dialer.Control = control.Append(dialer.Control, markFunc)
|
||||||
|
listener.Control = control.Append(listener.Control, markFunc)
|
||||||
|
}
|
||||||
if options.ReuseAddr {
|
if options.ReuseAddr {
|
||||||
listener.Control = control.Append(listener.Control, control.ReuseAddr())
|
listener.Control = control.Append(listener.Control, control.ReuseAddr())
|
||||||
}
|
}
|
||||||
@@ -142,9 +143,18 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
} else {
|
} else {
|
||||||
dialer.Timeout = C.TCPConnectTimeout
|
dialer.Timeout = C.TCPConnectTimeout
|
||||||
}
|
}
|
||||||
// TODO: Add an option to customize the keep alive period
|
if !options.DisableTCPKeepAlive {
|
||||||
dialer.KeepAlive = C.TCPKeepAliveInitial
|
keepIdle := time.Duration(options.TCPKeepAlive)
|
||||||
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(C.TCPKeepAliveInitial, C.TCPKeepAliveInterval))
|
if keepIdle == 0 {
|
||||||
|
keepIdle = C.TCPKeepAliveInitial
|
||||||
|
}
|
||||||
|
keepInterval := time.Duration(options.TCPKeepAliveInterval)
|
||||||
|
if keepInterval == 0 {
|
||||||
|
keepInterval = C.TCPKeepAliveInterval
|
||||||
|
}
|
||||||
|
dialer.KeepAlive = keepIdle
|
||||||
|
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(keepIdle, keepInterval))
|
||||||
|
}
|
||||||
var udpFragment bool
|
var udpFragment bool
|
||||||
if options.UDPFragment != nil {
|
if options.UDPFragment != nil {
|
||||||
udpFragment = *options.UDPFragment
|
udpFragment = *options.UDPFragment
|
||||||
@@ -178,24 +188,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
|
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
|
||||||
}
|
}
|
||||||
if options.TCPMultiPath {
|
if options.TCPMultiPath {
|
||||||
if !go121Available {
|
dialer4.SetMultipathTCP(true)
|
||||||
return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
|
|
||||||
}
|
|
||||||
setMultiPathTCP(&dialer4)
|
|
||||||
}
|
|
||||||
if options.IsWireGuardListener {
|
|
||||||
for _, controlFn := range WgControlFns {
|
|
||||||
listener.Control = control.Append(listener.Control, controlFn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tcpDialer6, err := newTCPDialer(dialer6, options.TCPFastOpen)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
tcpDialer4 := tfo.Dialer{Dialer: dialer4, DisableTFO: !options.TCPFastOpen}
|
||||||
|
tcpDialer6 := tfo.Dialer{Dialer: dialer6, DisableTFO: !options.TCPFastOpen}
|
||||||
return &DefaultDialer{
|
return &DefaultDialer{
|
||||||
dialer4: tcpDialer4,
|
dialer4: tcpDialer4,
|
||||||
dialer6: tcpDialer6,
|
dialer6: tcpDialer6,
|
||||||
@@ -204,7 +200,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
udpListener: listener,
|
udpListener: listener,
|
||||||
udpAddr4: udpAddr4,
|
udpAddr4: udpAddr4,
|
||||||
udpAddr6: udpAddr6,
|
udpAddr6: udpAddr6,
|
||||||
isWireGuardListener: options.IsWireGuardListener,
|
netns: options.NetNs,
|
||||||
networkManager: networkManager,
|
networkManager: networkManager,
|
||||||
networkStrategy: networkStrategy,
|
networkStrategy: networkStrategy,
|
||||||
defaultNetworkStrategy: defaultNetworkStrategy,
|
defaultNetworkStrategy: defaultNetworkStrategy,
|
||||||
@@ -214,24 +210,44 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setMarkWrapper(networkManager adapter.NetworkManager, mark uint32, isDefault bool) control.Func {
|
||||||
|
if networkManager == nil {
|
||||||
|
return control.RoutingMark(mark)
|
||||||
|
}
|
||||||
|
return func(network, address string, conn syscall.RawConn) error {
|
||||||
|
if networkManager.AutoRedirectOutputMark() != 0 {
|
||||||
|
if isDefault {
|
||||||
|
return E.New("`route.default_mark` is conflict with `tun.auto_redirect`")
|
||||||
|
} else {
|
||||||
|
return E.New("`routing_mark` is conflict with `tun.auto_redirect`")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return control.RoutingMark(mark)(network, address, conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
|
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
|
||||||
if !address.IsValid() {
|
if !address.IsValid() {
|
||||||
return nil, E.New("invalid address")
|
return nil, E.New("invalid address")
|
||||||
|
} else if address.IsFqdn() {
|
||||||
|
return nil, E.New("domain not resolved")
|
||||||
}
|
}
|
||||||
if d.networkStrategy == nil {
|
if d.networkStrategy == nil {
|
||||||
switch N.NetworkName(network) {
|
return trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) {
|
||||||
case N.NetworkUDP:
|
switch N.NetworkName(network) {
|
||||||
if !address.IsIPv6() {
|
case N.NetworkUDP:
|
||||||
return trackConn(d.udpDialer4.DialContext(ctx, network, address.String()))
|
if !address.IsIPv6() {
|
||||||
} else {
|
return d.udpDialer4.DialContext(ctx, network, address.String())
|
||||||
return trackConn(d.udpDialer6.DialContext(ctx, network, address.String()))
|
} else {
|
||||||
|
return d.udpDialer6.DialContext(ctx, network, address.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if !address.IsIPv6() {
|
||||||
if !address.IsIPv6() {
|
return DialSlowContext(&d.dialer4, ctx, network, address)
|
||||||
return trackConn(DialSlowContext(&d.dialer4, ctx, network, address))
|
} else {
|
||||||
} else {
|
return DialSlowContext(&d.dialer6, ctx, network, address)
|
||||||
return trackConn(DialSlowContext(&d.dialer6, ctx, network, address))
|
}
|
||||||
}
|
}))
|
||||||
} else {
|
} else {
|
||||||
return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
|
return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
|
||||||
}
|
}
|
||||||
@@ -255,11 +271,11 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
|
|||||||
}
|
}
|
||||||
var dialer net.Dialer
|
var dialer net.Dialer
|
||||||
if N.NetworkName(network) == N.NetworkTCP {
|
if N.NetworkName(network) == N.NetworkTCP {
|
||||||
dialer = dialerFromTCPDialer(d.dialer4)
|
dialer = d.dialer4.Dialer
|
||||||
} else {
|
} else {
|
||||||
dialer = d.udpDialer4
|
dialer = d.udpDialer4
|
||||||
}
|
}
|
||||||
fastFallback := time.Now().Sub(d.networkLastFallback.Load()) < C.TCPTimeout
|
fastFallback := time.Since(d.networkLastFallback.Load()) < C.TCPTimeout
|
||||||
var (
|
var (
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
isPrimary bool
|
isPrimary bool
|
||||||
@@ -287,18 +303,28 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
|
|||||||
|
|
||||||
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
if d.networkStrategy == nil {
|
if d.networkStrategy == nil {
|
||||||
if destination.IsIPv6() {
|
return trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) {
|
||||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6))
|
if destination.IsIPv6() {
|
||||||
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)
|
||||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4))
|
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
||||||
} else {
|
return d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4)
|
||||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4))
|
} else {
|
||||||
}
|
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4)
|
||||||
|
}
|
||||||
|
}))
|
||||||
} else {
|
} else {
|
||||||
return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
|
return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DefaultDialer) DialerForICMPDestination(destination netip.Addr) net.Dialer {
|
||||||
|
if !destination.Is6() {
|
||||||
|
return d.dialer6.Dialer
|
||||||
|
} else {
|
||||||
|
return d.dialer4.Dialer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
||||||
if strategy == nil {
|
if strategy == nil {
|
||||||
strategy = d.networkStrategy
|
strategy = d.networkStrategy
|
||||||
@@ -332,8 +358,8 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina
|
|||||||
return trackPacketConn(packetConn, nil)
|
return trackPacketConn(packetConn, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
|
func (d *DefaultDialer) WireGuardControl() control.Func {
|
||||||
return d.udpListener.ListenPacket(context.Background(), network, address)
|
return d.udpListener.Control
|
||||||
}
|
}
|
||||||
|
|
||||||
func trackConn(conn net.Conn, err error) (net.Conn, error) {
|
func trackConn(conn net.Conn, err error) (net.Conn, error) {
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
//go:build go1.20
|
|
||||||
|
|
||||||
package dialer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/metacubex/tfo-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
type tcpDialer = tfo.Dialer
|
|
||||||
|
|
||||||
func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) {
|
|
||||||
return tfo.Dialer{Dialer: dialer, DisableTFO: !tfoEnabled}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func dialerFromTCPDialer(dialer tcpDialer) net.Dialer {
|
|
||||||
return dialer.Dialer
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
//go:build go1.21
|
|
||||||
|
|
||||||
package dialer
|
|
||||||
|
|
||||||
import "net"
|
|
||||||
|
|
||||||
const go121Available = true
|
|
||||||
|
|
||||||
func setMultiPathTCP(dialer *net.Dialer) {
|
|
||||||
dialer.SetMultipathTCP(true)
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
//go:build !go1.20
|
|
||||||
|
|
||||||
package dialer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
type tcpDialer = net.Dialer
|
|
||||||
|
|
||||||
func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) {
|
|
||||||
if tfoEnabled {
|
|
||||||
return dialer, E.New("TCP Fast Open requires go1.20, please recompile your binary.")
|
|
||||||
}
|
|
||||||
return dialer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func dialerFromTCPDialer(dialer tcpDialer) net.Dialer {
|
|
||||||
return dialer
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
//go:build !go1.21
|
|
||||||
|
|
||||||
package dialer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
const go121Available = false
|
|
||||||
|
|
||||||
func setMultiPathTCP(dialer *net.Dialer) {
|
|
||||||
}
|
|
||||||
@@ -18,6 +18,7 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di
|
|||||||
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
||||||
return nil, false, E.New("no available network interface")
|
return nil, false, E.New("no available network interface")
|
||||||
}
|
}
|
||||||
|
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
|
||||||
if fallbackDelay == 0 {
|
if fallbackDelay == 0 {
|
||||||
fallbackDelay = N.DefaultFallbackDelay
|
fallbackDelay = N.DefaultFallbackDelay
|
||||||
}
|
}
|
||||||
@@ -31,7 +32,9 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di
|
|||||||
results := make(chan dialResult) // unbuffered
|
results := make(chan dialResult) // unbuffered
|
||||||
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
|
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
|
||||||
perNetDialer := dialer
|
perNetDialer := dialer
|
||||||
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
if defaultInterface == nil || iif.Index != defaultInterface.Index {
|
||||||
|
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
||||||
|
}
|
||||||
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
select {
|
select {
|
||||||
@@ -89,6 +92,7 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d
|
|||||||
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
||||||
return nil, false, E.New("no available network interface")
|
return nil, false, E.New("no available network interface")
|
||||||
}
|
}
|
||||||
|
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
|
||||||
if fallbackDelay == 0 {
|
if fallbackDelay == 0 {
|
||||||
fallbackDelay = N.DefaultFallbackDelay
|
fallbackDelay = N.DefaultFallbackDelay
|
||||||
}
|
}
|
||||||
@@ -103,7 +107,9 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d
|
|||||||
results := make(chan dialResult) // unbuffered
|
results := make(chan dialResult) // unbuffered
|
||||||
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
|
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
|
||||||
perNetDialer := dialer
|
perNetDialer := dialer
|
||||||
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
if defaultInterface == nil || iif.Index != defaultInterface.Index {
|
||||||
|
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
||||||
|
}
|
||||||
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
select {
|
select {
|
||||||
@@ -149,10 +155,13 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene
|
|||||||
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
||||||
return nil, E.New("no available network interface")
|
return nil, E.New("no available network interface")
|
||||||
}
|
}
|
||||||
|
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
|
||||||
var errors []error
|
var errors []error
|
||||||
for _, primaryInterface := range primaryInterfaces {
|
for _, primaryInterface := range primaryInterfaces {
|
||||||
perNetListener := listener
|
perNetListener := listener
|
||||||
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, primaryInterface.Name, primaryInterface.Index))
|
if defaultInterface == nil || primaryInterface.Index != defaultInterface.Index {
|
||||||
|
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, primaryInterface.Name, primaryInterface.Index))
|
||||||
|
}
|
||||||
conn, err := perNetListener.ListenPacket(ctx, network, addr)
|
conn, err := perNetListener.ListenPacket(ctx, network, addr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return conn, nil
|
return conn, nil
|
||||||
@@ -161,7 +170,9 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene
|
|||||||
}
|
}
|
||||||
for _, fallbackInterface := range fallbackInterfaces {
|
for _, fallbackInterface := range fallbackInterfaces {
|
||||||
perNetListener := listener
|
perNetListener := listener
|
||||||
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, fallbackInterface.Name, fallbackInterface.Index))
|
if defaultInterface == nil || fallbackInterface.Index != defaultInterface.Index {
|
||||||
|
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, fallbackInterface.Name, fallbackInterface.Index))
|
||||||
|
}
|
||||||
conn, err := perNetListener.ListenPacket(ctx, network, addr)
|
conn, err := perNetListener.ListenPacket(ctx, network, addr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return conn, nil
|
return conn, nil
|
||||||
|
|||||||
@@ -6,39 +6,63 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type DirectDialer interface {
|
||||||
|
IsEmpty() bool
|
||||||
|
}
|
||||||
|
|
||||||
type DetourDialer struct {
|
type DetourDialer struct {
|
||||||
outboundManager adapter.OutboundManager
|
outboundManager adapter.OutboundManager
|
||||||
detour string
|
detour string
|
||||||
|
legacyDNSDialer bool
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
initOnce sync.Once
|
initOnce sync.Once
|
||||||
initErr error
|
initErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDetour(outboundManager adapter.OutboundManager, detour string) N.Dialer {
|
func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNSDialer bool) N.Dialer {
|
||||||
return &DetourDialer{outboundManager: outboundManager, detour: detour}
|
return &DetourDialer{
|
||||||
|
outboundManager: outboundManager,
|
||||||
|
detour: detour,
|
||||||
|
legacyDNSDialer: legacyDNSDialer,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DetourDialer) Start() error {
|
func InitializeDetour(dialer N.Dialer) error {
|
||||||
_, err := d.Dialer()
|
detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
|
||||||
return err
|
if !isDetour {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return common.Error(detourDialer.Dialer())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DetourDialer) Dialer() (N.Dialer, error) {
|
func (d *DetourDialer) Dialer() (N.Dialer, error) {
|
||||||
d.initOnce.Do(func() {
|
d.initOnce.Do(d.init)
|
||||||
var loaded bool
|
|
||||||
d.dialer, loaded = d.outboundManager.Outbound(d.detour)
|
|
||||||
if !loaded {
|
|
||||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return d.dialer, d.initErr
|
return d.dialer, d.initErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DetourDialer) init() {
|
||||||
|
dialer, loaded := d.outboundManager.Outbound(d.detour)
|
||||||
|
if !loaded {
|
||||||
|
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !d.legacyDNSDialer {
|
||||||
|
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
|
||||||
|
if directDialer.IsEmpty() {
|
||||||
|
d.initErr = E.New("detour to an empty direct outbound makes no sense")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.dialer = dialer
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DetourDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func (d *DetourDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
dialer, err := d.Dialer()
|
dialer, err := d.Dialer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -16,81 +16,125 @@ import (
|
|||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
Context context.Context
|
||||||
|
Options option.DialerOptions
|
||||||
|
RemoteIsDomain bool
|
||||||
|
DirectResolver bool
|
||||||
|
ResolverOnDetour bool
|
||||||
|
NewDialer bool
|
||||||
|
LegacyDNSDialer bool
|
||||||
|
DirectOutbound bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: merge with NewWithOptions
|
||||||
func New(ctx context.Context, options option.DialerOptions, remoteIsDomain bool) (N.Dialer, error) {
|
func New(ctx context.Context, options option.DialerOptions, remoteIsDomain bool) (N.Dialer, error) {
|
||||||
if options.IsWireGuardListener {
|
return NewWithOptions(Options{
|
||||||
return NewDefault(ctx, options)
|
Context: ctx,
|
||||||
}
|
Options: options,
|
||||||
|
RemoteIsDomain: remoteIsDomain,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWithOptions(options Options) (N.Dialer, error) {
|
||||||
|
dialOptions := options.Options
|
||||||
var (
|
var (
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if options.Detour == "" {
|
if dialOptions.Detour != "" {
|
||||||
dialer, err = NewDefault(ctx, options)
|
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
outboundManager := service.FromContext[adapter.OutboundManager](ctx)
|
|
||||||
if outboundManager == nil {
|
if outboundManager == nil {
|
||||||
return nil, E.New("missing outbound manager")
|
return nil, E.New("missing outbound manager")
|
||||||
}
|
}
|
||||||
dialer = NewDetour(outboundManager, options.Detour)
|
dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
|
||||||
}
|
} else {
|
||||||
if remoteIsDomain && options.Detour == "" && options.DomainResolver == "" {
|
dialer, err = NewDefault(options.Context, dialOptions)
|
||||||
deprecated.Report(ctx, deprecated.OptionMissingDomainResolverInDialOptions)
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
if (options.Detour == "" && remoteIsDomain) || options.DomainResolver != "" {
|
|
||||||
router := service.FromContext[adapter.DNSRouter](ctx)
|
|
||||||
if router != nil {
|
|
||||||
var resolveTransport adapter.DNSTransport
|
|
||||||
if options.DomainResolver != "" {
|
|
||||||
transport, loaded := service.FromContext[adapter.DNSTransportManager](ctx).Transport(options.DomainResolver)
|
|
||||||
if !loaded {
|
|
||||||
return nil, E.New("DNS server not found: " + options.DomainResolver)
|
|
||||||
}
|
|
||||||
resolveTransport = transport
|
|
||||||
}
|
|
||||||
dialer = NewResolveDialer(
|
|
||||||
router,
|
|
||||||
dialer,
|
|
||||||
options.Detour == "" && !options.TCPFastOpen,
|
|
||||||
resolveTransport,
|
|
||||||
C.DomainStrategy(options.DomainStrategy),
|
|
||||||
time.Duration(options.FallbackDelay))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
||||||
|
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
|
||||||
|
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
|
||||||
|
var defaultOptions adapter.NetworkOptions
|
||||||
|
if networkManager != nil {
|
||||||
|
defaultOptions = networkManager.DefaultOptions()
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
server string
|
||||||
|
dnsQueryOptions adapter.DNSQueryOptions
|
||||||
|
resolveFallbackDelay time.Duration
|
||||||
|
)
|
||||||
|
if dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "" {
|
||||||
|
var transport adapter.DNSTransport
|
||||||
|
if !options.DirectResolver {
|
||||||
|
var loaded bool
|
||||||
|
transport, loaded = dnsTransport.Transport(dialOptions.DomainResolver.Server)
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("domain resolver not found: " + dialOptions.DomainResolver.Server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var strategy C.DomainStrategy
|
||||||
|
if dialOptions.DomainResolver.Strategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
||||||
|
strategy = C.DomainStrategy(dialOptions.DomainResolver.Strategy)
|
||||||
|
} else if
|
||||||
|
//nolint:staticcheck
|
||||||
|
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
||||||
|
//nolint:staticcheck
|
||||||
|
strategy = C.DomainStrategy(dialOptions.DomainStrategy)
|
||||||
|
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
|
||||||
|
}
|
||||||
|
server = dialOptions.DomainResolver.Server
|
||||||
|
dnsQueryOptions = adapter.DNSQueryOptions{
|
||||||
|
Transport: transport,
|
||||||
|
Strategy: strategy,
|
||||||
|
DisableCache: dialOptions.DomainResolver.DisableCache,
|
||||||
|
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
||||||
|
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||||
|
}
|
||||||
|
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||||
|
} else if options.DirectResolver {
|
||||||
|
return nil, E.New("missing domain resolver for domain server address")
|
||||||
|
} else {
|
||||||
|
if defaultOptions.DomainResolver != "" {
|
||||||
|
dnsQueryOptions = defaultOptions.DomainResolveOptions
|
||||||
|
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
|
||||||
|
}
|
||||||
|
dnsQueryOptions.Transport = transport
|
||||||
|
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||||
|
} else {
|
||||||
|
transports := dnsTransport.Transports()
|
||||||
|
if len(transports) < 2 {
|
||||||
|
dnsQueryOptions.Transport = dnsTransport.Default()
|
||||||
|
} else if options.NewDialer {
|
||||||
|
return nil, E.New("missing domain resolver for domain server address")
|
||||||
|
} else {
|
||||||
|
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if
|
||||||
|
//nolint:staticcheck
|
||||||
|
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
||||||
|
//nolint:staticcheck
|
||||||
|
dnsQueryOptions.Strategy = C.DomainStrategy(dialOptions.DomainStrategy)
|
||||||
|
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialer = NewResolveDialer(
|
||||||
|
options.Context,
|
||||||
|
dialer,
|
||||||
|
dialOptions.Detour == "" && !dialOptions.TCPFastOpen,
|
||||||
|
server,
|
||||||
|
dnsQueryOptions,
|
||||||
|
resolveFallbackDelay,
|
||||||
|
)
|
||||||
|
}
|
||||||
return dialer, nil
|
return dialer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDirect(ctx context.Context, options option.DialerOptions) (ParallelInterfaceDialer, error) {
|
|
||||||
if options.Detour != "" {
|
|
||||||
return nil, E.New("`detour` is not supported in direct context")
|
|
||||||
}
|
|
||||||
if options.IsWireGuardListener {
|
|
||||||
return NewDefault(ctx, options)
|
|
||||||
}
|
|
||||||
dialer, err := NewDefault(ctx, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var resolveTransport adapter.DNSTransport
|
|
||||||
if options.DomainResolver != "" {
|
|
||||||
transport, loaded := service.FromContext[adapter.DNSTransportManager](ctx).Transport(options.DomainResolver)
|
|
||||||
if !loaded {
|
|
||||||
return nil, E.New("DNS server not found: " + options.DomainResolver)
|
|
||||||
}
|
|
||||||
resolveTransport = transport
|
|
||||||
}
|
|
||||||
return NewResolveParallelInterfaceDialer(
|
|
||||||
service.FromContext[adapter.DNSRouter](ctx),
|
|
||||||
dialer,
|
|
||||||
true,
|
|
||||||
resolveTransport,
|
|
||||||
C.DomainStrategy(options.DomainStrategy),
|
|
||||||
time.Duration(options.FallbackDelay),
|
|
||||||
), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ParallelInterfaceDialer interface {
|
type ParallelInterfaceDialer interface {
|
||||||
N.Dialer
|
N.Dialer
|
||||||
DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
||||||
|
|||||||
@@ -3,14 +3,17 @@ package dialer
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -18,23 +21,51 @@ var (
|
|||||||
_ ParallelInterfaceDialer = (*resolveParallelNetworkDialer)(nil)
|
_ ParallelInterfaceDialer = (*resolveParallelNetworkDialer)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ResolveDialer interface {
|
||||||
|
N.Dialer
|
||||||
|
QueryOptions() adapter.DNSQueryOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParallelInterfaceResolveDialer interface {
|
||||||
|
ParallelInterfaceDialer
|
||||||
|
QueryOptions() adapter.DNSQueryOptions
|
||||||
|
}
|
||||||
|
|
||||||
type resolveDialer struct {
|
type resolveDialer struct {
|
||||||
|
transport adapter.DNSTransportManager
|
||||||
|
router adapter.DNSRouter
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
parallel bool
|
parallel bool
|
||||||
router adapter.DNSRouter
|
server string
|
||||||
transport adapter.DNSTransport
|
initOnce sync.Once
|
||||||
strategy C.DomainStrategy
|
initErr error
|
||||||
|
queryOptions adapter.DNSQueryOptions
|
||||||
fallbackDelay time.Duration
|
fallbackDelay time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResolveDialer(router adapter.DNSRouter, dialer N.Dialer, parallel bool, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) N.Dialer {
|
func NewResolveDialer(ctx context.Context, dialer N.Dialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ResolveDialer {
|
||||||
|
if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel {
|
||||||
|
return &resolveParallelNetworkDialer{
|
||||||
|
resolveDialer{
|
||||||
|
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
||||||
|
router: service.FromContext[adapter.DNSRouter](ctx),
|
||||||
|
dialer: dialer,
|
||||||
|
parallel: parallel,
|
||||||
|
server: server,
|
||||||
|
queryOptions: queryOptions,
|
||||||
|
fallbackDelay: fallbackDelay,
|
||||||
|
},
|
||||||
|
parallelDialer,
|
||||||
|
}
|
||||||
|
}
|
||||||
return &resolveDialer{
|
return &resolveDialer{
|
||||||
dialer,
|
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
||||||
parallel,
|
router: service.FromContext[adapter.DNSRouter](ctx),
|
||||||
router,
|
dialer: dialer,
|
||||||
transport,
|
parallel: parallel,
|
||||||
strategy,
|
server: server,
|
||||||
fallbackDelay,
|
queryOptions: queryOptions,
|
||||||
|
fallbackDelay: fallbackDelay,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,42 +74,53 @@ type resolveParallelNetworkDialer struct {
|
|||||||
dialer ParallelInterfaceDialer
|
dialer ParallelInterfaceDialer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResolveParallelInterfaceDialer(router adapter.DNSRouter, dialer ParallelInterfaceDialer, parallel bool, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) ParallelInterfaceDialer {
|
func (d *resolveDialer) initialize() error {
|
||||||
return &resolveParallelNetworkDialer{
|
d.initOnce.Do(d.initServer)
|
||||||
resolveDialer{
|
return d.initErr
|
||||||
dialer,
|
}
|
||||||
parallel,
|
|
||||||
router,
|
func (d *resolveDialer) initServer() {
|
||||||
transport,
|
if d.server == "" {
|
||||||
strategy,
|
return
|
||||||
fallbackDelay,
|
|
||||||
},
|
|
||||||
dialer,
|
|
||||||
}
|
}
|
||||||
|
transport, loaded := d.transport.Transport(d.server)
|
||||||
|
if !loaded {
|
||||||
|
d.initErr = E.New("domain resolver not found: " + d.server)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.queryOptions.Transport = transport
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *resolveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func (d *resolveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
err := d.initialize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsFqdn() {
|
||||||
return d.dialer.DialContext(ctx, network, destination)
|
return d.dialer.DialContext(ctx, network, destination)
|
||||||
}
|
}
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
addresses, err := d.router.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{Transport: d.transport, Strategy: d.strategy})
|
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if d.parallel {
|
if d.parallel {
|
||||||
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
|
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
|
||||||
} else {
|
} else {
|
||||||
return N.DialSerial(ctx, d.dialer, network, destination, addresses)
|
return N.DialSerial(ctx, d.dialer, network, destination, addresses)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
|
err := d.initialize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsFqdn() {
|
||||||
return d.dialer.ListenPacket(ctx, destination)
|
return d.dialer.ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
addresses, err := d.router.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{Transport: d.transport, Strategy: d.strategy})
|
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -89,12 +131,24 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
|||||||
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *resolveDialer) QueryOptions() adapter.DNSQueryOptions {
|
||||||
|
return d.queryOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *resolveDialer) Upstream() any {
|
||||||
|
return d.dialer
|
||||||
|
}
|
||||||
|
|
||||||
func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
||||||
|
err := d.initialize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsFqdn() {
|
||||||
return d.dialer.DialContext(ctx, network, destination)
|
return d.dialer.DialContext(ctx, network, destination)
|
||||||
}
|
}
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
addresses, err := d.router.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{Transport: d.transport, Strategy: d.strategy})
|
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -102,21 +156,28 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
|
|||||||
fallbackDelay = d.fallbackDelay
|
fallbackDelay = d.fallbackDelay
|
||||||
}
|
}
|
||||||
if d.parallel {
|
if d.parallel {
|
||||||
return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.strategy == C.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
} else {
|
} else {
|
||||||
return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
||||||
|
err := d.initialize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsFqdn() {
|
||||||
return d.dialer.ListenPacket(ctx, destination)
|
return d.dialer.ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
addresses, err := d.router.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{Transport: d.transport, Strategy: d.strategy})
|
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if fallbackDelay == 0 {
|
||||||
|
fallbackDelay = d.fallbackDelay
|
||||||
|
}
|
||||||
conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -124,6 +185,10 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C
|
|||||||
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *resolveDialer) Upstream() any {
|
func (d *resolveParallelNetworkDialer) QueryOptions() adapter.DNSQueryOptions {
|
||||||
|
return d.queryOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *resolveParallelNetworkDialer) Upstream() any {
|
||||||
return d.dialer
|
return d.dialer
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//go:build go1.20
|
|
||||||
|
|
||||||
package dialer
|
package dialer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -8,15 +6,15 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
"github.com/metacubex/tfo-go"
|
"github.com/database64128/tfo-go/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type slowOpenConn struct {
|
type slowOpenConn struct {
|
||||||
@@ -24,13 +22,15 @@ type slowOpenConn struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
network string
|
network string
|
||||||
destination M.Socksaddr
|
destination M.Socksaddr
|
||||||
conn net.Conn
|
conn atomic.Pointer[net.TCPConn]
|
||||||
create chan struct{}
|
create chan struct{}
|
||||||
|
done chan struct{}
|
||||||
access sync.Mutex
|
access sync.Mutex
|
||||||
|
closeOnce sync.Once
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func DialSlowContext(dialer *tfo.Dialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
if dialer.DisableTFO || N.NetworkName(network) != N.NetworkTCP {
|
if dialer.DisableTFO || N.NetworkName(network) != N.NetworkTCP {
|
||||||
switch N.NetworkName(network) {
|
switch N.NetworkName(network) {
|
||||||
case N.NetworkTCP, N.NetworkUDP:
|
case N.NetworkTCP, N.NetworkUDP:
|
||||||
@@ -45,26 +45,30 @@ func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, des
|
|||||||
network: network,
|
network: network,
|
||||||
destination: destination,
|
destination: destination,
|
||||||
create: make(chan struct{}),
|
create: make(chan struct{}),
|
||||||
|
done: make(chan struct{}),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) Read(b []byte) (n int, err error) {
|
func (c *slowOpenConn) Read(b []byte) (n int, err error) {
|
||||||
if c.conn == nil {
|
conn := c.conn.Load()
|
||||||
select {
|
if conn != nil {
|
||||||
case <-c.create:
|
return conn.Read(b)
|
||||||
if c.err != nil {
|
}
|
||||||
return 0, c.err
|
select {
|
||||||
}
|
case <-c.create:
|
||||||
case <-c.ctx.Done():
|
if c.err != nil {
|
||||||
return 0, c.ctx.Err()
|
return 0, c.err
|
||||||
}
|
}
|
||||||
|
return c.conn.Load().Read(b)
|
||||||
|
case <-c.done:
|
||||||
|
return 0, os.ErrClosed
|
||||||
}
|
}
|
||||||
return c.conn.Read(b)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
||||||
if c.conn != nil {
|
tcpConn := c.conn.Load()
|
||||||
return c.conn.Write(b)
|
if tcpConn != nil {
|
||||||
|
return tcpConn.Write(b)
|
||||||
}
|
}
|
||||||
c.access.Lock()
|
c.access.Lock()
|
||||||
defer c.access.Unlock()
|
defer c.access.Unlock()
|
||||||
@@ -73,13 +77,16 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
|||||||
if c.err != nil {
|
if c.err != nil {
|
||||||
return 0, c.err
|
return 0, c.err
|
||||||
}
|
}
|
||||||
return c.conn.Write(b)
|
return c.conn.Load().Write(b)
|
||||||
|
case <-c.done:
|
||||||
|
return 0, os.ErrClosed
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
c.conn, err = c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
|
conn, err := c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.conn = nil
|
c.err = err
|
||||||
c.err = E.Cause(err, "dial tcp fast open")
|
} else {
|
||||||
|
c.conn.Store(conn.(*net.TCPConn))
|
||||||
}
|
}
|
||||||
n = len(b)
|
n = len(b)
|
||||||
close(c.create)
|
close(c.create)
|
||||||
@@ -87,74 +94,87 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) Close() error {
|
func (c *slowOpenConn) Close() error {
|
||||||
return common.Close(c.conn)
|
c.closeOnce.Do(func() {
|
||||||
|
close(c.done)
|
||||||
|
conn := c.conn.Load()
|
||||||
|
if conn != nil {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) LocalAddr() net.Addr {
|
func (c *slowOpenConn) LocalAddr() net.Addr {
|
||||||
if c.conn == nil {
|
conn := c.conn.Load()
|
||||||
|
if conn == nil {
|
||||||
return M.Socksaddr{}
|
return M.Socksaddr{}
|
||||||
}
|
}
|
||||||
return c.conn.LocalAddr()
|
return conn.LocalAddr()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) RemoteAddr() net.Addr {
|
func (c *slowOpenConn) RemoteAddr() net.Addr {
|
||||||
if c.conn == nil {
|
conn := c.conn.Load()
|
||||||
|
if conn == nil {
|
||||||
return M.Socksaddr{}
|
return M.Socksaddr{}
|
||||||
}
|
}
|
||||||
return c.conn.RemoteAddr()
|
return conn.RemoteAddr()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) SetDeadline(t time.Time) error {
|
func (c *slowOpenConn) SetDeadline(t time.Time) error {
|
||||||
if c.conn == nil {
|
conn := c.conn.Load()
|
||||||
|
if conn == nil {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
return c.conn.SetDeadline(t)
|
return conn.SetDeadline(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) SetReadDeadline(t time.Time) error {
|
func (c *slowOpenConn) SetReadDeadline(t time.Time) error {
|
||||||
if c.conn == nil {
|
conn := c.conn.Load()
|
||||||
|
if conn == nil {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
return c.conn.SetReadDeadline(t)
|
return conn.SetReadDeadline(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) SetWriteDeadline(t time.Time) error {
|
func (c *slowOpenConn) SetWriteDeadline(t time.Time) error {
|
||||||
if c.conn == nil {
|
conn := c.conn.Load()
|
||||||
|
if conn == nil {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
return c.conn.SetWriteDeadline(t)
|
return conn.SetWriteDeadline(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) Upstream() any {
|
func (c *slowOpenConn) Upstream() any {
|
||||||
return c.conn
|
return common.PtrOrNil(c.conn.Load())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) ReaderReplaceable() bool {
|
func (c *slowOpenConn) ReaderReplaceable() bool {
|
||||||
return c.conn != nil
|
return c.conn.Load() != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) WriterReplaceable() bool {
|
func (c *slowOpenConn) WriterReplaceable() bool {
|
||||||
return c.conn != nil
|
return c.conn.Load() != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) LazyHeadroom() bool {
|
func (c *slowOpenConn) LazyHeadroom() bool {
|
||||||
return c.conn == nil
|
return c.conn.Load() == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) NeedHandshake() bool {
|
func (c *slowOpenConn) NeedHandshake() bool {
|
||||||
return c.conn == nil
|
return c.conn.Load() == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
|
func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
if c.conn == nil {
|
conn := c.conn.Load()
|
||||||
|
if conn == nil {
|
||||||
select {
|
select {
|
||||||
case <-c.create:
|
case <-c.create:
|
||||||
if c.err != nil {
|
if c.err != nil {
|
||||||
return 0, c.err
|
return 0, c.err
|
||||||
}
|
}
|
||||||
case <-c.ctx.Done():
|
case <-c.done:
|
||||||
return 0, c.ctx.Err()
|
return 0, c.err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bufio.Copy(w, c.conn)
|
return bufio.Copy(w, c.conn.Load())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
//go:build !go1.20
|
|
||||||
|
|
||||||
package dialer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
|
||||||
switch N.NetworkName(network) {
|
|
||||||
case N.NetworkTCP, N.NetworkUDP:
|
|
||||||
return dialer.DialContext(ctx, network, destination.String())
|
|
||||||
default:
|
|
||||||
return dialer.DialContext(ctx, network, destination.AddrString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
package dialer
|
package dialer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/control"
|
"github.com/sagernet/sing/common/control"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WireGuardListener interface {
|
type WireGuardListener interface {
|
||||||
ListenPacketCompat(network, address string) (net.PacketConn, error)
|
WireGuardControl() control.Func
|
||||||
}
|
}
|
||||||
|
|
||||||
var WgControlFns []control.Func
|
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
package humanize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IEC Sizes.
|
|
||||||
// kibis of bits
|
|
||||||
const (
|
|
||||||
Byte = 1 << (iota * 10)
|
|
||||||
KiByte
|
|
||||||
MiByte
|
|
||||||
GiByte
|
|
||||||
TiByte
|
|
||||||
PiByte
|
|
||||||
EiByte
|
|
||||||
)
|
|
||||||
|
|
||||||
// SI Sizes.
|
|
||||||
const (
|
|
||||||
IByte = 1
|
|
||||||
KByte = IByte * 1000
|
|
||||||
MByte = KByte * 1000
|
|
||||||
GByte = MByte * 1000
|
|
||||||
TByte = GByte * 1000
|
|
||||||
PByte = TByte * 1000
|
|
||||||
EByte = PByte * 1000
|
|
||||||
)
|
|
||||||
|
|
||||||
var defaultSizeTable = map[string]uint64{
|
|
||||||
"b": Byte,
|
|
||||||
"kib": KiByte,
|
|
||||||
"kb": KByte,
|
|
||||||
"mib": MiByte,
|
|
||||||
"mb": MByte,
|
|
||||||
"gib": GiByte,
|
|
||||||
"gb": GByte,
|
|
||||||
"tib": TiByte,
|
|
||||||
"tb": TByte,
|
|
||||||
"pib": PiByte,
|
|
||||||
"pb": PByte,
|
|
||||||
"eib": EiByte,
|
|
||||||
"eb": EByte,
|
|
||||||
// Without suffix
|
|
||||||
"": Byte,
|
|
||||||
"ki": KiByte,
|
|
||||||
"k": KByte,
|
|
||||||
"mi": MiByte,
|
|
||||||
"m": MByte,
|
|
||||||
"gi": GiByte,
|
|
||||||
"g": GByte,
|
|
||||||
"ti": TiByte,
|
|
||||||
"t": TByte,
|
|
||||||
"pi": PiByte,
|
|
||||||
"p": PByte,
|
|
||||||
"ei": EiByte,
|
|
||||||
"e": EByte,
|
|
||||||
}
|
|
||||||
|
|
||||||
var memorysSizeTable = map[string]uint64{
|
|
||||||
"b": Byte,
|
|
||||||
"kb": KiByte,
|
|
||||||
"mb": MiByte,
|
|
||||||
"gb": GiByte,
|
|
||||||
"tb": TiByte,
|
|
||||||
"pb": PiByte,
|
|
||||||
"eb": EiByte,
|
|
||||||
"": Byte,
|
|
||||||
"k": KiByte,
|
|
||||||
"m": MiByte,
|
|
||||||
"g": GiByte,
|
|
||||||
"t": TiByte,
|
|
||||||
"p": PiByte,
|
|
||||||
"e": EiByte,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
defaultSizes = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
|
||||||
iSizes = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
|
||||||
)
|
|
||||||
|
|
||||||
func Bytes(s uint64) string {
|
|
||||||
return humanateBytes(s, 1000, defaultSizes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func MemoryBytes(s uint64) string {
|
|
||||||
return humanateBytes(s, 1024, defaultSizes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func IBytes(s uint64) string {
|
|
||||||
return humanateBytes(s, 1024, iSizes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func logn(n, b float64) float64 {
|
|
||||||
return math.Log(n) / math.Log(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func humanateBytes(s uint64, base float64, sizes []string) string {
|
|
||||||
if s < 10 {
|
|
||||||
return fmt.Sprintf("%d B", s)
|
|
||||||
}
|
|
||||||
e := math.Floor(logn(float64(s), base))
|
|
||||||
suffix := sizes[int(e)]
|
|
||||||
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
|
||||||
f := "%.0f %s"
|
|
||||||
if val < 10 {
|
|
||||||
f = "%.1f %s"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf(f, val, suffix)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseBytes(s string) (uint64, error) {
|
|
||||||
return parseBytes0(s, defaultSizeTable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseMemoryBytes(s string) (uint64, error) {
|
|
||||||
return parseBytes0(s, memorysSizeTable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseBytes0(s string, sizeTable map[string]uint64) (uint64, error) {
|
|
||||||
lastDigit := 0
|
|
||||||
hasComma := false
|
|
||||||
for _, r := range s {
|
|
||||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if r == ',' {
|
|
||||||
hasComma = true
|
|
||||||
}
|
|
||||||
lastDigit++
|
|
||||||
}
|
|
||||||
|
|
||||||
num := s[:lastDigit]
|
|
||||||
if hasComma {
|
|
||||||
num = strings.Replace(num, ",", "", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := strconv.ParseFloat(num, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
|
||||||
if m, ok := sizeTable[extra]; ok {
|
|
||||||
f *= float64(m)
|
|
||||||
if f >= math.MaxUint64 {
|
|
||||||
return 0, fmt.Errorf("too large: %v", s)
|
|
||||||
}
|
|
||||||
return uint64(f), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, fmt.Errorf("unhandled size name: %v", extra)
|
|
||||||
}
|
|
||||||
133
common/ktls/ktls.go
Normal file
133
common/ktls/ktls.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
//go:build linux && go1.25 && badlinkname
|
||||||
|
|
||||||
|
package ktls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/badtls"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Conn struct {
|
||||||
|
aTLS.Conn
|
||||||
|
ctx context.Context
|
||||||
|
logger logger.ContextLogger
|
||||||
|
conn net.Conn
|
||||||
|
rawConn *badtls.RawConn
|
||||||
|
syscallConn syscall.Conn
|
||||||
|
rawSyscallConn syscall.RawConn
|
||||||
|
readWaitOptions N.ReadWaitOptions
|
||||||
|
kernelTx bool
|
||||||
|
kernelRx bool
|
||||||
|
pendingRxSplice bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) {
|
||||||
|
err := Load()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
syscallConn, isSyscallConn := N.CastReader[interface {
|
||||||
|
io.Reader
|
||||||
|
syscall.Conn
|
||||||
|
}](conn.NetConn())
|
||||||
|
if !isSyscallConn {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
rawSyscallConn, err := syscallConn.SyscallConn()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rawConn, err := badtls.NewRawConn(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if *rawConn.Vers != tls.VersionTLS13 {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
for rawConn.RawInput.Len() > 0 {
|
||||||
|
err = rawConn.ReadRecord()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for rawConn.Hand.Len() > 0 {
|
||||||
|
err = rawConn.HandlePostHandshakeMessage()
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "handle post-handshake messages")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kConn := &Conn{
|
||||||
|
Conn: conn,
|
||||||
|
ctx: ctx,
|
||||||
|
logger: logger,
|
||||||
|
conn: conn.NetConn(),
|
||||||
|
rawConn: rawConn,
|
||||||
|
syscallConn: syscallConn,
|
||||||
|
rawSyscallConn: rawSyscallConn,
|
||||||
|
}
|
||||||
|
err = kConn.setupKernel(txOffload, rxOffload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return kConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Upstream() any {
|
||||||
|
return c.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) SyscallConnForRead() syscall.RawConn {
|
||||||
|
if !c.kernelRx {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !*c.rawConn.IsClient {
|
||||||
|
c.logger.WarnContext(c.ctx, "ktls: RX splice is unavailable on the server size, since it will cause an unknown failure")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.logger.DebugContext(c.ctx, "ktls: RX splice requested")
|
||||||
|
return c.rawSyscallConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) HandleSyscallReadError(inputErr error) ([]byte, error) {
|
||||||
|
if errors.Is(inputErr, unix.EINVAL) {
|
||||||
|
c.pendingRxSplice = true
|
||||||
|
err := c.readRecord()
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "ktls: handle non-application-data record")
|
||||||
|
}
|
||||||
|
var input bytes.Buffer
|
||||||
|
if c.rawConn.Input.Len() > 0 {
|
||||||
|
_, err = c.rawConn.Input.WriteTo(&input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return input.Bytes(), nil
|
||||||
|
} else if errors.Is(inputErr, unix.EBADMSG) {
|
||||||
|
return nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertBadRecordMAC))
|
||||||
|
} else {
|
||||||
|
return nil, E.Cause(inputErr, "ktls: unexpected errno")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) SyscallConnForWrite() syscall.RawConn {
|
||||||
|
if !c.kernelTx {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.logger.DebugContext(c.ctx, "ktls: TX splice requested")
|
||||||
|
return c.rawSyscallConn
|
||||||
|
}
|
||||||
80
common/ktls/ktls_alert.go
Normal file
80
common/ktls/ktls_alert.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build linux && go1.25 && badlinkname
|
||||||
|
|
||||||
|
package ktls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// alert level
|
||||||
|
alertLevelWarning = 1
|
||||||
|
alertLevelError = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
alertCloseNotify = 0
|
||||||
|
alertUnexpectedMessage = 10
|
||||||
|
alertBadRecordMAC = 20
|
||||||
|
alertDecryptionFailed = 21
|
||||||
|
alertRecordOverflow = 22
|
||||||
|
alertDecompressionFailure = 30
|
||||||
|
alertHandshakeFailure = 40
|
||||||
|
alertBadCertificate = 42
|
||||||
|
alertUnsupportedCertificate = 43
|
||||||
|
alertCertificateRevoked = 44
|
||||||
|
alertCertificateExpired = 45
|
||||||
|
alertCertificateUnknown = 46
|
||||||
|
alertIllegalParameter = 47
|
||||||
|
alertUnknownCA = 48
|
||||||
|
alertAccessDenied = 49
|
||||||
|
alertDecodeError = 50
|
||||||
|
alertDecryptError = 51
|
||||||
|
alertExportRestriction = 60
|
||||||
|
alertProtocolVersion = 70
|
||||||
|
alertInsufficientSecurity = 71
|
||||||
|
alertInternalError = 80
|
||||||
|
alertInappropriateFallback = 86
|
||||||
|
alertUserCanceled = 90
|
||||||
|
alertNoRenegotiation = 100
|
||||||
|
alertMissingExtension = 109
|
||||||
|
alertUnsupportedExtension = 110
|
||||||
|
alertCertificateUnobtainable = 111
|
||||||
|
alertUnrecognizedName = 112
|
||||||
|
alertBadCertificateStatusResponse = 113
|
||||||
|
alertBadCertificateHashValue = 114
|
||||||
|
alertUnknownPSKIdentity = 115
|
||||||
|
alertCertificateRequired = 116
|
||||||
|
alertNoApplicationProtocol = 120
|
||||||
|
alertECHRequired = 121
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) sendAlertLocked(err uint8) error {
|
||||||
|
switch err {
|
||||||
|
case alertNoRenegotiation, alertCloseNotify:
|
||||||
|
c.rawConn.Tmp[0] = alertLevelWarning
|
||||||
|
default:
|
||||||
|
c.rawConn.Tmp[0] = alertLevelError
|
||||||
|
}
|
||||||
|
c.rawConn.Tmp[1] = byte(err)
|
||||||
|
|
||||||
|
_, writeErr := c.writeRecordLocked(recordTypeAlert, c.rawConn.Tmp[0:2])
|
||||||
|
if err == alertCloseNotify {
|
||||||
|
// closeNotify is a special case in that it isn't an error.
|
||||||
|
return writeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.rawConn.Out.SetErrorLocked(&net.OpError{Op: "local error", Err: tls.AlertError(err)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendAlert sends a TLS alert message.
|
||||||
|
func (c *Conn) sendAlert(err uint8) error {
|
||||||
|
c.rawConn.Out.Lock()
|
||||||
|
defer c.rawConn.Out.Unlock()
|
||||||
|
return c.sendAlertLocked(err)
|
||||||
|
}
|
||||||
326
common/ktls/ktls_cipher_suites_linux.go
Normal file
326
common/ktls/ktls_cipher_suites_linux.go
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
// Copyright 2010 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build linux && go1.25 && badlinkname
|
||||||
|
|
||||||
|
package ktls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/badtls"
|
||||||
|
)
|
||||||
|
|
||||||
|
type kernelCryptoCipherType uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
TLS_CIPHER_AES_GCM_128 kernelCryptoCipherType = 51
|
||||||
|
TLS_CIPHER_AES_GCM_128_IV_SIZE kernelCryptoCipherType = 8
|
||||||
|
TLS_CIPHER_AES_GCM_128_KEY_SIZE kernelCryptoCipherType = 16
|
||||||
|
TLS_CIPHER_AES_GCM_128_SALT_SIZE kernelCryptoCipherType = 4
|
||||||
|
TLS_CIPHER_AES_GCM_128_TAG_SIZE kernelCryptoCipherType = 16
|
||||||
|
TLS_CIPHER_AES_GCM_128_REC_SEQ_SIZE kernelCryptoCipherType = 8
|
||||||
|
|
||||||
|
TLS_CIPHER_AES_GCM_256 kernelCryptoCipherType = 52
|
||||||
|
TLS_CIPHER_AES_GCM_256_IV_SIZE kernelCryptoCipherType = 8
|
||||||
|
TLS_CIPHER_AES_GCM_256_KEY_SIZE kernelCryptoCipherType = 32
|
||||||
|
TLS_CIPHER_AES_GCM_256_SALT_SIZE kernelCryptoCipherType = 4
|
||||||
|
TLS_CIPHER_AES_GCM_256_TAG_SIZE kernelCryptoCipherType = 16
|
||||||
|
TLS_CIPHER_AES_GCM_256_REC_SEQ_SIZE kernelCryptoCipherType = 8
|
||||||
|
|
||||||
|
TLS_CIPHER_AES_CCM_128 kernelCryptoCipherType = 53
|
||||||
|
TLS_CIPHER_AES_CCM_128_IV_SIZE kernelCryptoCipherType = 8
|
||||||
|
TLS_CIPHER_AES_CCM_128_KEY_SIZE kernelCryptoCipherType = 16
|
||||||
|
TLS_CIPHER_AES_CCM_128_SALT_SIZE kernelCryptoCipherType = 4
|
||||||
|
TLS_CIPHER_AES_CCM_128_TAG_SIZE kernelCryptoCipherType = 16
|
||||||
|
TLS_CIPHER_AES_CCM_128_REC_SEQ_SIZE kernelCryptoCipherType = 8
|
||||||
|
|
||||||
|
TLS_CIPHER_CHACHA20_POLY1305 kernelCryptoCipherType = 54
|
||||||
|
TLS_CIPHER_CHACHA20_POLY1305_IV_SIZE kernelCryptoCipherType = 12
|
||||||
|
TLS_CIPHER_CHACHA20_POLY1305_KEY_SIZE kernelCryptoCipherType = 32
|
||||||
|
TLS_CIPHER_CHACHA20_POLY1305_SALT_SIZE kernelCryptoCipherType = 0
|
||||||
|
TLS_CIPHER_CHACHA20_POLY1305_TAG_SIZE kernelCryptoCipherType = 16
|
||||||
|
TLS_CIPHER_CHACHA20_POLY1305_REC_SEQ_SIZE kernelCryptoCipherType = 8
|
||||||
|
|
||||||
|
// TLS_CIPHER_SM4_GCM kernelCryptoCipherType = 55
|
||||||
|
// TLS_CIPHER_SM4_GCM_IV_SIZE kernelCryptoCipherType = 8
|
||||||
|
// TLS_CIPHER_SM4_GCM_KEY_SIZE kernelCryptoCipherType = 16
|
||||||
|
// TLS_CIPHER_SM4_GCM_SALT_SIZE kernelCryptoCipherType = 4
|
||||||
|
// TLS_CIPHER_SM4_GCM_TAG_SIZE kernelCryptoCipherType = 16
|
||||||
|
// TLS_CIPHER_SM4_GCM_REC_SEQ_SIZE kernelCryptoCipherType = 8
|
||||||
|
|
||||||
|
// TLS_CIPHER_SM4_CCM kernelCryptoCipherType = 56
|
||||||
|
// TLS_CIPHER_SM4_CCM_IV_SIZE kernelCryptoCipherType = 8
|
||||||
|
// TLS_CIPHER_SM4_CCM_KEY_SIZE kernelCryptoCipherType = 16
|
||||||
|
// TLS_CIPHER_SM4_CCM_SALT_SIZE kernelCryptoCipherType = 4
|
||||||
|
// TLS_CIPHER_SM4_CCM_TAG_SIZE kernelCryptoCipherType = 16
|
||||||
|
// TLS_CIPHER_SM4_CCM_REC_SEQ_SIZE kernelCryptoCipherType = 8
|
||||||
|
|
||||||
|
TLS_CIPHER_ARIA_GCM_128 kernelCryptoCipherType = 57
|
||||||
|
TLS_CIPHER_ARIA_GCM_128_IV_SIZE kernelCryptoCipherType = 8
|
||||||
|
TLS_CIPHER_ARIA_GCM_128_KEY_SIZE kernelCryptoCipherType = 16
|
||||||
|
TLS_CIPHER_ARIA_GCM_128_SALT_SIZE kernelCryptoCipherType = 4
|
||||||
|
TLS_CIPHER_ARIA_GCM_128_TAG_SIZE kernelCryptoCipherType = 16
|
||||||
|
TLS_CIPHER_ARIA_GCM_128_REC_SEQ_SIZE kernelCryptoCipherType = 8
|
||||||
|
|
||||||
|
TLS_CIPHER_ARIA_GCM_256 kernelCryptoCipherType = 58
|
||||||
|
TLS_CIPHER_ARIA_GCM_256_IV_SIZE kernelCryptoCipherType = 8
|
||||||
|
TLS_CIPHER_ARIA_GCM_256_KEY_SIZE kernelCryptoCipherType = 32
|
||||||
|
TLS_CIPHER_ARIA_GCM_256_SALT_SIZE kernelCryptoCipherType = 4
|
||||||
|
TLS_CIPHER_ARIA_GCM_256_TAG_SIZE kernelCryptoCipherType = 16
|
||||||
|
TLS_CIPHER_ARIA_GCM_256_REC_SEQ_SIZE kernelCryptoCipherType = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
type kernelCrypto interface {
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type kernelCryptoInfo struct {
|
||||||
|
version uint16
|
||||||
|
cipher_type kernelCryptoCipherType
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ kernelCrypto = &kernelCryptoAES128GCM{}
|
||||||
|
|
||||||
|
type kernelCryptoAES128GCM struct {
|
||||||
|
kernelCryptoInfo
|
||||||
|
iv [TLS_CIPHER_AES_GCM_128_IV_SIZE]byte
|
||||||
|
key [TLS_CIPHER_AES_GCM_128_KEY_SIZE]byte
|
||||||
|
salt [TLS_CIPHER_AES_GCM_128_SALT_SIZE]byte
|
||||||
|
rec_seq [TLS_CIPHER_AES_GCM_128_REC_SEQ_SIZE]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (crypto *kernelCryptoAES128GCM) String() string {
|
||||||
|
crypto.cipher_type = TLS_CIPHER_AES_GCM_128
|
||||||
|
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ kernelCrypto = &kernelCryptoAES256GCM{}
|
||||||
|
|
||||||
|
type kernelCryptoAES256GCM struct {
|
||||||
|
kernelCryptoInfo
|
||||||
|
iv [TLS_CIPHER_AES_GCM_256_IV_SIZE]byte
|
||||||
|
key [TLS_CIPHER_AES_GCM_256_KEY_SIZE]byte
|
||||||
|
salt [TLS_CIPHER_AES_GCM_256_SALT_SIZE]byte
|
||||||
|
rec_seq [TLS_CIPHER_AES_GCM_256_REC_SEQ_SIZE]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (crypto *kernelCryptoAES256GCM) String() string {
|
||||||
|
crypto.cipher_type = TLS_CIPHER_AES_GCM_256
|
||||||
|
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ kernelCrypto = &kernelCryptoAES128CCM{}
|
||||||
|
|
||||||
|
type kernelCryptoAES128CCM struct {
|
||||||
|
kernelCryptoInfo
|
||||||
|
iv [TLS_CIPHER_AES_CCM_128_IV_SIZE]byte
|
||||||
|
key [TLS_CIPHER_AES_CCM_128_KEY_SIZE]byte
|
||||||
|
salt [TLS_CIPHER_AES_CCM_128_SALT_SIZE]byte
|
||||||
|
rec_seq [TLS_CIPHER_AES_CCM_128_REC_SEQ_SIZE]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (crypto *kernelCryptoAES128CCM) String() string {
|
||||||
|
crypto.cipher_type = TLS_CIPHER_AES_CCM_128
|
||||||
|
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ kernelCrypto = &kernelCryptoChacha20Poly1035{}
|
||||||
|
|
||||||
|
type kernelCryptoChacha20Poly1035 struct {
|
||||||
|
kernelCryptoInfo
|
||||||
|
iv [TLS_CIPHER_CHACHA20_POLY1305_IV_SIZE]byte
|
||||||
|
key [TLS_CIPHER_CHACHA20_POLY1305_KEY_SIZE]byte
|
||||||
|
salt [TLS_CIPHER_CHACHA20_POLY1305_SALT_SIZE]byte
|
||||||
|
rec_seq [TLS_CIPHER_CHACHA20_POLY1305_REC_SEQ_SIZE]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (crypto *kernelCryptoChacha20Poly1035) String() string {
|
||||||
|
crypto.cipher_type = TLS_CIPHER_CHACHA20_POLY1305
|
||||||
|
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// var _ kernelCrypto = &kernelCryptoSM4GCM{}
|
||||||
|
|
||||||
|
// type kernelCryptoSM4GCM struct {
|
||||||
|
// kernelCryptoInfo
|
||||||
|
// iv [TLS_CIPHER_SM4_GCM_IV_SIZE]byte
|
||||||
|
// key [TLS_CIPHER_SM4_GCM_KEY_SIZE]byte
|
||||||
|
// salt [TLS_CIPHER_SM4_GCM_SALT_SIZE]byte
|
||||||
|
// rec_seq [TLS_CIPHER_SM4_GCM_REC_SEQ_SIZE]byte
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (crypto *kernelCryptoSM4GCM) String() string {
|
||||||
|
// crypto.cipher_type = TLS_CIPHER_SM4_GCM
|
||||||
|
// return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var _ kernelCrypto = &kernelCryptoSM4CCM{}
|
||||||
|
|
||||||
|
// type kernelCryptoSM4CCM struct {
|
||||||
|
// kernelCryptoInfo
|
||||||
|
// iv [TLS_CIPHER_SM4_CCM_IV_SIZE]byte
|
||||||
|
// key [TLS_CIPHER_SM4_CCM_KEY_SIZE]byte
|
||||||
|
// salt [TLS_CIPHER_SM4_CCM_SALT_SIZE]byte
|
||||||
|
// rec_seq [TLS_CIPHER_SM4_CCM_REC_SEQ_SIZE]byte
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (crypto *kernelCryptoSM4CCM) String() string {
|
||||||
|
// crypto.cipher_type = TLS_CIPHER_SM4_CCM
|
||||||
|
// return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
|
||||||
|
// }
|
||||||
|
|
||||||
|
var _ kernelCrypto = &kernelCryptoARIA128GCM{}
|
||||||
|
|
||||||
|
type kernelCryptoARIA128GCM struct {
|
||||||
|
kernelCryptoInfo
|
||||||
|
iv [TLS_CIPHER_ARIA_GCM_128_IV_SIZE]byte
|
||||||
|
key [TLS_CIPHER_ARIA_GCM_128_KEY_SIZE]byte
|
||||||
|
salt [TLS_CIPHER_ARIA_GCM_128_SALT_SIZE]byte
|
||||||
|
rec_seq [TLS_CIPHER_ARIA_GCM_128_REC_SEQ_SIZE]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (crypto *kernelCryptoARIA128GCM) String() string {
|
||||||
|
crypto.cipher_type = TLS_CIPHER_ARIA_GCM_128
|
||||||
|
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ kernelCrypto = &kernelCryptoARIA256GCM{}
|
||||||
|
|
||||||
|
type kernelCryptoARIA256GCM struct {
|
||||||
|
kernelCryptoInfo
|
||||||
|
iv [TLS_CIPHER_ARIA_GCM_256_IV_SIZE]byte
|
||||||
|
key [TLS_CIPHER_ARIA_GCM_256_KEY_SIZE]byte
|
||||||
|
salt [TLS_CIPHER_ARIA_GCM_256_SALT_SIZE]byte
|
||||||
|
rec_seq [TLS_CIPHER_ARIA_GCM_256_REC_SEQ_SIZE]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (crypto *kernelCryptoARIA256GCM) String() string {
|
||||||
|
crypto.cipher_type = TLS_CIPHER_ARIA_GCM_256
|
||||||
|
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func kernelCipher(kernel *Support, hc *badtls.RawHalfConn, cipherSuite uint16, isRX bool) kernelCrypto {
|
||||||
|
if !kernel.TLS {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch *hc.Version {
|
||||||
|
case tls.VersionTLS12:
|
||||||
|
if isRX && !kernel.TLS_Version13_RX {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case tls.VersionTLS13:
|
||||||
|
if !kernel.TLS_Version13 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isRX && !kernel.TLS_Version13_RX {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var key, iv []byte
|
||||||
|
if *hc.Version == tls.VersionTLS13 {
|
||||||
|
key, iv = trafficKey(cipherSuiteTLS13ByID(cipherSuite), *hc.TrafficSecret)
|
||||||
|
/*if isRX {
|
||||||
|
key, iv = trafficKey(cipherSuiteTLS13ByID(cipherSuite), keyLog.RemoteTrafficSecret)
|
||||||
|
} else {
|
||||||
|
key, iv = trafficKey(cipherSuiteTLS13ByID(cipherSuite), keyLog.TrafficSecret)
|
||||||
|
}*/
|
||||||
|
} else {
|
||||||
|
// csPtr := cipherSuiteByID(cipherSuite)
|
||||||
|
// keysFromMasterSecret(*hc.Version, csPtr, keyLog.Secret, keyLog.Random)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cipherSuite {
|
||||||
|
case tls.TLS_AES_128_GCM_SHA256, tls.TLS_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
|
||||||
|
crypto := new(kernelCryptoAES128GCM)
|
||||||
|
|
||||||
|
crypto.version = *hc.Version
|
||||||
|
copy(crypto.key[:], key)
|
||||||
|
copy(crypto.iv[:], iv[4:])
|
||||||
|
copy(crypto.salt[:], iv[:4])
|
||||||
|
crypto.rec_seq = *hc.Seq
|
||||||
|
|
||||||
|
return crypto
|
||||||
|
case tls.TLS_AES_256_GCM_SHA384, tls.TLS_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:
|
||||||
|
if !kernel.TLS_AES_256_GCM {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
crypto := new(kernelCryptoAES256GCM)
|
||||||
|
|
||||||
|
crypto.version = *hc.Version
|
||||||
|
copy(crypto.key[:], key)
|
||||||
|
copy(crypto.iv[:], iv[4:])
|
||||||
|
copy(crypto.salt[:], iv[:4])
|
||||||
|
crypto.rec_seq = *hc.Seq
|
||||||
|
|
||||||
|
return crypto
|
||||||
|
//case tls.TLS_AES_128_CCM_SHA256, tls.TLS_RSA_WITH_AES_128_CCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_SHA256:
|
||||||
|
// if !kernel.TLS_AES_128_CCM {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// crypto := new(kernelCryptoAES128CCM)
|
||||||
|
//
|
||||||
|
// crypto.version = *hc.Version
|
||||||
|
// copy(crypto.key[:], key)
|
||||||
|
// copy(crypto.iv[:], iv[4:])
|
||||||
|
// copy(crypto.salt[:], iv[:4])
|
||||||
|
// crypto.rec_seq = *hc.Seq
|
||||||
|
//
|
||||||
|
// return crypto
|
||||||
|
case tls.TLS_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:
|
||||||
|
if !kernel.TLS_CHACHA20_POLY1305 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
crypto := new(kernelCryptoChacha20Poly1035)
|
||||||
|
|
||||||
|
crypto.version = *hc.Version
|
||||||
|
copy(crypto.key[:], key)
|
||||||
|
copy(crypto.iv[:], iv)
|
||||||
|
crypto.rec_seq = *hc.Seq
|
||||||
|
|
||||||
|
return crypto
|
||||||
|
//case tls.TLS_RSA_WITH_ARIA_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256:
|
||||||
|
// if !kernel.TLS_ARIA_GCM {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// crypto := new(kernelCryptoARIA128GCM)
|
||||||
|
//
|
||||||
|
// crypto.version = *hc.Version
|
||||||
|
// copy(crypto.key[:], key)
|
||||||
|
// copy(crypto.iv[:], iv[4:])
|
||||||
|
// copy(crypto.salt[:], iv[:4])
|
||||||
|
// crypto.rec_seq = *hc.Seq
|
||||||
|
//
|
||||||
|
// return crypto
|
||||||
|
//case tls.TLS_RSA_WITH_ARIA_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384:
|
||||||
|
// if !kernel.TLS_ARIA_GCM {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// crypto := new(kernelCryptoARIA256GCM)
|
||||||
|
//
|
||||||
|
// crypto.version = *hc.Version
|
||||||
|
// copy(crypto.key[:], key)
|
||||||
|
// copy(crypto.iv[:], iv[4:])
|
||||||
|
// copy(crypto.salt[:], iv[:4])
|
||||||
|
// crypto.rec_seq = *hc.Seq
|
||||||
|
//
|
||||||
|
// return crypto
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
67
common/ktls/ktls_close.go
Normal file
67
common/ktls/ktls_close.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build linux && go1.25 && badlinkname
|
||||||
|
|
||||||
|
package ktls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) Close() error {
|
||||||
|
if !c.kernelTx {
|
||||||
|
return c.Conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interlock with Conn.Write above.
|
||||||
|
var x int32
|
||||||
|
for {
|
||||||
|
x = c.rawConn.ActiveCall.Load()
|
||||||
|
if x&1 != 0 {
|
||||||
|
return net.ErrClosed
|
||||||
|
}
|
||||||
|
if c.rawConn.ActiveCall.CompareAndSwap(x, x|1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if x != 0 {
|
||||||
|
// io.Writer and io.Closer should not be used concurrently.
|
||||||
|
// If Close is called while a Write is currently in-flight,
|
||||||
|
// interpret that as a sign that this Close is really just
|
||||||
|
// being used to break the Write and/or clean up resources and
|
||||||
|
// avoid sending the alertCloseNotify, which may block
|
||||||
|
// waiting on handshakeMutex or the c.out mutex.
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
var alertErr error
|
||||||
|
if c.rawConn.IsHandshakeComplete.Load() {
|
||||||
|
if err := c.closeNotify(); err != nil {
|
||||||
|
alertErr = fmt.Errorf("tls: failed to send closeNotify alert (but connection was closed anyway): %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.conn.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return alertErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) closeNotify() error {
|
||||||
|
c.rawConn.Out.Lock()
|
||||||
|
defer c.rawConn.Out.Unlock()
|
||||||
|
|
||||||
|
if !*c.rawConn.CloseNotifySent {
|
||||||
|
// Set a Write Deadline to prevent possibly blocking forever.
|
||||||
|
c.SetWriteDeadline(time.Now().Add(time.Second * 5))
|
||||||
|
*c.rawConn.CloseNotifyErr = c.sendAlertLocked(alertCloseNotify)
|
||||||
|
*c.rawConn.CloseNotifySent = true
|
||||||
|
// Any subsequent writes will fail.
|
||||||
|
c.SetWriteDeadline(time.Now())
|
||||||
|
}
|
||||||
|
return *c.rawConn.CloseNotifyErr
|
||||||
|
}
|
||||||
24
common/ktls/ktls_const.go
Normal file
24
common/ktls/ktls_const.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build linux && go1.25 && badlinkname
|
||||||
|
|
||||||
|
package ktls
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxPlaintext = 16384 // maximum plaintext payload length
|
||||||
|
maxCiphertext = 16384 + 2048 // maximum ciphertext payload length
|
||||||
|
maxCiphertextTLS13 = 16384 + 256 // maximum ciphertext length in TLS 1.3
|
||||||
|
recordHeaderLen = 5 // record header length
|
||||||
|
maxHandshake = 65536 // maximum handshake we support (protocol max is 16 MB)
|
||||||
|
maxHandshakeCertificateMsg = 262144 // maximum certificate message size (256 KiB)
|
||||||
|
maxUselessRecords = 16 // maximum number of consecutive non-advancing records
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
recordTypeChangeCipherSpec = 20
|
||||||
|
recordTypeAlert = 21
|
||||||
|
recordTypeHandshake = 22
|
||||||
|
recordTypeApplicationData = 23
|
||||||
|
)
|
||||||
238
common/ktls/ktls_handshake_messages.go
Normal file
238
common/ktls/ktls_handshake_messages.go
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build linux && go1.25 && badlinkname
|
||||||
|
|
||||||
|
package ktls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/cryptobyte"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The marshalingFunction type is an adapter to allow the use of ordinary
|
||||||
|
// functions as cryptobyte.MarshalingValue.
|
||||||
|
type marshalingFunction func(b *cryptobyte.Builder) error
|
||||||
|
|
||||||
|
func (f marshalingFunction) Marshal(b *cryptobyte.Builder) error {
|
||||||
|
return f(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addBytesWithLength appends a sequence of bytes to the cryptobyte.Builder. If
|
||||||
|
// the length of the sequence is not the value specified, it produces an error.
|
||||||
|
func addBytesWithLength(b *cryptobyte.Builder, v []byte, n int) {
|
||||||
|
b.AddValue(marshalingFunction(func(b *cryptobyte.Builder) error {
|
||||||
|
if len(v) != n {
|
||||||
|
return fmt.Errorf("invalid value length: expected %d, got %d", n, len(v))
|
||||||
|
}
|
||||||
|
b.AddBytes(v)
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// addUint64 appends a big-endian, 64-bit value to the cryptobyte.Builder.
|
||||||
|
func addUint64(b *cryptobyte.Builder, v uint64) {
|
||||||
|
b.AddUint32(uint32(v >> 32))
|
||||||
|
b.AddUint32(uint32(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// readUint64 decodes a big-endian, 64-bit value into out and advances over it.
|
||||||
|
// It reports whether the read was successful.
|
||||||
|
func readUint64(s *cryptobyte.String, out *uint64) bool {
|
||||||
|
var hi, lo uint32
|
||||||
|
if !s.ReadUint32(&hi) || !s.ReadUint32(&lo) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
*out = uint64(hi)<<32 | uint64(lo)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// readUint8LengthPrefixed acts like s.ReadUint8LengthPrefixed, but targets a
|
||||||
|
// []byte instead of a cryptobyte.String.
|
||||||
|
func readUint8LengthPrefixed(s *cryptobyte.String, out *[]byte) bool {
|
||||||
|
return s.ReadUint8LengthPrefixed((*cryptobyte.String)(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// readUint16LengthPrefixed acts like s.ReadUint16LengthPrefixed, but targets a
|
||||||
|
// []byte instead of a cryptobyte.String.
|
||||||
|
func readUint16LengthPrefixed(s *cryptobyte.String, out *[]byte) bool {
|
||||||
|
return s.ReadUint16LengthPrefixed((*cryptobyte.String)(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// readUint24LengthPrefixed acts like s.ReadUint24LengthPrefixed, but targets a
|
||||||
|
// []byte instead of a cryptobyte.String.
|
||||||
|
func readUint24LengthPrefixed(s *cryptobyte.String, out *[]byte) bool {
|
||||||
|
return s.ReadUint24LengthPrefixed((*cryptobyte.String)(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyUpdateMsg struct {
|
||||||
|
updateRequested bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *keyUpdateMsg) marshal() ([]byte, error) {
|
||||||
|
var b cryptobyte.Builder
|
||||||
|
b.AddUint8(typeKeyUpdate)
|
||||||
|
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||||
|
if m.updateRequested {
|
||||||
|
b.AddUint8(1)
|
||||||
|
} else {
|
||||||
|
b.AddUint8(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *keyUpdateMsg) unmarshal(data []byte) bool {
|
||||||
|
s := cryptobyte.String(data)
|
||||||
|
|
||||||
|
var updateRequested uint8
|
||||||
|
if !s.Skip(4) || // message type and uint24 length field
|
||||||
|
!s.ReadUint8(&updateRequested) || !s.Empty() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch updateRequested {
|
||||||
|
case 0:
|
||||||
|
m.updateRequested = false
|
||||||
|
case 1:
|
||||||
|
m.updateRequested = true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS handshake message types.
|
||||||
|
const (
|
||||||
|
typeHelloRequest uint8 = 0
|
||||||
|
typeClientHello uint8 = 1
|
||||||
|
typeServerHello uint8 = 2
|
||||||
|
typeNewSessionTicket uint8 = 4
|
||||||
|
typeEndOfEarlyData uint8 = 5
|
||||||
|
typeEncryptedExtensions uint8 = 8
|
||||||
|
typeCertificate uint8 = 11
|
||||||
|
typeServerKeyExchange uint8 = 12
|
||||||
|
typeCertificateRequest uint8 = 13
|
||||||
|
typeServerHelloDone uint8 = 14
|
||||||
|
typeCertificateVerify uint8 = 15
|
||||||
|
typeClientKeyExchange uint8 = 16
|
||||||
|
typeFinished uint8 = 20
|
||||||
|
typeCertificateStatus uint8 = 22
|
||||||
|
typeKeyUpdate uint8 = 24
|
||||||
|
typeCompressedCertificate uint8 = 25
|
||||||
|
typeMessageHash uint8 = 254 // synthetic message
|
||||||
|
)
|
||||||
|
|
||||||
|
// TLS compression types.
|
||||||
|
const (
|
||||||
|
compressionNone uint8 = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// TLS extension numbers
|
||||||
|
const (
|
||||||
|
extensionServerName uint16 = 0
|
||||||
|
extensionStatusRequest uint16 = 5
|
||||||
|
extensionSupportedCurves uint16 = 10 // supported_groups in TLS 1.3, see RFC 8446, Section 4.2.7
|
||||||
|
extensionSupportedPoints uint16 = 11
|
||||||
|
extensionSignatureAlgorithms uint16 = 13
|
||||||
|
extensionALPN uint16 = 16
|
||||||
|
extensionSCT uint16 = 18
|
||||||
|
extensionPadding uint16 = 21
|
||||||
|
extensionExtendedMasterSecret uint16 = 23
|
||||||
|
extensionCompressCertificate uint16 = 27 // compress_certificate in TLS 1.3
|
||||||
|
extensionSessionTicket uint16 = 35
|
||||||
|
extensionPreSharedKey uint16 = 41
|
||||||
|
extensionEarlyData uint16 = 42
|
||||||
|
extensionSupportedVersions uint16 = 43
|
||||||
|
extensionCookie uint16 = 44
|
||||||
|
extensionPSKModes uint16 = 45
|
||||||
|
extensionCertificateAuthorities uint16 = 47
|
||||||
|
extensionSignatureAlgorithmsCert uint16 = 50
|
||||||
|
extensionKeyShare uint16 = 51
|
||||||
|
extensionQUICTransportParameters uint16 = 57
|
||||||
|
extensionALPS uint16 = 17513
|
||||||
|
extensionRenegotiationInfo uint16 = 0xff01
|
||||||
|
extensionECHOuterExtensions uint16 = 0xfd00
|
||||||
|
extensionEncryptedClientHello uint16 = 0xfe0d
|
||||||
|
)
|
||||||
|
|
||||||
|
type handshakeMessage interface {
|
||||||
|
marshal() ([]byte, error)
|
||||||
|
unmarshal([]byte) bool
|
||||||
|
}
|
||||||
|
type newSessionTicketMsgTLS13 struct {
|
||||||
|
lifetime uint32
|
||||||
|
ageAdd uint32
|
||||||
|
nonce []byte
|
||||||
|
label []byte
|
||||||
|
maxEarlyData uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *newSessionTicketMsgTLS13) marshal() ([]byte, error) {
|
||||||
|
var b cryptobyte.Builder
|
||||||
|
b.AddUint8(typeNewSessionTicket)
|
||||||
|
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||||
|
b.AddUint32(m.lifetime)
|
||||||
|
b.AddUint32(m.ageAdd)
|
||||||
|
b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||||
|
b.AddBytes(m.nonce)
|
||||||
|
})
|
||||||
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||||
|
b.AddBytes(m.label)
|
||||||
|
})
|
||||||
|
|
||||||
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||||
|
if m.maxEarlyData > 0 {
|
||||||
|
b.AddUint16(extensionEarlyData)
|
||||||
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||||
|
b.AddUint32(m.maxEarlyData)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *newSessionTicketMsgTLS13) unmarshal(data []byte) bool {
|
||||||
|
*m = newSessionTicketMsgTLS13{}
|
||||||
|
s := cryptobyte.String(data)
|
||||||
|
|
||||||
|
var extensions cryptobyte.String
|
||||||
|
if !s.Skip(4) || // message type and uint24 length field
|
||||||
|
!s.ReadUint32(&m.lifetime) ||
|
||||||
|
!s.ReadUint32(&m.ageAdd) ||
|
||||||
|
!readUint8LengthPrefixed(&s, &m.nonce) ||
|
||||||
|
!readUint16LengthPrefixed(&s, &m.label) ||
|
||||||
|
!s.ReadUint16LengthPrefixed(&extensions) ||
|
||||||
|
!s.Empty() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for !extensions.Empty() {
|
||||||
|
var extension uint16
|
||||||
|
var extData cryptobyte.String
|
||||||
|
if !extensions.ReadUint16(&extension) ||
|
||||||
|
!extensions.ReadUint16LengthPrefixed(&extData) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch extension {
|
||||||
|
case extensionEarlyData:
|
||||||
|
if !extData.ReadUint32(&m.maxEarlyData) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Ignore unknown extensions.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !extData.Empty() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
173
common/ktls/ktls_key_update.go
Normal file
173
common/ktls/ktls_key_update.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build linux && go1.25 && badlinkname
|
||||||
|
|
||||||
|
package ktls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handlePostHandshakeMessage processes a handshake message arrived after the
|
||||||
|
// handshake is complete. Up to TLS 1.2, it indicates the start of a renegotiation.
|
||||||
|
func (c *Conn) handlePostHandshakeMessage() error {
|
||||||
|
if *c.rawConn.Vers != tls.VersionTLS13 {
|
||||||
|
return errors.New("ktls: kernel does not support TLS 1.2 renegotiation")
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := c.readHandshake(nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
//c.retryCount++
|
||||||
|
//if c.retryCount > maxUselessRecords {
|
||||||
|
// c.sendAlert(alertUnexpectedMessage)
|
||||||
|
// return c.in.setErrorLocked(errors.New("tls: too many non-advancing records"))
|
||||||
|
//}
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case *newSessionTicketMsgTLS13:
|
||||||
|
// return errors.New("ktls: received new session ticket")
|
||||||
|
return nil
|
||||||
|
case *keyUpdateMsg:
|
||||||
|
return c.handleKeyUpdate(msg)
|
||||||
|
}
|
||||||
|
// The QUIC layer is supposed to treat an unexpected post-handshake CertificateRequest
|
||||||
|
// as a QUIC-level PROTOCOL_VIOLATION error (RFC 9001, Section 4.4). Returning an
|
||||||
|
// unexpected_message alert here doesn't provide it with enough information to distinguish
|
||||||
|
// this condition from other unexpected messages. This is probably fine.
|
||||||
|
c.sendAlert(alertUnexpectedMessage)
|
||||||
|
return fmt.Errorf("tls: received unexpected handshake message of type %T", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) handleKeyUpdate(keyUpdate *keyUpdateMsg) error {
|
||||||
|
//if c.quic != nil {
|
||||||
|
// c.sendAlert(alertUnexpectedMessage)
|
||||||
|
// return c.in.setErrorLocked(errors.New("tls: received unexpected key update message"))
|
||||||
|
//}
|
||||||
|
|
||||||
|
cipherSuite := cipherSuiteTLS13ByID(*c.rawConn.CipherSuite)
|
||||||
|
if cipherSuite == nil {
|
||||||
|
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertInternalError))
|
||||||
|
}
|
||||||
|
|
||||||
|
newSecret := nextTrafficSecret(cipherSuite, *c.rawConn.In.TrafficSecret)
|
||||||
|
c.rawConn.In.SetTrafficSecret(cipherSuite, 0 /*tls.QUICEncryptionLevelInitial*/, newSecret)
|
||||||
|
|
||||||
|
err := c.resetupRX()
|
||||||
|
if err != nil {
|
||||||
|
c.sendAlert(alertInternalError)
|
||||||
|
return c.rawConn.In.SetErrorLocked(fmt.Errorf("ktls: resetupRX failed: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyUpdate.updateRequested {
|
||||||
|
c.rawConn.Out.Lock()
|
||||||
|
defer c.rawConn.Out.Unlock()
|
||||||
|
|
||||||
|
resetup, err := c.resetupTX()
|
||||||
|
if err != nil {
|
||||||
|
c.sendAlertLocked(alertInternalError)
|
||||||
|
return c.rawConn.Out.SetErrorLocked(fmt.Errorf("ktls: resetupTX failed: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &keyUpdateMsg{}
|
||||||
|
msgBytes, err := msg.marshal()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = c.writeRecordLocked(recordTypeHandshake, msgBytes)
|
||||||
|
if err != nil {
|
||||||
|
// Surface the error at the next write.
|
||||||
|
c.rawConn.Out.SetErrorLocked(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
newSecret := nextTrafficSecret(cipherSuite, *c.rawConn.Out.TrafficSecret)
|
||||||
|
c.rawConn.Out.SetTrafficSecret(cipherSuite, 0 /*QUICEncryptionLevelInitial*/, newSecret)
|
||||||
|
|
||||||
|
err = resetup()
|
||||||
|
if err != nil {
|
||||||
|
return c.rawConn.Out.SetErrorLocked(fmt.Errorf("ktls: resetupTX failed: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) readHandshakeBytes(n int) error {
|
||||||
|
//if c.quic != nil {
|
||||||
|
// return c.quicReadHandshakeBytes(n)
|
||||||
|
//}
|
||||||
|
for c.rawConn.Hand.Len() < n {
|
||||||
|
if err := c.readRecord(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) readHandshake(transcript io.Writer) (any, error) {
|
||||||
|
if err := c.readHandshakeBytes(4); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data := c.rawConn.Hand.Bytes()
|
||||||
|
|
||||||
|
maxHandshakeSize := maxHandshake
|
||||||
|
// hasVers indicates we're past the first message, forcing someone trying to
|
||||||
|
// make us just allocate a large buffer to at least do the initial part of
|
||||||
|
// the handshake first.
|
||||||
|
//if c.haveVers && data[0] == typeCertificate {
|
||||||
|
// Since certificate messages are likely to be the only messages that
|
||||||
|
// can be larger than maxHandshake, we use a special limit for just
|
||||||
|
// those messages.
|
||||||
|
//maxHandshakeSize = maxHandshakeCertificateMsg
|
||||||
|
//}
|
||||||
|
|
||||||
|
n := int(data[1])<<16 | int(data[2])<<8 | int(data[3])
|
||||||
|
if n > maxHandshakeSize {
|
||||||
|
c.sendAlertLocked(alertInternalError)
|
||||||
|
return nil, c.rawConn.In.SetErrorLocked(fmt.Errorf("tls: handshake message of length %d bytes exceeds maximum of %d bytes", n, maxHandshakeSize))
|
||||||
|
}
|
||||||
|
if err := c.readHandshakeBytes(4 + n); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data = c.rawConn.Hand.Next(4 + n)
|
||||||
|
return c.unmarshalHandshakeMessage(data, transcript)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) unmarshalHandshakeMessage(data []byte, transcript io.Writer) (any, error) {
|
||||||
|
var m handshakeMessage
|
||||||
|
switch data[0] {
|
||||||
|
case typeNewSessionTicket:
|
||||||
|
if *c.rawConn.Vers == tls.VersionTLS13 {
|
||||||
|
m = new(newSessionTicketMsgTLS13)
|
||||||
|
} else {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
case typeKeyUpdate:
|
||||||
|
m = new(keyUpdateMsg)
|
||||||
|
default:
|
||||||
|
return nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The handshake message unmarshalers
|
||||||
|
// expect to be able to keep references to data,
|
||||||
|
// so pass in a fresh copy that won't be overwritten.
|
||||||
|
data = append([]byte(nil), data...)
|
||||||
|
|
||||||
|
if !m.unmarshal(data) {
|
||||||
|
return nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertDecodeError))
|
||||||
|
}
|
||||||
|
|
||||||
|
if transcript != nil {
|
||||||
|
transcript.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
329
common/ktls/ktls_linux.go
Normal file
329
common/ktls/ktls_linux.go
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
//go:build linux && go1.25 && badlinkname
|
||||||
|
|
||||||
|
package ktls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/badversion"
|
||||||
|
"github.com/sagernet/sing/common/control"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/shell"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mod from https://gitlab.com/go-extension/tls
|
||||||
|
|
||||||
|
const (
|
||||||
|
TLS_TX = 1
|
||||||
|
TLS_RX = 2
|
||||||
|
TLS_TX_ZEROCOPY_RO = 3 // TX zerocopy (only sendfile now)
|
||||||
|
TLS_RX_EXPECT_NO_PAD = 4 // Attempt opportunistic zero-copy, TLS 1.3 only
|
||||||
|
|
||||||
|
TLS_SET_RECORD_TYPE = 1
|
||||||
|
TLS_GET_RECORD_TYPE = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type Support struct {
|
||||||
|
TLS, TLS_RX bool
|
||||||
|
TLS_Version13, TLS_Version13_RX bool
|
||||||
|
|
||||||
|
TLS_TX_ZEROCOPY bool
|
||||||
|
TLS_RX_NOPADDING bool
|
||||||
|
|
||||||
|
TLS_AES_256_GCM bool
|
||||||
|
TLS_AES_128_CCM bool
|
||||||
|
TLS_CHACHA20_POLY1305 bool
|
||||||
|
TLS_SM4 bool
|
||||||
|
TLS_ARIA_GCM bool
|
||||||
|
|
||||||
|
TLS_Version13_KeyUpdate bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var KernelSupport = sync.OnceValues(func() (*Support, error) {
|
||||||
|
var uname unix.Utsname
|
||||||
|
err := unix.Uname(&uname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
kernelVersion := badversion.Parse(strings.Trim(string(uname.Release[:]), "\x00"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var support Support
|
||||||
|
switch {
|
||||||
|
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 6, Minor: 14}):
|
||||||
|
support.TLS_Version13_KeyUpdate = true
|
||||||
|
fallthrough
|
||||||
|
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 6, Minor: 1}):
|
||||||
|
support.TLS_ARIA_GCM = true
|
||||||
|
fallthrough
|
||||||
|
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 6}):
|
||||||
|
support.TLS_Version13_RX = true
|
||||||
|
support.TLS_RX_NOPADDING = true
|
||||||
|
fallthrough
|
||||||
|
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 19}):
|
||||||
|
support.TLS_TX_ZEROCOPY = true
|
||||||
|
fallthrough
|
||||||
|
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 16}):
|
||||||
|
support.TLS_SM4 = true
|
||||||
|
fallthrough
|
||||||
|
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 11}):
|
||||||
|
support.TLS_CHACHA20_POLY1305 = true
|
||||||
|
fallthrough
|
||||||
|
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 2}):
|
||||||
|
support.TLS_AES_128_CCM = true
|
||||||
|
fallthrough
|
||||||
|
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 1}):
|
||||||
|
support.TLS_AES_256_GCM = true
|
||||||
|
support.TLS_Version13 = true
|
||||||
|
fallthrough
|
||||||
|
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 4, Minor: 17}):
|
||||||
|
support.TLS_RX = true
|
||||||
|
fallthrough
|
||||||
|
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 4, Minor: 13}):
|
||||||
|
support.TLS = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if support.TLS && support.TLS_Version13 {
|
||||||
|
_, err := os.Stat("/sys/module/tls")
|
||||||
|
if err != nil {
|
||||||
|
if os.Getuid() == 0 {
|
||||||
|
output, err := shell.Exec("modprobe", "tls").Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Extend(E.Cause(err, "modprobe tls"), output)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, E.New("ktls: kernel TLS module not loaded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &support, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
func Load() error {
|
||||||
|
support, err := KernelSupport()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "ktls: check availability")
|
||||||
|
}
|
||||||
|
if !support.TLS || !support.TLS_Version13 {
|
||||||
|
return E.New("ktls: kernel does not support TLS 1.3")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) setupKernel(txOffload, rxOffload bool) error {
|
||||||
|
if !txOffload && !rxOffload {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
support, err := KernelSupport()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "check availability")
|
||||||
|
}
|
||||||
|
if !support.TLS || !support.TLS_Version13 {
|
||||||
|
return E.New("kernel does not support TLS 1.3")
|
||||||
|
}
|
||||||
|
c.rawConn.Out.Lock()
|
||||||
|
defer c.rawConn.Out.Unlock()
|
||||||
|
err = control.Raw(c.rawSyscallConn, func(fd uintptr) error {
|
||||||
|
return syscall.SetsockoptString(int(fd), unix.SOL_TCP, unix.TCP_ULP, "tls")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return os.NewSyscallError("setsockopt", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if txOffload {
|
||||||
|
txCrypto := kernelCipher(support, c.rawConn.Out, *c.rawConn.CipherSuite, false)
|
||||||
|
if txCrypto == nil {
|
||||||
|
return E.New("unsupported cipher suite")
|
||||||
|
}
|
||||||
|
err = control.Raw(c.rawSyscallConn, func(fd uintptr) error {
|
||||||
|
return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_TX, txCrypto.String())
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if support.TLS_TX_ZEROCOPY {
|
||||||
|
err = control.Raw(c.rawSyscallConn, func(fd uintptr) error {
|
||||||
|
return syscall.SetsockoptInt(int(fd), unix.SOL_TLS, TLS_TX_ZEROCOPY_RO, 1)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.kernelTx = true
|
||||||
|
c.logger.DebugContext(c.ctx, "ktls: kernel TLS TX enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rxOffload {
|
||||||
|
rxCrypto := kernelCipher(support, c.rawConn.In, *c.rawConn.CipherSuite, true)
|
||||||
|
if rxCrypto == nil {
|
||||||
|
return E.New("unsupported cipher suite")
|
||||||
|
}
|
||||||
|
err = control.Raw(c.rawSyscallConn, func(fd uintptr) error {
|
||||||
|
return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_RX, rxCrypto.String())
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if *c.rawConn.Vers >= tls.VersionTLS13 && support.TLS_RX_NOPADDING {
|
||||||
|
err = control.Raw(c.rawSyscallConn, func(fd uintptr) error {
|
||||||
|
return syscall.SetsockoptInt(int(fd), unix.SOL_TLS, TLS_RX_EXPECT_NO_PAD, 1)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.kernelRx = true
|
||||||
|
c.logger.DebugContext(c.ctx, "ktls: kernel TLS RX enabled")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) resetupTX() (func() error, error) {
|
||||||
|
if !c.kernelTx {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
support, err := KernelSupport()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !support.TLS_Version13_KeyUpdate {
|
||||||
|
return nil, errors.New("ktls: kernel does not support rekey")
|
||||||
|
}
|
||||||
|
txCrypto := kernelCipher(support, c.rawConn.Out, *c.rawConn.CipherSuite, false)
|
||||||
|
if txCrypto == nil {
|
||||||
|
return nil, errors.New("ktls: set kernelCipher on unsupported tls session")
|
||||||
|
}
|
||||||
|
return func() error {
|
||||||
|
return control.Raw(c.rawSyscallConn, func(fd uintptr) error {
|
||||||
|
return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_TX, txCrypto.String())
|
||||||
|
})
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) resetupRX() error {
|
||||||
|
if !c.kernelRx {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
support, err := KernelSupport()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !support.TLS_Version13_KeyUpdate {
|
||||||
|
return errors.New("ktls: kernel does not support rekey")
|
||||||
|
}
|
||||||
|
rxCrypto := kernelCipher(support, c.rawConn.In, *c.rawConn.CipherSuite, true)
|
||||||
|
if rxCrypto == nil {
|
||||||
|
return errors.New("ktls: set kernelCipher on unsupported tls session")
|
||||||
|
}
|
||||||
|
return control.Raw(c.rawSyscallConn, func(fd uintptr) error {
|
||||||
|
return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_RX, rxCrypto.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) readKernelRecord() (uint8, []byte, error) {
|
||||||
|
if c.rawConn.RawInput.Len() < maxPlaintext {
|
||||||
|
c.rawConn.RawInput.Grow(maxPlaintext - c.rawConn.RawInput.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
data := c.rawConn.RawInput.Bytes()[:maxPlaintext]
|
||||||
|
|
||||||
|
// cmsg for record type
|
||||||
|
buffer := make([]byte, unix.CmsgSpace(1))
|
||||||
|
cmsg := (*unix.Cmsghdr)(unsafe.Pointer(&buffer[0]))
|
||||||
|
cmsg.SetLen(unix.CmsgLen(1))
|
||||||
|
|
||||||
|
var iov unix.Iovec
|
||||||
|
iov.Base = &data[0]
|
||||||
|
iov.SetLen(len(data))
|
||||||
|
|
||||||
|
var msg unix.Msghdr
|
||||||
|
msg.Control = &buffer[0]
|
||||||
|
msg.Controllen = cmsg.Len
|
||||||
|
msg.Iov = &iov
|
||||||
|
msg.Iovlen = 1
|
||||||
|
|
||||||
|
var n int
|
||||||
|
var err error
|
||||||
|
er := c.rawSyscallConn.Read(func(fd uintptr) bool {
|
||||||
|
n, err = recvmsg(int(fd), &msg, 0)
|
||||||
|
return err != unix.EAGAIN || c.pendingRxSplice
|
||||||
|
})
|
||||||
|
if er != nil {
|
||||||
|
return 0, nil, er
|
||||||
|
}
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
case syscall.EINVAL, syscall.EAGAIN:
|
||||||
|
return 0, nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertProtocolVersion))
|
||||||
|
case syscall.EMSGSIZE:
|
||||||
|
return 0, nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertRecordOverflow))
|
||||||
|
case syscall.EBADMSG:
|
||||||
|
return 0, nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertDecryptError))
|
||||||
|
default:
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if n <= 0 {
|
||||||
|
return 0, nil, c.rawConn.In.SetErrorLocked(io.EOF)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmsg.Level == unix.SOL_TLS && cmsg.Type == TLS_GET_RECORD_TYPE {
|
||||||
|
typ := buffer[unix.CmsgLen(0)]
|
||||||
|
return typ, data[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return recordTypeApplicationData, data[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeKernelRecord(typ uint16, data []byte) (int, error) {
|
||||||
|
if typ == recordTypeApplicationData {
|
||||||
|
return c.conn.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmsg for record type
|
||||||
|
buffer := make([]byte, unix.CmsgSpace(1))
|
||||||
|
cmsg := (*unix.Cmsghdr)(unsafe.Pointer(&buffer[0]))
|
||||||
|
cmsg.SetLen(unix.CmsgLen(1))
|
||||||
|
buffer[unix.CmsgLen(0)] = byte(typ)
|
||||||
|
cmsg.Level = unix.SOL_TLS
|
||||||
|
cmsg.Type = TLS_SET_RECORD_TYPE
|
||||||
|
|
||||||
|
var iov unix.Iovec
|
||||||
|
iov.Base = &data[0]
|
||||||
|
iov.SetLen(len(data))
|
||||||
|
|
||||||
|
var msg unix.Msghdr
|
||||||
|
msg.Control = &buffer[0]
|
||||||
|
msg.Controllen = cmsg.Len
|
||||||
|
msg.Iov = &iov
|
||||||
|
msg.Iovlen = 1
|
||||||
|
|
||||||
|
var n int
|
||||||
|
var err error
|
||||||
|
ew := c.rawSyscallConn.Write(func(fd uintptr) bool {
|
||||||
|
n, err = sendmsg(int(fd), &msg, 0)
|
||||||
|
return err != unix.EAGAIN
|
||||||
|
})
|
||||||
|
if ew != nil {
|
||||||
|
return 0, ew
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:linkname recvmsg golang.org/x/sys/unix.recvmsg
|
||||||
|
func recvmsg(fd int, msg *unix.Msghdr, flags int) (n int, err error)
|
||||||
|
|
||||||
|
//go:linkname sendmsg golang.org/x/sys/unix.sendmsg
|
||||||
|
func sendmsg(fd int, msg *unix.Msghdr, flags int) (n int, err error)
|
||||||
24
common/ktls/ktls_prf.go
Normal file
24
common/ktls/ktls_prf.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build linux && go1.25 && badlinkname
|
||||||
|
|
||||||
|
package ktls
|
||||||
|
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
//go:linkname cipherSuiteByID github.com/metacubex/utls.cipherSuiteByID
|
||||||
|
func cipherSuiteByID(id uint16) unsafe.Pointer
|
||||||
|
|
||||||
|
//go:linkname keysFromMasterSecret github.com/metacubex/utls.keysFromMasterSecret
|
||||||
|
func keysFromMasterSecret(version uint16, suite unsafe.Pointer, masterSecret, clientRandom, serverRandom []byte, macLen, keyLen, ivLen int) (clientMAC, serverMAC, clientKey, serverKey, clientIV, serverIV []byte)
|
||||||
|
|
||||||
|
//go:linkname cipherSuiteTLS13ByID github.com/metacubex/utls.cipherSuiteTLS13ByID
|
||||||
|
func cipherSuiteTLS13ByID(id uint16) unsafe.Pointer
|
||||||
|
|
||||||
|
//go:linkname nextTrafficSecret github.com/metacubex/utls.(*cipherSuiteTLS13).nextTrafficSecret
|
||||||
|
func nextTrafficSecret(cs unsafe.Pointer, trafficSecret []byte) []byte
|
||||||
|
|
||||||
|
//go:linkname trafficKey github.com/metacubex/utls.(*cipherSuiteTLS13).trafficKey
|
||||||
|
func trafficKey(cs unsafe.Pointer, trafficSecret []byte) (key, iv []byte)
|
||||||
292
common/ktls/ktls_read.go
Normal file
292
common/ktls/ktls_read.go
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build linux && go1.25 && badlinkname
|
||||||
|
|
||||||
|
package ktls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) Read(b []byte) (int, error) {
|
||||||
|
if !c.kernelRx {
|
||||||
|
return c.Conn.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) == 0 {
|
||||||
|
// Put this after Handshake, in case people were calling
|
||||||
|
// Read(nil) for the side effect of the Handshake.
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.rawConn.In.Lock()
|
||||||
|
defer c.rawConn.In.Unlock()
|
||||||
|
|
||||||
|
for c.rawConn.Input.Len() == 0 {
|
||||||
|
if err := c.readRecord(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
for c.rawConn.Hand.Len() > 0 {
|
||||||
|
if err := c.handlePostHandshakeMessage(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n, _ := c.rawConn.Input.Read(b)
|
||||||
|
|
||||||
|
// If a close-notify alert is waiting, read it so that we can return (n,
|
||||||
|
// EOF) instead of (n, nil), to signal to the HTTP response reading
|
||||||
|
// goroutine that the connection is now closed. This eliminates a race
|
||||||
|
// where the HTTP response reading goroutine would otherwise not observe
|
||||||
|
// the EOF until its next read, by which time a client goroutine might
|
||||||
|
// have already tried to reuse the HTTP connection for a new request.
|
||||||
|
// See https://golang.org/cl/76400046 and https://golang.org/issue/3514
|
||||||
|
if n != 0 && c.rawConn.Input.Len() == 0 && c.rawConn.RawInput.Len() > 0 &&
|
||||||
|
c.rawConn.RawInput.Bytes()[0] == recordTypeAlert {
|
||||||
|
if err := c.readRecord(); err != nil {
|
||||||
|
return n, err // will be io.EOF on closeNotify
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) readRecord() error {
|
||||||
|
if *c.rawConn.In.Err != nil {
|
||||||
|
return *c.rawConn.In.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
typ, data, err := c.readRawRecord()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) > maxPlaintext {
|
||||||
|
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertRecordOverflow))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application Data messages are always protected.
|
||||||
|
if c.rawConn.In.Cipher == nil && typ == recordTypeApplicationData {
|
||||||
|
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
|
}
|
||||||
|
|
||||||
|
//if typ != recordTypeAlert && typ != recordTypeChangeCipherSpec && len(data) > 0 {
|
||||||
|
// This is a state-advancing message: reset the retry count.
|
||||||
|
// c.retryCount = 0
|
||||||
|
//}
|
||||||
|
|
||||||
|
// Handshake messages MUST NOT be interleaved with other record types in TLS 1.3.
|
||||||
|
if *c.rawConn.Vers == tls.VersionTLS13 && typ != recordTypeHandshake && c.rawConn.Hand.Len() > 0 {
|
||||||
|
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
default:
|
||||||
|
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
|
case recordTypeAlert:
|
||||||
|
//if c.quic != nil {
|
||||||
|
// return c.rawConn.In.setErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
|
//}
|
||||||
|
if len(data) != 2 {
|
||||||
|
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
|
}
|
||||||
|
if data[1] == alertCloseNotify {
|
||||||
|
return c.rawConn.In.SetErrorLocked(io.EOF)
|
||||||
|
}
|
||||||
|
if *c.rawConn.Vers == tls.VersionTLS13 {
|
||||||
|
// TLS 1.3 removed warning-level alerts except for alertUserCanceled
|
||||||
|
// (RFC 8446, § 6.1). Since at least one major implementation
|
||||||
|
// (https://bugs.openjdk.org/browse/JDK-8323517) misuses this alert,
|
||||||
|
// many TLS stacks now ignore it outright when seen in a TLS 1.3
|
||||||
|
// handshake (e.g. BoringSSL, NSS, Rustls).
|
||||||
|
if data[1] == alertUserCanceled {
|
||||||
|
// Like TLS 1.2 alertLevelWarning alerts, we drop the record and retry.
|
||||||
|
return c.retryReadRecord( /*expectChangeCipherSpec*/ )
|
||||||
|
}
|
||||||
|
return c.rawConn.In.SetErrorLocked(&net.OpError{Op: "remote error", Err: tls.AlertError(data[1])})
|
||||||
|
}
|
||||||
|
switch data[0] {
|
||||||
|
case alertLevelWarning:
|
||||||
|
// Drop the record on the floor and retry.
|
||||||
|
return c.retryReadRecord( /*expectChangeCipherSpec*/ )
|
||||||
|
case alertLevelError:
|
||||||
|
return c.rawConn.In.SetErrorLocked(&net.OpError{Op: "remote error", Err: tls.AlertError(data[1])})
|
||||||
|
default:
|
||||||
|
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
|
}
|
||||||
|
|
||||||
|
case recordTypeChangeCipherSpec:
|
||||||
|
if len(data) != 1 || data[0] != 1 {
|
||||||
|
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertDecodeError))
|
||||||
|
}
|
||||||
|
// Handshake messages are not allowed to fragment across the CCS.
|
||||||
|
if c.rawConn.Hand.Len() > 0 {
|
||||||
|
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
|
}
|
||||||
|
// In TLS 1.3, change_cipher_spec records are ignored until the
|
||||||
|
// Finished. See RFC 8446, Appendix D.4. Note that according to Section
|
||||||
|
// 5, a server can send a ChangeCipherSpec before its ServerHello, when
|
||||||
|
// c.vers is still unset. That's not useful though and suspicious if the
|
||||||
|
// server then selects a lower protocol version, so don't allow that.
|
||||||
|
if *c.rawConn.Vers == tls.VersionTLS13 {
|
||||||
|
return c.retryReadRecord( /*expectChangeCipherSpec*/ )
|
||||||
|
}
|
||||||
|
// if !expectChangeCipherSpec {
|
||||||
|
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
|
//}
|
||||||
|
//if err := c.rawConn.In.changeCipherSpec(); err != nil {
|
||||||
|
// return c.rawConn.In.setErrorLocked(c.sendAlert(err.(alert)))
|
||||||
|
//}
|
||||||
|
|
||||||
|
case recordTypeApplicationData:
|
||||||
|
// Some OpenSSL servers send empty records in order to randomize the
|
||||||
|
// CBC RawIV. Ignore a limited number of empty records.
|
||||||
|
if len(data) == 0 {
|
||||||
|
return c.retryReadRecord( /*expectChangeCipherSpec*/ )
|
||||||
|
}
|
||||||
|
// Note that data is owned by c.rawInput, following the Next call above,
|
||||||
|
// to avoid copying the plaintext. This is safe because c.rawInput is
|
||||||
|
// not read from or written to until c.input is drained.
|
||||||
|
c.rawConn.Input.Reset(data)
|
||||||
|
case recordTypeHandshake:
|
||||||
|
if len(data) == 0 {
|
||||||
|
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
|
}
|
||||||
|
c.rawConn.Hand.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:staticcheck
|
||||||
|
func (c *Conn) readRawRecord() (typ uint8, data []byte, err error) {
|
||||||
|
// Read from kernel.
|
||||||
|
if c.kernelRx {
|
||||||
|
return c.readKernelRecord()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read header, payload.
|
||||||
|
if err = c.readFromUntil(c.conn, recordHeaderLen); err != nil {
|
||||||
|
// RFC 8446, Section 6.1 suggests that EOF without an alertCloseNotify
|
||||||
|
// is an error, but popular web sites seem to do this, so we accept it
|
||||||
|
// if and only if at the record boundary.
|
||||||
|
if err == io.ErrUnexpectedEOF && c.rawConn.RawInput.Len() == 0 {
|
||||||
|
err = io.EOF
|
||||||
|
}
|
||||||
|
if e, ok := err.(net.Error); !ok || !e.Temporary() {
|
||||||
|
c.rawConn.In.SetErrorLocked(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hdr := c.rawConn.RawInput.Bytes()[:recordHeaderLen]
|
||||||
|
typ = hdr[0]
|
||||||
|
|
||||||
|
vers := uint16(hdr[1])<<8 | uint16(hdr[2])
|
||||||
|
expectedVers := *c.rawConn.Vers
|
||||||
|
if expectedVers == tls.VersionTLS13 {
|
||||||
|
// All TLS 1.3 records are expected to have 0x0303 (1.2) after
|
||||||
|
// the initial hello (RFC 8446 Section 5.1).
|
||||||
|
expectedVers = tls.VersionTLS12
|
||||||
|
}
|
||||||
|
n := int(hdr[3])<<8 | int(hdr[4])
|
||||||
|
if /*c.haveVers && */ vers != expectedVers {
|
||||||
|
c.sendAlert(alertProtocolVersion)
|
||||||
|
msg := fmt.Sprintf("received record with version %x when expecting version %x", vers, expectedVers)
|
||||||
|
err = c.rawConn.In.SetErrorLocked(c.newRecordHeaderError(nil, msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//if !c.haveVers {
|
||||||
|
// // First message, be extra suspicious: this might not be a TLS
|
||||||
|
// // client. Bail out before reading a full 'body', if possible.
|
||||||
|
// // The current max version is 3.3 so if the version is >= 16.0,
|
||||||
|
// // it's probably not real.
|
||||||
|
// if (typ != recordTypeAlert && typ != recordTypeHandshake) || vers >= 0x1000 {
|
||||||
|
// err = c.rawConn.In.SetErrorLocked(c.newRecordHeaderError(c.conn, "first record does not look like a TLS handshake"))
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
if *c.rawConn.Vers == tls.VersionTLS13 && n > maxCiphertextTLS13 || n > maxCiphertext {
|
||||||
|
c.sendAlert(alertRecordOverflow)
|
||||||
|
msg := fmt.Sprintf("oversized record received with length %d", n)
|
||||||
|
err = c.rawConn.In.SetErrorLocked(c.newRecordHeaderError(nil, msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = c.readFromUntil(c.conn, recordHeaderLen+n); err != nil {
|
||||||
|
if e, ok := err.(net.Error); !ok || !e.Temporary() {
|
||||||
|
c.rawConn.In.SetErrorLocked(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process message.
|
||||||
|
record := c.rawConn.RawInput.Next(recordHeaderLen + n)
|
||||||
|
data, typ, err = c.rawConn.In.Decrypt(record)
|
||||||
|
if err != nil {
|
||||||
|
err = c.rawConn.In.SetErrorLocked(c.sendAlert(uint8(err.(tls.AlertError))))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// retryReadRecord recurs into readRecordOrCCS to drop a non-advancing record, like
|
||||||
|
// a warning alert, empty application_data, or a change_cipher_spec in TLS 1.3.
|
||||||
|
func (c *Conn) retryReadRecord( /*expectChangeCipherSpec bool*/ ) error {
|
||||||
|
//c.retryCount++
|
||||||
|
//if c.retryCount > maxUselessRecords {
|
||||||
|
// c.sendAlert(alertUnexpectedMessage)
|
||||||
|
// return c.in.setErrorLocked(errors.New("tls: too many ignored records"))
|
||||||
|
//}
|
||||||
|
return c.readRecord( /*expectChangeCipherSpec*/ )
|
||||||
|
}
|
||||||
|
|
||||||
|
// atLeastReader reads from R, stopping with EOF once at least N bytes have been
|
||||||
|
// read. It is different from an io.LimitedReader in that it doesn't cut short
|
||||||
|
// the last Read call, and in that it considers an early EOF an error.
|
||||||
|
type atLeastReader struct {
|
||||||
|
R io.Reader
|
||||||
|
N int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *atLeastReader) Read(p []byte) (int, error) {
|
||||||
|
if r.N <= 0 {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
n, err := r.R.Read(p)
|
||||||
|
r.N -= int64(n) // won't underflow unless len(p) >= n > 9223372036854775809
|
||||||
|
if r.N > 0 && err == io.EOF {
|
||||||
|
return n, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
if r.N <= 0 && err == nil {
|
||||||
|
return n, io.EOF
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// readFromUntil reads from r into c.rawConn.RawInput until c.rawConn.RawInput contains
|
||||||
|
// at least n bytes or else returns an error.
|
||||||
|
func (c *Conn) readFromUntil(r io.Reader, n int) error {
|
||||||
|
if c.rawConn.RawInput.Len() >= n {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
needs := n - c.rawConn.RawInput.Len()
|
||||||
|
// There might be extra input waiting on the wire. Make a best effort
|
||||||
|
// attempt to fetch it so that it can be used in (*Conn).Read to
|
||||||
|
// "predict" closeNotify alerts.
|
||||||
|
c.rawConn.RawInput.Grow(needs + bytes.MinRead)
|
||||||
|
_, err := c.rawConn.RawInput.ReadFrom(&atLeastReader{r, int64(needs)})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) newRecordHeaderError(conn net.Conn, msg string) (err tls.RecordHeaderError) {
|
||||||
|
err.Msg = msg
|
||||||
|
err.Conn = conn
|
||||||
|
copy(err.RecordHeader[:], c.rawConn.RawInput.Bytes())
|
||||||
|
return err
|
||||||
|
}
|
||||||
41
common/ktls/ktls_read_wait.go
Normal file
41
common/ktls/ktls_read_wait.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build linux && go1.25 && badlinkname
|
||||||
|
|
||||||
|
package ktls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) {
|
||||||
|
c.readWaitOptions = options
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
|
||||||
|
c.rawConn.In.Lock()
|
||||||
|
defer c.rawConn.In.Unlock()
|
||||||
|
for c.rawConn.Input.Len() == 0 {
|
||||||
|
err = c.readRecord()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer = c.readWaitOptions.NewBuffer()
|
||||||
|
n, err := c.rawConn.Input.Read(buffer.FreeBytes())
|
||||||
|
if err != nil {
|
||||||
|
buffer.Release()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buffer.Truncate(n)
|
||||||
|
if n != 0 && c.rawConn.Input.Len() == 0 && c.rawConn.Input.Len() > 0 &&
|
||||||
|
c.rawConn.RawInput.Bytes()[0] == recordTypeAlert {
|
||||||
|
_ = c.rawConn.ReadRecord()
|
||||||
|
}
|
||||||
|
c.readWaitOptions.PostReturn(buffer)
|
||||||
|
return
|
||||||
|
}
|
||||||
15
common/ktls/ktls_stub_nolinkname.go
Normal file
15
common/ktls/ktls_stub_nolinkname.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//go:build linux && go1.25 && !badlinkname
|
||||||
|
|
||||||
|
package ktls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) {
|
||||||
|
return nil, E.New("kTLS requires build flags `badlinkname` and `-ldflags=-checklinkname=0`, please recompile your binary")
|
||||||
|
}
|
||||||
15
common/ktls/ktls_stub_nonlinux.go
Normal file
15
common/ktls/ktls_stub_nonlinux.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package ktls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) {
|
||||||
|
return nil, E.New("kTLS is only supported on Linux")
|
||||||
|
}
|
||||||
15
common/ktls/ktls_stub_oldgo.go
Normal file
15
common/ktls/ktls_stub_oldgo.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//go:build linux && !go1.25
|
||||||
|
|
||||||
|
package ktls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) {
|
||||||
|
return nil, E.New("kTLS requires Go 1.25 or later, please recompile your binary")
|
||||||
|
}
|
||||||
154
common/ktls/ktls_write.go
Normal file
154
common/ktls/ktls_write.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build linux && go1.25 && badlinkname
|
||||||
|
|
||||||
|
package ktls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) Write(b []byte) (int, error) {
|
||||||
|
if !c.kernelTx {
|
||||||
|
return c.Conn.Write(b)
|
||||||
|
}
|
||||||
|
// interlock with Close below
|
||||||
|
for {
|
||||||
|
x := c.rawConn.ActiveCall.Load()
|
||||||
|
if x&1 != 0 {
|
||||||
|
return 0, net.ErrClosed
|
||||||
|
}
|
||||||
|
if c.rawConn.ActiveCall.CompareAndSwap(x, x+2) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer c.rawConn.ActiveCall.Add(-2)
|
||||||
|
|
||||||
|
//if err := c.Conn.HandshakeContext(context.Background()); err != nil {
|
||||||
|
// return 0, err
|
||||||
|
//}
|
||||||
|
|
||||||
|
c.rawConn.Out.Lock()
|
||||||
|
defer c.rawConn.Out.Unlock()
|
||||||
|
|
||||||
|
if err := *c.rawConn.Out.Err; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.rawConn.IsHandshakeComplete.Load() {
|
||||||
|
return 0, tls.AlertError(alertInternalError)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *c.rawConn.CloseNotifySent {
|
||||||
|
// return 0, errShutdown
|
||||||
|
return 0, errors.New("tls: protocol is shutdown")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS 1.0 is susceptible to a chosen-plaintext
|
||||||
|
// attack when using block mode ciphers due to predictable IVs.
|
||||||
|
// This can be prevented by splitting each Application Data
|
||||||
|
// record into two records, effectively randomizing the RawIV.
|
||||||
|
//
|
||||||
|
// https://www.openssl.org/~bodo/tls-cbc.txt
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=665814
|
||||||
|
// https://www.imperialviolet.org/2012/01/15/beastfollowup.html
|
||||||
|
|
||||||
|
var m int
|
||||||
|
if len(b) > 1 && *c.rawConn.Vers == tls.VersionTLS10 {
|
||||||
|
if _, ok := (*c.rawConn.Out.Cipher).(cipher.BlockMode); ok {
|
||||||
|
n, err := c.writeRecordLocked(recordTypeApplicationData, b[:1])
|
||||||
|
if err != nil {
|
||||||
|
return n, c.rawConn.Out.SetErrorLocked(err)
|
||||||
|
}
|
||||||
|
m, b = 1, b[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := c.writeRecordLocked(recordTypeApplicationData, b)
|
||||||
|
return n + m, c.rawConn.Out.SetErrorLocked(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeRecordLocked(typ uint16, data []byte) (n int, err error) {
|
||||||
|
if !c.kernelTx {
|
||||||
|
return c.rawConn.WriteRecordLocked(typ, data)
|
||||||
|
}
|
||||||
|
/*for len(data) > 0 {
|
||||||
|
m := len(data)
|
||||||
|
if maxPayload := c.maxPayloadSizeForWrite(typ); m > maxPayload {
|
||||||
|
m = maxPayload
|
||||||
|
}
|
||||||
|
_, err = c.writeKernelRecord(typ, data[:m])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n += m
|
||||||
|
data = data[m:]
|
||||||
|
}*/
|
||||||
|
return c.writeKernelRecord(typ, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// tcpMSSEstimate is a conservative estimate of the TCP maximum segment
|
||||||
|
// size (MSS). A constant is used, rather than querying the kernel for
|
||||||
|
// the actual MSS, to avoid complexity. The value here is the IPv6
|
||||||
|
// minimum MTU (1280 bytes) minus the overhead of an IPv6 header (40
|
||||||
|
// bytes) and a TCP header with timestamps (32 bytes).
|
||||||
|
tcpMSSEstimate = 1208
|
||||||
|
|
||||||
|
// recordSizeBoostThreshold is the number of bytes of application data
|
||||||
|
// sent after which the TLS record size will be increased to the
|
||||||
|
// maximum.
|
||||||
|
recordSizeBoostThreshold = 128 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) maxPayloadSizeForWrite(typ uint16) int {
|
||||||
|
if /*c.config.DynamicRecordSizingDisabled ||*/ typ != recordTypeApplicationData {
|
||||||
|
return maxPlaintext
|
||||||
|
}
|
||||||
|
|
||||||
|
if *c.rawConn.PacketsSent >= recordSizeBoostThreshold {
|
||||||
|
return maxPlaintext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtract TLS overheads to get the maximum payload size.
|
||||||
|
payloadBytes := tcpMSSEstimate - recordHeaderLen - c.rawConn.Out.ExplicitNonceLen()
|
||||||
|
if rawCipher := *c.rawConn.Out.Cipher; rawCipher != nil {
|
||||||
|
switch ciph := rawCipher.(type) {
|
||||||
|
case cipher.Stream:
|
||||||
|
payloadBytes -= (*c.rawConn.Out.Mac).Size()
|
||||||
|
case cipher.AEAD:
|
||||||
|
payloadBytes -= ciph.Overhead()
|
||||||
|
/*case cbcMode:
|
||||||
|
blockSize := ciph.BlockSize()
|
||||||
|
// The payload must fit in a multiple of blockSize, with
|
||||||
|
// room for at least one padding byte.
|
||||||
|
payloadBytes = (payloadBytes & ^(blockSize - 1)) - 1
|
||||||
|
// The RawMac is appended before padding so affects the
|
||||||
|
// payload size directly.
|
||||||
|
payloadBytes -= c.out.mac.Size()*/
|
||||||
|
default:
|
||||||
|
panic("unknown cipher type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if *c.rawConn.Vers == tls.VersionTLS13 {
|
||||||
|
payloadBytes-- // encrypted ContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow packet growth in arithmetic progression up to max.
|
||||||
|
pkt := *c.rawConn.PacketsSent
|
||||||
|
*c.rawConn.PacketsSent++
|
||||||
|
if pkt > 1000 {
|
||||||
|
return maxPlaintext // avoid overflow in multiply below
|
||||||
|
}
|
||||||
|
|
||||||
|
n := payloadBytes * int(pkt+1)
|
||||||
|
if n > maxPlaintext {
|
||||||
|
n = maxPlaintext
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user