mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-12 01:57:18 +10:00
Compare commits
997 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3f8567690 | ||
|
|
40c7f3e170 | ||
|
|
c506255e0f | ||
|
|
87c6fd4c0f | ||
|
|
19c445d28e | ||
|
|
9119a5209b | ||
|
|
46c8d6e61f | ||
|
|
ea17c2786d | ||
|
|
12ababd911 | ||
|
|
0523845833 | ||
|
|
57794919fa | ||
|
|
f5bb5cf343 | ||
|
|
3eed614dea | ||
|
|
76a295a660 | ||
|
|
082e3fb8df | ||
|
|
a0cab4f563 | ||
|
|
aeb7308e81 | ||
|
|
bb1ebfda83 | ||
|
|
c05c798221 | ||
|
|
55b1bcc6a5 | ||
|
|
d6eddce420 | ||
|
|
4bf057139b | ||
|
|
a1b28b8282 | ||
|
|
d0aaf71770 | ||
|
|
2f31202c6b | ||
|
|
e4cc510712 | ||
|
|
e329bf6865 | ||
|
|
2badcec765 | ||
|
|
e71c13b1a2 | ||
|
|
a959a67ed3 | ||
|
|
a1044af579 | ||
|
|
a64b57451a | ||
|
|
f0e2318cbd | ||
|
|
ebec308fd8 | ||
|
|
ca094587be | ||
|
|
ca3b86c781 | ||
|
|
5a1d0047b9 | ||
|
|
4669854039 | ||
|
|
2eecdc38a4 | ||
|
|
83581b7c1a | ||
|
|
d346f0023d | ||
|
|
47b7a29cbd | ||
|
|
cffc07579d | ||
|
|
0ef268637e | ||
|
|
50f5a76380 | ||
|
|
20ca05dd36 | ||
|
|
5a792b186a | ||
|
|
3f458064a3 | ||
|
|
5269231df0 | ||
|
|
fc8e49994c | ||
|
|
e911d4aa4b | ||
|
|
01f6e70bc5 | ||
|
|
5f1e39a42c | ||
|
|
4f7770e254 | ||
|
|
e8c4c942c0 | ||
|
|
253976d6c0 | ||
|
|
f0571b4122 | ||
|
|
1b71e52e90 | ||
|
|
6d24be23da | ||
|
|
2a45c178fa | ||
|
|
81e214812f | ||
|
|
4d23773a25 | ||
|
|
40a0b69918 | ||
|
|
a7b37c5953 | ||
|
|
03663a5093 | ||
|
|
b08226a850 | ||
|
|
edbae5dc4d | ||
|
|
0f8ad0234b | ||
|
|
661eadc3bd | ||
|
|
50c1290567 | ||
|
|
eaccc9759a | ||
|
|
925214869b | ||
|
|
6a2bfd26d0 | ||
|
|
72a81afb76 | ||
|
|
240abe204c | ||
|
|
7c49196792 | ||
|
|
3a2808cff6 | ||
|
|
005d6cf4cf | ||
|
|
36dff630d6 | ||
|
|
1825869124 | ||
|
|
3cadc90375 | ||
|
|
2c6967d7f9 | ||
|
|
fe866b123a | ||
|
|
cbef1b1e59 | ||
|
|
e21f84932c | ||
|
|
7a679bc328 | ||
|
|
6635dd9abc | ||
|
|
ce164724ea | ||
|
|
a3ef7a7d88 | ||
|
|
71218ef0d3 | ||
|
|
e777b4c6dc | ||
|
|
6815f94180 | ||
|
|
b013acd89d | ||
|
|
f7c2eb6e76 | ||
|
|
3ef9b1b343 | ||
|
|
2224c68959 | ||
|
|
bb7d03d1db | ||
|
|
50036924e8 | ||
|
|
c2c3f7284f | ||
|
|
f6fee53676 | ||
|
|
63b8e8ed23 | ||
|
|
6ae86eda98 | ||
|
|
267d9617b7 | ||
|
|
0a06ccae50 | ||
|
|
8de0fad9f5 | ||
|
|
e05bf6308e | ||
|
|
a20a0cb455 | ||
|
|
d29f7475d2 | ||
|
|
aaa6702863 | ||
|
|
bb928f096a | ||
|
|
9f01d5c5b4 | ||
|
|
11629a931b | ||
|
|
126f825241 | ||
|
|
998cc7bd22 | ||
|
|
3efccaa8f5 | ||
|
|
d57b35ec30 | ||
|
|
e82dab027d | ||
|
|
9350f3983b | ||
|
|
53b123241f | ||
|
|
97286eea1e | ||
|
|
343e24969d | ||
|
|
31c294d998 | ||
|
|
3b161ab30c | ||
|
|
41fd1778a7 | ||
|
|
ac930cf1aa | ||
|
|
e143fc510d | ||
|
|
bea177a4cd | ||
|
|
aa05a4d050 | ||
|
|
a8112ff824 | ||
|
|
a7710c3845 | ||
|
|
cb2e15f8a7 | ||
|
|
23aa8a0543 | ||
|
|
edf7d046eb | ||
|
|
de0b5cc1c2 | ||
|
|
2686e8afea | ||
|
|
d9853ca2be | ||
|
|
b617eb5adf | ||
|
|
ddf38799e2 | ||
|
|
5291d43dc8 | ||
|
|
a634830d85 | ||
|
|
e5d191ca73 | ||
|
|
2371f0fd51 | ||
|
|
cfdce7a96f | ||
|
|
dc8ac01dec | ||
|
|
5f18738b2b | ||
|
|
7b4e4ca2d0 | ||
|
|
01ba4668b6 | ||
|
|
e782d21806 | ||
|
|
00155d61fc | ||
|
|
8f2273a2b4 | ||
|
|
0d0526afa2 | ||
|
|
ac2d07b61a | ||
|
|
d35487f422 | ||
|
|
2749f4a013 | ||
|
|
45c679648e | ||
|
|
5f2f7fc8b9 | ||
|
|
83c79102cf | ||
|
|
8b95292e53 | ||
|
|
3de7a2ddd3 | ||
|
|
8437a6cb4e | ||
|
|
9c4d08c6e1 | ||
|
|
e26096085e | ||
|
|
2f1b2199c5 | ||
|
|
af791db01f | ||
|
|
abcf030d89 | ||
|
|
7840dc73e3 | ||
|
|
df9050400e | ||
|
|
fdd38d6cf8 | ||
|
|
9891fd672f | ||
|
|
92a84ee112 | ||
|
|
992331f17e | ||
|
|
4fb227ed86 | ||
|
|
5a1ddea100 | ||
|
|
fbaa2f9de9 | ||
|
|
97ab9bb194 | ||
|
|
61ac141124 | ||
|
|
d4d49d9df5 | ||
|
|
c60a944aac | ||
|
|
17584c245f | ||
|
|
6e84b694a4 | ||
|
|
34a93171f0 | ||
|
|
678f6ef72f | ||
|
|
ae8187ed15 | ||
|
|
12dd1ac87f | ||
|
|
85c8f00885 | ||
|
|
e7b7ae811f | ||
|
|
a9743b77f6 | ||
|
|
4068871d97 | ||
|
|
f05afcea39 | ||
|
|
688e9daef4 | ||
|
|
64edacffb7 | ||
|
|
743df5373b | ||
|
|
e80084316d | ||
|
|
9dcd427743 | ||
|
|
d17e93384b | ||
|
|
c1ffcf365e | ||
|
|
3040e97222 | ||
|
|
5f063fb0b5 | ||
|
|
a7dadd8671 | ||
|
|
c320be75a7 | ||
|
|
bd7adcbb7e | ||
|
|
1d6d3edec5 | ||
|
|
46bfeb574c | ||
|
|
a1449ee40e | ||
|
|
8cb41b5fa6 | ||
|
|
53475c7390 | ||
|
|
5d8af150a7 | ||
|
|
69499a51a5 | ||
|
|
4c050d7f4b | ||
|
|
533fca9fa3 | ||
|
|
187bf2f7bc | ||
|
|
983a4222ad | ||
|
|
2ea506aeb8 | ||
|
|
5b343d4c72 | ||
|
|
be61ca64d4 | ||
|
|
efe33cf48d | ||
|
|
fe8d46cce5 | ||
|
|
b1f289bce5 | ||
|
|
a8beb80876 | ||
|
|
ff209471d8 | ||
|
|
806f7d0a2b | ||
|
|
6b943caf37 | ||
|
|
4ea2d460f4 | ||
|
|
c84c18f960 | ||
|
|
1402bdab41 | ||
|
|
7082cf277e | ||
|
|
b9310154a7 | ||
|
|
55c34e3fb0 | ||
|
|
68f2202eec | ||
|
|
5057e50bb8 | ||
|
|
23e1a69955 | ||
|
|
b83c6c9d20 | ||
|
|
67deac6d44 | ||
|
|
ea3731162b | ||
|
|
c75e32e722 | ||
|
|
e7b35be5f6 | ||
|
|
5a309266f0 | ||
|
|
05669eaaad | ||
|
|
e91a6e5439 | ||
|
|
43f72a6419 | ||
|
|
6dcacf3b5e | ||
|
|
edad4d1ce7 | ||
|
|
262842c87d | ||
|
|
376f527742 | ||
|
|
c0bbb3849d | ||
|
|
738c25d818 | ||
|
|
027af4d4ee | ||
|
|
6011f4483a | ||
|
|
fc22466e3b | ||
|
|
975e13a313 | ||
|
|
f46732bc0e | ||
|
|
5c5c25e3ad | ||
|
|
53a0bf2d11 | ||
|
|
7b79d98f59 | ||
|
|
1dd2c26f31 | ||
|
|
d14170348d | ||
|
|
9f94b21687 | ||
|
|
cf57e46d69 | ||
|
|
b459001600 | ||
|
|
73267fd6ad | ||
|
|
1019ecfdcf | ||
|
|
81b847faca | ||
|
|
ce4c76cdd2 | ||
|
|
917420e79a | ||
|
|
0b14dc3228 | ||
|
|
cbdaf3272b | ||
|
|
d51ab2b0a7 | ||
|
|
1363e16312 | ||
|
|
f43d0141f3 | ||
|
|
90b3aad83a | ||
|
|
2675aff98a | ||
|
|
09ffa2c66e | ||
|
|
9fba4f02b6 | ||
|
|
59987747e5 | ||
|
|
c40140bbae | ||
|
|
2123b216c0 | ||
|
|
1983f54907 | ||
|
|
8d629ef323 | ||
|
|
f57bee2f4b | ||
|
|
679739683e | ||
|
|
4fcce1f073 | ||
|
|
ff14220e08 | ||
|
|
a7b7a5c3c5 | ||
|
|
b054441f34 | ||
|
|
1e31d26e03 | ||
|
|
ffe515d0e0 | ||
|
|
aad021f521 | ||
|
|
4a986459ee | ||
|
|
9532d0cba4 | ||
|
|
cadc34f3ad | ||
|
|
db23a48b36 | ||
|
|
407cf68e59 | ||
|
|
e0058ca9c5 | ||
|
|
8140af01aa | ||
|
|
98bf696d01 | ||
|
|
e075bb5c8d | ||
|
|
c6baabedef | ||
|
|
6e6998dab7 | ||
|
|
1a29c23263 | ||
|
|
0f87396ab6 | ||
|
|
ffde948860 | ||
|
|
69b5dbdcc3 | ||
|
|
1121517755 | ||
|
|
6879def619 | ||
|
|
5c0f6d0a6f | ||
|
|
d74abbd20e | ||
|
|
120dae4eed | ||
|
|
bb651db2d2 | ||
|
|
e929dde13e | ||
|
|
9d75385bbb | ||
|
|
1c526feec1 | ||
|
|
7df26986de | ||
|
|
5f2d23a12d | ||
|
|
d9e65c0969 | ||
|
|
ec1160924f | ||
|
|
230e8f895d | ||
|
|
af79378734 | ||
|
|
07ce5e0d22 | ||
|
|
9c8565cf21 | ||
|
|
5ad0ea2b5a | ||
|
|
e482053c8a | ||
|
|
945713d886 | ||
|
|
9bb62ad6b5 | ||
|
|
c2bda9fbde | ||
|
|
1d1db62a44 | ||
|
|
39405373f8 | ||
|
|
22a7988d3f | ||
|
|
b2092fafb7 | ||
|
|
cc7b5d8280 | ||
|
|
702d96a738 | ||
|
|
b9f34f1309 | ||
|
|
07724a0ddd | ||
|
|
83c3454685 | ||
|
|
7d263eb733 | ||
|
|
222687d9c5 | ||
|
|
07d3652e30 | ||
|
|
8d5b9d240a | ||
|
|
4f12eba944 | ||
|
|
a7f77d59c1 | ||
|
|
597248130f | ||
|
|
3c2c9cf317 | ||
|
|
e572b9d0cd | ||
|
|
52e9059a8d | ||
|
|
0cb9cff690 | ||
|
|
c0669cb2a5 | ||
|
|
c5902f2473 | ||
|
|
22028602e8 | ||
|
|
bd54608473 | ||
|
|
3741394269 | ||
|
|
6266d2df7e | ||
|
|
01dfba722a | ||
|
|
f8d5f01665 | ||
|
|
ad999d4791 | ||
|
|
6f1b258501 | ||
|
|
f949ddc0ab | ||
|
|
f53007cbf3 | ||
|
|
c287731df9 | ||
|
|
bc32c78d03 | ||
|
|
daee0db7bb | ||
|
|
91fbf4c79b | ||
|
|
54d9ef2f2a | ||
|
|
e056d4502b | ||
|
|
98c2c439aa | ||
|
|
b6068cea6b | ||
|
|
9c9affa719 | ||
|
|
8eb7dd0059 | ||
|
|
a62ad44883 | ||
|
|
2850354070 | ||
|
|
0a4abcbbc8 | ||
|
|
b491c350ae | ||
|
|
1fbe7c54bf | ||
|
|
9d32fc9bd1 | ||
|
|
542612129d | ||
|
|
750f87bb0a | ||
|
|
e168de79c7 | ||
|
|
9bca5a517f | ||
|
|
aa94cfb876 | ||
|
|
52b776b561 | ||
|
|
c74d3a53d4 | ||
|
|
fe7ac80a6c | ||
|
|
e50b334b9a | ||
|
|
a0d8e374fb | ||
|
|
d3a67cb5ae | ||
|
|
e69e98b185 | ||
|
|
5e1499d67b | ||
|
|
e8dad1afeb | ||
|
|
6ce4e31fc8 | ||
|
|
d2d4faf520 | ||
|
|
438de36749 | ||
|
|
df0eef770e | ||
|
|
bbdd495ed5 | ||
|
|
d686172854 | ||
|
|
e1d96cb64e | ||
|
|
d5f94b65b7 | ||
|
|
ec2d0b6b3c | ||
|
|
3a92bf993d | ||
|
|
ec13965fd0 | ||
|
|
ddf747006e | ||
|
|
4382093868 | ||
|
|
a5322850b3 | ||
|
|
407b08975c | ||
|
|
c7067ff5e8 | ||
|
|
9b2384b296 | ||
|
|
b498a22972 | ||
|
|
20e9da5c67 | ||
|
|
ec8974673b | ||
|
|
5e6e7923e4 | ||
|
|
de1b5971e1 | ||
|
|
5c20d0b4d5 | ||
|
|
9df96ac7f1 | ||
|
|
87cd925144 | ||
|
|
fecb796000 | ||
|
|
cfb6c804aa | ||
|
|
11c50c7558 | ||
|
|
34cc7f176e | ||
|
|
b54da9c6af | ||
|
|
f44f86b832 | ||
|
|
4ebf40f582 | ||
|
|
53e4302143 | ||
|
|
cf778eda4f | ||
|
|
bb63429079 | ||
|
|
f7f9a7ae20 | ||
|
|
8699412a4c | ||
|
|
0d7aa19cd1 | ||
|
|
50a7295360 | ||
|
|
e57b6ae98d | ||
|
|
6843970536 | ||
|
|
62425ad3e4 | ||
|
|
e1e217854e | ||
|
|
5bf177b021 | ||
|
|
72dbf2e2b4 | ||
|
|
46c318c6fe | ||
|
|
05bb1b88c3 | ||
|
|
5176ea9fe0 | ||
|
|
36d349acd2 | ||
|
|
4feee983b5 | ||
|
|
9b12e3e389 | ||
|
|
afd3464216 | ||
|
|
8b64446274 | ||
|
|
28aa4c4d1f | ||
|
|
0be3cdc8fb | ||
|
|
f8be484019 | ||
|
|
35f03f092d | ||
|
|
c3d7401ead | ||
|
|
4db7eb9d9e | ||
|
|
fd4efd6104 | ||
|
|
19a35ec6a4 | ||
|
|
2012c0ca1e | ||
|
|
187421c754 | ||
|
|
b3fb86d415 | ||
|
|
88fafd4e30 | ||
|
|
8056932f9c | ||
|
|
c8af003bfc | ||
|
|
4999441a85 | ||
|
|
09b001e795 | ||
|
|
3b3a251008 | ||
|
|
2e4eb9aa39 | ||
|
|
77fd284703 | ||
|
|
0a4517f4b7 | ||
|
|
4395db3206 | ||
|
|
dd5b0abc67 | ||
|
|
466800aa3a | ||
|
|
4328c535a9 | ||
|
|
f9516709da | ||
|
|
5dce722879 | ||
|
|
9324a39d4e | ||
|
|
84904c5206 | ||
|
|
fe4b429fc2 | ||
|
|
f680d0acaf | ||
|
|
4baff5aeb1 | ||
|
|
f25296fb23 | ||
|
|
e717852c73 | ||
|
|
13dc70f649 | ||
|
|
46040a71c3 | ||
|
|
0558b3fc5c | ||
|
|
99b2ab5526 | ||
|
|
e5f3bb6344 | ||
|
|
c7f89ad88e | ||
|
|
e0d9f79445 | ||
|
|
b6dbb69fc4 | ||
|
|
b76fabee65 | ||
|
|
872bcfd1c0 | ||
|
|
b033c13ca2 | ||
|
|
2db188f3a1 | ||
|
|
11de271c8f | ||
|
|
40c800c57c | ||
|
|
91b0540e95 | ||
|
|
ce6d186345 | ||
|
|
32bc4450a7 | ||
|
|
43f31b40ba | ||
|
|
a3a5185b15 | ||
|
|
14a0f180c8 | ||
|
|
cc9cb0b477 | ||
|
|
2cb0e37f50 | ||
|
|
dbd5be55b0 | ||
|
|
f674b4fbd5 | ||
|
|
5a4e8fea81 | ||
|
|
78e02b52ca | ||
|
|
ffdaae90d7 | ||
|
|
c77681ea17 | ||
|
|
d824390167 | ||
|
|
70cf681ff2 | ||
|
|
b004b9ec81 | ||
|
|
657b05fd96 | ||
|
|
caad60da45 | ||
|
|
7d22cf9b45 | ||
|
|
5cb178ca93 | ||
|
|
16788008b6 | ||
|
|
6ec7a33046 | ||
|
|
6af9c2b3ca | ||
|
|
bdc620dab1 | ||
|
|
a88820af31 | ||
|
|
3688f2e114 | ||
|
|
a183958d53 | ||
|
|
1c8a9e91b7 | ||
|
|
325f6c71ff | ||
|
|
6c6c0792ad | ||
|
|
9264f2307c | ||
|
|
87dd328700 | ||
|
|
0cec92dd0f | ||
|
|
e88afa9665 | ||
|
|
3883a81315 | ||
|
|
c919ad079a | ||
|
|
83593aee70 | ||
|
|
ac7cc09694 | ||
|
|
d032e3568b | ||
|
|
c24df037ac | ||
|
|
a2d43b3746 | ||
|
|
5b3b74bd0f | ||
|
|
d24d3b26dc | ||
|
|
5db3cd7781 | ||
|
|
c88af8b081 | ||
|
|
45852ca3e7 | ||
|
|
03ce555104 | ||
|
|
dd0a07624e | ||
|
|
b9b2b77814 | ||
|
|
2366835121 | ||
|
|
42e1dea7d2 | ||
|
|
13d7716b02 | ||
|
|
7ecb9fc738 | ||
|
|
19b15e0d10 | ||
|
|
0b15de461b | ||
|
|
27aba99e6c | ||
|
|
8151bcfd6b | ||
|
|
e8802357e1 | ||
|
|
6e22c004f6 | ||
|
|
20e1caa531 | ||
|
|
32ad3c3db3 | ||
|
|
1f5f8a7dde | ||
|
|
6da1460795 | ||
|
|
b14ae51f71 | ||
|
|
5af8d001ae | ||
|
|
0ca344df5f | ||
|
|
49f568abbd | ||
|
|
3b4e811907 | ||
|
|
d0e9443031 | ||
|
|
f7e9d9ab1f | ||
|
|
7834d6bca7 | ||
|
|
ed50257735 | ||
|
|
f15f525c5c | ||
|
|
e4bff0460d | ||
|
|
5ce3ddee9b | ||
|
|
22bf7a9509 | ||
|
|
842730707c | ||
|
|
a8f13bd956 | ||
|
|
cd5c2a7999 | ||
|
|
fbc94b9e3e | ||
|
|
e766f25d55 | ||
|
|
140ed9a4cb | ||
|
|
60094884cd | ||
|
|
0e8a4d141a | ||
|
|
17b78a6339 | ||
|
|
e99741159b | ||
|
|
6b9603227b | ||
|
|
23e8d282a3 | ||
|
|
611d6bbfc5 | ||
|
|
f26785c0ba | ||
|
|
5bcfb71737 | ||
|
|
4135c4974f | ||
|
|
222196b182 | ||
|
|
86e55c5c1c | ||
|
|
73c068b96f | ||
|
|
f516026540 | ||
|
|
3c5bc842ed | ||
|
|
d8270a66f4 | ||
|
|
123c383eae | ||
|
|
67814faf92 | ||
|
|
ec4a0c8497 | ||
|
|
21cb227bc2 | ||
|
|
1610bdc5dd | ||
|
|
3296a2f7b2 | ||
|
|
2bd91baad0 | ||
|
|
a624cd9b49 | ||
|
|
02afba132f | ||
|
|
99890a1af0 | ||
|
|
437f1f819c | ||
|
|
92a79e6158 | ||
|
|
c9efd0a74f | ||
|
|
9da349748a | ||
|
|
2423cbbbfe | ||
|
|
4833f6d5db | ||
|
|
9db3cb5cb7 | ||
|
|
c14b353a29 | ||
|
|
19d08b55c8 | ||
|
|
39514b3ca0 | ||
|
|
7ea9d48987 | ||
|
|
df3a982141 | ||
|
|
687b4509df | ||
|
|
41ec2e7944 | ||
|
|
1bd3a9144d | ||
|
|
6e852cc99b | ||
|
|
8320dd0b51 | ||
|
|
960d04d172 | ||
|
|
86ea035bdd | ||
|
|
9b6449dcf4 | ||
|
|
4e22ac1a35 | ||
|
|
8a779f6e94 | ||
|
|
d461768ffb | ||
|
|
5d41e328d4 | ||
|
|
fe492904e9 | ||
|
|
168253b851 | ||
|
|
05620a369e | ||
|
|
8e0fe55363 | ||
|
|
59e521c1db | ||
|
|
f32c149738 | ||
|
|
23a35b3c06 | ||
|
|
044f9c5d4f | ||
|
|
54f9625bdc | ||
|
|
ff0693be32 | ||
|
|
53d9ad93e3 | ||
|
|
f5c5570bec | ||
|
|
53f19a6ead | ||
|
|
cfaf15f429 | ||
|
|
9e67f3b4a5 | ||
|
|
4d2185a2d4 | ||
|
|
33f22263ca | ||
|
|
d09aa07d21 | ||
|
|
8afb8ca7eb | ||
|
|
80ed5bf8fb | ||
|
|
81e7b0b320 | ||
|
|
a828c3b5da | ||
|
|
c95e4a13a1 | ||
|
|
726a7e19eb | ||
|
|
8953ddc6e0 | ||
|
|
7ebbd58b00 | ||
|
|
d0095fd0f4 | ||
|
|
66d8d563eb | ||
|
|
4bf96c7eb5 | ||
|
|
f687c25fa9 | ||
|
|
a92412ecac | ||
|
|
8dcafa5b33 | ||
|
|
7a02cb83a7 | ||
|
|
51ce672076 | ||
|
|
7734afc40c | ||
|
|
ee3cd49aa5 | ||
|
|
bf20ff84b5 | ||
|
|
c58302554c | ||
|
|
05ed88aba8 | ||
|
|
9f5cc0442b | ||
|
|
2641a43ad8 | ||
|
|
4a6ab5e9fd | ||
|
|
d1fe17a4db | ||
|
|
7c910165ef | ||
|
|
8c1fddcf8d | ||
|
|
01b4769852 | ||
|
|
a401828ed5 | ||
|
|
ffd54eef6c | ||
|
|
c16e4316d6 | ||
|
|
8b7fe20b7f | ||
|
|
696c1065b6 | ||
|
|
5d690f4147 | ||
|
|
f906641a82 | ||
|
|
89913dfa8c | ||
|
|
468778f67f | ||
|
|
22a22aebe2 | ||
|
|
a2d2ec9b45 | ||
|
|
2695b3516e | ||
|
|
3a9ef8fac0 | ||
|
|
ebad363201 | ||
|
|
11076d52cd | ||
|
|
5eb132063e | ||
|
|
13ab5d3348 | ||
|
|
ce1ddc400f | ||
|
|
2c9d25e853 | ||
|
|
3d76777760 | ||
|
|
24f4dfea04 | ||
|
|
2fc1a0a9dd | ||
|
|
617aba84e4 | ||
|
|
5510c474c7 | ||
|
|
eb2e8a0b40 | ||
|
|
972491c19d | ||
|
|
7358ca4a52 | ||
|
|
61c274045a | ||
|
|
f205140b04 | ||
|
|
1db8e03c86 | ||
|
|
2ecf86c2bc | ||
|
|
999a847e86 | ||
|
|
1f63ce5dee | ||
|
|
0ad1bbea11 | ||
|
|
b2cd78d279 | ||
|
|
d5bb58a0b4 | ||
|
|
7f84936050 | ||
|
|
6adfea0a72 | ||
|
|
10f213bf3d | ||
|
|
6e8c4f6576 | ||
|
|
9779dc0154 | ||
|
|
a2abe31298 | ||
|
|
930d177dd0 | ||
|
|
f3d1b59173 | ||
|
|
14452f3049 | ||
|
|
4119c8647b | ||
|
|
90a94a8c63 | ||
|
|
b0c39ac7ff | ||
|
|
8703e1ff98 | ||
|
|
35886b88d7 | ||
|
|
d583b35717 | ||
|
|
217ffb2f95 | ||
|
|
22f06f582b | ||
|
|
f2b5098fa0 | ||
|
|
0ca3290364 | ||
|
|
43d5b8598b | ||
|
|
f3e1d1defc | ||
|
|
95c03c9373 | ||
|
|
7e0958b4ac | ||
|
|
6a26737508 | ||
|
|
92a92f39c5 | ||
|
|
fc533cd38d | ||
|
|
68e286499d | ||
|
|
f5c1900aad | ||
|
|
6591dd58ca | ||
|
|
54af113363 | ||
|
|
3f1fe814ef | ||
|
|
5a2cebebd1 | ||
|
|
b8009d61b2 | ||
|
|
a61a64bf9e | ||
|
|
7d17c52fea | ||
|
|
f5b15b392b | ||
|
|
8a53846efd | ||
|
|
badc454452 | ||
|
|
a01bb569d1 | ||
|
|
89ff9f8368 | ||
|
|
7f816a2ebc | ||
|
|
39c141651a | ||
|
|
b0ad9bb6f1 | ||
|
|
d135d0f287 | ||
|
|
b183ccf23d | ||
|
|
c2969bc186 | ||
|
|
bd86bfcd22 | ||
|
|
8aec64b855 | ||
|
|
1445bdba37 | ||
|
|
29d08e63b5 | ||
|
|
1173fdea64 | ||
|
|
968430c338 | ||
|
|
3e5bee6faf | ||
|
|
aa613cba73 | ||
|
|
1e510511ae | ||
|
|
1b44faed17 | ||
|
|
c7a485815c | ||
|
|
7f9c870bba | ||
|
|
b5564ef3d3 | ||
|
|
8ce244dd04 | ||
|
|
0f57b93925 | ||
|
|
c90a77a185 | ||
|
|
c6586f19fa | ||
|
|
cbab86ae38 | ||
|
|
17b5f031f1 | ||
|
|
b00b6b9e25 | ||
|
|
fb6b3b0401 | ||
|
|
22ea878fe9 | ||
|
|
abe3dc6039 | ||
|
|
852829b9dc | ||
|
|
407509c985 | ||
|
|
9856b73cb5 | ||
|
|
f42356fbcb | ||
|
|
d0b467671a | ||
|
|
c18c545798 | ||
|
|
693ef293ac | ||
|
|
a006627795 | ||
|
|
0738b184e4 | ||
|
|
42524ba04e | ||
|
|
63fc95b96d | ||
|
|
ab436fc137 | ||
|
|
1546770bfd | ||
|
|
f4b2099488 | ||
|
|
a2c4d68031 | ||
|
|
cfe14f2817 | ||
|
|
a5402ffb69 | ||
|
|
4d24cf5ec4 | ||
|
|
668d354771 | ||
|
|
ad14719b14 | ||
|
|
d9aa0a67d6 | ||
|
|
92bf784f4f | ||
|
|
395b13103a | ||
|
|
628cf56d3c | ||
|
|
ac5582537f | ||
|
|
9aa7a20d96 | ||
|
|
189f02c802 | ||
|
|
2373281c41 | ||
|
|
e8f4c2d36f | ||
|
|
07b6db23c1 | ||
|
|
9a3360e5d0 | ||
|
|
007a278ac8 | ||
|
|
1db7f45370 | ||
|
|
b271e19a23 | ||
|
|
79b6bdfda1 | ||
|
|
38088f28b0 | ||
|
|
dfb8b5f2fa | ||
|
|
9913e0e025 | ||
|
|
ce567ffdde | ||
|
|
5a9913eca5 | ||
|
|
eaf1ace681 | ||
|
|
a2d1f89922 | ||
|
|
7e09beb0c3 | ||
|
|
ebf5cbf1b9 | ||
|
|
d727710d60 | ||
|
|
0e31aeea00 | ||
|
|
2f437a0382 | ||
|
|
3ad4370fa5 | ||
|
|
a3bb9c2877 | ||
|
|
ee7e976084 | ||
|
|
099358d3e5 | ||
|
|
5297273937 | ||
|
|
80cfc9a25b | ||
|
|
2ae4da524e | ||
|
|
bbe7f28545 | ||
|
|
78ddd497ee | ||
|
|
8d044232af | ||
|
|
aa7e85caa7 | ||
|
|
46a8f24400 | ||
|
|
87bc292296 | ||
|
|
ac539ace70 | ||
|
|
a15b13978f | ||
|
|
0c975db0a6 | ||
|
|
cb4fea0240 | ||
|
|
8e7957d440 | ||
|
|
f7bed32c6f | ||
|
|
ef7f2d82c0 | ||
|
|
7aa97a332e | ||
|
|
7c30dde96b | ||
|
|
9cef2a0a8f | ||
|
|
f376683fc3 | ||
|
|
4b61d6e875 | ||
|
|
7d83e350fd | ||
|
|
500ba69548 | ||
|
|
9a422549b1 | ||
|
|
3b48fa455e | ||
|
|
ef013e0639 | ||
|
|
8f8437a88d | ||
|
|
1b091c9b07 | ||
|
|
4801b6f057 | ||
|
|
9078bc2de5 | ||
|
|
b69464dfe9 | ||
|
|
62fa48293a | ||
|
|
b206d0889b | ||
|
|
ee691d81bf | ||
|
|
56876a67cc | ||
|
|
4a0df713aa | ||
|
|
ef801cbfbe | ||
|
|
9378fc88d2 | ||
|
|
f46bfcc3d8 | ||
|
|
ccdb238843 | ||
|
|
f1f61b4e2b | ||
|
|
a44cb745d9 | ||
|
|
f5f5cb023c | ||
|
|
5813e0ce7a | ||
|
|
5a9c2b1e80 | ||
|
|
bda34fdb3b | ||
|
|
426b677eb8 | ||
|
|
67c7e9fd86 | ||
|
|
d8028a8632 | ||
|
|
374743d022 | ||
|
|
cd98ea5008 | ||
|
|
dbda0ed98a | ||
|
|
f5e0ead01c | ||
|
|
44818701bc | ||
|
|
e0f7387dff | ||
|
|
d440a01792 | ||
|
|
665c84ee42 | ||
|
|
e0de96eb4c | ||
|
|
c6ef276811 | ||
|
|
1701aaf78c | ||
|
|
122daa4bfb | ||
|
|
561a9e5275 | ||
|
|
de2453fce9 | ||
|
|
d59d40c118 | ||
|
|
3469df001f | ||
|
|
0d8cfa3031 | ||
|
|
0289586880 | ||
|
|
e46427c7fc | ||
|
|
3ea59d9a8e | ||
|
|
e85dfc6adf | ||
|
|
d0703b78fa | ||
|
|
432e6adf3e | ||
|
|
a057754035 | ||
|
|
0348ace253 | ||
|
|
c5e38203eb | ||
|
|
9ac31d0233 | ||
|
|
9d8d1cd69d | ||
|
|
07a0381f8b | ||
|
|
f841459004 | ||
|
|
78a26fc139 | ||
|
|
9f6628445e | ||
|
|
fa017b5977 | ||
|
|
58f4a970f2 | ||
|
|
021aa8faed | ||
|
|
83f6e037d6 | ||
|
|
baf153434d | ||
|
|
d481bd7993 | ||
|
|
e859c0a6ef | ||
|
|
59a39e66b1 | ||
|
|
fd5ac69a35 | ||
|
|
a940703ae1 | ||
|
|
350729cde8 | ||
|
|
2e14cd6d66 | ||
|
|
f703524f04 | ||
|
|
aa4435c775 | ||
|
|
31a2e368cc | ||
|
|
97e284e65e | ||
|
|
a6baab92f3 | ||
|
|
7c76e0c3ee | ||
|
|
591a4fcf8e | ||
|
|
71dac85600 | ||
|
|
ad90ddd327 | ||
|
|
03f457f3d0 | ||
|
|
a878256367 | ||
|
|
553f78ed55 | ||
|
|
1bc7d2237e | ||
|
|
132222013b | ||
|
|
2008fb552a | ||
|
|
236c034c62 | ||
|
|
f87baf08d3 | ||
|
|
22aa0c2f40 | ||
|
|
88469d4aaa | ||
|
|
1413c5022a | ||
|
|
aa8cdaee22 | ||
|
|
9f6ff54a76 | ||
|
|
e750c747c6 | ||
|
|
9edfe7d9d3 | ||
|
|
c9b7acd22c | ||
|
|
2ba2f0298c | ||
|
|
a24a2b475a | ||
|
|
4005452772 | ||
|
|
d4b7e221f0 | ||
|
|
77c98fd042 | ||
|
|
082872b2f3 | ||
|
|
6253e2e24c | ||
|
|
4216afe62f | ||
|
|
8fec78a5cd | ||
|
|
7ba0a14e97 | ||
|
|
3a442347a5 | ||
|
|
c4f4fd97d6 | ||
|
|
ac0ead1473 | ||
|
|
83cea9475d | ||
|
|
dc6bb7ab1b | ||
|
|
c71f6ba377 | ||
|
|
b1b1ab5350 | ||
|
|
7613b8dbfe | ||
|
|
e4cece6095 | ||
|
|
bcefe8716f | ||
|
|
746b5d8be0 | ||
|
|
f13ecbd9bb | ||
|
|
e839beb73b | ||
|
|
b797cdf91e | ||
|
|
84e4677a94 | ||
|
|
0377a11719 | ||
|
|
d0fa79044a | ||
|
|
f381f8d35a | ||
|
|
92e1e5b893 | ||
|
|
8e8b4dba22 | ||
|
|
767cd55817 | ||
|
|
eb0ef439d6 | ||
|
|
0bf78c0a8a | ||
|
|
12d7e19f32 | ||
|
|
d1c3dd0ee1 | ||
|
|
3dfa99efe1 | ||
|
|
d7bd221a47 | ||
|
|
1b7a3b4a74 | ||
|
|
c8424ed8fd | ||
|
|
150df1ae8e | ||
|
|
5ca9d77176 | ||
|
|
aa89fcc29d | ||
|
|
7ead0de26b | ||
|
|
f22c2690ec | ||
|
|
738bb0eabc | ||
|
|
002a519a17 | ||
|
|
f51128f772 | ||
|
|
d6a0aa7ccf | ||
|
|
ca94a2ddcb | ||
|
|
835ae1217b | ||
|
|
c165969399 | ||
|
|
88c69a06dc | ||
|
|
cd5e7055d2 | ||
|
|
3157593b6b | ||
|
|
c8399a297e | ||
|
|
529cfe2d9a | ||
|
|
50869c6cd2 | ||
|
|
44fcfab9aa |
116
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
116
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,54 +1,82 @@
|
||||
name: Bug Report
|
||||
description: "Create a report to help us improve."
|
||||
labels: [ bug ]
|
||||
name: Bug report
|
||||
description: "Report sing-box bug"
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Welcome
|
||||
label: Operating system
|
||||
description: Operating system type
|
||||
options:
|
||||
- label: Yes, I'm using the latest major release. Only such installations are supported.
|
||||
required: true
|
||||
- label: Yes, I've searched similar issues on GitHub and didn't find any.
|
||||
required: true
|
||||
- label: Yes, I've included all information below (version, config, etc).
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Description of the problem
|
||||
placeholder: Your problem description
|
||||
- iOS
|
||||
- macOS
|
||||
- Apple tvOS
|
||||
- Android
|
||||
- Windows
|
||||
- Linux
|
||||
- Others
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: version
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version of sing-box
|
||||
value: |-
|
||||
<details>
|
||||
|
||||
```console
|
||||
$ sing-box --version
|
||||
# Paste output here
|
||||
```
|
||||
|
||||
</details>
|
||||
label: System version
|
||||
description: Please provide the operating system version
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: config
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Server and client configuration file
|
||||
value: |-
|
||||
<details>
|
||||
|
||||
```console
|
||||
# paste json here
|
||||
```
|
||||
|
||||
</details>
|
||||
label: Installation type
|
||||
description: Please provide the sing-box installation type
|
||||
options:
|
||||
- Original sing-box Command Line
|
||||
- sing-box for iOS Graphical Client
|
||||
- sing-box for macOS Graphical Client
|
||||
- sing-box for Apple tvOS Graphical Client
|
||||
- sing-box for Android Graphical Client
|
||||
- Third-party graphical clients that advertise themselves as using sing-box (Windows)
|
||||
- Third-party graphical clients that advertise themselves as using sing-box (Android)
|
||||
- Others
|
||||
validations:
|
||||
required: true
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
description: Graphical client version
|
||||
label: If you are using a graphical client, please provide the version of the client.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Version
|
||||
description: If you are using the original command line program, please provide the output of the `sing-box version` command.
|
||||
render: shell
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please provide a detailed description of the error.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction
|
||||
description: Please provide the steps to reproduce the error, including the configuration files and procedures that can locally (not dependent on the remote server) reproduce the error using the original command line program of sing-box.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Logs
|
||||
description: |-
|
||||
In addition, if you encounter a crash with the graphical client, please also provide crash logs.
|
||||
For Apple platform clients, please check `Settings - View Service Log` for crash logs.
|
||||
For the Android client, please check the `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` file for crash logs.
|
||||
render: shell
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Integrity requirements
|
||||
description: |-
|
||||
Please check all of the following options to prove that you have read and understood the requirements, otherwise this issue will be closed.
|
||||
Sing-box is not a project aimed to please users who can't make any meaningful contributions and gain unethical influence. If you deceive here to deliberately waste the time of the developers, you will be permanently blocked.
|
||||
options:
|
||||
- label: I confirm that I have read the documentation, understand the meaning of all the configuration items I wrote, and did not pile up seemingly useful options or default values.
|
||||
required: true
|
||||
- label: I confirm that I have provided the server and client configuration files and process that can be reproduced locally, instead of a complicated client configuration file that has been stripped of sensitive data.
|
||||
required: true
|
||||
- label: I confirm that I have provided the simplest configuration that can be used to reproduce the error I reported, instead of depending on remote servers, TUN, graphical interface clients, or other closed-source software.
|
||||
required: true
|
||||
- label: I confirm that I have provided the complete configuration files and logs, rather than just providing parts I think are useful out of confidence in my own intelligence.
|
||||
required: true
|
||||
82
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
Normal file
82
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: 错误反馈
|
||||
description: "提交 sing-box 漏洞"
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: 操作系统
|
||||
description: 请提供操作系统类型
|
||||
options:
|
||||
- iOS
|
||||
- macOS
|
||||
- Apple tvOS
|
||||
- Android
|
||||
- Windows
|
||||
- Linux
|
||||
- 其他
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: 系统版本
|
||||
description: 请提供操作系统版本
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: 安装类型
|
||||
description: 请提供该 sing-box 安装类型
|
||||
options:
|
||||
- sing-box 原始命令行程序
|
||||
- sing-box for iOS 图形客户端程序
|
||||
- sing-box for macOS 图形客户端程序
|
||||
- sing-box for Apple tvOS 图形客户端程序
|
||||
- sing-box for Android 图形客户端程序
|
||||
- 宣传使用 sing-box 的第三方图形客户端程序 (Windows)
|
||||
- 宣传使用 sing-box 的第三方图形客户端程序 (Android)
|
||||
- 其他
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
description: 图形客户端版本
|
||||
label: 如果您使用图形客户端程序,请提供该程序版本。
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 版本
|
||||
description: 如果您使用原始命令行程序,请提供 `sing-box version` 命令的输出。
|
||||
render: shell
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 描述
|
||||
description: 请提供错误的详细描述。
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 重现方式
|
||||
description: 请提供重现错误的步骤,必须包括可以在本地(不依赖与远程服务器)使用 sing-box 原始命令行程序重现错误的配置文件与流程。
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 日志
|
||||
description: |-
|
||||
此外,如果您遭遇图形界面应用程序崩溃,请附加提供崩溃日志。
|
||||
对于 Apple 平台图形客户端程序,请检查 `Settings - View Service Log` 以导出崩溃日志。
|
||||
对于 Android 图形客户端程序,请检查 `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` 文件以导出崩溃日志。
|
||||
render: shell
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 完整性要求
|
||||
description: |-
|
||||
请勾选以下所有选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
|
||||
sing-box 不是讨好无法作出任何意义上的贡献的最终用户并获取非道德影响力的项目,如果您在此处欺骗以故意浪费开发者的时间,您将被永久封锁。
|
||||
options:
|
||||
- label: 我保证阅读了文档,了解所有我编写的配置文件项的含义,而不是大量堆砌看似有用的选项或默认值。
|
||||
required: true
|
||||
- label: 我保证提供了可以在本地重现该问题的服务器、客户端配置文件与流程,而不是一个脱敏的复杂客户端配置文件。
|
||||
required: true
|
||||
- label: 我保证提供了可用于重现我报告的错误的最简配置,而不是依赖远程服务器、TUN、图形界面客户端或者其他闭源软件。
|
||||
required: true
|
||||
- label: 我保证提供了完整的配置文件与日志,而不是出于对自身智力的自信而仅提供了部分认为有用的部分。
|
||||
required: true
|
||||
28
.github/renovate.json
vendored
Normal file
28
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"commitMessagePrefix": "[dependencies]",
|
||||
"extends": [
|
||||
"config:base",
|
||||
":disableRateLimiting"
|
||||
],
|
||||
"baseBranches": [
|
||||
"dev-next"
|
||||
],
|
||||
"golang": {
|
||||
"enabled": false
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": [
|
||||
"github-actions"
|
||||
],
|
||||
"groupName": "github-actions"
|
||||
},
|
||||
{
|
||||
"matchManagers": [
|
||||
"dockerfile"
|
||||
],
|
||||
"groupName": "Dockerfile"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
.github/update_dependencies.sh
vendored
10
.github/update_dependencies.sh
vendored
@@ -1,13 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PROJECTS=$(dirname "$0")/../..
|
||||
|
||||
go get -x github.com/sagernet/sing@$(git -C $PROJECTS/sing rev-parse HEAD)
|
||||
go get -x github.com/sagernet/sing-dns@$(git -C $PROJECTS/sing-dns rev-parse HEAD)
|
||||
go get -x github.com/sagernet/sing-tun@$(git -C $PROJECTS/sing-tun rev-parse HEAD)
|
||||
go get -x github.com/sagernet/sing-shadowsocks@$(git -C $PROJECTS/sing-shadowsocks rev-parse HEAD)
|
||||
go get -x github.com/sagernet/sing-vmess@$(git -C $PROJECTS/sing-vmess rev-parse HEAD)
|
||||
go get -x github.com/sagernet/$1@$(git -C $PROJECTS/$1 rev-parse HEAD)
|
||||
go mod tidy
|
||||
pushd test
|
||||
go mod tidy
|
||||
popd
|
||||
|
||||
98
.github/workflows/debug.yml
vendored
98
.github/workflows/debug.yml
vendored
@@ -3,42 +3,36 @@ name: Debug build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- stable-next
|
||||
- main-next
|
||||
- dev-next
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.github/**'
|
||||
- '!.github/workflows/debug.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- stable-next
|
||||
- main-next
|
||||
- dev-next
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Debug build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel previous
|
||||
uses: styfle/cancel-workflow-action@0.7.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get latest go version
|
||||
id: version
|
||||
run: |
|
||||
echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
|
||||
echo go_version=$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') >> $GITHUB_OUTPUT
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ steps.version.outputs.go_version }}
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: go-${{ hashFiles('**/go.sum') }}
|
||||
- name: Add cache to Go proxy
|
||||
run: |
|
||||
version=`git rev-parse HEAD`
|
||||
@@ -47,9 +41,50 @@ jobs:
|
||||
go mod init build
|
||||
go get -v github.com/sagernet/sing-box@$version
|
||||
popd
|
||||
continue-on-error: true
|
||||
- name: Run Test
|
||||
run: |
|
||||
go test -v ./...
|
||||
build_go118:
|
||||
name: Debug build (Go 1.18)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.18.10
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: go118-${{ hashFiles('**/go.sum') }}
|
||||
- name: Run Test
|
||||
run: make ci_build_go118
|
||||
build_go120:
|
||||
name: Debug build (Go 1.20)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.20.7
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: go118-${{ hashFiles('**/go.sum') }}
|
||||
- name: Run Test
|
||||
run: make ci_build
|
||||
cross:
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -123,6 +158,9 @@ jobs:
|
||||
- name: linux-mips64el
|
||||
goos: linux
|
||||
goarch: mips64le
|
||||
- name: linux-s390x
|
||||
goos: linux
|
||||
goarch: s390x
|
||||
# darwin
|
||||
- name: darwin-amd64
|
||||
goos: darwin
|
||||
@@ -160,39 +198,25 @@ jobs:
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GOMIPS: ${{ matrix.gomips }}
|
||||
CGO_ENABLED: 0
|
||||
TAGS: with_clash_api,with_quic
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get latest go version
|
||||
id: version
|
||||
run: |
|
||||
echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
|
||||
echo go_version=$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') >> $GITHUB_OUTPUT
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ steps.version.outputs.go_version }}
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: go-${{ hashFiles('**/go.sum') }}
|
||||
- name: Build
|
||||
id: build
|
||||
run: |
|
||||
VERSION="$(date +%Y%m%d).$(git rev-parse --short HEAD)"
|
||||
BUILDTIME="$(LANG=en_US.UTF-8 date -u)"
|
||||
|
||||
go build -v -trimpath -ldflags '\
|
||||
-X "github.com/sagernet/sing-box/constant.Version=$VERSION" \
|
||||
-X "github.com/sagernet/sing-box/constant.BuildTime=$BUILDTIME" \
|
||||
-s -w -buildid=' ./cmd/sing-box
|
||||
|
||||
echo "::set-output name=VERSION::$VERSION"
|
||||
run: make
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sing-box-${{ matrix.name }}-${{ steps.build.outputs.VERSION }}
|
||||
path: sing-box*
|
||||
name: sing-box-${{ matrix.name }}
|
||||
path: sing-box*
|
||||
|
||||
47
.github/workflows/docker.yml
vendored
Normal file
47
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Build Docker Images
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "The tag version you want to build"
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Setup QEMU for Docker Buildx
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Docker metadata
|
||||
id: metadata
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/sagernet/sing-box
|
||||
- name: Get tag to build
|
||||
id: tag
|
||||
run: |
|
||||
echo "latest=ghcr.io/sagernet/sing-box:latest" >> $GITHUB_OUTPUT
|
||||
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
|
||||
echo "versioned=ghcr.io/sagernet/sing-box:${{ github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "versioned=ghcr.io/sagernet/sing-box:${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Build and release Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
platforms: linux/386,linux/amd64,linux/arm64,linux/s390x
|
||||
target: dist
|
||||
build-args: |
|
||||
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
||||
tags: |
|
||||
${{ steps.tag.outputs.latest }}
|
||||
${{ steps.tag.outputs.versioned }}
|
||||
push: true
|
||||
33
.github/workflows/lint.yml
vendored
33
.github/workflows/lint.yml
vendored
@@ -3,46 +3,39 @@ name: Lint
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- stable-next
|
||||
- main-next
|
||||
- dev-next
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.github/**'
|
||||
- '!.github/workflows/debug.yml'
|
||||
- '!.github/workflows/lint.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- stable-next
|
||||
- main-next
|
||||
- dev-next
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel previous
|
||||
uses: styfle/cancel-workflow-action@0.7.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get latest go version
|
||||
id: version
|
||||
run: |
|
||||
echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
|
||||
echo go_version=$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') >> $GITHUB_OUTPUT
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ steps.version.outputs.go_version }}
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: go-${{ hashFiles('**/go.sum') }}
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go mod download -x
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: latest
|
||||
version: latest
|
||||
args: --timeout=30m
|
||||
install-mode: binary
|
||||
18
.github/workflows/mkdocs.yml
vendored
18
.github/workflows/mkdocs.yml
vendored
@@ -1,18 +0,0 @@
|
||||
name: Generate Documents
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- docs/**
|
||||
- .github/workflows/mkdocs.yml
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: pip install mkdocs-material
|
||||
- run: mkdocs gh-deploy -m "{sha}" --force --ignore-version --no-history
|
||||
15
.github/workflows/stale.yml
vendored
Normal file
15
.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: Mark stale issues and pull requests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
|
||||
days-before-stale: 60
|
||||
days-before-close: 5
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -3,4 +3,13 @@
|
||||
/*.json
|
||||
/*.db
|
||||
/site/
|
||||
/bin/
|
||||
/bin/
|
||||
/dist/
|
||||
/sing-box
|
||||
/sing-box.exe
|
||||
/build/
|
||||
/*.jar
|
||||
/*.aar
|
||||
/*.xcframework/
|
||||
.DS_Store
|
||||
/config.d/
|
||||
|
||||
@@ -7,14 +7,20 @@ linters:
|
||||
- staticcheck
|
||||
- paralleltest
|
||||
|
||||
issues:
|
||||
fix: true
|
||||
run:
|
||||
skip-dirs:
|
||||
- transport/simple-obfs
|
||||
- transport/clashssr
|
||||
- transport/cloudflaretls
|
||||
- transport/shadowtls/tls
|
||||
- transport/shadowtls/tls_go119
|
||||
|
||||
linters-settings:
|
||||
gci:
|
||||
custom-order: true
|
||||
sections:
|
||||
- standard
|
||||
- prefix(github.com/sagernet/)
|
||||
- default
|
||||
staticcheck:
|
||||
go: '1.18'
|
||||
go: '1.20'
|
||||
|
||||
188
.goreleaser.yaml
Normal file
188
.goreleaser.yaml
Normal file
@@ -0,0 +1,188 @@
|
||||
project_name: sing-box
|
||||
builds:
|
||||
- id: main
|
||||
main: ./cmd/sing-box
|
||||
flags:
|
||||
- -v
|
||||
- -trimpath
|
||||
asmflags:
|
||||
- all=-trimpath={{.Env.GOPATH}}
|
||||
gcflags:
|
||||
- all=-trimpath={{.Env.GOPATH}}
|
||||
ldflags:
|
||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
|
||||
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_amd64_v3
|
||||
- linux_arm64
|
||||
- linux_arm_7
|
||||
- linux_s390x
|
||||
- windows_amd64_v1
|
||||
- windows_amd64_v3
|
||||
- windows_386
|
||||
- windows_arm64
|
||||
- darwin_amd64_v1
|
||||
- darwin_amd64_v3
|
||||
- darwin_arm64
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
- id: legacy
|
||||
main: ./cmd/sing-box
|
||||
flags:
|
||||
- -v
|
||||
- -trimpath
|
||||
asmflags:
|
||||
- all=-trimpath={{.Env.GOPATH}}
|
||||
gcflags:
|
||||
- all=-trimpath={{.Env.GOPATH}}
|
||||
ldflags:
|
||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
|
||||
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
|
||||
- GOROOT=/nix/store/kg6i737jjqs923jcijnm003h68c1dghj-go-1.20.11/share/go
|
||||
gobinary: /nix/store/kg6i737jjqs923jcijnm003h68c1dghj-go-1.20.11/bin/go
|
||||
targets:
|
||||
- windows_amd64_v1
|
||||
- windows_386
|
||||
- darwin_amd64_v1
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
- id: android
|
||||
main: ./cmd/sing-box
|
||||
flags:
|
||||
- -v
|
||||
- -trimpath
|
||||
asmflags:
|
||||
- all=-trimpath={{.Env.GOPATH}}
|
||||
gcflags:
|
||||
- all=-trimpath={{.Env.GOPATH}}
|
||||
ldflags:
|
||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_ech
|
||||
- with_utls
|
||||
- with_reality_server
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
overrides:
|
||||
- goos: android
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
env:
|
||||
- CC=armv7a-linux-androideabi19-clang
|
||||
- CXX=armv7a-linux-androideabi19-clang++
|
||||
- goos: android
|
||||
goarch: arm64
|
||||
env:
|
||||
- CC=aarch64-linux-android21-clang
|
||||
- CXX=aarch64-linux-android21-clang++
|
||||
- goos: android
|
||||
goarch: 386
|
||||
env:
|
||||
- CC=i686-linux-android19-clang
|
||||
- CXX=i686-linux-android19-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
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
snapshot:
|
||||
name_template: "{{ .Version }}.{{ .ShortCommit }}"
|
||||
archives:
|
||||
- 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 }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
- id: archive-legacy
|
||||
builds:
|
||||
- legacy
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
wrap_in_directory: true
|
||||
files:
|
||||
- LICENSE
|
||||
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 }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
vendor: sagernet
|
||||
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
|
||||
priority: extra
|
||||
contents:
|
||||
- src: release/config/config.json
|
||||
dst: /etc/sing-box/config.json
|
||||
type: config
|
||||
- src: release/config/sing-box.service
|
||||
dst: /etc/systemd/system/sing-box.service
|
||||
- src: release/config/sing-box@.service
|
||||
dst: /etc/systemd/system/sing-box@.service
|
||||
- src: LICENSE
|
||||
dst: /usr/share/licenses/sing-box/LICENSE
|
||||
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
|
||||
name_template: '{{ if .IsSnapshot }}{{ nightly }}{{ else }}{{ .Version }}{{ end }}'
|
||||
draft: true
|
||||
mode: replace
|
||||
27
Dockerfile
Normal file
27
Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder
|
||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||
COPY . /go/src/github.com/sagernet/sing-box
|
||||
WORKDIR /go/src/github.com/sagernet/sing-box
|
||||
ARG TARGETOS TARGETARCH
|
||||
ARG GOPROXY=""
|
||||
ENV GOPROXY ${GOPROXY}
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOOS=$TARGETOS
|
||||
ENV GOARCH=$TARGETARCH
|
||||
RUN set -ex \
|
||||
&& apk add git build-base \
|
||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||
&& go build -v -trimpath -tags \
|
||||
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \
|
||||
-o /go/bin/sing-box \
|
||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
||||
./cmd/sing-box
|
||||
FROM --platform=$TARGETPLATFORM alpine AS dist
|
||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||
RUN set -ex \
|
||||
&& apk upgrade \
|
||||
&& apk add bash tzdata ca-certificates \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
||||
ENTRYPOINT ["sing-box"]
|
||||
5
LICENSE
5
LICENSE
@@ -11,4 +11,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
In addition, no derivative work may use the name or imply association
|
||||
with this application without prior consent.
|
||||
|
||||
200
Makefile
Normal file
200
Makefile
Normal file
@@ -0,0 +1,200 @@
|
||||
NAME = sing-box
|
||||
COMMIT = $(shell git rev-parse --short HEAD)
|
||||
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api
|
||||
TAGS_GO120 = with_quic,with_ech
|
||||
TAGS ?= $(TAGS_GO118),$(TAGS_GO120)
|
||||
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
|
||||
|
||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
|
||||
|
||||
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
|
||||
MAIN_PARAMS = $(PARAMS) -tags $(TAGS)
|
||||
MAIN = ./cmd/sing-box
|
||||
PREFIX ?= $(shell go env GOPATH)
|
||||
|
||||
.PHONY: test release docs
|
||||
|
||||
build:
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
ci_build_go118:
|
||||
go build $(PARAMS) $(MAIN)
|
||||
go build $(PARAMS) -tags "$(TAGS_GO118)" $(MAIN)
|
||||
|
||||
ci_build:
|
||||
go build $(PARAMS) $(MAIN)
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
install:
|
||||
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
fmt:
|
||||
@gofumpt -l -w .
|
||||
@gofmt -s -w .
|
||||
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
|
||||
|
||||
fmt_install:
|
||||
go install -v mvdan.cc/gofumpt@latest
|
||||
go install -v github.com/daixiang0/gci@latest
|
||||
|
||||
lint:
|
||||
GOOS=linux golangci-lint run ./...
|
||||
GOOS=android golangci-lint run ./...
|
||||
GOOS=windows golangci-lint run ./...
|
||||
GOOS=darwin golangci-lint run ./...
|
||||
GOOS=freebsd golangci-lint run ./...
|
||||
|
||||
lint_install:
|
||||
go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
|
||||
proto:
|
||||
@go run ./cmd/internal/protogen
|
||||
@gofumpt -l -w .
|
||||
@gofumpt -l -w .
|
||||
|
||||
proto_install:
|
||||
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
|
||||
release:
|
||||
go run ./cmd/internal/build goreleaser release --clean --skip-publish || exit 1
|
||||
mkdir dist/release
|
||||
mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/*.pkg.tar.zst dist/release
|
||||
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release
|
||||
rm -r dist/release
|
||||
|
||||
release_install:
|
||||
go install -v github.com/goreleaser/goreleaser@latest
|
||||
go install -v github.com/tcnksm/ghr@latest
|
||||
|
||||
update_android_version:
|
||||
go run ./cmd/internal/update_android_version
|
||||
|
||||
build_android:
|
||||
cd ../sing-box-for-android && ./gradlew :app:assemblePlayRelease && ./gradlew --stop
|
||||
|
||||
upload_android:
|
||||
mkdir -p dist/release_android
|
||||
cp ../sing-box-for-android/app/build/outputs/apk/play/release/*.apk dist/release_android
|
||||
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release_android
|
||||
rm -rf dist/release_android
|
||||
|
||||
release_android: lib_android update_android_version build_android upload_android
|
||||
|
||||
publish_android:
|
||||
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle
|
||||
|
||||
publish_android_appcenter:
|
||||
cd ../sing-box-for-android && ./gradlew :app:appCenterAssembleAndUploadPlayRelease
|
||||
|
||||
build_ios:
|
||||
cd ../sing-box-for-apple && \
|
||||
rm -rf build/SFI.xcarchive && \
|
||||
xcodebuild archive -scheme SFI -configuration Release -archivePath build/SFI.xcarchive
|
||||
|
||||
upload_ios_app_store:
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
||||
|
||||
release_ios: build_ios upload_ios_app_store
|
||||
|
||||
build_macos:
|
||||
cd ../sing-box-for-apple && \
|
||||
rm -rf build/SFM.xcarchive && \
|
||||
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive
|
||||
|
||||
upload_macos_app_store:
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportArchive -archivePath build/SFM.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
||||
|
||||
release_macos: build_macos upload_macos_app_store
|
||||
|
||||
build_macos_independent:
|
||||
cd ../sing-box-for-apple && \
|
||||
rm -rf build/SFT.System.xcarchive && \
|
||||
xcodebuild archive -scheme SFM.System -configuration Release -archivePath build/SFM.System.xcarchive
|
||||
|
||||
notarize_macos_independent:
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportArchive -archivePath "build/SFM.System.xcarchive" -exportOptionsPlist SFM.System/Upload.plist -allowProvisioningUpdates
|
||||
|
||||
wait_notarize_macos_independent:
|
||||
sleep 60
|
||||
|
||||
export_macos_independent:
|
||||
rm -rf dist/SFM
|
||||
mkdir -p dist/SFM
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportNotarizedApp -archivePath build/SFM.System.xcarchive -exportPath "../sing-box/dist/SFM"
|
||||
|
||||
upload_macos_independent:
|
||||
cd dist/SFM && \
|
||||
rm -f *.zip && \
|
||||
zip -ry "SFM-${VERSION}-universal.zip" SFM.app && \
|
||||
ghr --replace --draft --prerelease "v${VERSION}" *.zip
|
||||
|
||||
release_macos_independent: build_macos_independent notarize_macos_independent wait_notarize_macos_independent export_macos_independent upload_macos_independent
|
||||
|
||||
build_tvos:
|
||||
cd ../sing-box-for-apple && \
|
||||
rm -rf build/SFT.xcarchive && \
|
||||
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive
|
||||
|
||||
upload_tvos_app_store:
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
||||
|
||||
release_tvos: build_tvos upload_tvos_app_store
|
||||
|
||||
update_apple_version:
|
||||
go run ./cmd/internal/update_apple_version
|
||||
|
||||
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_independent
|
||||
|
||||
release_apple_beta: update_apple_version release_ios release_macos release_tvos
|
||||
|
||||
test:
|
||||
@go test -v ./... && \
|
||||
cd test && \
|
||||
go mod tidy && \
|
||||
go test -v -tags "$(TAGS_TEST)" .
|
||||
|
||||
test_stdio:
|
||||
@go test -v ./... && \
|
||||
cd test && \
|
||||
go mod tidy && \
|
||||
go test -v -tags "$(TAGS_TEST),force_stdio" .
|
||||
|
||||
lib_android:
|
||||
go run ./cmd/internal/build_libbox -target android
|
||||
|
||||
lib_ios:
|
||||
go run ./cmd/internal/build_libbox -target ios
|
||||
|
||||
lib:
|
||||
go run ./cmd/internal/build_libbox -target android
|
||||
go run ./cmd/internal/build_libbox -target ios
|
||||
|
||||
lib_install:
|
||||
go get -v -d
|
||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20230915142329-c6740b6d2950
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230915142329-c6740b6d2950
|
||||
|
||||
docs:
|
||||
mkdocs serve
|
||||
|
||||
publish_docs:
|
||||
mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
|
||||
|
||||
docs_install:
|
||||
pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
|
||||
clean:
|
||||
rm -rf bin dist sing-box
|
||||
rm -f $(shell go env GOPATH)/sing-box
|
||||
|
||||
update:
|
||||
git fetch
|
||||
git reset FETCH_HEAD --hard
|
||||
git clean -fdx
|
||||
@@ -2,10 +2,16 @@
|
||||
|
||||
The universal proxy platform.
|
||||
|
||||
[](https://repology.org/project/sing-box/versions)
|
||||
|
||||
## Documentation
|
||||
|
||||
https://sing-box.sagernet.org
|
||||
|
||||
## Support
|
||||
|
||||
https://community.sagernet.org/c/sing-box/
|
||||
|
||||
## License
|
||||
|
||||
```
|
||||
@@ -23,4 +29,7 @@ GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
In addition, no derivative work may use the name or imply association
|
||||
with this application without prior consent.
|
||||
```
|
||||
104
adapter/conn_router.go
Normal file
104
adapter/conn_router.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type ConnectionRouter interface {
|
||||
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
}
|
||||
|
||||
func NewRouteHandler(
|
||||
metadata InboundContext,
|
||||
router ConnectionRouter,
|
||||
logger logger.ContextLogger,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeHandlerWrapper{
|
||||
metadata: metadata,
|
||||
router: router,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func NewRouteContextHandler(
|
||||
router ConnectionRouter,
|
||||
logger logger.ContextLogger,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeContextHandlerWrapper{
|
||||
router: router,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
|
||||
|
||||
type routeHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
router ConnectionRouter
|
||||
logger logger.ContextLogger
|
||||
}
|
||||
|
||||
func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.router.RouteConnection(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.router.RoutePacketConnection(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.logger.ErrorContext(ctx, err)
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
|
||||
|
||||
type routeContextHandlerWrapper struct {
|
||||
router ConnectionRouter
|
||||
logger logger.ContextLogger
|
||||
}
|
||||
|
||||
func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.router.RouteConnection(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.router.RoutePacketConnection(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.logger.ErrorContext(ctx, err)
|
||||
}
|
||||
@@ -4,31 +4,61 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type ClashServer interface {
|
||||
Service
|
||||
TrafficController
|
||||
PreStarter
|
||||
Mode() string
|
||||
ModeList() []string
|
||||
StoreSelected() bool
|
||||
StoreFakeIP() bool
|
||||
CacheFile() ClashCacheFile
|
||||
HistoryStorage() *urltest.HistoryStorage
|
||||
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
|
||||
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker)
|
||||
}
|
||||
|
||||
type ClashCacheFile interface {
|
||||
LoadMode() string
|
||||
StoreMode(mode string) error
|
||||
LoadSelected(group string) string
|
||||
StoreSelected(group string, selected string) error
|
||||
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
||||
StoreGroupExpand(group string, expand bool) error
|
||||
FakeIPStorage
|
||||
}
|
||||
|
||||
type Tracker interface {
|
||||
Leave()
|
||||
}
|
||||
|
||||
type TrafficController interface {
|
||||
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
|
||||
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker)
|
||||
}
|
||||
|
||||
type OutboundGroup interface {
|
||||
Outbound
|
||||
Now() string
|
||||
All() []string
|
||||
}
|
||||
|
||||
type URLTestGroup interface {
|
||||
OutboundGroup
|
||||
URLTest(ctx context.Context) (map[string]uint16, error)
|
||||
}
|
||||
|
||||
func OutboundTag(detour Outbound) string {
|
||||
if group, isGroup := detour.(OutboundGroup); isGroup {
|
||||
return group.Now()
|
||||
}
|
||||
return detour.Tag()
|
||||
}
|
||||
|
||||
type V2RayServer interface {
|
||||
Service
|
||||
StatsService() V2RayStatsService
|
||||
}
|
||||
|
||||
type V2RayStatsService interface {
|
||||
RoutedConnection(inbound string, outbound string, user string, conn net.Conn) net.Conn
|
||||
RoutedPacketConnection(inbound string, outbound string, user string, conn N.PacketConn) N.PacketConn
|
||||
}
|
||||
|
||||
32
adapter/fakeip.go
Normal file
32
adapter/fakeip.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-dns"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
type FakeIPStore interface {
|
||||
Service
|
||||
Contains(address netip.Addr) bool
|
||||
Create(domain string, isIPv6 bool) (netip.Addr, error)
|
||||
Lookup(address netip.Addr) (string, bool)
|
||||
Reset() error
|
||||
}
|
||||
|
||||
type FakeIPStorage interface {
|
||||
FakeIPMetadata() *FakeIPMetadata
|
||||
FakeIPSaveMetadata(metadata *FakeIPMetadata) error
|
||||
FakeIPSaveMetadataAsync(metadata *FakeIPMetadata)
|
||||
FakeIPStore(address netip.Addr, domain string) error
|
||||
FakeIPStoreAsync(address netip.Addr, domain string, logger logger.Logger)
|
||||
FakeIPLoad(address netip.Addr) (string, bool)
|
||||
FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bool)
|
||||
FakeIPReset() error
|
||||
}
|
||||
|
||||
type FakeIPTransport interface {
|
||||
dns.Transport
|
||||
Store() FakeIPStore
|
||||
}
|
||||
50
adapter/fakeip_metadata.go
Normal file
50
adapter/fakeip_metadata.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
type FakeIPMetadata struct {
|
||||
Inet4Range netip.Prefix
|
||||
Inet6Range netip.Prefix
|
||||
Inet4Current netip.Addr
|
||||
Inet6Current netip.Addr
|
||||
}
|
||||
|
||||
func (m *FakeIPMetadata) MarshalBinary() (data []byte, err error) {
|
||||
var buffer bytes.Buffer
|
||||
for _, marshaler := range []encoding.BinaryMarshaler{m.Inet4Range, m.Inet6Range, m.Inet4Current, m.Inet6Current} {
|
||||
data, err = marshaler.MarshalBinary()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
common.Must(binary.Write(&buffer, binary.BigEndian, uint16(len(data))))
|
||||
buffer.Write(data)
|
||||
}
|
||||
data = buffer.Bytes()
|
||||
return
|
||||
}
|
||||
|
||||
func (m *FakeIPMetadata) UnmarshalBinary(data []byte) error {
|
||||
reader := bytes.NewReader(data)
|
||||
for _, unmarshaler := range []encoding.BinaryUnmarshaler{&m.Inet4Range, &m.Inet6Range, &m.Inet4Current, &m.Inet6Current} {
|
||||
var length uint16
|
||||
common.Must(binary.Read(reader, binary.BigEndian, &length))
|
||||
element := make([]byte, length)
|
||||
_, err := io.ReadFull(reader, element)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = unmarshaler.UnmarshalBinary(element)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,11 +2,13 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-box/common/process"
|
||||
"github.com/sagernet/sing-dns"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type Inbound interface {
|
||||
@@ -15,9 +17,17 @@ type Inbound interface {
|
||||
Tag() string
|
||||
}
|
||||
|
||||
type InjectableInbound interface {
|
||||
Inbound
|
||||
Network() []string
|
||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
}
|
||||
|
||||
type InboundContext struct {
|
||||
Inbound string
|
||||
InboundType string
|
||||
IPVersion uint8
|
||||
Network string
|
||||
Source M.Socksaddr
|
||||
Destination M.Socksaddr
|
||||
@@ -28,14 +38,19 @@ type InboundContext struct {
|
||||
|
||||
// cache
|
||||
|
||||
DomainStrategy dns.DomainStrategy
|
||||
SniffEnabled bool
|
||||
SniffOverrideDestination bool
|
||||
DestinationAddresses []netip.Addr
|
||||
InboundDetour string
|
||||
LastInbound string
|
||||
OriginDestination M.Socksaddr
|
||||
InboundOptions option.InboundOptions
|
||||
DestinationAddresses []netip.Addr
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *process.Info
|
||||
FakeIP bool
|
||||
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *process.Info
|
||||
// dns cache
|
||||
|
||||
QueryType uint16
|
||||
}
|
||||
|
||||
type inboundContextKey struct{}
|
||||
@@ -60,3 +75,11 @@ func AppendContext(ctx context.Context) (context.Context, *InboundContext) {
|
||||
metadata = new(InboundContext)
|
||||
return WithContext(ctx, metadata), metadata
|
||||
}
|
||||
|
||||
func ExtendContext(ctx context.Context) (context.Context, *InboundContext) {
|
||||
var newMetadata InboundContext
|
||||
if metadata := ContextFrom(ctx); metadata != nil {
|
||||
newMetadata = *metadata
|
||||
}
|
||||
return WithContext(ctx, &newMetadata), &newMetadata
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ type Outbound interface {
|
||||
Type() string
|
||||
Tag() string
|
||||
Network() []string
|
||||
Dependencies() []string
|
||||
N.Dialer
|
||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
|
||||
9
adapter/prestart.go
Normal file
9
adapter/prestart.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package adapter
|
||||
|
||||
type PreStarter interface {
|
||||
PreStart() error
|
||||
}
|
||||
|
||||
type PostStarter interface {
|
||||
PostStart() error
|
||||
}
|
||||
@@ -2,43 +2,64 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-box/common/geoip"
|
||||
"github.com/sagernet/sing-dns"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
mdns "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type Router interface {
|
||||
Service
|
||||
PostStarter
|
||||
|
||||
Outbounds() []Outbound
|
||||
Outbound(tag string) (Outbound, bool)
|
||||
DefaultOutbound(network string) Outbound
|
||||
|
||||
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
FakeIPStore() FakeIPStore
|
||||
|
||||
ConnectionRouter
|
||||
|
||||
GeoIPReader() *geoip.Reader
|
||||
LoadGeosite(code string) (Rule, error)
|
||||
|
||||
Exchange(ctx context.Context, message *dnsmessage.Message) (*dnsmessage.Message, error)
|
||||
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
|
||||
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
|
||||
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
|
||||
ClearDNSCache()
|
||||
|
||||
InterfaceBindManager() control.BindManager
|
||||
InterfaceFinder() control.InterfaceFinder
|
||||
UpdateInterfaces() error
|
||||
DefaultInterface() string
|
||||
AutoDetectInterface() bool
|
||||
AutoDetectInterfaceFunc() control.Func
|
||||
DefaultMark() int
|
||||
NetworkMonitor() tun.NetworkUpdateMonitor
|
||||
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
||||
PackageManager() tun.PackageManager
|
||||
WIFIState() WIFIState
|
||||
Rules() []Rule
|
||||
SetTrafficController(controller TrafficController)
|
||||
|
||||
ClashServer() ClashServer
|
||||
SetClashServer(server ClashServer)
|
||||
|
||||
V2RayServer() V2RayServer
|
||||
SetV2RayServer(server V2RayServer)
|
||||
|
||||
ResetNetwork() error
|
||||
}
|
||||
|
||||
func ContextWithRouter(ctx context.Context, router Router) context.Context {
|
||||
return service.ContextWith(ctx, router)
|
||||
}
|
||||
|
||||
func RouterFromContext(ctx context.Context) Router {
|
||||
return service.FromContext[Router](ctx)
|
||||
}
|
||||
|
||||
type Rule interface {
|
||||
@@ -53,4 +74,14 @@ type Rule interface {
|
||||
type DNSRule interface {
|
||||
Rule
|
||||
DisableCache() bool
|
||||
RewriteTTL() *uint32
|
||||
}
|
||||
|
||||
type InterfaceUpdateListener interface {
|
||||
InterfaceUpdated()
|
||||
}
|
||||
|
||||
type WIFIState struct {
|
||||
SSID string
|
||||
BSSID string
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
package adapter
|
||||
|
||||
import "io"
|
||||
|
||||
type Starter interface {
|
||||
Start() error
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
Starter
|
||||
io.Closer
|
||||
Start() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
8
adapter/time.go
Normal file
8
adapter/time.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package adapter
|
||||
|
||||
import "time"
|
||||
|
||||
type TimeService interface {
|
||||
Service
|
||||
TimeFunc() func() time.Time
|
||||
}
|
||||
@@ -38,13 +38,25 @@ type myUpstreamHandlerWrapper struct {
|
||||
}
|
||||
|
||||
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
w.metadata.Destination = metadata.Destination
|
||||
return w.connectionHandler(ctx, conn, w.metadata)
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.connectionHandler(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
w.metadata.Destination = metadata.Destination
|
||||
return w.packetHandler(ctx, conn, w.metadata)
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.packetHandler(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
@@ -78,13 +90,23 @@ func NewUpstreamContextHandler(
|
||||
|
||||
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
myMetadata.Destination = metadata.Destination
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.connectionHandler(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
myMetadata.Destination = metadata.Destination
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.packetHandler(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
|
||||
25
adapter/v2ray.go
Normal file
25
adapter/v2ray.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type V2RayServerTransport interface {
|
||||
Network() []string
|
||||
Serve(listener net.Listener) error
|
||||
ServePacket(listener net.PacketConn) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type V2RayServerTransportHandler interface {
|
||||
N.TCPConnectionHandler
|
||||
E.Handler
|
||||
}
|
||||
|
||||
type V2RayClientTransport interface {
|
||||
DialContext(ctx context.Context) (net.Conn, error)
|
||||
}
|
||||
320
box.go
320
box.go
@@ -2,12 +2,15 @@ package box
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||
"github.com/sagernet/sing-box/inbound"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -16,82 +19,72 @@ import (
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/pause"
|
||||
)
|
||||
|
||||
var _ adapter.Service = (*Box)(nil)
|
||||
|
||||
type Box struct {
|
||||
createdAt time.Time
|
||||
router adapter.Router
|
||||
inbounds []adapter.Inbound
|
||||
outbounds []adapter.Outbound
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
logFile *os.File
|
||||
clashServer adapter.ClashServer
|
||||
done chan struct{}
|
||||
createdAt time.Time
|
||||
router adapter.Router
|
||||
inbounds []adapter.Inbound
|
||||
outbounds []adapter.Outbound
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
preServices map[string]adapter.Service
|
||||
postServices map[string]adapter.Service
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func New(ctx context.Context, options option.Options) (*Box, error) {
|
||||
createdAt := time.Now()
|
||||
logOptions := common.PtrValueOrDefault(options.Log)
|
||||
type Options struct {
|
||||
option.Options
|
||||
Context context.Context
|
||||
PlatformInterface platform.Interface
|
||||
PlatformLogWriter log.PlatformWriter
|
||||
}
|
||||
|
||||
func New(options Options) (*Box, error) {
|
||||
ctx := options.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
ctx = service.ContextWithDefaultRegistry(ctx)
|
||||
ctx = pause.ContextWithDefaultManager(ctx)
|
||||
createdAt := time.Now()
|
||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
||||
var needClashAPI bool
|
||||
if options.Experimental != nil && options.Experimental.ClashAPI != nil && options.Experimental.ClashAPI.ExternalController != "" {
|
||||
var needV2RayAPI bool
|
||||
if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil {
|
||||
needClashAPI = true
|
||||
}
|
||||
|
||||
var logFactory log.Factory
|
||||
var observableLogFactory log.ObservableFactory
|
||||
var logFile *os.File
|
||||
if logOptions.Disabled {
|
||||
observableLogFactory = log.NewNOPFactory()
|
||||
logFactory = observableLogFactory
|
||||
} else {
|
||||
var logWriter io.Writer
|
||||
switch logOptions.Output {
|
||||
case "", "stderr":
|
||||
logWriter = os.Stderr
|
||||
case "stdout":
|
||||
logWriter = os.Stdout
|
||||
default:
|
||||
var err error
|
||||
logFile, err = os.OpenFile(logOptions.Output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
logFormatter := log.Formatter{
|
||||
BaseTime: createdAt,
|
||||
DisableColors: logOptions.DisableColor || logFile != nil,
|
||||
DisableTimestamp: !logOptions.Timestamp && logFile != nil,
|
||||
FullTimestamp: logOptions.Timestamp,
|
||||
TimestampFormat: "-0700 2006-01-02 15:04:05",
|
||||
}
|
||||
if needClashAPI {
|
||||
observableLogFactory = log.NewObservableFactory(logFormatter, logWriter)
|
||||
logFactory = observableLogFactory
|
||||
} else {
|
||||
logFactory = log.NewFactory(logFormatter, logWriter)
|
||||
}
|
||||
if logOptions.Level != "" {
|
||||
logLevel, err := log.ParseLevel(logOptions.Level)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse log level")
|
||||
}
|
||||
logFactory.SetLevel(logLevel)
|
||||
} else {
|
||||
logFactory.SetLevel(log.LevelTrace)
|
||||
}
|
||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||
needV2RayAPI = true
|
||||
}
|
||||
var defaultLogWriter io.Writer
|
||||
if options.PlatformInterface != nil {
|
||||
defaultLogWriter = io.Discard
|
||||
}
|
||||
logFactory, err := log.New(log.Options{
|
||||
Context: ctx,
|
||||
Options: common.PtrValueOrDefault(options.Log),
|
||||
Observable: needClashAPI,
|
||||
DefaultWriter: defaultLogWriter,
|
||||
BaseTime: createdAt,
|
||||
PlatformWriter: options.PlatformLogWriter,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create log factory")
|
||||
}
|
||||
|
||||
router, err := route.NewRouter(
|
||||
ctx,
|
||||
logFactory.NewLogger("router"),
|
||||
logFactory.NewLogger("dns"),
|
||||
logFactory,
|
||||
common.PtrValueOrDefault(options.Route),
|
||||
common.PtrValueOrDefault(options.DNS),
|
||||
common.PtrValueOrDefault(options.NTP),
|
||||
options.Inbounds,
|
||||
options.PlatformInterface,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse route options")
|
||||
@@ -111,6 +104,7 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
|
||||
router,
|
||||
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
|
||||
inboundOptions,
|
||||
options.PlatformInterface,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse inbound[", i, "]")
|
||||
@@ -129,14 +123,15 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
|
||||
ctx,
|
||||
router,
|
||||
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
outboundOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse outbound[", i, "]")
|
||||
}
|
||||
outbounds = append(outbounds, out)
|
||||
}
|
||||
err = router.Initialize(outbounds, func() adapter.Outbound {
|
||||
out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), option.Outbound{Type: "direct", Tag: "default"})
|
||||
err = router.Initialize(inbounds, outbounds, func() adapter.Outbound {
|
||||
out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.Outbound{Type: "direct", Tag: "default"})
|
||||
common.Must(oErr)
|
||||
outbounds = append(outbounds, out)
|
||||
return out
|
||||
@@ -144,52 +139,149 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var clashServer adapter.ClashServer
|
||||
if options.PlatformInterface != nil {
|
||||
err = options.PlatformInterface.Initialize(ctx, router)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize platform interface")
|
||||
}
|
||||
}
|
||||
preServices := make(map[string]adapter.Service)
|
||||
postServices := make(map[string]adapter.Service)
|
||||
if needClashAPI {
|
||||
clashServer, err = experimental.NewClashServer(router, observableLogFactory, common.PtrValueOrDefault(options.Experimental.ClashAPI))
|
||||
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
||||
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
||||
clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), clashAPIOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create clash api server")
|
||||
}
|
||||
router.SetTrafficController(clashServer)
|
||||
router.SetClashServer(clashServer)
|
||||
preServices["clash api"] = clashServer
|
||||
}
|
||||
if needV2RayAPI {
|
||||
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create v2ray api server")
|
||||
}
|
||||
router.SetV2RayServer(v2rayServer)
|
||||
preServices["v2ray api"] = v2rayServer
|
||||
}
|
||||
return &Box{
|
||||
router: router,
|
||||
inbounds: inbounds,
|
||||
outbounds: outbounds,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.NewLogger(""),
|
||||
logFile: logFile,
|
||||
clashServer: clashServer,
|
||||
done: make(chan struct{}),
|
||||
router: router,
|
||||
inbounds: inbounds,
|
||||
outbounds: outbounds,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
preServices: preServices,
|
||||
postServices: postServices,
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Box) Start() error {
|
||||
err := s.router.Start()
|
||||
func (s *Box) PreStart() error {
|
||||
err := s.preStart()
|
||||
if err != nil {
|
||||
// TODO: remove catch error
|
||||
defer func() {
|
||||
v := recover()
|
||||
if v != nil {
|
||||
log.Error(E.Cause(err, "origin error"))
|
||||
debug.PrintStack()
|
||||
panic("panic on early close: " + fmt.Sprint(v))
|
||||
}
|
||||
}()
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
for i, in := range s.inbounds {
|
||||
err = in.Start()
|
||||
if err != nil {
|
||||
for g := 0; g < i; g++ {
|
||||
s.inbounds[g].Close()
|
||||
s.logger.Info("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) Start() error {
|
||||
err := s.start()
|
||||
if err != nil {
|
||||
// TODO: remove catch error
|
||||
defer func() {
|
||||
v := recover()
|
||||
if v != nil {
|
||||
log.Error(E.Cause(err, "origin error"))
|
||||
debug.PrintStack()
|
||||
panic("panic on early close: " + fmt.Sprint(v))
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
if s.clashServer != nil {
|
||||
err = s.clashServer.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start clash api server")
|
||||
}
|
||||
}()
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) preStart() error {
|
||||
for serviceName, service := range s.preServices {
|
||||
if preService, isPreService := service.(adapter.PreStarter); isPreService {
|
||||
s.logger.Trace("pre-start ", serviceName)
|
||||
err := preService.PreStart()
|
||||
if err != nil {
|
||||
return E.Cause(err, "pre-starting ", serviceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
err := s.startOutbounds()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.router.Start()
|
||||
}
|
||||
|
||||
func (s *Box) start() error {
|
||||
err := s.preStart()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for serviceName, service := range s.preServices {
|
||||
s.logger.Trace("starting ", serviceName)
|
||||
err = service.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start ", serviceName)
|
||||
}
|
||||
}
|
||||
for i, in := range s.inbounds {
|
||||
var tag string
|
||||
if in.Tag() == "" {
|
||||
tag = F.ToString(i)
|
||||
} else {
|
||||
tag = in.Tag()
|
||||
}
|
||||
s.logger.Trace("initializing inbound/", in.Type(), "[", tag, "]")
|
||||
err = in.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
|
||||
}
|
||||
}
|
||||
return s.postStart()
|
||||
}
|
||||
|
||||
func (s *Box) postStart() error {
|
||||
for serviceName, service := range s.postServices {
|
||||
s.logger.Trace("starting ", service)
|
||||
err := service.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start ", serviceName)
|
||||
}
|
||||
}
|
||||
for _, outbound := range s.outbounds {
|
||||
if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound {
|
||||
s.logger.Trace("post-starting outbound/", outbound.Tag())
|
||||
err := lateOutbound.PostStart()
|
||||
if err != nil {
|
||||
return E.Cause(err, "post-start outbound/", outbound.Tag())
|
||||
}
|
||||
}
|
||||
}
|
||||
s.logger.Trace("post-starting router")
|
||||
return s.router.PostStart()
|
||||
}
|
||||
|
||||
func (s *Box) Close() error {
|
||||
select {
|
||||
case <-s.done:
|
||||
@@ -197,16 +289,46 @@ func (s *Box) Close() error {
|
||||
default:
|
||||
close(s.done)
|
||||
}
|
||||
for _, in := range s.inbounds {
|
||||
in.Close()
|
||||
var errors error
|
||||
for serviceName, service := range s.postServices {
|
||||
s.logger.Trace("closing ", serviceName)
|
||||
errors = E.Append(errors, service.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", serviceName)
|
||||
})
|
||||
}
|
||||
for _, out := range s.outbounds {
|
||||
common.Close(out)
|
||||
for i, in := range s.inbounds {
|
||||
s.logger.Trace("closing inbound/", in.Type(), "[", i, "]")
|
||||
errors = E.Append(errors, in.Close(), func(err error) error {
|
||||
return E.Cause(err, "close inbound/", in.Type(), "[", i, "]")
|
||||
})
|
||||
}
|
||||
return common.Close(
|
||||
s.router,
|
||||
s.logFactory,
|
||||
s.clashServer,
|
||||
common.PtrOrNil(s.logFile),
|
||||
)
|
||||
for i, out := range s.outbounds {
|
||||
s.logger.Trace("closing outbound/", out.Type(), "[", i, "]")
|
||||
errors = E.Append(errors, common.Close(out), func(err error) error {
|
||||
return E.Cause(err, "close outbound/", out.Type(), "[", i, "]")
|
||||
})
|
||||
}
|
||||
s.logger.Trace("closing router")
|
||||
if err := common.Close(s.router); err != nil {
|
||||
errors = E.Append(errors, err, func(err error) error {
|
||||
return E.Cause(err, "close router")
|
||||
})
|
||||
}
|
||||
for serviceName, service := range s.preServices {
|
||||
s.logger.Trace("closing ", serviceName)
|
||||
errors = E.Append(errors, service.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", serviceName)
|
||||
})
|
||||
}
|
||||
s.logger.Trace("closing log factory")
|
||||
if err := common.Close(s.logFactory); err != nil {
|
||||
errors = E.Append(errors, err, func(err error) error {
|
||||
return E.Cause(err, "close log factory")
|
||||
})
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
func (s *Box) Router() adapter.Router {
|
||||
return s.router
|
||||
}
|
||||
|
||||
79
box_outbound.go
Normal file
79
box_outbound.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package box
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
func (s *Box) startOutbounds() error {
|
||||
outboundTags := make(map[adapter.Outbound]string)
|
||||
outbounds := make(map[string]adapter.Outbound)
|
||||
for i, outboundToStart := range s.outbounds {
|
||||
var outboundTag string
|
||||
if outboundToStart.Tag() == "" {
|
||||
outboundTag = F.ToString(i)
|
||||
} else {
|
||||
outboundTag = outboundToStart.Tag()
|
||||
}
|
||||
if _, exists := outbounds[outboundTag]; exists {
|
||||
return E.New("outbound tag ", outboundTag, " duplicated")
|
||||
}
|
||||
outboundTags[outboundToStart] = outboundTag
|
||||
outbounds[outboundTag] = outboundToStart
|
||||
}
|
||||
started := make(map[string]bool)
|
||||
for {
|
||||
canContinue := false
|
||||
startOne:
|
||||
for _, outboundToStart := range s.outbounds {
|
||||
outboundTag := outboundTags[outboundToStart]
|
||||
if started[outboundTag] {
|
||||
continue
|
||||
}
|
||||
dependencies := outboundToStart.Dependencies()
|
||||
for _, dependency := range dependencies {
|
||||
if !started[dependency] {
|
||||
continue startOne
|
||||
}
|
||||
}
|
||||
started[outboundTag] = true
|
||||
canContinue = true
|
||||
if starter, isStarter := outboundToStart.(common.Starter); isStarter {
|
||||
s.logger.Trace("initializing outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||
err := starter.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(started) == len(s.outbounds) {
|
||||
break
|
||||
}
|
||||
if canContinue {
|
||||
continue
|
||||
}
|
||||
currentOutbound := common.Find(s.outbounds, func(it adapter.Outbound) bool {
|
||||
return !started[outboundTags[it]]
|
||||
})
|
||||
var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error
|
||||
lintOutbound = func(oTree []string, oCurrent adapter.Outbound) error {
|
||||
problemOutboundTag := common.Find(oCurrent.Dependencies(), func(it string) bool {
|
||||
return !started[it]
|
||||
})
|
||||
if common.Contains(oTree, problemOutboundTag) {
|
||||
return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag)
|
||||
}
|
||||
problemOutbound := outbounds[problemOutboundTag]
|
||||
if problemOutbound == nil {
|
||||
return E.New("dependency[", problemOutboundTag, "] not found for outbound[", outboundTags[oCurrent], "]")
|
||||
}
|
||||
return lintOutbound(append(oTree, problemOutboundTag), problemOutbound)
|
||||
}
|
||||
return lintOutbound([]string{outboundTags[currentOutbound]}, currentOutbound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
26
cmd/internal/build/main.go
Normal file
26
cmd/internal/build/main.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go/build"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
build_shared.FindSDK()
|
||||
|
||||
if os.Getenv("GOPATH") == "" {
|
||||
os.Setenv("GOPATH", build.Default.GOPATH)
|
||||
}
|
||||
|
||||
command := exec.Command(os.Args[1], os.Args[2:]...)
|
||||
command.Stdout = os.Stdout
|
||||
command.Stderr = os.Stderr
|
||||
err := command.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
144
cmd/internal/build_libbox/main.go
Normal file
144
cmd/internal/build_libbox/main.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
_ "github.com/sagernet/gomobile/event/key"
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
)
|
||||
|
||||
var (
|
||||
debugEnabled bool
|
||||
target string
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
|
||||
flag.StringVar(&target, "target", "android", "target platform")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
build_shared.FindMobile()
|
||||
|
||||
switch target {
|
||||
case "android":
|
||||
buildAndroid()
|
||||
case "ios":
|
||||
buildiOS()
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
sharedFlags []string
|
||||
debugFlags []string
|
||||
sharedTags []string
|
||||
iosTags []string
|
||||
debugTags []string
|
||||
)
|
||||
|
||||
func init() {
|
||||
sharedFlags = append(sharedFlags, "-trimpath")
|
||||
sharedFlags = append(sharedFlags, "-ldflags")
|
||||
currentTag, err := build_shared.ReadTag()
|
||||
if err != nil {
|
||||
currentTag = "unknown"
|
||||
}
|
||||
sharedFlags = append(sharedFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
||||
debugFlags = append(debugFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
||||
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api")
|
||||
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
||||
debugTags = append(debugTags, "debug")
|
||||
}
|
||||
|
||||
func buildAndroid() {
|
||||
build_shared.FindSDK()
|
||||
|
||||
args := []string{
|
||||
"bind",
|
||||
"-v",
|
||||
"-androidapi", "21",
|
||||
"-javapkg=io.nekohasekai",
|
||||
"-libname=box",
|
||||
}
|
||||
if !debugEnabled {
|
||||
args = append(args, sharedFlags...)
|
||||
} else {
|
||||
args = append(args, debugFlags...)
|
||||
}
|
||||
|
||||
args = append(args, "-tags")
|
||||
if !debugEnabled {
|
||||
args = append(args, strings.Join(sharedTags, ","))
|
||||
} else {
|
||||
args = append(args, strings.Join(append(sharedTags, debugTags...), ","))
|
||||
}
|
||||
args = append(args, "./experimental/libbox")
|
||||
|
||||
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
||||
command.Stdout = os.Stdout
|
||||
command.Stderr = os.Stderr
|
||||
err := command.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
const name = "libbox.aar"
|
||||
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
|
||||
if rw.FileExists(copyPath) {
|
||||
copyPath, _ = filepath.Abs(copyPath)
|
||||
err = rw.CopyFile(name, filepath.Join(copyPath, name))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Info("copied to ", copyPath)
|
||||
}
|
||||
}
|
||||
|
||||
func buildiOS() {
|
||||
args := []string{
|
||||
"bind",
|
||||
"-v",
|
||||
"-target", "ios,iossimulator,tvos,tvossimulator,macos",
|
||||
"-libname=box",
|
||||
}
|
||||
if !debugEnabled {
|
||||
args = append(args, sharedFlags...)
|
||||
} else {
|
||||
args = append(args, debugFlags...)
|
||||
}
|
||||
|
||||
tags := append(sharedTags, iosTags...)
|
||||
args = append(args, "-tags")
|
||||
if !debugEnabled {
|
||||
args = append(args, strings.Join(tags, ","))
|
||||
} else {
|
||||
args = append(args, strings.Join(append(tags, debugTags...), ","))
|
||||
}
|
||||
args = append(args, "./experimental/libbox")
|
||||
|
||||
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
||||
command.Stdout = os.Stdout
|
||||
command.Stderr = os.Stderr
|
||||
err := command.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
copyPath := filepath.Join("..", "sing-box-for-apple")
|
||||
if rw.FileExists(copyPath) {
|
||||
targetDir := filepath.Join(copyPath, "Libbox.xcframework")
|
||||
targetDir, _ = filepath.Abs(targetDir)
|
||||
os.RemoveAll(targetDir)
|
||||
os.Rename("Libbox.xcframework", targetDir)
|
||||
log.Info("copied to ", targetDir)
|
||||
}
|
||||
}
|
||||
92
cmd/internal/build_shared/sdk.go
Normal file
92
cmd/internal/build_shared/sdk.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package build_shared
|
||||
|
||||
import (
|
||||
"go/build"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
)
|
||||
|
||||
var (
|
||||
androidSDKPath string
|
||||
androidNDKPath string
|
||||
)
|
||||
|
||||
func FindSDK() {
|
||||
searchPath := []string{
|
||||
"$ANDROID_HOME",
|
||||
"$HOME/Android/Sdk",
|
||||
"$HOME/.local/lib/android/sdk",
|
||||
"$HOME/Library/Android/sdk",
|
||||
}
|
||||
for _, path := range searchPath {
|
||||
path = os.ExpandEnv(path)
|
||||
if rw.FileExists(path + "/licenses/android-sdk-license") {
|
||||
androidSDKPath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
if androidSDKPath == "" {
|
||||
log.Fatal("android SDK not found")
|
||||
}
|
||||
if !findNDK() {
|
||||
log.Fatal("android NDK not found")
|
||||
}
|
||||
|
||||
os.Setenv("ANDROID_HOME", androidSDKPath)
|
||||
os.Setenv("ANDROID_SDK_HOME", androidSDKPath)
|
||||
os.Setenv("ANDROID_NDK_HOME", androidNDKPath)
|
||||
os.Setenv("NDK", androidNDKPath)
|
||||
os.Setenv("PATH", os.Getenv("PATH")+":"+filepath.Join(androidNDKPath, "toolchains", "llvm", "prebuilt", runtime.GOOS+"-x86_64", "bin"))
|
||||
}
|
||||
|
||||
func findNDK() bool {
|
||||
if rw.FileExists(androidSDKPath + "/ndk/25.1.8937393") {
|
||||
androidNDKPath = androidSDKPath + "/ndk/25.1.8937393"
|
||||
return true
|
||||
}
|
||||
ndkVersions, err := os.ReadDir(androidSDKPath + "/ndk")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
versionNames := common.Map(ndkVersions, os.DirEntry.Name)
|
||||
if len(versionNames) == 0 {
|
||||
return false
|
||||
}
|
||||
sort.Slice(versionNames, func(i, j int) bool {
|
||||
iVersions := strings.Split(versionNames[i], ".")
|
||||
jVersions := strings.Split(versionNames[j], ".")
|
||||
for k := 0; k < len(iVersions) && k < len(jVersions); k++ {
|
||||
iVersion, _ := strconv.Atoi(iVersions[k])
|
||||
jVersion, _ := strconv.Atoi(jVersions[k])
|
||||
if iVersion != jVersion {
|
||||
return iVersion > jVersion
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
for _, versionName := range versionNames {
|
||||
if rw.FileExists(androidSDKPath + "/ndk/" + versionName) {
|
||||
androidNDKPath = androidSDKPath + "/ndk/" + versionName
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var GoBinPath string
|
||||
|
||||
func FindMobile() {
|
||||
goBin := filepath.Join(build.Default.GOPATH, "bin")
|
||||
if !rw.FileExists(goBin + "/" + "gobind") {
|
||||
log.Fatal("missing gomobile installation")
|
||||
}
|
||||
GoBinPath = goBin
|
||||
}
|
||||
33
cmd/internal/build_shared/tag.go
Normal file
33
cmd/internal/build_shared/tag.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package build_shared
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/common/badversion"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/shell"
|
||||
)
|
||||
|
||||
func ReadTag() (string, error) {
|
||||
currentTag, err := shell.Exec("git", "describe", "--tags").ReadOutput()
|
||||
if err != nil {
|
||||
return currentTag, err
|
||||
}
|
||||
currentTagRev, _ := shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput()
|
||||
if currentTagRev == currentTag {
|
||||
return currentTag[1:], nil
|
||||
}
|
||||
shortCommit, _ := shell.Exec("git", "rev-parse", "--short", "HEAD").ReadOutput()
|
||||
version := badversion.Parse(currentTagRev[1:])
|
||||
return version.String() + "-" + shortCommit, nil
|
||||
}
|
||||
|
||||
func ReadTagVersion() (badversion.Version, error) {
|
||||
currentTag := common.Must1(shell.Exec("git", "describe", "--tags").ReadOutput())
|
||||
currentTagRev := common.Must1(shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput())
|
||||
version := badversion.Parse(currentTagRev[1:])
|
||||
if currentTagRev != currentTag {
|
||||
if version.PreReleaseIdentifier == "" {
|
||||
version.Patch++
|
||||
}
|
||||
}
|
||||
return version, nil
|
||||
}
|
||||
218
cmd/internal/protogen/main.go
Normal file
218
cmd/internal/protogen/main.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// envFile returns the name of the Go environment configuration file.
|
||||
// Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166
|
||||
func envFile() (string, error) {
|
||||
if file := os.Getenv("GOENV"); file != "" {
|
||||
if file == "off" {
|
||||
return "", fmt.Errorf("GOENV=off")
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
dir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if dir == "" {
|
||||
return "", fmt.Errorf("missing user-config dir")
|
||||
}
|
||||
return filepath.Join(dir, "go", "env"), nil
|
||||
}
|
||||
|
||||
// GetRuntimeEnv returns the value of runtime environment variable,
|
||||
// that is set by running following command: `go env -w key=value`.
|
||||
func GetRuntimeEnv(key string) (string, error) {
|
||||
file, err := envFile()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if file == "" {
|
||||
return "", fmt.Errorf("missing runtime env file")
|
||||
}
|
||||
var data []byte
|
||||
var runtimeEnv string
|
||||
data, readErr := os.ReadFile(file)
|
||||
if readErr != nil {
|
||||
return "", readErr
|
||||
}
|
||||
envStrings := strings.Split(string(data), "\n")
|
||||
for _, envItem := range envStrings {
|
||||
envItem = strings.TrimSuffix(envItem, "\r")
|
||||
envKeyValue := strings.Split(envItem, "=")
|
||||
if strings.EqualFold(strings.TrimSpace(envKeyValue[0]), key) {
|
||||
runtimeEnv = strings.TrimSpace(envKeyValue[1])
|
||||
}
|
||||
}
|
||||
return runtimeEnv, nil
|
||||
}
|
||||
|
||||
// GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty.
|
||||
func GetGOBIN() string {
|
||||
// The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command`
|
||||
GOBIN := os.Getenv("GOBIN")
|
||||
if GOBIN == "" {
|
||||
var err error
|
||||
// The one set by user by running `go env -w GOBIN=/path`
|
||||
GOBIN, err = GetRuntimeEnv("GOBIN")
|
||||
if err != nil {
|
||||
// The default one that Golang uses
|
||||
return filepath.Join(build.Default.GOPATH, "bin")
|
||||
}
|
||||
if GOBIN == "" {
|
||||
return filepath.Join(build.Default.GOPATH, "bin")
|
||||
}
|
||||
return GOBIN
|
||||
}
|
||||
return GOBIN
|
||||
}
|
||||
|
||||
func main() {
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Println("Can not get current working directory.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
GOBIN := GetGOBIN()
|
||||
binPath := os.Getenv("PATH")
|
||||
pathSlice := []string{pwd, GOBIN, binPath}
|
||||
binPath = strings.Join(pathSlice, string(os.PathListSeparator))
|
||||
os.Setenv("PATH", binPath)
|
||||
|
||||
suffix := ""
|
||||
if runtime.GOOS == "windows" {
|
||||
suffix = ".exe"
|
||||
}
|
||||
|
||||
protoc := "protoc"
|
||||
|
||||
if linkPath, err := os.Readlink(protoc); err == nil {
|
||||
protoc = linkPath
|
||||
}
|
||||
|
||||
protoFilesMap := make(map[string][]string)
|
||||
walkErr := filepath.Walk("./", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
filename := filepath.Base(path)
|
||||
if strings.HasSuffix(filename, ".proto") &&
|
||||
filename != "typed_message.proto" &&
|
||||
filename != "descriptor.proto" {
|
||||
protoFilesMap[dir] = append(protoFilesMap[dir], path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if walkErr != nil {
|
||||
fmt.Println(walkErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, files := range protoFilesMap {
|
||||
for _, relProtoFile := range files {
|
||||
args := []string{
|
||||
"-I", ".",
|
||||
"--go_out", pwd,
|
||||
"--go_opt", "paths=source_relative",
|
||||
"--go-grpc_out", pwd,
|
||||
"--go-grpc_opt", "paths=source_relative",
|
||||
"--plugin", "protoc-gen-go=" + filepath.Join(GOBIN, "protoc-gen-go"+suffix),
|
||||
"--plugin", "protoc-gen-go-grpc=" + filepath.Join(GOBIN, "protoc-gen-go-grpc"+suffix),
|
||||
}
|
||||
args = append(args, relProtoFile)
|
||||
cmd := exec.Command(protoc, args...)
|
||||
cmd.Env = append(cmd.Env, os.Environ()...)
|
||||
output, cmdErr := cmd.CombinedOutput()
|
||||
if len(output) > 0 {
|
||||
fmt.Println(string(output))
|
||||
}
|
||||
if cmdErr != nil {
|
||||
fmt.Println(cmdErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
normalizeWalkErr := filepath.Walk("./", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
filename := filepath.Base(path)
|
||||
if strings.HasSuffix(filename, ".pb.go") &&
|
||||
path != "config.pb.go" {
|
||||
if err := NormalizeGeneratedProtoFile(path); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if normalizeWalkErr != nil {
|
||||
fmt.Println(normalizeWalkErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeGeneratedProtoFile(path string) error {
|
||||
fd, err := os.OpenFile(path, os.O_RDWR, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = fd.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out := bytes.NewBuffer(nil)
|
||||
scanner := bufio.NewScanner(fd)
|
||||
valid := false
|
||||
for scanner.Scan() {
|
||||
if !valid && !strings.HasPrefix(scanner.Text(), "package ") {
|
||||
continue
|
||||
}
|
||||
valid = true
|
||||
out.Write(scanner.Bytes())
|
||||
out.Write([]byte("\n"))
|
||||
}
|
||||
_, err = fd.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = fd.Truncate(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(fd, bytes.NewReader(out.Bytes()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
21
cmd/internal/read_tag/main.go
Normal file
21
cmd/internal/read_tag/main.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
currentTag, err := build_shared.ReadTag()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
_, err = os.Stdout.WriteString("unknown\n")
|
||||
} else {
|
||||
_, err = os.Stdout.WriteString(currentTag + "\n")
|
||||
}
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
51
cmd/internal/update_android_version/main.go
Normal file
51
cmd/internal/update_android_version/main.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
func main() {
|
||||
newVersion := common.Must1(build_shared.ReadTagVersion())
|
||||
androidPath, err := filepath.Abs("../sing-box-for-android")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
common.Must(os.Chdir(androidPath))
|
||||
localProps := common.Must1(os.ReadFile("local.properties"))
|
||||
var propsList [][]string
|
||||
for _, propLine := range strings.Split(string(localProps), "\n") {
|
||||
propsList = append(propsList, strings.Split(propLine, "="))
|
||||
}
|
||||
for _, propPair := range propsList {
|
||||
if propPair[0] == "VERSION_NAME" {
|
||||
if propPair[1] == newVersion.String() {
|
||||
log.Info("version not changed")
|
||||
return
|
||||
}
|
||||
propPair[1] = newVersion.String()
|
||||
log.Info("updated version to ", newVersion.String())
|
||||
}
|
||||
}
|
||||
for _, propPair := range propsList {
|
||||
switch propPair[0] {
|
||||
case "VERSION_CODE":
|
||||
versionCode := common.Must1(strconv.ParseInt(propPair[1], 10, 64))
|
||||
propPair[1] = strconv.Itoa(int(versionCode + 1))
|
||||
log.Info("updated version code to ", propPair[1])
|
||||
case "RELEASE_NOTES":
|
||||
propPair[1] = "sing-box " + newVersion.String()
|
||||
}
|
||||
}
|
||||
var newProps []string
|
||||
for _, propPair := range propsList {
|
||||
newProps = append(newProps, strings.Join(propPair, "="))
|
||||
}
|
||||
common.Must(os.WriteFile("local.properties", []byte(strings.Join(newProps, "\n")), 0o644))
|
||||
}
|
||||
131
cmd/internal/update_apple_version/main.go
Normal file
131
cmd/internal/update_apple_version/main.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
|
||||
"howett.net/plist"
|
||||
)
|
||||
|
||||
func main() {
|
||||
newVersion := common.Must1(build_shared.ReadTagVersion())
|
||||
applePath, err := filepath.Abs("../sing-box-for-apple")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
common.Must(os.Chdir(applePath))
|
||||
projectFile := common.Must1(os.Open("sing-box.xcodeproj/project.pbxproj"))
|
||||
var project map[string]any
|
||||
decoder := plist.NewDecoder(projectFile)
|
||||
common.Must(decoder.Decode(&project))
|
||||
objectsMap := project["objects"].(map[string]any)
|
||||
projectContent := string(common.Must1(os.ReadFile("sing-box.xcodeproj/project.pbxproj")))
|
||||
newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfa"}, newVersion.VersionString())
|
||||
newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfa.independent", "io.nekohasekai.sfa.system"}, newVersion.String())
|
||||
if updated0 || updated1 {
|
||||
log.Info("updated version to ", newVersion.VersionString(), " (", newVersion.String(), ")")
|
||||
}
|
||||
var updated2 bool
|
||||
if macProjectVersion := os.Getenv("MACOS_PROJECT_VERSION"); macProjectVersion != "" {
|
||||
newContent, updated2 = findAndReplaceProjectVersion(objectsMap, newContent, []string{"SFM"}, macProjectVersion)
|
||||
if updated2 {
|
||||
log.Info("updated macos project version to ", macProjectVersion)
|
||||
}
|
||||
}
|
||||
if updated0 || updated1 || updated2 {
|
||||
common.Must(os.WriteFile("sing-box.xcodeproj/project.pbxproj", []byte(newContent), 0o644))
|
||||
}
|
||||
}
|
||||
|
||||
func findAndReplace(objectsMap map[string]any, projectContent string, bundleIDList []string, newVersion string) (string, bool) {
|
||||
objectKeyList := findObjectKey(objectsMap, bundleIDList)
|
||||
var updated bool
|
||||
for _, objectKey := range objectKeyList {
|
||||
matchRegexp := common.Must1(regexp.Compile(objectKey + ".*= \\{"))
|
||||
indexes := matchRegexp.FindStringIndex(projectContent)
|
||||
if len(indexes) < 2 {
|
||||
println(projectContent)
|
||||
log.Fatal("failed to find object key ", objectKey, ": ", strings.Index(projectContent, objectKey))
|
||||
}
|
||||
indexStart := indexes[1]
|
||||
indexEnd := indexStart + strings.Index(projectContent[indexStart:], "}")
|
||||
versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "MARKETING_VERSION = ") + 20
|
||||
versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";")
|
||||
version := projectContent[versionStart:versionEnd]
|
||||
if version == newVersion {
|
||||
continue
|
||||
}
|
||||
updated = true
|
||||
projectContent = projectContent[:versionStart] + newVersion + projectContent[versionEnd:]
|
||||
}
|
||||
return projectContent, updated
|
||||
}
|
||||
|
||||
func findAndReplaceProjectVersion(objectsMap map[string]any, projectContent string, directoryList []string, newVersion string) (string, bool) {
|
||||
objectKeyList := findObjectKeyByDirectory(objectsMap, directoryList)
|
||||
var updated bool
|
||||
for _, objectKey := range objectKeyList {
|
||||
matchRegexp := common.Must1(regexp.Compile(objectKey + ".*= \\{"))
|
||||
indexes := matchRegexp.FindStringIndex(projectContent)
|
||||
if len(indexes) < 2 {
|
||||
println(projectContent)
|
||||
log.Fatal("failed to find object key ", objectKey, ": ", strings.Index(projectContent, objectKey))
|
||||
}
|
||||
indexStart := indexes[1]
|
||||
indexEnd := indexStart + strings.Index(projectContent[indexStart:], "}")
|
||||
versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "CURRENT_PROJECT_VERSION = ") + 26
|
||||
versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";")
|
||||
version := projectContent[versionStart:versionEnd]
|
||||
if version == newVersion {
|
||||
continue
|
||||
}
|
||||
updated = true
|
||||
projectContent = projectContent[:versionStart] + newVersion + projectContent[versionEnd:]
|
||||
}
|
||||
return projectContent, updated
|
||||
}
|
||||
|
||||
func findObjectKey(objectsMap map[string]any, bundleIDList []string) []string {
|
||||
var objectKeyList []string
|
||||
for objectKey, object := range objectsMap {
|
||||
buildSettings := object.(map[string]any)["buildSettings"]
|
||||
if buildSettings == nil {
|
||||
continue
|
||||
}
|
||||
bundleIDObject := buildSettings.(map[string]any)["PRODUCT_BUNDLE_IDENTIFIER"]
|
||||
if bundleIDObject == nil {
|
||||
continue
|
||||
}
|
||||
if common.Contains(bundleIDList, bundleIDObject.(string)) {
|
||||
objectKeyList = append(objectKeyList, objectKey)
|
||||
}
|
||||
}
|
||||
return objectKeyList
|
||||
}
|
||||
|
||||
func findObjectKeyByDirectory(objectsMap map[string]any, directoryList []string) []string {
|
||||
var objectKeyList []string
|
||||
for objectKey, object := range objectsMap {
|
||||
buildSettings := object.(map[string]any)["buildSettings"]
|
||||
if buildSettings == nil {
|
||||
continue
|
||||
}
|
||||
infoPListFile := buildSettings.(map[string]any)["INFOPLIST_FILE"]
|
||||
if infoPListFile == nil {
|
||||
continue
|
||||
}
|
||||
for _, searchDirectory := range directoryList {
|
||||
if strings.HasPrefix(infoPListFile.(string), searchDirectory+"/") {
|
||||
objectKeyList = append(objectKeyList, objectKey)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return objectKeyList
|
||||
}
|
||||
@@ -2,12 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box"
|
||||
"github.com/sagernet/sing-box/common/json"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -15,24 +12,32 @@ import (
|
||||
var commandCheck = &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Check configuration",
|
||||
Run: checkConfiguration,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := check()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
Args: cobra.NoArgs,
|
||||
}
|
||||
|
||||
func checkConfiguration(cmd *cobra.Command, args []string) {
|
||||
configContent, err := os.ReadFile(configPath)
|
||||
func init() {
|
||||
mainCommand.AddCommand(commandCheck)
|
||||
}
|
||||
|
||||
func check() error {
|
||||
options, err := readConfigAndMerge()
|
||||
if err != nil {
|
||||
log.Fatal("read config: ", err)
|
||||
}
|
||||
var options option.Options
|
||||
err = json.Unmarshal(configContent, &options)
|
||||
if err != nil {
|
||||
log.Fatal("decode config: ", err)
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
_, err = box.New(ctx, options)
|
||||
if err != nil {
|
||||
log.Fatal("create service: ", err)
|
||||
instance, err := box.New(box.Options{
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
})
|
||||
if err == nil {
|
||||
instance.Close()
|
||||
}
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/sagernet/sing-box/common/json"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -17,47 +18,92 @@ var commandFormatFlagWrite bool
|
||||
var commandFormat = &cobra.Command{
|
||||
Use: "format",
|
||||
Short: "Format configuration",
|
||||
Run: formatConfiguration,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := format()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
Args: cobra.NoArgs,
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandFormat.Flags().BoolVarP(&commandFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
|
||||
mainCommand.AddCommand(commandFormat)
|
||||
}
|
||||
|
||||
func formatConfiguration(cmd *cobra.Command, args []string) {
|
||||
func format() error {
|
||||
optionsList, err := readConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, optionsEntry := range optionsList {
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(optionsEntry.options)
|
||||
if err != nil {
|
||||
return E.Cause(err, "encode config")
|
||||
}
|
||||
outputPath, _ := filepath.Abs(optionsEntry.path)
|
||||
if !commandFormatFlagWrite {
|
||||
if len(optionsList) > 1 {
|
||||
os.Stdout.WriteString(outputPath + "\n")
|
||||
}
|
||||
os.Stdout.WriteString(buffer.String() + "\n")
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(optionsEntry.content, buffer.Bytes()) {
|
||||
continue
|
||||
}
|
||||
output, err := os.Create(optionsEntry.path)
|
||||
if err != nil {
|
||||
return E.Cause(err, "open output")
|
||||
}
|
||||
_, err = output.Write(buffer.Bytes())
|
||||
output.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "write output")
|
||||
}
|
||||
os.Stderr.WriteString(outputPath + "\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatOne(configPath string) error {
|
||||
configContent, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
log.Fatal("read config: ", err)
|
||||
return E.Cause(err, "read config")
|
||||
}
|
||||
var options option.Options
|
||||
err = json.Unmarshal(configContent, &options)
|
||||
err = options.UnmarshalJSON(configContent)
|
||||
if err != nil {
|
||||
log.Fatal("decode config: ", err)
|
||||
return E.Cause(err, "decode config")
|
||||
}
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(options)
|
||||
if err != nil {
|
||||
log.Fatal("encode config: ", err)
|
||||
return E.Cause(err, "encode config")
|
||||
}
|
||||
if !commandFormatFlagWrite {
|
||||
os.Stdout.WriteString(buffer.String() + "\n")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if bytes.Equal(configContent, buffer.Bytes()) {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
output, err := os.Create(configPath)
|
||||
if err != nil {
|
||||
log.Fatal("open output: ", err)
|
||||
return E.Cause(err, "open output")
|
||||
}
|
||||
_, err = output.Write(buffer.Bytes())
|
||||
output.Close()
|
||||
if err != nil {
|
||||
log.Fatal("write output: ", err)
|
||||
return E.Cause(err, "write output")
|
||||
}
|
||||
outputPath, _ := filepath.Abs(configPath)
|
||||
os.Stderr.WriteString(outputPath + "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
92
cmd/sing-box/cmd_generate.go
Normal file
92
cmd/sing-box/cmd_generate.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandGenerate = &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Generate things",
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGenerate.AddCommand(commandGenerateUUID)
|
||||
commandGenerate.AddCommand(commandGenerateRandom)
|
||||
|
||||
mainCommand.AddCommand(commandGenerate)
|
||||
}
|
||||
|
||||
var (
|
||||
outputBase64 bool
|
||||
outputHex bool
|
||||
)
|
||||
|
||||
var commandGenerateRandom = &cobra.Command{
|
||||
Use: "rand <length>",
|
||||
Short: "Generate random bytes",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := generateRandom(args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGenerateRandom.Flags().BoolVar(&outputBase64, "base64", false, "Generate base64 string")
|
||||
commandGenerateRandom.Flags().BoolVar(&outputHex, "hex", false, "Generate hex string")
|
||||
}
|
||||
|
||||
func generateRandom(args []string) error {
|
||||
length, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
randomBytes := make([]byte, length)
|
||||
_, err = rand.Read(randomBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputBase64 {
|
||||
_, err = os.Stdout.WriteString(base64.StdEncoding.EncodeToString(randomBytes) + "\n")
|
||||
} else if outputHex {
|
||||
_, err = os.Stdout.WriteString(hex.EncodeToString(randomBytes) + "\n")
|
||||
} else {
|
||||
_, err = os.Stdout.Write(randomBytes)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var commandGenerateUUID = &cobra.Command{
|
||||
Use: "uuid",
|
||||
Short: "Generate UUID string",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := generateUUID()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func generateUUID() error {
|
||||
newUUID, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Stdout.WriteString(newUUID.String() + "\n")
|
||||
return err
|
||||
}
|
||||
39
cmd/sing-box/cmd_generate_ech.go
Normal file
39
cmd/sing-box/cmd_generate_ech.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var pqSignatureSchemesEnabled bool
|
||||
|
||||
var commandGenerateECHKeyPair = &cobra.Command{
|
||||
Use: "ech-keypair <plain_server_name>",
|
||||
Short: "Generate TLS ECH key pair",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := generateECHKeyPair(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGenerateECHKeyPair.Flags().BoolVar(&pqSignatureSchemesEnabled, "pq-signature-schemes-enabled", false, "Enable PQ signature schemes")
|
||||
commandGenerate.AddCommand(commandGenerateECHKeyPair)
|
||||
}
|
||||
|
||||
func generateECHKeyPair(serverName string) error {
|
||||
configPem, keyPem, err := tls.ECHKeygenDefault(serverName, pqSignatureSchemesEnabled)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout.WriteString(configPem)
|
||||
os.Stdout.WriteString(keyPem)
|
||||
return nil
|
||||
}
|
||||
40
cmd/sing-box/cmd_generate_tls.go
Normal file
40
cmd/sing-box/cmd_generate_tls.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var flagGenerateTLSKeyPairMonths int
|
||||
|
||||
var commandGenerateTLSKeyPair = &cobra.Command{
|
||||
Use: "tls-keypair <server_name>",
|
||||
Short: "Generate TLS self sign key pair",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := generateTLSKeyPair(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGenerateTLSKeyPair.Flags().IntVarP(&flagGenerateTLSKeyPairMonths, "months", "m", 1, "Valid months")
|
||||
commandGenerate.AddCommand(commandGenerateTLSKeyPair)
|
||||
}
|
||||
|
||||
func generateTLSKeyPair(serverName string) error {
|
||||
privateKeyPem, publicKeyPem, err := tls.GenerateKeyPair(time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout.WriteString(string(privateKeyPem) + "\n")
|
||||
os.Stdout.WriteString(string(publicKeyPem) + "\n")
|
||||
return nil
|
||||
}
|
||||
40
cmd/sing-box/cmd_generate_vapid.go
Normal file
40
cmd/sing-box/cmd_generate_vapid.go
Normal file
@@ -0,0 +1,40 @@
|
||||
//go:build go1.20
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ecdh"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandGenerateVAPIDKeyPair = &cobra.Command{
|
||||
Use: "vapid-keypair",
|
||||
Short: "Generate VAPID key pair",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := generateVAPIDKeyPair()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGenerate.AddCommand(commandGenerateVAPIDKeyPair)
|
||||
}
|
||||
|
||||
func generateVAPIDKeyPair() error {
|
||||
privateKey, err := ecdh.P256().GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicKey := privateKey.PublicKey()
|
||||
os.Stdout.WriteString("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey.Bytes()) + "\n")
|
||||
os.Stdout.WriteString("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey.Bytes()) + "\n")
|
||||
return nil
|
||||
}
|
||||
61
cmd/sing-box/cmd_generate_wireguard.go
Normal file
61
cmd/sing-box/cmd_generate_wireguard.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commandGenerate.AddCommand(commandGenerateWireGuardKeyPair)
|
||||
commandGenerate.AddCommand(commandGenerateRealityKeyPair)
|
||||
}
|
||||
|
||||
var commandGenerateWireGuardKeyPair = &cobra.Command{
|
||||
Use: "wg-keypair",
|
||||
Short: "Generate WireGuard key pair",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := generateWireGuardKey()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func generateWireGuardKey() error {
|
||||
privateKey, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout.WriteString("PrivateKey: " + privateKey.String() + "\n")
|
||||
os.Stdout.WriteString("PublicKey: " + privateKey.PublicKey().String() + "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
var commandGenerateRealityKeyPair = &cobra.Command{
|
||||
Use: "reality-keypair",
|
||||
Short: "Generate reality key pair",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := generateRealityKey()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func generateRealityKey() error {
|
||||
privateKey, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicKey := privateKey.PublicKey()
|
||||
os.Stdout.WriteString("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]) + "\n")
|
||||
os.Stdout.WriteString("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:]) + "\n")
|
||||
return nil
|
||||
}
|
||||
174
cmd/sing-box/cmd_merge.go
Normal file
174
cmd/sing-box/cmd_merge.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/common/json"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandMerge = &cobra.Command{
|
||||
Use: "merge [output]",
|
||||
Short: "Merge configurations",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := merge(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
|
||||
func init() {
|
||||
mainCommand.AddCommand(commandMerge)
|
||||
}
|
||||
|
||||
func merge(outputPath string) error {
|
||||
mergedOptions, err := readConfigAndMerge()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = mergePathResources(&mergedOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(mergedOptions)
|
||||
if err != nil {
|
||||
return E.Cause(err, "encode config")
|
||||
}
|
||||
if existsContent, err := os.ReadFile(outputPath); err != nil {
|
||||
if string(existsContent) == buffer.String() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
err = rw.WriteFile(outputPath, buffer.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outputPath, _ = filepath.Abs(outputPath)
|
||||
os.Stderr.WriteString(outputPath + "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergePathResources(options *option.Options) error {
|
||||
for index, inbound := range options.Inbounds {
|
||||
switch inbound.Type {
|
||||
case C.TypeHTTP:
|
||||
inbound.HTTPOptions.TLS = mergeTLSInboundOptions(inbound.HTTPOptions.TLS)
|
||||
case C.TypeMixed:
|
||||
inbound.MixedOptions.TLS = mergeTLSInboundOptions(inbound.MixedOptions.TLS)
|
||||
case C.TypeVMess:
|
||||
inbound.VMessOptions.TLS = mergeTLSInboundOptions(inbound.VMessOptions.TLS)
|
||||
case C.TypeTrojan:
|
||||
inbound.TrojanOptions.TLS = mergeTLSInboundOptions(inbound.TrojanOptions.TLS)
|
||||
case C.TypeNaive:
|
||||
inbound.NaiveOptions.TLS = mergeTLSInboundOptions(inbound.NaiveOptions.TLS)
|
||||
case C.TypeHysteria:
|
||||
inbound.HysteriaOptions.TLS = mergeTLSInboundOptions(inbound.HysteriaOptions.TLS)
|
||||
case C.TypeVLESS:
|
||||
inbound.VLESSOptions.TLS = mergeTLSInboundOptions(inbound.VLESSOptions.TLS)
|
||||
case C.TypeTUIC:
|
||||
inbound.TUICOptions.TLS = mergeTLSInboundOptions(inbound.TUICOptions.TLS)
|
||||
case C.TypeHysteria2:
|
||||
inbound.Hysteria2Options.TLS = mergeTLSInboundOptions(inbound.Hysteria2Options.TLS)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
options.Inbounds[index] = inbound
|
||||
}
|
||||
for index, outbound := range options.Outbounds {
|
||||
switch outbound.Type {
|
||||
case C.TypeHTTP:
|
||||
outbound.HTTPOptions.TLS = mergeTLSOutboundOptions(outbound.HTTPOptions.TLS)
|
||||
case C.TypeVMess:
|
||||
outbound.VMessOptions.TLS = mergeTLSOutboundOptions(outbound.VMessOptions.TLS)
|
||||
case C.TypeTrojan:
|
||||
outbound.TrojanOptions.TLS = mergeTLSOutboundOptions(outbound.TrojanOptions.TLS)
|
||||
case C.TypeHysteria:
|
||||
outbound.HysteriaOptions.TLS = mergeTLSOutboundOptions(outbound.HysteriaOptions.TLS)
|
||||
case C.TypeSSH:
|
||||
outbound.SSHOptions = mergeSSHOutboundOptions(outbound.SSHOptions)
|
||||
case C.TypeVLESS:
|
||||
outbound.VLESSOptions.TLS = mergeTLSOutboundOptions(outbound.VLESSOptions.TLS)
|
||||
case C.TypeTUIC:
|
||||
outbound.TUICOptions.TLS = mergeTLSOutboundOptions(outbound.TUICOptions.TLS)
|
||||
case C.TypeHysteria2:
|
||||
outbound.Hysteria2Options.TLS = mergeTLSOutboundOptions(outbound.Hysteria2Options.TLS)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
options.Outbounds[index] = outbound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeTLSInboundOptions(options *option.InboundTLSOptions) *option.InboundTLSOptions {
|
||||
if options == nil {
|
||||
return nil
|
||||
}
|
||||
if options.CertificatePath != "" {
|
||||
if content, err := os.ReadFile(options.CertificatePath); err == nil {
|
||||
options.Certificate = trimStringArray(strings.Split(string(content), "\n"))
|
||||
}
|
||||
}
|
||||
if options.KeyPath != "" {
|
||||
if content, err := os.ReadFile(options.KeyPath); err == nil {
|
||||
options.Key = trimStringArray(strings.Split(string(content), "\n"))
|
||||
}
|
||||
}
|
||||
if options.ECH != nil {
|
||||
if options.ECH.KeyPath != "" {
|
||||
if content, err := os.ReadFile(options.ECH.KeyPath); err == nil {
|
||||
options.ECH.Key = trimStringArray(strings.Split(string(content), "\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func mergeTLSOutboundOptions(options *option.OutboundTLSOptions) *option.OutboundTLSOptions {
|
||||
if options == nil {
|
||||
return nil
|
||||
}
|
||||
if options.CertificatePath != "" {
|
||||
if content, err := os.ReadFile(options.CertificatePath); err == nil {
|
||||
options.Certificate = trimStringArray(strings.Split(string(content), "\n"))
|
||||
}
|
||||
}
|
||||
if options.ECH != nil {
|
||||
if options.ECH.ConfigPath != "" {
|
||||
if content, err := os.ReadFile(options.ECH.ConfigPath); err == nil {
|
||||
options.ECH.Config = trimStringArray(strings.Split(string(content), "\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func mergeSSHOutboundOptions(options option.SSHOutboundOptions) option.SSHOutboundOptions {
|
||||
if options.PrivateKeyPath != "" {
|
||||
if content, err := os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)); err == nil {
|
||||
options.PrivateKey = trimStringArray(strings.Split(string(content), "\n"))
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func trimStringArray(array []string) []string {
|
||||
return common.Filter(array, func(it string) bool {
|
||||
return strings.TrimSpace(it) != ""
|
||||
})
|
||||
}
|
||||
@@ -2,16 +2,20 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
runtimeDebug "runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box"
|
||||
"github.com/sagernet/sing-box/common/json"
|
||||
"github.com/sagernet/sing-box/common/badjsonmerge"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/debug"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -20,25 +24,102 @@ import (
|
||||
var commandRun = &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "Run service",
|
||||
Run: run,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func run(cmd *cobra.Command, args []string) {
|
||||
err := run0()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
func init() {
|
||||
mainCommand.AddCommand(commandRun)
|
||||
}
|
||||
|
||||
type OptionsEntry struct {
|
||||
content []byte
|
||||
path string
|
||||
options option.Options
|
||||
}
|
||||
|
||||
func readConfigAt(path string) (*OptionsEntry, error) {
|
||||
var (
|
||||
configContent []byte
|
||||
err error
|
||||
)
|
||||
if path == "stdin" {
|
||||
configContent, err = io.ReadAll(os.Stdin)
|
||||
} else {
|
||||
configContent, err = os.ReadFile(path)
|
||||
}
|
||||
}
|
||||
|
||||
func run0() error {
|
||||
configContent, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "read config")
|
||||
return nil, E.Cause(err, "read config at ", path)
|
||||
}
|
||||
var options option.Options
|
||||
err = json.Unmarshal(configContent, &options)
|
||||
err = options.UnmarshalJSON(configContent)
|
||||
if err != nil {
|
||||
return E.Cause(err, "decode config")
|
||||
return nil, E.Cause(err, "decode config at ", path)
|
||||
}
|
||||
return &OptionsEntry{
|
||||
content: configContent,
|
||||
path: path,
|
||||
options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readConfig() ([]*OptionsEntry, error) {
|
||||
var optionsList []*OptionsEntry
|
||||
for _, path := range configPaths {
|
||||
optionsEntry, err := readConfigAt(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
optionsList = append(optionsList, optionsEntry)
|
||||
}
|
||||
for _, directory := range configDirectories {
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read config directory at ", directory)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
optionsEntry, err := readConfigAt(filepath.Join(directory, entry.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
optionsList = append(optionsList, optionsEntry)
|
||||
}
|
||||
}
|
||||
sort.Slice(optionsList, func(i, j int) bool {
|
||||
return optionsList[i].path < optionsList[j].path
|
||||
})
|
||||
return optionsList, nil
|
||||
}
|
||||
|
||||
func readConfigAndMerge() (option.Options, error) {
|
||||
optionsList, err := readConfig()
|
||||
if err != nil {
|
||||
return option.Options{}, err
|
||||
}
|
||||
if len(optionsList) == 1 {
|
||||
return optionsList[0].options, nil
|
||||
}
|
||||
var mergedOptions option.Options
|
||||
for _, options := range optionsList {
|
||||
mergedOptions, err = badjsonmerge.MergeOptions(options.options, mergedOptions)
|
||||
if err != nil {
|
||||
return option.Options{}, E.Cause(err, "merge config at ", options.path)
|
||||
}
|
||||
}
|
||||
return mergedOptions, nil
|
||||
}
|
||||
|
||||
func create() (*box.Box, context.CancelFunc, error) {
|
||||
options, err := readConfigAndMerge()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if disableColor {
|
||||
if options.Log == nil {
|
||||
@@ -47,26 +128,76 @@ func run0() error {
|
||||
options.Log.DisableColor = true
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
instance, err := box.New(ctx, options)
|
||||
instance, err := box.New(box.Options{
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
})
|
||||
if err != nil {
|
||||
cancel()
|
||||
return E.Cause(err, "create service")
|
||||
}
|
||||
err = instance.Start()
|
||||
if err != nil {
|
||||
cancel()
|
||||
return E.Cause(err, "start service")
|
||||
}
|
||||
if debug.Enabled {
|
||||
http.HandleFunc("/debug/close", func(writer http.ResponseWriter, request *http.Request) {
|
||||
cancel()
|
||||
instance.Close()
|
||||
})
|
||||
return nil, nil, E.Cause(err, "create service")
|
||||
}
|
||||
|
||||
osSignals := make(chan os.Signal, 1)
|
||||
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM)
|
||||
<-osSignals
|
||||
cancel()
|
||||
instance.Close()
|
||||
return nil
|
||||
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
|
||||
defer func() {
|
||||
signal.Stop(osSignals)
|
||||
close(osSignals)
|
||||
}()
|
||||
startCtx, finishStart := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
_, loaded := <-osSignals
|
||||
if loaded {
|
||||
cancel()
|
||||
closeMonitor(startCtx)
|
||||
}
|
||||
}()
|
||||
err = instance.Start()
|
||||
finishStart()
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, nil, E.Cause(err, "start service")
|
||||
}
|
||||
return instance, cancel, nil
|
||||
}
|
||||
|
||||
func run() error {
|
||||
osSignals := make(chan os.Signal, 1)
|
||||
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
|
||||
defer signal.Stop(osSignals)
|
||||
for {
|
||||
instance, cancel, err := create()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtimeDebug.FreeOSMemory()
|
||||
for {
|
||||
osSignal := <-osSignals
|
||||
if osSignal == syscall.SIGHUP {
|
||||
err = check()
|
||||
if err != nil {
|
||||
log.Error(E.Cause(err, "reload service"))
|
||||
continue
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
closeCtx, closed := context.WithCancel(context.Background())
|
||||
go closeMonitor(closeCtx)
|
||||
instance.Close()
|
||||
closed()
|
||||
if osSignal != syscall.SIGHUP {
|
||||
return nil
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func closeMonitor(ctx context.Context) {
|
||||
time.Sleep(3 * time.Second)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
log.Fatal("sing-box did not close!")
|
||||
}
|
||||
|
||||
53
cmd/sing-box/cmd_tools.go
Normal file
53
cmd/sing-box/cmd_tools.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandToolsFlagOutbound string
|
||||
|
||||
var commandTools = &cobra.Command{
|
||||
Use: "tools",
|
||||
Short: "Experimental tools",
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandTools.PersistentFlags().StringVarP(&commandToolsFlagOutbound, "outbound", "o", "", "Use specified tag instead of default outbound")
|
||||
mainCommand.AddCommand(commandTools)
|
||||
}
|
||||
|
||||
func createPreStartedClient() (*box.Box, error) {
|
||||
options, err := readConfigAndMerge()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
instance, err := box.New(box.Options{Options: options})
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create service")
|
||||
}
|
||||
err = instance.PreStart()
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "start service")
|
||||
}
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) {
|
||||
if outboundTag == "" {
|
||||
outbound := instance.Router().DefaultOutbound(N.NetworkName(network))
|
||||
if outbound == nil {
|
||||
return nil, E.New("missing default outbound")
|
||||
}
|
||||
return outbound, nil
|
||||
} else {
|
||||
outbound, loaded := instance.Router().Outbound(outboundTag)
|
||||
if !loaded {
|
||||
return nil, E.New("outbound not found: ", outboundTag)
|
||||
}
|
||||
return outbound, nil
|
||||
}
|
||||
}
|
||||
73
cmd/sing-box/cmd_tools_connect.go
Normal file
73
cmd/sing-box/cmd_tools_connect.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/task"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandConnectFlagNetwork string
|
||||
|
||||
var commandConnect = &cobra.Command{
|
||||
Use: "connect [address]",
|
||||
Short: "Connect to an address",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := connect(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandConnect.Flags().StringVarP(&commandConnectFlagNetwork, "network", "n", "tcp", "network type")
|
||||
commandTools.AddCommand(commandConnect)
|
||||
}
|
||||
|
||||
func connect(address string) error {
|
||||
switch N.NetworkName(commandConnectFlagNetwork) {
|
||||
case N.NetworkTCP, N.NetworkUDP:
|
||||
default:
|
||||
return E.Cause(N.ErrUnknownNetwork, commandConnectFlagNetwork)
|
||||
}
|
||||
instance, err := createPreStartedClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer instance.Close()
|
||||
dialer, err := createDialer(instance, commandConnectFlagNetwork, commandToolsFlagOutbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn, err := dialer.DialContext(context.Background(), commandConnectFlagNetwork, M.ParseSocksaddr(address))
|
||||
if err != nil {
|
||||
return E.Cause(err, "connect to server")
|
||||
}
|
||||
var group task.Group
|
||||
group.Append("upload", func(ctx context.Context) error {
|
||||
return common.Error(bufio.Copy(conn, os.Stdin))
|
||||
})
|
||||
group.Append("download", func(ctx context.Context) error {
|
||||
return common.Error(bufio.Copy(os.Stdout, conn))
|
||||
})
|
||||
group.Cleanup(func() {
|
||||
conn.Close()
|
||||
})
|
||||
err = group.Run(context.Background())
|
||||
if E.IsClosed(err) {
|
||||
log.Info(err)
|
||||
} else {
|
||||
log.Error(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
91
cmd/sing-box/cmd_tools_fetch.go
Normal file
91
cmd/sing-box/cmd_tools_fetch.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandFetch = &cobra.Command{
|
||||
Use: "fetch",
|
||||
Short: "Fetch an URL",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := fetch(args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandTools.AddCommand(commandFetch)
|
||||
}
|
||||
|
||||
var httpClient *http.Client
|
||||
|
||||
func fetch(args []string) error {
|
||||
instance, err := createPreStartedClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer instance.Close()
|
||||
httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dialer, err := createDialer(instance, network, commandToolsFlagOutbound)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
ForceAttemptHTTP2: true,
|
||||
},
|
||||
}
|
||||
defer httpClient.CloseIdleConnections()
|
||||
for _, urlString := range args {
|
||||
parsedURL, err := url.Parse(urlString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch parsedURL.Scheme {
|
||||
case "":
|
||||
parsedURL.Scheme = "http"
|
||||
fallthrough
|
||||
case "http", "https":
|
||||
err = fetchHTTP(parsedURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchHTTP(parsedURL *url.URL) error {
|
||||
request, err := http.NewRequest("GET", parsedURL.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Add("User-Agent", "curl/7.88.0")
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
_, err = bufio.Copy(os.Stdout, response.Body)
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
69
cmd/sing-box/cmd_tools_synctime.go
Normal file
69
cmd/sing-box/cmd_tools_synctime.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/common/settings"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
commandSyncTimeFlagServer string
|
||||
commandSyncTimeOutputFormat string
|
||||
commandSyncTimeWrite bool
|
||||
)
|
||||
|
||||
var commandSyncTime = &cobra.Command{
|
||||
Use: "synctime",
|
||||
Short: "Sync time using the NTP protocol",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := syncTime()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandSyncTime.Flags().StringVarP(&commandSyncTimeFlagServer, "server", "s", "time.apple.com", "Set NTP server")
|
||||
commandSyncTime.Flags().StringVarP(&commandSyncTimeOutputFormat, "format", "f", C.TimeLayout, "Set output format")
|
||||
commandSyncTime.Flags().BoolVarP(&commandSyncTimeWrite, "write", "w", false, "Write time to system")
|
||||
commandTools.AddCommand(commandSyncTime)
|
||||
}
|
||||
|
||||
func syncTime() error {
|
||||
instance, err := createPreStartedClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer instance.Close()
|
||||
serverAddress := M.ParseSocksaddr(commandSyncTimeFlagServer)
|
||||
if serverAddress.Port == 0 {
|
||||
serverAddress.Port = 123
|
||||
}
|
||||
response, err := ntp.Exchange(context.Background(), dialer, serverAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if commandSyncTimeWrite {
|
||||
err = settings.SetSystemTime(response.Time)
|
||||
if err != nil {
|
||||
return E.Cause(err, "write time to system")
|
||||
}
|
||||
}
|
||||
os.Stdout.WriteString(response.Time.Local().Format(commandSyncTimeOutputFormat))
|
||||
return nil
|
||||
}
|
||||
@@ -3,9 +3,9 @@ package main
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -17,11 +17,48 @@ var commandVersion = &cobra.Command{
|
||||
Args: cobra.NoArgs,
|
||||
}
|
||||
|
||||
func printVersion(cmd *cobra.Command, args []string) {
|
||||
os.Stderr.WriteString(F.ToString("sing-box version ", C.Version, " (", runtime.Version(), ", ", runtime.GOOS, "/", runtime.GOARCH, ", CGO "))
|
||||
if C.CGO_ENABLED {
|
||||
os.Stderr.WriteString("enabled)\n")
|
||||
} else {
|
||||
os.Stderr.WriteString("disabled)\n")
|
||||
}
|
||||
var nameOnly bool
|
||||
|
||||
func init() {
|
||||
commandVersion.Flags().BoolVarP(&nameOnly, "name", "n", false, "print version name only")
|
||||
mainCommand.AddCommand(commandVersion)
|
||||
}
|
||||
|
||||
func printVersion(cmd *cobra.Command, args []string) {
|
||||
if nameOnly {
|
||||
os.Stdout.WriteString(C.Version + "\n")
|
||||
return
|
||||
}
|
||||
version := "sing-box version " + C.Version + "\n\n"
|
||||
version += "Environment: " + runtime.Version() + " " + runtime.GOOS + "/" + runtime.GOARCH + "\n"
|
||||
|
||||
var tags string
|
||||
var revision string
|
||||
|
||||
debugInfo, loaded := debug.ReadBuildInfo()
|
||||
if loaded {
|
||||
for _, setting := range debugInfo.Settings {
|
||||
switch setting.Key {
|
||||
case "-tags":
|
||||
tags = setting.Value
|
||||
case "vcs.revision":
|
||||
revision = setting.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tags != "" {
|
||||
version += "Tags: " + tags + "\n"
|
||||
}
|
||||
if revision != "" {
|
||||
version += "Revision: " + revision + "\n"
|
||||
}
|
||||
|
||||
if C.CGO_ENABLED {
|
||||
version += "CGO: enabled\n"
|
||||
} else {
|
||||
version += "CGO: disabled\n"
|
||||
}
|
||||
|
||||
os.Stdout.WriteString(version)
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
//go:build debug
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/sagernet/sing-box/common/badjson"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
func init() {
|
||||
http.HandleFunc("/debug/gc", func(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.WriteHeader(http.StatusNoContent)
|
||||
go debug.FreeOSMemory()
|
||||
})
|
||||
http.HandleFunc("/debug/memory", func(writer http.ResponseWriter, request *http.Request) {
|
||||
var memStats runtime.MemStats
|
||||
runtime.ReadMemStats(&memStats)
|
||||
|
||||
var memObject badjson.JSONObject
|
||||
memObject.Put("heap", humanize.Bytes(memStats.HeapInuse))
|
||||
memObject.Put("stack", humanize.Bytes(memStats.StackInuse))
|
||||
memObject.Put("idle", humanize.Bytes(memStats.HeapIdle-memStats.HeapReleased))
|
||||
memObject.Put("goroutines", runtime.NumGoroutine())
|
||||
memObject.Put("rss", rusageMaxRSS())
|
||||
|
||||
encoder := json.NewEncoder(writer)
|
||||
encoder.SetIndent("", " ")
|
||||
encoder.Encode(memObject)
|
||||
})
|
||||
go func() {
|
||||
err := http.ListenAndServe("0.0.0.0:8964", nil)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -2,16 +2,19 @@ package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "github.com/sagernet/sing-box/include"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
configPath string
|
||||
workingDir string
|
||||
disableColor bool
|
||||
configPaths []string
|
||||
configDirectories []string
|
||||
workingDir string
|
||||
disableColor bool
|
||||
)
|
||||
|
||||
var mainCommand = &cobra.Command{
|
||||
@@ -20,14 +23,10 @@ var mainCommand = &cobra.Command{
|
||||
}
|
||||
|
||||
func init() {
|
||||
mainCommand.PersistentFlags().StringVarP(&configPath, "config", "c", "config.json", "set configuration file path")
|
||||
mainCommand.PersistentFlags().StringArrayVarP(&configPaths, "config", "c", nil, "set configuration file path")
|
||||
mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path")
|
||||
mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
|
||||
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
|
||||
|
||||
mainCommand.AddCommand(commandRun)
|
||||
mainCommand.AddCommand(commandCheck)
|
||||
mainCommand.AddCommand(commandFormat)
|
||||
mainCommand.AddCommand(commandVersion)
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -37,9 +36,19 @@ func main() {
|
||||
}
|
||||
|
||||
func preRun(cmd *cobra.Command, args []string) {
|
||||
if disableColor {
|
||||
log.SetStdLogger(log.NewFactory(log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, nil).Logger())
|
||||
}
|
||||
if workingDir != "" {
|
||||
_, err := os.Stat(workingDir)
|
||||
if err != nil {
|
||||
os.MkdirAll(workingDir, 0o777)
|
||||
}
|
||||
if err := os.Chdir(workingDir); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
if len(configPaths) == 0 && len(configDirectories) == 0 {
|
||||
configPaths = append(configPaths, "config.json")
|
||||
}
|
||||
}
|
||||
|
||||
80
common/badjsonmerge/merge.go
Normal file
80
common/badjsonmerge/merge.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package badjsonmerge
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
|
||||
"github.com/sagernet/sing-box/common/badjson"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func MergeOptions(source option.Options, destination option.Options) (option.Options, error) {
|
||||
rawSource, err := json.Marshal(source)
|
||||
if err != nil {
|
||||
return option.Options{}, E.Cause(err, "marshal source")
|
||||
}
|
||||
rawDestination, err := json.Marshal(destination)
|
||||
if err != nil {
|
||||
return option.Options{}, E.Cause(err, "marshal destination")
|
||||
}
|
||||
rawMerged, err := MergeJSON(rawSource, rawDestination)
|
||||
if err != nil {
|
||||
return option.Options{}, E.Cause(err, "merge options")
|
||||
}
|
||||
var merged option.Options
|
||||
err = json.Unmarshal(rawMerged, &merged)
|
||||
if err != nil {
|
||||
return option.Options{}, E.Cause(err, "unmarshal merged options")
|
||||
}
|
||||
return merged, nil
|
||||
}
|
||||
|
||||
func MergeJSON(rawSource json.RawMessage, rawDestination json.RawMessage) (json.RawMessage, error) {
|
||||
source, err := badjson.Decode(rawSource)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode source")
|
||||
}
|
||||
destination, err := badjson.Decode(rawDestination)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode destination")
|
||||
}
|
||||
merged, err := mergeJSON(source, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(merged)
|
||||
}
|
||||
|
||||
func mergeJSON(anySource any, anyDestination any) (any, error) {
|
||||
switch destination := anyDestination.(type) {
|
||||
case badjson.JSONArray:
|
||||
switch source := anySource.(type) {
|
||||
case badjson.JSONArray:
|
||||
destination = append(destination, source...)
|
||||
default:
|
||||
destination = append(destination, source)
|
||||
}
|
||||
return destination, nil
|
||||
case *badjson.JSONObject:
|
||||
switch source := anySource.(type) {
|
||||
case *badjson.JSONObject:
|
||||
for _, entry := range source.Entries() {
|
||||
oldValue, loaded := destination.Get(entry.Key)
|
||||
if loaded {
|
||||
var err error
|
||||
entry.Value, err = mergeJSON(entry.Value, oldValue)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "merge object item ", entry.Key)
|
||||
}
|
||||
}
|
||||
destination.Put(entry.Key, entry.Value)
|
||||
}
|
||||
default:
|
||||
return nil, E.New("cannot merge json object into ", reflect.TypeOf(destination))
|
||||
}
|
||||
return destination, nil
|
||||
default:
|
||||
return destination, nil
|
||||
}
|
||||
}
|
||||
59
common/badjsonmerge/merge_test.go
Normal file
59
common/badjsonmerge/merge_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package badjsonmerge
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMergeJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
options := option.Options{
|
||||
Log: &option.LogOptions{
|
||||
Level: "info",
|
||||
},
|
||||
Route: &option.RouteOptions{
|
||||
Rules: []option.Rule{
|
||||
{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultRule{
|
||||
Network: []string{N.NetworkTCP},
|
||||
Outbound: "direct",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
anotherOptions := option.Options{
|
||||
Outbounds: []option.Outbound{
|
||||
{
|
||||
Type: C.TypeDirect,
|
||||
Tag: "direct",
|
||||
},
|
||||
},
|
||||
}
|
||||
thirdOptions := option.Options{
|
||||
Route: &option.RouteOptions{
|
||||
Rules: []option.Rule{
|
||||
{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultRule{
|
||||
Network: []string{N.NetworkUDP},
|
||||
Outbound: "direct",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
mergeOptions, err := MergeOptions(options, anotherOptions)
|
||||
require.NoError(t, err)
|
||||
mergeOptions, err = MergeOptions(thirdOptions, mergeOptions)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "info", mergeOptions.Log.Level)
|
||||
require.Equal(t, 2, len(mergeOptions.Route.Rules))
|
||||
require.Equal(t, C.TypeDirect, mergeOptions.Outbounds[0].Type)
|
||||
}
|
||||
233
common/badtls/badtls.go
Normal file
233
common/badtls/badtls.go
Normal file
@@ -0,0 +1,233 @@
|
||||
//go:build go1.20 && !go1.21
|
||||
|
||||
package badtls
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"reflect"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
*tls.Conn
|
||||
writer N.ExtendedWriter
|
||||
isHandshakeComplete *atomic.Bool
|
||||
activeCall *atomic.Int32
|
||||
closeNotifySent *bool
|
||||
version *uint16
|
||||
rand io.Reader
|
||||
halfAccess *sync.Mutex
|
||||
halfError *error
|
||||
cipher cipher.AEAD
|
||||
explicitNonceLen int
|
||||
halfPtr uintptr
|
||||
halfSeq []byte
|
||||
halfScratchBuf []byte
|
||||
}
|
||||
|
||||
func TryCreate(conn aTLS.Conn) aTLS.Conn {
|
||||
tlsConn, ok := conn.(*tls.Conn)
|
||||
if !ok {
|
||||
return conn
|
||||
}
|
||||
badConn, err := Create(tlsConn)
|
||||
if err != nil {
|
||||
log.Warn("initialize badtls: ", err)
|
||||
return conn
|
||||
}
|
||||
return badConn
|
||||
}
|
||||
|
||||
func Create(conn *tls.Conn) (aTLS.Conn, error) {
|
||||
rawConn := reflect.Indirect(reflect.ValueOf(conn))
|
||||
rawIsHandshakeComplete := rawConn.FieldByName("isHandshakeComplete")
|
||||
if !rawIsHandshakeComplete.IsValid() || rawIsHandshakeComplete.Kind() != reflect.Struct {
|
||||
return nil, E.New("badtls: invalid isHandshakeComplete")
|
||||
}
|
||||
isHandshakeComplete := (*atomic.Bool)(unsafe.Pointer(rawIsHandshakeComplete.UnsafeAddr()))
|
||||
if !isHandshakeComplete.Load() {
|
||||
return nil, E.New("handshake not finished")
|
||||
}
|
||||
rawActiveCall := rawConn.FieldByName("activeCall")
|
||||
if !rawActiveCall.IsValid() || rawActiveCall.Kind() != reflect.Struct {
|
||||
return nil, E.New("badtls: invalid active call")
|
||||
}
|
||||
activeCall := (*atomic.Int32)(unsafe.Pointer(rawActiveCall.UnsafeAddr()))
|
||||
rawHalfConn := rawConn.FieldByName("out")
|
||||
if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct {
|
||||
return nil, E.New("badtls: invalid half conn")
|
||||
}
|
||||
rawVersion := rawConn.FieldByName("vers")
|
||||
if !rawVersion.IsValid() || rawVersion.Kind() != reflect.Uint16 {
|
||||
return nil, E.New("badtls: invalid version")
|
||||
}
|
||||
version := (*uint16)(unsafe.Pointer(rawVersion.UnsafeAddr()))
|
||||
rawCloseNotifySent := rawConn.FieldByName("closeNotifySent")
|
||||
if !rawCloseNotifySent.IsValid() || rawCloseNotifySent.Kind() != reflect.Bool {
|
||||
return nil, E.New("badtls: invalid notify")
|
||||
}
|
||||
closeNotifySent := (*bool)(unsafe.Pointer(rawCloseNotifySent.UnsafeAddr()))
|
||||
rawConfig := reflect.Indirect(rawConn.FieldByName("config"))
|
||||
if !rawConfig.IsValid() || rawConfig.Kind() != reflect.Struct {
|
||||
return nil, E.New("badtls: bad config")
|
||||
}
|
||||
config := (*tls.Config)(unsafe.Pointer(rawConfig.UnsafeAddr()))
|
||||
randReader := config.Rand
|
||||
if randReader == nil {
|
||||
randReader = rand.Reader
|
||||
}
|
||||
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()))
|
||||
rawHalfError := rawHalfConn.FieldByName("err")
|
||||
if !rawHalfError.IsValid() || rawHalfError.Kind() != reflect.Interface {
|
||||
return nil, E.New("badtls: invalid half error")
|
||||
}
|
||||
halfError := (*error)(unsafe.Pointer(rawHalfError.UnsafeAddr()))
|
||||
rawHalfCipherInterface := rawHalfConn.FieldByName("cipher")
|
||||
if !rawHalfCipherInterface.IsValid() || rawHalfCipherInterface.Kind() != reflect.Interface {
|
||||
return nil, E.New("badtls: invalid cipher interface")
|
||||
}
|
||||
rawHalfCipher := rawHalfCipherInterface.Elem()
|
||||
aeadCipher, loaded := valueInterface(rawHalfCipher, false).(cipher.AEAD)
|
||||
if !loaded {
|
||||
return nil, E.New("badtls: invalid AEAD cipher")
|
||||
}
|
||||
var explicitNonceLen int
|
||||
switch cipherName := reflect.Indirect(rawHalfCipher).Type().String(); cipherName {
|
||||
case "tls.prefixNonceAEAD":
|
||||
explicitNonceLen = aeadCipher.NonceSize()
|
||||
case "tls.xorNonceAEAD":
|
||||
default:
|
||||
return nil, E.New("badtls: unknown cipher type: ", cipherName)
|
||||
}
|
||||
rawHalfSeq := rawHalfConn.FieldByName("seq")
|
||||
if !rawHalfSeq.IsValid() || rawHalfSeq.Kind() != reflect.Array {
|
||||
return nil, E.New("badtls: invalid seq")
|
||||
}
|
||||
halfSeq := rawHalfSeq.Bytes()
|
||||
rawHalfScratchBuf := rawHalfConn.FieldByName("scratchBuf")
|
||||
if !rawHalfScratchBuf.IsValid() || rawHalfScratchBuf.Kind() != reflect.Array {
|
||||
return nil, E.New("badtls: invalid scratchBuf")
|
||||
}
|
||||
halfScratchBuf := rawHalfScratchBuf.Bytes()
|
||||
return &Conn{
|
||||
Conn: conn,
|
||||
writer: bufio.NewExtendedWriter(conn.NetConn()),
|
||||
isHandshakeComplete: isHandshakeComplete,
|
||||
activeCall: activeCall,
|
||||
closeNotifySent: closeNotifySent,
|
||||
version: version,
|
||||
halfAccess: halfAccess,
|
||||
halfError: halfError,
|
||||
cipher: aeadCipher,
|
||||
explicitNonceLen: explicitNonceLen,
|
||||
rand: randReader,
|
||||
halfPtr: rawHalfConn.UnsafeAddr(),
|
||||
halfSeq: halfSeq,
|
||||
halfScratchBuf: halfScratchBuf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Conn) WriteBuffer(buffer *buf.Buffer) error {
|
||||
if buffer.Len() > maxPlaintext {
|
||||
defer buffer.Release()
|
||||
return common.Error(c.Write(buffer.Bytes()))
|
||||
}
|
||||
for {
|
||||
x := c.activeCall.Load()
|
||||
if x&1 != 0 {
|
||||
return net.ErrClosed
|
||||
}
|
||||
if c.activeCall.CompareAndSwap(x, x+2) {
|
||||
break
|
||||
}
|
||||
}
|
||||
defer c.activeCall.Add(-2)
|
||||
c.halfAccess.Lock()
|
||||
defer c.halfAccess.Unlock()
|
||||
if err := *c.halfError; err != nil {
|
||||
return err
|
||||
}
|
||||
if *c.closeNotifySent {
|
||||
return errShutdown
|
||||
}
|
||||
dataLen := buffer.Len()
|
||||
dataBytes := buffer.Bytes()
|
||||
outBuf := buffer.ExtendHeader(recordHeaderLen + c.explicitNonceLen)
|
||||
outBuf[0] = 23
|
||||
version := *c.version
|
||||
if version == 0 {
|
||||
version = tls.VersionTLS10
|
||||
} else if version == tls.VersionTLS13 {
|
||||
version = tls.VersionTLS12
|
||||
}
|
||||
binary.BigEndian.PutUint16(outBuf[1:], version)
|
||||
var nonce []byte
|
||||
if c.explicitNonceLen > 0 {
|
||||
nonce = outBuf[5 : 5+c.explicitNonceLen]
|
||||
if c.explicitNonceLen < 16 {
|
||||
copy(nonce, c.halfSeq)
|
||||
} else {
|
||||
if _, err := io.ReadFull(c.rand, nonce); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(nonce) == 0 {
|
||||
nonce = c.halfSeq
|
||||
}
|
||||
if *c.version == tls.VersionTLS13 {
|
||||
buffer.FreeBytes()[0] = 23
|
||||
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen+1+c.cipher.Overhead()))
|
||||
c.cipher.Seal(outBuf, nonce, outBuf[recordHeaderLen:recordHeaderLen+c.explicitNonceLen+dataLen+1], outBuf[:recordHeaderLen])
|
||||
buffer.Extend(1 + c.cipher.Overhead())
|
||||
} else {
|
||||
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen))
|
||||
additionalData := append(c.halfScratchBuf[:0], c.halfSeq...)
|
||||
additionalData = append(additionalData, outBuf[:recordHeaderLen]...)
|
||||
c.cipher.Seal(outBuf, nonce, dataBytes, additionalData)
|
||||
buffer.Extend(c.cipher.Overhead())
|
||||
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen+c.explicitNonceLen+c.cipher.Overhead()))
|
||||
}
|
||||
incSeq(c.halfPtr)
|
||||
log.Trace("badtls write ", buffer.Len())
|
||||
return c.writer.WriteBuffer(buffer)
|
||||
}
|
||||
|
||||
func (c *Conn) FrontHeadroom() int {
|
||||
return recordHeaderLen + c.explicitNonceLen
|
||||
}
|
||||
|
||||
func (c *Conn) RearHeadroom() int {
|
||||
return 1 + c.cipher.Overhead()
|
||||
}
|
||||
|
||||
func (c *Conn) WriterMTU() int {
|
||||
return maxPlaintext
|
||||
}
|
||||
|
||||
func (c *Conn) Upstream() any {
|
||||
return c.Conn
|
||||
}
|
||||
|
||||
func (c *Conn) UpstreamWriter() any {
|
||||
return c.NetConn()
|
||||
}
|
||||
14
common/badtls/badtls_stub.go
Normal file
14
common/badtls/badtls_stub.go
Normal file
@@ -0,0 +1,14 @@
|
||||
//go:build !go1.19 || go1.21
|
||||
|
||||
package badtls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"os"
|
||||
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
|
||||
func Create(conn *tls.Conn) (aTLS.Conn, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
22
common/badtls/link.go
Normal file
22
common/badtls/link.go
Normal file
@@ -0,0 +1,22 @@
|
||||
//go:build go1.20 && !go.1.21
|
||||
|
||||
package badtls
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
maxPlaintext = 16384 // maximum plaintext payload length
|
||||
recordHeaderLen = 5 // record header length
|
||||
)
|
||||
|
||||
//go:linkname errShutdown crypto/tls.errShutdown
|
||||
var errShutdown error
|
||||
|
||||
//go:linkname incSeq crypto/tls.(*halfConn).incSeq
|
||||
func incSeq(conn uintptr)
|
||||
|
||||
//go:linkname valueInterface reflect.valueInterface
|
||||
func valueInterface(v reflect.Value, safe bool) any
|
||||
124
common/badversion/version.go
Normal file
124
common/badversion/version.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package badversion
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
type Version struct {
|
||||
Major int
|
||||
Minor int
|
||||
Patch int
|
||||
Commit string
|
||||
PreReleaseIdentifier string
|
||||
PreReleaseVersion int
|
||||
}
|
||||
|
||||
func (v Version) After(anotherVersion Version) bool {
|
||||
if v.Major > anotherVersion.Major {
|
||||
return true
|
||||
} else if v.Major < anotherVersion.Major {
|
||||
return false
|
||||
}
|
||||
if v.Minor > anotherVersion.Minor {
|
||||
return true
|
||||
} else if v.Minor < anotherVersion.Minor {
|
||||
return false
|
||||
}
|
||||
if v.Patch > anotherVersion.Patch {
|
||||
return true
|
||||
} else if v.Patch < anotherVersion.Patch {
|
||||
return false
|
||||
}
|
||||
if v.PreReleaseIdentifier == "" && anotherVersion.PreReleaseIdentifier != "" {
|
||||
return true
|
||||
} else if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier == "" {
|
||||
return false
|
||||
}
|
||||
if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier != "" {
|
||||
if v.PreReleaseIdentifier == anotherVersion.PreReleaseIdentifier {
|
||||
if v.PreReleaseVersion > anotherVersion.PreReleaseVersion {
|
||||
return true
|
||||
} else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion {
|
||||
return false
|
||||
}
|
||||
} else if v.PreReleaseIdentifier == "rc" && anotherVersion.PreReleaseIdentifier == "beta" {
|
||||
return true
|
||||
} else if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "rc" {
|
||||
return false
|
||||
} else if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "alpha" {
|
||||
return true
|
||||
} else if v.PreReleaseIdentifier == "alpha" && anotherVersion.PreReleaseIdentifier == "beta" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (v Version) VersionString() string {
|
||||
return F.ToString(v.Major, ".", v.Minor, ".", v.Patch)
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
version := F.ToString(v.Major, ".", v.Minor, ".", v.Patch)
|
||||
if v.PreReleaseIdentifier != "" {
|
||||
version = F.ToString(version, "-", v.PreReleaseIdentifier, ".", v.PreReleaseVersion)
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
func (v Version) BadString() string {
|
||||
version := F.ToString(v.Major, ".", v.Minor)
|
||||
if v.Patch > 0 {
|
||||
version = F.ToString(version, ".", v.Patch)
|
||||
}
|
||||
if v.PreReleaseIdentifier != "" {
|
||||
version = F.ToString(version, "-", v.PreReleaseIdentifier)
|
||||
if v.PreReleaseVersion > 0 {
|
||||
version = F.ToString(version, v.PreReleaseVersion)
|
||||
}
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
func Parse(versionName string) (version Version) {
|
||||
if strings.HasPrefix(versionName, "v") {
|
||||
versionName = versionName[1:]
|
||||
}
|
||||
if strings.Contains(versionName, "-") {
|
||||
parts := strings.Split(versionName, "-")
|
||||
versionName = parts[0]
|
||||
identifier := parts[1]
|
||||
if strings.Contains(identifier, ".") {
|
||||
identifierParts := strings.Split(identifier, ".")
|
||||
version.PreReleaseIdentifier = identifierParts[0]
|
||||
if len(identifierParts) >= 2 {
|
||||
version.PreReleaseVersion, _ = strconv.Atoi(identifierParts[1])
|
||||
}
|
||||
} else {
|
||||
if strings.HasPrefix(identifier, "alpha") {
|
||||
version.PreReleaseIdentifier = "alpha"
|
||||
version.PreReleaseVersion, _ = strconv.Atoi(identifier[5:])
|
||||
} else if strings.HasPrefix(identifier, "beta") {
|
||||
version.PreReleaseIdentifier = "beta"
|
||||
version.PreReleaseVersion, _ = strconv.Atoi(identifier[4:])
|
||||
} else {
|
||||
version.Commit = identifier
|
||||
}
|
||||
}
|
||||
}
|
||||
versionElements := strings.Split(versionName, ".")
|
||||
versionLen := len(versionElements)
|
||||
if versionLen >= 1 {
|
||||
version.Major, _ = strconv.Atoi(versionElements[0])
|
||||
}
|
||||
if versionLen >= 2 {
|
||||
version.Minor, _ = strconv.Atoi(versionElements[1])
|
||||
}
|
||||
if versionLen >= 3 {
|
||||
version.Patch, _ = strconv.Atoi(versionElements[2])
|
||||
}
|
||||
return
|
||||
}
|
||||
17
common/badversion/version_json.go
Normal file
17
common/badversion/version_json.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package badversion
|
||||
|
||||
import "github.com/sagernet/sing-box/common/json"
|
||||
|
||||
func (v Version) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.String())
|
||||
}
|
||||
|
||||
func (v *Version) UnmarshalJSON(data []byte) error {
|
||||
var version string
|
||||
err := json.Unmarshal(data, &version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = Parse(version)
|
||||
return nil
|
||||
}
|
||||
18
common/badversion/version_test.go
Normal file
18
common/badversion/version_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package badversion
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCompareVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
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.True(t, Parse("1.3.0").After(Parse("1.3-beta1")))
|
||||
require.True(t, Parse("1.3.0").After(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.1").After(Parse("1.3.0")))
|
||||
require.True(t, Parse("1.4").After(Parse("1.3")))
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package canceler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Instance struct {
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
timer *time.Timer
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func New(ctx context.Context, cancelFunc context.CancelFunc, timeout time.Duration) *Instance {
|
||||
instance := &Instance{
|
||||
ctx,
|
||||
cancelFunc,
|
||||
time.NewTimer(timeout),
|
||||
timeout,
|
||||
}
|
||||
go instance.wait()
|
||||
return instance
|
||||
}
|
||||
|
||||
func (i *Instance) Update() bool {
|
||||
if !i.timer.Stop() {
|
||||
return false
|
||||
}
|
||||
if !i.timer.Reset(i.timeout) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (i *Instance) wait() {
|
||||
select {
|
||||
case <-i.timer.C:
|
||||
case <-i.ctx.Done():
|
||||
}
|
||||
i.Close()
|
||||
}
|
||||
|
||||
func (i *Instance) Close() error {
|
||||
i.timer.Stop()
|
||||
i.cancelFunc()
|
||||
return nil
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package canceler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type PacketConn struct {
|
||||
N.PacketConn
|
||||
instance *Instance
|
||||
}
|
||||
|
||||
func NewPacketConn(ctx context.Context, conn N.PacketConn, timeout time.Duration) (context.Context, N.PacketConn) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
instance := New(ctx, cancel, timeout)
|
||||
return ctx, &PacketConn{conn, instance}
|
||||
}
|
||||
|
||||
func (c *PacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||
destination, err = c.PacketConn.ReadPacket(buffer)
|
||||
if err == nil {
|
||||
c.instance.Update()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *PacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
err := c.PacketConn.WritePacket(buffer, destination)
|
||||
if err == nil {
|
||||
c.instance.Update()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *PacketConn) Close() error {
|
||||
return common.Close(
|
||||
c.PacketConn,
|
||||
c.instance,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *PacketConn) Upstream() any {
|
||||
return c.PacketConn
|
||||
}
|
||||
54
common/conntrack/conn.go
Normal file
54
common/conntrack/conn.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package conntrack
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
net.Conn
|
||||
element *list.Element[io.Closer]
|
||||
}
|
||||
|
||||
func NewConn(conn net.Conn) (net.Conn, error) {
|
||||
connAccess.Lock()
|
||||
element := openConnection.PushBack(conn)
|
||||
connAccess.Unlock()
|
||||
if KillerEnabled {
|
||||
err := KillerCheck()
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &Conn{
|
||||
Conn: conn,
|
||||
element: element,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
if c.element.Value != nil {
|
||||
connAccess.Lock()
|
||||
if c.element.Value != nil {
|
||||
openConnection.Remove(c.element)
|
||||
c.element.Value = nil
|
||||
}
|
||||
connAccess.Unlock()
|
||||
}
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c *Conn) Upstream() any {
|
||||
return c.Conn
|
||||
}
|
||||
|
||||
func (c *Conn) ReaderReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Conn) WriterReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
35
common/conntrack/killer.go
Normal file
35
common/conntrack/killer.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package conntrack
|
||||
|
||||
import (
|
||||
runtimeDebug "runtime/debug"
|
||||
"time"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/memory"
|
||||
)
|
||||
|
||||
var (
|
||||
KillerEnabled bool
|
||||
MemoryLimit uint64
|
||||
killerLastCheck time.Time
|
||||
)
|
||||
|
||||
func KillerCheck() error {
|
||||
if !KillerEnabled {
|
||||
return nil
|
||||
}
|
||||
nowTime := time.Now()
|
||||
if nowTime.Sub(killerLastCheck) < 3*time.Second {
|
||||
return nil
|
||||
}
|
||||
killerLastCheck = nowTime
|
||||
if memory.Total() > MemoryLimit {
|
||||
Close()
|
||||
go func() {
|
||||
time.Sleep(time.Second)
|
||||
runtimeDebug.FreeOSMemory()
|
||||
}()
|
||||
return E.New("out of memory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
55
common/conntrack/packet_conn.go
Normal file
55
common/conntrack/packet_conn.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package conntrack
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
)
|
||||
|
||||
type PacketConn struct {
|
||||
net.PacketConn
|
||||
element *list.Element[io.Closer]
|
||||
}
|
||||
|
||||
func NewPacketConn(conn net.PacketConn) (net.PacketConn, error) {
|
||||
connAccess.Lock()
|
||||
element := openConnection.PushBack(conn)
|
||||
connAccess.Unlock()
|
||||
if KillerEnabled {
|
||||
err := KillerCheck()
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &PacketConn{
|
||||
PacketConn: conn,
|
||||
element: element,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *PacketConn) Close() error {
|
||||
if c.element.Value != nil {
|
||||
connAccess.Lock()
|
||||
if c.element.Value != nil {
|
||||
openConnection.Remove(c.element)
|
||||
c.element.Value = nil
|
||||
}
|
||||
connAccess.Unlock()
|
||||
}
|
||||
return c.PacketConn.Close()
|
||||
}
|
||||
|
||||
func (c *PacketConn) Upstream() any {
|
||||
return bufio.NewPacketConn(c.PacketConn)
|
||||
}
|
||||
|
||||
func (c *PacketConn) ReaderReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *PacketConn) WriterReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
47
common/conntrack/track.go
Normal file
47
common/conntrack/track.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package conntrack
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
)
|
||||
|
||||
var (
|
||||
connAccess sync.RWMutex
|
||||
openConnection list.List[io.Closer]
|
||||
)
|
||||
|
||||
func Count() int {
|
||||
if !Enabled {
|
||||
return 0
|
||||
}
|
||||
return openConnection.Len()
|
||||
}
|
||||
|
||||
func List() []io.Closer {
|
||||
if !Enabled {
|
||||
return nil
|
||||
}
|
||||
connAccess.RLock()
|
||||
defer connAccess.RUnlock()
|
||||
connList := make([]io.Closer, 0, openConnection.Len())
|
||||
for element := openConnection.Front(); element != nil; element = element.Next() {
|
||||
connList = append(connList, element.Value)
|
||||
}
|
||||
return connList
|
||||
}
|
||||
|
||||
func Close() {
|
||||
if !Enabled {
|
||||
return
|
||||
}
|
||||
connAccess.Lock()
|
||||
defer connAccess.Unlock()
|
||||
for element := openConnection.Front(); element != nil; element = element.Next() {
|
||||
common.Close(element.Value)
|
||||
element.Value = nil
|
||||
}
|
||||
openConnection.Init()
|
||||
}
|
||||
5
common/conntrack/track_disable.go
Normal file
5
common/conntrack/track_disable.go
Normal file
@@ -0,0 +1,5 @@
|
||||
//go:build !with_conntrack
|
||||
|
||||
package conntrack
|
||||
|
||||
const Enabled = false
|
||||
5
common/conntrack/track_enable.go
Normal file
5
common/conntrack/track_enable.go
Normal file
@@ -0,0 +1,5 @@
|
||||
//go:build with_conntrack
|
||||
|
||||
package conntrack
|
||||
|
||||
const Enabled = true
|
||||
@@ -1,87 +0,0 @@
|
||||
package debugio
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type LogConn struct {
|
||||
N.ExtendedConn
|
||||
logger log.Logger
|
||||
prefix string
|
||||
}
|
||||
|
||||
func NewLogConn(conn net.Conn, logger log.Logger, prefix string) N.ExtendedConn {
|
||||
return &LogConn{bufio.NewExtendedConn(conn), logger, prefix}
|
||||
}
|
||||
|
||||
func (c *LogConn) Read(p []byte) (n int, err error) {
|
||||
n, err = c.ExtendedConn.Read(p)
|
||||
if n > 0 {
|
||||
c.logger.Debug(c.prefix, " read ", buf.EncodeHexString(p[:n]))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *LogConn) Write(p []byte) (n int, err error) {
|
||||
c.logger.Debug(c.prefix, " write ", buf.EncodeHexString(p))
|
||||
return c.ExtendedConn.Write(p)
|
||||
}
|
||||
|
||||
func (c *LogConn) ReadBuffer(buffer *buf.Buffer) error {
|
||||
err := c.ExtendedConn.ReadBuffer(buffer)
|
||||
if err == nil {
|
||||
c.logger.Debug(c.prefix, " read buffer ", buf.EncodeHexString(buffer.Bytes()))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *LogConn) WriteBuffer(buffer *buf.Buffer) error {
|
||||
c.logger.Debug(c.prefix, " write buffer ", buf.EncodeHexString(buffer.Bytes()))
|
||||
return c.ExtendedConn.WriteBuffer(buffer)
|
||||
}
|
||||
|
||||
func (c *LogConn) Upstream() any {
|
||||
return c.ExtendedConn
|
||||
}
|
||||
|
||||
type LogPacketConn struct {
|
||||
N.NetPacketConn
|
||||
logger log.Logger
|
||||
prefix string
|
||||
}
|
||||
|
||||
func NewLogPacketConn(conn net.PacketConn, logger log.Logger, prefix string) N.NetPacketConn {
|
||||
return &LogPacketConn{bufio.NewPacketConn(conn), logger, prefix}
|
||||
}
|
||||
|
||||
func (c *LogPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
n, addr, err = c.NetPacketConn.ReadFrom(p)
|
||||
if n > 0 {
|
||||
c.logger.Debug(c.prefix, " read from ", addr, " ", buf.EncodeHexString(p[:n]))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *LogPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
c.logger.Debug(c.prefix, " write to ", addr, " ", buf.EncodeHexString(p))
|
||||
return c.NetPacketConn.WriteTo(p, addr)
|
||||
}
|
||||
|
||||
func (c *LogPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||
destination, err = c.NetPacketConn.ReadPacket(buffer)
|
||||
if err == nil {
|
||||
c.logger.Debug(c.prefix, " read packet from ", destination, " ", buf.EncodeHexString(buffer.Bytes()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *LogPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
c.logger.Debug(c.prefix, " write packet to ", destination, " ", buf.EncodeHexString(buffer.Bytes()))
|
||||
return c.NetPacketConn.WritePacket(buffer, destination)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package debugio
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type RaceConn struct {
|
||||
N.ExtendedConn
|
||||
readAccess sync.Mutex
|
||||
writeAccess sync.Mutex
|
||||
}
|
||||
|
||||
func NewRaceConn(conn net.Conn) N.ExtendedConn {
|
||||
return &RaceConn{ExtendedConn: bufio.NewExtendedConn(conn)}
|
||||
}
|
||||
|
||||
func (c *RaceConn) Read(p []byte) (n int, err error) {
|
||||
c.readAccess.Lock()
|
||||
defer c.readAccess.Unlock()
|
||||
return c.ExtendedConn.Read(p)
|
||||
}
|
||||
|
||||
func (c *RaceConn) Write(p []byte) (n int, err error) {
|
||||
c.writeAccess.Lock()
|
||||
defer c.writeAccess.Unlock()
|
||||
return c.ExtendedConn.Write(p)
|
||||
}
|
||||
|
||||
func (c *RaceConn) ReadBuffer(buffer *buf.Buffer) error {
|
||||
c.readAccess.Lock()
|
||||
defer c.readAccess.Unlock()
|
||||
return c.ExtendedConn.ReadBuffer(buffer)
|
||||
}
|
||||
|
||||
func (c *RaceConn) WriteBuffer(buffer *buf.Buffer) error {
|
||||
c.writeAccess.Lock()
|
||||
defer c.writeAccess.Unlock()
|
||||
return c.ExtendedConn.WriteBuffer(buffer)
|
||||
}
|
||||
|
||||
func (c *RaceConn) Upstream() any {
|
||||
return c.ExtendedConn
|
||||
}
|
||||
@@ -6,85 +6,42 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/warning"
|
||||
"github.com/sagernet/sing-box/common/conntrack"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"github.com/database64128/tfo-go"
|
||||
)
|
||||
|
||||
var warnBindInterfaceOnUnsupportedPlatform = warning.New(
|
||||
func() bool {
|
||||
return !(C.IsLinux || C.IsWindows || C.IsDarwin)
|
||||
},
|
||||
"outbound option `bind_interface` is only supported on Linux and Windows",
|
||||
)
|
||||
|
||||
var warnRoutingMarkOnUnsupportedPlatform = warning.New(
|
||||
func() bool {
|
||||
return !C.IsLinux
|
||||
},
|
||||
"outbound option `routing_mark` is only supported on Linux",
|
||||
)
|
||||
|
||||
var warnReuseAdderOnUnsupportedPlatform = warning.New(
|
||||
func() bool {
|
||||
return !(C.IsDarwin || C.IsDragonfly || C.IsFreebsd || C.IsLinux || C.IsNetbsd || C.IsOpenbsd || C.IsSolaris || C.IsWindows)
|
||||
},
|
||||
"outbound option `reuse_addr` is unsupported on current platform",
|
||||
)
|
||||
|
||||
var warnProtectPathOnNonAndroid = warning.New(
|
||||
func() bool {
|
||||
return !C.IsAndroid
|
||||
},
|
||||
"outbound option `protect_path` is only supported on Android",
|
||||
)
|
||||
|
||||
var warnTFOOnUnsupportedPlatform = warning.New(
|
||||
func() bool {
|
||||
return !(C.IsDarwin || C.IsFreebsd || C.IsLinux || C.IsWindows)
|
||||
},
|
||||
"outbound option `tcp_fast_open` is unsupported on current platform",
|
||||
)
|
||||
|
||||
type DefaultDialer struct {
|
||||
tfo.Dialer
|
||||
net.ListenConfig
|
||||
dialer4 tcpDialer
|
||||
dialer6 tcpDialer
|
||||
udpDialer4 net.Dialer
|
||||
udpDialer6 net.Dialer
|
||||
udpListener net.ListenConfig
|
||||
udpAddr4 string
|
||||
udpAddr6 string
|
||||
}
|
||||
|
||||
func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDialer {
|
||||
func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDialer, error) {
|
||||
var dialer net.Dialer
|
||||
var listener net.ListenConfig
|
||||
if options.BindInterface != "" {
|
||||
warnBindInterfaceOnUnsupportedPlatform.Check()
|
||||
bindFunc := control.BindToInterface(router.InterfaceBindManager(), options.BindInterface)
|
||||
bindFunc := control.BindToInterface(router.InterfaceFinder(), options.BindInterface, -1)
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
} else if router.AutoDetectInterface() {
|
||||
if C.IsWindows {
|
||||
bindFunc := control.BindToInterfaceIndexFunc(func() int {
|
||||
return router.InterfaceMonitor().DefaultInterfaceIndex()
|
||||
})
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
} else {
|
||||
bindFunc := control.BindToInterfaceFunc(router.InterfaceBindManager(), func() string {
|
||||
return router.InterfaceMonitor().DefaultInterfaceName()
|
||||
})
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
}
|
||||
bindFunc := router.AutoDetectInterfaceFunc()
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
} else if router.DefaultInterface() != "" {
|
||||
bindFunc := control.BindToInterface(router.InterfaceBindManager(), router.DefaultInterface())
|
||||
bindFunc := control.BindToInterface(router.InterfaceFinder(), router.DefaultInterface(), -1)
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
}
|
||||
if options.RoutingMark != 0 {
|
||||
warnRoutingMarkOnUnsupportedPlatform.Check()
|
||||
dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))
|
||||
listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark))
|
||||
} else if router.DefaultMark() != 0 {
|
||||
@@ -92,11 +49,9 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
|
||||
listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark()))
|
||||
}
|
||||
if options.ReuseAddr {
|
||||
warnReuseAdderOnUnsupportedPlatform.Check()
|
||||
listener.Control = control.Append(listener.Control, control.ReuseAddr())
|
||||
}
|
||||
if options.ProtectPath != "" {
|
||||
warnProtectPathOnNonAndroid.Check()
|
||||
dialer.Control = control.Append(dialer.Control, control.ProtectPath(options.ProtectPath))
|
||||
listener.Control = control.Append(listener.Control, control.ProtectPath(options.ProtectPath))
|
||||
}
|
||||
@@ -105,20 +60,102 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
|
||||
} else {
|
||||
dialer.Timeout = C.TCPTimeout
|
||||
}
|
||||
if options.TCPFastOpen {
|
||||
warnTFOOnUnsupportedPlatform.Check()
|
||||
var udpFragment bool
|
||||
if options.UDPFragment != nil {
|
||||
udpFragment = *options.UDPFragment
|
||||
} else {
|
||||
udpFragment = options.UDPFragmentDefault
|
||||
}
|
||||
return &DefaultDialer{tfo.Dialer{Dialer: dialer, DisableTFO: !options.TCPFastOpen}, listener}
|
||||
if !udpFragment {
|
||||
dialer.Control = control.Append(dialer.Control, control.DisableUDPFragment())
|
||||
listener.Control = control.Append(listener.Control, control.DisableUDPFragment())
|
||||
}
|
||||
var (
|
||||
dialer4 = dialer
|
||||
udpDialer4 = dialer
|
||||
udpAddr4 string
|
||||
)
|
||||
if options.Inet4BindAddress != nil {
|
||||
bindAddr := options.Inet4BindAddress.Build()
|
||||
dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
|
||||
udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
|
||||
udpAddr4 = M.SocksaddrFrom(bindAddr, 0).String()
|
||||
}
|
||||
var (
|
||||
dialer6 = dialer
|
||||
udpDialer6 = dialer
|
||||
udpAddr6 string
|
||||
)
|
||||
if options.Inet6BindAddress != nil {
|
||||
bindAddr := options.Inet6BindAddress.Build()
|
||||
dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
|
||||
udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
|
||||
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
|
||||
}
|
||||
if options.TCPMultiPath {
|
||||
if !go121Available {
|
||||
return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
|
||||
}
|
||||
setMultiPathTCP(&dialer4)
|
||||
}
|
||||
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tcpDialer6, err := newTCPDialer(dialer6, options.TCPFastOpen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DefaultDialer{
|
||||
tcpDialer4,
|
||||
tcpDialer6,
|
||||
udpDialer4,
|
||||
udpDialer6,
|
||||
listener,
|
||||
udpAddr4,
|
||||
udpAddr6,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
|
||||
return d.Dialer.DialContext(ctx, network, address.Unwrap().String())
|
||||
if !address.IsValid() {
|
||||
return nil, E.New("invalid address")
|
||||
}
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkUDP:
|
||||
if !address.IsIPv6() {
|
||||
return trackConn(d.udpDialer4.DialContext(ctx, network, address.String()))
|
||||
} else {
|
||||
return trackConn(d.udpDialer6.DialContext(ctx, network, address.String()))
|
||||
}
|
||||
}
|
||||
if !address.IsIPv6() {
|
||||
return trackConn(DialSlowContext(&d.dialer4, ctx, network, address))
|
||||
} else {
|
||||
return trackConn(DialSlowContext(&d.dialer6, ctx, network, address))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
return d.ListenConfig.ListenPacket(ctx, N.NetworkUDP, "")
|
||||
if destination.IsIPv6() {
|
||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6))
|
||||
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4))
|
||||
} else {
|
||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DefaultDialer) Upstream() any {
|
||||
return &d.Dialer
|
||||
func trackConn(conn net.Conn, err error) (net.Conn, error) {
|
||||
if !conntrack.Enabled || err != nil {
|
||||
return conn, err
|
||||
}
|
||||
return conntrack.NewConn(conn)
|
||||
}
|
||||
|
||||
func trackPacketConn(conn net.PacketConn, err error) (net.PacketConn, error) {
|
||||
if !conntrack.Enabled || err != nil {
|
||||
return conn, err
|
||||
}
|
||||
return conntrack.NewPacketConn(conn)
|
||||
}
|
||||
|
||||
15
common/dialer/default_go1.20.go
Normal file
15
common/dialer/default_go1.20.go
Normal file
@@ -0,0 +1,15 @@
|
||||
//go:build go1.20
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/tfo-go"
|
||||
)
|
||||
|
||||
type tcpDialer = tfo.Dialer
|
||||
|
||||
func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) {
|
||||
return tfo.Dialer{Dialer: dialer, DisableTFO: !tfoEnabled}, nil
|
||||
}
|
||||
11
common/dialer/default_go1.21.go
Normal file
11
common/dialer/default_go1.21.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build go1.21
|
||||
|
||||
package dialer
|
||||
|
||||
import "net"
|
||||
|
||||
const go121Available = true
|
||||
|
||||
func setMultiPathTCP(dialer *net.Dialer) {
|
||||
dialer.SetMultipathTCP(true)
|
||||
}
|
||||
18
common/dialer/default_nongo1.20.go
Normal file
18
common/dialer/default_nongo1.20.go
Normal file
@@ -0,0 +1,18 @@
|
||||
//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
|
||||
}
|
||||
12
common/dialer/default_nongo1.21.go
Normal file
12
common/dialer/default_nongo1.21.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build !go1.21
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
const go121Available = false
|
||||
|
||||
func setMultiPathTCP(dialer *net.Dialer) {
|
||||
}
|
||||
@@ -6,22 +6,35 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-dns"
|
||||
"github.com/sagernet/sing/common"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func New(router adapter.Router, options option.DialerOptions) N.Dialer {
|
||||
if options.Detour == "" {
|
||||
return NewDefault(router, options)
|
||||
} else {
|
||||
return NewDetour(router, options.Detour)
|
||||
}
|
||||
func MustNew(router adapter.Router, options option.DialerOptions) N.Dialer {
|
||||
return common.Must1(New(router, options))
|
||||
}
|
||||
|
||||
func NewOutbound(router adapter.Router, options option.OutboundDialerOptions) N.Dialer {
|
||||
dialer := New(router, options.DialerOptions)
|
||||
func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error) {
|
||||
var (
|
||||
dialer N.Dialer
|
||||
err error
|
||||
)
|
||||
if options.Detour == "" {
|
||||
dialer, err = NewDefault(router, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
dialer = NewDetour(router, options.Detour)
|
||||
}
|
||||
domainStrategy := dns.DomainStrategy(options.DomainStrategy)
|
||||
if domainStrategy != dns.DomainStrategyAsIS || options.Detour == "" {
|
||||
dialer = NewResolveDialer(router, dialer, domainStrategy, time.Duration(options.FallbackDelay))
|
||||
dialer = NewResolveDialer(
|
||||
router,
|
||||
dialer,
|
||||
options.Detour == "" && !options.TCPFastOpen,
|
||||
domainStrategy,
|
||||
time.Duration(options.FallbackDelay))
|
||||
}
|
||||
return dialer
|
||||
return dialer, nil
|
||||
}
|
||||
|
||||
@@ -9,20 +9,23 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-dns"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type ResolveDialer struct {
|
||||
dialer N.Dialer
|
||||
parallel bool
|
||||
router adapter.Router
|
||||
strategy dns.DomainStrategy
|
||||
fallbackDelay time.Duration
|
||||
}
|
||||
|
||||
func NewResolveDialer(router adapter.Router, dialer N.Dialer, strategy dns.DomainStrategy, fallbackDelay time.Duration) *ResolveDialer {
|
||||
func NewResolveDialer(router adapter.Router, dialer N.Dialer, parallel bool, strategy dns.DomainStrategy, fallbackDelay time.Duration) *ResolveDialer {
|
||||
return &ResolveDialer{
|
||||
dialer,
|
||||
parallel,
|
||||
router,
|
||||
strategy,
|
||||
fallbackDelay,
|
||||
@@ -33,7 +36,7 @@ func (d *ResolveDialer) DialContext(ctx context.Context, network string, destina
|
||||
if !destination.IsFqdn() {
|
||||
return d.dialer.DialContext(ctx, network, destination)
|
||||
}
|
||||
ctx, metadata := adapter.AppendContext(ctx)
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||
metadata.Destination = destination
|
||||
metadata.Domain = ""
|
||||
@@ -47,14 +50,18 @@ func (d *ResolveDialer) DialContext(ctx context.Context, network string, destina
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, d.fallbackDelay)
|
||||
if d.parallel {
|
||||
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, d.fallbackDelay)
|
||||
} else {
|
||||
return N.DialSerial(ctx, d.dialer, network, destination, addresses)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *ResolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
if !destination.IsFqdn() || destination.Fqdn == "" {
|
||||
if !destination.IsFqdn() {
|
||||
return d.dialer.ListenPacket(ctx, destination)
|
||||
}
|
||||
ctx, metadata := adapter.AppendContext(ctx)
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||
metadata.Destination = destination
|
||||
metadata.Domain = ""
|
||||
@@ -68,11 +75,11 @@ func (d *ResolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := N.ListenSerial(ctx, d.dialer, destination, addresses)
|
||||
conn, destinationAddress, err := N.ListenSerial(ctx, d.dialer, destination, addresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewResolvePacketConn(ctx, d.router, d.strategy, conn), nil
|
||||
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
||||
}
|
||||
|
||||
func (d *ResolveDialer) Upstream() any {
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-dns"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func NewResolvePacketConn(ctx context.Context, router adapter.Router, strategy dns.DomainStrategy, conn net.PacketConn) N.NetPacketConn {
|
||||
if udpConn, ok := conn.(*net.UDPConn); ok {
|
||||
return &ResolveUDPConn{udpConn, ctx, router, strategy}
|
||||
} else {
|
||||
return &ResolvePacketConn{conn, ctx, router, strategy}
|
||||
}
|
||||
}
|
||||
|
||||
type ResolveUDPConn struct {
|
||||
*net.UDPConn
|
||||
ctx context.Context
|
||||
router adapter.Router
|
||||
strategy dns.DomainStrategy
|
||||
}
|
||||
|
||||
func (w *ResolveUDPConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) {
|
||||
n, addr, err := w.ReadFromUDPAddrPort(buffer.FreeBytes())
|
||||
if err != nil {
|
||||
return M.Socksaddr{}, err
|
||||
}
|
||||
buffer.Truncate(n)
|
||||
return M.SocksaddrFromNetIP(addr), nil
|
||||
}
|
||||
|
||||
func (w *ResolveUDPConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
defer buffer.Release()
|
||||
if destination.IsFqdn() {
|
||||
addresses, err := w.router.Lookup(w.ctx, destination.Fqdn, w.strategy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return common.Error(w.UDPConn.WriteToUDPAddrPort(buffer.Bytes(), M.SocksaddrFrom(addresses[0], destination.Port).AddrPort()))
|
||||
}
|
||||
return common.Error(w.UDPConn.WriteToUDPAddrPort(buffer.Bytes(), destination.AddrPort()))
|
||||
}
|
||||
|
||||
func (w *ResolveUDPConn) Upstream() any {
|
||||
return w.UDPConn
|
||||
}
|
||||
|
||||
type ResolvePacketConn struct {
|
||||
net.PacketConn
|
||||
ctx context.Context
|
||||
router adapter.Router
|
||||
strategy dns.DomainStrategy
|
||||
}
|
||||
|
||||
func (w *ResolvePacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) {
|
||||
_, addr, err := buffer.ReadPacketFrom(w)
|
||||
if err != nil {
|
||||
return M.Socksaddr{}, err
|
||||
}
|
||||
return M.SocksaddrFromNet(addr), err
|
||||
}
|
||||
|
||||
func (w *ResolvePacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
defer buffer.Release()
|
||||
if destination.IsFqdn() {
|
||||
addresses, err := w.router.Lookup(w.ctx, destination.Fqdn, w.strategy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return common.Error(w.WriteTo(buffer.Bytes(), M.SocksaddrFrom(addresses[0], destination.Port).UDPAddr()))
|
||||
}
|
||||
return common.Error(w.WriteTo(buffer.Bytes(), destination.UDPAddr()))
|
||||
}
|
||||
|
||||
func (w *ResolvePacketConn) Upstream() any {
|
||||
return w.PacketConn
|
||||
}
|
||||
158
common/dialer/tfo.go
Normal file
158
common/dialer/tfo.go
Normal file
@@ -0,0 +1,158 @@
|
||||
//go:build go1.20
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/tfo-go"
|
||||
)
|
||||
|
||||
type slowOpenConn struct {
|
||||
dialer *tfo.Dialer
|
||||
ctx context.Context
|
||||
network string
|
||||
destination M.Socksaddr
|
||||
conn net.Conn
|
||||
create chan struct{}
|
||||
access sync.Mutex
|
||||
err error
|
||||
}
|
||||
|
||||
func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
if dialer.DisableTFO || N.NetworkName(network) != N.NetworkTCP {
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkTCP, N.NetworkUDP:
|
||||
return dialer.Dialer.DialContext(ctx, network, destination.String())
|
||||
default:
|
||||
return dialer.Dialer.DialContext(ctx, network, destination.AddrString())
|
||||
}
|
||||
}
|
||||
return &slowOpenConn{
|
||||
dialer: dialer,
|
||||
ctx: ctx,
|
||||
network: network,
|
||||
destination: destination,
|
||||
create: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) Read(b []byte) (n int, err error) {
|
||||
if c.conn == nil {
|
||||
select {
|
||||
case <-c.create:
|
||||
if c.err != nil {
|
||||
return 0, c.err
|
||||
}
|
||||
case <-c.ctx.Done():
|
||||
return 0, c.ctx.Err()
|
||||
}
|
||||
}
|
||||
return c.conn.Read(b)
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
||||
if c.conn != nil {
|
||||
return c.conn.Write(b)
|
||||
}
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
select {
|
||||
case <-c.create:
|
||||
if c.err != nil {
|
||||
return 0, c.err
|
||||
}
|
||||
return c.conn.Write(b)
|
||||
default:
|
||||
}
|
||||
c.conn, err = c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
|
||||
if err != nil {
|
||||
c.conn = nil
|
||||
c.err = E.Cause(err, "dial tcp fast open")
|
||||
}
|
||||
close(c.create)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) Close() error {
|
||||
return common.Close(c.conn)
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) LocalAddr() net.Addr {
|
||||
if c.conn == nil {
|
||||
return M.Socksaddr{}
|
||||
}
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) RemoteAddr() net.Addr {
|
||||
if c.conn == nil {
|
||||
return M.Socksaddr{}
|
||||
}
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) SetDeadline(t time.Time) error {
|
||||
if c.conn == nil {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
return c.conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) SetReadDeadline(t time.Time) error {
|
||||
if c.conn == nil {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
return c.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) SetWriteDeadline(t time.Time) error {
|
||||
if c.conn == nil {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
return c.conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) Upstream() any {
|
||||
return c.conn
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) ReaderReplaceable() bool {
|
||||
return c.conn != nil
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) WriterReplaceable() bool {
|
||||
return c.conn != nil
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) LazyHeadroom() bool {
|
||||
return c.conn == nil
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) NeedHandshake() bool {
|
||||
return c.conn == nil
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
|
||||
if c.conn == nil {
|
||||
select {
|
||||
case <-c.create:
|
||||
if c.err != nil {
|
||||
return 0, c.err
|
||||
}
|
||||
case <-c.ctx.Done():
|
||||
return 0, c.ctx.Err()
|
||||
}
|
||||
}
|
||||
return bufio.Copy(w, c.conn)
|
||||
}
|
||||
20
common/dialer/tfo_stub.go
Normal file
20
common/dialer/tfo_stub.go
Normal file
@@ -0,0 +1,20 @@
|
||||
//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())
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"net/netip"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
)
|
||||
@@ -31,8 +30,9 @@ func (r *Reader) Lookup(addr netip.Addr) string {
|
||||
if code != "" {
|
||||
return code
|
||||
}
|
||||
if !N.IsPublicAddr(addr) {
|
||||
return "private"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func (r *Reader) Close() error {
|
||||
return r.reader.Close()
|
||||
}
|
||||
|
||||
@@ -20,13 +20,11 @@ func Write(writer io.Writer, domains map[string][]Item) error {
|
||||
for _, code := range keys {
|
||||
index[code] = content.Len()
|
||||
for _, domain := range domains[code] {
|
||||
err := rw.WriteByte(content, domain.Type)
|
||||
content.WriteByte(domain.Type)
|
||||
err := rw.WriteVString(content, domain.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = rw.WriteVString(content, domain.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
158
common/humanize/bytes.go
Normal file
158
common/humanize/bytes.go
Normal file
@@ -0,0 +1,158 @@
|
||||
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)
|
||||
}
|
||||
75
common/interrupt/conn.go
Normal file
75
common/interrupt/conn.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package interrupt
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
)
|
||||
|
||||
/*type GroupedConn interface {
|
||||
MarkAsInternal()
|
||||
}
|
||||
|
||||
func MarkAsInternal(conn any) {
|
||||
if groupedConn, isGroupConn := common.Cast[GroupedConn](conn); isGroupConn {
|
||||
groupedConn.MarkAsInternal()
|
||||
}
|
||||
}*/
|
||||
|
||||
type Conn struct {
|
||||
net.Conn
|
||||
group *Group
|
||||
element *list.Element[*groupConnItem]
|
||||
}
|
||||
|
||||
/*func (c *Conn) MarkAsInternal() {
|
||||
c.element.Value.internal = true
|
||||
}*/
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
c.group.access.Lock()
|
||||
defer c.group.access.Unlock()
|
||||
c.group.connections.Remove(c.element)
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c *Conn) ReaderReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Conn) WriterReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Conn) Upstream() any {
|
||||
return c.Conn
|
||||
}
|
||||
|
||||
type PacketConn struct {
|
||||
net.PacketConn
|
||||
group *Group
|
||||
element *list.Element[*groupConnItem]
|
||||
}
|
||||
|
||||
/*func (c *PacketConn) MarkAsInternal() {
|
||||
c.element.Value.internal = true
|
||||
}*/
|
||||
|
||||
func (c *PacketConn) Close() error {
|
||||
c.group.access.Lock()
|
||||
defer c.group.access.Unlock()
|
||||
c.group.connections.Remove(c.element)
|
||||
return c.PacketConn.Close()
|
||||
}
|
||||
|
||||
func (c *PacketConn) ReaderReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *PacketConn) WriterReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *PacketConn) Upstream() any {
|
||||
return c.PacketConn
|
||||
}
|
||||
13
common/interrupt/context.go
Normal file
13
common/interrupt/context.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package interrupt
|
||||
|
||||
import "context"
|
||||
|
||||
type contextKeyIsExternalConnection struct{}
|
||||
|
||||
func ContextWithIsExternalConnection(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, contextKeyIsExternalConnection{}, true)
|
||||
}
|
||||
|
||||
func IsExternalConnectionFromContext(ctx context.Context) bool {
|
||||
return ctx.Value(contextKeyIsExternalConnection{}) != nil
|
||||
}
|
||||
52
common/interrupt/group.go
Normal file
52
common/interrupt/group.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package interrupt
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
)
|
||||
|
||||
type Group struct {
|
||||
access sync.Mutex
|
||||
connections list.List[*groupConnItem]
|
||||
}
|
||||
|
||||
type groupConnItem struct {
|
||||
conn io.Closer
|
||||
isExternal bool
|
||||
}
|
||||
|
||||
func NewGroup() *Group {
|
||||
return &Group{}
|
||||
}
|
||||
|
||||
func (g *Group) NewConn(conn net.Conn, isExternal bool) net.Conn {
|
||||
g.access.Lock()
|
||||
defer g.access.Unlock()
|
||||
item := g.connections.PushBack(&groupConnItem{conn, isExternal})
|
||||
return &Conn{Conn: conn, group: g, element: item}
|
||||
}
|
||||
|
||||
func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool) net.PacketConn {
|
||||
g.access.Lock()
|
||||
defer g.access.Unlock()
|
||||
item := g.connections.PushBack(&groupConnItem{conn, isExternal})
|
||||
return &PacketConn{PacketConn: conn, group: g, element: item}
|
||||
}
|
||||
|
||||
func (g *Group) Interrupt(interruptExternalConnections bool) {
|
||||
g.access.Lock()
|
||||
defer g.access.Unlock()
|
||||
var toDelete []*list.Element[*groupConnItem]
|
||||
for element := g.connections.Front(); element != nil; element = element.Next() {
|
||||
if !element.Value.isExternal || interruptExternalConnections {
|
||||
element.Value.conn.Close()
|
||||
toDelete = append(toDelete, element)
|
||||
}
|
||||
}
|
||||
for _, element := range toDelete {
|
||||
g.connections.Remove(element)
|
||||
}
|
||||
}
|
||||
128
common/json/comment.go
Normal file
128
common/json/comment.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
)
|
||||
|
||||
// kanged from v2ray
|
||||
|
||||
type commentFilterState = byte
|
||||
|
||||
const (
|
||||
commentFilterStateContent commentFilterState = iota
|
||||
commentFilterStateEscape
|
||||
commentFilterStateDoubleQuote
|
||||
commentFilterStateDoubleQuoteEscape
|
||||
commentFilterStateSingleQuote
|
||||
commentFilterStateSingleQuoteEscape
|
||||
commentFilterStateComment
|
||||
commentFilterStateSlash
|
||||
commentFilterStateMultilineComment
|
||||
commentFilterStateMultilineCommentStar
|
||||
)
|
||||
|
||||
type CommentFilter struct {
|
||||
br *bufio.Reader
|
||||
state commentFilterState
|
||||
}
|
||||
|
||||
func NewCommentFilter(reader io.Reader) io.Reader {
|
||||
return &CommentFilter{br: bufio.NewReader(reader)}
|
||||
}
|
||||
|
||||
func (v *CommentFilter) Read(b []byte) (int, error) {
|
||||
p := b[:0]
|
||||
for len(p) < len(b)-2 {
|
||||
x, err := v.br.ReadByte()
|
||||
if err != nil {
|
||||
if len(p) == 0 {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
switch v.state {
|
||||
case commentFilterStateContent:
|
||||
switch x {
|
||||
case '"':
|
||||
v.state = commentFilterStateDoubleQuote
|
||||
p = append(p, x)
|
||||
case '\'':
|
||||
v.state = commentFilterStateSingleQuote
|
||||
p = append(p, x)
|
||||
case '\\':
|
||||
v.state = commentFilterStateEscape
|
||||
case '#':
|
||||
v.state = commentFilterStateComment
|
||||
case '/':
|
||||
v.state = commentFilterStateSlash
|
||||
default:
|
||||
p = append(p, x)
|
||||
}
|
||||
case commentFilterStateEscape:
|
||||
p = append(p, '\\', x)
|
||||
v.state = commentFilterStateContent
|
||||
case commentFilterStateDoubleQuote:
|
||||
switch x {
|
||||
case '"':
|
||||
v.state = commentFilterStateContent
|
||||
p = append(p, x)
|
||||
case '\\':
|
||||
v.state = commentFilterStateDoubleQuoteEscape
|
||||
default:
|
||||
p = append(p, x)
|
||||
}
|
||||
case commentFilterStateDoubleQuoteEscape:
|
||||
p = append(p, '\\', x)
|
||||
v.state = commentFilterStateDoubleQuote
|
||||
case commentFilterStateSingleQuote:
|
||||
switch x {
|
||||
case '\'':
|
||||
v.state = commentFilterStateContent
|
||||
p = append(p, x)
|
||||
case '\\':
|
||||
v.state = commentFilterStateSingleQuoteEscape
|
||||
default:
|
||||
p = append(p, x)
|
||||
}
|
||||
case commentFilterStateSingleQuoteEscape:
|
||||
p = append(p, '\\', x)
|
||||
v.state = commentFilterStateSingleQuote
|
||||
case commentFilterStateComment:
|
||||
if x == '\n' {
|
||||
v.state = commentFilterStateContent
|
||||
p = append(p, '\n')
|
||||
}
|
||||
case commentFilterStateSlash:
|
||||
switch x {
|
||||
case '/':
|
||||
v.state = commentFilterStateComment
|
||||
case '*':
|
||||
v.state = commentFilterStateMultilineComment
|
||||
default:
|
||||
p = append(p, '/', x)
|
||||
}
|
||||
case commentFilterStateMultilineComment:
|
||||
switch x {
|
||||
case '*':
|
||||
v.state = commentFilterStateMultilineCommentStar
|
||||
case '\n':
|
||||
p = append(p, '\n')
|
||||
}
|
||||
case commentFilterStateMultilineCommentStar:
|
||||
switch x {
|
||||
case '/':
|
||||
v.state = commentFilterStateContent
|
||||
case '*':
|
||||
// Stay
|
||||
case '\n':
|
||||
p = append(p, '\n')
|
||||
default:
|
||||
v.state = commentFilterStateMultilineComment
|
||||
}
|
||||
default:
|
||||
panic("Unknown state.")
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
@@ -1,500 +1,42 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
"github.com/sagernet/sing-mux"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
)
|
||||
|
||||
var _ N.Dialer = (*Client)(nil)
|
||||
type Client = mux.Client
|
||||
|
||||
type Client struct {
|
||||
access sync.Mutex
|
||||
connections list.List[abstractSession]
|
||||
ctx context.Context
|
||||
dialer N.Dialer
|
||||
protocol Protocol
|
||||
maxConnections int
|
||||
minStreams int
|
||||
maxStreams int
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, dialer N.Dialer, protocol Protocol, maxConnections int, minStreams int, maxStreams int) *Client {
|
||||
return &Client{
|
||||
ctx: ctx,
|
||||
dialer: dialer,
|
||||
protocol: protocol,
|
||||
maxConnections: maxConnections,
|
||||
minStreams: minStreams,
|
||||
maxStreams: maxStreams,
|
||||
}
|
||||
}
|
||||
|
||||
func NewClientWithOptions(ctx context.Context, dialer N.Dialer, options option.MultiplexOptions) (N.Dialer, error) {
|
||||
func NewClientWithOptions(dialer N.Dialer, logger logger.Logger, options option.OutboundMultiplexOptions) (*Client, error) {
|
||||
if !options.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
if options.MaxConnections == 0 && options.MaxStreams == 0 {
|
||||
options.MinStreams = 8
|
||||
}
|
||||
protocol, err := ParseProtocol(options.Protocol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewClient(ctx, dialer, protocol, options.MaxConnections, options.MinStreams, options.MaxStreams), nil
|
||||
}
|
||||
|
||||
func (c *Client) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkTCP:
|
||||
stream, err := c.openStream()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var brutalOptions mux.BrutalOptions
|
||||
if options.Brutal != nil && options.Brutal.Enabled {
|
||||
brutalOptions = mux.BrutalOptions{
|
||||
Enabled: true,
|
||||
SendBPS: uint64(options.Brutal.UpMbps * C.MbpsToBps),
|
||||
ReceiveBPS: uint64(options.Brutal.DownMbps * C.MbpsToBps),
|
||||
}
|
||||
return &ClientConn{Conn: stream, destination: destination}, nil
|
||||
case N.NetworkUDP:
|
||||
stream, err := c.openStream()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if brutalOptions.SendBPS < mux.BrutalMinSpeedBPS {
|
||||
return nil, E.New("brutal: invalid upload speed")
|
||||
}
|
||||
return bufio.NewUnbindPacketConn(&ClientPacketConn{ExtendedConn: bufio.NewExtendedConn(stream), destination: destination}), nil
|
||||
default:
|
||||
return nil, E.Extend(N.ErrUnknownNetwork, network)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
stream, err := c.openStream()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ClientPacketAddrConn{ExtendedConn: bufio.NewExtendedConn(stream), destination: destination}, nil
|
||||
}
|
||||
|
||||
func (c *Client) openStream() (net.Conn, error) {
|
||||
var (
|
||||
session abstractSession
|
||||
stream net.Conn
|
||||
err error
|
||||
)
|
||||
for attempts := 0; attempts < 2; attempts++ {
|
||||
session, err = c.offer()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
stream, err = session.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &wrapStream{stream}, nil
|
||||
}
|
||||
|
||||
func (c *Client) offer() (abstractSession, error) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
|
||||
sessions := make([]abstractSession, 0, c.maxConnections)
|
||||
for element := c.connections.Front(); element != nil; {
|
||||
if element.Value.IsClosed() {
|
||||
nextElement := element.Next()
|
||||
c.connections.Remove(element)
|
||||
element = nextElement
|
||||
continue
|
||||
}
|
||||
sessions = append(sessions, element.Value)
|
||||
element = element.Next()
|
||||
}
|
||||
sLen := len(sessions)
|
||||
if sLen == 0 {
|
||||
return c.offerNew()
|
||||
}
|
||||
session := common.MinBy(sessions, abstractSession.NumStreams)
|
||||
numStreams := session.NumStreams()
|
||||
if numStreams == 0 {
|
||||
return session, nil
|
||||
}
|
||||
if c.maxConnections > 0 {
|
||||
if sLen >= c.maxConnections || numStreams < c.minStreams {
|
||||
return session, nil
|
||||
}
|
||||
} else {
|
||||
if c.maxStreams > 0 && numStreams < c.maxStreams {
|
||||
return session, nil
|
||||
if brutalOptions.ReceiveBPS < mux.BrutalMinSpeedBPS {
|
||||
return nil, E.New("brutal: invalid download speed")
|
||||
}
|
||||
}
|
||||
return c.offerNew()
|
||||
}
|
||||
|
||||
func (c *Client) offerNew() (abstractSession, error) {
|
||||
conn, err := c.dialer.DialContext(c.ctx, N.NetworkTCP, Destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if vectorisedWriter, isVectorised := bufio.CreateVectorisedWriter(conn); isVectorised {
|
||||
conn = &vectorisedProtocolConn{protocolConn{Conn: conn, protocol: c.protocol}, vectorisedWriter}
|
||||
} else {
|
||||
conn = &protocolConn{Conn: conn, protocol: c.protocol}
|
||||
}
|
||||
session, err := c.protocol.newClient(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.connections.PushBack(session)
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
for _, session := range c.connections.Array() {
|
||||
session.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ClientConn struct {
|
||||
net.Conn
|
||||
destination M.Socksaddr
|
||||
requestWrite bool
|
||||
responseRead bool
|
||||
}
|
||||
|
||||
func (c *ClientConn) readResponse() error {
|
||||
response, err := ReadStreamResponse(c.Conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.Status == statusError {
|
||||
return E.New("remote error: ", response.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClientConn) Read(b []byte) (n int, err error) {
|
||||
if !c.responseRead {
|
||||
err = c.readResponse()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.responseRead = true
|
||||
}
|
||||
return c.Conn.Read(b)
|
||||
}
|
||||
|
||||
func (c *ClientConn) Write(b []byte) (n int, err error) {
|
||||
if c.requestWrite {
|
||||
return c.Conn.Write(b)
|
||||
}
|
||||
request := StreamRequest{
|
||||
Network: N.NetworkTCP,
|
||||
Destination: c.destination,
|
||||
}
|
||||
_buffer := buf.StackNewSize(requestLen(request) + len(b))
|
||||
defer common.KeepAlive(_buffer)
|
||||
buffer := common.Dup(_buffer)
|
||||
defer buffer.Release()
|
||||
EncodeStreamRequest(request, buffer)
|
||||
buffer.Write(b)
|
||||
_, err = c.Conn.Write(buffer.Bytes())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.requestWrite = true
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (c *ClientConn) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
if !c.requestWrite {
|
||||
return bufio.ReadFrom0(c, r)
|
||||
}
|
||||
return bufio.Copy(c.Conn, r)
|
||||
}
|
||||
|
||||
func (c *ClientConn) WriteTo(w io.Writer) (n int64, err error) {
|
||||
if !c.responseRead {
|
||||
return bufio.WriteTo0(c, w)
|
||||
}
|
||||
return bufio.Copy(w, c.Conn)
|
||||
}
|
||||
|
||||
func (c *ClientConn) LocalAddr() net.Addr {
|
||||
return c.Conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *ClientConn) RemoteAddr() net.Addr {
|
||||
return c.destination.TCPAddr()
|
||||
}
|
||||
|
||||
func (c *ClientConn) ReaderReplaceable() bool {
|
||||
return c.responseRead
|
||||
}
|
||||
|
||||
func (c *ClientConn) WriterReplaceable() bool {
|
||||
return c.requestWrite
|
||||
}
|
||||
|
||||
func (c *ClientConn) Upstream() any {
|
||||
return c.Conn
|
||||
}
|
||||
|
||||
type ClientPacketConn struct {
|
||||
N.ExtendedConn
|
||||
destination M.Socksaddr
|
||||
requestWrite bool
|
||||
responseRead bool
|
||||
}
|
||||
|
||||
func (c *ClientPacketConn) readResponse() error {
|
||||
response, err := ReadStreamResponse(c.ExtendedConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.Status == statusError {
|
||||
return E.New("remote error: ", response.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClientPacketConn) Read(b []byte) (n int, err error) {
|
||||
if !c.responseRead {
|
||||
err = c.readResponse()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.responseRead = true
|
||||
}
|
||||
var length uint16
|
||||
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if cap(b) < int(length) {
|
||||
return 0, io.ErrShortBuffer
|
||||
}
|
||||
return io.ReadFull(c.ExtendedConn, b[:length])
|
||||
}
|
||||
|
||||
func (c *ClientPacketConn) writeRequest(payload []byte) (n int, err error) {
|
||||
request := StreamRequest{
|
||||
Network: N.NetworkUDP,
|
||||
Destination: c.destination,
|
||||
}
|
||||
rLen := requestLen(request)
|
||||
if len(payload) > 0 {
|
||||
rLen += 2 + len(payload)
|
||||
}
|
||||
_buffer := buf.StackNewSize(rLen)
|
||||
defer common.KeepAlive(_buffer)
|
||||
buffer := common.Dup(_buffer)
|
||||
defer buffer.Release()
|
||||
EncodeStreamRequest(request, buffer)
|
||||
if len(payload) > 0 {
|
||||
common.Must(
|
||||
binary.Write(buffer, binary.BigEndian, uint16(len(payload))),
|
||||
common.Error(buffer.Write(payload)),
|
||||
)
|
||||
}
|
||||
_, err = c.ExtendedConn.Write(buffer.Bytes())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.requestWrite = true
|
||||
return len(payload), nil
|
||||
}
|
||||
|
||||
func (c *ClientPacketConn) Write(b []byte) (n int, err error) {
|
||||
if !c.requestWrite {
|
||||
return c.writeRequest(b)
|
||||
}
|
||||
err = binary.Write(c.ExtendedConn, binary.BigEndian, uint16(len(b)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return c.ExtendedConn.Write(b)
|
||||
}
|
||||
|
||||
func (c *ClientPacketConn) WriteBuffer(buffer *buf.Buffer) error {
|
||||
if !c.requestWrite {
|
||||
defer buffer.Release()
|
||||
return common.Error(c.writeRequest(buffer.Bytes()))
|
||||
}
|
||||
bLen := buffer.Len()
|
||||
binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(bLen))
|
||||
return c.ExtendedConn.WriteBuffer(buffer)
|
||||
}
|
||||
|
||||
func (c *ClientPacketConn) FrontHeadroom() int {
|
||||
return 2
|
||||
}
|
||||
|
||||
func (c *ClientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
return c.WriteBuffer(buffer)
|
||||
}
|
||||
|
||||
func (c *ClientPacketConn) LocalAddr() net.Addr {
|
||||
return c.ExtendedConn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *ClientPacketConn) RemoteAddr() net.Addr {
|
||||
return c.destination.UDPAddr()
|
||||
}
|
||||
|
||||
func (c *ClientPacketConn) Upstream() any {
|
||||
return c.ExtendedConn
|
||||
}
|
||||
|
||||
var _ N.NetPacketConn = (*ClientPacketAddrConn)(nil)
|
||||
|
||||
type ClientPacketAddrConn struct {
|
||||
N.ExtendedConn
|
||||
destination M.Socksaddr
|
||||
requestWrite bool
|
||||
responseRead bool
|
||||
}
|
||||
|
||||
func (c *ClientPacketAddrConn) readResponse() error {
|
||||
response, err := ReadStreamResponse(c.ExtendedConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.Status == statusError {
|
||||
return E.New("remote error: ", response.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClientPacketAddrConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
if !c.responseRead {
|
||||
err = c.readResponse()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.responseRead = true
|
||||
}
|
||||
destination, err := M.SocksaddrSerializer.ReadAddrPort(c.ExtendedConn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
addr = destination.UDPAddr()
|
||||
var length uint16
|
||||
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if cap(p) < int(length) {
|
||||
return 0, nil, io.ErrShortBuffer
|
||||
}
|
||||
n, err = io.ReadFull(c.ExtendedConn, p[:length])
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ClientPacketAddrConn) writeRequest(payload []byte, destination M.Socksaddr) (n int, err error) {
|
||||
request := StreamRequest{
|
||||
Network: N.NetworkUDP,
|
||||
Destination: c.destination,
|
||||
PacketAddr: true,
|
||||
}
|
||||
rLen := requestLen(request)
|
||||
if len(payload) > 0 {
|
||||
rLen += M.SocksaddrSerializer.AddrPortLen(destination) + 2 + len(payload)
|
||||
}
|
||||
_buffer := buf.StackNewSize(rLen)
|
||||
defer common.KeepAlive(_buffer)
|
||||
buffer := common.Dup(_buffer)
|
||||
defer buffer.Release()
|
||||
EncodeStreamRequest(request, buffer)
|
||||
if len(payload) > 0 {
|
||||
common.Must(
|
||||
M.SocksaddrSerializer.WriteAddrPort(buffer, destination),
|
||||
binary.Write(buffer, binary.BigEndian, uint16(len(payload))),
|
||||
common.Error(buffer.Write(payload)),
|
||||
)
|
||||
}
|
||||
_, err = c.ExtendedConn.Write(buffer.Bytes())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.requestWrite = true
|
||||
return len(payload), nil
|
||||
}
|
||||
|
||||
func (c *ClientPacketAddrConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
if !c.requestWrite {
|
||||
return c.writeRequest(p, M.SocksaddrFromNet(addr))
|
||||
}
|
||||
err = M.SocksaddrSerializer.WriteAddrPort(c.ExtendedConn, M.SocksaddrFromNet(addr))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = binary.Write(c.ExtendedConn, binary.BigEndian, uint16(len(p)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return c.ExtendedConn.Write(p)
|
||||
}
|
||||
|
||||
func (c *ClientPacketAddrConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||
if !c.responseRead {
|
||||
err = c.readResponse()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.responseRead = true
|
||||
}
|
||||
destination, err = M.SocksaddrSerializer.ReadAddrPort(c.ExtendedConn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var length uint16
|
||||
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if buffer.FreeLen() < int(length) {
|
||||
return destination, io.ErrShortBuffer
|
||||
}
|
||||
_, err = io.ReadFull(c.ExtendedConn, buffer.Extend(int(length)))
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ClientPacketAddrConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
if !c.requestWrite {
|
||||
defer buffer.Release()
|
||||
return common.Error(c.writeRequest(buffer.Bytes(), destination))
|
||||
}
|
||||
bLen := buffer.Len()
|
||||
header := buf.With(buffer.ExtendHeader(M.SocksaddrSerializer.AddrPortLen(destination) + 2))
|
||||
common.Must(
|
||||
M.SocksaddrSerializer.WriteAddrPort(header, destination),
|
||||
binary.Write(header, binary.BigEndian, uint16(bLen)),
|
||||
)
|
||||
return c.ExtendedConn.WriteBuffer(buffer)
|
||||
}
|
||||
|
||||
func (c *ClientPacketAddrConn) LocalAddr() net.Addr {
|
||||
return c.ExtendedConn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *ClientPacketAddrConn) FrontHeadroom() int {
|
||||
return 2 + M.MaxSocksaddrLength
|
||||
}
|
||||
|
||||
func (c *ClientPacketAddrConn) Upstream() any {
|
||||
return c.ExtendedConn
|
||||
return mux.NewClient(mux.Options{
|
||||
Dialer: dialer,
|
||||
Logger: logger,
|
||||
Protocol: options.Protocol,
|
||||
MaxConnections: options.MaxConnections,
|
||||
MinStreams: options.MinStreams,
|
||||
MaxStreams: options.MaxStreams,
|
||||
Padding: options.Padding,
|
||||
Brutal: brutalOptions,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
"github.com/sagernet/smux"
|
||||
|
||||
"github.com/hashicorp/yamux"
|
||||
)
|
||||
|
||||
var Destination = M.Socksaddr{
|
||||
Fqdn: "sp.mux.sing-box.arpa",
|
||||
Port: 444,
|
||||
}
|
||||
|
||||
const (
|
||||
ProtocolSMux Protocol = iota
|
||||
ProtocolYAMux
|
||||
)
|
||||
|
||||
type Protocol byte
|
||||
|
||||
func ParseProtocol(name string) (Protocol, error) {
|
||||
switch name {
|
||||
case "", "smux":
|
||||
return ProtocolSMux, nil
|
||||
case "yamux":
|
||||
return ProtocolYAMux, nil
|
||||
default:
|
||||
return ProtocolYAMux, E.New("unknown multiplex protocol: ", name)
|
||||
}
|
||||
}
|
||||
|
||||
func (p Protocol) newServer(conn net.Conn) (abstractSession, error) {
|
||||
switch p {
|
||||
case ProtocolSMux:
|
||||
session, err := smux.Server(conn, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &smuxSession{session}, nil
|
||||
case ProtocolYAMux:
|
||||
return yamux.Server(conn, yaMuxConfig())
|
||||
default:
|
||||
panic("unknown protocol")
|
||||
}
|
||||
}
|
||||
|
||||
func (p Protocol) newClient(conn net.Conn) (abstractSession, error) {
|
||||
switch p {
|
||||
case ProtocolSMux:
|
||||
session, err := smux.Client(conn, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &smuxSession{session}, nil
|
||||
case ProtocolYAMux:
|
||||
return yamux.Client(conn, yaMuxConfig())
|
||||
default:
|
||||
panic("unknown protocol")
|
||||
}
|
||||
}
|
||||
|
||||
func yaMuxConfig() *yamux.Config {
|
||||
config := yamux.DefaultConfig()
|
||||
config.LogOutput = io.Discard
|
||||
config.StreamCloseTimeout = C.TCPTimeout
|
||||
config.StreamOpenTimeout = C.TCPTimeout
|
||||
return config
|
||||
}
|
||||
|
||||
func (p Protocol) String() string {
|
||||
switch p {
|
||||
case ProtocolSMux:
|
||||
return "smux"
|
||||
case ProtocolYAMux:
|
||||
return "yamux"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
version0 = 0
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
Protocol Protocol
|
||||
}
|
||||
|
||||
func ReadRequest(reader io.Reader) (*Request, error) {
|
||||
version, err := rw.ReadByte(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if version != version0 {
|
||||
return nil, E.New("unsupported version: ", version)
|
||||
}
|
||||
protocol, err := rw.ReadByte(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if protocol > byte(ProtocolYAMux) {
|
||||
return nil, E.New("unsupported protocol: ", protocol)
|
||||
}
|
||||
return &Request{Protocol: Protocol(protocol)}, nil
|
||||
}
|
||||
|
||||
func EncodeRequest(buffer *buf.Buffer, request Request) {
|
||||
buffer.WriteByte(version0)
|
||||
buffer.WriteByte(byte(request.Protocol))
|
||||
}
|
||||
|
||||
const (
|
||||
flagUDP = 1
|
||||
flagAddr = 2
|
||||
statusSuccess = 0
|
||||
statusError = 1
|
||||
)
|
||||
|
||||
type StreamRequest struct {
|
||||
Network string
|
||||
Destination M.Socksaddr
|
||||
PacketAddr bool
|
||||
}
|
||||
|
||||
func ReadStreamRequest(reader io.Reader) (*StreamRequest, error) {
|
||||
var flags uint16
|
||||
err := binary.Read(reader, binary.BigEndian, &flags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
destination, err := M.SocksaddrSerializer.ReadAddrPort(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var network string
|
||||
var udpAddr bool
|
||||
if flags&flagUDP == 0 {
|
||||
network = N.NetworkTCP
|
||||
} else {
|
||||
network = N.NetworkUDP
|
||||
udpAddr = flags&flagAddr != 0
|
||||
}
|
||||
return &StreamRequest{network, destination, udpAddr}, nil
|
||||
}
|
||||
|
||||
func requestLen(request StreamRequest) int {
|
||||
var rLen int
|
||||
rLen += 1 // version
|
||||
rLen += 2 // flags
|
||||
rLen += M.SocksaddrSerializer.AddrPortLen(request.Destination)
|
||||
return rLen
|
||||
}
|
||||
|
||||
func EncodeStreamRequest(request StreamRequest, buffer *buf.Buffer) {
|
||||
destination := request.Destination
|
||||
var flags uint16
|
||||
if request.Network == N.NetworkUDP {
|
||||
flags |= flagUDP
|
||||
}
|
||||
if request.PacketAddr {
|
||||
flags |= flagAddr
|
||||
if !destination.IsValid() {
|
||||
destination = Destination
|
||||
}
|
||||
}
|
||||
common.Must(
|
||||
binary.Write(buffer, binary.BigEndian, flags),
|
||||
M.SocksaddrSerializer.WriteAddrPort(buffer, destination),
|
||||
)
|
||||
}
|
||||
|
||||
type StreamResponse struct {
|
||||
Status uint8
|
||||
Message string
|
||||
}
|
||||
|
||||
func ReadStreamResponse(reader io.Reader) (*StreamResponse, error) {
|
||||
var response StreamResponse
|
||||
status, err := rw.ReadByte(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.Status = status
|
||||
if status == statusError {
|
||||
response.Message, err = rw.ReadVString(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
type wrapStream struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (w *wrapStream) Read(p []byte) (n int, err error) {
|
||||
n, err = w.Conn.Read(p)
|
||||
err = wrapError(err)
|
||||
return
|
||||
}
|
||||
|
||||
func (w *wrapStream) Write(p []byte) (n int, err error) {
|
||||
n, err = w.Conn.Write(p)
|
||||
err = wrapError(err)
|
||||
return
|
||||
}
|
||||
|
||||
func (w *wrapStream) WriteIsThreadUnsafe() {
|
||||
}
|
||||
|
||||
func (w *wrapStream) Upstream() any {
|
||||
return w.Conn
|
||||
}
|
||||
|
||||
func wrapError(err error) error {
|
||||
switch err {
|
||||
case yamux.ErrStreamClosed:
|
||||
return io.EOF
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
65
common/mux/router.go
Normal file
65
common/mux/router.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-mux"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
router adapter.ConnectionRouter
|
||||
service *mux.Service
|
||||
}
|
||||
|
||||
func NewRouterWithOptions(router adapter.ConnectionRouter, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouter, error) {
|
||||
if !options.Enabled {
|
||||
return router, nil
|
||||
}
|
||||
var brutalOptions mux.BrutalOptions
|
||||
if options.Brutal != nil && options.Brutal.Enabled {
|
||||
brutalOptions = mux.BrutalOptions{
|
||||
Enabled: true,
|
||||
SendBPS: uint64(options.Brutal.UpMbps * C.MbpsToBps),
|
||||
ReceiveBPS: uint64(options.Brutal.DownMbps * C.MbpsToBps),
|
||||
}
|
||||
if brutalOptions.SendBPS < mux.BrutalMinSpeedBPS {
|
||||
return nil, E.New("brutal: invalid upload speed")
|
||||
}
|
||||
if brutalOptions.ReceiveBPS < mux.BrutalMinSpeedBPS {
|
||||
return nil, E.New("brutal: invalid download speed")
|
||||
}
|
||||
}
|
||||
service, err := mux.NewService(mux.ServiceOptions{
|
||||
NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context {
|
||||
return log.ContextWithNewID(ctx)
|
||||
},
|
||||
Logger: logger,
|
||||
Handler: adapter.NewRouteContextHandler(router, logger),
|
||||
Padding: options.Padding,
|
||||
Brutal: brutalOptions,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Router{router, service}, nil
|
||||
}
|
||||
|
||||
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||
if metadata.Destination == mux.Destination {
|
||||
return r.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata))
|
||||
} else {
|
||||
return r.router.RouteConnection(ctx, conn, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
||||
return r.router.RoutePacketConnection(ctx, conn, metadata)
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
)
|
||||
|
||||
func NewConnection(ctx context.Context, router adapter.Router, errorHandler E.Handler, logger log.ContextLogger, conn net.Conn, metadata adapter.InboundContext) error {
|
||||
request, err := ReadRequest(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session, err := request.Protocol.newServer(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var stream net.Conn
|
||||
for {
|
||||
stream, err = session.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go newConnection(ctx, router, errorHandler, logger, stream, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func newConnection(ctx context.Context, router adapter.Router, errorHandler E.Handler, logger log.ContextLogger, stream net.Conn, metadata adapter.InboundContext) {
|
||||
stream = &wrapStream{stream}
|
||||
request, err := ReadStreamRequest(stream)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, err)
|
||||
return
|
||||
}
|
||||
metadata.Destination = request.Destination
|
||||
if request.Network == N.NetworkTCP {
|
||||
logger.InfoContext(ctx, "inbound multiplex connection to ", metadata.Destination)
|
||||
hErr := router.RouteConnection(ctx, &ServerConn{ExtendedConn: bufio.NewExtendedConn(stream)}, metadata)
|
||||
stream.Close()
|
||||
if hErr != nil {
|
||||
errorHandler.NewError(ctx, hErr)
|
||||
}
|
||||
} else {
|
||||
var packetConn N.PacketConn
|
||||
if !request.PacketAddr {
|
||||
logger.InfoContext(ctx, "inbound multiplex packet connection to ", metadata.Destination)
|
||||
packetConn = &ServerPacketConn{ExtendedConn: bufio.NewExtendedConn(stream), destination: request.Destination}
|
||||
} else {
|
||||
logger.InfoContext(ctx, "inbound multiplex packet connection")
|
||||
packetConn = &ServerPacketAddrConn{ExtendedConn: bufio.NewExtendedConn(stream)}
|
||||
}
|
||||
hErr := router.RoutePacketConnection(ctx, packetConn, metadata)
|
||||
stream.Close()
|
||||
if hErr != nil {
|
||||
errorHandler.NewError(ctx, hErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var _ N.HandshakeConn = (*ServerConn)(nil)
|
||||
|
||||
type ServerConn struct {
|
||||
N.ExtendedConn
|
||||
responseWrite bool
|
||||
}
|
||||
|
||||
func (c *ServerConn) HandshakeFailure(err error) error {
|
||||
errMessage := err.Error()
|
||||
_buffer := buf.StackNewSize(1 + rw.UVariantLen(uint64(len(errMessage))) + len(errMessage))
|
||||
defer common.KeepAlive(_buffer)
|
||||
buffer := common.Dup(_buffer)
|
||||
defer buffer.Release()
|
||||
common.Must(
|
||||
buffer.WriteByte(statusError),
|
||||
rw.WriteVString(_buffer, errMessage),
|
||||
)
|
||||
return c.ExtendedConn.WriteBuffer(buffer)
|
||||
}
|
||||
|
||||
func (c *ServerConn) Write(b []byte) (n int, err error) {
|
||||
if c.responseWrite {
|
||||
return c.ExtendedConn.Write(b)
|
||||
}
|
||||
_buffer := buf.StackNewSize(1 + len(b))
|
||||
defer common.KeepAlive(_buffer)
|
||||
buffer := common.Dup(_buffer)
|
||||
defer buffer.Release()
|
||||
common.Must(
|
||||
buffer.WriteByte(statusSuccess),
|
||||
common.Error(buffer.Write(b)),
|
||||
)
|
||||
_, err = c.ExtendedConn.Write(buffer.Bytes())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.responseWrite = true
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (c *ServerConn) WriteBuffer(buffer *buf.Buffer) error {
|
||||
if c.responseWrite {
|
||||
return c.ExtendedConn.WriteBuffer(buffer)
|
||||
}
|
||||
buffer.ExtendHeader(1)[0] = statusSuccess
|
||||
c.responseWrite = true
|
||||
return c.ExtendedConn.WriteBuffer(buffer)
|
||||
}
|
||||
|
||||
func (c *ServerConn) FrontHeadroom() int {
|
||||
if !c.responseWrite {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *ServerConn) Upstream() any {
|
||||
return c.ExtendedConn
|
||||
}
|
||||
|
||||
var (
|
||||
_ N.HandshakeConn = (*ServerPacketConn)(nil)
|
||||
_ N.PacketConn = (*ServerPacketConn)(nil)
|
||||
)
|
||||
|
||||
type ServerPacketConn struct {
|
||||
N.ExtendedConn
|
||||
destination M.Socksaddr
|
||||
responseWrite bool
|
||||
}
|
||||
|
||||
func (c *ServerPacketConn) HandshakeFailure(err error) error {
|
||||
errMessage := err.Error()
|
||||
_buffer := buf.StackNewSize(1 + rw.UVariantLen(uint64(len(errMessage))) + len(errMessage))
|
||||
defer common.KeepAlive(_buffer)
|
||||
buffer := common.Dup(_buffer)
|
||||
defer buffer.Release()
|
||||
common.Must(
|
||||
buffer.WriteByte(statusError),
|
||||
rw.WriteVString(_buffer, errMessage),
|
||||
)
|
||||
return c.ExtendedConn.WriteBuffer(buffer)
|
||||
}
|
||||
|
||||
func (c *ServerPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||
var length uint16
|
||||
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if buffer.FreeLen() < int(length) {
|
||||
return destination, io.ErrShortBuffer
|
||||
}
|
||||
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
destination = c.destination
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ServerPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
pLen := buffer.Len()
|
||||
common.Must(binary.Write(buf.With(buffer.ExtendHeader(2)), binary.BigEndian, uint16(pLen)))
|
||||
if !c.responseWrite {
|
||||
buffer.ExtendHeader(1)[0] = statusSuccess
|
||||
c.responseWrite = true
|
||||
}
|
||||
return c.ExtendedConn.WriteBuffer(buffer)
|
||||
}
|
||||
|
||||
func (c *ServerPacketConn) Upstream() any {
|
||||
return c.ExtendedConn
|
||||
}
|
||||
|
||||
func (c *ServerPacketConn) FrontHeadroom() int {
|
||||
if !c.responseWrite {
|
||||
return 3
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
var (
|
||||
_ N.HandshakeConn = (*ServerPacketAddrConn)(nil)
|
||||
_ N.PacketConn = (*ServerPacketAddrConn)(nil)
|
||||
)
|
||||
|
||||
type ServerPacketAddrConn struct {
|
||||
N.ExtendedConn
|
||||
responseWrite bool
|
||||
}
|
||||
|
||||
func (c *ServerPacketAddrConn) HandshakeFailure(err error) error {
|
||||
errMessage := err.Error()
|
||||
_buffer := buf.StackNewSize(1 + rw.UVariantLen(uint64(len(errMessage))) + len(errMessage))
|
||||
defer common.KeepAlive(_buffer)
|
||||
buffer := common.Dup(_buffer)
|
||||
defer buffer.Release()
|
||||
common.Must(
|
||||
buffer.WriteByte(statusError),
|
||||
rw.WriteVString(_buffer, errMessage),
|
||||
)
|
||||
return c.ExtendedConn.WriteBuffer(buffer)
|
||||
}
|
||||
|
||||
func (c *ServerPacketAddrConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||
destination, err = M.SocksaddrSerializer.ReadAddrPort(c.ExtendedConn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var length uint16
|
||||
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if buffer.FreeLen() < int(length) {
|
||||
return destination, io.ErrShortBuffer
|
||||
}
|
||||
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ServerPacketAddrConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
pLen := buffer.Len()
|
||||
common.Must(binary.Write(buf.With(buffer.ExtendHeader(2)), binary.BigEndian, uint16(pLen)))
|
||||
common.Must(M.SocksaddrSerializer.WriteAddrPort(buf.With(buffer.ExtendHeader(M.SocksaddrSerializer.AddrPortLen(destination))), destination))
|
||||
if !c.responseWrite {
|
||||
buffer.ExtendHeader(1)[0] = statusSuccess
|
||||
c.responseWrite = true
|
||||
}
|
||||
return c.ExtendedConn.WriteBuffer(buffer)
|
||||
}
|
||||
|
||||
func (c *ServerPacketAddrConn) Upstream() any {
|
||||
return c.ExtendedConn
|
||||
}
|
||||
|
||||
func (c *ServerPacketAddrConn) FrontHeadroom() int {
|
||||
if !c.responseWrite {
|
||||
return 3 + M.MaxSocksaddrLength
|
||||
}
|
||||
return 2 + M.MaxSocksaddrLength
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/smux"
|
||||
)
|
||||
|
||||
type abstractSession interface {
|
||||
Open() (net.Conn, error)
|
||||
Accept() (net.Conn, error)
|
||||
NumStreams() int
|
||||
Close() error
|
||||
IsClosed() bool
|
||||
}
|
||||
|
||||
var _ abstractSession = (*smuxSession)(nil)
|
||||
|
||||
type smuxSession struct {
|
||||
*smux.Session
|
||||
}
|
||||
|
||||
func (s *smuxSession) Open() (net.Conn, error) {
|
||||
return s.OpenStream()
|
||||
}
|
||||
|
||||
func (s *smuxSession) Accept() (net.Conn, error) {
|
||||
return s.AcceptStream()
|
||||
}
|
||||
|
||||
type protocolConn struct {
|
||||
net.Conn
|
||||
protocol Protocol
|
||||
protocolWritten bool
|
||||
}
|
||||
|
||||
func (c *protocolConn) Write(p []byte) (n int, err error) {
|
||||
if c.protocolWritten {
|
||||
return c.Conn.Write(p)
|
||||
}
|
||||
_buffer := buf.StackNewSize(2 + len(p))
|
||||
defer common.KeepAlive(_buffer)
|
||||
buffer := common.Dup(_buffer)
|
||||
defer buffer.Release()
|
||||
EncodeRequest(buffer, Request{
|
||||
Protocol: c.protocol,
|
||||
})
|
||||
common.Must(common.Error(buffer.Write(p)))
|
||||
n, err = c.Conn.Write(buffer.Bytes())
|
||||
if err == nil {
|
||||
n--
|
||||
}
|
||||
c.protocolWritten = true
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *protocolConn) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
if !c.protocolWritten {
|
||||
return bufio.ReadFrom0(c, r)
|
||||
}
|
||||
return bufio.Copy(c.Conn, r)
|
||||
}
|
||||
|
||||
func (c *protocolConn) Upstream() any {
|
||||
return c.Conn
|
||||
}
|
||||
|
||||
type vectorisedProtocolConn struct {
|
||||
protocolConn
|
||||
N.VectorisedWriter
|
||||
}
|
||||
|
||||
func (c *vectorisedProtocolConn) WriteVectorised(buffers []*buf.Buffer) error {
|
||||
if c.protocolWritten {
|
||||
return c.VectorisedWriter.WriteVectorised(buffers)
|
||||
}
|
||||
c.protocolWritten = true
|
||||
_buffer := buf.StackNewSize(2)
|
||||
defer common.KeepAlive(_buffer)
|
||||
buffer := common.Dup(_buffer)
|
||||
defer buffer.Release()
|
||||
EncodeRequest(buffer, Request{
|
||||
Protocol: c.protocol,
|
||||
})
|
||||
return c.VectorisedWriter.WriteVectorised(append([]*buf.Buffer{buffer}, buffers...))
|
||||
}
|
||||
32
common/mux/v2ray_legacy.go
Normal file
32
common/mux/v2ray_legacy.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
vmess "github.com/sagernet/sing-vmess"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type V2RayLegacyRouter struct {
|
||||
router adapter.ConnectionRouter
|
||||
logger logger.ContextLogger
|
||||
}
|
||||
|
||||
func NewV2RayLegacyRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) adapter.ConnectionRouter {
|
||||
return &V2RayLegacyRouter{router, logger}
|
||||
}
|
||||
|
||||
func (r *V2RayLegacyRouter) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||
if metadata.Destination.Fqdn == vmess.MuxDestination.Fqdn {
|
||||
r.logger.InfoContext(ctx, "inbound legacy multiplex connection")
|
||||
return vmess.HandleMuxConnection(ctx, conn, adapter.NewRouteHandler(metadata, r.router, r.logger))
|
||||
}
|
||||
return r.router.RouteConnection(ctx, conn, metadata)
|
||||
}
|
||||
|
||||
func (r *V2RayLegacyRouter) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
||||
return r.router.RoutePacketConnection(ctx, conn, metadata)
|
||||
}
|
||||
@@ -3,19 +3,42 @@ package process
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"os/user"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-tun"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
type Searcher interface {
|
||||
FindProcessInfo(ctx context.Context, network string, srcIP netip.Addr, srcPort int) (*Info, error)
|
||||
FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error)
|
||||
}
|
||||
|
||||
var ErrNotFound = E.New("process not found")
|
||||
|
||||
type Config struct {
|
||||
Logger log.ContextLogger
|
||||
PackageManager tun.PackageManager
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
ProcessPath string
|
||||
PackageName string
|
||||
User string
|
||||
UserId int32
|
||||
}
|
||||
|
||||
func FindProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
||||
info, err := searcher.FindProcessInfo(ctx, network, source, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.UserId != -1 {
|
||||
osUser, _ := user.LookupId(F.ToString(info.UserId))
|
||||
if osUser != nil {
|
||||
info.User = osUser.Username
|
||||
}
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
@@ -2,170 +2,37 @@ package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/sagernet/sing-tun"
|
||||
)
|
||||
|
||||
var _ Searcher = (*androidSearcher)(nil)
|
||||
|
||||
type androidSearcher struct {
|
||||
logger log.ContextLogger
|
||||
watcher *fsnotify.Watcher
|
||||
userMap map[string]int32
|
||||
packageMap map[int32]string
|
||||
sharedUserMap map[int32]string
|
||||
packageManager tun.PackageManager
|
||||
}
|
||||
|
||||
func NewSearcher(logger log.ContextLogger) (Searcher, error) {
|
||||
return &androidSearcher{logger: logger}, nil
|
||||
func NewSearcher(config Config) (Searcher, error) {
|
||||
return &androidSearcher{config.PackageManager}, nil
|
||||
}
|
||||
|
||||
func (s *androidSearcher) Start() error {
|
||||
err := s.updatePackages()
|
||||
if err != nil {
|
||||
return E.Cause(err, "read packages list")
|
||||
}
|
||||
err = s.startWatcher()
|
||||
if err != nil {
|
||||
s.logger.Warn("create fsnotify watcher: ", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *androidSearcher) startWatcher() error {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = watcher.Add("/data/system/packages.xml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.watcher = watcher
|
||||
go s.loopUpdate()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *androidSearcher) loopUpdate() {
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-s.watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
err := s.updatePackages()
|
||||
if err != nil {
|
||||
s.logger.Error(E.Cause(err, "update packages list"))
|
||||
}
|
||||
case err, ok := <-s.watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
s.logger.Error(E.Cause(err, "fsnotify error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *androidSearcher) Close() error {
|
||||
return common.Close(common.PtrOrNil(s.watcher))
|
||||
}
|
||||
|
||||
func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, srcIP netip.Addr, srcPort int) (*Info, error) {
|
||||
_, uid, err := resolveSocketByNetlink(network, srcIP, srcPort)
|
||||
func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
||||
_, uid, err := resolveSocketByNetlink(network, source, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sharedUser, loaded := s.sharedUserMap[uid]; loaded {
|
||||
if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded {
|
||||
return &Info{
|
||||
UserId: uid,
|
||||
PackageName: sharedUser,
|
||||
UserId: int32(uid),
|
||||
PackageName: sharedPackage,
|
||||
}, nil
|
||||
}
|
||||
if packageName, loaded := s.packageMap[uid]; loaded {
|
||||
if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
|
||||
return &Info{
|
||||
UserId: uid,
|
||||
UserId: int32(uid),
|
||||
PackageName: packageName,
|
||||
}, nil
|
||||
}
|
||||
return &Info{UserId: uid}, nil
|
||||
}
|
||||
|
||||
func (s *androidSearcher) updatePackages() error {
|
||||
userMap := make(map[string]int32)
|
||||
packageMap := make(map[int32]string)
|
||||
sharedUserMap := make(map[int32]string)
|
||||
packagesData, err := os.Open("/data/system/packages.xml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := xml.NewDecoder(packagesData)
|
||||
var token xml.Token
|
||||
for {
|
||||
token, err = decoder.Token()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
element, isStart := token.(xml.StartElement)
|
||||
if !isStart {
|
||||
continue
|
||||
}
|
||||
|
||||
switch element.Name.Local {
|
||||
case "package":
|
||||
var name string
|
||||
var userID int64
|
||||
for _, attr := range element.Attr {
|
||||
switch attr.Name.Local {
|
||||
case "name":
|
||||
name = attr.Value
|
||||
case "userId", "sharedUserId":
|
||||
userID, err = strconv.ParseInt(attr.Value, 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if userID == 0 && name == "" {
|
||||
continue
|
||||
}
|
||||
userMap[name] = int32(userID)
|
||||
packageMap[int32(userID)] = name
|
||||
case "shared-user":
|
||||
var name string
|
||||
var userID int64
|
||||
for _, attr := range element.Attr {
|
||||
switch attr.Name.Local {
|
||||
case "name":
|
||||
name = attr.Value
|
||||
case "userId":
|
||||
userID, err = strconv.ParseInt(attr.Value, 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
packageMap[int32(userID)] = name
|
||||
}
|
||||
}
|
||||
if userID == 0 && name == "" {
|
||||
continue
|
||||
}
|
||||
sharedUserMap[int32(userID)] = name
|
||||
}
|
||||
}
|
||||
s.logger.Info("updated packages list: ", len(packageMap), " packages, ", len(sharedUserMap), " shared users")
|
||||
s.userMap = userMap
|
||||
s.packageMap = packageMap
|
||||
s.sharedUserMap = sharedUserMap
|
||||
return nil
|
||||
return &Info{UserId: int32(uid)}, nil
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
@@ -18,18 +19,34 @@ var _ Searcher = (*darwinSearcher)(nil)
|
||||
|
||||
type darwinSearcher struct{}
|
||||
|
||||
func NewSearcher(logger log.ContextLogger) (Searcher, error) {
|
||||
func NewSearcher(_ Config) (Searcher, error) {
|
||||
return &darwinSearcher{}, nil
|
||||
}
|
||||
|
||||
func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, srcIP netip.Addr, srcPort int) (*Info, error) {
|
||||
processName, err := findProcessName(network, srcIP, srcPort)
|
||||
func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
||||
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Info{ProcessPath: processName, UserId: -1}, nil
|
||||
}
|
||||
|
||||
var structSize = func() int {
|
||||
value, _ := syscall.Sysctl("kern.osrelease")
|
||||
major, _, _ := strings.Cut(value, ".")
|
||||
n, _ := strconv.ParseInt(major, 10, 64)
|
||||
switch true {
|
||||
case n >= 22:
|
||||
return 408
|
||||
default:
|
||||
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
|
||||
// size/offset are round up (aligned) to 8 bytes in darwin
|
||||
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
|
||||
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
|
||||
return 384
|
||||
}
|
||||
}()
|
||||
|
||||
func findProcessName(network string, ip netip.Addr, port int) (string, error) {
|
||||
var spath string
|
||||
switch network {
|
||||
@@ -54,7 +71,7 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
|
||||
// size/offset are round up (aligned) to 8 bytes in darwin
|
||||
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
|
||||
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
|
||||
itemSize := 384
|
||||
itemSize := structSize
|
||||
if network == N.NetworkTCP {
|
||||
// rup8(sizeof(xtcpcb_n))
|
||||
itemSize += 208
|
||||
|
||||
@@ -15,12 +15,12 @@ type linuxSearcher struct {
|
||||
logger log.ContextLogger
|
||||
}
|
||||
|
||||
func NewSearcher(logger log.ContextLogger) (Searcher, error) {
|
||||
return &linuxSearcher{logger}, nil
|
||||
func NewSearcher(config Config) (Searcher, error) {
|
||||
return &linuxSearcher{config.Logger}, nil
|
||||
}
|
||||
|
||||
func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, srcIP netip.Addr, srcPort int) (*Info, error) {
|
||||
inode, uid, err := resolveSocketByNetlink(network, srcIP, srcPort)
|
||||
func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
||||
inode, uid, err := resolveSocketByNetlink(network, source, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, src
|
||||
s.logger.DebugContext(ctx, "find process path: ", err)
|
||||
}
|
||||
return &Info{
|
||||
UserId: uid,
|
||||
UserId: int32(uid),
|
||||
ProcessPath: processPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"unicode"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
@@ -37,19 +36,9 @@ const (
|
||||
pathProc = "/proc"
|
||||
)
|
||||
|
||||
func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (inode int32, uid int32, err error) {
|
||||
for attempts := 0; attempts < 3; attempts++ {
|
||||
inode, uid, err = resolveSocketByNetlink0(network, ip, srcPort)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func resolveSocketByNetlink0(network string, ip netip.Addr, srcPort int) (inode int32, uid int32, err error) {
|
||||
var family byte
|
||||
var protocol byte
|
||||
func resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||
var family uint8
|
||||
var protocol uint8
|
||||
|
||||
switch network {
|
||||
case N.NetworkTCP:
|
||||
@@ -60,13 +49,13 @@ func resolveSocketByNetlink0(network string, ip netip.Addr, srcPort int) (inode
|
||||
return 0, 0, os.ErrInvalid
|
||||
}
|
||||
|
||||
if ip.Is4() {
|
||||
if source.Addr().Is4() {
|
||||
family = syscall.AF_INET
|
||||
} else {
|
||||
family = syscall.AF_INET6
|
||||
}
|
||||
|
||||
req := packSocketDiagRequest(family, protocol, ip, uint16(srcPort))
|
||||
req := packSocketDiagRequest(family, protocol, source)
|
||||
|
||||
socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG)
|
||||
if err != nil {
|
||||
@@ -77,22 +66,22 @@ func resolveSocketByNetlink0(network string, ip netip.Addr, srcPort int) (inode
|
||||
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100})
|
||||
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100})
|
||||
|
||||
if err = syscall.Connect(socket, &syscall.SockaddrNetlink{
|
||||
err = syscall.Connect(socket, &syscall.SockaddrNetlink{
|
||||
Family: syscall.AF_NETLINK,
|
||||
Pad: 0,
|
||||
Pid: 0,
|
||||
Groups: 0,
|
||||
}); err != nil {
|
||||
return 0, 0, err
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = syscall.Write(socket, req); err != nil {
|
||||
_, err = syscall.Write(socket, req)
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "write netlink request")
|
||||
}
|
||||
|
||||
_buffer := buf.StackNew()
|
||||
defer common.KeepAlive(_buffer)
|
||||
buffer := common.Dup(_buffer)
|
||||
buffer := buf.New()
|
||||
defer buffer.Release()
|
||||
|
||||
n, err := syscall.Read(socket, buffer.FreeBytes())
|
||||
@@ -115,15 +104,12 @@ func resolveSocketByNetlink0(network string, ip netip.Addr, srcPort int) (inode
|
||||
}
|
||||
|
||||
inode, uid = unpackSocketDiagResponse(&messages[0])
|
||||
if inode < 0 || uid < 0 {
|
||||
return 0, 0, E.New("invalid inode(", inode, ") or uid(", uid, ")")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func packSocketDiagRequest(family, protocol byte, source netip.Addr, sourcePort uint16) []byte {
|
||||
func packSocketDiagRequest(family, protocol byte, source netip.AddrPort) []byte {
|
||||
s := make([]byte, 16)
|
||||
copy(s, source.AsSlice())
|
||||
copy(s, source.Addr().AsSlice())
|
||||
|
||||
buf := make([]byte, sizeOfSocketDiagRequest)
|
||||
|
||||
@@ -139,7 +125,7 @@ func packSocketDiagRequest(family, protocol byte, source netip.Addr, sourcePort
|
||||
buf[19] = 0
|
||||
nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF)
|
||||
|
||||
binary.BigEndian.PutUint16(buf[24:26], sourcePort)
|
||||
binary.BigEndian.PutUint16(buf[24:26], source.Port())
|
||||
binary.BigEndian.PutUint16(buf[26:28], 0)
|
||||
|
||||
copy(buf[28:44], s)
|
||||
@@ -151,20 +137,20 @@ func packSocketDiagRequest(family, protocol byte, source netip.Addr, sourcePort
|
||||
return buf
|
||||
}
|
||||
|
||||
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid int32) {
|
||||
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
|
||||
if len(msg.Data) < 72 {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
data := msg.Data
|
||||
|
||||
uid = int32(nativeEndian.Uint32(data[64:68]))
|
||||
inode = int32(nativeEndian.Uint32(data[68:72]))
|
||||
uid = nativeEndian.Uint32(data[64:68])
|
||||
inode = nativeEndian.Uint32(data[68:72])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func resolveProcessNameByProcSearch(inode, uid int32) (string, error) {
|
||||
func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) {
|
||||
files, err := os.ReadDir(pathProc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -182,7 +168,7 @@ func resolveProcessNameByProcSearch(inode, uid int32) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if info.Sys().(*syscall.Stat_t).Uid != uint32(uid) {
|
||||
if info.Sys().(*syscall.Stat_t).Uid != uid {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user