mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
Compare commits
593 Commits
v1.9.0-alp
...
dev-next
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0e5f1814cc | ||
![]() |
d8e1cd0d51 | ||
![]() |
0a5f09f147 | ||
![]() |
e7ae3ddf31 | ||
![]() |
8b15576289 | ||
![]() |
c86f47bdf8 | ||
![]() |
ce013ceabb | ||
![]() |
d0824ea7f6 | ||
![]() |
7291d7e802 | ||
![]() |
3f38520f5e | ||
![]() |
de7e0364e8 | ||
![]() |
867c267bef | ||
![]() |
2dfe93c99c | ||
![]() |
656301bb9e | ||
![]() |
301549f453 | ||
![]() |
cd7229f39b | ||
![]() |
f0e42f8b8f | ||
![]() |
65a629cbb5 | ||
![]() |
7774f16552 | ||
![]() |
2ded81f1f7 | ||
![]() |
e68036ea62 | ||
![]() |
f5dd883fb5 | ||
![]() |
aa2a4ea2bb | ||
![]() |
aeb340170d | ||
![]() |
009b224de6 | ||
![]() |
f6898a7806 | ||
![]() |
44ada21eb2 | ||
![]() |
d276db1ccc | ||
![]() |
1016ba6cef | ||
![]() |
ecfd010ac9 | ||
![]() |
3c07a9dbf5 | ||
![]() |
115eb9e872 | ||
![]() |
26f0c5dd93 | ||
![]() |
0d9883b2c8 | ||
![]() |
b026e78f79 | ||
![]() |
2e20110f02 | ||
![]() |
9fc424498d | ||
![]() |
886ea0c893 | ||
![]() |
fbd79337e5 | ||
![]() |
de31c316a0 | ||
![]() |
ebaaee7b75 | ||
![]() |
1f35f25ada | ||
![]() |
5a1d726242 | ||
![]() |
2f3ebe6f68 | ||
![]() |
c2e962f9da | ||
![]() |
d0052e3803 | ||
![]() |
f2cb235305 | ||
![]() |
d6bd251e2d | ||
![]() |
1f07157ab5 | ||
![]() |
10afe58231 | ||
![]() |
23a99c2af1 | ||
![]() |
8337505d0d | ||
![]() |
9bd951264f | ||
![]() |
7ef938ed05 | ||
![]() |
45d20963c4 | ||
![]() |
e53428481f | ||
![]() |
9d3c47b612 | ||
![]() |
029783522c | ||
![]() |
b7f55e931e | ||
![]() |
e3405c656e | ||
![]() |
264c67f755 | ||
![]() |
b5f5e94fdb | ||
![]() |
3b53ca6e94 | ||
![]() |
6cb3030457 | ||
![]() |
47a717c6c1 | ||
![]() |
2167079ac6 | ||
![]() |
cf77712dc1 | ||
![]() |
883e9dac43 | ||
![]() |
d1ebc72b9d | ||
![]() |
27efb76ac0 | ||
![]() |
ada4633f49 | ||
![]() |
f4b6f828d6 | ||
![]() |
0ddef18bce | ||
![]() |
2e196ebba6 | ||
![]() |
15e41980f3 | ||
![]() |
6e8eff3296 | ||
![]() |
243a40ae1a | ||
![]() |
0d300cd5f8 | ||
![]() |
4879dd6992 | ||
![]() |
ad2ff84166 | ||
![]() |
753f036748 | ||
![]() |
27fa1a730d | ||
![]() |
afdf750da4 | ||
![]() |
75e0ce993f | ||
![]() |
88b4a8ae8e | ||
![]() |
92c639040c | ||
![]() |
f329e859dd | ||
![]() |
4918580820 | ||
![]() |
9dbd482971 | ||
![]() |
7cc6e1c0ae | ||
![]() |
2a03668d15 | ||
![]() |
095721e0e5 | ||
![]() |
7d0d59c9ee | ||
![]() |
18e9eaa244 | ||
![]() |
3439b75e41 | ||
![]() |
7b095b7719 | ||
![]() |
5dd1a09663 | ||
![]() |
750a9be4a9 | ||
![]() |
d21d724345 | ||
![]() |
74f1c3aa09 | ||
![]() |
aef0d477d2 | ||
![]() |
24cf4e9062 | ||
![]() |
dd89730ee7 | ||
![]() |
912bd00f1c | ||
![]() |
3ab6e6c0b0 | ||
![]() |
81ff3eef20 | ||
![]() |
d511698f3f | ||
![]() |
cb435ea232 | ||
![]() |
43a9016c83 | ||
![]() |
255068fd40 | ||
![]() |
098a00b025 | ||
![]() |
dba0b5276b | ||
![]() |
78ae935468 | ||
![]() |
3ea5f76470 | ||
![]() |
b4d294c05e | ||
![]() |
83cf5f5c6a | ||
![]() |
e7b3a8eebe | ||
![]() |
ee3a42a67e | ||
![]() |
50227c0f5f | ||
![]() |
bc5eb1e1a5 | ||
![]() |
995267a042 | ||
![]() |
41226a6075 | ||
![]() |
81d32181ce | ||
![]() |
c5ecca3938 | ||
![]() |
900888731c | ||
![]() |
13e648e4b1 | ||
![]() |
aff12ff671 | ||
![]() |
101fb88255 | ||
![]() |
8b489354e4 | ||
![]() |
7dea6eb7a6 | ||
![]() |
af1bfe4e3e | ||
![]() |
d574e9eb52 | ||
![]() |
2d7df1e1f2 | ||
![]() |
1c0ffcf5b1 | ||
![]() |
348cc39975 | ||
![]() |
987899f94a | ||
![]() |
d8b2d5142f | ||
![]() |
134802d1ee | ||
![]() |
e5e81b4de1 | ||
![]() |
300c961efa | ||
![]() |
7c7f512405 | ||
![]() |
03e8d029c2 | ||
![]() |
787b5f1931 | ||
![]() |
56a7624618 | ||
![]() |
3a84acf122 | ||
![]() |
f600e02e47 | ||
![]() |
e6d19de58a | ||
![]() |
f2bbf6b2aa | ||
![]() |
c54d50fd36 | ||
![]() |
6a051054db | ||
![]() |
49498f6439 | ||
![]() |
144a890c71 | ||
![]() |
afb4993445 | ||
![]() |
4c9455b944 | ||
![]() |
5fdc051a08 | ||
![]() |
cb68a40c43 | ||
![]() |
023218e6e7 | ||
![]() |
2a24b94b8d | ||
![]() |
c6531cf184 | ||
![]() |
d4fa0ed349 | ||
![]() |
10874d2dc4 | ||
![]() |
5adaf1ac75 | ||
![]() |
9668ea69b8 | ||
![]() |
ae9bc7acf1 | ||
![]() |
594ee480a2 | ||
![]() |
a15b5a2463 | ||
![]() |
991e755789 | ||
![]() |
97d41ffde8 | ||
![]() |
24af0766ac | ||
![]() |
af17eaa537 | ||
![]() |
3adc10a797 | ||
![]() |
5eeef6b28e | ||
![]() |
f4c29840c3 | ||
![]() |
47fc3ebda4 | ||
![]() |
9774a659b0 | ||
![]() |
2e4a6de4e7 | ||
![]() |
a530e424e9 | ||
![]() |
0bfd487ee9 | ||
![]() |
6aae834493 | ||
![]() |
f56131f38e | ||
![]() |
273a11d550 | ||
![]() |
ae8ce75e41 | ||
![]() |
d6d94b689f | ||
![]() |
30d785f1ee | ||
![]() |
db5ec3cdfc | ||
![]() |
9aca54d039 | ||
![]() |
d55d5009c2 | ||
![]() |
4f3ee61104 | ||
![]() |
96eb98c00a | ||
![]() |
68ce9577c6 | ||
![]() |
3ae036e997 | ||
![]() |
5da2d1d470 | ||
![]() |
8e2baf40f1 | ||
![]() |
c24c40dfee | ||
![]() |
32e52ce1ed | ||
![]() |
ed46438359 | ||
![]() |
0b5490d5a3 | ||
![]() |
2d73ef511d | ||
![]() |
63e6c85f6f | ||
![]() |
8946a6d2d0 | ||
![]() |
d3132645fb | ||
![]() |
373f158fe0 | ||
![]() |
ce36835fab | ||
![]() |
619fa671d7 | ||
![]() |
eb07c7a79e | ||
![]() |
7eb3535094 | ||
![]() |
93b68312cf | ||
![]() |
97ce666e43 | ||
![]() |
4000e1e66d | ||
![]() |
270740e859 | ||
![]() |
6cad142cfe | ||
![]() |
093013687c | ||
![]() |
ff31c469a0 | ||
![]() |
fbe390268c | ||
![]() |
07ac01dcb7 | ||
![]() |
badfdb62cd | ||
![]() |
986a410b30 | ||
![]() |
9db2d58545 | ||
![]() |
4eed46ac59 | ||
![]() |
abc38d1dab | ||
![]() |
8d6c4f1289 | ||
![]() |
a2d40eb8b8 | ||
![]() |
17b502bb4b | ||
![]() |
a0d4421085 | ||
![]() |
0d443072d1 | ||
![]() |
c9fb99b799 | ||
![]() |
92d245ad04 | ||
![]() |
0908627297 | ||
![]() |
7f79458b4f | ||
![]() |
9b4c11ba95 | ||
![]() |
27c31eac5d | ||
![]() |
bab8dc0b82 | ||
![]() |
d09d2fb665 | ||
![]() |
e64cf3b7df | ||
![]() |
9b73222314 | ||
![]() |
3923b57abf | ||
![]() |
4807e64609 | ||
![]() |
eeb37d89f1 | ||
![]() |
08c1ec4b7e | ||
![]() |
6b4cf67add | ||
![]() |
e65926fd08 | ||
![]() |
f2ec319fe1 | ||
![]() |
32377a61b7 | ||
![]() |
7aac801ccd | ||
![]() |
96fdf59ee4 | ||
![]() |
50b8f3ab94 | ||
![]() |
ff7aaf977b | ||
![]() |
9a1efbe54d | ||
![]() |
906c21f458 | ||
![]() |
d5e7af7a7e | ||
![]() |
4d41f03bd5 | ||
![]() |
30704a15a7 | ||
![]() |
83889178ed | ||
![]() |
1d2720bf5e | ||
![]() |
c4b6d0eadb | ||
![]() |
0c66888691 | ||
![]() |
68781387fe | ||
![]() |
fd299a0961 | ||
![]() |
285a82050c | ||
![]() |
2dbb8c55c9 | ||
![]() |
effcf39469 | ||
![]() |
9db9484863 | ||
![]() |
ca813f461b | ||
![]() |
bb46cdb2b3 | ||
![]() |
dcb10c21a1 | ||
![]() |
05ea0ca00e | ||
![]() |
c098f282b1 | ||
![]() |
ecf82d197c | ||
![]() |
9afe75586a | ||
![]() |
a1be455202 | ||
![]() |
19fb214226 | ||
![]() |
28ec898a8c | ||
![]() |
467b1bbeeb | ||
![]() |
02ab8ce806 | ||
![]() |
ce69e620e9 | ||
![]() |
1133cf3ef5 | ||
![]() |
59a607e303 | ||
![]() |
313be3d7a4 | ||
![]() |
4fe40fcee0 | ||
![]() |
e233fd4fe5 | ||
![]() |
9f7683818f | ||
![]() |
179e3cb2f5 | ||
![]() |
41b960552d | ||
![]() |
8304295c48 | ||
![]() |
253b41936e | ||
![]() |
ce5b4b06b5 | ||
![]() |
50f5006c43 | ||
![]() |
e42ff22c2e | ||
![]() |
578571b972 | ||
![]() |
935beca45d | ||
![]() |
3e246f1173 | ||
![]() |
1bc27a32c2 | ||
![]() |
bc2e3960e4 | ||
![]() |
9c4ab0bf33 | ||
![]() |
27bdef34c7 | ||
![]() |
3c00099ed4 | ||
![]() |
2babf07f9a | ||
![]() |
4795ed712b | ||
![]() |
d4cd564dbe | ||
![]() |
1676e13d3e | ||
![]() |
50576084c6 | ||
![]() |
3a94e792a2 | ||
![]() |
9f69f41f68 | ||
![]() |
e6847ff50e | ||
![]() |
2ac2589d14 | ||
![]() |
64a94e8144 | ||
![]() |
3ed8a5c5d1 | ||
![]() |
0a922c6fe3 | ||
![]() |
52f3a4226c | ||
![]() |
483d9fa503 | ||
![]() |
dd9de694f8 | ||
![]() |
5cdf5c1d9e | ||
![]() |
cec7e47086 | ||
![]() |
1e6a3f1f0b | ||
![]() |
f0b6818b4c | ||
![]() |
3032317918 | ||
![]() |
db22f61846 | ||
![]() |
8c3a98faa2 | ||
![]() |
1e787cb607 | ||
![]() |
558585b01d | ||
![]() |
6e7ecbd4f5 | ||
![]() |
5a661cde67 | ||
![]() |
3cc0e87cfb | ||
![]() |
effea5a2b3 | ||
![]() |
7f168c5ec6 | ||
![]() |
0e9129ee3f | ||
![]() |
1086d5e665 | ||
![]() |
d9102ba599 | ||
![]() |
17019f1729 | ||
![]() |
6be07ed51f | ||
![]() |
af58e3bec0 | ||
![]() |
e58b549d0f | ||
![]() |
1d81996ceb | ||
![]() |
97c47e72c4 | ||
![]() |
122be275b0 | ||
![]() |
0bb1132034 | ||
![]() |
de14337b4b | ||
![]() |
1e07633914 | ||
![]() |
e3e203844e | ||
![]() |
84a102a6ef | ||
![]() |
f1c76c4dde | ||
![]() |
8df0aa5719 | ||
![]() |
21faadb992 | ||
![]() |
88099a304a | ||
![]() |
f504fb0d46 | ||
![]() |
1d517b6ca5 | ||
![]() |
b702d0b67a | ||
![]() |
a001e30d8b | ||
![]() |
cdb93f0bb2 | ||
![]() |
718cffea9a | ||
![]() |
9585c53e9f | ||
![]() |
d66d5cd457 | ||
![]() |
8c143feec8 | ||
![]() |
419058f466 | ||
![]() |
1a6047a61b | ||
![]() |
327bb35ddd | ||
![]() |
6ed9a06394 | ||
![]() |
b80ec55ba0 | ||
![]() |
08718112ae | ||
![]() |
956ee361df | ||
![]() |
e93d0408be | ||
![]() |
137832ff3e | ||
![]() |
3ede29fb6d | ||
![]() |
82ab68b542 | ||
![]() |
e55723d84d | ||
![]() |
2f4d2d97f9 | ||
![]() |
926d6f769e | ||
![]() |
846777cd0c | ||
![]() |
06533b7a3b | ||
![]() |
4a95558c53 | ||
![]() |
e39a28ed5a | ||
![]() |
b2c708a3e6 | ||
![]() |
a9209bb3e5 | ||
![]() |
9dc3bb975a | ||
![]() |
3a7acaa92a | ||
![]() |
6bebe2483b | ||
![]() |
93cf134995 | ||
![]() |
ff7d8c9ba8 | ||
![]() |
50f07b42f6 | ||
![]() |
db3a0c636d | ||
![]() |
fec38f85cd | ||
![]() |
dcb0141646 | ||
![]() |
f4f5a3c925 | ||
![]() |
9b8d6c1b73 | ||
![]() |
2f776168de | ||
![]() |
923d3222b0 | ||
![]() |
bda93d516b | ||
![]() |
7eec3fb57a | ||
![]() |
b1d75812c5 | ||
![]() |
d44e7d9834 | ||
![]() |
369bc7cea3 | ||
![]() |
4b7a83da16 | ||
![]() |
0f7154afbd | ||
![]() |
a06d10c3bc | ||
![]() |
63cc6cc76c | ||
![]() |
d55c5b5cab | ||
![]() |
b624c2dcc7 | ||
![]() |
9415444ebd | ||
![]() |
95606191d8 | ||
![]() |
e586d9e9bc | ||
![]() |
8c7eaa4477 | ||
![]() |
8464c8cb7c | ||
![]() |
39d7127651 | ||
![]() |
e2077009c4 | ||
![]() |
700a8eb425 | ||
![]() |
3b0cba0852 | ||
![]() |
f5554dd8b8 | ||
![]() |
4d0362d530 | ||
![]() |
97ccd2ca04 | ||
![]() |
1ed6654ad4 | ||
![]() |
5385f75f53 | ||
![]() |
ad97d4e11f | ||
![]() |
09d4e91b77 | ||
![]() |
3dbdda9555 | ||
![]() |
1f4ed6ff8f | ||
![]() |
6ddbe19bc0 | ||
![]() |
d7205ecc60 | ||
![]() |
9e243e0ff9 | ||
![]() |
02bc3e0a0a | ||
![]() |
87be6dc235 | ||
![]() |
c1c30976dc | ||
![]() |
9bac18bcd1 | ||
![]() |
ceda5cc95d | ||
![]() |
27d6b63e71 | ||
![]() |
b57abcc73c | ||
![]() |
f1147965dd | ||
![]() |
45f3234c73 | ||
![]() |
aae3fded32 | ||
![]() |
090494faf5 | ||
![]() |
db5719e22f | ||
![]() |
064fb9b873 | ||
![]() |
f6a1e123fc | ||
![]() |
3066dfe3b3 | ||
![]() |
1128fdd8c7 | ||
![]() |
cfd9879b17 | ||
![]() |
9ceb660c57 | ||
![]() |
7d00d7df28 | ||
![]() |
21b1ac26b9 | ||
![]() |
7fec8d842e | ||
![]() |
07c678fb85 | ||
![]() |
baecfc7778 | ||
![]() |
07de36ecdb | ||
![]() |
2c8a8303cd | ||
![]() |
e5991cae0b | ||
![]() |
1349acfd5a | ||
![]() |
98ff897f35 | ||
![]() |
6144c8e340 | ||
![]() |
c8caac9f67 | ||
![]() |
81e9eda357 | ||
![]() |
7cba3da108 | ||
![]() |
82d06b43e7 | ||
![]() |
a7ac91f573 | ||
![]() |
0540a95a43 | ||
![]() |
94707dfcdd | ||
![]() |
8a17043502 | ||
![]() |
b0aaa86806 | ||
![]() |
8a2d3fbb28 | ||
![]() |
4652019608 | ||
![]() |
06fa5abf63 | ||
![]() |
996fbbf0c3 | ||
![]() |
142ff1b455 | ||
![]() |
74d662f7a3 | ||
![]() |
085f603377 | ||
![]() |
460fae83dc | ||
![]() |
bb9bd9bff6 | ||
![]() |
c2354ebf25 | ||
![]() |
c1f4755c4e | ||
![]() |
0ca5909b06 | ||
![]() |
e77a8114c5 | ||
![]() |
f1393235ff | ||
![]() |
bdba2365de | ||
![]() |
ce0da5b557 | ||
![]() |
3853201412 | ||
![]() |
7003ef40a3 | ||
![]() |
59ec92228c | ||
![]() |
0eeb2da323 | ||
![]() |
977b0fac02 | ||
![]() |
51964801ff | ||
![]() |
e08c052fc9 | ||
![]() |
53927d8bbd | ||
![]() |
968b9bc217 | ||
![]() |
69dc87aa6d | ||
![]() |
4193df375f | ||
![]() |
5ff7006326 | ||
![]() |
a89107ea9d | ||
![]() |
9ffdbba2ed | ||
![]() |
65c71049ea | ||
![]() |
7d4e6a7f4e | ||
![]() |
d612620c5d | ||
![]() |
8a9a77a438 | ||
![]() |
a2098c18e1 | ||
![]() |
cf2181dd3a | ||
![]() |
5899e95ff1 | ||
![]() |
d7160c19cf | ||
![]() |
da9e22b4e6 | ||
![]() |
0e120f8a44 | ||
![]() |
d918863ac5 | ||
![]() |
2ae192305c | ||
![]() |
71d1879bd6 | ||
![]() |
917514e09f | ||
![]() |
5327aeaea4 | ||
![]() |
93ae3f7a1e | ||
![]() |
f24a2aed7d | ||
![]() |
0517ceef76 | ||
![]() |
830ea46932 | ||
![]() |
cd0fcd5ddc | ||
![]() |
003176f069 | ||
![]() |
71d92518c1 | ||
![]() |
b5dcd6bf59 | ||
![]() |
11c7b4a866 | ||
![]() |
ee14135298 | ||
![]() |
cbcf005f37 | ||
![]() |
daee0b154e | ||
![]() |
d530c724c0 | ||
![]() |
7f698c1104 | ||
![]() |
7a4a44c6d2 | ||
![]() |
44277e5dd2 | ||
![]() |
1f470c69c4 | ||
![]() |
742adacce7 | ||
![]() |
32e1d5a5e2 | ||
![]() |
cb9f4ce597 | ||
![]() |
4b1a6185ba | ||
![]() |
8d85c92356 | ||
![]() |
c6164c9eca | ||
![]() |
3c85b8bc48 | ||
![]() |
8b8fb4344c | ||
![]() |
e85a38e059 | ||
![]() |
f3ac91673a | ||
![]() |
0f1e58b917 | ||
![]() |
c4cfe24aef | ||
![]() |
3d73b159ba | ||
![]() |
0ae1afef44 | ||
![]() |
a5e2a4073b | ||
![]() |
b6cb3948a3 | ||
![]() |
7b0f5061dc | ||
![]() |
76f20482f7 | ||
![]() |
e735a5bdc8 | ||
![]() |
70381e93c8 | ||
![]() |
07a40716e8 | ||
![]() |
5fea5956db | ||
![]() |
d20a389043 | ||
![]() |
4a4180bde5 | ||
![]() |
7ecb6daabb | ||
![]() |
712bdd9ae5 | ||
![]() |
a3b74591a7 | ||
![]() |
2f4abc6523 | ||
![]() |
965ab075d9 | ||
![]() |
ed2f8b9637 | ||
![]() |
0f71ce5120 | ||
![]() |
f8085ab111 | ||
![]() |
f61b272cbf | ||
![]() |
59d437b9d2 | ||
![]() |
a7338fdc2b | ||
![]() |
d88860928e | ||
![]() |
20a2e38f47 | ||
![]() |
acd438be23 | ||
![]() |
e27fb51b54 | ||
![]() |
adc38b26eb | ||
![]() |
7e943e743a | ||
![]() |
ceffcc0ad2 | ||
![]() |
fdc451f7c6 | ||
![]() |
b48c471e6a | ||
![]() |
4b1fabd007 | ||
![]() |
2b5eb1c59e | ||
![]() |
e2d3862e64 | ||
![]() |
4f5e7b974d | ||
![]() |
21dedddd93 | ||
![]() |
e02502bec0 | ||
![]() |
ba67633ee8 | ||
![]() |
7fd9abe802 | ||
![]() |
78a5f59202 | ||
![]() |
8d0da685d2 | ||
![]() |
e6644f784e | ||
![]() |
2b93b74d38 | ||
![]() |
dd52c26ae1 | ||
![]() |
f288e3898b | ||
![]() |
1bc893a73a | ||
![]() |
7359fdf195 | ||
![]() |
02b7041de6 | ||
![]() |
96ac931b11 | ||
![]() |
3077a82650 | ||
![]() |
de998c5119 | ||
![]() |
d32c30c4b7 | ||
![]() |
4823023806 | ||
![]() |
bb355d17b2 | ||
![]() |
aaf30bf92b | ||
![]() |
f8c400cffc | ||
![]() |
3c24411e14 | ||
![]() |
4a44aa3c21 | ||
![]() |
8db2ae0c83 | ||
![]() |
80d1aebcb7 | ||
![]() |
5583e01c99 | ||
![]() |
bca0b86549 |
30
.fpm_openwrt
Normal file
30
.fpm_openwrt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
-s dir
|
||||||
|
--name sing-box
|
||||||
|
--category net
|
||||||
|
--license GPL-3.0-or-later
|
||||||
|
--description "The universal proxy platform."
|
||||||
|
--url "https://sing-box.sagernet.org/"
|
||||||
|
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||||
|
--no-deb-generate-changes
|
||||||
|
|
||||||
|
--config-files /etc/config/sing-box
|
||||||
|
--config-files /etc/sing-box/config.json
|
||||||
|
|
||||||
|
--depends ca-bundle
|
||||||
|
--depends kmod-inet-diag
|
||||||
|
--depends kmod-tun
|
||||||
|
--depends firewall4
|
||||||
|
|
||||||
|
--before-remove release/config/openwrt.prerm
|
||||||
|
|
||||||
|
release/config/config.json=/etc/sing-box/config.json
|
||||||
|
|
||||||
|
release/config/openwrt.conf=/etc/config/sing-box
|
||||||
|
release/config/openwrt.init=/etc/init.d/sing-box
|
||||||
|
release/config/openwrt.keep=/lib/upgrade/keep.d/sing-box
|
||||||
|
|
||||||
|
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
|
||||||
|
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
|
||||||
|
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
|
||||||
|
|
||||||
|
LICENSE=/usr/share/licenses/sing-box/LICENSE
|
25
.fpm_systemd
Normal file
25
.fpm_systemd
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
-s dir
|
||||||
|
--name sing-box
|
||||||
|
--category net
|
||||||
|
--license GPL-3.0-or-later
|
||||||
|
--description "The universal proxy platform."
|
||||||
|
--url "https://sing-box.sagernet.org/"
|
||||||
|
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||||
|
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
|
||||||
|
--no-deb-generate-changes
|
||||||
|
--config-files /etc/sing-box/config.json
|
||||||
|
--after-install release/config/sing-box.postinst
|
||||||
|
|
||||||
|
release/config/config.json=/etc/sing-box/config.json
|
||||||
|
|
||||||
|
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
|
||||||
|
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
|
||||||
|
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
|
||||||
|
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
|
||||||
|
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
||||||
|
|
||||||
|
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
|
||||||
|
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
|
||||||
|
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
|
||||||
|
|
||||||
|
LICENSE=/usr/share/licenses/sing-box/LICENSE
|
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -65,6 +65,12 @@ body:
|
|||||||
For Apple platform clients, please check `Settings - View Service Log` for 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.
|
For the Android client, please check the `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` file for crash logs.
|
||||||
render: shell
|
render: shell
|
||||||
|
- type: checkboxes
|
||||||
|
id: supporter
|
||||||
|
attributes:
|
||||||
|
label: Supporter
|
||||||
|
options:
|
||||||
|
- label: I am a [sponsor](https://github.com/sponsors/nekohasekai/)
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Integrity requirements
|
label: Integrity requirements
|
||||||
|
6
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
@ -65,6 +65,12 @@ body:
|
|||||||
对于 Apple 平台图形客户端程序,请检查 `Settings - View Service Log` 以导出崩溃日志。
|
对于 Apple 平台图形客户端程序,请检查 `Settings - View Service Log` 以导出崩溃日志。
|
||||||
对于 Android 图形客户端程序,请检查 `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` 文件以导出崩溃日志。
|
对于 Android 图形客户端程序,请检查 `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` 文件以导出崩溃日志。
|
||||||
render: shell
|
render: shell
|
||||||
|
- type: checkboxes
|
||||||
|
id: supporter
|
||||||
|
attributes:
|
||||||
|
label: 支持我们
|
||||||
|
options:
|
||||||
|
- label: 我已经 [赞助](https://github.com/sponsors/nekohasekai/)
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: 完整性要求
|
label: 完整性要求
|
||||||
|
28
.github/deb2ipk.sh
vendored
Executable file
28
.github/deb2ipk.sh
vendored
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# mod from https://gist.github.com/pldubouilh/c5703052986bfdd404005951dee54683
|
||||||
|
|
||||||
|
set -e -o pipefail
|
||||||
|
|
||||||
|
PROJECT=$(dirname "$0")/../..
|
||||||
|
TMP_PATH=`mktemp -d`
|
||||||
|
cp $2 $TMP_PATH
|
||||||
|
pushd $TMP_PATH
|
||||||
|
|
||||||
|
DEB_NAME=`ls *.deb`
|
||||||
|
ar x $DEB_NAME
|
||||||
|
|
||||||
|
mkdir control
|
||||||
|
pushd control
|
||||||
|
tar xf ../control.tar.gz
|
||||||
|
rm md5sums
|
||||||
|
sed "s/Architecture:\\ \w*/Architecture:\\ $1/g" ./control -i
|
||||||
|
cat control
|
||||||
|
tar czf ../control.tar.gz ./*
|
||||||
|
popd
|
||||||
|
|
||||||
|
DEB_NAME=${DEB_NAME%.deb}
|
||||||
|
tar czf $DEB_NAME.ipk control.tar.gz data.tar.gz debian-binary
|
||||||
|
popd
|
||||||
|
|
||||||
|
cp $TMP_PATH/$DEB_NAME.ipk $3
|
||||||
|
rm -r $TMP_PATH
|
25
.github/setup_legacy_go.sh
vendored
Executable file
25
.github/setup_legacy_go.sh
vendored
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
VERSION="1.23.6"
|
||||||
|
|
||||||
|
mkdir -p $HOME/go
|
||||||
|
cd $HOME/go
|
||||||
|
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
|
||||||
|
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
|
||||||
|
mv go go_legacy
|
||||||
|
cd go_legacy
|
||||||
|
|
||||||
|
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||||
|
# this patch file only works on golang1.23.x
|
||||||
|
# that means after golang1.24 release it must be changed
|
||||||
|
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
|
||||||
|
# revert:
|
||||||
|
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
||||||
|
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||||
|
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
||||||
|
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
||||||
|
|
||||||
|
curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1
|
||||||
|
curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1
|
||||||
|
curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1
|
||||||
|
curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1
|
14
.github/update_clients.sh
vendored
Executable file
14
.github/update_clients.sh
vendored
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
PROJECTS=$(dirname "$0")/../..
|
||||||
|
|
||||||
|
function updateClient() {
|
||||||
|
pushd clients/$1
|
||||||
|
git fetch
|
||||||
|
git reset FETCH_HEAD --hard
|
||||||
|
popd
|
||||||
|
git add clients/$1
|
||||||
|
}
|
||||||
|
|
||||||
|
updateClient "apple"
|
||||||
|
updateClient "android"
|
667
.github/workflows/build.yml
vendored
Normal file
667
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,667 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Version name"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
build:
|
||||||
|
description: "Build type"
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
default: "All"
|
||||||
|
options:
|
||||||
|
- All
|
||||||
|
- Binary
|
||||||
|
- Android
|
||||||
|
- Apple
|
||||||
|
- app-store
|
||||||
|
- iOS
|
||||||
|
- macOS
|
||||||
|
- tvOS
|
||||||
|
- macOS-standalone
|
||||||
|
- publish-android
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main-next
|
||||||
|
- dev-next
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
calculate_version:
|
||||||
|
name: Calculate version
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.outputs.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ^1.24.4
|
||||||
|
- name: Check input version
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
run: |-
|
||||||
|
echo "version=${{ inputs.version }}"
|
||||||
|
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||||
|
- name: Calculate version
|
||||||
|
if: github.event_name != 'workflow_dispatch'
|
||||||
|
run: |-
|
||||||
|
go run -v ./cmd/internal/read_tag --ci --nightly
|
||||||
|
- name: Set outputs
|
||||||
|
id: outputs
|
||||||
|
run: |-
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
build:
|
||||||
|
name: Build binary
|
||||||
|
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- calculate_version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
|
||||||
|
- { os: linux, arch: "386", go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
|
||||||
|
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
|
||||||
|
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
|
||||||
|
- { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" }
|
||||||
|
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" }
|
||||||
|
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" }
|
||||||
|
- { os: linux, arch: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" }
|
||||||
|
- { os: linux, arch: mipsle, gomips: hardfloat, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc_24kf" }
|
||||||
|
- { os: linux, arch: mipsle, gomips: softfloat, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
|
||||||
|
- { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" }
|
||||||
|
- { os: linux, arch: mips64le, gomips: hardfloat, debian: mips64el, rpm: mips64el }
|
||||||
|
- { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" }
|
||||||
|
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||||
|
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||||
|
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
|
||||||
|
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
|
||||||
|
|
||||||
|
- { os: windows, arch: amd64 }
|
||||||
|
- { os: windows, arch: amd64, legacy_go: true }
|
||||||
|
- { os: windows, arch: "386" }
|
||||||
|
- { os: windows, arch: "386", legacy_go: true }
|
||||||
|
- { os: windows, arch: arm64 }
|
||||||
|
|
||||||
|
- { os: darwin, arch: amd64 }
|
||||||
|
- { os: darwin, arch: arm64 }
|
||||||
|
|
||||||
|
- { os: android, arch: arm64, ndk: "aarch64-linux-android21" }
|
||||||
|
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" }
|
||||||
|
- { os: android, arch: amd64, ndk: "x86_64-linux-android21" }
|
||||||
|
- { os: android, arch: "386", ndk: "i686-linux-android21" }
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Go
|
||||||
|
if: ${{ ! matrix.legacy_go }}
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ^1.24.4
|
||||||
|
- name: Cache Legacy Go
|
||||||
|
if: matrix.require_legacy_go
|
||||||
|
id: cache-legacy-go
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/go_legacy
|
||||||
|
key: go_legacy_1236
|
||||||
|
- name: Setup Legacy Go
|
||||||
|
if: matrix.legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
|
||||||
|
run: |-
|
||||||
|
.github/setup_legacy_go.sh
|
||||||
|
- name: Setup Legacy Go 2
|
||||||
|
if: matrix.legacy_go
|
||||||
|
run: |-
|
||||||
|
echo "PATH=$HOME/go/go_legacy/bin:$PATH" >> $GITHUB_ENV
|
||||||
|
echo "GOROOT=$HOME/go/go_legacy" >> $GITHUB_ENV
|
||||||
|
- name: Setup Android NDK
|
||||||
|
if: matrix.os == 'android'
|
||||||
|
uses: nttld/setup-ndk@v1
|
||||||
|
with:
|
||||||
|
ndk-version: r28
|
||||||
|
local-cache: true
|
||||||
|
- name: Set tag
|
||||||
|
run: |-
|
||||||
|
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||||
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
|
- name: Set build tags
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
|
||||||
|
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||||
|
- name: Build
|
||||||
|
if: matrix.os != 'android'
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
mkdir -p dist
|
||||||
|
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||||
|
./cmd/sing-box
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
GOOS: ${{ matrix.os }}
|
||||||
|
GOARCH: ${{ matrix.arch }}
|
||||||
|
GO386: ${{ matrix.go386 }}
|
||||||
|
GOARM: ${{ matrix.goarm }}
|
||||||
|
GOMIPS: ${{ matrix.gomips }}
|
||||||
|
GOMIPS64: ${{ matrix.gomips }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Build Android
|
||||||
|
if: matrix.os == 'android'
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
go install -v ./cmd/internal/build
|
||||||
|
export CC='${{ matrix.ndk }}-clang'
|
||||||
|
export CXX="${CC}++"
|
||||||
|
mkdir -p dist
|
||||||
|
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||||
|
./cmd/sing-box
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: "1"
|
||||||
|
BUILD_GOOS: ${{ matrix.os }}
|
||||||
|
BUILD_GOARCH: ${{ matrix.arch }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Set name
|
||||||
|
run: |-
|
||||||
|
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}"
|
||||||
|
if [[ -n "${{ matrix.goarm }}" ]]; then
|
||||||
|
DIR_NAME="${DIR_NAME}v${{ matrix.goarm }}"
|
||||||
|
elif [[ -n "${{ matrix.go386 }}" && "${{ matrix.go386 }}" != 'sse2' ]]; then
|
||||||
|
DIR_NAME="${DIR_NAME}-${{ matrix.go386 }}"
|
||||||
|
elif [[ -n "${{ matrix.gomips }}" && "${{ matrix.gomips }}" != 'hardfloat' ]]; then
|
||||||
|
DIR_NAME="${DIR_NAME}-${{ matrix.gomips }}"
|
||||||
|
elif [[ "${{ matrix.legacy_go }}" == 'true' ]]; then
|
||||||
|
DIR_NAME="${DIR_NAME}-legacy"
|
||||||
|
fi
|
||||||
|
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
|
||||||
|
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||||
|
PKG_VERSION="${PKG_VERSION//-/\~}"
|
||||||
|
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
|
||||||
|
- name: Package DEB
|
||||||
|
if: matrix.debian != ''
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y debsigs
|
||||||
|
cp .fpm_systemd .fpm
|
||||||
|
fpm -t deb \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.debian }}.deb" \
|
||||||
|
--architecture ${{ matrix.debian }} \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
||||||
|
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
|
||||||
|
rm -rf $HOME/.gnupg
|
||||||
|
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||||
|
${{ secrets.GPG_KEY }}
|
||||||
|
EOF
|
||||||
|
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
|
||||||
|
- name: Package RPM
|
||||||
|
if: matrix.rpm != ''
|
||||||
|
run: |-
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
cp .fpm_systemd .fpm
|
||||||
|
fpm -t rpm \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.rpm }}.rpm" \
|
||||||
|
--architecture ${{ matrix.rpm }} \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
cat > $HOME/.rpmmacros <<EOF
|
||||||
|
%_gpg_name ${{ secrets.GPG_KEY_ID }}
|
||||||
|
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
|
||||||
|
EOF
|
||||||
|
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||||
|
${{ secrets.GPG_KEY }}
|
||||||
|
EOF
|
||||||
|
rpmsign --addsign dist/*.rpm
|
||||||
|
- name: Package Pacman
|
||||||
|
if: matrix.pacman != ''
|
||||||
|
run: |-
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libarchive-tools
|
||||||
|
cp .fpm_systemd .fpm
|
||||||
|
fpm -t pacman \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \
|
||||||
|
--architecture ${{ matrix.pacman }} \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
- name: Package OpenWrt
|
||||||
|
if: matrix.openwrt != ''
|
||||||
|
run: |-
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
cp .fpm_openwrt .fpm
|
||||||
|
fpm -t deb \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/openwrt.deb" \
|
||||||
|
--architecture all \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
for architecture in ${{ matrix.openwrt }}; do
|
||||||
|
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk"
|
||||||
|
done
|
||||||
|
rm "dist/openwrt.deb"
|
||||||
|
- name: Archive
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
cd dist
|
||||||
|
mkdir -p "${DIR_NAME}"
|
||||||
|
cp ../LICENSE "${DIR_NAME}"
|
||||||
|
if [ '${{ matrix.os }}' = 'windows' ]; then
|
||||||
|
cp sing-box "${DIR_NAME}/sing-box.exe"
|
||||||
|
zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
|
||||||
|
else
|
||||||
|
cp sing-box "${DIR_NAME}"
|
||||||
|
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
|
||||||
|
fi
|
||||||
|
rm -r "${DIR_NAME}"
|
||||||
|
- name: Cleanup
|
||||||
|
run: rm dist/sing-box
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_go && '-legacy' || '' }}
|
||||||
|
path: "dist"
|
||||||
|
build_android:
|
||||||
|
name: Build Android
|
||||||
|
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- calculate_version
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
submodules: 'recursive'
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ^1.24.4
|
||||||
|
- name: Setup Android NDK
|
||||||
|
id: setup-ndk
|
||||||
|
uses: nttld/setup-ndk@v1
|
||||||
|
with:
|
||||||
|
ndk-version: r28
|
||||||
|
- name: Setup OpenJDK
|
||||||
|
run: |-
|
||||||
|
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
||||||
|
/usr/lib/jvm/java-17-openjdk-amd64/bin/java --version
|
||||||
|
- name: Set tag
|
||||||
|
run: |-
|
||||||
|
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||||
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
|
- name: Build library
|
||||||
|
run: |-
|
||||||
|
make lib_install
|
||||||
|
export PATH="$PATH:$(go env GOPATH)/bin"
|
||||||
|
make lib_android
|
||||||
|
env:
|
||||||
|
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||||
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
|
- name: Checkout main branch
|
||||||
|
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
||||||
|
run: |-
|
||||||
|
cd clients/android
|
||||||
|
git checkout main
|
||||||
|
- name: Checkout dev branch
|
||||||
|
if: github.ref == 'refs/heads/dev-next'
|
||||||
|
run: |-
|
||||||
|
cd clients/android
|
||||||
|
git checkout dev
|
||||||
|
- name: Gradle cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.gradle
|
||||||
|
key: gradle-${{ hashFiles('**/*.gradle') }}
|
||||||
|
- name: Update version
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
run: |-
|
||||||
|
go run -v ./cmd/internal/update_android_version --ci
|
||||||
|
- name: Update nightly version
|
||||||
|
if: github.event_name != 'workflow_dispatch'
|
||||||
|
run: |-
|
||||||
|
go run -v ./cmd/internal/update_android_version --ci --nightly
|
||||||
|
- name: Build
|
||||||
|
run: |-
|
||||||
|
mkdir clients/android/app/libs
|
||||||
|
cp libbox.aar clients/android/app/libs
|
||||||
|
cd clients/android
|
||||||
|
./gradlew :app:assemblePlayRelease :app:assembleOtherRelease
|
||||||
|
env:
|
||||||
|
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||||
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
|
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
||||||
|
- name: Prepare upload
|
||||||
|
run: |-
|
||||||
|
mkdir -p dist
|
||||||
|
cp clients/android/app/build/outputs/apk/play/release/*.apk dist
|
||||||
|
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: binary-android-apks
|
||||||
|
path: 'dist'
|
||||||
|
publish_android:
|
||||||
|
name: Publish Android
|
||||||
|
if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- calculate_version
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
submodules: 'recursive'
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ^1.24.4
|
||||||
|
- name: Setup Android NDK
|
||||||
|
id: setup-ndk
|
||||||
|
uses: nttld/setup-ndk@v1
|
||||||
|
with:
|
||||||
|
ndk-version: r28
|
||||||
|
- name: Setup OpenJDK
|
||||||
|
run: |-
|
||||||
|
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
||||||
|
/usr/lib/jvm/java-17-openjdk-amd64/bin/java --version
|
||||||
|
- name: Set tag
|
||||||
|
run: |-
|
||||||
|
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||||
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
|
- name: Build library
|
||||||
|
run: |-
|
||||||
|
make lib_install
|
||||||
|
export PATH="$PATH:$(go env GOPATH)/bin"
|
||||||
|
make lib_android
|
||||||
|
env:
|
||||||
|
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||||
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
|
- name: Checkout main branch
|
||||||
|
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
||||||
|
run: |-
|
||||||
|
cd clients/android
|
||||||
|
git checkout main
|
||||||
|
- name: Checkout dev branch
|
||||||
|
if: github.ref == 'refs/heads/dev-next'
|
||||||
|
run: |-
|
||||||
|
cd clients/android
|
||||||
|
git checkout dev
|
||||||
|
- name: Gradle cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.gradle
|
||||||
|
key: gradle-${{ hashFiles('**/*.gradle') }}
|
||||||
|
- name: Build
|
||||||
|
run: |-
|
||||||
|
go run -v ./cmd/internal/update_android_version --ci
|
||||||
|
mkdir clients/android/app/libs
|
||||||
|
cp libbox.aar clients/android/app/libs
|
||||||
|
cd clients/android
|
||||||
|
echo -n "$SERVICE_ACCOUNT_CREDENTIALS" | base64 --decode > service-account-credentials.json
|
||||||
|
./gradlew :app:publishPlayReleaseBundle
|
||||||
|
env:
|
||||||
|
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||||
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
|
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
||||||
|
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
|
||||||
|
build_apple:
|
||||||
|
name: Build Apple clients
|
||||||
|
runs-on: macos-15
|
||||||
|
needs:
|
||||||
|
- calculate_version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: iOS
|
||||||
|
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'iOS' }}
|
||||||
|
platform: ios
|
||||||
|
scheme: SFI
|
||||||
|
destination: 'generic/platform=iOS'
|
||||||
|
archive: build/SFI.xcarchive
|
||||||
|
upload: SFI/Upload.plist
|
||||||
|
- name: macOS
|
||||||
|
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'macOS' }}
|
||||||
|
platform: macos
|
||||||
|
scheme: SFM
|
||||||
|
destination: 'generic/platform=macOS'
|
||||||
|
archive: build/SFM.xcarchive
|
||||||
|
upload: SFI/Upload.plist
|
||||||
|
- name: tvOS
|
||||||
|
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'tvOS' }}
|
||||||
|
platform: tvos
|
||||||
|
scheme: SFT
|
||||||
|
destination: 'generic/platform=tvOS'
|
||||||
|
archive: build/SFT.xcarchive
|
||||||
|
upload: SFI/Upload.plist
|
||||||
|
- name: macOS-standalone
|
||||||
|
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone' }}
|
||||||
|
platform: macos
|
||||||
|
scheme: SFM.System
|
||||||
|
destination: 'generic/platform=macOS'
|
||||||
|
archive: build/SFM.System.xcarchive
|
||||||
|
export: SFM.System/Export.plist
|
||||||
|
export_path: build/SFM.System
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
if: matrix.if
|
||||||
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
submodules: 'recursive'
|
||||||
|
- name: Setup Go
|
||||||
|
if: matrix.if
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ^1.24.4
|
||||||
|
- name: Setup Xcode stable
|
||||||
|
if: matrix.if && github.ref == 'refs/heads/main-next'
|
||||||
|
run: |-
|
||||||
|
sudo xcode-select -s /Applications/Xcode_16.2.app
|
||||||
|
- name: Setup Xcode beta
|
||||||
|
if: matrix.if && github.ref == 'refs/heads/dev-next'
|
||||||
|
run: |-
|
||||||
|
sudo xcode-select -s /Applications/Xcode_16.2.app
|
||||||
|
- name: Set tag
|
||||||
|
if: matrix.if
|
||||||
|
run: |-
|
||||||
|
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||||
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
|
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||||
|
- name: Checkout main branch
|
||||||
|
if: matrix.if && github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
||||||
|
run: |-
|
||||||
|
cd clients/apple
|
||||||
|
git checkout main
|
||||||
|
- name: Checkout dev branch
|
||||||
|
if: matrix.if && github.ref == 'refs/heads/dev-next'
|
||||||
|
run: |-
|
||||||
|
cd clients/apple
|
||||||
|
git checkout dev
|
||||||
|
- name: Setup certificates
|
||||||
|
if: matrix.if
|
||||||
|
run: |-
|
||||||
|
CERTIFICATE_PATH=$RUNNER_TEMP/Certificates.p12
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/certificates.keychain-db
|
||||||
|
echo -n "$CERTIFICATES_P12" | base64 --decode -o $CERTIFICATE_PATH
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||||
|
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
PROFILES_ZIP_PATH=$RUNNER_TEMP/Profiles.zip
|
||||||
|
echo -n "$PROVISIONING_PROFILES" | base64 --decode -o $PROFILES_ZIP_PATH
|
||||||
|
|
||||||
|
PROFILES_PATH="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||||
|
mkdir -p "$PROFILES_PATH"
|
||||||
|
unzip $PROFILES_ZIP_PATH -d "$PROFILES_PATH"
|
||||||
|
|
||||||
|
ASC_KEY_PATH=$RUNNER_TEMP/Key.p12
|
||||||
|
echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH
|
||||||
|
|
||||||
|
xcrun notarytool store-credentials "notarytool-password" \
|
||||||
|
--key $ASC_KEY_PATH \
|
||||||
|
--key-id $ASC_KEY_ID \
|
||||||
|
--issuer $ASC_KEY_ISSUER_ID
|
||||||
|
|
||||||
|
echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV"
|
||||||
|
echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV"
|
||||||
|
echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV"
|
||||||
|
env:
|
||||||
|
CERTIFICATES_P12: ${{ secrets.CERTIFICATES_P12 }}
|
||||||
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
|
KEYCHAIN_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
|
PROVISIONING_PROFILES: ${{ secrets.PROVISIONING_PROFILES }}
|
||||||
|
ASC_KEY: ${{ secrets.ASC_KEY }}
|
||||||
|
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
||||||
|
ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }}
|
||||||
|
- name: Build library
|
||||||
|
if: matrix.if
|
||||||
|
run: |-
|
||||||
|
make lib_install
|
||||||
|
export PATH="$PATH:$(go env GOPATH)/bin"
|
||||||
|
go run ./cmd/internal/build_libbox -target apple -platform ${{ matrix.platform }}
|
||||||
|
mv Libbox.xcframework clients/apple
|
||||||
|
- name: Update macOS version
|
||||||
|
if: matrix.if && matrix.name == 'macOS' && github.event_name == 'workflow_dispatch'
|
||||||
|
run: |-
|
||||||
|
MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version)
|
||||||
|
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION"
|
||||||
|
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV"
|
||||||
|
- name: Update version
|
||||||
|
if: matrix.if && matrix.name != 'iOS'
|
||||||
|
run: |-
|
||||||
|
go run -v ./cmd/internal/update_apple_version --ci
|
||||||
|
- name: Build
|
||||||
|
if: matrix.if
|
||||||
|
run: |-
|
||||||
|
cd clients/apple
|
||||||
|
xcodebuild archive \
|
||||||
|
-scheme "${{ matrix.scheme }}" \
|
||||||
|
-configuration Release \
|
||||||
|
-destination "${{ matrix.destination }}" \
|
||||||
|
-archivePath "${{ matrix.archive }}" \
|
||||||
|
-allowProvisioningUpdates \
|
||||||
|
-authenticationKeyPath $ASC_KEY_PATH \
|
||||||
|
-authenticationKeyID $ASC_KEY_ID \
|
||||||
|
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
||||||
|
- name: Upload to App Store Connect
|
||||||
|
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
||||||
|
run: |-
|
||||||
|
go run -v ./cmd/internal/app_store_connect cancel_app_store ${{ matrix.platform }}
|
||||||
|
cd clients/apple
|
||||||
|
xcodebuild -exportArchive \
|
||||||
|
-archivePath "${{ matrix.archive }}" \
|
||||||
|
-exportOptionsPlist ${{ matrix.upload }} \
|
||||||
|
-allowProvisioningUpdates \
|
||||||
|
-authenticationKeyPath $ASC_KEY_PATH \
|
||||||
|
-authenticationKeyID $ASC_KEY_ID \
|
||||||
|
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
||||||
|
- name: Publish to TestFlight
|
||||||
|
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/dev-next'
|
||||||
|
run: |-
|
||||||
|
go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }}
|
||||||
|
- name: Build image
|
||||||
|
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
||||||
|
run: |-
|
||||||
|
pushd clients/apple
|
||||||
|
xcodebuild -exportArchive \
|
||||||
|
-archivePath "${{ matrix.archive }}" \
|
||||||
|
-exportOptionsPlist ${{ matrix.export }} \
|
||||||
|
-exportPath "${{ matrix.export_path }}"
|
||||||
|
brew install create-dmg
|
||||||
|
create-dmg \
|
||||||
|
--volname "sing-box" \
|
||||||
|
--volicon "${{ matrix.export_path }}/SFM.app/Contents/Resources/AppIcon.icns" \
|
||||||
|
--icon "SFM.app" 0 0 \
|
||||||
|
--hide-extension "SFM.app" \
|
||||||
|
--app-drop-link 0 0 \
|
||||||
|
--skip-jenkins \
|
||||||
|
SFM.dmg "${{ matrix.export_path }}/SFM.app"
|
||||||
|
xcrun notarytool submit "SFM.dmg" --wait --keychain-profile "notarytool-password"
|
||||||
|
cd "${{ matrix.archive }}"
|
||||||
|
zip -r SFM.dSYMs.zip dSYMs
|
||||||
|
popd
|
||||||
|
|
||||||
|
mkdir -p dist
|
||||||
|
cp clients/apple/SFM.dmg "dist/SFM-${VERSION}-universal.dmg"
|
||||||
|
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/SFM-${VERSION}-universal.dSYMs.zip"
|
||||||
|
- name: Upload image
|
||||||
|
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: binary-macos-dmg
|
||||||
|
path: 'dist'
|
||||||
|
upload:
|
||||||
|
name: Upload builds
|
||||||
|
if: always() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- calculate_version
|
||||||
|
- build
|
||||||
|
- build_android
|
||||||
|
- build_apple
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Cache ghr
|
||||||
|
uses: actions/cache@v4
|
||||||
|
id: cache-ghr
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/bin/ghr
|
||||||
|
key: ghr
|
||||||
|
- name: Setup ghr
|
||||||
|
if: steps.cache-ghr.outputs.cache-hit != 'true'
|
||||||
|
run: |-
|
||||||
|
cd $HOME
|
||||||
|
git clone https://github.com/nekohasekai/ghr ghr
|
||||||
|
cd ghr
|
||||||
|
go install -v .
|
||||||
|
- name: Set tag
|
||||||
|
run: |-
|
||||||
|
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||||
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
|
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||||
|
- name: Download builds
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: dist
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Upload builds
|
||||||
|
if: ${{ env.PUBLISHED == 'false' }}
|
||||||
|
run: |-
|
||||||
|
export PATH="$PATH:$HOME/go/bin"
|
||||||
|
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Replace builds
|
||||||
|
if: ${{ env.PUBLISHED != 'false' }}
|
||||||
|
run: |-
|
||||||
|
export PATH="$PATH:$HOME/go/bin"
|
||||||
|
ghr --replace -p 5 "v${VERSION}" dist
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
220
.github/workflows/debug.yml
vendored
220
.github/workflows/debug.yml
vendored
@ -1,220 +0,0 @@
|
|||||||
name: Debug build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- stable-next
|
|
||||||
- main-next
|
|
||||||
- dev-next
|
|
||||||
paths-ignore:
|
|
||||||
- '**.md'
|
|
||||||
- '.github/**'
|
|
||||||
- '!.github/workflows/debug.yml'
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- stable-next
|
|
||||||
- main-next
|
|
||||||
- dev-next
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Debug build
|
|
||||||
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.22
|
|
||||||
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
|
|
||||||
- name: Cache go module
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/go/pkg/mod
|
|
||||||
key: go118-${{ hashFiles('**/go.sum') }}
|
|
||||||
- name: Run Test
|
|
||||||
run: make ci_build_go118
|
|
||||||
build_go120:
|
|
||||||
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
|
|
||||||
- name: Cache go module
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/go/pkg/mod
|
|
||||||
key: go120-${{ hashFiles('**/go.sum') }}
|
|
||||||
- name: Run Test
|
|
||||||
run: make ci_build_go120
|
|
||||||
build_go121:
|
|
||||||
name: Debug build (Go 1.21)
|
|
||||||
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.21
|
|
||||||
- name: Cache go module
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/go/pkg/mod
|
|
||||||
key: go121-${{ hashFiles('**/go.sum') }}
|
|
||||||
- name: Run Test
|
|
||||||
run: make ci_build
|
|
||||||
cross:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
# windows
|
|
||||||
- name: windows-amd64
|
|
||||||
goos: windows
|
|
||||||
goarch: amd64
|
|
||||||
goamd64: v1
|
|
||||||
- name: windows-amd64-v3
|
|
||||||
goos: windows
|
|
||||||
goarch: amd64
|
|
||||||
goamd64: v3
|
|
||||||
- name: windows-386
|
|
||||||
goos: windows
|
|
||||||
goarch: 386
|
|
||||||
- name: windows-arm64
|
|
||||||
goos: windows
|
|
||||||
goarch: arm64
|
|
||||||
- name: windows-arm32v7
|
|
||||||
goos: windows
|
|
||||||
goarch: arm
|
|
||||||
goarm: 7
|
|
||||||
|
|
||||||
# linux
|
|
||||||
- name: linux-amd64
|
|
||||||
goos: linux
|
|
||||||
goarch: amd64
|
|
||||||
goamd64: v1
|
|
||||||
- name: linux-amd64-v3
|
|
||||||
goos: linux
|
|
||||||
goarch: amd64
|
|
||||||
goamd64: v3
|
|
||||||
- name: linux-386
|
|
||||||
goos: linux
|
|
||||||
goarch: 386
|
|
||||||
- name: linux-arm64
|
|
||||||
goos: linux
|
|
||||||
goarch: arm64
|
|
||||||
- name: linux-armv5
|
|
||||||
goos: linux
|
|
||||||
goarch: arm
|
|
||||||
goarm: 5
|
|
||||||
- name: linux-armv6
|
|
||||||
goos: linux
|
|
||||||
goarch: arm
|
|
||||||
goarm: 6
|
|
||||||
- name: linux-armv7
|
|
||||||
goos: linux
|
|
||||||
goarch: arm
|
|
||||||
goarm: 7
|
|
||||||
- name: linux-mips-softfloat
|
|
||||||
goos: linux
|
|
||||||
goarch: mips
|
|
||||||
gomips: softfloat
|
|
||||||
- name: linux-mips-hardfloat
|
|
||||||
goos: linux
|
|
||||||
goarch: mips
|
|
||||||
gomips: hardfloat
|
|
||||||
- name: linux-mipsel-softfloat
|
|
||||||
goos: linux
|
|
||||||
goarch: mipsle
|
|
||||||
gomips: softfloat
|
|
||||||
- name: linux-mipsel-hardfloat
|
|
||||||
goos: linux
|
|
||||||
goarch: mipsle
|
|
||||||
gomips: hardfloat
|
|
||||||
- name: linux-mips64
|
|
||||||
goos: linux
|
|
||||||
goarch: mips64
|
|
||||||
- name: linux-mips64el
|
|
||||||
goos: linux
|
|
||||||
goarch: mips64le
|
|
||||||
- name: linux-s390x
|
|
||||||
goos: linux
|
|
||||||
goarch: s390x
|
|
||||||
# darwin
|
|
||||||
- name: darwin-amd64
|
|
||||||
goos: darwin
|
|
||||||
goarch: amd64
|
|
||||||
goamd64: v1
|
|
||||||
- name: darwin-amd64-v3
|
|
||||||
goos: darwin
|
|
||||||
goarch: amd64
|
|
||||||
goamd64: v3
|
|
||||||
- name: darwin-arm64
|
|
||||||
goos: darwin
|
|
||||||
goarch: arm64
|
|
||||||
# freebsd
|
|
||||||
- name: freebsd-amd64
|
|
||||||
goos: freebsd
|
|
||||||
goarch: amd64
|
|
||||||
goamd64: v1
|
|
||||||
- name: freebsd-amd64-v3
|
|
||||||
goos: freebsd
|
|
||||||
goarch: amd64
|
|
||||||
goamd64: v3
|
|
||||||
- name: freebsd-386
|
|
||||||
goos: freebsd
|
|
||||||
goarch: 386
|
|
||||||
- name: freebsd-arm64
|
|
||||||
goos: freebsd
|
|
||||||
goarch: arm64
|
|
||||||
fail-fast: true
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
GOOS: ${{ matrix.goos }}
|
|
||||||
GOARCH: ${{ matrix.goarch }}
|
|
||||||
GOAMD64: ${{ matrix.goamd64 }}
|
|
||||||
GOARM: ${{ matrix.goarm }}
|
|
||||||
GOMIPS: ${{ matrix.gomips }}
|
|
||||||
CGO_ENABLED: 0
|
|
||||||
TAGS: with_clash_api,with_quic
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: ^1.21
|
|
||||||
- name: Build
|
|
||||||
id: build
|
|
||||||
run: make
|
|
124
.github/workflows/docker.yml
vendored
124
.github/workflows/docker.yml
vendored
@ -1,39 +1,133 @@
|
|||||||
name: Build Docker Images
|
name: Publish Docker Images
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- published
|
- published
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: "The tag version you want to build"
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY_IMAGE: ghcr.io/sagernet/sing-box
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm/v6
|
||||||
|
- linux/arm/v7
|
||||||
|
- linux/arm64
|
||||||
|
- linux/386
|
||||||
|
- linux/ppc64le
|
||||||
|
- linux/riscv64
|
||||||
|
- linux/s390x
|
||||||
steps:
|
steps:
|
||||||
|
- name: Get commit to build
|
||||||
|
id: ref
|
||||||
|
run: |-
|
||||||
|
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
|
||||||
|
ref="${{ github.ref_name }}"
|
||||||
|
else
|
||||||
|
ref="${{ github.event.inputs.tag }}"
|
||||||
|
fi
|
||||||
|
echo "ref=$ref"
|
||||||
|
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
|
with:
|
||||||
|
ref: ${{ steps.ref.outputs.ref }}
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
platform=${{ matrix.platform }}
|
||||||
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
- name: Setup QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Setup QEMU for Docker Buildx
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Docker metadata
|
- name: Docker meta
|
||||||
id: metadata
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/sagernet/sing-box
|
images: ${{ env.REGISTRY_IMAGE }}
|
||||||
- name: Build and release Docker images
|
- name: Build and push by digest
|
||||||
uses: docker/build-push-action@v5
|
id: build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
platforms: linux/386,linux/amd64,linux/arm64,linux/s390x
|
platforms: ${{ matrix.platform }}
|
||||||
target: dist
|
context: .
|
||||||
build-args: |
|
build-args: |
|
||||||
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
||||||
tags: |
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
ghcr.io/sagernet/sing-box:latest
|
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||||
ghcr.io/sagernet/sing-box:${{ github.ref_name }}
|
- name: Export digest
|
||||||
push: true
|
run: |
|
||||||
|
mkdir -p /tmp/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: /tmp/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
merge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
steps:
|
||||||
|
- name: Get commit to build
|
||||||
|
id: ref
|
||||||
|
run: |-
|
||||||
|
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
|
||||||
|
ref="${{ github.ref_name }}"
|
||||||
|
else
|
||||||
|
ref="${{ github.event.inputs.tag }}"
|
||||||
|
fi
|
||||||
|
echo "ref=$ref"
|
||||||
|
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||||
|
if [[ $ref == *"-"* ]]; then
|
||||||
|
latest=latest-beta
|
||||||
|
else
|
||||||
|
latest=latest
|
||||||
|
fi
|
||||||
|
echo "latest=$latest"
|
||||||
|
echo "latest=$latest" >> $GITHUB_OUTPUT
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: /tmp/digests
|
||||||
|
pattern: digests-*
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Create manifest list and push
|
||||||
|
working-directory: /tmp/digests
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create \
|
||||||
|
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}" \
|
||||||
|
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
|
||||||
|
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||||
|
- name: Inspect image
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
|
||||||
|
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}
|
||||||
|
13
.github/workflows/lint.yml
vendored
13
.github/workflows/lint.yml
vendored
@ -22,20 +22,17 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.22
|
go-version: ^1.24.4
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: ${{ steps.version.outputs.go_version }}
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v4
|
uses: golangci/golangci-lint-action@v6
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: --timeout=30m
|
args: --timeout=30m
|
||||||
install-mode: binary
|
install-mode: binary
|
||||||
|
verify: false
|
||||||
|
184
.github/workflows/linux.yml
vendored
Normal file
184
.github/workflows/linux.yml
vendored
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
name: Build Linux Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Version name"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
calculate_version:
|
||||||
|
name: Calculate version
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.outputs.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ^1.24.4
|
||||||
|
- name: Check input version
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
run: |-
|
||||||
|
echo "version=${{ inputs.version }}"
|
||||||
|
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||||
|
- name: Calculate version
|
||||||
|
if: github.event_name != 'workflow_dispatch'
|
||||||
|
run: |-
|
||||||
|
go run -v ./cmd/internal/read_tag --ci --nightly
|
||||||
|
- name: Set outputs
|
||||||
|
id: outputs
|
||||||
|
run: |-
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
build:
|
||||||
|
name: Build binary
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- calculate_version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 }
|
||||||
|
- { os: linux, arch: "386", debian: i386, rpm: i386 }
|
||||||
|
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
|
||||||
|
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl }
|
||||||
|
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64 }
|
||||||
|
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
|
||||||
|
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
|
||||||
|
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||||
|
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||||
|
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64 }
|
||||||
|
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ^1.24.4
|
||||||
|
- name: Setup Android NDK
|
||||||
|
if: matrix.os == 'android'
|
||||||
|
uses: nttld/setup-ndk@v1
|
||||||
|
with:
|
||||||
|
ndk-version: r28
|
||||||
|
local-cache: true
|
||||||
|
- name: Set tag
|
||||||
|
run: |-
|
||||||
|
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||||
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
|
- name: Set build tags
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
|
||||||
|
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
mkdir -p dist
|
||||||
|
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||||
|
./cmd/sing-box
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
GOOS: ${{ matrix.os }}
|
||||||
|
GOARCH: ${{ matrix.arch }}
|
||||||
|
GOARM: ${{ matrix.goarm }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Set mtime
|
||||||
|
run: |-
|
||||||
|
TZ=UTC touch -t '197001010000' dist/sing-box
|
||||||
|
- name: Set name
|
||||||
|
if: ${{ ! contains(needs.calculate_version.outputs.version, '-') }}
|
||||||
|
run: |-
|
||||||
|
echo "NAME=sing-box" >> "$GITHUB_ENV"
|
||||||
|
- name: Set beta name
|
||||||
|
if: contains(needs.calculate_version.outputs.version, '-')
|
||||||
|
run: |-
|
||||||
|
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
|
||||||
|
- name: Set version
|
||||||
|
run: |-
|
||||||
|
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||||
|
PKG_VERSION="${PKG_VERSION//-/\~}"
|
||||||
|
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
|
||||||
|
- name: Package DEB
|
||||||
|
if: matrix.debian != ''
|
||||||
|
run: |
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
sudo apt-get install -y debsigs
|
||||||
|
cp .fpm_systemd .fpm
|
||||||
|
fpm -t deb \
|
||||||
|
--name "${NAME}" \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
|
||||||
|
--architecture ${{ matrix.debian }} \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
||||||
|
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
|
||||||
|
rm -rf $HOME/.gnupg
|
||||||
|
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||||
|
${{ secrets.GPG_KEY }}
|
||||||
|
EOF
|
||||||
|
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
|
||||||
|
- name: Package RPM
|
||||||
|
if: matrix.rpm != ''
|
||||||
|
run: |-
|
||||||
|
set -xeuo pipefail
|
||||||
|
sudo gem install fpm
|
||||||
|
cp .fpm_systemd .fpm
|
||||||
|
fpm -t rpm \
|
||||||
|
--name "${NAME}" \
|
||||||
|
-v "$PKG_VERSION" \
|
||||||
|
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
|
||||||
|
--architecture ${{ matrix.rpm }} \
|
||||||
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
cat > $HOME/.rpmmacros <<EOF
|
||||||
|
%_gpg_name ${{ secrets.GPG_KEY_ID }}
|
||||||
|
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
|
||||||
|
EOF
|
||||||
|
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||||
|
${{ secrets.GPG_KEY }}
|
||||||
|
EOF
|
||||||
|
rpmsign --addsign dist/*.rpm
|
||||||
|
- name: Cleanup
|
||||||
|
run: rm dist/sing-box
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.legacy_go && '-legacy' || '' }}
|
||||||
|
path: "dist"
|
||||||
|
upload:
|
||||||
|
name: Upload builds
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- calculate_version
|
||||||
|
- build
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set tag
|
||||||
|
run: |-
|
||||||
|
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||||
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
|
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||||
|
- name: Download builds
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: dist
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Publish packages
|
||||||
|
run: |-
|
||||||
|
ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/
|
3
.github/workflows/stale.yml
vendored
3
.github/workflows/stale.yml
vendored
@ -12,4 +12,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
|
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
days-before-close: 5
|
days-before-close: 5
|
||||||
|
exempt-issue-labels: 'bug,enhancement'
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -14,3 +14,5 @@
|
|||||||
/*.xcframework/
|
/*.xcframework/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/config.d/
|
/config.d/
|
||||||
|
/venv/
|
||||||
|
|
||||||
|
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[submodule "clients/apple"]
|
||||||
|
path = clients/apple
|
||||||
|
url = https://github.com/SagerNet/sing-box-for-apple.git
|
||||||
|
[submodule "clients/android"]
|
||||||
|
path = clients/android
|
||||||
|
url = https://github.com/SagerNet/sing-box-for-android.git
|
@ -6,14 +6,7 @@ linters:
|
|||||||
- gci
|
- gci
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- paralleltest
|
- paralleltest
|
||||||
|
- ineffassign
|
||||||
run:
|
|
||||||
skip-dirs:
|
|
||||||
- transport/simple-obfs
|
|
||||||
- transport/clashssr
|
|
||||||
- transport/cloudflaretls
|
|
||||||
- transport/shadowtls/tls
|
|
||||||
- transport/shadowtls/tls_go119
|
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
gci:
|
gci:
|
||||||
@ -23,4 +16,21 @@ linters-settings:
|
|||||||
- prefix(github.com/sagernet/)
|
- prefix(github.com/sagernet/)
|
||||||
- default
|
- default
|
||||||
staticcheck:
|
staticcheck:
|
||||||
go: '1.20'
|
checks:
|
||||||
|
- all
|
||||||
|
- -SA1003
|
||||||
|
|
||||||
|
run:
|
||||||
|
go: "1.23"
|
||||||
|
build-tags:
|
||||||
|
- with_gvisor
|
||||||
|
- with_quic
|
||||||
|
- with_dhcp
|
||||||
|
- with_wireguard
|
||||||
|
- with_utls
|
||||||
|
- with_acme
|
||||||
|
- with_clash_api
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-dirs:
|
||||||
|
- transport/simple-obfs
|
||||||
|
103
.goreleaser.fury.yaml
Normal file
103
.goreleaser.fury.yaml
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
project_name: sing-box
|
||||||
|
builds:
|
||||||
|
- id: main
|
||||||
|
main: ./cmd/sing-box
|
||||||
|
flags:
|
||||||
|
- -v
|
||||||
|
- -trimpath
|
||||||
|
ldflags:
|
||||||
|
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||||
|
- -s
|
||||||
|
- -buildid=
|
||||||
|
tags:
|
||||||
|
- with_gvisor
|
||||||
|
- with_quic
|
||||||
|
- with_dhcp
|
||||||
|
- with_wireguard
|
||||||
|
- with_utls
|
||||||
|
- with_acme
|
||||||
|
- with_clash_api
|
||||||
|
- with_tailscale
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
targets:
|
||||||
|
- linux_386
|
||||||
|
- linux_amd64_v1
|
||||||
|
- linux_arm64
|
||||||
|
- linux_arm_7
|
||||||
|
- linux_s390x
|
||||||
|
- linux_riscv64
|
||||||
|
- linux_mips64le
|
||||||
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ .Version }}.{{ .ShortCommit }}"
|
||||||
|
nfpms:
|
||||||
|
- &template
|
||||||
|
id: package
|
||||||
|
package_name: sing-box
|
||||||
|
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||||
|
builds:
|
||||||
|
- main
|
||||||
|
homepage: https://sing-box.sagernet.org/
|
||||||
|
maintainer: nekohasekai <contact-git@sekai.icu>
|
||||||
|
description: The universal proxy platform.
|
||||||
|
license: GPLv3 or later
|
||||||
|
formats:
|
||||||
|
- deb
|
||||||
|
- rpm
|
||||||
|
priority: extra
|
||||||
|
contents:
|
||||||
|
- src: release/config/config.json
|
||||||
|
dst: /etc/sing-box/config.json
|
||||||
|
type: "config|noreplace"
|
||||||
|
|
||||||
|
- src: release/config/sing-box.service
|
||||||
|
dst: /usr/lib/systemd/system/sing-box.service
|
||||||
|
- src: release/config/sing-box@.service
|
||||||
|
dst: /usr/lib/systemd/system/sing-box@.service
|
||||||
|
- src: release/config/sing-box.sysusers
|
||||||
|
dst: /usr/lib/sysusers.d/sing-box.conf
|
||||||
|
- src: release/config/sing-box.rules
|
||||||
|
dst: /usr/share/polkit-1/rules.d/sing-box.rules
|
||||||
|
- src: release/config/sing-box-split-dns.xml
|
||||||
|
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
||||||
|
|
||||||
|
- src: release/completions/sing-box.bash
|
||||||
|
dst: /usr/share/bash-completion/completions/sing-box.bash
|
||||||
|
- src: release/completions/sing-box.fish
|
||||||
|
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
|
||||||
|
- src: release/completions/sing-box.zsh
|
||||||
|
dst: /usr/share/zsh/site-functions/_sing-box
|
||||||
|
|
||||||
|
- src: LICENSE
|
||||||
|
dst: /usr/share/licenses/sing-box/LICENSE
|
||||||
|
deb:
|
||||||
|
signature:
|
||||||
|
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
||||||
|
fields:
|
||||||
|
Bugs: https://github.com/SagerNet/sing-box/issues
|
||||||
|
rpm:
|
||||||
|
signature:
|
||||||
|
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
||||||
|
conflicts:
|
||||||
|
- sing-box-beta
|
||||||
|
- id: package_beta
|
||||||
|
<<: *template
|
||||||
|
package_name: sing-box-beta
|
||||||
|
file_name_template: '{{ .ProjectName }}-beta_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||||
|
formats:
|
||||||
|
- deb
|
||||||
|
- rpm
|
||||||
|
conflicts:
|
||||||
|
- sing-box
|
||||||
|
release:
|
||||||
|
disable: true
|
||||||
|
furies:
|
||||||
|
- account: sagernet
|
||||||
|
ids:
|
||||||
|
- package
|
||||||
|
disable: "{{ not (not .Prerelease) }}"
|
||||||
|
- account: sagernet
|
||||||
|
ids:
|
||||||
|
- package_beta
|
||||||
|
disable: "{{ not .Prerelease }}"
|
168
.goreleaser.yaml
168
.goreleaser.yaml
@ -1,102 +1,73 @@
|
|||||||
|
version: 2
|
||||||
project_name: sing-box
|
project_name: sing-box
|
||||||
builds:
|
builds:
|
||||||
- id: main
|
- &template
|
||||||
|
id: main
|
||||||
main: ./cmd/sing-box
|
main: ./cmd/sing-box
|
||||||
flags:
|
flags:
|
||||||
- -v
|
- -v
|
||||||
- -trimpath
|
- -trimpath
|
||||||
asmflags:
|
|
||||||
- all=-trimpath={{.Env.GOPATH}}
|
|
||||||
gcflags:
|
|
||||||
- all=-trimpath={{.Env.GOPATH}}
|
|
||||||
ldflags:
|
ldflags:
|
||||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
|
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||||
|
- -s
|
||||||
|
- -buildid=
|
||||||
tags:
|
tags:
|
||||||
- with_gvisor
|
- with_gvisor
|
||||||
- with_quic
|
- with_quic
|
||||||
- with_dhcp
|
- with_dhcp
|
||||||
- with_wireguard
|
- with_wireguard
|
||||||
- with_ech
|
|
||||||
- with_utls
|
- with_utls
|
||||||
- with_reality_server
|
|
||||||
- with_acme
|
- with_acme
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
|
- with_tailscale
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
|
- GOTOOLCHAIN=local
|
||||||
targets:
|
targets:
|
||||||
- linux_386
|
- linux_386
|
||||||
- linux_amd64_v1
|
- linux_amd64_v1
|
||||||
- linux_amd64_v3
|
|
||||||
- linux_arm64
|
- linux_arm64
|
||||||
|
- linux_arm_6
|
||||||
- linux_arm_7
|
- linux_arm_7
|
||||||
- linux_s390x
|
- linux_s390x
|
||||||
|
- linux_riscv64
|
||||||
|
- linux_mips64le
|
||||||
- windows_amd64_v1
|
- windows_amd64_v1
|
||||||
- windows_amd64_v3
|
|
||||||
- windows_386
|
- windows_386
|
||||||
- windows_arm64
|
- windows_arm64
|
||||||
- darwin_amd64_v1
|
- darwin_amd64_v1
|
||||||
- darwin_amd64_v3
|
|
||||||
- darwin_arm64
|
- darwin_arm64
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
- id: legacy
|
- id: legacy
|
||||||
main: ./cmd/sing-box
|
<<: *template
|
||||||
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:
|
tags:
|
||||||
- with_gvisor
|
- with_gvisor
|
||||||
- with_quic
|
- with_quic
|
||||||
- with_dhcp
|
- with_dhcp
|
||||||
- with_wireguard
|
- with_wireguard
|
||||||
- with_utls
|
- with_utls
|
||||||
- with_reality_server
|
|
||||||
- with_acme
|
- with_acme
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
|
- with_tailscale
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
- GOROOT=/nix/store/kg6i737jjqs923jcijnm003h68c1dghj-go-1.20.11/share/go
|
- GOROOT={{ .Env.GOPATH }}/go_legacy
|
||||||
gobinary: /nix/store/kg6i737jjqs923jcijnm003h68c1dghj-go-1.20.11/bin/go
|
tool: "{{ .Env.GOPATH }}/go_legacy/bin/go"
|
||||||
targets:
|
targets:
|
||||||
- windows_amd64_v1
|
- windows_amd64_v1
|
||||||
- windows_386
|
- windows_386
|
||||||
- darwin_amd64_v1
|
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
|
||||||
- id: android
|
- id: android
|
||||||
main: ./cmd/sing-box
|
<<: *template
|
||||||
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:
|
env:
|
||||||
- CGO_ENABLED=1
|
- CGO_ENABLED=1
|
||||||
|
- GOTOOLCHAIN=local
|
||||||
overrides:
|
overrides:
|
||||||
- goos: android
|
- goos: android
|
||||||
goarch: arm
|
goarch: arm
|
||||||
goarm: 7
|
goarm: 7
|
||||||
env:
|
env:
|
||||||
- CC=armv7a-linux-androideabi19-clang
|
- CC=armv7a-linux-androideabi21-clang
|
||||||
- CXX=armv7a-linux-androideabi19-clang++
|
- CXX=armv7a-linux-androideabi21-clang++
|
||||||
- goos: android
|
- goos: android
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
env:
|
env:
|
||||||
@ -105,8 +76,8 @@ builds:
|
|||||||
- goos: android
|
- goos: android
|
||||||
goarch: 386
|
goarch: 386
|
||||||
env:
|
env:
|
||||||
- CC=i686-linux-android19-clang
|
- CC=i686-linux-android21-clang
|
||||||
- CXX=i686-linux-android19-clang++
|
- CXX=i686-linux-android21-clang++
|
||||||
- goos: android
|
- goos: android
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
goamd64: v1
|
goamd64: v1
|
||||||
@ -118,38 +89,33 @@ builds:
|
|||||||
- android_arm64
|
- android_arm64
|
||||||
- android_386
|
- android_386
|
||||||
- android_amd64
|
- android_amd64
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
|
||||||
snapshot:
|
|
||||||
name_template: "{{ .Version }}.{{ .ShortCommit }}"
|
|
||||||
archives:
|
archives:
|
||||||
- id: archive
|
- &template
|
||||||
|
id: archive
|
||||||
builds:
|
builds:
|
||||||
- main
|
- main
|
||||||
- android
|
- android
|
||||||
format: tar.gz
|
formats:
|
||||||
|
- tar.gz
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
formats:
|
||||||
|
- zip
|
||||||
wrap_in_directory: true
|
wrap_in_directory: true
|
||||||
files:
|
files:
|
||||||
- LICENSE
|
- LICENSE
|
||||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||||
- id: archive-legacy
|
- id: archive-legacy
|
||||||
|
<<: *template
|
||||||
builds:
|
builds:
|
||||||
- legacy
|
- legacy
|
||||||
format: tar.gz
|
|
||||||
format_overrides:
|
|
||||||
- goos: windows
|
|
||||||
format: zip
|
|
||||||
wrap_in_directory: true
|
|
||||||
files:
|
|
||||||
- LICENSE
|
|
||||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
|
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
|
||||||
nfpms:
|
nfpms:
|
||||||
- id: package
|
- id: package
|
||||||
package_name: sing-box
|
package_name: sing-box
|
||||||
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||||
vendor: sagernet
|
builds:
|
||||||
|
- main
|
||||||
homepage: https://sing-box.sagernet.org/
|
homepage: https://sing-box.sagernet.org/
|
||||||
maintainer: nekohasekai <contact-git@sekai.icu>
|
maintainer: nekohasekai <contact-git@sekai.icu>
|
||||||
description: The universal proxy platform.
|
description: The universal proxy platform.
|
||||||
@ -158,17 +124,71 @@ nfpms:
|
|||||||
- deb
|
- deb
|
||||||
- rpm
|
- rpm
|
||||||
- archlinux
|
- archlinux
|
||||||
|
# - apk
|
||||||
|
# - ipk
|
||||||
priority: extra
|
priority: extra
|
||||||
contents:
|
contents:
|
||||||
- src: release/config/config.json
|
- src: release/config/config.json
|
||||||
dst: /etc/sing-box/config.json
|
dst: /etc/sing-box/config.json
|
||||||
type: config
|
type: "config|noreplace"
|
||||||
|
|
||||||
- src: release/config/sing-box.service
|
- src: release/config/sing-box.service
|
||||||
dst: /etc/systemd/system/sing-box.service
|
dst: /usr/lib/systemd/system/sing-box.service
|
||||||
- src: release/config/sing-box@.service
|
- src: release/config/sing-box@.service
|
||||||
dst: /etc/systemd/system/sing-box@.service
|
dst: /usr/lib/systemd/system/sing-box@.service
|
||||||
|
- src: release/config/sing-box.sysusers
|
||||||
|
dst: /usr/lib/sysusers.d/sing-box.conf
|
||||||
|
- src: release/config/sing-box.rules
|
||||||
|
dst: /usr/share/polkit-1/rules.d/sing-box.rules
|
||||||
|
- src: release/config/sing-box-split-dns.xml
|
||||||
|
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
||||||
|
|
||||||
|
- src: release/completions/sing-box.bash
|
||||||
|
dst: /usr/share/bash-completion/completions/sing-box.bash
|
||||||
|
- src: release/completions/sing-box.fish
|
||||||
|
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
|
||||||
|
- src: release/completions/sing-box.zsh
|
||||||
|
dst: /usr/share/zsh/site-functions/_sing-box
|
||||||
|
|
||||||
- src: LICENSE
|
- src: LICENSE
|
||||||
dst: /usr/share/licenses/sing-box/LICENSE
|
dst: /usr/share/licenses/sing-box/LICENSE
|
||||||
|
deb:
|
||||||
|
signature:
|
||||||
|
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
||||||
|
fields:
|
||||||
|
Bugs: https://github.com/SagerNet/sing-box/issues
|
||||||
|
rpm:
|
||||||
|
signature:
|
||||||
|
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
||||||
|
overrides:
|
||||||
|
apk:
|
||||||
|
contents:
|
||||||
|
- src: release/config/config.json
|
||||||
|
dst: /etc/sing-box/config.json
|
||||||
|
type: config
|
||||||
|
|
||||||
|
- src: release/config/sing-box.initd
|
||||||
|
dst: /etc/init.d/sing-box
|
||||||
|
|
||||||
|
- src: release/completions/sing-box.bash
|
||||||
|
dst: /usr/share/bash-completion/completions/sing-box.bash
|
||||||
|
- src: release/completions/sing-box.fish
|
||||||
|
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
|
||||||
|
- src: release/completions/sing-box.zsh
|
||||||
|
dst: /usr/share/zsh/site-functions/_sing-box
|
||||||
|
|
||||||
|
- src: LICENSE
|
||||||
|
dst: /usr/share/licenses/sing-box/LICENSE
|
||||||
|
ipk:
|
||||||
|
contents:
|
||||||
|
- src: release/config/config.json
|
||||||
|
dst: /etc/sing-box/config.json
|
||||||
|
type: config
|
||||||
|
|
||||||
|
- src: release/config/openwrt.init
|
||||||
|
dst: /etc/init.d/sing-box
|
||||||
|
- src: release/config/openwrt.conf
|
||||||
|
dst: /etc/config/sing-box
|
||||||
source:
|
source:
|
||||||
enabled: false
|
enabled: false
|
||||||
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
|
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
|
||||||
@ -182,6 +202,12 @@ release:
|
|||||||
github:
|
github:
|
||||||
owner: SagerNet
|
owner: SagerNet
|
||||||
name: sing-box
|
name: sing-box
|
||||||
name_template: '{{ if .IsSnapshot }}{{ nightly }}{{ else }}{{ .Version }}{{ end }}'
|
|
||||||
draft: true
|
draft: true
|
||||||
mode: replace
|
prerelease: auto
|
||||||
|
mode: replace
|
||||||
|
ids:
|
||||||
|
- archive
|
||||||
|
- package
|
||||||
|
skip_upload: true
|
||||||
|
partial:
|
||||||
|
by: target
|
@ -1,4 +1,4 @@
|
|||||||
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder
|
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
|
||||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||||
COPY . /go/src/github.com/sagernet/sing-box
|
COPY . /go/src/github.com/sagernet/sing-box
|
||||||
WORKDIR /go/src/github.com/sagernet/sing-box
|
WORKDIR /go/src/github.com/sagernet/sing-box
|
||||||
@ -13,7 +13,7 @@ RUN set -ex \
|
|||||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||||
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||||
&& go build -v -trimpath -tags \
|
&& go build -v -trimpath -tags \
|
||||||
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \
|
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale" \
|
||||||
-o /go/bin/sing-box \
|
-o /go/bin/sing-box \
|
||||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
@ -21,7 +21,7 @@ FROM --platform=$TARGETPLATFORM alpine AS dist
|
|||||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||||
RUN set -ex \
|
RUN set -ex \
|
||||||
&& apk upgrade \
|
&& apk upgrade \
|
||||||
&& apk add bash tzdata ca-certificates \
|
&& apk add bash tzdata ca-certificates nftables \
|
||||||
&& rm -rf /var/cache/apk/*
|
&& rm -rf /var/cache/apk/*
|
||||||
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
||||||
ENTRYPOINT ["sing-box"]
|
ENTRYPOINT ["sing-box"]
|
||||||
|
158
Makefile
158
Makefile
@ -1,37 +1,30 @@
|
|||||||
NAME = sing-box
|
NAME = sing-box
|
||||||
COMMIT = $(shell git rev-parse --short HEAD)
|
COMMIT = $(shell git rev-parse --short HEAD)
|
||||||
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api
|
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale
|
||||||
TAGS_GO120 = with_quic,with_utls
|
|
||||||
TAGS_GO121 = with_ech
|
|
||||||
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121)
|
|
||||||
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
|
|
||||||
|
|
||||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||||
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
|
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
|
||||||
|
|
||||||
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
|
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
|
||||||
MAIN_PARAMS = $(PARAMS) -tags $(TAGS)
|
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
|
||||||
MAIN = ./cmd/sing-box
|
MAIN = ./cmd/sing-box
|
||||||
PREFIX ?= $(shell go env GOPATH)
|
PREFIX ?= $(shell go env GOPATH)
|
||||||
|
|
||||||
.PHONY: test release docs
|
.PHONY: test release docs build
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
export GOTOOLCHAIN=local && \
|
||||||
go build $(MAIN_PARAMS) $(MAIN)
|
go build $(MAIN_PARAMS) $(MAIN)
|
||||||
|
|
||||||
ci_build_go118:
|
|
||||||
go build $(PARAMS) $(MAIN)
|
|
||||||
go build $(PARAMS) -tags "$(TAGS_GO118)" $(MAIN)
|
|
||||||
|
|
||||||
ci_build_go120:
|
|
||||||
go build $(PARAMS) $(MAIN)
|
|
||||||
go build $(PARAMS) -tags "$(TAGS_GO118),$(TAGS_GO120)" $(MAIN)
|
|
||||||
|
|
||||||
ci_build:
|
ci_build:
|
||||||
go build $(PARAMS) $(MAIN)
|
export GOTOOLCHAIN=local && \
|
||||||
|
go build $(PARAMS) $(MAIN) && \
|
||||||
go build $(MAIN_PARAMS) $(MAIN)
|
go build $(MAIN_PARAMS) $(MAIN)
|
||||||
|
|
||||||
|
generate_completions:
|
||||||
|
go run -v --tags "$(TAGS),generate,generate_completions" $(MAIN)
|
||||||
|
|
||||||
install:
|
install:
|
||||||
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
|
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
|
||||||
|
|
||||||
@ -63,41 +56,53 @@ proto_install:
|
|||||||
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||||
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||||
|
|
||||||
|
update_certificates:
|
||||||
|
go run ./cmd/internal/update_certificates
|
||||||
|
|
||||||
release:
|
release:
|
||||||
go run ./cmd/internal/build goreleaser release --clean --skip-publish || exit 1
|
go run ./cmd/internal/build goreleaser release --clean --skip publish
|
||||||
mkdir dist/release
|
mkdir dist/release
|
||||||
mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/*.pkg.tar.zst dist/release
|
mv dist/*.tar.gz \
|
||||||
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release
|
dist/*.zip \
|
||||||
|
dist/*.deb \
|
||||||
|
dist/*.rpm \
|
||||||
|
dist/*_amd64.pkg.tar.zst \
|
||||||
|
dist/*_arm64.pkg.tar.zst \
|
||||||
|
dist/release
|
||||||
|
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||||
rm -r dist/release
|
rm -r dist/release
|
||||||
|
|
||||||
|
release_repo:
|
||||||
|
go run ./cmd/internal/build goreleaser release -f .goreleaser.fury.yaml --clean
|
||||||
|
|
||||||
release_install:
|
release_install:
|
||||||
go install -v github.com/goreleaser/goreleaser@latest
|
|
||||||
go install -v github.com/tcnksm/ghr@latest
|
go install -v github.com/tcnksm/ghr@latest
|
||||||
|
|
||||||
update_android_version:
|
update_android_version:
|
||||||
go run ./cmd/internal/update_android_version
|
go run ./cmd/internal/update_android_version
|
||||||
|
|
||||||
build_android:
|
build_android:
|
||||||
cd ../sing-box-for-android && ./gradlew :app:assemblePlayRelease && ./gradlew --stop
|
cd ../sing-box-for-android && ./gradlew :app:clean :app:assemblePlayRelease :app:assembleOtherRelease && ./gradlew --stop
|
||||||
|
|
||||||
upload_android:
|
upload_android:
|
||||||
mkdir -p dist/release_android
|
mkdir -p dist/release_android
|
||||||
cp ../sing-box-for-android/app/build/outputs/apk/play/release/*.apk 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
|
cp ../sing-box-for-android/app/build/outputs/apk/other/release/*-universal.apk dist/release_android
|
||||||
|
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
|
||||||
rm -rf dist/release_android
|
rm -rf dist/release_android
|
||||||
|
|
||||||
release_android: lib_android update_android_version build_android upload_android
|
release_android: lib_android update_android_version build_android upload_android
|
||||||
|
|
||||||
publish_android:
|
publish_android:
|
||||||
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle
|
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop
|
||||||
|
|
||||||
publish_android_appcenter:
|
|
||||||
cd ../sing-box-for-android && ./gradlew :app:appCenterAssembleAndUploadPlayRelease
|
|
||||||
|
|
||||||
|
# TODO: find why and remove `-destination 'generic/platform=iOS'`
|
||||||
|
# TODO: remove xcode clean when fix control widget fixed
|
||||||
build_ios:
|
build_ios:
|
||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
rm -rf build/SFI.xcarchive && \
|
rm -rf build/SFI.xcarchive && \
|
||||||
xcodebuild archive -scheme SFI -configuration Release -archivePath build/SFI.xcarchive
|
xcodebuild clean -scheme SFI && \
|
||||||
|
xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates
|
||||||
|
|
||||||
upload_ios_app_store:
|
upload_ios_app_store:
|
||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
@ -108,58 +113,89 @@ release_ios: build_ios upload_ios_app_store
|
|||||||
build_macos:
|
build_macos:
|
||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
rm -rf build/SFM.xcarchive && \
|
rm -rf build/SFM.xcarchive && \
|
||||||
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive
|
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates
|
||||||
|
|
||||||
upload_macos_app_store:
|
upload_macos_app_store:
|
||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
xcodebuild -exportArchive -archivePath build/SFM.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
xcodebuild -exportArchive -archivePath build/SFM.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
||||||
|
|
||||||
release_macos: build_macos upload_macos_app_store
|
release_macos: build_macos upload_macos_app_store
|
||||||
|
|
||||||
build_macos_independent:
|
build_macos_standalone:
|
||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
rm -rf build/SFT.System.xcarchive && \
|
rm -rf build/SFM.System.xcarchive && \
|
||||||
xcodebuild archive -scheme SFM.System -configuration Release -archivePath build/SFM.System.xcarchive
|
xcodebuild archive -scheme SFM.System -configuration Release -archivePath build/SFM.System.xcarchive -allowProvisioningUpdates
|
||||||
|
|
||||||
notarize_macos_independent:
|
build_macos_dmg:
|
||||||
cd ../sing-box-for-apple && \
|
|
||||||
xcodebuild -exportArchive -archivePath "build/SFM.System.xcarchive" -exportOptionsPlist SFM.System/Upload.plist -allowProvisioningUpdates
|
|
||||||
|
|
||||||
wait_notarize_macos_independent:
|
|
||||||
sleep 60
|
|
||||||
|
|
||||||
export_macos_independent:
|
|
||||||
rm -rf dist/SFM
|
rm -rf dist/SFM
|
||||||
mkdir -p dist/SFM
|
mkdir -p dist/SFM
|
||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
xcodebuild -exportNotarizedApp -archivePath build/SFM.System.xcarchive -exportPath "../sing-box/dist/SFM"
|
rm -rf build/SFM.System && \
|
||||||
|
rm -rf build/SFM.dmg && \
|
||||||
|
xcodebuild -exportArchive \
|
||||||
|
-archivePath "build/SFM.System.xcarchive" \
|
||||||
|
-exportOptionsPlist SFM.System/Export.plist -allowProvisioningUpdates \
|
||||||
|
-exportPath "build/SFM.System" && \
|
||||||
|
create-dmg \
|
||||||
|
--volname "sing-box" \
|
||||||
|
--volicon "build/SFM.System/SFM.app/Contents/Resources/AppIcon.icns" \
|
||||||
|
--icon "SFM.app" 0 0 \
|
||||||
|
--hide-extension "SFM.app" \
|
||||||
|
--app-drop-link 0 0 \
|
||||||
|
--skip-jenkins \
|
||||||
|
"../sing-box/dist/SFM/SFM.dmg" "build/SFM.System/SFM.app"
|
||||||
|
|
||||||
upload_macos_independent:
|
notarize_macos_dmg:
|
||||||
|
xcrun notarytool submit "dist/SFM/SFM.dmg" --wait \
|
||||||
|
--keychain-profile "notarytool-password" \
|
||||||
|
--no-s3-acceleration
|
||||||
|
|
||||||
|
upload_macos_dmg:
|
||||||
cd dist/SFM && \
|
cd dist/SFM && \
|
||||||
rm -f *.zip && \
|
cp SFM.dmg "SFM-${VERSION}-universal.dmg" && \
|
||||||
zip -ry "SFM-${VERSION}-universal.zip" SFM.app && \
|
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dmg"
|
||||||
ghr --replace --draft --prerelease "v${VERSION}" *.zip
|
|
||||||
|
|
||||||
release_macos_independent: build_macos_independent notarize_macos_independent wait_notarize_macos_independent export_macos_independent upload_macos_independent
|
upload_macos_dsyms:
|
||||||
|
pushd ../sing-box-for-apple/build/SFM.System.xcarchive && \
|
||||||
|
zip -r SFM.dSYMs.zip dSYMs && \
|
||||||
|
mv SFM.dSYMs.zip ../../../sing-box/dist/SFM && \
|
||||||
|
popd && \
|
||||||
|
cd dist/SFM && \
|
||||||
|
cp SFM.dSYMs.zip "SFM-${VERSION}-universal.dSYMs.zip" && \
|
||||||
|
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dSYMs.zip"
|
||||||
|
|
||||||
|
release_macos_standalone: build_macos_standalone build_macos_dmg notarize_macos_dmg upload_macos_dmg upload_macos_dsyms
|
||||||
|
|
||||||
build_tvos:
|
build_tvos:
|
||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
rm -rf build/SFT.xcarchive && \
|
rm -rf build/SFT.xcarchive && \
|
||||||
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive
|
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive -allowProvisioningUpdates
|
||||||
|
|
||||||
upload_tvos_app_store:
|
upload_tvos_app_store:
|
||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
||||||
|
|
||||||
release_tvos: build_tvos upload_tvos_app_store
|
release_tvos: build_tvos upload_tvos_app_store
|
||||||
|
|
||||||
update_apple_version:
|
update_apple_version:
|
||||||
go run ./cmd/internal/update_apple_version
|
go run ./cmd/internal/update_apple_version
|
||||||
|
|
||||||
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_independent
|
update_macos_version:
|
||||||
|
MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version
|
||||||
|
|
||||||
|
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_standalone
|
||||||
|
|
||||||
release_apple_beta: update_apple_version release_ios release_macos release_tvos
|
release_apple_beta: update_apple_version release_ios release_macos release_tvos
|
||||||
|
|
||||||
|
publish_testflight:
|
||||||
|
go run -v ./cmd/internal/app_store_connect publish_testflight
|
||||||
|
|
||||||
|
prepare_app_store:
|
||||||
|
go run -v ./cmd/internal/app_store_connect prepare_app_store
|
||||||
|
|
||||||
|
publish_app_store:
|
||||||
|
go run -v ./cmd/internal/app_store_connect publish_app_store
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@go test -v ./... && \
|
@go test -v ./... && \
|
||||||
cd test && \
|
cd test && \
|
||||||
@ -175,25 +211,33 @@ test_stdio:
|
|||||||
lib_android:
|
lib_android:
|
||||||
go run ./cmd/internal/build_libbox -target android
|
go run ./cmd/internal/build_libbox -target android
|
||||||
|
|
||||||
|
lib_android_debug:
|
||||||
|
go run ./cmd/internal/build_libbox -target android -debug
|
||||||
|
|
||||||
|
lib_apple:
|
||||||
|
go run ./cmd/internal/build_libbox -target apple
|
||||||
|
|
||||||
lib_ios:
|
lib_ios:
|
||||||
go run ./cmd/internal/build_libbox -target ios
|
go run ./cmd/internal/build_libbox -target apple -platform ios -debug
|
||||||
|
|
||||||
lib:
|
lib:
|
||||||
go run ./cmd/internal/build_libbox -target android
|
go run ./cmd/internal/build_libbox -target android
|
||||||
go run ./cmd/internal/build_libbox -target ios
|
go run ./cmd/internal/build_libbox -target ios
|
||||||
|
|
||||||
lib_install:
|
lib_install:
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.1
|
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.6
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.1
|
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.6
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
mkdocs serve
|
venv/bin/mkdocs serve
|
||||||
|
|
||||||
publish_docs:
|
publish_docs:
|
||||||
mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
|
venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
|
||||||
|
|
||||||
docs_install:
|
docs_install:
|
||||||
pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
|
python -m venv venv
|
||||||
|
source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf bin dist sing-box
|
rm -rf bin dist sing-box
|
||||||
rm -f $(shell go env GOPATH)/sing-box
|
rm -f $(shell go env GOPATH)/sing-box
|
||||||
@ -201,4 +245,4 @@ clean:
|
|||||||
update:
|
update:
|
||||||
git fetch
|
git fetch
|
||||||
git reset FETCH_HEAD --hard
|
git reset FETCH_HEAD --hard
|
||||||
git clean -fdx
|
git clean -fdx
|
||||||
|
@ -8,10 +8,6 @@ The universal proxy platform.
|
|||||||
|
|
||||||
https://sing-box.sagernet.org
|
https://sing-box.sagernet.org
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
https://community.sagernet.org/c/sing-box/
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
```
|
```
|
||||||
|
21
adapter/certificate.go
Normal file
21
adapter/certificate.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CertificateStore interface {
|
||||||
|
LifecycleService
|
||||||
|
Pool() *x509.CertPool
|
||||||
|
}
|
||||||
|
|
||||||
|
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
||||||
|
store := service.FromContext[CertificateStore](ctx)
|
||||||
|
if store == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return store.Pool()
|
||||||
|
}
|
@ -1,104 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
14
adapter/connections.go
Normal file
14
adapter/connections.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConnectionManager interface {
|
||||||
|
Lifecycle
|
||||||
|
NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||||
|
NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||||
|
}
|
94
adapter/dns.go
Normal file
94
adapter/dns.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DNSRouter interface {
|
||||||
|
Lifecycle
|
||||||
|
Exchange(ctx context.Context, message *dns.Msg, options DNSQueryOptions) (*dns.Msg, error)
|
||||||
|
Lookup(ctx context.Context, domain string, options DNSQueryOptions) ([]netip.Addr, error)
|
||||||
|
ClearCache()
|
||||||
|
LookupReverseMapping(ip netip.Addr) (string, bool)
|
||||||
|
ResetNetwork()
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSClient interface {
|
||||||
|
Start()
|
||||||
|
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
||||||
|
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
||||||
|
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
|
||||||
|
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
|
||||||
|
ClearCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSQueryOptions struct {
|
||||||
|
Transport DNSTransport
|
||||||
|
Strategy C.DomainStrategy
|
||||||
|
LookupStrategy C.DomainStrategy
|
||||||
|
DisableCache bool
|
||||||
|
RewriteTTL *uint32
|
||||||
|
ClientSubnet netip.Prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
||||||
|
if options == nil {
|
||||||
|
return &DNSQueryOptions{}, nil
|
||||||
|
}
|
||||||
|
transportManager := service.FromContext[DNSTransportManager](ctx)
|
||||||
|
transport, loaded := transportManager.Transport(options.Server)
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("domain resolver not found: " + options.Server)
|
||||||
|
}
|
||||||
|
return &DNSQueryOptions{
|
||||||
|
Transport: transport,
|
||||||
|
Strategy: C.DomainStrategy(options.Strategy),
|
||||||
|
DisableCache: options.DisableCache,
|
||||||
|
RewriteTTL: options.RewriteTTL,
|
||||||
|
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RDRCStore interface {
|
||||||
|
LoadRDRC(transportName string, qName string, qType uint16) (rejected bool)
|
||||||
|
SaveRDRC(transportName string, qName string, qType uint16) error
|
||||||
|
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSTransport interface {
|
||||||
|
Lifecycle
|
||||||
|
Type() string
|
||||||
|
Tag() string
|
||||||
|
Dependencies() []string
|
||||||
|
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LegacyDNSTransport interface {
|
||||||
|
LegacyStrategy() C.DomainStrategy
|
||||||
|
LegacyClientSubnet() netip.Prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSTransportRegistry interface {
|
||||||
|
option.DNSTransportOptionsRegistry
|
||||||
|
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSTransportManager interface {
|
||||||
|
Lifecycle
|
||||||
|
Transports() []DNSTransport
|
||||||
|
Transport(tag string) (DNSTransport, bool)
|
||||||
|
Default() DNSTransport
|
||||||
|
FakeIP() FakeIPTransport
|
||||||
|
Remove(tag string) error
|
||||||
|
Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error
|
||||||
|
}
|
28
adapter/endpoint.go
Normal file
28
adapter/endpoint.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Endpoint interface {
|
||||||
|
Lifecycle
|
||||||
|
Type() string
|
||||||
|
Tag() string
|
||||||
|
Outbound
|
||||||
|
}
|
||||||
|
|
||||||
|
type EndpointRegistry interface {
|
||||||
|
option.EndpointOptionsRegistry
|
||||||
|
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) (Endpoint, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type EndpointManager interface {
|
||||||
|
Lifecycle
|
||||||
|
Endpoints() []Endpoint
|
||||||
|
Get(tag string) (Endpoint, bool)
|
||||||
|
Remove(tag string) error
|
||||||
|
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) error
|
||||||
|
}
|
43
adapter/endpoint/adapter.go
Normal file
43
adapter/endpoint/adapter.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package endpoint
|
||||||
|
|
||||||
|
import "github.com/sagernet/sing-box/option"
|
||||||
|
|
||||||
|
type Adapter struct {
|
||||||
|
endpointType string
|
||||||
|
endpointTag string
|
||||||
|
network []string
|
||||||
|
dependencies []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdapter(endpointType string, endpointTag string, network []string, dependencies []string) Adapter {
|
||||||
|
return Adapter{
|
||||||
|
endpointType: endpointType,
|
||||||
|
endpointTag: endpointTag,
|
||||||
|
network: network,
|
||||||
|
dependencies: dependencies,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdapterWithDialerOptions(endpointType string, endpointTag string, network []string, dialOptions option.DialerOptions) Adapter {
|
||||||
|
var dependencies []string
|
||||||
|
if dialOptions.Detour != "" {
|
||||||
|
dependencies = []string{dialOptions.Detour}
|
||||||
|
}
|
||||||
|
return NewAdapter(endpointType, endpointTag, network, dependencies)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Type() string {
|
||||||
|
return a.endpointType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Tag() string {
|
||||||
|
return a.endpointTag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Network() []string {
|
||||||
|
return a.network
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Dependencies() []string {
|
||||||
|
return a.dependencies
|
||||||
|
}
|
147
adapter/endpoint/manager.go
Normal file
147
adapter/endpoint/manager.go
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
package endpoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.EndpointManager = (*Manager)(nil)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
logger log.ContextLogger
|
||||||
|
registry adapter.EndpointRegistry
|
||||||
|
access sync.Mutex
|
||||||
|
started bool
|
||||||
|
stage adapter.StartStage
|
||||||
|
endpoints []adapter.Endpoint
|
||||||
|
endpointByTag map[string]adapter.Endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Manager {
|
||||||
|
return &Manager{
|
||||||
|
logger: logger,
|
||||||
|
registry: registry,
|
||||||
|
endpointByTag: make(map[string]adapter.Endpoint),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
if m.started && m.stage >= stage {
|
||||||
|
panic("already started")
|
||||||
|
}
|
||||||
|
m.started = true
|
||||||
|
m.stage = stage
|
||||||
|
if stage == adapter.StartStateStart {
|
||||||
|
// started with outbound manager
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, endpoint := range m.endpoints {
|
||||||
|
err := adapter.LegacyStart(endpoint, stage)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Close() error {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
if !m.started {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.started = false
|
||||||
|
endpoints := m.endpoints
|
||||||
|
m.endpoints = nil
|
||||||
|
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||||
|
var err error
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
monitor.Start("close endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
||||||
|
err = E.Append(err, endpoint.Close(), func(err error) error {
|
||||||
|
return E.Cause(err, "close endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
||||||
|
})
|
||||||
|
monitor.Finish()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Endpoints() []adapter.Endpoint {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
return m.endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Get(tag string) (adapter.Endpoint, bool) {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
endpoint, found := m.endpointByTag[tag]
|
||||||
|
return endpoint, found
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Remove(tag string) error {
|
||||||
|
m.access.Lock()
|
||||||
|
endpoint, found := m.endpointByTag[tag]
|
||||||
|
if !found {
|
||||||
|
m.access.Unlock()
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
delete(m.endpointByTag, tag)
|
||||||
|
index := common.Index(m.endpoints, func(it adapter.Endpoint) bool {
|
||||||
|
return it == endpoint
|
||||||
|
})
|
||||||
|
if index == -1 {
|
||||||
|
panic("invalid endpoint index")
|
||||||
|
}
|
||||||
|
m.endpoints = append(m.endpoints[:index], m.endpoints[index+1:]...)
|
||||||
|
started := m.started
|
||||||
|
m.access.Unlock()
|
||||||
|
if started {
|
||||||
|
return endpoint.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
|
||||||
|
endpoint, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
if m.started {
|
||||||
|
for _, stage := range adapter.ListStartStages {
|
||||||
|
err = adapter.LegacyStart(endpoint, stage)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if existsEndpoint, loaded := m.endpointByTag[tag]; loaded {
|
||||||
|
if m.started {
|
||||||
|
err = existsEndpoint.Close()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "close endpoint/", existsEndpoint.Type(), "[", existsEndpoint.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existsIndex := common.Index(m.endpoints, func(it adapter.Endpoint) bool {
|
||||||
|
return it == existsEndpoint
|
||||||
|
})
|
||||||
|
if existsIndex == -1 {
|
||||||
|
panic("invalid endpoint index")
|
||||||
|
}
|
||||||
|
m.endpoints = append(m.endpoints[:existsIndex], m.endpoints[existsIndex+1:]...)
|
||||||
|
}
|
||||||
|
m.endpoints = append(m.endpoints, endpoint)
|
||||||
|
m.endpointByTag[tag] = endpoint
|
||||||
|
return nil
|
||||||
|
}
|
72
adapter/endpoint/registry.go
Normal file
72
adapter/endpoint/registry.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package endpoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Endpoint, error)
|
||||||
|
|
||||||
|
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
|
||||||
|
registry.register(outboundType, func() any {
|
||||||
|
return new(Options)
|
||||||
|
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Endpoint, error) {
|
||||||
|
var options *Options
|
||||||
|
if rawOptions != nil {
|
||||||
|
options = rawOptions.(*Options)
|
||||||
|
}
|
||||||
|
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ adapter.EndpointRegistry = (*Registry)(nil)
|
||||||
|
|
||||||
|
type (
|
||||||
|
optionsConstructorFunc func() any
|
||||||
|
constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Endpoint, error)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Registry struct {
|
||||||
|
access sync.Mutex
|
||||||
|
optionsType map[string]optionsConstructorFunc
|
||||||
|
constructor map[string]constructorFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistry() *Registry {
|
||||||
|
return &Registry{
|
||||||
|
optionsType: make(map[string]optionsConstructorFunc),
|
||||||
|
constructor: make(map[string]constructorFunc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
optionsConstructor, loaded := m.optionsType[outboundType]
|
||||||
|
if !loaded {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return optionsConstructor(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Endpoint, error) {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
constructor, loaded := m.constructor[outboundType]
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("outbound type not found: " + outboundType)
|
||||||
|
}
|
||||||
|
return constructor(ctx, router, logger, tag, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
m.optionsType[outboundType] = optionsConstructor
|
||||||
|
m.constructor[outboundType] = constructor
|
||||||
|
}
|
@ -4,83 +4,91 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/urltest"
|
"github.com/sagernet/sing/common/varbin"
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/sagernet/sing/common/rw"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClashServer interface {
|
type ClashServer interface {
|
||||||
Service
|
LifecycleService
|
||||||
PreStarter
|
ConnectionTracker
|
||||||
Mode() string
|
Mode() string
|
||||||
ModeList() []string
|
ModeList() []string
|
||||||
HistoryStorage() *urltest.HistoryStorage
|
HistoryStorage() URLTestHistoryStorage
|
||||||
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 URLTestHistory struct {
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
Delay uint16 `json:"delay"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type URLTestHistoryStorage interface {
|
||||||
|
SetHook(hook chan<- struct{})
|
||||||
|
LoadURLTestHistory(tag string) *URLTestHistory
|
||||||
|
DeleteURLTestHistory(tag string)
|
||||||
|
StoreURLTestHistory(tag string, history *URLTestHistory)
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type V2RayServer interface {
|
||||||
|
LifecycleService
|
||||||
|
StatsService() ConnectionTracker
|
||||||
}
|
}
|
||||||
|
|
||||||
type CacheFile interface {
|
type CacheFile interface {
|
||||||
Service
|
LifecycleService
|
||||||
PreStarter
|
|
||||||
|
|
||||||
StoreFakeIP() bool
|
StoreFakeIP() bool
|
||||||
FakeIPStorage
|
FakeIPStorage
|
||||||
|
|
||||||
|
StoreRDRC() bool
|
||||||
|
RDRCStore
|
||||||
|
|
||||||
LoadMode() string
|
LoadMode() string
|
||||||
StoreMode(mode string) error
|
StoreMode(mode string) error
|
||||||
LoadSelected(group string) string
|
LoadSelected(group string) string
|
||||||
StoreSelected(group string, selected string) error
|
StoreSelected(group string, selected string) error
|
||||||
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
||||||
StoreGroupExpand(group string, expand bool) error
|
StoreGroupExpand(group string, expand bool) error
|
||||||
LoadRuleSet(tag string) *SavedRuleSet
|
LoadRuleSet(tag string) *SavedBinary
|
||||||
SaveRuleSet(tag string, set *SavedRuleSet) error
|
SaveRuleSet(tag string, set *SavedBinary) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type SavedRuleSet struct {
|
type SavedBinary struct {
|
||||||
Content []byte
|
Content []byte
|
||||||
LastUpdated time.Time
|
LastUpdated time.Time
|
||||||
LastEtag string
|
LastEtag string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
|
func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
|
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = rw.WriteUVariant(&buffer, uint64(len(s.Content)))
|
err = varbin.Write(&buffer, binary.BigEndian, s.Content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
buffer.Write(s.Content)
|
|
||||||
err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix())
|
err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = rw.WriteVString(&buffer, s.LastEtag)
|
err = varbin.Write(&buffer, binary.BigEndian, s.LastEtag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return buffer.Bytes(), nil
|
return buffer.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
|
func (s *SavedBinary) UnmarshalBinary(data []byte) error {
|
||||||
reader := bytes.NewReader(data)
|
reader := bytes.NewReader(data)
|
||||||
var version uint8
|
var version uint8
|
||||||
err := binary.Read(reader, binary.BigEndian, &version)
|
err := binary.Read(reader, binary.BigEndian, &version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
contentLen, err := rw.ReadUVariant(reader)
|
err = varbin.Read(reader, binary.BigEndian, &s.Content)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.Content = make([]byte, contentLen)
|
|
||||||
_, err = io.ReadFull(reader, s.Content)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -90,17 +98,13 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.LastUpdated = time.Unix(lastUpdated, 0)
|
s.LastUpdated = time.Unix(lastUpdated, 0)
|
||||||
s.LastEtag, err = rw.ReadVString(reader)
|
err = varbin.Read(reader, binary.BigEndian, &s.LastEtag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tracker interface {
|
|
||||||
Leave()
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutboundGroup interface {
|
type OutboundGroup interface {
|
||||||
Outbound
|
Outbound
|
||||||
Now() string
|
Now() string
|
||||||
@ -118,13 +122,3 @@ func OutboundTag(detour Outbound) string {
|
|||||||
}
|
}
|
||||||
return detour.Tag()
|
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
|
|
||||||
}
|
|
||||||
|
@ -3,12 +3,11 @@ package adapter
|
|||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/sagernet/sing-dns"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FakeIPStore interface {
|
type FakeIPStore interface {
|
||||||
Service
|
SimpleLifecycle
|
||||||
Contains(address netip.Addr) bool
|
Contains(address netip.Addr) bool
|
||||||
Create(domain string, isIPv6 bool) (netip.Addr, error)
|
Create(domain string, isIPv6 bool) (netip.Addr, error)
|
||||||
Lookup(address netip.Addr) (string, bool)
|
Lookup(address netip.Addr) (string, bool)
|
||||||
@ -27,6 +26,6 @@ type FakeIPStorage interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FakeIPTransport interface {
|
type FakeIPTransport interface {
|
||||||
dns.Transport
|
DNSTransport
|
||||||
Store() FakeIPStore
|
Store() FakeIPStore
|
||||||
}
|
}
|
||||||
|
@ -6,27 +6,56 @@ import (
|
|||||||
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Deprecated
|
||||||
type ConnectionHandler interface {
|
type ConnectionHandler interface {
|
||||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConnectionHandlerEx interface {
|
||||||
|
NewConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: use PacketHandlerEx instead
|
||||||
type PacketHandler interface {
|
type PacketHandler interface {
|
||||||
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error
|
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PacketHandlerEx interface {
|
||||||
|
NewPacketEx(buffer *buf.Buffer, source M.Socksaddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: use OOBPacketHandlerEx instead
|
||||||
type OOBPacketHandler interface {
|
type OOBPacketHandler interface {
|
||||||
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error
|
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OOBPacketHandlerEx interface {
|
||||||
|
NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated
|
||||||
type PacketConnectionHandler interface {
|
type PacketConnectionHandler interface {
|
||||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PacketConnectionHandlerEx interface {
|
||||||
|
NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: use TCPConnectionHandlerEx instead
|
||||||
|
//
|
||||||
|
//nolint:staticcheck
|
||||||
type UpstreamHandlerAdapter interface {
|
type UpstreamHandlerAdapter interface {
|
||||||
N.TCPConnectionHandler
|
N.TCPConnectionHandler
|
||||||
N.UDPConnectionHandler
|
N.UDPConnectionHandler
|
||||||
E.Handler
|
E.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpstreamHandlerAdapterEx interface {
|
||||||
|
N.TCPConnectionHandlerEx
|
||||||
|
N.UDPConnectionHandlerEx
|
||||||
|
}
|
||||||
|
@ -2,26 +2,43 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/process"
|
"github.com/sagernet/sing-box/common/process"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Inbound interface {
|
type Inbound interface {
|
||||||
Service
|
Lifecycle
|
||||||
Type() string
|
Type() string
|
||||||
Tag() string
|
Tag() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type InjectableInbound interface {
|
type TCPInjectableInbound interface {
|
||||||
Inbound
|
Inbound
|
||||||
Network() []string
|
ConnectionHandlerEx
|
||||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
}
|
||||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
|
||||||
|
type UDPInjectableInbound interface {
|
||||||
|
Inbound
|
||||||
|
PacketConnectionHandlerEx
|
||||||
|
}
|
||||||
|
|
||||||
|
type InboundRegistry interface {
|
||||||
|
option.InboundOptionsRegistry
|
||||||
|
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type InboundManager interface {
|
||||||
|
Lifecycle
|
||||||
|
Inbounds() []Inbound
|
||||||
|
Get(tag string) (Inbound, bool)
|
||||||
|
Remove(tag string) error
|
||||||
|
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type InboundContext struct {
|
type InboundContext struct {
|
||||||
@ -31,17 +48,39 @@ type InboundContext struct {
|
|||||||
Network string
|
Network string
|
||||||
Source M.Socksaddr
|
Source M.Socksaddr
|
||||||
Destination M.Socksaddr
|
Destination M.Socksaddr
|
||||||
Domain string
|
|
||||||
Protocol string
|
|
||||||
User string
|
User string
|
||||||
Outbound string
|
Outbound string
|
||||||
|
|
||||||
|
// sniffer
|
||||||
|
|
||||||
|
Protocol string
|
||||||
|
Domain string
|
||||||
|
Client string
|
||||||
|
SniffContext any
|
||||||
|
PacketSniffError error
|
||||||
|
|
||||||
// cache
|
// cache
|
||||||
|
|
||||||
InboundDetour string
|
// Deprecated: implement in rule action
|
||||||
LastInbound string
|
InboundDetour string
|
||||||
OriginDestination M.Socksaddr
|
LastInbound string
|
||||||
InboundOptions option.InboundOptions
|
OriginDestination M.Socksaddr
|
||||||
|
RouteOriginalDestination M.Socksaddr
|
||||||
|
// Deprecated: to be removed
|
||||||
|
//nolint:staticcheck
|
||||||
|
InboundOptions option.InboundOptions
|
||||||
|
UDPDisableDomainUnmapping bool
|
||||||
|
UDPConnect bool
|
||||||
|
UDPTimeout time.Duration
|
||||||
|
TLSFragment bool
|
||||||
|
TLSFragmentFallbackDelay time.Duration
|
||||||
|
TLSRecordFragment bool
|
||||||
|
|
||||||
|
NetworkStrategy *C.NetworkStrategy
|
||||||
|
NetworkType []C.InterfaceType
|
||||||
|
FallbackNetworkType []C.InterfaceType
|
||||||
|
FallbackDelay time.Duration
|
||||||
|
|
||||||
DestinationAddresses []netip.Addr
|
DestinationAddresses []netip.Addr
|
||||||
SourceGeoIPCode string
|
SourceGeoIPCode string
|
||||||
GeoIPCode string
|
GeoIPCode string
|
||||||
@ -51,7 +90,9 @@ type InboundContext struct {
|
|||||||
|
|
||||||
// rule cache
|
// rule cache
|
||||||
|
|
||||||
IPCIDRMatchSource bool
|
IPCIDRMatchSource bool
|
||||||
|
IPCIDRAcceptEmpty bool
|
||||||
|
|
||||||
SourceAddressMatch bool
|
SourceAddressMatch bool
|
||||||
SourcePortMatch bool
|
SourcePortMatch bool
|
||||||
DestinationAddressMatch bool
|
DestinationAddressMatch bool
|
||||||
@ -62,10 +103,12 @@ type InboundContext struct {
|
|||||||
|
|
||||||
func (c *InboundContext) ResetRuleCache() {
|
func (c *InboundContext) ResetRuleCache() {
|
||||||
c.IPCIDRMatchSource = false
|
c.IPCIDRMatchSource = false
|
||||||
|
c.IPCIDRAcceptEmpty = false
|
||||||
c.SourceAddressMatch = false
|
c.SourceAddressMatch = false
|
||||||
c.SourcePortMatch = false
|
c.SourcePortMatch = false
|
||||||
c.DestinationAddressMatch = false
|
c.DestinationAddressMatch = false
|
||||||
c.DestinationPortMatch = false
|
c.DestinationPortMatch = false
|
||||||
|
c.DidMatch = false
|
||||||
}
|
}
|
||||||
|
|
||||||
type inboundContextKey struct{}
|
type inboundContextKey struct{}
|
||||||
@ -82,15 +125,6 @@ func ContextFrom(ctx context.Context) *InboundContext {
|
|||||||
return metadata.(*InboundContext)
|
return metadata.(*InboundContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func AppendContext(ctx context.Context) (context.Context, *InboundContext) {
|
|
||||||
metadata := ContextFrom(ctx)
|
|
||||||
if metadata != nil {
|
|
||||||
return ctx, metadata
|
|
||||||
}
|
|
||||||
metadata = new(InboundContext)
|
|
||||||
return WithContext(ctx, metadata), metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExtendContext(ctx context.Context) (context.Context, *InboundContext) {
|
func ExtendContext(ctx context.Context) (context.Context, *InboundContext) {
|
||||||
var newMetadata InboundContext
|
var newMetadata InboundContext
|
||||||
if metadata := ContextFrom(ctx); metadata != nil {
|
if metadata := ContextFrom(ctx); metadata != nil {
|
||||||
@ -98,3 +132,12 @@ func ExtendContext(ctx context.Context) (context.Context, *InboundContext) {
|
|||||||
}
|
}
|
||||||
return WithContext(ctx, &newMetadata), &newMetadata
|
return WithContext(ctx, &newMetadata), &newMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func OverrideContext(ctx context.Context) context.Context {
|
||||||
|
if metadata := ContextFrom(ctx); metadata != nil {
|
||||||
|
var newMetadata InboundContext
|
||||||
|
newMetadata = *metadata
|
||||||
|
return WithContext(ctx, &newMetadata)
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
21
adapter/inbound/adapter.go
Normal file
21
adapter/inbound/adapter.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package inbound
|
||||||
|
|
||||||
|
type Adapter struct {
|
||||||
|
inboundType string
|
||||||
|
inboundTag string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdapter(inboundType string, inboundTag string) Adapter {
|
||||||
|
return Adapter{
|
||||||
|
inboundType: inboundType,
|
||||||
|
inboundTag: inboundTag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Type() string {
|
||||||
|
return a.inboundType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Tag() string {
|
||||||
|
return a.inboundTag
|
||||||
|
}
|
149
adapter/inbound/manager.go
Normal file
149
adapter/inbound/manager.go
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
package inbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.InboundManager = (*Manager)(nil)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
logger log.ContextLogger
|
||||||
|
registry adapter.InboundRegistry
|
||||||
|
endpoint adapter.EndpointManager
|
||||||
|
access sync.Mutex
|
||||||
|
started bool
|
||||||
|
stage adapter.StartStage
|
||||||
|
inbounds []adapter.Inbound
|
||||||
|
inboundByTag map[string]adapter.Inbound
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager {
|
||||||
|
return &Manager{
|
||||||
|
logger: logger,
|
||||||
|
registry: registry,
|
||||||
|
endpoint: endpoint,
|
||||||
|
inboundByTag: make(map[string]adapter.Inbound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||||
|
m.access.Lock()
|
||||||
|
if m.started && m.stage >= stage {
|
||||||
|
panic("already started")
|
||||||
|
}
|
||||||
|
m.started = true
|
||||||
|
m.stage = stage
|
||||||
|
inbounds := m.inbounds
|
||||||
|
m.access.Unlock()
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
err := adapter.LegacyStart(inbound, stage)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Close() error {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
if !m.started {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.started = false
|
||||||
|
inbounds := m.inbounds
|
||||||
|
m.inbounds = nil
|
||||||
|
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||||
|
var err error
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
monitor.Start("close inbound/", inbound.Type(), "[", inbound.Tag(), "]")
|
||||||
|
err = E.Append(err, inbound.Close(), func(err error) error {
|
||||||
|
return E.Cause(err, "close inbound/", inbound.Type(), "[", inbound.Tag(), "]")
|
||||||
|
})
|
||||||
|
monitor.Finish()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Inbounds() []adapter.Inbound {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
return m.inbounds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Get(tag string) (adapter.Inbound, bool) {
|
||||||
|
m.access.Lock()
|
||||||
|
inbound, found := m.inboundByTag[tag]
|
||||||
|
m.access.Unlock()
|
||||||
|
if found {
|
||||||
|
return inbound, true
|
||||||
|
}
|
||||||
|
return m.endpoint.Get(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Remove(tag string) error {
|
||||||
|
m.access.Lock()
|
||||||
|
inbound, found := m.inboundByTag[tag]
|
||||||
|
if !found {
|
||||||
|
m.access.Unlock()
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
delete(m.inboundByTag, tag)
|
||||||
|
index := common.Index(m.inbounds, func(it adapter.Inbound) bool {
|
||||||
|
return it == inbound
|
||||||
|
})
|
||||||
|
if index == -1 {
|
||||||
|
panic("invalid inbound index")
|
||||||
|
}
|
||||||
|
m.inbounds = append(m.inbounds[:index], m.inbounds[index+1:]...)
|
||||||
|
started := m.started
|
||||||
|
m.access.Unlock()
|
||||||
|
if started {
|
||||||
|
return inbound.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
|
||||||
|
inbound, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
if m.started {
|
||||||
|
for _, stage := range adapter.ListStartStages {
|
||||||
|
err = adapter.LegacyStart(inbound, stage)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if existsInbound, loaded := m.inboundByTag[tag]; loaded {
|
||||||
|
if m.started {
|
||||||
|
err = existsInbound.Close()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "close inbound/", existsInbound.Type(), "[", existsInbound.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existsIndex := common.Index(m.inbounds, func(it adapter.Inbound) bool {
|
||||||
|
return it == existsInbound
|
||||||
|
})
|
||||||
|
if existsIndex == -1 {
|
||||||
|
panic("invalid inbound index")
|
||||||
|
}
|
||||||
|
m.inbounds = append(m.inbounds[:existsIndex], m.inbounds[existsIndex+1:]...)
|
||||||
|
}
|
||||||
|
m.inbounds = append(m.inbounds, inbound)
|
||||||
|
m.inboundByTag[tag] = inbound
|
||||||
|
return nil
|
||||||
|
}
|
72
adapter/inbound/registry.go
Normal file
72
adapter/inbound/registry.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package inbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Inbound, error)
|
||||||
|
|
||||||
|
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
|
||||||
|
registry.register(outboundType, func() any {
|
||||||
|
return new(Options)
|
||||||
|
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Inbound, error) {
|
||||||
|
var options *Options
|
||||||
|
if rawOptions != nil {
|
||||||
|
options = rawOptions.(*Options)
|
||||||
|
}
|
||||||
|
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ adapter.InboundRegistry = (*Registry)(nil)
|
||||||
|
|
||||||
|
type (
|
||||||
|
optionsConstructorFunc func() any
|
||||||
|
constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Inbound, error)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Registry struct {
|
||||||
|
access sync.Mutex
|
||||||
|
optionsType map[string]optionsConstructorFunc
|
||||||
|
constructor map[string]constructorFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistry() *Registry {
|
||||||
|
return &Registry{
|
||||||
|
optionsType: make(map[string]optionsConstructorFunc),
|
||||||
|
constructor: make(map[string]constructorFunc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
optionsConstructor, loaded := m.optionsType[outboundType]
|
||||||
|
if !loaded {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return optionsConstructor(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
constructor, loaded := m.constructor[outboundType]
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("outbound type not found: " + outboundType)
|
||||||
|
}
|
||||||
|
return constructor(ctx, router, logger, tag, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
m.optionsType[outboundType] = optionsConstructor
|
||||||
|
m.constructor[outboundType] = constructor
|
||||||
|
}
|
69
adapter/lifecycle.go
Normal file
69
adapter/lifecycle.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
type SimpleLifecycle interface {
|
||||||
|
Start() error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type StartStage uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
StartStateInitialize StartStage = iota
|
||||||
|
StartStateStart
|
||||||
|
StartStatePostStart
|
||||||
|
StartStateStarted
|
||||||
|
)
|
||||||
|
|
||||||
|
var ListStartStages = []StartStage{
|
||||||
|
StartStateInitialize,
|
||||||
|
StartStateStart,
|
||||||
|
StartStatePostStart,
|
||||||
|
StartStateStarted,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s StartStage) String() string {
|
||||||
|
switch s {
|
||||||
|
case StartStateInitialize:
|
||||||
|
return "initialize"
|
||||||
|
case StartStateStart:
|
||||||
|
return "start"
|
||||||
|
case StartStatePostStart:
|
||||||
|
return "post-start"
|
||||||
|
case StartStateStarted:
|
||||||
|
return "finish-start"
|
||||||
|
default:
|
||||||
|
panic("unknown stage")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Lifecycle interface {
|
||||||
|
Start(stage StartStage) error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type LifecycleService interface {
|
||||||
|
Name() string
|
||||||
|
Lifecycle
|
||||||
|
}
|
||||||
|
|
||||||
|
func Start(stage StartStage, services ...Lifecycle) error {
|
||||||
|
for _, service := range services {
|
||||||
|
err := service.Start(stage)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartNamed(stage StartStage, services []LifecycleService) error {
|
||||||
|
for _, service := range services {
|
||||||
|
err := service.Start(stage)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, stage.String(), " ", service.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
52
adapter/lifecycle_legacy.go
Normal file
52
adapter/lifecycle_legacy.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
func LegacyStart(starter any, stage StartStage) error {
|
||||||
|
if lifecycle, isLifecycle := starter.(Lifecycle); isLifecycle {
|
||||||
|
return lifecycle.Start(stage)
|
||||||
|
}
|
||||||
|
switch stage {
|
||||||
|
case StartStateInitialize:
|
||||||
|
if preStarter, isPreStarter := starter.(interface {
|
||||||
|
PreStart() error
|
||||||
|
}); isPreStarter {
|
||||||
|
return preStarter.PreStart()
|
||||||
|
}
|
||||||
|
case StartStateStart:
|
||||||
|
if starter, isStarter := starter.(interface {
|
||||||
|
Start() error
|
||||||
|
}); isStarter {
|
||||||
|
return starter.Start()
|
||||||
|
}
|
||||||
|
case StartStateStarted:
|
||||||
|
if postStarter, isPostStarter := starter.(interface {
|
||||||
|
PostStart() error
|
||||||
|
}); isPostStarter {
|
||||||
|
return postStarter.PostStart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type lifecycleServiceWrapper struct {
|
||||||
|
SimpleLifecycle
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService {
|
||||||
|
return &lifecycleServiceWrapper{
|
||||||
|
SimpleLifecycle: service,
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lifecycleServiceWrapper) Name() string {
|
||||||
|
return l.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
|
||||||
|
return LegacyStart(l.SimpleLifecycle, stage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lifecycleServiceWrapper) Close() error {
|
||||||
|
return l.SimpleLifecycle.Close()
|
||||||
|
}
|
57
adapter/network.go
Normal file
57
adapter/network.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-tun"
|
||||||
|
"github.com/sagernet/sing/common/control"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NetworkManager interface {
|
||||||
|
Lifecycle
|
||||||
|
InterfaceFinder() control.InterfaceFinder
|
||||||
|
UpdateInterfaces() error
|
||||||
|
DefaultNetworkInterface() *NetworkInterface
|
||||||
|
NetworkInterfaces() []NetworkInterface
|
||||||
|
AutoDetectInterface() bool
|
||||||
|
AutoDetectInterfaceFunc() control.Func
|
||||||
|
ProtectFunc() control.Func
|
||||||
|
DefaultOptions() NetworkOptions
|
||||||
|
RegisterAutoRedirectOutputMark(mark uint32) error
|
||||||
|
AutoRedirectOutputMark() uint32
|
||||||
|
NetworkMonitor() tun.NetworkUpdateMonitor
|
||||||
|
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
||||||
|
PackageManager() tun.PackageManager
|
||||||
|
WIFIState() WIFIState
|
||||||
|
ResetNetwork()
|
||||||
|
UpdateWIFIState()
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetworkOptions struct {
|
||||||
|
BindInterface string
|
||||||
|
RoutingMark uint32
|
||||||
|
DomainResolver string
|
||||||
|
DomainResolveOptions DNSQueryOptions
|
||||||
|
NetworkStrategy *C.NetworkStrategy
|
||||||
|
NetworkType []C.InterfaceType
|
||||||
|
FallbackNetworkType []C.InterfaceType
|
||||||
|
FallbackDelay time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterfaceUpdateListener interface {
|
||||||
|
InterfaceUpdated()
|
||||||
|
}
|
||||||
|
|
||||||
|
type WIFIState struct {
|
||||||
|
SSID string
|
||||||
|
BSSID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetworkInterface struct {
|
||||||
|
control.Interface
|
||||||
|
Type C.InterfaceType
|
||||||
|
DNSServers []string
|
||||||
|
Expensive bool
|
||||||
|
Constrained bool
|
||||||
|
}
|
@ -2,8 +2,9 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,6 +16,18 @@ type Outbound interface {
|
|||||||
Network() []string
|
Network() []string
|
||||||
Dependencies() []string
|
Dependencies() []string
|
||||||
N.Dialer
|
N.Dialer
|
||||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
}
|
||||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
|
||||||
|
type OutboundRegistry interface {
|
||||||
|
option.OutboundOptionsRegistry
|
||||||
|
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutboundManager interface {
|
||||||
|
Lifecycle
|
||||||
|
Outbounds() []Outbound
|
||||||
|
Outbound(tag string) (Outbound, bool)
|
||||||
|
Default() Outbound
|
||||||
|
Remove(tag string) error
|
||||||
|
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) error
|
||||||
}
|
}
|
||||||
|
45
adapter/outbound/adapter.go
Normal file
45
adapter/outbound/adapter.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Adapter struct {
|
||||||
|
outboundType string
|
||||||
|
outboundTag string
|
||||||
|
network []string
|
||||||
|
dependencies []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdapter(outboundType string, outboundTag string, network []string, dependencies []string) Adapter {
|
||||||
|
return Adapter{
|
||||||
|
outboundType: outboundType,
|
||||||
|
outboundTag: outboundTag,
|
||||||
|
network: network,
|
||||||
|
dependencies: dependencies,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdapterWithDialerOptions(outboundType string, outboundTag string, network []string, dialOptions option.DialerOptions) Adapter {
|
||||||
|
var dependencies []string
|
||||||
|
if dialOptions.Detour != "" {
|
||||||
|
dependencies = []string{dialOptions.Detour}
|
||||||
|
}
|
||||||
|
return NewAdapter(outboundType, outboundTag, network, dependencies)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Type() string {
|
||||||
|
return a.outboundType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Tag() string {
|
||||||
|
return a.outboundTag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Network() []string {
|
||||||
|
return a.network
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Dependencies() []string {
|
||||||
|
return a.dependencies
|
||||||
|
}
|
287
adapter/outbound/manager.go
Normal file
287
adapter/outbound/manager.go
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.OutboundManager = (*Manager)(nil)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
logger log.ContextLogger
|
||||||
|
registry adapter.OutboundRegistry
|
||||||
|
endpoint adapter.EndpointManager
|
||||||
|
defaultTag string
|
||||||
|
access sync.RWMutex
|
||||||
|
started bool
|
||||||
|
stage adapter.StartStage
|
||||||
|
outbounds []adapter.Outbound
|
||||||
|
outboundByTag map[string]adapter.Outbound
|
||||||
|
dependByTag map[string][]string
|
||||||
|
defaultOutbound adapter.Outbound
|
||||||
|
defaultOutboundFallback adapter.Outbound
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
|
||||||
|
return &Manager{
|
||||||
|
logger: logger,
|
||||||
|
registry: registry,
|
||||||
|
endpoint: endpoint,
|
||||||
|
defaultTag: defaultTag,
|
||||||
|
outboundByTag: make(map[string]adapter.Outbound),
|
||||||
|
dependByTag: make(map[string][]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Initialize(defaultOutboundFallback adapter.Outbound) {
|
||||||
|
m.defaultOutboundFallback = defaultOutboundFallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||||
|
m.access.Lock()
|
||||||
|
if m.started && m.stage >= stage {
|
||||||
|
panic("already started")
|
||||||
|
}
|
||||||
|
m.started = true
|
||||||
|
m.stage = stage
|
||||||
|
outbounds := m.outbounds
|
||||||
|
m.access.Unlock()
|
||||||
|
if stage == adapter.StartStateStart {
|
||||||
|
if m.defaultTag != "" && m.defaultOutbound == nil {
|
||||||
|
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
|
||||||
|
if !loaded {
|
||||||
|
return E.New("default outbound not found: ", m.defaultTag)
|
||||||
|
}
|
||||||
|
m.defaultOutbound = defaultEndpoint
|
||||||
|
}
|
||||||
|
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
|
||||||
|
} else {
|
||||||
|
for _, outbound := range outbounds {
|
||||||
|
err := adapter.LegacyStart(outbound, stage)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, stage, " outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
|
||||||
|
monitor := taskmonitor.New(m.logger, C.StartTimeout)
|
||||||
|
started := make(map[string]bool)
|
||||||
|
for {
|
||||||
|
canContinue := false
|
||||||
|
startOne:
|
||||||
|
for _, outboundToStart := range outbounds {
|
||||||
|
outboundTag := outboundToStart.Tag()
|
||||||
|
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.(adapter.Lifecycle); isStarter {
|
||||||
|
monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||||
|
err := starter.Start(adapter.StartStateStart)
|
||||||
|
monitor.Finish()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||||
|
}
|
||||||
|
} else if starter, isStarter := outboundToStart.(interface {
|
||||||
|
Start() error
|
||||||
|
}); isStarter {
|
||||||
|
monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||||
|
err := starter.Start()
|
||||||
|
monitor.Finish()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(started) == len(outbounds) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if canContinue {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentOutbound := common.Find(outbounds, func(it adapter.Outbound) bool {
|
||||||
|
return !started[it.Tag()]
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
m.access.Lock()
|
||||||
|
problemOutbound := m.outboundByTag[problemOutboundTag]
|
||||||
|
m.access.Unlock()
|
||||||
|
if problemOutbound == nil {
|
||||||
|
return E.New("dependency[", problemOutboundTag, "] not found for outbound[", oCurrent.Tag(), "]")
|
||||||
|
}
|
||||||
|
return lintOutbound(append(oTree, problemOutboundTag), problemOutbound)
|
||||||
|
}
|
||||||
|
return lintOutbound([]string{currentOutbound.Tag()}, currentOutbound)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Close() error {
|
||||||
|
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||||
|
m.access.Lock()
|
||||||
|
if !m.started {
|
||||||
|
m.access.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.started = false
|
||||||
|
outbounds := m.outbounds
|
||||||
|
m.outbounds = nil
|
||||||
|
m.access.Unlock()
|
||||||
|
var err error
|
||||||
|
for _, outbound := range outbounds {
|
||||||
|
if closer, isCloser := outbound.(io.Closer); isCloser {
|
||||||
|
monitor.Start("close outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
||||||
|
err = E.Append(err, closer.Close(), func(err error) error {
|
||||||
|
return E.Cause(err, "close outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
||||||
|
})
|
||||||
|
monitor.Finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Outbounds() []adapter.Outbound {
|
||||||
|
m.access.RLock()
|
||||||
|
defer m.access.RUnlock()
|
||||||
|
return m.outbounds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
|
||||||
|
m.access.RLock()
|
||||||
|
outbound, found := m.outboundByTag[tag]
|
||||||
|
m.access.RUnlock()
|
||||||
|
if found {
|
||||||
|
return outbound, true
|
||||||
|
}
|
||||||
|
return m.endpoint.Get(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Default() adapter.Outbound {
|
||||||
|
m.access.RLock()
|
||||||
|
defer m.access.RUnlock()
|
||||||
|
if m.defaultOutbound != nil {
|
||||||
|
return m.defaultOutbound
|
||||||
|
} else {
|
||||||
|
return m.defaultOutboundFallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Remove(tag string) error {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
outbound, found := m.outboundByTag[tag]
|
||||||
|
if !found {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
delete(m.outboundByTag, tag)
|
||||||
|
index := common.Index(m.outbounds, func(it adapter.Outbound) bool {
|
||||||
|
return it == outbound
|
||||||
|
})
|
||||||
|
if index == -1 {
|
||||||
|
panic("invalid inbound index")
|
||||||
|
}
|
||||||
|
m.outbounds = append(m.outbounds[:index], m.outbounds[index+1:]...)
|
||||||
|
started := m.started
|
||||||
|
if m.defaultOutbound == outbound {
|
||||||
|
if len(m.outbounds) > 0 {
|
||||||
|
m.defaultOutbound = m.outbounds[0]
|
||||||
|
m.logger.Info("updated default outbound to ", m.defaultOutbound.Tag())
|
||||||
|
} else {
|
||||||
|
m.defaultOutbound = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependBy := m.dependByTag[tag]
|
||||||
|
if len(dependBy) > 0 {
|
||||||
|
return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", "))
|
||||||
|
}
|
||||||
|
dependencies := outbound.Dependencies()
|
||||||
|
for _, dependency := range dependencies {
|
||||||
|
if len(m.dependByTag[dependency]) == 1 {
|
||||||
|
delete(m.dependByTag, dependency)
|
||||||
|
} else {
|
||||||
|
m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool {
|
||||||
|
return it != tag
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if started {
|
||||||
|
return common.Close(outbound)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, inboundType string, options any) error {
|
||||||
|
if tag == "" {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
outbound, err := m.registry.CreateOutbound(ctx, router, logger, tag, inboundType, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if m.started {
|
||||||
|
for _, stage := range adapter.ListStartStages {
|
||||||
|
err = adapter.LegacyStart(outbound, stage)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, stage, " outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
|
||||||
|
if m.started {
|
||||||
|
err = common.Close(existsOutbound)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existsIndex := common.Index(m.outbounds, func(it adapter.Outbound) bool {
|
||||||
|
return it == existsOutbound
|
||||||
|
})
|
||||||
|
if existsIndex == -1 {
|
||||||
|
panic("invalid inbound index")
|
||||||
|
}
|
||||||
|
m.outbounds = append(m.outbounds[:existsIndex], m.outbounds[existsIndex+1:]...)
|
||||||
|
}
|
||||||
|
m.outbounds = append(m.outbounds, outbound)
|
||||||
|
m.outboundByTag[tag] = outbound
|
||||||
|
dependencies := outbound.Dependencies()
|
||||||
|
for _, dependency := range dependencies {
|
||||||
|
m.dependByTag[dependency] = append(m.dependByTag[dependency], tag)
|
||||||
|
}
|
||||||
|
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) {
|
||||||
|
m.defaultOutbound = outbound
|
||||||
|
if m.started {
|
||||||
|
m.logger.Info("updated default outbound to ", outbound.Tag())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
72
adapter/outbound/registry.go
Normal file
72
adapter/outbound/registry.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Outbound, error)
|
||||||
|
|
||||||
|
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
|
||||||
|
registry.register(outboundType, func() any {
|
||||||
|
return new(Options)
|
||||||
|
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Outbound, error) {
|
||||||
|
var options *Options
|
||||||
|
if rawOptions != nil {
|
||||||
|
options = rawOptions.(*Options)
|
||||||
|
}
|
||||||
|
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ adapter.OutboundRegistry = (*Registry)(nil)
|
||||||
|
|
||||||
|
type (
|
||||||
|
optionsConstructorFunc func() any
|
||||||
|
constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Outbound, error)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Registry struct {
|
||||||
|
access sync.Mutex
|
||||||
|
optionsType map[string]optionsConstructorFunc
|
||||||
|
constructors map[string]constructorFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistry() *Registry {
|
||||||
|
return &Registry{
|
||||||
|
optionsType: make(map[string]optionsConstructorFunc),
|
||||||
|
constructors: make(map[string]constructorFunc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||||
|
r.access.Lock()
|
||||||
|
defer r.access.Unlock()
|
||||||
|
optionsConstructor, loaded := r.optionsType[outboundType]
|
||||||
|
if !loaded {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return optionsConstructor(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) CreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
|
||||||
|
r.access.Lock()
|
||||||
|
defer r.access.Unlock()
|
||||||
|
constructor, loaded := r.constructors[outboundType]
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("outbound type not found: " + outboundType)
|
||||||
|
}
|
||||||
|
return constructor(ctx, router, logger, tag, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||||
|
r.access.Lock()
|
||||||
|
defer r.access.Unlock()
|
||||||
|
r.optionsType[outboundType] = optionsConstructor
|
||||||
|
r.constructors[outboundType] = constructor
|
||||||
|
}
|
@ -1,9 +1 @@
|
|||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
type PreStarter interface {
|
|
||||||
PreStart() error
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostStarter interface {
|
|
||||||
PostStart() error
|
|
||||||
}
|
|
||||||
|
@ -2,117 +2,111 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"sync"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/geoip"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-dns"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
"github.com/sagernet/sing-tun"
|
|
||||||
"github.com/sagernet/sing/common/control"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
|
||||||
mdns "github.com/miekg/dns"
|
"go4.org/netipx"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Router interface {
|
type Router interface {
|
||||||
Service
|
Lifecycle
|
||||||
PreStarter
|
|
||||||
PostStarter
|
|
||||||
|
|
||||||
Outbounds() []Outbound
|
|
||||||
Outbound(tag string) (Outbound, bool)
|
|
||||||
DefaultOutbound(network string) (Outbound, error)
|
|
||||||
|
|
||||||
FakeIPStore() FakeIPStore
|
|
||||||
|
|
||||||
ConnectionRouter
|
ConnectionRouter
|
||||||
|
PreMatch(metadata InboundContext) error
|
||||||
GeoIPReader() *geoip.Reader
|
ConnectionRouterEx
|
||||||
LoadGeosite(code string) (Rule, error)
|
|
||||||
|
|
||||||
RuleSet(tag string) (RuleSet, bool)
|
RuleSet(tag string) (RuleSet, bool)
|
||||||
|
NeedWIFIState() bool
|
||||||
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
|
|
||||||
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
|
|
||||||
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
|
|
||||||
ClearDNSCache()
|
|
||||||
|
|
||||||
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
|
Rules() []Rule
|
||||||
|
AppendTracker(tracker ConnectionTracker)
|
||||||
ClashServer() ClashServer
|
ResetNetwork()
|
||||||
SetClashServer(server ClashServer)
|
|
||||||
|
|
||||||
V2RayServer() V2RayServer
|
|
||||||
SetV2RayServer(server V2RayServer)
|
|
||||||
|
|
||||||
ResetNetwork() error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ContextWithRouter(ctx context.Context, router Router) context.Context {
|
type ConnectionTracker interface {
|
||||||
return service.ContextWith(ctx, router)
|
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) net.Conn
|
||||||
|
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) N.PacketConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func RouterFromContext(ctx context.Context) Router {
|
// Deprecated: Use ConnectionRouterEx instead.
|
||||||
return service.FromContext[Router](ctx)
|
type ConnectionRouter interface {
|
||||||
|
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||||
|
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeadlessRule interface {
|
type ConnectionRouterEx interface {
|
||||||
Match(metadata *InboundContext) bool
|
ConnectionRouter
|
||||||
}
|
RouteConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||||
|
RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||||
type Rule interface {
|
|
||||||
HeadlessRule
|
|
||||||
Service
|
|
||||||
Type() string
|
|
||||||
UpdateGeosite() error
|
|
||||||
Outbound() string
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type DNSRule interface {
|
|
||||||
Rule
|
|
||||||
DisableCache() bool
|
|
||||||
RewriteTTL() *uint32
|
|
||||||
ClientSubnet() *netip.Addr
|
|
||||||
WithAddressLimit() bool
|
|
||||||
MatchAddressLimit(metadata *InboundContext) bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuleSet interface {
|
type RuleSet interface {
|
||||||
StartContext(ctx context.Context, startContext RuleSetStartContext) error
|
Name() string
|
||||||
|
StartContext(ctx context.Context, startContext *HTTPStartContext) error
|
||||||
PostStart() error
|
PostStart() error
|
||||||
Metadata() RuleSetMetadata
|
Metadata() RuleSetMetadata
|
||||||
|
ExtractIPSet() []*netipx.IPSet
|
||||||
|
IncRef()
|
||||||
|
DecRef()
|
||||||
|
Cleanup()
|
||||||
|
RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback]
|
||||||
|
UnregisterCallback(element *list.Element[RuleSetUpdateCallback])
|
||||||
Close() error
|
Close() error
|
||||||
HeadlessRule
|
HeadlessRule
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RuleSetUpdateCallback func(it RuleSet)
|
||||||
|
|
||||||
type RuleSetMetadata struct {
|
type RuleSetMetadata struct {
|
||||||
ContainsProcessRule bool
|
ContainsProcessRule bool
|
||||||
ContainsWIFIRule bool
|
ContainsWIFIRule bool
|
||||||
ContainsIPCIDRRule bool
|
ContainsIPCIDRRule bool
|
||||||
}
|
}
|
||||||
|
type HTTPStartContext struct {
|
||||||
type RuleSetStartContext interface {
|
ctx context.Context
|
||||||
HTTPClient(detour string, dialer N.Dialer) *http.Client
|
access sync.Mutex
|
||||||
Close()
|
httpClientCache map[string]*http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterfaceUpdateListener interface {
|
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
|
||||||
InterfaceUpdated()
|
return &HTTPStartContext{
|
||||||
|
ctx: ctx,
|
||||||
|
httpClientCache: make(map[string]*http.Client),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type WIFIState struct {
|
func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
|
||||||
SSID string
|
c.access.Lock()
|
||||||
BSSID string
|
defer c.access.Unlock()
|
||||||
|
if httpClient, loaded := c.httpClientCache[detour]; loaded {
|
||||||
|
return httpClient
|
||||||
|
}
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
TLSHandshakeTimeout: C.TCPTimeout,
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||||
|
},
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
Time: ntp.TimeFuncFromContext(c.ctx),
|
||||||
|
RootCAs: RootPoolFromContext(c.ctx),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.httpClientCache[detour] = httpClient
|
||||||
|
return httpClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HTTPStartContext) Close() {
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
for _, client := range c.httpClientCache {
|
||||||
|
client.CloseIdleConnections()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
37
adapter/rule.go
Normal file
37
adapter/rule.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HeadlessRule interface {
|
||||||
|
Match(metadata *InboundContext) bool
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rule interface {
|
||||||
|
HeadlessRule
|
||||||
|
SimpleLifecycle
|
||||||
|
Type() string
|
||||||
|
Action() RuleAction
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSRule interface {
|
||||||
|
Rule
|
||||||
|
WithAddressLimit() bool
|
||||||
|
MatchAddressLimit(metadata *InboundContext) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleAction interface {
|
||||||
|
Type() string
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsFinalAction(action RuleAction) bool {
|
||||||
|
switch action.Type() {
|
||||||
|
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,27 @@
|
|||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
Start() error
|
Lifecycle
|
||||||
Close() error
|
Type() string
|
||||||
|
Tag() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceRegistry interface {
|
||||||
|
option.ServiceOptionsRegistry
|
||||||
|
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) (Service, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceManager interface {
|
||||||
|
Lifecycle
|
||||||
|
Services() []Service
|
||||||
|
Get(tag string) (Service, bool)
|
||||||
|
Remove(tag string) error
|
||||||
|
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error
|
||||||
}
|
}
|
||||||
|
21
adapter/service/adapter.go
Normal file
21
adapter/service/adapter.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
type Adapter struct {
|
||||||
|
serviceType string
|
||||||
|
serviceTag string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdapter(serviceType string, serviceTag string) Adapter {
|
||||||
|
return Adapter{
|
||||||
|
serviceType: serviceType,
|
||||||
|
serviceTag: serviceTag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Type() string {
|
||||||
|
return a.serviceType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Tag() string {
|
||||||
|
return a.serviceTag
|
||||||
|
}
|
144
adapter/service/manager.go
Normal file
144
adapter/service/manager.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.ServiceManager = (*Manager)(nil)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
logger log.ContextLogger
|
||||||
|
registry adapter.ServiceRegistry
|
||||||
|
access sync.Mutex
|
||||||
|
started bool
|
||||||
|
stage adapter.StartStage
|
||||||
|
services []adapter.Service
|
||||||
|
serviceByTag map[string]adapter.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager {
|
||||||
|
return &Manager{
|
||||||
|
logger: logger,
|
||||||
|
registry: registry,
|
||||||
|
serviceByTag: make(map[string]adapter.Service),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||||
|
m.access.Lock()
|
||||||
|
if m.started && m.stage >= stage {
|
||||||
|
panic("already started")
|
||||||
|
}
|
||||||
|
m.started = true
|
||||||
|
m.stage = stage
|
||||||
|
services := m.services
|
||||||
|
m.access.Unlock()
|
||||||
|
for _, service := range services {
|
||||||
|
err := adapter.LegacyStart(service, stage)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Close() error {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
if !m.started {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.started = false
|
||||||
|
services := m.services
|
||||||
|
m.services = nil
|
||||||
|
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||||
|
var err error
|
||||||
|
for _, service := range services {
|
||||||
|
monitor.Start("close service/", service.Type(), "[", service.Tag(), "]")
|
||||||
|
err = E.Append(err, service.Close(), func(err error) error {
|
||||||
|
return E.Cause(err, "close service/", service.Type(), "[", service.Tag(), "]")
|
||||||
|
})
|
||||||
|
monitor.Finish()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Services() []adapter.Service {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
return m.services
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Get(tag string) (adapter.Service, bool) {
|
||||||
|
m.access.Lock()
|
||||||
|
service, found := m.serviceByTag[tag]
|
||||||
|
m.access.Unlock()
|
||||||
|
return service, found
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Remove(tag string) error {
|
||||||
|
m.access.Lock()
|
||||||
|
service, found := m.serviceByTag[tag]
|
||||||
|
if !found {
|
||||||
|
m.access.Unlock()
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
delete(m.serviceByTag, tag)
|
||||||
|
index := common.Index(m.services, func(it adapter.Service) bool {
|
||||||
|
return it == service
|
||||||
|
})
|
||||||
|
if index == -1 {
|
||||||
|
panic("invalid service index")
|
||||||
|
}
|
||||||
|
m.services = append(m.services[:index], m.services[index+1:]...)
|
||||||
|
started := m.started
|
||||||
|
m.access.Unlock()
|
||||||
|
if started {
|
||||||
|
return service.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error {
|
||||||
|
service, err := m.registry.Create(ctx, logger, tag, serviceType, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
if m.started {
|
||||||
|
for _, stage := range adapter.ListStartStages {
|
||||||
|
err = adapter.LegacyStart(service, stage)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if existsService, loaded := m.serviceByTag[tag]; loaded {
|
||||||
|
if m.started {
|
||||||
|
err = existsService.Close()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "close service/", existsService.Type(), "[", existsService.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existsIndex := common.Index(m.services, func(it adapter.Service) bool {
|
||||||
|
return it == existsService
|
||||||
|
})
|
||||||
|
if existsIndex == -1 {
|
||||||
|
panic("invalid service index")
|
||||||
|
}
|
||||||
|
m.services = append(m.services[:existsIndex], m.services[existsIndex+1:]...)
|
||||||
|
}
|
||||||
|
m.services = append(m.services, service)
|
||||||
|
m.serviceByTag[tag] = service
|
||||||
|
return nil
|
||||||
|
}
|
72
adapter/service/registry.go
Normal file
72
adapter/service/registry.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.Service, error)
|
||||||
|
|
||||||
|
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
|
||||||
|
registry.register(outboundType, func() any {
|
||||||
|
return new(Options)
|
||||||
|
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.Service, error) {
|
||||||
|
var options *Options
|
||||||
|
if rawOptions != nil {
|
||||||
|
options = rawOptions.(*Options)
|
||||||
|
}
|
||||||
|
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ adapter.ServiceRegistry = (*Registry)(nil)
|
||||||
|
|
||||||
|
type (
|
||||||
|
optionsConstructorFunc func() any
|
||||||
|
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.Service, error)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Registry struct {
|
||||||
|
access sync.Mutex
|
||||||
|
optionsType map[string]optionsConstructorFunc
|
||||||
|
constructor map[string]constructorFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistry() *Registry {
|
||||||
|
return &Registry{
|
||||||
|
optionsType: make(map[string]optionsConstructorFunc),
|
||||||
|
constructor: make(map[string]constructorFunc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
optionsConstructor, loaded := m.optionsType[outboundType]
|
||||||
|
if !loaded {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return optionsConstructor(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Service, error) {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
constructor, loaded := m.constructor[outboundType]
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("outbound type not found: " + outboundType)
|
||||||
|
}
|
||||||
|
return constructor(ctx, logger, tag, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
m.optionsType[outboundType] = optionsConstructor
|
||||||
|
m.constructor[outboundType] = constructor
|
||||||
|
}
|
18
adapter/ssm.go
Normal file
18
adapter/ssm.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ManagedSSMServer interface {
|
||||||
|
Inbound
|
||||||
|
SetTracker(tracker SSMTracker)
|
||||||
|
UpdateUsers(users []string, uPSKs []string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type SSMTracker interface {
|
||||||
|
TrackConnection(conn net.Conn, metadata InboundContext) net.Conn
|
||||||
|
TrackPacketConnection(conn N.PacketConn, metadata InboundContext) N.PacketConn
|
||||||
|
}
|
@ -3,6 +3,6 @@ package adapter
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type TimeService interface {
|
type TimeService interface {
|
||||||
Service
|
SimpleLifecycle
|
||||||
TimeFunc() func() time.Time
|
TimeFunc() func() time.Time
|
||||||
}
|
}
|
||||||
|
@ -4,112 +4,165 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
ConnectionHandlerFuncEx = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||||
PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
PacketConnectionHandlerFuncEx = func(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewUpstreamHandler(
|
func NewUpstreamHandlerEx(
|
||||||
metadata InboundContext,
|
metadata InboundContext,
|
||||||
connectionHandler ConnectionHandlerFunc,
|
connectionHandler ConnectionHandlerFuncEx,
|
||||||
packetHandler PacketConnectionHandlerFunc,
|
packetHandler PacketConnectionHandlerFuncEx,
|
||||||
errorHandler E.Handler,
|
) UpstreamHandlerAdapterEx {
|
||||||
) UpstreamHandlerAdapter {
|
return &myUpstreamHandlerWrapperEx{
|
||||||
return &myUpstreamHandlerWrapper{
|
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
connectionHandler: connectionHandler,
|
connectionHandler: connectionHandler,
|
||||||
packetHandler: packetHandler,
|
packetHandler: packetHandler,
|
||||||
errorHandler: errorHandler,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
|
var _ UpstreamHandlerAdapterEx = (*myUpstreamHandlerWrapperEx)(nil)
|
||||||
|
|
||||||
type myUpstreamHandlerWrapper struct {
|
type myUpstreamHandlerWrapperEx struct {
|
||||||
metadata InboundContext
|
metadata InboundContext
|
||||||
connectionHandler ConnectionHandlerFunc
|
connectionHandler ConnectionHandlerFuncEx
|
||||||
packetHandler PacketConnectionHandlerFunc
|
packetHandler PacketConnectionHandlerFuncEx
|
||||||
errorHandler E.Handler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
func (w *myUpstreamHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
myMetadata := w.metadata
|
myMetadata := w.metadata
|
||||||
if metadata.Source.IsValid() {
|
if source.IsValid() {
|
||||||
myMetadata.Source = metadata.Source
|
myMetadata.Source = source
|
||||||
}
|
}
|
||||||
if metadata.Destination.IsValid() {
|
if destination.IsValid() {
|
||||||
myMetadata.Destination = metadata.Destination
|
myMetadata.Destination = destination
|
||||||
}
|
}
|
||||||
return w.connectionHandler(ctx, conn, myMetadata)
|
w.connectionHandler(ctx, conn, myMetadata, onClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
func (w *myUpstreamHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
myMetadata := w.metadata
|
myMetadata := w.metadata
|
||||||
if metadata.Source.IsValid() {
|
if source.IsValid() {
|
||||||
myMetadata.Source = metadata.Source
|
myMetadata.Source = source
|
||||||
}
|
}
|
||||||
if metadata.Destination.IsValid() {
|
if destination.IsValid() {
|
||||||
myMetadata.Destination = metadata.Destination
|
myMetadata.Destination = destination
|
||||||
}
|
}
|
||||||
return w.packetHandler(ctx, conn, myMetadata)
|
w.packetHandler(ctx, conn, myMetadata, onClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
|
var _ UpstreamHandlerAdapterEx = (*myUpstreamContextHandlerWrapperEx)(nil)
|
||||||
w.errorHandler.NewError(ctx, err)
|
|
||||||
|
type myUpstreamContextHandlerWrapperEx struct {
|
||||||
|
connectionHandler ConnectionHandlerFuncEx
|
||||||
|
packetHandler PacketConnectionHandlerFuncEx
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpstreamMetadata(metadata InboundContext) M.Metadata {
|
func NewUpstreamContextHandlerEx(
|
||||||
return M.Metadata{
|
connectionHandler ConnectionHandlerFuncEx,
|
||||||
Source: metadata.Source,
|
packetHandler PacketConnectionHandlerFuncEx,
|
||||||
Destination: metadata.Destination,
|
) UpstreamHandlerAdapterEx {
|
||||||
}
|
return &myUpstreamContextHandlerWrapperEx{
|
||||||
}
|
|
||||||
|
|
||||||
type myUpstreamContextHandlerWrapper struct {
|
|
||||||
connectionHandler ConnectionHandlerFunc
|
|
||||||
packetHandler PacketConnectionHandlerFunc
|
|
||||||
errorHandler E.Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUpstreamContextHandler(
|
|
||||||
connectionHandler ConnectionHandlerFunc,
|
|
||||||
packetHandler PacketConnectionHandlerFunc,
|
|
||||||
errorHandler E.Handler,
|
|
||||||
) UpstreamHandlerAdapter {
|
|
||||||
return &myUpstreamContextHandlerWrapper{
|
|
||||||
connectionHandler: connectionHandler,
|
connectionHandler: connectionHandler,
|
||||||
packetHandler: packetHandler,
|
packetHandler: packetHandler,
|
||||||
errorHandler: errorHandler,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
myMetadata := ContextFrom(ctx)
|
myMetadata := ContextFrom(ctx)
|
||||||
if metadata.Source.IsValid() {
|
if source.IsValid() {
|
||||||
myMetadata.Source = metadata.Source
|
myMetadata.Source = source
|
||||||
}
|
}
|
||||||
if metadata.Destination.IsValid() {
|
if destination.IsValid() {
|
||||||
myMetadata.Destination = metadata.Destination
|
myMetadata.Destination = destination
|
||||||
}
|
}
|
||||||
return w.connectionHandler(ctx, conn, *myMetadata)
|
w.connectionHandler(ctx, conn, *myMetadata, onClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
myMetadata := ContextFrom(ctx)
|
myMetadata := ContextFrom(ctx)
|
||||||
if metadata.Source.IsValid() {
|
if source.IsValid() {
|
||||||
myMetadata.Source = metadata.Source
|
myMetadata.Source = source
|
||||||
}
|
}
|
||||||
if metadata.Destination.IsValid() {
|
if destination.IsValid() {
|
||||||
myMetadata.Destination = metadata.Destination
|
myMetadata.Destination = destination
|
||||||
}
|
}
|
||||||
return w.packetHandler(ctx, conn, *myMetadata)
|
w.packetHandler(ctx, conn, *myMetadata, onClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
func NewRouteHandlerEx(
|
||||||
w.errorHandler.NewError(ctx, err)
|
metadata InboundContext,
|
||||||
|
router ConnectionRouterEx,
|
||||||
|
) UpstreamHandlerAdapterEx {
|
||||||
|
return &routeHandlerWrapperEx{
|
||||||
|
metadata: metadata,
|
||||||
|
router: router,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ UpstreamHandlerAdapterEx = (*routeHandlerWrapperEx)(nil)
|
||||||
|
|
||||||
|
type routeHandlerWrapperEx struct {
|
||||||
|
metadata InboundContext
|
||||||
|
router ConnectionRouterEx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *routeHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
|
if source.IsValid() {
|
||||||
|
r.metadata.Source = source
|
||||||
|
}
|
||||||
|
if destination.IsValid() {
|
||||||
|
r.metadata.Destination = destination
|
||||||
|
}
|
||||||
|
r.router.RouteConnectionEx(ctx, conn, r.metadata, onClose)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *routeHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
|
if source.IsValid() {
|
||||||
|
r.metadata.Source = source
|
||||||
|
}
|
||||||
|
if destination.IsValid() {
|
||||||
|
r.metadata.Destination = destination
|
||||||
|
}
|
||||||
|
r.router.RoutePacketConnectionEx(ctx, conn, r.metadata, onClose)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRouteContextHandlerEx(
|
||||||
|
router ConnectionRouterEx,
|
||||||
|
) UpstreamHandlerAdapterEx {
|
||||||
|
return &routeContextHandlerWrapperEx{
|
||||||
|
router: router,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ UpstreamHandlerAdapterEx = (*routeContextHandlerWrapperEx)(nil)
|
||||||
|
|
||||||
|
type routeContextHandlerWrapperEx struct {
|
||||||
|
router ConnectionRouterEx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
|
metadata := ContextFrom(ctx)
|
||||||
|
if source.IsValid() {
|
||||||
|
metadata.Source = source
|
||||||
|
}
|
||||||
|
if destination.IsValid() {
|
||||||
|
metadata.Destination = destination
|
||||||
|
}
|
||||||
|
r.router.RouteConnectionEx(ctx, conn, *metadata, onClose)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
|
metadata := ContextFrom(ctx)
|
||||||
|
if source.IsValid() {
|
||||||
|
metadata.Source = source
|
||||||
|
}
|
||||||
|
if destination.IsValid() {
|
||||||
|
metadata.Destination = destination
|
||||||
|
}
|
||||||
|
r.router.RoutePacketConnectionEx(ctx, conn, *metadata, onClose)
|
||||||
}
|
}
|
||||||
|
234
adapter/upstream_legacy.go
Normal file
234
adapter/upstream_legacy.go
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Deprecated
|
||||||
|
ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||||
|
// Deprecated
|
||||||
|
PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deprecated
|
||||||
|
//
|
||||||
|
//nolint:staticcheck
|
||||||
|
func NewUpstreamHandler(
|
||||||
|
metadata InboundContext,
|
||||||
|
connectionHandler ConnectionHandlerFunc,
|
||||||
|
packetHandler PacketConnectionHandlerFunc,
|
||||||
|
errorHandler E.Handler,
|
||||||
|
) UpstreamHandlerAdapter {
|
||||||
|
return &myUpstreamHandlerWrapper{
|
||||||
|
metadata: metadata,
|
||||||
|
connectionHandler: connectionHandler,
|
||||||
|
packetHandler: packetHandler,
|
||||||
|
errorHandler: errorHandler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
|
||||||
|
|
||||||
|
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||||
|
//
|
||||||
|
//nolint:staticcheck
|
||||||
|
type myUpstreamHandlerWrapper struct {
|
||||||
|
metadata InboundContext
|
||||||
|
connectionHandler ConnectionHandlerFunc
|
||||||
|
packetHandler PacketConnectionHandlerFunc
|
||||||
|
errorHandler E.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||||
|
func (w *myUpstreamHandlerWrapper) 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.connectionHandler(ctx, conn, myMetadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||||
|
func (w *myUpstreamHandlerWrapper) 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.packetHandler(ctx, conn, myMetadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||||
|
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||||
|
w.errorHandler.NewError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: removed
|
||||||
|
func UpstreamMetadata(metadata InboundContext) M.Metadata {
|
||||||
|
return M.Metadata{
|
||||||
|
Source: metadata.Source,
|
||||||
|
Destination: metadata.Destination,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||||
|
type myUpstreamContextHandlerWrapper struct {
|
||||||
|
connectionHandler ConnectionHandlerFunc
|
||||||
|
packetHandler PacketConnectionHandlerFunc
|
||||||
|
errorHandler E.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||||
|
func NewUpstreamContextHandler(
|
||||||
|
connectionHandler ConnectionHandlerFunc,
|
||||||
|
packetHandler PacketConnectionHandlerFunc,
|
||||||
|
errorHandler E.Handler,
|
||||||
|
) UpstreamHandlerAdapter {
|
||||||
|
return &myUpstreamContextHandlerWrapper{
|
||||||
|
connectionHandler: connectionHandler,
|
||||||
|
packetHandler: packetHandler,
|
||||||
|
errorHandler: errorHandler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||||
|
func (w *myUpstreamContextHandlerWrapper) 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.connectionHandler(ctx, conn, *myMetadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||||
|
func (w *myUpstreamContextHandlerWrapper) 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.packetHandler(ctx, conn, *myMetadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||||
|
func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||||
|
w.errorHandler.NewError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ConnectionRouterEx instead.
|
||||||
|
func NewRouteHandler(
|
||||||
|
metadata InboundContext,
|
||||||
|
router ConnectionRouter,
|
||||||
|
logger logger.ContextLogger,
|
||||||
|
) UpstreamHandlerAdapter {
|
||||||
|
return &routeHandlerWrapper{
|
||||||
|
metadata: metadata,
|
||||||
|
router: router,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ConnectionRouterEx instead.
|
||||||
|
func NewRouteContextHandler(
|
||||||
|
router ConnectionRouter,
|
||||||
|
logger logger.ContextLogger,
|
||||||
|
) UpstreamHandlerAdapter {
|
||||||
|
return &routeContextHandlerWrapper{
|
||||||
|
router: router,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
|
||||||
|
|
||||||
|
// Deprecated: Use ConnectionRouterEx instead.
|
||||||
|
//
|
||||||
|
//nolint:staticcheck
|
||||||
|
type routeHandlerWrapper struct {
|
||||||
|
metadata InboundContext
|
||||||
|
router ConnectionRouter
|
||||||
|
logger logger.ContextLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ConnectionRouterEx instead.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ConnectionRouterEx instead.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ConnectionRouterEx instead.
|
||||||
|
func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||||
|
w.logger.ErrorContext(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
|
||||||
|
|
||||||
|
// Deprecated: Use ConnectionRouterEx instead.
|
||||||
|
type routeContextHandlerWrapper struct {
|
||||||
|
router ConnectionRouter
|
||||||
|
logger logger.ContextLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ConnectionRouterEx instead.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ConnectionRouterEx instead.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ConnectionRouterEx instead.
|
||||||
|
func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||||
|
w.logger.ErrorContext(ctx, err)
|
||||||
|
}
|
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -16,10 +15,10 @@ type V2RayServerTransport interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type V2RayServerTransportHandler interface {
|
type V2RayServerTransportHandler interface {
|
||||||
N.TCPConnectionHandler
|
N.TCPConnectionHandlerEx
|
||||||
E.Handler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type V2RayClientTransport interface {
|
type V2RayClientTransport interface {
|
||||||
DialContext(ctx context.Context) (net.Conn, error)
|
DialContext(ctx context.Context) (net.Conn, error)
|
||||||
|
Close() error
|
||||||
}
|
}
|
||||||
|
526
box.go
526
box.go
@ -9,45 +9,91 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||||
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
|
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||||
|
"github.com/sagernet/sing-box/common/certificate"
|
||||||
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing-box/dns/transport/local"
|
||||||
"github.com/sagernet/sing-box/experimental"
|
"github.com/sagernet/sing-box/experimental"
|
||||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
"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/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-box/outbound"
|
"github.com/sagernet/sing-box/protocol/direct"
|
||||||
"github.com/sagernet/sing-box/route"
|
"github.com/sagernet/sing-box/route"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
"github.com/sagernet/sing/service/pause"
|
"github.com/sagernet/sing/service/pause"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.Service = (*Box)(nil)
|
var _ adapter.SimpleLifecycle = (*Box)(nil)
|
||||||
|
|
||||||
type Box struct {
|
type Box struct {
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
router adapter.Router
|
logFactory log.Factory
|
||||||
inbounds []adapter.Inbound
|
logger log.ContextLogger
|
||||||
outbounds []adapter.Outbound
|
network *route.NetworkManager
|
||||||
logFactory log.Factory
|
endpoint *endpoint.Manager
|
||||||
logger log.ContextLogger
|
inbound *inbound.Manager
|
||||||
preServices1 map[string]adapter.Service
|
outbound *outbound.Manager
|
||||||
preServices2 map[string]adapter.Service
|
service *boxService.Manager
|
||||||
postServices map[string]adapter.Service
|
dnsTransport *dns.TransportManager
|
||||||
done chan struct{}
|
dnsRouter *dns.Router
|
||||||
|
connection *route.ConnectionManager
|
||||||
|
router *route.Router
|
||||||
|
internalService []adapter.LifecycleService
|
||||||
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
option.Options
|
option.Options
|
||||||
Context context.Context
|
Context context.Context
|
||||||
PlatformInterface platform.Interface
|
|
||||||
PlatformLogWriter log.PlatformWriter
|
PlatformLogWriter log.PlatformWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Context(
|
||||||
|
ctx context.Context,
|
||||||
|
inboundRegistry adapter.InboundRegistry,
|
||||||
|
outboundRegistry adapter.OutboundRegistry,
|
||||||
|
endpointRegistry adapter.EndpointRegistry,
|
||||||
|
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||||
|
serviceRegistry adapter.ServiceRegistry,
|
||||||
|
) context.Context {
|
||||||
|
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
||||||
|
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
||||||
|
ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry)
|
||||||
|
ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry)
|
||||||
|
}
|
||||||
|
if service.FromContext[option.OutboundOptionsRegistry](ctx) == nil ||
|
||||||
|
service.FromContext[adapter.OutboundRegistry](ctx) == nil {
|
||||||
|
ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry)
|
||||||
|
ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry)
|
||||||
|
}
|
||||||
|
if service.FromContext[option.EndpointOptionsRegistry](ctx) == nil ||
|
||||||
|
service.FromContext[adapter.EndpointRegistry](ctx) == nil {
|
||||||
|
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
|
||||||
|
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
|
||||||
|
}
|
||||||
|
if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil {
|
||||||
|
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
|
||||||
|
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
|
||||||
|
}
|
||||||
|
if service.FromContext[adapter.ServiceRegistry](ctx) == nil {
|
||||||
|
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
|
||||||
|
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
func New(options Options) (*Box, error) {
|
func New(options Options) (*Box, error) {
|
||||||
createdAt := time.Now()
|
createdAt := time.Now()
|
||||||
ctx := options.Context
|
ctx := options.Context
|
||||||
@ -55,6 +101,29 @@ func New(options Options) (*Box, error) {
|
|||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
ctx = service.ContextWithDefaultRegistry(ctx)
|
ctx = service.ContextWithDefaultRegistry(ctx)
|
||||||
|
|
||||||
|
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
|
||||||
|
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
||||||
|
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||||
|
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
||||||
|
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
|
||||||
|
|
||||||
|
if endpointRegistry == nil {
|
||||||
|
return nil, E.New("missing endpoint registry in context")
|
||||||
|
}
|
||||||
|
if inboundRegistry == nil {
|
||||||
|
return nil, E.New("missing inbound registry in context")
|
||||||
|
}
|
||||||
|
if outboundRegistry == nil {
|
||||||
|
return nil, E.New("missing outbound registry in context")
|
||||||
|
}
|
||||||
|
if dnsTransportRegistry == nil {
|
||||||
|
return nil, E.New("missing DNS transport registry in context")
|
||||||
|
}
|
||||||
|
if serviceRegistry == nil {
|
||||||
|
return nil, E.New("missing service registry in context")
|
||||||
|
}
|
||||||
|
|
||||||
ctx = pause.WithDefaultManager(ctx)
|
ctx = pause.WithDefaultManager(ctx)
|
||||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||||
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
||||||
@ -70,8 +139,9 @@ func New(options Options) (*Box, error) {
|
|||||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||||
needV2RayAPI = true
|
needV2RayAPI = true
|
||||||
}
|
}
|
||||||
|
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||||
var defaultLogWriter io.Writer
|
var defaultLogWriter io.Writer
|
||||||
if options.PlatformInterface != nil {
|
if platformInterface != nil {
|
||||||
defaultLogWriter = io.Discard
|
defaultLogWriter = io.Discard
|
||||||
}
|
}
|
||||||
logFactory, err := log.New(log.Options{
|
logFactory, err := log.New(log.Options{
|
||||||
@ -85,114 +155,245 @@ func New(options Options) (*Box, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "create log factory")
|
return nil, E.Cause(err, "create log factory")
|
||||||
}
|
}
|
||||||
router, err := route.NewRouter(
|
|
||||||
ctx,
|
var internalServices []adapter.LifecycleService
|
||||||
logFactory,
|
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||||
common.PtrValueOrDefault(options.Route),
|
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||||
common.PtrValueOrDefault(options.DNS),
|
len(certificateOptions.Certificate) > 0 ||
|
||||||
common.PtrValueOrDefault(options.NTP),
|
len(certificateOptions.CertificatePath) > 0 ||
|
||||||
options.Inbounds,
|
len(certificateOptions.CertificateDirectoryPath) > 0 {
|
||||||
options.PlatformInterface,
|
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
|
||||||
)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, E.Cause(err, "parse route options")
|
}
|
||||||
|
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||||
|
internalServices = append(internalServices, certificateStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||||
|
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||||
|
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||||
|
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||||
|
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
||||||
|
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
||||||
|
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
|
||||||
|
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||||
|
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||||
|
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||||
|
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||||
|
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||||
|
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||||
|
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||||
|
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize network manager")
|
||||||
|
}
|
||||||
|
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||||
|
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||||
|
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||||
|
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
||||||
|
service.MustRegister[adapter.Router](ctx, router)
|
||||||
|
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize router")
|
||||||
|
}
|
||||||
|
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||||
|
var timeService *tls.TimeServiceWrapper
|
||||||
|
if ntpOptions.Enabled {
|
||||||
|
timeService = new(tls.TimeServiceWrapper)
|
||||||
|
service.MustRegister[ntp.TimeService](ctx, timeService)
|
||||||
|
}
|
||||||
|
for i, transportOptions := range dnsOptions.Servers {
|
||||||
|
var tag string
|
||||||
|
if transportOptions.Tag != "" {
|
||||||
|
tag = transportOptions.Tag
|
||||||
|
} else {
|
||||||
|
tag = F.ToString(i)
|
||||||
|
}
|
||||||
|
err = dnsTransportManager.Create(
|
||||||
|
ctx,
|
||||||
|
logFactory.NewLogger(F.ToString("dns/", transportOptions.Type, "[", tag, "]")),
|
||||||
|
tag,
|
||||||
|
transportOptions.Type,
|
||||||
|
transportOptions.Options,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize DNS server[", i, "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = dnsRouter.Initialize(dnsOptions.Rules)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize dns router")
|
||||||
|
}
|
||||||
|
for i, endpointOptions := range options.Endpoints {
|
||||||
|
var tag string
|
||||||
|
if endpointOptions.Tag != "" {
|
||||||
|
tag = endpointOptions.Tag
|
||||||
|
} else {
|
||||||
|
tag = F.ToString(i)
|
||||||
|
}
|
||||||
|
endpointCtx := ctx
|
||||||
|
if tag != "" {
|
||||||
|
// TODO: remove this
|
||||||
|
endpointCtx = adapter.WithContext(endpointCtx, &adapter.InboundContext{
|
||||||
|
Outbound: tag,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
err = endpointManager.Create(
|
||||||
|
endpointCtx,
|
||||||
|
router,
|
||||||
|
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
|
||||||
|
tag,
|
||||||
|
endpointOptions.Type,
|
||||||
|
endpointOptions.Options,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize endpoint[", i, "]")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
inbounds := make([]adapter.Inbound, 0, len(options.Inbounds))
|
|
||||||
outbounds := make([]adapter.Outbound, 0, len(options.Outbounds))
|
|
||||||
for i, inboundOptions := range options.Inbounds {
|
for i, inboundOptions := range options.Inbounds {
|
||||||
var in adapter.Inbound
|
|
||||||
var tag string
|
var tag string
|
||||||
if inboundOptions.Tag != "" {
|
if inboundOptions.Tag != "" {
|
||||||
tag = inboundOptions.Tag
|
tag = inboundOptions.Tag
|
||||||
} else {
|
} else {
|
||||||
tag = F.ToString(i)
|
tag = F.ToString(i)
|
||||||
}
|
}
|
||||||
in, err = inbound.New(
|
err = inboundManager.Create(
|
||||||
ctx,
|
ctx,
|
||||||
router,
|
router,
|
||||||
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
|
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
|
||||||
inboundOptions,
|
tag,
|
||||||
options.PlatformInterface,
|
inboundOptions.Type,
|
||||||
|
inboundOptions.Options,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "parse inbound[", i, "]")
|
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
||||||
}
|
}
|
||||||
inbounds = append(inbounds, in)
|
|
||||||
}
|
}
|
||||||
for i, outboundOptions := range options.Outbounds {
|
for i, outboundOptions := range options.Outbounds {
|
||||||
var out adapter.Outbound
|
|
||||||
var tag string
|
var tag string
|
||||||
if outboundOptions.Tag != "" {
|
if outboundOptions.Tag != "" {
|
||||||
tag = outboundOptions.Tag
|
tag = outboundOptions.Tag
|
||||||
} else {
|
} else {
|
||||||
tag = F.ToString(i)
|
tag = F.ToString(i)
|
||||||
}
|
}
|
||||||
out, err = outbound.New(
|
outboundCtx := ctx
|
||||||
ctx,
|
if tag != "" {
|
||||||
|
// TODO: remove this
|
||||||
|
outboundCtx = adapter.WithContext(outboundCtx, &adapter.InboundContext{
|
||||||
|
Outbound: tag,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
err = outboundManager.Create(
|
||||||
|
outboundCtx,
|
||||||
router,
|
router,
|
||||||
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
|
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
|
||||||
tag,
|
tag,
|
||||||
outboundOptions)
|
outboundOptions.Type,
|
||||||
|
outboundOptions.Options,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "parse outbound[", i, "]")
|
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
||||||
}
|
}
|
||||||
outbounds = append(outbounds, out)
|
|
||||||
}
|
}
|
||||||
err = router.Initialize(inbounds, outbounds, func() adapter.Outbound {
|
for i, serviceOptions := range options.Services {
|
||||||
out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.Outbound{Type: "direct", Tag: "default"})
|
var tag string
|
||||||
common.Must(oErr)
|
if serviceOptions.Tag != "" {
|
||||||
outbounds = append(outbounds, out)
|
tag = serviceOptions.Tag
|
||||||
return out
|
} else {
|
||||||
})
|
tag = F.ToString(i)
|
||||||
if err != nil {
|
}
|
||||||
return nil, err
|
err = serviceManager.Create(
|
||||||
|
ctx,
|
||||||
|
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
||||||
|
tag,
|
||||||
|
serviceOptions.Type,
|
||||||
|
serviceOptions.Options,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize service[", i, "]")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if options.PlatformInterface != nil {
|
outboundManager.Initialize(common.Must1(
|
||||||
err = options.PlatformInterface.Initialize(ctx, router)
|
direct.NewOutbound(
|
||||||
|
ctx,
|
||||||
|
router,
|
||||||
|
logFactory.NewLogger("outbound/direct"),
|
||||||
|
"direct",
|
||||||
|
option.DirectOutboundOptions{},
|
||||||
|
),
|
||||||
|
))
|
||||||
|
dnsTransportManager.Initialize(common.Must1(
|
||||||
|
local.NewTransport(
|
||||||
|
ctx,
|
||||||
|
logFactory.NewLogger("dns/local"),
|
||||||
|
"local",
|
||||||
|
option.LocalDNSServerOptions{},
|
||||||
|
)))
|
||||||
|
if platformInterface != nil {
|
||||||
|
err = platformInterface.Initialize(networkManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize platform interface")
|
return nil, E.Cause(err, "initialize platform interface")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
preServices1 := make(map[string]adapter.Service)
|
|
||||||
preServices2 := make(map[string]adapter.Service)
|
|
||||||
postServices := make(map[string]adapter.Service)
|
|
||||||
if needCacheFile {
|
if needCacheFile {
|
||||||
cacheFile := service.FromContext[adapter.CacheFile](ctx)
|
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||||
if cacheFile == nil {
|
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||||
cacheFile = cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
internalServices = append(internalServices, cacheFile)
|
||||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
|
||||||
}
|
|
||||||
preServices1["cache file"] = cacheFile
|
|
||||||
}
|
}
|
||||||
if needClashAPI {
|
if needClashAPI {
|
||||||
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
||||||
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
||||||
clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), clashAPIOptions)
|
clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "create clash api server")
|
return nil, E.Cause(err, "create clash-server")
|
||||||
}
|
}
|
||||||
router.SetClashServer(clashServer)
|
router.AppendTracker(clashServer)
|
||||||
preServices2["clash api"] = clashServer
|
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
||||||
|
internalServices = append(internalServices, clashServer)
|
||||||
}
|
}
|
||||||
if needV2RayAPI {
|
if needV2RayAPI {
|
||||||
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
|
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "create v2ray api server")
|
return nil, E.Cause(err, "create v2ray-server")
|
||||||
}
|
}
|
||||||
router.SetV2RayServer(v2rayServer)
|
if v2rayServer.StatsService() != nil {
|
||||||
preServices2["v2ray api"] = v2rayServer
|
router.AppendTracker(v2rayServer.StatsService())
|
||||||
|
internalServices = append(internalServices, v2rayServer)
|
||||||
|
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ntpOptions.Enabled {
|
||||||
|
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain())
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "create NTP service")
|
||||||
|
}
|
||||||
|
ntpService := ntp.NewService(ntp.Options{
|
||||||
|
Context: ctx,
|
||||||
|
Dialer: ntpDialer,
|
||||||
|
Logger: logFactory.NewLogger("ntp"),
|
||||||
|
Server: ntpOptions.ServerOptions.Build(),
|
||||||
|
Interval: time.Duration(ntpOptions.Interval),
|
||||||
|
WriteToSystem: ntpOptions.WriteToSystem,
|
||||||
|
})
|
||||||
|
timeService.TimeService = ntpService
|
||||||
|
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||||
}
|
}
|
||||||
return &Box{
|
return &Box{
|
||||||
router: router,
|
network: networkManager,
|
||||||
inbounds: inbounds,
|
endpoint: endpointManager,
|
||||||
outbounds: outbounds,
|
inbound: inboundManager,
|
||||||
createdAt: createdAt,
|
outbound: outboundManager,
|
||||||
logFactory: logFactory,
|
dnsTransport: dnsTransportManager,
|
||||||
logger: logFactory.Logger(),
|
service: serviceManager,
|
||||||
preServices1: preServices1,
|
dnsRouter: dnsRouter,
|
||||||
preServices2: preServices2,
|
connection: connectionManager,
|
||||||
postServices: postServices,
|
router: router,
|
||||||
done: make(chan struct{}),
|
createdAt: createdAt,
|
||||||
|
logFactory: logFactory,
|
||||||
|
logger: logFactory.Logger(),
|
||||||
|
internalService: internalServices,
|
||||||
|
done: make(chan struct{}),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,7 +404,7 @@ func (s *Box) PreStart() error {
|
|||||||
defer func() {
|
defer func() {
|
||||||
v := recover()
|
v := recover()
|
||||||
if v != nil {
|
if v != nil {
|
||||||
log.Error(E.Cause(err, "origin error"))
|
println(err.Error())
|
||||||
debug.PrintStack()
|
debug.PrintStack()
|
||||||
panic("panic on early close: " + fmt.Sprint(v))
|
panic("panic on early close: " + fmt.Sprint(v))
|
||||||
}
|
}
|
||||||
@ -222,9 +423,9 @@ func (s *Box) Start() error {
|
|||||||
defer func() {
|
defer func() {
|
||||||
v := recover()
|
v := recover()
|
||||||
if v != nil {
|
if v != nil {
|
||||||
log.Error(E.Cause(err, "origin error"))
|
println(err.Error())
|
||||||
debug.PrintStack()
|
debug.PrintStack()
|
||||||
panic("panic on early close: " + fmt.Sprint(v))
|
println("panic on early start: " + fmt.Sprint(v))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
s.Close()
|
s.Close()
|
||||||
@ -235,42 +436,26 @@ func (s *Box) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Box) preStart() error {
|
func (s *Box) preStart() error {
|
||||||
monitor := taskmonitor.New(s.logger, C.DefaultStartTimeout)
|
monitor := taskmonitor.New(s.logger, C.StartTimeout)
|
||||||
monitor.Start("start logger")
|
monitor.Start("start logger")
|
||||||
err := s.logFactory.Start()
|
err := s.logFactory.Start()
|
||||||
monitor.Finish()
|
monitor.Finish()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "start logger")
|
return E.Cause(err, "start logger")
|
||||||
}
|
}
|
||||||
for serviceName, service := range s.preServices1 {
|
err = adapter.StartNamed(adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
|
||||||
if preService, isPreService := service.(adapter.PreStarter); isPreService {
|
|
||||||
monitor.Start("pre-start ", serviceName)
|
|
||||||
err := preService.PreStart()
|
|
||||||
monitor.Finish()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "pre-start ", serviceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for serviceName, service := range s.preServices2 {
|
|
||||||
if preService, isPreService := service.(adapter.PreStarter); isPreService {
|
|
||||||
monitor.Start("pre-start ", serviceName)
|
|
||||||
err := preService.PreStart()
|
|
||||||
monitor.Finish()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "pre-start ", serviceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = s.router.PreStart()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "pre-start router")
|
|
||||||
}
|
|
||||||
err = s.startOutbounds()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return s.router.Start()
|
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Box) start() error {
|
func (s *Box) start() error {
|
||||||
@ -278,50 +463,31 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for serviceName, service := range s.preServices1 {
|
err = adapter.StartNamed(adapter.StartStateStart, s.internalService)
|
||||||
err = service.Start()
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return E.Cause(err, "start ", serviceName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for serviceName, service := range s.preServices2 {
|
err = adapter.Start(adapter.StartStateStart, s.inbound, s.endpoint, s.service)
|
||||||
err = service.Start()
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return E.Cause(err, "start ", serviceName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for i, in := range s.inbounds {
|
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
|
||||||
var tag string
|
if err != nil {
|
||||||
if in.Tag() == "" {
|
return err
|
||||||
tag = F.ToString(i)
|
|
||||||
} else {
|
|
||||||
tag = in.Tag()
|
|
||||||
}
|
|
||||||
err = in.Start()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return s.postStart()
|
err = adapter.StartNamed(adapter.StartStatePostStart, s.internalService)
|
||||||
}
|
if err != nil {
|
||||||
|
return err
|
||||||
func (s *Box) postStart() error {
|
|
||||||
for serviceName, service := range s.postServices {
|
|
||||||
err := service.Start()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "start ", serviceName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for _, outbound := range s.outbounds {
|
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||||
if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound {
|
if err != nil {
|
||||||
err := lateOutbound.PostStart()
|
return err
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "post-start outbound/", outbound.Tag())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
err = adapter.StartNamed(adapter.StartStateStarted, s.internalService)
|
||||||
return s.router.PostStart()
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Box) Close() error {
|
func (s *Box) Close() error {
|
||||||
@ -331,58 +497,32 @@ func (s *Box) Close() error {
|
|||||||
default:
|
default:
|
||||||
close(s.done)
|
close(s.done)
|
||||||
}
|
}
|
||||||
monitor := taskmonitor.New(s.logger, C.DefaultStopTimeout)
|
err := common.Close(
|
||||||
var errors error
|
s.service, s.endpoint, s.inbound, s.outbound, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
||||||
for serviceName, service := range s.postServices {
|
)
|
||||||
monitor.Start("close ", serviceName)
|
for _, lifecycleService := range s.internalService {
|
||||||
errors = E.Append(errors, service.Close(), func(err error) error {
|
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||||
return E.Cause(err, "close ", serviceName)
|
return E.Cause(err, "close ", lifecycleService.Name())
|
||||||
})
|
|
||||||
monitor.Finish()
|
|
||||||
}
|
|
||||||
for i, in := range s.inbounds {
|
|
||||||
monitor.Start("close inbound/", in.Type(), "[", i, "]")
|
|
||||||
errors = E.Append(errors, in.Close(), func(err error) error {
|
|
||||||
return E.Cause(err, "close inbound/", in.Type(), "[", i, "]")
|
|
||||||
})
|
|
||||||
monitor.Finish()
|
|
||||||
}
|
|
||||||
for i, out := range s.outbounds {
|
|
||||||
monitor.Start("close outbound/", out.Type(), "[", i, "]")
|
|
||||||
errors = E.Append(errors, common.Close(out), func(err error) error {
|
|
||||||
return E.Cause(err, "close outbound/", out.Type(), "[", i, "]")
|
|
||||||
})
|
|
||||||
monitor.Finish()
|
|
||||||
}
|
|
||||||
monitor.Start("close router")
|
|
||||||
if err := common.Close(s.router); err != nil {
|
|
||||||
errors = E.Append(errors, err, func(err error) error {
|
|
||||||
return E.Cause(err, "close router")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
monitor.Finish()
|
err = E.Append(err, s.logFactory.Close(), func(err error) error {
|
||||||
for serviceName, service := range s.preServices1 {
|
return E.Cause(err, "close logger")
|
||||||
monitor.Start("close ", serviceName)
|
})
|
||||||
errors = E.Append(errors, service.Close(), func(err error) error {
|
return err
|
||||||
return E.Cause(err, "close ", serviceName)
|
}
|
||||||
})
|
|
||||||
monitor.Finish()
|
func (s *Box) Network() adapter.NetworkManager {
|
||||||
}
|
return s.network
|
||||||
for serviceName, service := range s.preServices2 {
|
|
||||||
monitor.Start("close ", serviceName)
|
|
||||||
errors = E.Append(errors, service.Close(), func(err error) error {
|
|
||||||
return E.Cause(err, "close ", serviceName)
|
|
||||||
})
|
|
||||||
monitor.Finish()
|
|
||||||
}
|
|
||||||
if err := common.Close(s.logFactory); err != nil {
|
|
||||||
errors = E.Append(errors, err, func(err error) error {
|
|
||||||
return E.Cause(err, "close logger")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return errors
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Box) Router() adapter.Router {
|
func (s *Box) Router() adapter.Router {
|
||||||
return s.router
|
return s.router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Box) Inbound() adapter.InboundManager {
|
||||||
|
return s.inbound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Box) Outbound() adapter.OutboundManager {
|
||||||
|
return s.outbound
|
||||||
|
}
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
package box
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Box) startOutbounds() error {
|
|
||||||
monitor := taskmonitor.New(s.logger, C.DefaultStartTimeout)
|
|
||||||
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 {
|
|
||||||
monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
|
||||||
err := starter.Start()
|
|
||||||
monitor.Finish()
|
|
||||||
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
|
|
||||||
}
|
|
1
clients/android
Submodule
1
clients/android
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 320170a1077ea5c93872b3e055b96b8836615ef0
|
1
clients/apple
Submodule
1
clients/apple
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit ae5818ee5a24af965dc91f80bffa16e1e6c109c1
|
450
cmd/internal/app_store_connect/main.go
Normal file
450
cmd/internal/app_store_connect/main.go
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/asc-go/asc"
|
||||||
|
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "next_macos_project_version":
|
||||||
|
err := fetchMacOSVersion(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
case "publish_testflight":
|
||||||
|
err := publishTestflight(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
case "cancel_app_store":
|
||||||
|
err := cancelAppStore(ctx, os.Args[2])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
case "prepare_app_store":
|
||||||
|
err := prepareAppStore(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
case "publish_app_store":
|
||||||
|
err := publishAppStore(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Fatal("unknown action: ", os.Args[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
appID = "6673731168"
|
||||||
|
groupID = "5c5f3b78-b7a0-40c0-bcad-e6ef87bbefda"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createClient(expireDuration time.Duration) *asc.Client {
|
||||||
|
privateKey, err := os.ReadFile(os.Getenv("ASC_KEY_PATH"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
tokenConfig, err := asc.NewTokenConfig(os.Getenv("ASC_KEY_ID"), os.Getenv("ASC_KEY_ISSUER_ID"), expireDuration, privateKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return asc.NewClient(tokenConfig.Client())
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchMacOSVersion(ctx context.Context) error {
|
||||||
|
client := createClient(time.Minute)
|
||||||
|
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||||
|
FilterPlatform: []string{"MAC_OS"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var versionID string
|
||||||
|
findVersion:
|
||||||
|
for _, version := range versions.Data {
|
||||||
|
switch *version.Attributes.AppStoreState {
|
||||||
|
case asc.AppStoreVersionStateReadyForSale,
|
||||||
|
asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||||
|
versionID = version.ID
|
||||||
|
break findVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if versionID == "" {
|
||||||
|
return E.New("no version found")
|
||||||
|
}
|
||||||
|
latestBuild, _, err := client.Builds.GetBuildForAppStoreVersion(ctx, versionID, &asc.GetBuildForAppStoreVersionQuery{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
versionInt, err := strconv.Atoi(*latestBuild.Data.Attributes.Version)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "parse version code")
|
||||||
|
}
|
||||||
|
os.Stdout.WriteString(F.ToString(versionInt+1, "\n"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func publishTestflight(ctx context.Context) error {
|
||||||
|
tagVersion, err := build_shared.ReadTagVersion()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tag := tagVersion.VersionString()
|
||||||
|
client := createClient(20 * time.Minute)
|
||||||
|
|
||||||
|
log.Info(tag, " list build IDs")
|
||||||
|
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
|
||||||
|
return it.ID
|
||||||
|
})
|
||||||
|
var platforms []asc.Platform
|
||||||
|
if len(os.Args) == 3 {
|
||||||
|
switch os.Args[2] {
|
||||||
|
case "ios":
|
||||||
|
platforms = []asc.Platform{asc.PlatformIOS}
|
||||||
|
case "macos":
|
||||||
|
platforms = []asc.Platform{asc.PlatformMACOS}
|
||||||
|
case "tvos":
|
||||||
|
platforms = []asc.Platform{asc.PlatformTVOS}
|
||||||
|
default:
|
||||||
|
return E.New("unknown platform: ", os.Args[2])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
platforms = []asc.Platform{
|
||||||
|
asc.PlatformIOS,
|
||||||
|
asc.PlatformMACOS,
|
||||||
|
asc.PlatformTVOS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, platform := range platforms {
|
||||||
|
log.Info(string(platform), " list builds")
|
||||||
|
for {
|
||||||
|
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||||
|
FilterApp: []string{appID},
|
||||||
|
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
build := builds.Data[0]
|
||||||
|
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute {
|
||||||
|
log.Info(string(platform), " ", tag, " waiting for process")
|
||||||
|
time.Sleep(15 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if *build.Attributes.ProcessingState != "VALID" {
|
||||||
|
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
|
||||||
|
time.Sleep(15 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Info(string(platform), " ", tag, " list localizations")
|
||||||
|
localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
localization := common.Find(localizations.Data, func(it asc.BetaBuildLocalization) bool {
|
||||||
|
return *it.Attributes.Locale == "en-US"
|
||||||
|
})
|
||||||
|
if localization.ID == "" {
|
||||||
|
log.Fatal(string(platform), " ", tag, " no en-US localization found")
|
||||||
|
}
|
||||||
|
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
|
||||||
|
log.Info(string(platform), " ", tag, " update localization")
|
||||||
|
_, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(
|
||||||
|
F.ToString("sing-box ", tagVersion.String()),
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Info(string(platform), " ", tag, " publish")
|
||||||
|
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
|
||||||
|
if response != nil && response.StatusCode == http.StatusUnprocessableEntity {
|
||||||
|
log.Info("waiting for process")
|
||||||
|
time.Sleep(15 * time.Second)
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Info(string(platform), " ", tag, " list submissions")
|
||||||
|
betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{
|
||||||
|
FilterBuild: []string{build.ID},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(betaSubmissions.Data) == 0 {
|
||||||
|
log.Info(string(platform), " ", tag, " create submission")
|
||||||
|
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
|
||||||
|
log.Error(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelAppStore(ctx context.Context, platform string) error {
|
||||||
|
switch platform {
|
||||||
|
case "ios":
|
||||||
|
platform = string(asc.PlatformIOS)
|
||||||
|
case "macos":
|
||||||
|
platform = string(asc.PlatformMACOS)
|
||||||
|
case "tvos":
|
||||||
|
platform = string(asc.PlatformTVOS)
|
||||||
|
}
|
||||||
|
tag, err := build_shared.ReadTag()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := createClient(time.Minute)
|
||||||
|
for {
|
||||||
|
log.Info(platform, " list versions")
|
||||||
|
versions, response, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||||
|
FilterPlatform: []string{string(platform)},
|
||||||
|
})
|
||||||
|
if isRetryable(response) {
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
||||||
|
return *it.Attributes.VersionString == tag
|
||||||
|
})
|
||||||
|
if version.ID == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Info(platform, " ", tag, " get submission")
|
||||||
|
submission, response, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
|
||||||
|
if response != nil && response.StatusCode == http.StatusNotFound {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if isRetryable(response) {
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Info(platform, " ", tag, " delete submission")
|
||||||
|
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareAppStore(ctx context.Context) error {
|
||||||
|
tag, err := build_shared.ReadTag()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := createClient(time.Minute)
|
||||||
|
for _, platform := range []asc.Platform{
|
||||||
|
asc.PlatformIOS,
|
||||||
|
asc.PlatformMACOS,
|
||||||
|
asc.PlatformTVOS,
|
||||||
|
} {
|
||||||
|
log.Info(string(platform), " list versions")
|
||||||
|
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||||
|
FilterPlatform: []string{string(platform)},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
||||||
|
return *it.Attributes.VersionString == tag
|
||||||
|
})
|
||||||
|
log.Info(string(platform), " ", tag, " list builds")
|
||||||
|
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||||
|
FilterApp: []string{appID},
|
||||||
|
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(builds.Data) == 0 {
|
||||||
|
log.Fatal(platform, " ", tag, " no build found")
|
||||||
|
}
|
||||||
|
buildID := common.Ptr(builds.Data[0].ID)
|
||||||
|
if version.ID == "" {
|
||||||
|
log.Info(string(platform), " ", tag, " create version")
|
||||||
|
newVersion, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
|
||||||
|
Platform: platform,
|
||||||
|
VersionString: tag,
|
||||||
|
}, appID, buildID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
version = newVersion.Data
|
||||||
|
|
||||||
|
} else {
|
||||||
|
log.Info(string(platform), " ", tag, " check build")
|
||||||
|
currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if response.StatusCode != http.StatusOK || currentBuild.Data.ID != *buildID {
|
||||||
|
switch *version.Attributes.AppStoreState {
|
||||||
|
case asc.AppStoreVersionStatePrepareForSubmission,
|
||||||
|
asc.AppStoreVersionStateRejected,
|
||||||
|
asc.AppStoreVersionStateDeveloperRejected:
|
||||||
|
case asc.AppStoreVersionStateWaitingForReview,
|
||||||
|
asc.AppStoreVersionStateInReview,
|
||||||
|
asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||||
|
submission, _, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if submission != nil {
|
||||||
|
log.Info(string(platform), " ", tag, " delete submission")
|
||||||
|
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
||||||
|
}
|
||||||
|
log.Info(string(platform), " ", tag, " update build")
|
||||||
|
response, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if response.StatusCode != http.StatusNoContent {
|
||||||
|
response.Write(os.Stderr)
|
||||||
|
log.Fatal(string(platform), " ", tag, " unexpected response: ", response.Status)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch *version.Attributes.AppStoreState {
|
||||||
|
case asc.AppStoreVersionStatePrepareForSubmission,
|
||||||
|
asc.AppStoreVersionStateRejected,
|
||||||
|
asc.AppStoreVersionStateDeveloperRejected:
|
||||||
|
case asc.AppStoreVersionStateWaitingForReview,
|
||||||
|
asc.AppStoreVersionStateInReview,
|
||||||
|
asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Info(string(platform), " ", tag, " list localization")
|
||||||
|
localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool {
|
||||||
|
return *it.Attributes.Locale == "en-US"
|
||||||
|
})
|
||||||
|
if localization.ID == "" {
|
||||||
|
log.Info(string(platform), " ", tag, " no en-US localization found")
|
||||||
|
}
|
||||||
|
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
|
||||||
|
log.Info(string(platform), " ", tag, " update localization")
|
||||||
|
_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
|
||||||
|
PromotionalText: common.Ptr("Yet another distribution for sing-box, the universal proxy platform."),
|
||||||
|
WhatsNew: common.Ptr(F.ToString("sing-box ", tag, ": Fixes and improvements.")),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Info(string(platform), " ", tag, " create submission")
|
||||||
|
fixSubmit:
|
||||||
|
for {
|
||||||
|
_, response, err := client.Submission.CreateSubmission(ctx, version.ID)
|
||||||
|
if err != nil {
|
||||||
|
switch response.StatusCode {
|
||||||
|
case http.StatusInternalServerError:
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch response.StatusCode {
|
||||||
|
case http.StatusCreated:
|
||||||
|
break fixSubmit
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func publishAppStore(ctx context.Context) error {
|
||||||
|
tag, err := build_shared.ReadTag()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := createClient(time.Minute)
|
||||||
|
for _, platform := range []asc.Platform{
|
||||||
|
asc.PlatformIOS,
|
||||||
|
asc.PlatformMACOS,
|
||||||
|
asc.PlatformTVOS,
|
||||||
|
} {
|
||||||
|
log.Info(string(platform), " list versions")
|
||||||
|
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||||
|
FilterPlatform: []string{string(platform)},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
||||||
|
return *it.Attributes.VersionString == tag
|
||||||
|
})
|
||||||
|
switch *version.Attributes.AppStoreState {
|
||||||
|
case asc.AppStoreVersionStatePrepareForSubmission, asc.AppStoreVersionStateDeveloperRejected:
|
||||||
|
log.Fatal(string(platform), " ", tag, " not submitted")
|
||||||
|
case asc.AppStoreVersionStateWaitingForReview,
|
||||||
|
asc.AppStoreVersionStateInReview:
|
||||||
|
log.Warn(string(platform), " ", tag, " waiting for review")
|
||||||
|
continue
|
||||||
|
case asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||||
|
default:
|
||||||
|
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
||||||
|
}
|
||||||
|
_, _, err = client.Publishing.CreatePhasedRelease(ctx, common.Ptr(asc.PhasedReleaseStateComplete), version.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRetryable(response *asc.Response) bool {
|
||||||
|
if response == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch response.StatusCode {
|
||||||
|
case http.StatusInternalServerError, http.StatusUnprocessableEntity:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
@ -10,17 +10,21 @@ import (
|
|||||||
_ "github.com/sagernet/gomobile"
|
_ "github.com/sagernet/gomobile"
|
||||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/rw"
|
"github.com/sagernet/sing/common/rw"
|
||||||
|
"github.com/sagernet/sing/common/shell"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
debugEnabled bool
|
debugEnabled bool
|
||||||
target string
|
target string
|
||||||
|
platform string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
|
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
|
||||||
flag.StringVar(&target, "target", "android", "target platform")
|
flag.StringVar(&target, "target", "android", "target platform")
|
||||||
|
flag.StringVar(&platform, "platform", "", "specify platform")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -31,8 +35,8 @@ func main() {
|
|||||||
switch target {
|
switch target {
|
||||||
case "android":
|
case "android":
|
||||||
buildAndroid()
|
buildAndroid()
|
||||||
case "ios":
|
case "apple":
|
||||||
buildiOS()
|
buildApple()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,59 +45,88 @@ var (
|
|||||||
debugFlags []string
|
debugFlags []string
|
||||||
sharedTags []string
|
sharedTags []string
|
||||||
iosTags []string
|
iosTags []string
|
||||||
|
memcTags []string
|
||||||
debugTags []string
|
debugTags []string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
sharedFlags = append(sharedFlags, "-trimpath")
|
sharedFlags = append(sharedFlags, "-trimpath")
|
||||||
sharedFlags = append(sharedFlags, "-ldflags")
|
sharedFlags = append(sharedFlags, "-buildvcs=false")
|
||||||
currentTag, err := build_shared.ReadTag()
|
currentTag, err := build_shared.ReadTag()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
currentTag = "unknown"
|
currentTag = "unknown"
|
||||||
}
|
}
|
||||||
sharedFlags = append(sharedFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
||||||
debugFlags = append(debugFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
||||||
|
|
||||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api")
|
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack")
|
||||||
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
iosTags = append(iosTags, "with_dhcp", "with_low_memory")
|
||||||
|
memcTags = append(memcTags, "with_tailscale")
|
||||||
debugTags = append(debugTags, "debug")
|
debugTags = append(debugTags, "debug")
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildAndroid() {
|
func buildAndroid() {
|
||||||
build_shared.FindSDK()
|
build_shared.FindSDK()
|
||||||
|
|
||||||
|
var javaPath string
|
||||||
|
javaHome := os.Getenv("JAVA_HOME")
|
||||||
|
if javaHome == "" {
|
||||||
|
javaPath = "java"
|
||||||
|
} else {
|
||||||
|
javaPath = filepath.Join(javaHome, "bin", "java")
|
||||||
|
}
|
||||||
|
|
||||||
|
javaVersion, err := shell.Exec(javaPath, "--version").ReadOutput()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(E.Cause(err, "check java version"))
|
||||||
|
}
|
||||||
|
if !strings.Contains(javaVersion, "openjdk 17") {
|
||||||
|
log.Fatal("java version should be openjdk 17")
|
||||||
|
}
|
||||||
|
|
||||||
|
var bindTarget string
|
||||||
|
if platform != "" {
|
||||||
|
bindTarget = platform
|
||||||
|
} else if debugEnabled {
|
||||||
|
bindTarget = "android/arm64"
|
||||||
|
} else {
|
||||||
|
bindTarget = "android"
|
||||||
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"bind",
|
"bind",
|
||||||
"-v",
|
"-v",
|
||||||
|
"-target", bindTarget,
|
||||||
"-androidapi", "21",
|
"-androidapi", "21",
|
||||||
"-javapkg=io.nekohasekai",
|
"-javapkg=io.nekohasekai",
|
||||||
"-libname=box",
|
"-libname=box",
|
||||||
}
|
}
|
||||||
|
|
||||||
if !debugEnabled {
|
if !debugEnabled {
|
||||||
args = append(args, sharedFlags...)
|
args = append(args, sharedFlags...)
|
||||||
} else {
|
} else {
|
||||||
args = append(args, debugFlags...)
|
args = append(args, debugFlags...)
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, "-tags")
|
tags := append(sharedTags, memcTags...)
|
||||||
if !debugEnabled {
|
if debugEnabled {
|
||||||
args = append(args, strings.Join(sharedTags, ","))
|
tags = append(tags, debugTags...)
|
||||||
} else {
|
|
||||||
args = append(args, strings.Join(append(sharedTags, debugTags...), ","))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args = append(args, "-tags", strings.Join(tags, ","))
|
||||||
args = append(args, "./experimental/libbox")
|
args = append(args, "./experimental/libbox")
|
||||||
|
|
||||||
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
||||||
command.Stdout = os.Stdout
|
command.Stdout = os.Stdout
|
||||||
command.Stderr = os.Stderr
|
command.Stderr = os.Stderr
|
||||||
err := command.Run()
|
err = command.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = "libbox.aar"
|
const name = "libbox.aar"
|
||||||
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
|
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
|
||||||
if rw.FileExists(copyPath) {
|
if rw.IsDir(copyPath) {
|
||||||
copyPath, _ = filepath.Abs(copyPath)
|
copyPath, _ = filepath.Abs(copyPath)
|
||||||
err = rw.CopyFile(name, filepath.Join(copyPath, name))
|
err = rw.CopyFile(name, filepath.Join(copyPath, name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -103,13 +136,24 @@ func buildAndroid() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildiOS() {
|
func buildApple() {
|
||||||
|
var bindTarget string
|
||||||
|
if platform != "" {
|
||||||
|
bindTarget = platform
|
||||||
|
} else if debugEnabled {
|
||||||
|
bindTarget = "ios"
|
||||||
|
} else {
|
||||||
|
bindTarget = "ios,tvos,macos"
|
||||||
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"bind",
|
"bind",
|
||||||
"-v",
|
"-v",
|
||||||
"-target", "ios,iossimulator,tvos,tvossimulator,macos",
|
"-target", bindTarget,
|
||||||
"-libname=box",
|
"-libname=box",
|
||||||
|
"-tags-macos=" + strings.Join(memcTags, ","),
|
||||||
}
|
}
|
||||||
|
|
||||||
if !debugEnabled {
|
if !debugEnabled {
|
||||||
args = append(args, sharedFlags...)
|
args = append(args, sharedFlags...)
|
||||||
} else {
|
} else {
|
||||||
@ -117,12 +161,11 @@ func buildiOS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tags := append(sharedTags, iosTags...)
|
tags := append(sharedTags, iosTags...)
|
||||||
args = append(args, "-tags")
|
if debugEnabled {
|
||||||
if !debugEnabled {
|
tags = append(tags, debugTags...)
|
||||||
args = append(args, strings.Join(tags, ","))
|
|
||||||
} else {
|
|
||||||
args = append(args, strings.Join(append(tags, debugTags...), ","))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args = append(args, "-tags", strings.Join(tags, ","))
|
||||||
args = append(args, "./experimental/libbox")
|
args = append(args, "./experimental/libbox")
|
||||||
|
|
||||||
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
||||||
@ -134,7 +177,7 @@ func buildiOS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
copyPath := filepath.Join("..", "sing-box-for-apple")
|
copyPath := filepath.Join("..", "sing-box-for-apple")
|
||||||
if rw.FileExists(copyPath) {
|
if rw.IsDir(copyPath) {
|
||||||
targetDir := filepath.Join(copyPath, "Libbox.xcframework")
|
targetDir := filepath.Join(copyPath, "Libbox.xcframework")
|
||||||
targetDir, _ = filepath.Abs(targetDir)
|
targetDir, _ = filepath.Abs(targetDir)
|
||||||
os.RemoveAll(targetDir)
|
os.RemoveAll(targetDir)
|
||||||
|
@ -28,7 +28,7 @@ func FindSDK() {
|
|||||||
}
|
}
|
||||||
for _, path := range searchPath {
|
for _, path := range searchPath {
|
||||||
path = os.ExpandEnv(path)
|
path = os.ExpandEnv(path)
|
||||||
if rw.FileExists(path + "/licenses/android-sdk-license") {
|
if rw.IsFile(filepath.Join(path, "licenses", "android-sdk-license")) {
|
||||||
androidSDKPath = path
|
androidSDKPath = path
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -48,11 +48,17 @@ func FindSDK() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findNDK() bool {
|
func findNDK() bool {
|
||||||
if rw.FileExists(androidSDKPath + "/ndk/25.1.8937393") {
|
const fixedVersion = "28.0.13004108"
|
||||||
androidNDKPath = androidSDKPath + "/ndk/25.1.8937393"
|
const versionFile = "source.properties"
|
||||||
|
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
|
||||||
|
androidNDKPath = fixedPath
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
ndkVersions, err := os.ReadDir(androidSDKPath + "/ndk")
|
if ndkHomeEnv := os.Getenv("ANDROID_NDK_HOME"); rw.IsFile(filepath.Join(ndkHomeEnv, versionFile)) {
|
||||||
|
androidNDKPath = ndkHomeEnv
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
ndkVersions, err := os.ReadDir(filepath.Join(androidSDKPath, "ndk"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -73,8 +79,10 @@ func findNDK() bool {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
for _, versionName := range versionNames {
|
for _, versionName := range versionNames {
|
||||||
if rw.FileExists(androidSDKPath + "/ndk/" + versionName) {
|
currentNDKPath := filepath.Join(androidSDKPath, "ndk", versionName)
|
||||||
androidNDKPath = androidSDKPath + "/ndk/" + versionName
|
if rw.IsFile(filepath.Join(currentNDKPath, versionFile)) {
|
||||||
|
androidNDKPath = currentNDKPath
|
||||||
|
log.Warn("reproducibility warning: using NDK version " + versionName + " instead of " + fixedVersion)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,13 +93,12 @@ var GoBinPath string
|
|||||||
|
|
||||||
func FindMobile() {
|
func FindMobile() {
|
||||||
goBin := filepath.Join(build.Default.GOPATH, "bin")
|
goBin := filepath.Join(build.Default.GOPATH, "bin")
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
if !rw.FileExists(goBin + "/" + "gobind.exe") {
|
if !rw.IsFile(filepath.Join(goBin, "gobind.exe")) {
|
||||||
log.Fatal("missing gomobile.exe installation")
|
log.Fatal("missing gomobile installation")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !rw.FileExists(goBin + "/" + "gobind") {
|
if !rw.IsFile(filepath.Join(goBin, "gobind")) {
|
||||||
log.Fatal("missing gomobile installation")
|
log.Fatal("missing gomobile installation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,11 @@ func ReadTag() (string, error) {
|
|||||||
return version.String() + "-" + shortCommit, nil
|
return version.String() + "-" + shortCommit, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReadTagVersionRev() (badversion.Version, error) {
|
||||||
|
currentTagRev := common.Must1(shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput())
|
||||||
|
return badversion.Parse(currentTagRev[1:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
func ReadTagVersion() (badversion.Version, error) {
|
func ReadTagVersion() (badversion.Version, error) {
|
||||||
currentTag := common.Must1(shell.Exec("git", "describe", "--tags").ReadOutput())
|
currentTag := common.Must1(shell.Exec("git", "describe", "--tags").ReadOutput())
|
||||||
currentTagRev := common.Must1(shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput())
|
currentTagRev := common.Must1(shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput())
|
||||||
|
@ -1,21 +1,71 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||||
|
"github.com/sagernet/sing-box/common/badversion"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagRunInCI bool
|
||||||
|
flagRunNightly bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
||||||
|
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
currentTag, err := build_shared.ReadTag()
|
flag.Parse()
|
||||||
if err != nil {
|
var (
|
||||||
log.Error(err)
|
versionStr string
|
||||||
_, err = os.Stdout.WriteString("unknown\n")
|
err error
|
||||||
|
)
|
||||||
|
if flagRunNightly {
|
||||||
|
var version badversion.Version
|
||||||
|
version, err = build_shared.ReadTagVersion()
|
||||||
|
if err == nil {
|
||||||
|
versionStr = version.String()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_, err = os.Stdout.WriteString(currentTag + "\n")
|
versionStr, err = build_shared.ReadTag()
|
||||||
}
|
}
|
||||||
if err != nil {
|
if flagRunInCI {
|
||||||
log.Error(err)
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
err = setGitHubEnv("version", versionStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
os.Stdout.WriteString("unknown\n")
|
||||||
|
} else {
|
||||||
|
os.Stdout.WriteString(versionStr + "\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setGitHubEnv(name string, value string) error {
|
||||||
|
outputFile, err := os.OpenFile(os.Getenv("GITHUB_ENV"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = outputFile.WriteString(name + "=" + value + "\n")
|
||||||
|
if err != nil {
|
||||||
|
outputFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = outputFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
os.Stderr.WriteString(name + "=" + value + "\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -11,41 +13,72 @@ import (
|
|||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagRunInCI bool
|
||||||
|
flagRunNightly bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
||||||
|
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
newVersion := common.Must1(build_shared.ReadTagVersion())
|
flag.Parse()
|
||||||
androidPath, err := filepath.Abs("../sing-box-for-android")
|
newVersion := common.Must1(build_shared.ReadTag())
|
||||||
|
var androidPath string
|
||||||
|
if flagRunInCI {
|
||||||
|
androidPath = "clients/android"
|
||||||
|
} else {
|
||||||
|
androidPath = "../sing-box-for-android"
|
||||||
|
}
|
||||||
|
androidPath, err := filepath.Abs(androidPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
common.Must(os.Chdir(androidPath))
|
common.Must(os.Chdir(androidPath))
|
||||||
localProps := common.Must1(os.ReadFile("local.properties"))
|
localProps := common.Must1(os.ReadFile("version.properties"))
|
||||||
var propsList [][]string
|
var propsList [][]string
|
||||||
for _, propLine := range strings.Split(string(localProps), "\n") {
|
for _, propLine := range strings.Split(string(localProps), "\n") {
|
||||||
propsList = append(propsList, strings.Split(propLine, "="))
|
propsList = append(propsList, strings.Split(propLine, "="))
|
||||||
}
|
}
|
||||||
|
var (
|
||||||
|
versionUpdated bool
|
||||||
|
goVersionUpdated bool
|
||||||
|
)
|
||||||
for _, propPair := range propsList {
|
for _, propPair := range propsList {
|
||||||
if propPair[0] == "VERSION_NAME" {
|
switch propPair[0] {
|
||||||
if propPair[1] == newVersion.String() {
|
case "VERSION_NAME":
|
||||||
log.Info("version not changed")
|
if propPair[1] != newVersion {
|
||||||
return
|
log.Info("updated version from ", propPair[1], " to ", newVersion)
|
||||||
|
versionUpdated = true
|
||||||
|
propPair[1] = newVersion
|
||||||
|
}
|
||||||
|
case "GO_VERSION":
|
||||||
|
if propPair[1] != runtime.Version() {
|
||||||
|
log.Info("updated Go version from ", propPair[1], " to ", runtime.Version())
|
||||||
|
goVersionUpdated = true
|
||||||
|
propPair[1] = runtime.Version()
|
||||||
}
|
}
|
||||||
propPair[1] = newVersion.String()
|
|
||||||
log.Info("updated version to ", newVersion.String())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !(versionUpdated || goVersionUpdated) {
|
||||||
|
log.Info("version not changed")
|
||||||
|
return
|
||||||
|
} else if flagRunInCI && !flagRunNightly {
|
||||||
|
log.Fatal("version changed, commit changes first.")
|
||||||
|
}
|
||||||
for _, propPair := range propsList {
|
for _, propPair := range propsList {
|
||||||
switch propPair[0] {
|
switch propPair[0] {
|
||||||
case "VERSION_CODE":
|
case "VERSION_CODE":
|
||||||
versionCode := common.Must1(strconv.ParseInt(propPair[1], 10, 64))
|
versionCode := common.Must1(strconv.ParseInt(propPair[1], 10, 64))
|
||||||
propPair[1] = strconv.Itoa(int(versionCode + 1))
|
propPair[1] = strconv.Itoa(int(versionCode + 1))
|
||||||
log.Info("updated version code to ", propPair[1])
|
log.Info("updated version code to ", propPair[1])
|
||||||
case "RELEASE_NOTES":
|
|
||||||
propPair[1] = "sing-box " + newVersion.String()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var newProps []string
|
var newProps []string
|
||||||
for _, propPair := range propsList {
|
for _, propPair := range propsList {
|
||||||
newProps = append(newProps, strings.Join(propPair, "="))
|
newProps = append(newProps, strings.Join(propPair, "="))
|
||||||
}
|
}
|
||||||
common.Must(os.WriteFile("local.properties", []byte(strings.Join(newProps, "\n")), 0o644))
|
common.Must(os.WriteFile("version.properties", []byte(strings.Join(newProps, "\n")), 0o644))
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -13,9 +14,22 @@ import (
|
|||||||
"howett.net/plist"
|
"howett.net/plist"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var flagRunInCI bool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
newVersion := common.Must1(build_shared.ReadTagVersion())
|
newVersion := common.Must1(build_shared.ReadTagVersion())
|
||||||
applePath, err := filepath.Abs("../sing-box-for-apple")
|
var applePath string
|
||||||
|
if flagRunInCI {
|
||||||
|
applePath = "clients/apple"
|
||||||
|
} else {
|
||||||
|
applePath = "../sing-box-for-apple"
|
||||||
|
}
|
||||||
|
applePath, err := filepath.Abs(applePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -26,8 +40,8 @@ func main() {
|
|||||||
common.Must(decoder.Decode(&project))
|
common.Must(decoder.Decode(&project))
|
||||||
objectsMap := project["objects"].(map[string]any)
|
objectsMap := project["objects"].(map[string]any)
|
||||||
projectContent := string(common.Must1(os.ReadFile("sing-box.xcodeproj/project.pbxproj")))
|
projectContent := string(common.Must1(os.ReadFile("sing-box.xcodeproj/project.pbxproj")))
|
||||||
newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfa"}, newVersion.VersionString())
|
newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfavt"}, newVersion.VersionString())
|
||||||
newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfa.independent", "io.nekohasekai.sfa.system"}, newVersion.String())
|
newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfavt.standalone", "io.nekohasekai.sfavt.system"}, newVersion.String())
|
||||||
if updated0 || updated1 {
|
if updated0 || updated1 {
|
||||||
log.Info("updated version to ", newVersion.VersionString(), " (", newVersion.String(), ")")
|
log.Info("updated version to ", newVersion.VersionString(), " (", newVersion.String(), ")")
|
||||||
}
|
}
|
||||||
|
71
cmd/internal/update_certificates/main.go
Normal file
71
cmd/internal/update_certificates/main.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := updateMozillaIncludedRootCAs()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMozillaIncludedRootCAs() error {
|
||||||
|
response, err := http.Get("https://ccadb.my.salesforce-sites.com/mozilla/IncludedCACertificateReportPEMCSV")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
reader := csv.NewReader(response.Body)
|
||||||
|
header, err := reader.Read()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
geoIndex := slices.Index(header, "Geographic Focus")
|
||||||
|
nameIndex := slices.Index(header, "Common Name or Certificate Name")
|
||||||
|
certIndex := slices.Index(header, "PEM Info")
|
||||||
|
|
||||||
|
generated := strings.Builder{}
|
||||||
|
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||||
|
|
||||||
|
package certificate
|
||||||
|
|
||||||
|
import "crypto/x509"
|
||||||
|
|
||||||
|
var mozillaIncluded *x509.CertPool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
mozillaIncluded = x509.NewCertPool()
|
||||||
|
`)
|
||||||
|
for {
|
||||||
|
record, err := reader.Read()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if record[geoIndex] == "China" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
generated.WriteString("\n // ")
|
||||||
|
generated.WriteString(record[nameIndex])
|
||||||
|
generated.WriteString("\n")
|
||||||
|
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
|
||||||
|
cert := record[certIndex]
|
||||||
|
// Remove single quotes
|
||||||
|
cert = cert[1 : len(cert)-1]
|
||||||
|
generated.WriteString(cert)
|
||||||
|
generated.WriteString("`))\n")
|
||||||
|
}
|
||||||
|
generated.WriteString("}\n")
|
||||||
|
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
|
||||||
|
}
|
71
cmd/sing-box/cmd.go
Normal file
71
cmd/sing-box/cmd.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||||
|
"github.com/sagernet/sing-box/include"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
"github.com/sagernet/sing/service/filemanager"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
globalCtx context.Context
|
||||||
|
configPaths []string
|
||||||
|
configDirectories []string
|
||||||
|
workingDir string
|
||||||
|
disableColor bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var mainCommand = &cobra.Command{
|
||||||
|
Use: "sing-box",
|
||||||
|
PersistentPreRun: preRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
mainCommand.PersistentFlags().StringArrayVarP(&configPaths, "config", "c", nil, "set configuration file path")
|
||||||
|
mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path")
|
||||||
|
mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
|
||||||
|
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
|
||||||
|
}
|
||||||
|
|
||||||
|
func preRun(cmd *cobra.Command, args []string) {
|
||||||
|
globalCtx = context.Background()
|
||||||
|
sudoUser := os.Getenv("SUDO_USER")
|
||||||
|
sudoUID, _ := strconv.Atoi(os.Getenv("SUDO_UID"))
|
||||||
|
sudoGID, _ := strconv.Atoi(os.Getenv("SUDO_GID"))
|
||||||
|
if sudoUID == 0 && sudoGID == 0 && sudoUser != "" {
|
||||||
|
sudoUserObject, _ := user.Lookup(sudoUser)
|
||||||
|
if sudoUserObject != nil {
|
||||||
|
sudoUID, _ = strconv.Atoi(sudoUserObject.Uid)
|
||||||
|
sudoGID, _ = strconv.Atoi(sudoUserObject.Gid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sudoUID > 0 && sudoGID > 0 {
|
||||||
|
globalCtx = filemanager.WithDefault(globalCtx, "", "", sudoUID, sudoGID)
|
||||||
|
}
|
||||||
|
if disableColor {
|
||||||
|
log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger())
|
||||||
|
}
|
||||||
|
if workingDir != "" {
|
||||||
|
_, err := os.Stat(workingDir)
|
||||||
|
if err != nil {
|
||||||
|
filemanager.MkdirAll(globalCtx, workingDir, 0o777)
|
||||||
|
}
|
||||||
|
err = os.Chdir(workingDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(configPaths) == 0 && len(configDirectories) == 0 {
|
||||||
|
configPaths = append(configPaths, "config.json")
|
||||||
|
}
|
||||||
|
globalCtx = include.Context(service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger())))
|
||||||
|
}
|
@ -30,7 +30,7 @@ func check() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(globalCtx)
|
||||||
instance, err := box.New(box.Options{
|
instance, err := box.New(box.Options{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Options: options,
|
Options: options,
|
||||||
|
@ -38,7 +38,7 @@ func format() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, optionsEntry := range optionsList {
|
for _, optionsEntry := range optionsList {
|
||||||
optionsEntry.options, err = badjson.Omitempty(optionsEntry.options)
|
optionsEntry.options, err = badjson.Omitempty(globalCtx, optionsEntry.options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,6 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var pqSignatureSchemesEnabled bool
|
|
||||||
|
|
||||||
var commandGenerateECHKeyPair = &cobra.Command{
|
var commandGenerateECHKeyPair = &cobra.Command{
|
||||||
Use: "ech-keypair <plain_server_name>",
|
Use: "ech-keypair <plain_server_name>",
|
||||||
Short: "Generate TLS ECH key pair",
|
Short: "Generate TLS ECH key pair",
|
||||||
@ -24,12 +22,11 @@ var commandGenerateECHKeyPair = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
commandGenerateECHKeyPair.Flags().BoolVar(&pqSignatureSchemesEnabled, "pq-signature-schemes-enabled", false, "Enable PQ signature schemes")
|
|
||||||
commandGenerate.AddCommand(commandGenerateECHKeyPair)
|
commandGenerate.AddCommand(commandGenerateECHKeyPair)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateECHKeyPair(serverName string) error {
|
func generateECHKeyPair(serverName string) error {
|
||||||
configPem, keyPem, err := tls.ECHKeygenDefault(serverName, pqSignatureSchemesEnabled)
|
configPem, keyPem, err := tls.ECHKeygenDefault(serverName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateTLSKeyPair(serverName string) error {
|
func generateTLSKeyPair(serverName string) error {
|
||||||
privateKeyPem, publicKeyPem, err := tls.GenerateKeyPair(time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
|
privateKeyPem, publicKeyPem, err := tls.GenerateCertificate(nil, nil, time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ func geoipExport(countryCode string) error {
|
|||||||
headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String())
|
headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String())
|
||||||
}
|
}
|
||||||
var plainRuleSet option.PlainRuleSetCompat
|
var plainRuleSet option.PlainRuleSetCompat
|
||||||
plainRuleSet.Version = C.RuleSetVersion1
|
plainRuleSet.Version = C.RuleSetVersion2
|
||||||
plainRuleSet.Options.Rules = []option.HeadlessRule{
|
plainRuleSet.Options.Rules = []option.HeadlessRule{
|
||||||
{
|
{
|
||||||
Type: C.RuleTypeDefault,
|
Type: C.RuleTypeDefault,
|
||||||
|
@ -70,7 +70,7 @@ func geositeExport(category string) error {
|
|||||||
headlessRule.DomainKeyword = defaultRule.DomainKeyword
|
headlessRule.DomainKeyword = defaultRule.DomainKeyword
|
||||||
headlessRule.DomainRegex = defaultRule.DomainRegex
|
headlessRule.DomainRegex = defaultRule.DomainRegex
|
||||||
var plainRuleSet option.PlainRuleSetCompat
|
var plainRuleSet option.PlainRuleSetCompat
|
||||||
plainRuleSet.Version = C.RuleSetVersion1
|
plainRuleSet.Version = C.RuleSetVersion2
|
||||||
plainRuleSet.Options.Rules = []option.HeadlessRule{
|
plainRuleSet.Options.Rules = []option.HeadlessRule{
|
||||||
{
|
{
|
||||||
Type: C.RuleTypeDefault,
|
Type: C.RuleTypeDefault,
|
||||||
|
@ -18,7 +18,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var commandMerge = &cobra.Command{
|
var commandMerge = &cobra.Command{
|
||||||
Use: "merge <output>",
|
Use: "merge <output-path>",
|
||||||
Short: "Merge configurations",
|
Short: "Merge configurations",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
err := merge(args[0])
|
err := merge(args[0])
|
||||||
@ -54,7 +54,11 @@ func merge(outputPath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = rw.WriteFile(outputPath, buffer.Bytes())
|
err = rw.MkdirParent(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.WriteFile(outputPath, buffer.Bytes(), 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -64,29 +68,19 @@ func merge(outputPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func mergePathResources(options *option.Options) error {
|
func mergePathResources(options *option.Options) error {
|
||||||
for index, inbound := range options.Inbounds {
|
for _, inbound := range options.Inbounds {
|
||||||
rawOptions, err := inbound.RawOptions()
|
if tlsOptions, containsTLSOptions := inbound.Options.(option.InboundTLSOptionsWrapper); containsTLSOptions {
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if tlsOptions, containsTLSOptions := rawOptions.(option.InboundTLSOptionsWrapper); containsTLSOptions {
|
|
||||||
tlsOptions.ReplaceInboundTLSOptions(mergeTLSInboundOptions(tlsOptions.TakeInboundTLSOptions()))
|
tlsOptions.ReplaceInboundTLSOptions(mergeTLSInboundOptions(tlsOptions.TakeInboundTLSOptions()))
|
||||||
}
|
}
|
||||||
options.Inbounds[index] = inbound
|
|
||||||
}
|
}
|
||||||
for index, outbound := range options.Outbounds {
|
for _, outbound := range options.Outbounds {
|
||||||
rawOptions, err := outbound.RawOptions()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
switch outbound.Type {
|
switch outbound.Type {
|
||||||
case C.TypeSSH:
|
case C.TypeSSH:
|
||||||
outbound.SSHOptions = mergeSSHOutboundOptions(outbound.SSHOptions)
|
mergeSSHOutboundOptions(outbound.Options.(*option.SSHOutboundOptions))
|
||||||
}
|
}
|
||||||
if tlsOptions, containsTLSOptions := rawOptions.(option.OutboundTLSOptionsWrapper); containsTLSOptions {
|
if tlsOptions, containsTLSOptions := outbound.Options.(option.OutboundTLSOptionsWrapper); containsTLSOptions {
|
||||||
tlsOptions.ReplaceOutboundTLSOptions(mergeTLSOutboundOptions(tlsOptions.TakeOutboundTLSOptions()))
|
tlsOptions.ReplaceOutboundTLSOptions(mergeTLSOutboundOptions(tlsOptions.TakeOutboundTLSOptions()))
|
||||||
}
|
}
|
||||||
options.Outbounds[index] = outbound
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -134,13 +128,12 @@ func mergeTLSOutboundOptions(options *option.OutboundTLSOptions) *option.Outboun
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeSSHOutboundOptions(options option.SSHOutboundOptions) option.SSHOutboundOptions {
|
func mergeSSHOutboundOptions(options *option.SSHOutboundOptions) {
|
||||||
if options.PrivateKeyPath != "" {
|
if options.PrivateKeyPath != "" {
|
||||||
if content, err := os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)); err == nil {
|
if content, err := os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)); err == nil {
|
||||||
options.PrivateKey = trimStringArray(strings.Split(string(content), "\n"))
|
options.PrivateKey = trimStringArray(strings.Split(string(content), "\n"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return options
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func trimStringArray(array []string) []string {
|
func trimStringArray(array []string) []string {
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
var commandRuleSet = &cobra.Command{
|
var commandRuleSet = &cobra.Command{
|
||||||
Use: "rule-set",
|
Use: "rule-set",
|
||||||
Short: "Manage rule sets",
|
Short: "Manage rule-sets",
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -55,10 +55,6 @@ func compileRuleSet(sourcePath string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ruleSet := plainRuleSet.Upgrade()
|
|
||||||
var outputPath string
|
var outputPath string
|
||||||
if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput {
|
if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput {
|
||||||
if strings.HasSuffix(sourcePath, ".json") {
|
if strings.HasSuffix(sourcePath, ".json") {
|
||||||
@ -73,7 +69,7 @@ func compileRuleSet(sourcePath string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = srs.Write(outputFile, ruleSet)
|
err = srs.Write(outputFile, plainRuleSet.Options, plainRuleSet.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
outputFile.Close()
|
outputFile.Close()
|
||||||
os.Remove(outputPath)
|
os.Remove(outputPath)
|
||||||
|
89
cmd/sing-box/cmd_rule_set_convert.go
Normal file
89
cmd/sing-box/cmd_rule_set_convert.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/cmd/sing-box/internal/convertor/adguard"
|
||||||
|
"github.com/sagernet/sing-box/common/srs"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagRuleSetConvertType string
|
||||||
|
flagRuleSetConvertOutput string
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandRuleSetConvert = &cobra.Command{
|
||||||
|
Use: "convert [source-path]",
|
||||||
|
Short: "Convert adguard DNS filter to rule-set",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := convertRuleSet(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandRuleSet.AddCommand(commandRuleSetConvert)
|
||||||
|
commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertType, "type", "t", "", "Source type, available: adguard")
|
||||||
|
commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertRuleSet(sourcePath string) error {
|
||||||
|
var (
|
||||||
|
reader io.Reader
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if sourcePath == "stdin" {
|
||||||
|
reader = os.Stdin
|
||||||
|
} else {
|
||||||
|
reader, err = os.Open(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var rules []option.HeadlessRule
|
||||||
|
switch flagRuleSetConvertType {
|
||||||
|
case "adguard":
|
||||||
|
rules, err = adguard.Convert(reader)
|
||||||
|
case "":
|
||||||
|
return E.New("source type is required")
|
||||||
|
default:
|
||||||
|
return E.New("unsupported source type: ", flagRuleSetConvertType)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var outputPath string
|
||||||
|
if flagRuleSetConvertOutput == flagRuleSetCompileDefaultOutput {
|
||||||
|
if strings.HasSuffix(sourcePath, ".txt") {
|
||||||
|
outputPath = sourcePath[:len(sourcePath)-4] + ".srs"
|
||||||
|
} else {
|
||||||
|
outputPath = sourcePath + ".srs"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputPath = flagRuleSetConvertOutput
|
||||||
|
}
|
||||||
|
outputFile, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer outputFile.Close()
|
||||||
|
err = srs.Write(outputFile, option.PlainRuleSet{Rules: rules}, C.RuleSetVersion2)
|
||||||
|
if err != nil {
|
||||||
|
outputFile.Close()
|
||||||
|
os.Remove(outputPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outputFile.Close()
|
||||||
|
return nil
|
||||||
|
}
|
77
cmd/sing-box/cmd_rule_set_decompile.go
Normal file
77
cmd/sing-box/cmd_rule_set_decompile.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/srs"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var flagRuleSetDecompileOutput string
|
||||||
|
|
||||||
|
const flagRuleSetDecompileDefaultOutput = "<file_name>.json"
|
||||||
|
|
||||||
|
var commandRuleSetDecompile = &cobra.Command{
|
||||||
|
Use: "decompile [binary-path]",
|
||||||
|
Short: "Decompile rule-set binary to json",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := decompileRuleSet(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandRuleSet.AddCommand(commandRuleSetDecompile)
|
||||||
|
commandRuleSetDecompile.Flags().StringVarP(&flagRuleSetDecompileOutput, "output", "o", flagRuleSetDecompileDefaultOutput, "Output file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func decompileRuleSet(sourcePath string) error {
|
||||||
|
var (
|
||||||
|
reader io.Reader
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if sourcePath == "stdin" {
|
||||||
|
reader = os.Stdin
|
||||||
|
} else {
|
||||||
|
reader, err = os.Open(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ruleSet, err := srs.Read(reader, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var outputPath string
|
||||||
|
if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput {
|
||||||
|
if strings.HasSuffix(sourcePath, ".srs") {
|
||||||
|
outputPath = sourcePath[:len(sourcePath)-4] + ".json"
|
||||||
|
} else {
|
||||||
|
outputPath = sourcePath + ".json"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputPath = flagRuleSetDecompileOutput
|
||||||
|
}
|
||||||
|
outputFile, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
encoder := json.NewEncoder(outputFile)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
err = encoder.Encode(ruleSet)
|
||||||
|
if err != nil {
|
||||||
|
outputFile.Close()
|
||||||
|
os.Remove(outputPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outputFile.Close()
|
||||||
|
return nil
|
||||||
|
}
|
105
cmd/sing-box/cmd_rule_set_match.go
Normal file
105
cmd/sing-box/cmd_rule_set_match.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/srs"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-box/route/rule"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var flagRuleSetMatchFormat string
|
||||||
|
|
||||||
|
var commandRuleSetMatch = &cobra.Command{
|
||||||
|
Use: "match <rule-set path> <IP address/domain>",
|
||||||
|
Short: "Check if an IP address or a domain matches the rule-set",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := ruleSetMatch(args[0], args[1])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandRuleSetMatch.Flags().StringVarP(&flagRuleSetMatchFormat, "format", "f", "source", "rule-set format")
|
||||||
|
commandRuleSet.AddCommand(commandRuleSetMatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ruleSetMatch(sourcePath string, domain string) error {
|
||||||
|
var (
|
||||||
|
reader io.Reader
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if sourcePath == "stdin" {
|
||||||
|
reader = os.Stdin
|
||||||
|
} else {
|
||||||
|
reader, err = os.Open(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "read rule-set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "read rule-set")
|
||||||
|
}
|
||||||
|
if flagRuleSetMatchFormat == "" {
|
||||||
|
switch filepath.Ext(sourcePath) {
|
||||||
|
case ".json":
|
||||||
|
flagRuleSetMatchFormat = C.RuleSetFormatSource
|
||||||
|
case ".srs":
|
||||||
|
flagRuleSetMatchFormat = C.RuleSetFormatBinary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var ruleSet option.PlainRuleSetCompat
|
||||||
|
switch flagRuleSetMatchFormat {
|
||||||
|
case C.RuleSetFormatSource:
|
||||||
|
ruleSet, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case C.RuleSetFormatBinary:
|
||||||
|
ruleSet, err = srs.Read(bytes.NewReader(content), false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return E.New("unknown rule-set format: ", flagRuleSetMatchFormat)
|
||||||
|
}
|
||||||
|
plainRuleSet, err := ruleSet.Upgrade()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ipAddress := M.ParseAddr(domain)
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
if ipAddress.IsValid() {
|
||||||
|
metadata.Destination = M.SocksaddrFrom(ipAddress, 0)
|
||||||
|
} else {
|
||||||
|
metadata.Domain = domain
|
||||||
|
}
|
||||||
|
for i, ruleOptions := range plainRuleSet.Rules {
|
||||||
|
var currentRule adapter.HeadlessRule
|
||||||
|
currentRule, err = rule.NewHeadlessRule(context.Background(), ruleOptions)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "parse rule_set.rules.[", i, "]")
|
||||||
|
}
|
||||||
|
if currentRule.Match(&metadata) {
|
||||||
|
println(F.ToString("match rules.[", i, "]: ", currentRule))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
162
cmd/sing-box/cmd_rule_set_merge.go
Normal file
162
cmd/sing-box/cmd_rule_set_merge.go
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
"github.com/sagernet/sing/common/json/badjson"
|
||||||
|
"github.com/sagernet/sing/common/rw"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ruleSetPaths []string
|
||||||
|
ruleSetDirectories []string
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandRuleSetMerge = &cobra.Command{
|
||||||
|
Use: "merge <output-path>",
|
||||||
|
Short: "Merge rule-set source files",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := mergeRuleSet(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetPaths, "config", "c", nil, "set input rule-set file path")
|
||||||
|
commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetDirectories, "config-directory", "C", nil, "set input rule-set directory path")
|
||||||
|
commandRuleSet.AddCommand(commandRuleSetMerge)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleSetEntry struct {
|
||||||
|
content []byte
|
||||||
|
path string
|
||||||
|
options option.PlainRuleSetCompat
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRuleSetAt(path string) (*RuleSetEntry, error) {
|
||||||
|
var (
|
||||||
|
configContent []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if path == "stdin" {
|
||||||
|
configContent, err = io.ReadAll(os.Stdin)
|
||||||
|
} else {
|
||||||
|
configContent, err = os.ReadFile(path)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read config at ", path)
|
||||||
|
}
|
||||||
|
options, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, configContent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "decode config at ", path)
|
||||||
|
}
|
||||||
|
return &RuleSetEntry{
|
||||||
|
content: configContent,
|
||||||
|
path: path,
|
||||||
|
options: options,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRuleSet() ([]*RuleSetEntry, error) {
|
||||||
|
var optionsList []*RuleSetEntry
|
||||||
|
for _, path := range ruleSetPaths {
|
||||||
|
optionsEntry, err := readRuleSetAt(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
optionsList = append(optionsList, optionsEntry)
|
||||||
|
}
|
||||||
|
for _, directory := range ruleSetDirectories {
|
||||||
|
entries, err := os.ReadDir(directory)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read rule-set directory at ", directory)
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
optionsEntry, err := readRuleSetAt(filepath.Join(directory, entry.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
optionsList = append(optionsList, optionsEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(optionsList, func(i, j int) bool {
|
||||||
|
return optionsList[i].path < optionsList[j].path
|
||||||
|
})
|
||||||
|
return optionsList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRuleSetAndMerge() (option.PlainRuleSetCompat, error) {
|
||||||
|
optionsList, err := readRuleSet()
|
||||||
|
if err != nil {
|
||||||
|
return option.PlainRuleSetCompat{}, err
|
||||||
|
}
|
||||||
|
if len(optionsList) == 1 {
|
||||||
|
return optionsList[0].options, nil
|
||||||
|
}
|
||||||
|
var optionVersion uint8
|
||||||
|
for _, options := range optionsList {
|
||||||
|
if optionVersion < options.options.Version {
|
||||||
|
optionVersion = options.options.Version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var mergedMessage json.RawMessage
|
||||||
|
for _, options := range optionsList {
|
||||||
|
mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false)
|
||||||
|
if err != nil {
|
||||||
|
return option.PlainRuleSetCompat{}, E.Cause(err, "merge config at ", options.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mergedOptions, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, mergedMessage)
|
||||||
|
if err != nil {
|
||||||
|
return option.PlainRuleSetCompat{}, E.Cause(err, "unmarshal merged config")
|
||||||
|
}
|
||||||
|
mergedOptions.Version = optionVersion
|
||||||
|
return mergedOptions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeRuleSet(outputPath string) error {
|
||||||
|
mergedOptions, err := readRuleSetAndMerge()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
encoder := json.NewEncoder(buffer)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
err = encoder.Encode(mergedOptions)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "encode config")
|
||||||
|
}
|
||||||
|
if existsContent, err := os.ReadFile(outputPath); err != nil {
|
||||||
|
if string(existsContent) == buffer.String() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = rw.MkdirParent(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.WriteFile(outputPath, buffer.Bytes(), 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outputPath, _ = filepath.Abs(outputPath)
|
||||||
|
os.Stderr.WriteString(outputPath + "\n")
|
||||||
|
return nil
|
||||||
|
}
|
95
cmd/sing-box/cmd_rule_set_upgrade.go
Normal file
95
cmd/sing-box/cmd_rule_set_upgrade.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandRuleSetUpgradeFlagWrite bool
|
||||||
|
|
||||||
|
var commandRuleSetUpgrade = &cobra.Command{
|
||||||
|
Use: "upgrade <source-path>",
|
||||||
|
Short: "Upgrade rule-set json",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := upgradeRuleSet(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandRuleSetUpgrade.Flags().BoolVarP(&commandRuleSetUpgradeFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
|
||||||
|
commandRuleSet.AddCommand(commandRuleSetUpgrade)
|
||||||
|
}
|
||||||
|
|
||||||
|
func upgradeRuleSet(sourcePath string) error {
|
||||||
|
var (
|
||||||
|
reader io.Reader
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if sourcePath == "stdin" {
|
||||||
|
reader = os.Stdin
|
||||||
|
} else {
|
||||||
|
reader, err = os.Open(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
plainRuleSetCompat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch plainRuleSetCompat.Version {
|
||||||
|
case C.RuleSetVersion1:
|
||||||
|
default:
|
||||||
|
log.Info("already up-to-date")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
plainRuleSetCompat.Options, err = plainRuleSetCompat.Upgrade()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
plainRuleSetCompat.Version = C.RuleSetVersionCurrent
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
encoder := json.NewEncoder(buffer)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
err = encoder.Encode(plainRuleSetCompat)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "encode config")
|
||||||
|
}
|
||||||
|
outputPath, _ := filepath.Abs(sourcePath)
|
||||||
|
if !commandRuleSetUpgradeFlagWrite || sourcePath == "stdin" {
|
||||||
|
os.Stdout.WriteString(buffer.String() + "\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if bytes.Equal(content, buffer.Bytes()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
output, err := os.Create(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "open output")
|
||||||
|
}
|
||||||
|
_, err = output.Write(buffer.Bytes())
|
||||||
|
output.Close()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "write output")
|
||||||
|
}
|
||||||
|
os.Stderr.WriteString(outputPath + "\n")
|
||||||
|
return nil
|
||||||
|
}
|
@ -57,7 +57,7 @@ func readConfigAt(path string) (*OptionsEntry, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "read config at ", path)
|
return nil, E.Cause(err, "read config at ", path)
|
||||||
}
|
}
|
||||||
options, err := json.UnmarshalExtended[option.Options](configContent)
|
options, err := json.UnmarshalExtendedContext[option.Options](globalCtx, configContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "decode config at ", path)
|
return nil, E.Cause(err, "decode config at ", path)
|
||||||
}
|
}
|
||||||
@ -109,13 +109,13 @@ func readConfigAndMerge() (option.Options, error) {
|
|||||||
}
|
}
|
||||||
var mergedMessage json.RawMessage
|
var mergedMessage json.RawMessage
|
||||||
for _, options := range optionsList {
|
for _, options := range optionsList {
|
||||||
mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage)
|
mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return option.Options{}, E.Cause(err, "merge config at ", options.path)
|
return option.Options{}, E.Cause(err, "merge config at ", options.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var mergedOptions option.Options
|
var mergedOptions option.Options
|
||||||
err = mergedOptions.UnmarshalJSON(mergedMessage)
|
err = mergedOptions.UnmarshalJSONContext(globalCtx, mergedMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return option.Options{}, E.Cause(err, "unmarshal merged config")
|
return option.Options{}, E.Cause(err, "unmarshal merged config")
|
||||||
}
|
}
|
||||||
@ -188,9 +188,12 @@ func run() error {
|
|||||||
cancel()
|
cancel()
|
||||||
closeCtx, closed := context.WithCancel(context.Background())
|
closeCtx, closed := context.WithCancel(context.Background())
|
||||||
go closeMonitor(closeCtx)
|
go closeMonitor(closeCtx)
|
||||||
instance.Close()
|
err = instance.Close()
|
||||||
closed()
|
closed()
|
||||||
if osSignal != syscall.SIGHUP {
|
if osSignal != syscall.SIGHUP {
|
||||||
|
if err != nil {
|
||||||
|
log.Error(E.Cause(err, "sing-box did not closed properly"))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@ -199,7 +202,7 @@ func run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func closeMonitor(ctx context.Context) {
|
func closeMonitor(ctx context.Context) {
|
||||||
time.Sleep(C.DefaultStopFatalTimeout)
|
time.Sleep(C.FatalStopTimeout)
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box"
|
"github.com/sagernet/sing-box"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
@ -23,9 +26,11 @@ func init() {
|
|||||||
func createPreStartedClient() (*box.Box, error) {
|
func createPreStartedClient() (*box.Box, error) {
|
||||||
options, err := readConfigAndMerge()
|
options, err := readConfigAndMerge()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
if !(errors.Is(err, os.ErrNotExist) && len(configDirectories) == 0 && len(configPaths) == 1) || configPaths[0] != "config.json" {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
instance, err := box.New(box.Options{Options: options})
|
instance, err := box.New(box.Options{Context: globalCtx, Options: options})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "create service")
|
return nil, E.Cause(err, "create service")
|
||||||
}
|
}
|
||||||
@ -36,11 +41,11 @@ func createPreStartedClient() (*box.Box, error) {
|
|||||||
return instance, nil
|
return instance, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) {
|
func createDialer(instance *box.Box, outboundTag string) (N.Dialer, error) {
|
||||||
if outboundTag == "" {
|
if outboundTag == "" {
|
||||||
return instance.Router().DefaultOutbound(N.NetworkName(network))
|
return instance.Outbound().Default(), nil
|
||||||
} else {
|
} else {
|
||||||
outbound, loaded := instance.Router().Outbound(outboundTag)
|
outbound, loaded := instance.Outbound().Outbound(outboundTag)
|
||||||
if !loaded {
|
if !loaded {
|
||||||
return nil, E.New("outbound not found: ", outboundTag)
|
return nil, E.New("outbound not found: ", outboundTag)
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ func connect(address string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer instance.Close()
|
defer instance.Close()
|
||||||
dialer, err := createDialer(instance, commandConnectFlagNetwork, commandToolsFlagOutbound)
|
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,10 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -32,7 +34,10 @@ func init() {
|
|||||||
commandTools.AddCommand(commandFetch)
|
commandTools.AddCommand(commandFetch)
|
||||||
}
|
}
|
||||||
|
|
||||||
var httpClient *http.Client
|
var (
|
||||||
|
httpClient *http.Client
|
||||||
|
http3Client *http.Client
|
||||||
|
)
|
||||||
|
|
||||||
func fetch(args []string) error {
|
func fetch(args []string) error {
|
||||||
instance, err := createPreStartedClient()
|
instance, err := createPreStartedClient()
|
||||||
@ -43,7 +48,7 @@ func fetch(args []string) error {
|
|||||||
httpClient = &http.Client{
|
httpClient = &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
dialer, err := createDialer(instance, network, commandToolsFlagOutbound)
|
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -53,8 +58,16 @@ func fetch(args []string) error {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
defer httpClient.CloseIdleConnections()
|
defer httpClient.CloseIdleConnections()
|
||||||
|
if C.WithQUIC {
|
||||||
|
err = initializeHTTP3Client(instance)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer http3Client.CloseIdleConnections()
|
||||||
|
}
|
||||||
for _, urlString := range args {
|
for _, urlString := range args {
|
||||||
parsedURL, err := url.Parse(urlString)
|
var parsedURL *url.URL
|
||||||
|
parsedURL, err = url.Parse(urlString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -63,16 +76,27 @@ func fetch(args []string) error {
|
|||||||
parsedURL.Scheme = "http"
|
parsedURL.Scheme = "http"
|
||||||
fallthrough
|
fallthrough
|
||||||
case "http", "https":
|
case "http", "https":
|
||||||
err = fetchHTTP(parsedURL)
|
err = fetchHTTP(httpClient, parsedURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case "http3":
|
||||||
|
if !C.WithQUIC {
|
||||||
|
return C.ErrQUICNotIncluded
|
||||||
|
}
|
||||||
|
parsedURL.Scheme = "https"
|
||||||
|
err = fetchHTTP(http3Client, parsedURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return E.New("unsupported scheme: ", parsedURL.Scheme)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchHTTP(parsedURL *url.URL) error {
|
func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error {
|
||||||
request, err := http.NewRequest("GET", parsedURL.String(), nil)
|
request, err := http.NewRequest("GET", parsedURL.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
36
cmd/sing-box/cmd_tools_fetch_http3.go
Normal file
36
cmd/sing-box/cmd_tools_fetch_http3.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//go:build with_quic
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/sagernet/quic-go"
|
||||||
|
"github.com/sagernet/quic-go/http3"
|
||||||
|
box "github.com/sagernet/sing-box"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initializeHTTP3Client(instance *box.Box) error {
|
||||||
|
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
http3Client = &http.Client{
|
||||||
|
Transport: &http3.Transport{
|
||||||
|
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||||
|
destination := M.ParseSocksaddr(addr)
|
||||||
|
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
|
||||||
|
if dErr != nil {
|
||||||
|
return nil, dErr
|
||||||
|
}
|
||||||
|
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
18
cmd/sing-box/cmd_tools_fetch_http3_stub.go
Normal file
18
cmd/sing-box/cmd_tools_fetch_http3_stub.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//go:build !with_quic
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
box "github.com/sagernet/sing-box"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initializeHTTP3Client(instance *box.Box) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchHTTP3(parsedURL *url.URL) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
@ -4,12 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/settings"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/sagernet/sing/common/ntp"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -45,7 +43,7 @@ func syncTime() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound)
|
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -59,7 +57,7 @@ func syncTime() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if commandSyncTimeWrite {
|
if commandSyncTimeWrite {
|
||||||
err = settings.SetSystemTime(response.Time)
|
err = ntp.SetSystemTime(response.Time)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "write time to system")
|
return E.Cause(err, "write time to system")
|
||||||
}
|
}
|
||||||
|
28
cmd/sing-box/generate_completions.go
Normal file
28
cmd/sing-box/generate_completions.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
//go:build generate && generate_completions
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/sagernet/sing-box/log"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := generateCompletions()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateCompletions() error {
|
||||||
|
err := mainCommand.GenBashCompletionFile("release/completions/sing-box.bash")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = mainCommand.GenFishCompletionFile("release/completions/sing-box.fish", true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = mainCommand.GenZshCompletionFile("release/completions/sing-box.zsh")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
346
cmd/sing-box/internal/convertor/adguard/convertor.go
Normal file
346
cmd/sing-box/internal/convertor/adguard/convertor.go
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
package adguard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type agdguardRuleLine struct {
|
||||||
|
ruleLine string
|
||||||
|
isRawDomain bool
|
||||||
|
isExclude bool
|
||||||
|
isSuffix bool
|
||||||
|
hasStart bool
|
||||||
|
hasEnd bool
|
||||||
|
isRegexp bool
|
||||||
|
isImportant bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func Convert(reader io.Reader) ([]option.HeadlessRule, error) {
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
var (
|
||||||
|
ruleLines []agdguardRuleLine
|
||||||
|
ignoredLines int
|
||||||
|
)
|
||||||
|
parseLine:
|
||||||
|
for scanner.Scan() {
|
||||||
|
ruleLine := scanner.Text()
|
||||||
|
if ruleLine == "" || ruleLine[0] == '!' || ruleLine[0] == '#' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
originRuleLine := ruleLine
|
||||||
|
if M.IsDomainName(ruleLine) {
|
||||||
|
ruleLines = append(ruleLines, agdguardRuleLine{
|
||||||
|
ruleLine: ruleLine,
|
||||||
|
isRawDomain: true,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hostLine, err := parseAdGuardHostLine(ruleLine)
|
||||||
|
if err == nil {
|
||||||
|
if hostLine != "" {
|
||||||
|
ruleLines = append(ruleLines, agdguardRuleLine{
|
||||||
|
ruleLine: hostLine,
|
||||||
|
isRawDomain: true,
|
||||||
|
hasStart: true,
|
||||||
|
hasEnd: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(ruleLine, "|") {
|
||||||
|
ruleLine = ruleLine[:len(ruleLine)-1]
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
isExclude bool
|
||||||
|
isSuffix bool
|
||||||
|
hasStart bool
|
||||||
|
hasEnd bool
|
||||||
|
isRegexp bool
|
||||||
|
isImportant bool
|
||||||
|
)
|
||||||
|
if !strings.HasPrefix(ruleLine, "/") && strings.Contains(ruleLine, "$") {
|
||||||
|
params := common.SubstringAfter(ruleLine, "$")
|
||||||
|
for _, param := range strings.Split(params, ",") {
|
||||||
|
paramParts := strings.Split(param, "=")
|
||||||
|
var ignored bool
|
||||||
|
if len(paramParts) > 0 && len(paramParts) <= 2 {
|
||||||
|
switch paramParts[0] {
|
||||||
|
case "app", "network":
|
||||||
|
// maybe support by package_name/process_name
|
||||||
|
case "dnstype":
|
||||||
|
// maybe support by query_type
|
||||||
|
case "important":
|
||||||
|
ignored = true
|
||||||
|
isImportant = true
|
||||||
|
case "dnsrewrite":
|
||||||
|
if len(paramParts) == 2 && M.ParseAddr(paramParts[1]).IsUnspecified() {
|
||||||
|
ignored = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ignored {
|
||||||
|
ignoredLines++
|
||||||
|
log.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", ruleLine)
|
||||||
|
continue parseLine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ruleLine = common.SubstringBefore(ruleLine, "$")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(ruleLine, "@@") {
|
||||||
|
ruleLine = ruleLine[2:]
|
||||||
|
isExclude = true
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(ruleLine, "|") {
|
||||||
|
ruleLine = ruleLine[:len(ruleLine)-1]
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(ruleLine, "||") {
|
||||||
|
ruleLine = ruleLine[2:]
|
||||||
|
isSuffix = true
|
||||||
|
} else if strings.HasPrefix(ruleLine, "|") {
|
||||||
|
ruleLine = ruleLine[1:]
|
||||||
|
hasStart = true
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(ruleLine, "^") {
|
||||||
|
ruleLine = ruleLine[:len(ruleLine)-1]
|
||||||
|
hasEnd = true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(ruleLine, "/") && strings.HasSuffix(ruleLine, "/") {
|
||||||
|
ruleLine = ruleLine[1 : len(ruleLine)-1]
|
||||||
|
if ignoreIPCIDRRegexp(ruleLine) {
|
||||||
|
ignoredLines++
|
||||||
|
log.Debug("ignored unsupported rule with IPCIDR regexp: ", ruleLine)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
isRegexp = true
|
||||||
|
} else {
|
||||||
|
if strings.Contains(ruleLine, "://") {
|
||||||
|
ruleLine = common.SubstringAfter(ruleLine, "://")
|
||||||
|
}
|
||||||
|
if strings.Contains(ruleLine, "/") {
|
||||||
|
ignoredLines++
|
||||||
|
log.Debug("ignored unsupported rule with path: ", ruleLine)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(ruleLine, "##") {
|
||||||
|
ignoredLines++
|
||||||
|
log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(ruleLine, "#$#") {
|
||||||
|
ignoredLines++
|
||||||
|
log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var domainCheck string
|
||||||
|
if strings.HasPrefix(ruleLine, ".") || strings.HasPrefix(ruleLine, "-") {
|
||||||
|
domainCheck = "r" + ruleLine
|
||||||
|
} else {
|
||||||
|
domainCheck = ruleLine
|
||||||
|
}
|
||||||
|
if ruleLine == "" {
|
||||||
|
ignoredLines++
|
||||||
|
log.Debug("ignored unsupported rule with empty domain", originRuleLine)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
domainCheck = strings.ReplaceAll(domainCheck, "*", "x")
|
||||||
|
if !M.IsDomainName(domainCheck) {
|
||||||
|
_, ipErr := parseADGuardIPCIDRLine(ruleLine)
|
||||||
|
if ipErr == nil {
|
||||||
|
ignoredLines++
|
||||||
|
log.Debug("ignored unsupported rule with IPCIDR: ", ruleLine)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if M.ParseSocksaddr(domainCheck).Port != 0 {
|
||||||
|
log.Debug("ignored unsupported rule with port: ", ruleLine)
|
||||||
|
} else {
|
||||||
|
log.Debug("ignored unsupported rule with invalid domain: ", ruleLine)
|
||||||
|
}
|
||||||
|
ignoredLines++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ruleLines = append(ruleLines, agdguardRuleLine{
|
||||||
|
ruleLine: ruleLine,
|
||||||
|
isExclude: isExclude,
|
||||||
|
isSuffix: isSuffix,
|
||||||
|
hasStart: hasStart,
|
||||||
|
hasEnd: hasEnd,
|
||||||
|
isRegexp: isRegexp,
|
||||||
|
isImportant: isImportant,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(ruleLines) == 0 {
|
||||||
|
return nil, E.New("AdGuard rule-set is empty or all rules are unsupported")
|
||||||
|
}
|
||||||
|
if common.All(ruleLines, func(it agdguardRuleLine) bool {
|
||||||
|
return it.isRawDomain
|
||||||
|
}) {
|
||||||
|
return []option.HeadlessRule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultHeadlessRule{
|
||||||
|
Domain: common.Map(ruleLines, func(it agdguardRuleLine) string {
|
||||||
|
return it.ruleLine
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
mapDomain := func(it agdguardRuleLine) string {
|
||||||
|
ruleLine := it.ruleLine
|
||||||
|
if it.isSuffix {
|
||||||
|
ruleLine = "||" + ruleLine
|
||||||
|
} else if it.hasStart {
|
||||||
|
ruleLine = "|" + ruleLine
|
||||||
|
}
|
||||||
|
if it.hasEnd {
|
||||||
|
ruleLine += "^"
|
||||||
|
}
|
||||||
|
return ruleLine
|
||||||
|
}
|
||||||
|
|
||||||
|
importantDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain)
|
||||||
|
importantDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && !it.isExclude }), mapDomain)
|
||||||
|
importantExcludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && it.isExclude }), mapDomain)
|
||||||
|
importantExcludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && it.isExclude }), mapDomain)
|
||||||
|
domain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain)
|
||||||
|
domainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && !it.isExclude }), mapDomain)
|
||||||
|
excludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && it.isExclude }), mapDomain)
|
||||||
|
excludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && it.isExclude }), mapDomain)
|
||||||
|
currentRule := option.HeadlessRule{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultHeadlessRule{
|
||||||
|
AdGuardDomain: domain,
|
||||||
|
DomainRegex: domainRegex,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if len(excludeDomain) > 0 || len(excludeDomainRegex) > 0 {
|
||||||
|
currentRule = option.HeadlessRule{
|
||||||
|
Type: C.RuleTypeLogical,
|
||||||
|
LogicalOptions: option.LogicalHeadlessRule{
|
||||||
|
Mode: C.LogicalTypeAnd,
|
||||||
|
Rules: []option.HeadlessRule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultHeadlessRule{
|
||||||
|
AdGuardDomain: excludeDomain,
|
||||||
|
DomainRegex: excludeDomainRegex,
|
||||||
|
Invert: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentRule,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(importantDomain) > 0 || len(importantDomainRegex) > 0 {
|
||||||
|
currentRule = option.HeadlessRule{
|
||||||
|
Type: C.RuleTypeLogical,
|
||||||
|
LogicalOptions: option.LogicalHeadlessRule{
|
||||||
|
Mode: C.LogicalTypeOr,
|
||||||
|
Rules: []option.HeadlessRule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultHeadlessRule{
|
||||||
|
AdGuardDomain: importantDomain,
|
||||||
|
DomainRegex: importantDomainRegex,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentRule,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(importantExcludeDomain) > 0 || len(importantExcludeDomainRegex) > 0 {
|
||||||
|
currentRule = option.HeadlessRule{
|
||||||
|
Type: C.RuleTypeLogical,
|
||||||
|
LogicalOptions: option.LogicalHeadlessRule{
|
||||||
|
Mode: C.LogicalTypeAnd,
|
||||||
|
Rules: []option.HeadlessRule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultHeadlessRule{
|
||||||
|
AdGuardDomain: importantExcludeDomain,
|
||||||
|
DomainRegex: importantExcludeDomainRegex,
|
||||||
|
Invert: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentRule,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Info("parsed rules: ", len(ruleLines), "/", len(ruleLines)+ignoredLines)
|
||||||
|
return []option.HeadlessRule{currentRule}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ignoreIPCIDRRegexp(ruleLine string) bool {
|
||||||
|
if strings.HasPrefix(ruleLine, "(http?:\\/\\/)") {
|
||||||
|
ruleLine = ruleLine[12:]
|
||||||
|
} else if strings.HasPrefix(ruleLine, "(https?:\\/\\/)") {
|
||||||
|
ruleLine = ruleLine[13:]
|
||||||
|
} else if strings.HasPrefix(ruleLine, "^") {
|
||||||
|
ruleLine = ruleLine[1:]
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, parseErr := strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8)
|
||||||
|
return parseErr == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAdGuardHostLine(ruleLine string) (string, error) {
|
||||||
|
idx := strings.Index(ruleLine, " ")
|
||||||
|
if idx == -1 {
|
||||||
|
return "", os.ErrInvalid
|
||||||
|
}
|
||||||
|
address, err := netip.ParseAddr(ruleLine[:idx])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !address.IsUnspecified() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
domain := ruleLine[idx+1:]
|
||||||
|
if !M.IsDomainName(domain) {
|
||||||
|
return "", E.New("invalid domain name: ", domain)
|
||||||
|
}
|
||||||
|
return domain, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseADGuardIPCIDRLine(ruleLine string) (netip.Prefix, error) {
|
||||||
|
var isPrefix bool
|
||||||
|
if strings.HasSuffix(ruleLine, ".") {
|
||||||
|
isPrefix = true
|
||||||
|
ruleLine = ruleLine[:len(ruleLine)-1]
|
||||||
|
}
|
||||||
|
ruleStringParts := strings.Split(ruleLine, ".")
|
||||||
|
if len(ruleStringParts) > 4 || len(ruleStringParts) < 4 && !isPrefix {
|
||||||
|
return netip.Prefix{}, os.ErrInvalid
|
||||||
|
}
|
||||||
|
ruleParts := make([]uint8, 0, len(ruleStringParts))
|
||||||
|
for _, part := range ruleStringParts {
|
||||||
|
rulePart, err := strconv.ParseUint(part, 10, 8)
|
||||||
|
if err != nil {
|
||||||
|
return netip.Prefix{}, err
|
||||||
|
}
|
||||||
|
ruleParts = append(ruleParts, uint8(rulePart))
|
||||||
|
}
|
||||||
|
bitLen := len(ruleParts) * 8
|
||||||
|
for len(ruleParts) < 4 {
|
||||||
|
ruleParts = append(ruleParts, 0)
|
||||||
|
}
|
||||||
|
return netip.PrefixFrom(netip.AddrFrom4(*(*[4]byte)(ruleParts)), bitLen), nil
|
||||||
|
}
|
141
cmd/sing-box/internal/convertor/adguard/convertor_test.go
Normal file
141
cmd/sing-box/internal/convertor/adguard/convertor_test.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package adguard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/route/rule"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConverter(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
rules, err := Convert(strings.NewReader(`
|
||||||
|
||example.org^
|
||||||
|
|example.com^
|
||||||
|
example.net^
|
||||||
|
||example.edu
|
||||||
|
||example.edu.tw^
|
||||||
|
|example.gov
|
||||||
|
example.arpa
|
||||||
|
@@|sagernet.example.org|
|
||||||
|
||sagernet.org^$important
|
||||||
|
@@|sing-box.sagernet.org^$important
|
||||||
|
`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, rules, 1)
|
||||||
|
rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
matchDomain := []string{
|
||||||
|
"example.org",
|
||||||
|
"www.example.org",
|
||||||
|
"example.com",
|
||||||
|
"example.net",
|
||||||
|
"isexample.net",
|
||||||
|
"www.example.net",
|
||||||
|
"example.edu",
|
||||||
|
"example.edu.cn",
|
||||||
|
"example.edu.tw",
|
||||||
|
"www.example.edu",
|
||||||
|
"www.example.edu.cn",
|
||||||
|
"example.gov",
|
||||||
|
"example.gov.cn",
|
||||||
|
"example.arpa",
|
||||||
|
"www.example.arpa",
|
||||||
|
"isexample.arpa",
|
||||||
|
"example.arpa.cn",
|
||||||
|
"www.example.arpa.cn",
|
||||||
|
"isexample.arpa.cn",
|
||||||
|
"sagernet.org",
|
||||||
|
"www.sagernet.org",
|
||||||
|
}
|
||||||
|
notMatchDomain := []string{
|
||||||
|
"example.org.cn",
|
||||||
|
"notexample.org",
|
||||||
|
"example.com.cn",
|
||||||
|
"www.example.com.cn",
|
||||||
|
"example.net.cn",
|
||||||
|
"notexample.edu",
|
||||||
|
"notexample.edu.cn",
|
||||||
|
"www.example.gov",
|
||||||
|
"notexample.gov",
|
||||||
|
"sagernet.example.org",
|
||||||
|
"sing-box.sagernet.org",
|
||||||
|
}
|
||||||
|
for _, domain := range matchDomain {
|
||||||
|
require.True(t, rule.Match(&adapter.InboundContext{
|
||||||
|
Domain: domain,
|
||||||
|
}), domain)
|
||||||
|
}
|
||||||
|
for _, domain := range notMatchDomain {
|
||||||
|
require.False(t, rule.Match(&adapter.InboundContext{
|
||||||
|
Domain: domain,
|
||||||
|
}), domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHosts(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
rules, err := Convert(strings.NewReader(`
|
||||||
|
127.0.0.1 localhost
|
||||||
|
::1 localhost #[IPv6]
|
||||||
|
0.0.0.0 google.com
|
||||||
|
`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, rules, 1)
|
||||||
|
rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
matchDomain := []string{
|
||||||
|
"google.com",
|
||||||
|
}
|
||||||
|
notMatchDomain := []string{
|
||||||
|
"www.google.com",
|
||||||
|
"notgoogle.com",
|
||||||
|
"localhost",
|
||||||
|
}
|
||||||
|
for _, domain := range matchDomain {
|
||||||
|
require.True(t, rule.Match(&adapter.InboundContext{
|
||||||
|
Domain: domain,
|
||||||
|
}), domain)
|
||||||
|
}
|
||||||
|
for _, domain := range notMatchDomain {
|
||||||
|
require.False(t, rule.Match(&adapter.InboundContext{
|
||||||
|
Domain: domain,
|
||||||
|
}), domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleHosts(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
rules, err := Convert(strings.NewReader(`
|
||||||
|
example.com
|
||||||
|
www.example.org
|
||||||
|
`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, rules, 1)
|
||||||
|
rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
matchDomain := []string{
|
||||||
|
"example.com",
|
||||||
|
"www.example.org",
|
||||||
|
}
|
||||||
|
notMatchDomain := []string{
|
||||||
|
"example.com.cn",
|
||||||
|
"www.example.com",
|
||||||
|
"notexample.com",
|
||||||
|
"example.org",
|
||||||
|
}
|
||||||
|
for _, domain := range matchDomain {
|
||||||
|
require.True(t, rule.Match(&adapter.InboundContext{
|
||||||
|
Domain: domain,
|
||||||
|
}), domain)
|
||||||
|
}
|
||||||
|
for _, domain := range notMatchDomain {
|
||||||
|
require.False(t, rule.Match(&adapter.InboundContext{
|
||||||
|
Domain: domain,
|
||||||
|
}), domain)
|
||||||
|
}
|
||||||
|
}
|
@ -1,74 +1,11 @@
|
|||||||
|
//go:build !generate
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "github.com/sagernet/sing-box/log"
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"os/user"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
_ "github.com/sagernet/sing-box/include"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing/service/filemanager"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
globalCtx context.Context
|
|
||||||
configPaths []string
|
|
||||||
configDirectories []string
|
|
||||||
workingDir string
|
|
||||||
disableColor bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var mainCommand = &cobra.Command{
|
|
||||||
Use: "sing-box",
|
|
||||||
PersistentPreRun: preRun,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
mainCommand.PersistentFlags().StringArrayVarP(&configPaths, "config", "c", nil, "set configuration file path")
|
|
||||||
mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path")
|
|
||||||
mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
|
|
||||||
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := mainCommand.Execute(); err != nil {
|
if err := mainCommand.Execute(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func preRun(cmd *cobra.Command, args []string) {
|
|
||||||
globalCtx = context.Background()
|
|
||||||
sudoUser := os.Getenv("SUDO_USER")
|
|
||||||
sudoUID, _ := strconv.Atoi(os.Getenv("SUDO_UID"))
|
|
||||||
sudoGID, _ := strconv.Atoi(os.Getenv("SUDO_GID"))
|
|
||||||
if sudoUID == 0 && sudoGID == 0 && sudoUser != "" {
|
|
||||||
sudoUserObject, _ := user.Lookup(sudoUser)
|
|
||||||
if sudoUserObject != nil {
|
|
||||||
sudoUID, _ = strconv.Atoi(sudoUserObject.Uid)
|
|
||||||
sudoGID, _ = strconv.Atoi(sudoUserObject.Gid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sudoUID > 0 && sudoGID > 0 {
|
|
||||||
globalCtx = filemanager.WithDefault(globalCtx, "", "", sudoUID, sudoGID)
|
|
||||||
}
|
|
||||||
if disableColor {
|
|
||||||
log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger())
|
|
||||||
}
|
|
||||||
if workingDir != "" {
|
|
||||||
_, err := os.Stat(workingDir)
|
|
||||||
if err != nil {
|
|
||||||
filemanager.MkdirAll(globalCtx, workingDir, 0o777)
|
|
||||||
}
|
|
||||||
err = os.Chdir(workingDir)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(configPaths) == 0 && len(configDirectories) == 0 {
|
|
||||||
configPaths = append(configPaths, "config.json")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
//go:build go1.21 && !without_badtls && with_ech
|
|
||||||
|
|
||||||
package badtls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
_ "unsafe"
|
|
||||||
|
|
||||||
"github.com/sagernet/cloudflare-tls"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
|
|
||||||
tlsConn, loaded := common.Cast[*tls.Conn](conn)
|
|
||||||
if !loaded {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return true, func() error {
|
|
||||||
return echReadRecord(tlsConn)
|
|
||||||
}, func() error {
|
|
||||||
return echHandlePostHandshakeMessage(tlsConn)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:linkname echReadRecord github.com/sagernet/cloudflare-tls.(*Conn).readRecord
|
|
||||||
func echReadRecord(c *tls.Conn) error
|
|
||||||
|
|
||||||
//go:linkname echHandlePostHandshakeMessage github.com/sagernet/cloudflare-tls.(*Conn).handlePostHandshakeMessage
|
|
||||||
func echHandlePostHandshakeMessage(c *tls.Conn) error
|
|
@ -7,7 +7,8 @@ import (
|
|||||||
_ "unsafe"
|
_ "unsafe"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/utls"
|
|
||||||
|
"github.com/metacubex/utls"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -24,8 +25,8 @@ func init() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:linkname utlsReadRecord github.com/sagernet/utls.(*Conn).readRecord
|
//go:linkname utlsReadRecord github.com/metacubex/utls.(*Conn).readRecord
|
||||||
func utlsReadRecord(c *tls.Conn) error
|
func utlsReadRecord(c *tls.Conn) error
|
||||||
|
|
||||||
//go:linkname utlsHandlePostHandshakeMessage github.com/sagernet/utls.(*Conn).handlePostHandshakeMessage
|
//go:linkname utlsHandlePostHandshakeMessage github.com/metacubex/utls.(*Conn).handlePostHandshakeMessage
|
||||||
func utlsHandlePostHandshakeMessage(c *tls.Conn) error
|
func utlsHandlePostHandshakeMessage(c *tls.Conn) error
|
||||||
|
4394
common/certificate/mozilla.go
Normal file
4394
common/certificate/mozilla.go
Normal file
File diff suppressed because it is too large
Load Diff
185
common/certificate/store.go
Normal file
185
common/certificate/store.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package certificate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/fswatch"
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.CertificateStore = (*Store)(nil)
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
systemPool *x509.CertPool
|
||||||
|
currentPool *x509.CertPool
|
||||||
|
certificate string
|
||||||
|
certificatePaths []string
|
||||||
|
certificateDirectoryPaths []string
|
||||||
|
watcher *fswatch.Watcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
|
||||||
|
var systemPool *x509.CertPool
|
||||||
|
switch options.Store {
|
||||||
|
case C.CertificateStoreSystem, "":
|
||||||
|
systemPool = x509.NewCertPool()
|
||||||
|
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||||
|
var systemValid bool
|
||||||
|
if platformInterface != nil {
|
||||||
|
for _, cert := range platformInterface.SystemCertificates() {
|
||||||
|
if systemPool.AppendCertsFromPEM([]byte(cert)) {
|
||||||
|
systemValid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !systemValid {
|
||||||
|
certPool, err := x509.SystemCertPool()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
systemPool = certPool
|
||||||
|
}
|
||||||
|
case C.CertificateStoreMozilla:
|
||||||
|
systemPool = mozillaIncluded
|
||||||
|
case C.CertificateStoreNone:
|
||||||
|
systemPool = nil
|
||||||
|
default:
|
||||||
|
return nil, E.New("unknown certificate store: ", options.Store)
|
||||||
|
}
|
||||||
|
store := &Store{
|
||||||
|
systemPool: systemPool,
|
||||||
|
certificate: strings.Join(options.Certificate, "\n"),
|
||||||
|
certificatePaths: options.CertificatePath,
|
||||||
|
certificateDirectoryPaths: options.CertificateDirectoryPath,
|
||||||
|
}
|
||||||
|
var watchPaths []string
|
||||||
|
for _, target := range options.CertificatePath {
|
||||||
|
watchPaths = append(watchPaths, target)
|
||||||
|
}
|
||||||
|
for _, target := range options.CertificateDirectoryPath {
|
||||||
|
watchPaths = append(watchPaths, target)
|
||||||
|
}
|
||||||
|
if len(watchPaths) > 0 {
|
||||||
|
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
||||||
|
Path: watchPaths,
|
||||||
|
Logger: logger,
|
||||||
|
Callback: func(_ string) {
|
||||||
|
err := store.update()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(E.Cause(err, "reload certificates"))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "fswatch: create fsnotify watcher")
|
||||||
|
}
|
||||||
|
store.watcher = watcher
|
||||||
|
}
|
||||||
|
err := store.update()
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initializing certificate store")
|
||||||
|
}
|
||||||
|
return store, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Name() string {
|
||||||
|
return "certificate"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Start(stage adapter.StartStage) error {
|
||||||
|
if stage != adapter.StartStateStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if s.watcher != nil {
|
||||||
|
return s.watcher.Start()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Close() error {
|
||||||
|
if s.watcher != nil {
|
||||||
|
return s.watcher.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Pool() *x509.CertPool {
|
||||||
|
return s.currentPool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) update() error {
|
||||||
|
var currentPool *x509.CertPool
|
||||||
|
if s.systemPool == nil {
|
||||||
|
currentPool = x509.NewCertPool()
|
||||||
|
} else {
|
||||||
|
currentPool = s.systemPool.Clone()
|
||||||
|
}
|
||||||
|
if s.certificate != "" {
|
||||||
|
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
||||||
|
return E.New("invalid certificate PEM strings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, path := range s.certificatePaths {
|
||||||
|
pemContent, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !currentPool.AppendCertsFromPEM(pemContent) {
|
||||||
|
return E.New("invalid certificate PEM file: ", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var firstErr error
|
||||||
|
for _, directoryPath := range s.certificateDirectoryPaths {
|
||||||
|
directoryEntries, err := readUniqueDirectoryEntries(directoryPath)
|
||||||
|
if err != nil {
|
||||||
|
if firstErr == nil && !os.IsNotExist(err) {
|
||||||
|
firstErr = E.Cause(err, "invalid certificate directory: ", directoryPath)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, directoryEntry := range directoryEntries {
|
||||||
|
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
|
||||||
|
if err == nil {
|
||||||
|
currentPool.AppendCertsFromPEM(pemContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if firstErr != nil {
|
||||||
|
return firstErr
|
||||||
|
}
|
||||||
|
s.currentPool = currentPool
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readUniqueDirectoryEntries(dir string) ([]fs.DirEntry, error) {
|
||||||
|
files, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
uniq := files[:0]
|
||||||
|
for _, f := range files {
|
||||||
|
if !isSameDirSymlink(f, dir) {
|
||||||
|
uniq = append(uniq, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSameDirSymlink(f fs.DirEntry, dir string) bool {
|
||||||
|
if f.Type()&fs.ModeSymlink == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
target, err := os.Readlink(filepath.Join(dir, f.Name()))
|
||||||
|
return err == nil && !strings.Contains(target, "/")
|
||||||
|
}
|
@ -2,54 +2,130 @@ package dialer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/conntrack"
|
"github.com/sagernet/sing-box/common/conntrack"
|
||||||
|
"github.com/sagernet/sing-box/common/listener"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/atomic"
|
||||||
"github.com/sagernet/sing/common/control"
|
"github.com/sagernet/sing/common/control"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ WireGuardListener = (*DefaultDialer)(nil)
|
var (
|
||||||
|
_ ParallelInterfaceDialer = (*DefaultDialer)(nil)
|
||||||
|
_ WireGuardListener = (*DefaultDialer)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
type DefaultDialer struct {
|
type DefaultDialer struct {
|
||||||
dialer4 tcpDialer
|
dialer4 tcpDialer
|
||||||
dialer6 tcpDialer
|
dialer6 tcpDialer
|
||||||
udpDialer4 net.Dialer
|
udpDialer4 net.Dialer
|
||||||
udpDialer6 net.Dialer
|
udpDialer6 net.Dialer
|
||||||
udpListener net.ListenConfig
|
udpListener net.ListenConfig
|
||||||
udpAddr4 string
|
udpAddr4 string
|
||||||
udpAddr6 string
|
udpAddr6 string
|
||||||
isWireGuardListener bool
|
netns string
|
||||||
|
networkManager adapter.NetworkManager
|
||||||
|
networkStrategy *C.NetworkStrategy
|
||||||
|
defaultNetworkStrategy bool
|
||||||
|
networkType []C.InterfaceType
|
||||||
|
fallbackNetworkType []C.InterfaceType
|
||||||
|
networkFallbackDelay time.Duration
|
||||||
|
networkLastFallback atomic.TypedValue[time.Time]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDialer, error) {
|
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
|
||||||
var dialer net.Dialer
|
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
||||||
var listener net.ListenConfig
|
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dialer net.Dialer
|
||||||
|
listener net.ListenConfig
|
||||||
|
interfaceFinder control.InterfaceFinder
|
||||||
|
networkStrategy *C.NetworkStrategy
|
||||||
|
defaultNetworkStrategy bool
|
||||||
|
networkType []C.InterfaceType
|
||||||
|
fallbackNetworkType []C.InterfaceType
|
||||||
|
networkFallbackDelay time.Duration
|
||||||
|
)
|
||||||
|
if networkManager != nil {
|
||||||
|
interfaceFinder = networkManager.InterfaceFinder()
|
||||||
|
} else {
|
||||||
|
interfaceFinder = control.NewDefaultInterfaceFinder()
|
||||||
|
}
|
||||||
if options.BindInterface != "" {
|
if options.BindInterface != "" {
|
||||||
bindFunc := control.BindToInterface(router.InterfaceFinder(), options.BindInterface, -1)
|
if !(C.IsLinux || C.IsDarwin || C.IsWindows) {
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
return nil, E.New("`bind_interface` is only supported on Linux, macOS and Windows")
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
}
|
||||||
} else if router.AutoDetectInterface() {
|
bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1)
|
||||||
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.InterfaceFinder(), router.DefaultInterface(), -1)
|
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
}
|
}
|
||||||
if options.RoutingMark != 0 {
|
if options.RoutingMark > 0 {
|
||||||
dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))
|
if !C.IsLinux {
|
||||||
listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark))
|
return nil, E.New("`routing_mark` is only supported on Linux")
|
||||||
} else if router.DefaultMark() != 0 {
|
}
|
||||||
dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark()))
|
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
|
||||||
listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark()))
|
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
|
||||||
|
}
|
||||||
|
disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil
|
||||||
|
if disableDefaultBind || options.TCPFastOpen {
|
||||||
|
if options.NetworkStrategy != nil || len(options.NetworkType) > 0 && options.FallbackNetworkType == nil && options.FallbackDelay == 0 {
|
||||||
|
return nil, E.New("`network_strategy` is conflict with `bind_interface`, `inet4_bind_address`, `inet6_bind_address` and `tcp_fast_open`")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if networkManager != nil {
|
||||||
|
defaultOptions := networkManager.DefaultOptions()
|
||||||
|
if !disableDefaultBind {
|
||||||
|
if defaultOptions.BindInterface != "" {
|
||||||
|
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
||||||
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
|
} else if networkManager.AutoDetectInterface() {
|
||||||
|
if platformInterface != nil {
|
||||||
|
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
|
||||||
|
networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
|
||||||
|
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
|
||||||
|
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 {
|
||||||
|
networkStrategy = defaultOptions.NetworkStrategy
|
||||||
|
networkType = defaultOptions.NetworkType
|
||||||
|
fallbackNetworkType = defaultOptions.FallbackNetworkType
|
||||||
|
}
|
||||||
|
networkFallbackDelay = time.Duration(options.FallbackDelay)
|
||||||
|
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
|
||||||
|
networkFallbackDelay = defaultOptions.FallbackDelay
|
||||||
|
}
|
||||||
|
if networkStrategy == nil {
|
||||||
|
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
|
||||||
|
defaultNetworkStrategy = true
|
||||||
|
}
|
||||||
|
bindFunc := networkManager.ProtectFunc()
|
||||||
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
|
} else {
|
||||||
|
bindFunc := networkManager.AutoDetectInterfaceFunc()
|
||||||
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
|
||||||
|
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
|
||||||
|
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if options.ReuseAddr {
|
if options.ReuseAddr {
|
||||||
listener.Control = control.Append(listener.Control, control.ReuseAddr())
|
listener.Control = control.Append(listener.Control, control.ReuseAddr())
|
||||||
@ -61,8 +137,11 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
|||||||
if options.ConnectTimeout != 0 {
|
if options.ConnectTimeout != 0 {
|
||||||
dialer.Timeout = time.Duration(options.ConnectTimeout)
|
dialer.Timeout = time.Duration(options.ConnectTimeout)
|
||||||
} else {
|
} else {
|
||||||
dialer.Timeout = C.TCPTimeout
|
dialer.Timeout = C.TCPConnectTimeout
|
||||||
}
|
}
|
||||||
|
// TODO: Add an option to customize the keep alive period
|
||||||
|
dialer.KeepAlive = C.TCPKeepAliveInitial
|
||||||
|
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(C.TCPKeepAliveInitial, C.TCPKeepAliveInterval))
|
||||||
var udpFragment bool
|
var udpFragment bool
|
||||||
if options.UDPFragment != nil {
|
if options.UDPFragment != nil {
|
||||||
udpFragment = *options.UDPFragment
|
udpFragment = *options.UDPFragment
|
||||||
@ -79,7 +158,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
|||||||
udpAddr4 string
|
udpAddr4 string
|
||||||
)
|
)
|
||||||
if options.Inet4BindAddress != nil {
|
if options.Inet4BindAddress != nil {
|
||||||
bindAddr := options.Inet4BindAddress.Build()
|
bindAddr := options.Inet4BindAddress.Build(netip.IPv4Unspecified())
|
||||||
dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
|
dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
|
||||||
udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
|
udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
|
||||||
udpAddr4 = M.SocksaddrFrom(bindAddr, 0).String()
|
udpAddr4 = M.SocksaddrFrom(bindAddr, 0).String()
|
||||||
@ -90,7 +169,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
|||||||
udpAddr6 string
|
udpAddr6 string
|
||||||
)
|
)
|
||||||
if options.Inet6BindAddress != nil {
|
if options.Inet6BindAddress != nil {
|
||||||
bindAddr := options.Inet6BindAddress.Build()
|
bindAddr := options.Inet6BindAddress.Build(netip.IPv6Unspecified())
|
||||||
dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
|
dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
|
||||||
udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
|
udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
|
||||||
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
|
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
|
||||||
@ -101,11 +180,6 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
|||||||
}
|
}
|
||||||
setMultiPathTCP(&dialer4)
|
setMultiPathTCP(&dialer4)
|
||||||
}
|
}
|
||||||
if options.IsWireGuardListener {
|
|
||||||
for _, controlFn := range wgControlFns {
|
|
||||||
listener.Control = control.Append(listener.Control, controlFn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
|
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -115,48 +189,175 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &DefaultDialer{
|
return &DefaultDialer{
|
||||||
tcpDialer4,
|
dialer4: tcpDialer4,
|
||||||
tcpDialer6,
|
dialer6: tcpDialer6,
|
||||||
udpDialer4,
|
udpDialer4: udpDialer4,
|
||||||
udpDialer6,
|
udpDialer6: udpDialer6,
|
||||||
listener,
|
udpListener: listener,
|
||||||
udpAddr4,
|
udpAddr4: udpAddr4,
|
||||||
udpAddr6,
|
udpAddr6: udpAddr6,
|
||||||
options.IsWireGuardListener,
|
netns: options.NetNs,
|
||||||
|
networkManager: networkManager,
|
||||||
|
networkStrategy: networkStrategy,
|
||||||
|
defaultNetworkStrategy: defaultNetworkStrategy,
|
||||||
|
networkType: networkType,
|
||||||
|
fallbackNetworkType: fallbackNetworkType,
|
||||||
|
networkFallbackDelay: networkFallbackDelay,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setMarkWrapper(networkManager adapter.NetworkManager, mark uint32, isDefault bool) control.Func {
|
||||||
|
if networkManager == nil {
|
||||||
|
return control.RoutingMark(mark)
|
||||||
|
}
|
||||||
|
return func(network, address string, conn syscall.RawConn) error {
|
||||||
|
if networkManager.AutoRedirectOutputMark() != 0 {
|
||||||
|
if isDefault {
|
||||||
|
return E.New("`route.default_mark` is conflict with `tun.auto_redirect`")
|
||||||
|
} else {
|
||||||
|
return E.New("`routing_mark` is conflict with `tun.auto_redirect`")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return control.RoutingMark(mark)(network, address, conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
|
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
|
||||||
if !address.IsValid() {
|
if !address.IsValid() {
|
||||||
return nil, E.New("invalid address")
|
return nil, E.New("invalid address")
|
||||||
|
} else if address.IsFqdn() {
|
||||||
|
return nil, E.New("domain not resolved")
|
||||||
}
|
}
|
||||||
switch N.NetworkName(network) {
|
if d.networkStrategy == nil {
|
||||||
case N.NetworkUDP:
|
return trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) {
|
||||||
if !address.IsIPv6() {
|
switch N.NetworkName(network) {
|
||||||
return trackConn(d.udpDialer4.DialContext(ctx, network, address.String()))
|
case N.NetworkUDP:
|
||||||
|
if !address.IsIPv6() {
|
||||||
|
return d.udpDialer4.DialContext(ctx, network, address.String())
|
||||||
|
} else {
|
||||||
|
return d.udpDialer6.DialContext(ctx, network, address.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !address.IsIPv6() {
|
||||||
|
return DialSlowContext(&d.dialer4, ctx, network, address)
|
||||||
|
} else {
|
||||||
|
return DialSlowContext(&d.dialer6, ctx, network, address)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network string, address M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
||||||
|
if strategy == nil {
|
||||||
|
strategy = d.networkStrategy
|
||||||
|
}
|
||||||
|
if strategy == nil {
|
||||||
|
return d.DialContext(ctx, network, address)
|
||||||
|
}
|
||||||
|
if len(interfaceType) == 0 {
|
||||||
|
interfaceType = d.networkType
|
||||||
|
}
|
||||||
|
if len(fallbackInterfaceType) == 0 {
|
||||||
|
fallbackInterfaceType = d.fallbackNetworkType
|
||||||
|
}
|
||||||
|
if fallbackDelay == 0 {
|
||||||
|
fallbackDelay = d.networkFallbackDelay
|
||||||
|
}
|
||||||
|
var dialer net.Dialer
|
||||||
|
if N.NetworkName(network) == N.NetworkTCP {
|
||||||
|
dialer = dialerFromTCPDialer(d.dialer4)
|
||||||
|
} else {
|
||||||
|
dialer = d.udpDialer4
|
||||||
|
}
|
||||||
|
fastFallback := time.Now().Sub(d.networkLastFallback.Load()) < C.TCPTimeout
|
||||||
|
var (
|
||||||
|
conn net.Conn
|
||||||
|
isPrimary bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if !fastFallback {
|
||||||
|
conn, isPrimary, err = d.dialParallelInterface(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
|
} else {
|
||||||
|
conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay, d.networkLastFallback.Store)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// bind interface failed on legacy xiaomi systems
|
||||||
|
if d.defaultNetworkStrategy && errors.Is(err, syscall.EPERM) {
|
||||||
|
d.networkStrategy = nil
|
||||||
|
return d.DialContext(ctx, network, address)
|
||||||
} else {
|
} else {
|
||||||
return trackConn(d.udpDialer6.DialContext(ctx, network, address.String()))
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !address.IsIPv6() {
|
if !fastFallback && !isPrimary {
|
||||||
return trackConn(DialSlowContext(&d.dialer4, ctx, network, address))
|
d.networkLastFallback.Store(time.Now())
|
||||||
} else {
|
|
||||||
return trackConn(DialSlowContext(&d.dialer6, ctx, network, address))
|
|
||||||
}
|
}
|
||||||
|
return trackConn(conn, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
if destination.IsIPv6() {
|
if d.networkStrategy == nil {
|
||||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6))
|
return trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) {
|
||||||
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
if destination.IsIPv6() {
|
||||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4))
|
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)
|
||||||
|
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
||||||
|
return d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4)
|
||||||
|
} else {
|
||||||
|
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4)
|
||||||
|
}
|
||||||
|
}))
|
||||||
} else {
|
} else {
|
||||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4))
|
return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
||||||
|
if strategy == nil {
|
||||||
|
strategy = d.networkStrategy
|
||||||
|
}
|
||||||
|
if strategy == nil {
|
||||||
|
return d.ListenPacket(ctx, destination)
|
||||||
|
}
|
||||||
|
if len(interfaceType) == 0 {
|
||||||
|
interfaceType = d.networkType
|
||||||
|
}
|
||||||
|
if len(fallbackInterfaceType) == 0 {
|
||||||
|
fallbackInterfaceType = d.fallbackNetworkType
|
||||||
|
}
|
||||||
|
if fallbackDelay == 0 {
|
||||||
|
fallbackDelay = d.networkFallbackDelay
|
||||||
|
}
|
||||||
|
network := N.NetworkUDP
|
||||||
|
if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
||||||
|
network += "4"
|
||||||
|
}
|
||||||
|
packetConn, err := d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", *strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
|
if err != nil {
|
||||||
|
// bind interface failed on legacy xiaomi systems
|
||||||
|
if d.defaultNetworkStrategy && errors.Is(err, syscall.EPERM) {
|
||||||
|
d.networkStrategy = nil
|
||||||
|
return d.ListenPacket(ctx, destination)
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trackPacketConn(packetConn, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
|
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
|
||||||
return trackPacketConn(d.udpListener.ListenPacket(context.Background(), network, address))
|
udpListener := d.udpListener
|
||||||
|
udpListener.Control = control.Append(udpListener.Control, func(network, address string, conn syscall.RawConn) error {
|
||||||
|
for _, wgControlFn := range WgControlFns {
|
||||||
|
err := wgControlFn(network, address, conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return udpListener.ListenPacket(context.Background(), network, address)
|
||||||
}
|
}
|
||||||
|
|
||||||
func trackConn(conn net.Conn, err error) (net.Conn, error) {
|
func trackConn(conn net.Conn, err error) (net.Conn, error) {
|
||||||
|
@ -5,7 +5,7 @@ package dialer
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/sagernet/tfo-go"
|
"github.com/metacubex/tfo-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
type tcpDialer = tfo.Dialer
|
type tcpDialer = tfo.Dialer
|
||||||
@ -13,3 +13,7 @@ type tcpDialer = tfo.Dialer
|
|||||||
func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) {
|
func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) {
|
||||||
return tfo.Dialer{Dialer: dialer, DisableTFO: !tfoEnabled}, nil
|
return tfo.Dialer{Dialer: dialer, DisableTFO: !tfoEnabled}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dialerFromTCPDialer(dialer tcpDialer) net.Dialer {
|
||||||
|
return dialer.Dialer
|
||||||
|
}
|
||||||
|
@ -16,3 +16,7 @@ func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) {
|
|||||||
}
|
}
|
||||||
return dialer, nil
|
return dialer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dialerFromTCPDialer(dialer tcpDialer) net.Dialer {
|
||||||
|
return dialer
|
||||||
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user