Compare commits
2293 Commits
989f6244d5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faeeb9bc3c | ||
|
|
d6ab7ddfad | ||
|
|
5a06cf400b | ||
|
|
95b664a772 | ||
|
|
830944682f | ||
|
|
dc9b2320ad | ||
|
|
6db9f6941c | ||
|
|
182cd7d0d6 | ||
|
|
6bc7900b27 | ||
|
|
e94db9dfd0 | ||
|
|
f279e70ea5 | ||
|
|
948c0a713f | ||
|
|
0f03da97c6 | ||
|
|
a69d1cd4ad | ||
|
|
551935c819 | ||
|
|
0416696599 | ||
|
|
4a331b89f1 | ||
|
|
66c252d6ef | ||
|
|
d5adb54bc6 | ||
|
|
1cfcea769f | ||
|
|
f43fc797d4 | ||
|
|
8e3176b789 | ||
|
|
025b947a24 | ||
|
|
76fa3c2e5e | ||
|
|
53db1f178c | ||
|
|
55ec8abf17 | ||
|
|
5a957fd750 | ||
|
|
7c3d8cf8db | ||
|
|
813b634d08 | ||
|
|
d9b435fb62 | ||
|
|
354b4b040e | ||
|
|
7ffdc48b49 | ||
|
|
e15bdf11eb | ||
|
|
e3bcb06c3e | ||
|
|
84d2280960 | ||
|
|
4fd2532b0a | ||
|
|
02ccde6c71 | ||
|
|
e98b4ad449 | ||
|
|
d09182614c | ||
|
|
6381de7bab | ||
|
|
b0c6762bc1 | ||
|
|
7425100bac | ||
|
|
d454aa0fdf | ||
|
|
a3623eb41a | ||
|
|
72bc4c1f87 | ||
|
|
9ac1e2ff32 | ||
|
|
0045103d14 | ||
|
|
d2a933784c | ||
|
|
3f05a37f65 | ||
|
|
b8e5a71450 | ||
|
|
c13faa8e3c | ||
|
|
7623bcd19e | ||
|
|
795d1c2892 | ||
|
|
6913b11e0a | ||
|
|
1e57c06295 | ||
|
|
ea464cef8d | ||
|
|
a8e3cd3256 | ||
|
|
686cf1f304 | ||
|
|
9fbfb87723 | ||
|
|
d2fa21d07b | ||
|
|
d3768cca36 | ||
|
|
0889ddd001 | ||
|
|
f46fbf188a | ||
|
|
f2d15139f5 | ||
|
|
041646b728 | ||
|
|
b990de2e12 | ||
|
|
fe585157d2 | ||
|
|
eed6a36e5d | ||
|
|
eb0f38544c | ||
|
|
54468a1a2a | ||
|
|
8289bbd846 | ||
|
|
49c450d942 | ||
|
|
a7ee943216 | ||
|
|
8bb4c4dd32 | ||
|
|
67621ee6ba | ||
|
|
a09ffe6a0f | ||
|
|
e0be8743f6 | ||
|
|
0b04528803 | ||
|
|
65875e6dac | ||
|
|
4d6fb1d38d | ||
|
|
305b930d90 | ||
|
|
bc3884ca91 | ||
|
|
df0bf927e4 | ||
|
|
efe20ea51c | ||
|
|
e21a72fcd1 | ||
|
|
e1477bd065 | ||
|
|
aa495fce38 | ||
|
|
9cd60c28c0 | ||
|
|
2ba896c5ac | ||
|
|
1d388547ee | ||
|
|
e343cec4d5 | ||
|
|
d58efc5d01 | ||
|
|
4b26ab16fb | ||
|
|
0e27312eda | ||
|
|
4e0a953b98 | ||
|
|
27c5b0b1af | ||
|
|
84019b06d9 | ||
|
|
7fd21f8bf4 | ||
|
|
88695b0d1f | ||
|
|
fb269c9032 | ||
|
|
e62dc7bfa2 | ||
|
|
f295e195b5 | ||
|
|
ab76062a41 | ||
|
|
d14417d392 | ||
|
|
96c5c27610 | ||
|
|
91f92bee49 | ||
|
|
1803471e02 | ||
|
|
3de56d344e | ||
|
|
c71abbdfb8 | ||
|
|
ed15121e95 | ||
|
|
46c6945da5 | ||
|
|
1beb4cb002 | ||
|
|
4c65fea1ac | ||
|
|
8ae93a98e5 | ||
|
|
6da7e538e1 | ||
|
|
13e6ba4cb2 | ||
|
|
93b7328c3f | ||
|
|
11dc5bcbe1 | ||
|
|
fa3ab87b11 | ||
|
|
9bd9e9a58b | ||
|
|
9d6dee7451 | ||
|
|
9c2cdc7203 | ||
|
|
65150f5cc3 | ||
|
|
21a1512e6c | ||
|
|
cf4791f1ad | ||
|
|
0bc66e5a56 | ||
|
|
d48236da94 | ||
|
|
4c05d7b888 | ||
|
|
94ed42caf1 | ||
|
|
e0c18cc3d4 | ||
|
|
0817c25f4c | ||
|
|
7745a97cca | ||
|
|
9bcd715d31 | ||
|
|
6a95c66bc7 | ||
|
|
b5800847ae | ||
|
|
aa85cbb86e | ||
|
|
c59991420e | ||
|
|
c0304b8362 | ||
|
|
d1f1271a02 | ||
|
|
de4fdbe553 | ||
|
|
804606042f | ||
|
|
53f2db3f97 | ||
|
|
1f2fdec89d | ||
|
|
8714c157c9 | ||
|
|
657fba4ca5 | ||
|
|
0a69621207 | ||
|
|
58ccf82e0b | ||
|
|
ceab244329 | ||
|
|
58fcdceca2 | ||
|
|
98af3c0ad6 | ||
|
|
172a9d5e4e | ||
|
|
aba8346bd6 | ||
|
|
d8e269e0ac | ||
|
|
c45ea8dfac | ||
|
|
a2d313c59b | ||
|
|
15722b06dd | ||
|
|
d230dae0a5 | ||
|
|
e11dbf3a8e | ||
|
|
baa9f29f0d | ||
|
|
55b6e7dbfe | ||
|
|
a05e05a47c | ||
|
|
c1dc6cb0fb | ||
|
|
432fe1b3c9 | ||
|
|
8dd8897fd8 | ||
|
|
ff58edb1c1 | ||
|
|
79bab39502 | ||
|
|
a4d5d59901 | ||
|
|
1af14a0237 | ||
|
|
944a9986d9 | ||
|
|
60a1e4c866 | ||
|
|
5d67c131fa | ||
|
|
b9cc87d35a | ||
|
|
490d501257 | ||
|
|
725e4adc46 | ||
|
|
4a14d39cad | ||
|
|
8ec58c96f5 | ||
|
|
e8450b2e61 | ||
|
|
30c3855e4b | ||
|
|
ccf90aee8a | ||
|
|
e6c03fd448 | ||
|
|
e0f1cdf464 | ||
|
|
8d88c6532f | ||
|
|
3890bd2be7 | ||
|
|
6cd1eb9b94 | ||
|
|
f196b7a583 | ||
|
|
bd9935eebb | ||
|
|
0e0e838ff5 | ||
|
|
0caebd3171 | ||
|
|
7d2944eba9 | ||
|
|
a5db2feb5e | ||
|
|
708ceb3d29 | ||
|
|
157e33f2a4 | ||
|
|
1d4fb83313 | ||
|
|
85f5f6cebb | ||
|
|
6a750f4522 | ||
|
|
46c2cc37c3 | ||
|
|
aa8dd6e44f | ||
|
|
4e94a64dcc | ||
|
|
494990f914 | ||
|
|
95ccb837d3 | ||
|
|
24b33a43fc | ||
|
|
8ae16aa452 | ||
|
|
bf4a9edc89 | ||
|
|
78b4eac974 | ||
|
|
a34868468f | ||
|
|
e392c70b6f | ||
|
|
511d1bb3fa | ||
|
|
4273ffa77e | ||
|
|
f5ccf746ea | ||
|
|
b2d90b7d86 | ||
|
|
e0a78fde07 | ||
|
|
203f4134b0 | ||
|
|
c2b697a778 | ||
|
|
ddec2ab282 | ||
|
|
35ff7d1fb4 | ||
|
|
cba18635c8 | ||
|
|
0d8c7a9c5d | ||
|
|
faff3174a3 | ||
|
|
2fc1b672cc | ||
|
|
143983b585 | ||
|
|
4afdf4153a | ||
|
|
750dc9c3e0 | ||
|
|
48b7adde7d | ||
|
|
0585f6d065 | ||
|
|
8101a7b0bd | ||
|
|
e8620587dd | ||
|
|
a89680fa2d | ||
|
|
b919039c43 | ||
|
|
9b0960bb5a | ||
|
|
ad7b982242 | ||
|
|
7e68013b05 | ||
|
|
ac427b98f4 | ||
|
|
a5fb467db2 | ||
|
|
a930356b04 | ||
|
|
5bc0dfa9dd | ||
|
|
743b460e51 | ||
|
|
8d8ca282a1 | ||
|
|
cd56eaaba2 | ||
|
|
e92938364d | ||
|
|
1c4614318e | ||
|
|
0f5cda4169 | ||
|
|
d87c9fd242 | ||
|
|
fce21607bd | ||
|
|
3dc285be8c | ||
|
|
79bbce3db3 | ||
|
|
dfd95b2615 | ||
|
|
ab0869c972 | ||
|
|
9ac0539ffd | ||
|
|
cb4deb0c20 | ||
|
|
6b90b61358 | ||
|
|
ed1ee4c3a4 | ||
|
|
7f3ea8dbd8 | ||
|
|
12b055989b | ||
|
|
49056b5060 | ||
|
|
c530995832 | ||
|
|
60d81a73d9 | ||
|
|
e9c46cc359 | ||
|
|
9110851af3 | ||
|
|
107f92381b | ||
|
|
f84129ca79 | ||
|
|
44fafcef73 | ||
|
|
a5e09fcd43 | ||
|
|
387b42c9c2 | ||
|
|
044eb728cb | ||
|
|
2be8a45f14 | ||
|
|
1336987756 | ||
|
|
e3473d3de0 | ||
|
|
bba92146b1 | ||
|
|
48f84b31d6 | ||
|
|
1c846df903 | ||
|
|
0bd98a300f | ||
|
|
87eaf3ce6e | ||
|
|
239e6ec701 | ||
|
|
5be1887f92 | ||
|
|
65264afdf9 | ||
|
|
fecdbf20de | ||
|
|
1f03080540 | ||
|
|
737162e75a | ||
|
|
51ce402dbb | ||
|
|
8b404b5a4c | ||
|
|
3ce94d50dd | ||
|
|
29d56fca9c | ||
|
|
ab18010ee1 | ||
|
|
e69c202c79 | ||
|
|
0a812f2a46 | ||
|
|
fffe9fc566 | ||
|
|
6fdf27a701 | ||
|
|
7fa7d4f0a9 | ||
|
|
f511ebc1d4 | ||
|
|
84bbdc2eba | ||
|
|
568612fc70 | ||
|
|
d78828fd81 | ||
|
|
f56d9ab945 | ||
|
|
86fabd6a22 | ||
|
|
24a1e7cee4 | ||
|
|
223dd8bb1a | ||
|
|
68448de7d0 | ||
|
|
1ebff74c21 | ||
|
|
f0cd3422c1 | ||
|
|
e385a98ced | ||
|
|
670f32baee | ||
|
|
2747a00ba2 | ||
|
|
48e76038d0 | ||
|
|
6421252d44 | ||
|
|
216c4c8bd4 | ||
|
|
5841d410a1 | ||
|
|
63c8207d7a | ||
|
|
54ed58499d | ||
|
|
b1bdc18c85 | ||
|
|
a38030cc0b | ||
|
|
4626aa2cb0 | ||
|
|
5a40b673a4 | ||
|
|
541f63fee4 | ||
|
|
5de6f4a14f | ||
|
|
5658830077 | ||
|
|
0e50edc009 | ||
|
|
444f454810 | ||
|
|
d0e1fd6c7e | ||
|
|
17b4d1e010 | ||
|
|
06791470c9 | ||
|
|
ef14c8ca0e | ||
|
|
36dc883c7c | ||
|
|
6557bd7029 | ||
|
|
41b30c91d9 | ||
|
|
0f767d5ce1 | ||
|
|
328a6de797 | ||
|
|
886be6414d | ||
|
|
9362d3cab3 | ||
|
|
ced2e39dbf | ||
|
|
2159d8877b | ||
|
|
cb7dba3eff | ||
|
|
d9d7f7880d | ||
|
|
a031aaf2c0 | ||
|
|
4bca951773 | ||
|
|
140735dbde | ||
|
|
714a68bba1 | ||
|
|
573c6179ab | ||
|
|
510bf05e36 | ||
|
|
ae852e0be4 | ||
|
|
1955002ed8 | ||
|
|
44559fb7b9 | ||
|
|
0977c5cf73 | ||
|
|
07697bf931 | ||
|
|
5d1d1a1456 | ||
|
|
146383499e | ||
|
|
e81a76fdf9 | ||
|
|
de13137418 | ||
|
|
e42b818c2a | ||
|
|
fcde0c94e0 | ||
|
|
1af83e997d | ||
|
|
59ee7be72a | ||
|
|
c331ee3d5c | ||
|
|
36babe4bef | ||
|
|
c5f2cea802 | ||
|
|
8a200bf913 | ||
|
|
f16468e74f | ||
|
|
79c0b9f51d | ||
|
|
f98a3a4f65 | ||
|
|
b14cecaeb2 | ||
|
|
2594745ef8 | ||
|
|
cc3041322e | ||
|
|
f352f84483 | ||
|
|
cbf48e9b8c | ||
|
|
0ef7e8eca2 | ||
|
|
1a18e43a88 | ||
|
|
6849288d6d | ||
|
|
2edfed7d91 | ||
|
|
30c069f5b7 | ||
|
|
649163cb7b | ||
|
|
980e96250b | ||
|
|
963bc4b647 | ||
|
|
031f25c1c1 | ||
|
|
b40f642fa4 | ||
|
|
22782ca6fc | ||
|
|
1468d83895 | ||
|
|
97f0dc8a60 | ||
|
|
ee02532ab5 | ||
|
|
f1dd0dba78 | ||
|
|
f4ed684146 | ||
|
|
83f02d0bfb | ||
|
|
52fa5f20a3 | ||
|
|
f462ce5615 | ||
|
|
cef3e538ba | ||
|
|
acda4ce985 | ||
|
|
354ece2bdf | ||
|
|
de10bb00a9 | ||
|
|
fdc181106d | ||
|
|
8752b631bd | ||
|
|
378e39f70c | ||
|
|
043a2e7a07 | ||
|
|
7e190e92ca | ||
|
|
5eb318ba06 | ||
|
|
4a209f1afb | ||
|
|
c0ac3c748c | ||
|
|
a65d3e040a | ||
|
|
2358efe44a | ||
|
|
09d3b8f2c2 | ||
|
|
531de77124 | ||
|
|
44981fd803 | ||
|
|
4fb5ac292b | ||
|
|
0e23a3d7c2 | ||
|
|
76ee64ae50 | ||
|
|
e1dbcccab5 | ||
|
|
fba802effd | ||
|
|
9495b56772 | ||
|
|
a8434b176f | ||
|
|
ef0004400d | ||
|
|
0a63049845 | ||
|
|
2dcb86941f | ||
|
|
5c6eb89cfb | ||
|
|
5b92eeb3bf | ||
|
|
3518ce083b | ||
|
|
f13c54afc1 | ||
|
|
3388efe65a | ||
|
|
a11384b286 | ||
|
|
9dd9fb27cd | ||
|
|
0f2035149c | ||
|
|
cba364204a | ||
|
|
4e17788549 | ||
|
|
18a6719893 | ||
|
|
687343f6ca | ||
|
|
e061538c30 | ||
|
|
a6375c7530 | ||
|
|
45fa18a2e3 | ||
|
|
534cccce91 | ||
|
|
72dbcd3ad4 | ||
|
|
5533094984 | ||
|
|
ae2ecd6002 | ||
|
|
0098a2adc5 | ||
|
|
c0dd4a3f07 | ||
|
|
497ddb5829 | ||
|
|
811ff93549 | ||
|
|
96df69bcdc | ||
|
|
6cfa2b8b86 | ||
|
|
eea1e701b7 | ||
|
|
455e5de74d | ||
|
|
9533031891 | ||
|
|
80f8ea6849 | ||
|
|
50eadb00c7 | ||
|
|
d4012bd0b2 | ||
|
|
a902e9f9f6 | ||
|
|
da3ba573d8 | ||
|
|
bea9048cfe | ||
|
|
fc0f5ed83a | ||
|
|
c0588c30d7 | ||
|
|
24c940c51c | ||
|
|
407ee08d8a | ||
|
|
756585fb2a | ||
|
|
5662784afb | ||
|
|
3801901726 | ||
|
|
7d58174f1f | ||
|
|
d339f85087 | ||
|
|
b6a114f7f4 | ||
|
|
e586ef070e | ||
|
|
71a76e9ecb | ||
|
|
1d66474022 | ||
|
|
3934e53476 | ||
|
|
0146fbfc40 | ||
|
|
6ee3117755 | ||
|
|
e2440a569e | ||
|
|
7a1eee78df | ||
|
|
e3c8c0705f | ||
|
|
886d427337 | ||
|
|
d5432b4c27 | ||
|
|
42064fe7ec | ||
|
|
7cee76f9a6 | ||
|
|
ed5b2f2997 | ||
|
|
3b480de38a | ||
|
|
f990630ccc | ||
|
|
d33614d6a0 | ||
|
|
b3866bcea0 | ||
|
|
26ec73c71b | ||
|
|
c3403c5413 | ||
|
|
3b6ddcae37 | ||
|
|
dbdcce20a8 | ||
|
|
e7ef1b2368 | ||
|
|
ce32d1c2c3 | ||
|
|
596b66f397 | ||
|
|
d4fd43cf6f | ||
|
|
6c377f16e7 | ||
|
|
349db7baec | ||
|
|
1f3097da00 | ||
|
|
0b4b5e6f0f | ||
|
|
245273e6c1 | ||
|
|
54a0004de6 | ||
|
|
6a211f6ed6 | ||
|
|
aadb44ebd6 | ||
|
|
9b0db6ab15 | ||
|
|
5b363c347f | ||
|
|
cdea3f63d4 | ||
|
|
40a6260f6e | ||
|
|
a5e47f4e0f | ||
|
|
ac7bc587cb | ||
|
|
4e11a3585a | ||
|
|
63d3e9f6e5 | ||
|
|
d115e36ed8 | ||
|
|
af56b1a950 | ||
|
|
f9999a76fe | ||
|
|
42eb3841a1 | ||
|
|
fb622ccbdf | ||
|
|
d2dc3ddf72 | ||
|
|
e8499452f8 | ||
|
|
e0a6b31c03 | ||
|
|
7c923209ad | ||
|
|
bca2bd2fa1 | ||
|
|
fa99ca2757 | ||
|
|
7073f2a272 | ||
|
|
390e30ae7b | ||
|
|
23cf8c49e0 | ||
|
|
b17a024f6c | ||
|
|
1ed21085bb | ||
|
|
56409ff269 | ||
|
|
0c523980ff | ||
|
|
32873d06bc | ||
|
|
4accaccf77 | ||
|
|
ff416aacaf | ||
|
|
b97947e8ac | ||
|
|
dfcd9fb8c3 | ||
|
|
803811568e | ||
|
|
50b0bd5c39 | ||
|
|
2d02b2b1cf | ||
|
|
456fbecf16 | ||
|
|
668923c392 | ||
|
|
c51e9cbe06 | ||
|
|
60b451e6cf | ||
|
|
3e35390d8f | ||
|
|
f2dad289fb | ||
|
|
b4a8fa59f5 | ||
|
|
73de2a7d07 | ||
|
|
1699a7ce33 | ||
|
|
7743c6e881 | ||
|
|
9a5f69f435 | ||
|
|
5c4211e849 | ||
|
|
c1189e2a7b | ||
|
|
f18889369f | ||
|
|
91c7b638e8 | ||
|
|
6f793a0273 | ||
|
|
0f6c417c3c | ||
|
|
c830e9a634 | ||
|
|
e809623ec9 | ||
|
|
061276902b | ||
|
|
fa6f7d396e | ||
|
|
23666a9230 | ||
|
|
17576e9f66 | ||
|
|
90ec9c8bcb | ||
|
|
988ac62a1b | ||
|
|
3016338e34 | ||
|
|
bc35aca017 | ||
|
|
281d52a1ea | ||
|
|
b8502759b5 | ||
|
|
6f804adf39 | ||
|
|
36db31c55a | ||
|
|
4dbbf59c82 | ||
|
|
832eb4458d | ||
|
|
2cf989d306 | ||
|
|
7d3ee29bd0 | ||
|
|
cba0e46aba | ||
|
|
9b8ab3e61e | ||
|
|
47f18e823a | ||
|
|
2d1b824b62 | ||
|
|
d511698f3f | ||
|
|
cb435ea232 | ||
|
|
43a9016c83 | ||
|
|
255068fd40 | ||
|
|
098a00b025 | ||
|
|
dba0b5276b | ||
|
|
78ae935468 | ||
|
|
3ea5f76470 | ||
|
|
b4d294c05e | ||
|
|
83cf5f5c6a | ||
|
|
e7b3a8eebe | ||
|
|
ee3a42a67e | ||
|
|
50227c0f5f | ||
|
|
bc5eb1e1a5 | ||
|
|
995267a042 | ||
|
|
41226a6075 | ||
|
|
81d32181ce | ||
|
|
c5ecca3938 | ||
|
|
900888731c | ||
|
|
13e648e4b1 | ||
|
|
aff12ff671 | ||
|
|
101fb88255 | ||
|
|
8b489354e4 | ||
|
|
7dea6eb7a6 | ||
|
|
af1bfe4e3e | ||
|
|
d574e9eb52 | ||
|
|
2d7df1e1f2 | ||
|
|
1c0ffcf5b1 | ||
|
|
348cc39975 | ||
|
|
987899f94a | ||
|
|
d8b2d5142f | ||
|
|
134802d1ee | ||
|
|
e5e81b4de1 | ||
|
|
300c961efa | ||
|
|
7c7f512405 | ||
|
|
03e8d029c2 | ||
|
|
787b5f1931 | ||
|
|
56a7624618 | ||
|
|
3a84acf122 | ||
|
|
f600e02e47 | ||
|
|
e6d19de58a | ||
|
|
f2bbf6b2aa | ||
|
|
c54d50fd36 | ||
|
|
6a051054db | ||
|
|
49498f6439 | ||
|
|
144a890c71 | ||
|
|
afb4993445 | ||
|
|
4c9455b944 | ||
|
|
5fdc051a08 | ||
|
|
cb68a40c43 | ||
|
|
023218e6e7 | ||
|
|
2a24b94b8d | ||
|
|
c6531cf184 | ||
|
|
d4fa0ed349 | ||
|
|
10874d2dc4 | ||
|
|
5adaf1ac75 | ||
|
|
9668ea69b8 | ||
|
|
ae9bc7acf1 | ||
|
|
594ee480a2 | ||
|
|
a15b5a2463 | ||
|
|
991e755789 | ||
|
|
97d41ffde8 | ||
|
|
24af0766ac | ||
|
|
af17eaa537 | ||
|
|
3adc10a797 | ||
|
|
5eeef6b28e | ||
|
|
f4c29840c3 | ||
|
|
47fc3ebda4 | ||
|
|
9774a659b0 | ||
|
|
2e4a6de4e7 | ||
|
|
a530e424e9 | ||
|
|
0bfd487ee9 | ||
|
|
6aae834493 | ||
|
|
f56131f38e | ||
|
|
273a11d550 | ||
|
|
ae8ce75e41 | ||
|
|
d6d94b689f | ||
|
|
30d785f1ee | ||
|
|
db5ec3cdfc | ||
|
|
9aca54d039 | ||
|
|
d55d5009c2 | ||
|
|
4f3ee61104 | ||
|
|
96eb98c00a | ||
|
|
68ce9577c6 | ||
|
|
3ae036e997 | ||
|
|
5da2d1d470 | ||
|
|
8e2baf40f1 | ||
|
|
c24c40dfee | ||
|
|
32e52ce1ed | ||
|
|
ed46438359 | ||
|
|
0b5490d5a3 | ||
|
|
2d73ef511d | ||
|
|
63e6c85f6f | ||
|
|
8946a6d2d0 | ||
|
|
d3132645fb | ||
|
|
373f158fe0 | ||
|
|
ce36835fab | ||
|
|
619fa671d7 | ||
|
|
eb07c7a79e | ||
|
|
7eb3535094 | ||
|
|
93b68312cf | ||
|
|
97ce666e43 | ||
|
|
4000e1e66d | ||
|
|
270740e859 | ||
|
|
6cad142cfe | ||
|
|
093013687c | ||
|
|
ff31c469a0 | ||
|
|
fbe390268c | ||
|
|
07ac01dcb7 | ||
|
|
badfdb62cd | ||
|
|
986a410b30 | ||
|
|
9db2d58545 | ||
|
|
4eed46ac59 | ||
|
|
abc38d1dab | ||
|
|
8d6c4f1289 | ||
|
|
a2d40eb8b8 | ||
|
|
17b502bb4b | ||
|
|
a0d4421085 | ||
|
|
0d443072d1 | ||
|
|
c9fb99b799 | ||
|
|
92d245ad04 | ||
|
|
0908627297 | ||
|
|
7f79458b4f | ||
|
|
9b4c11ba95 | ||
|
|
27c31eac5d | ||
|
|
bab8dc0b82 | ||
|
|
d09d2fb665 | ||
|
|
e64cf3b7df | ||
|
|
9b73222314 | ||
|
|
3923b57abf | ||
|
|
4807e64609 | ||
|
|
eeb37d89f1 | ||
|
|
08c1ec4b7e | ||
|
|
6b4cf67add | ||
|
|
e65926fd08 | ||
|
|
f2ec319fe1 | ||
|
|
32377a61b7 | ||
|
|
7aac801ccd | ||
|
|
96fdf59ee4 | ||
|
|
50b8f3ab94 | ||
|
|
ff7aaf977b | ||
|
|
9a1efbe54d | ||
|
|
906c21f458 | ||
|
|
d5e7af7a7e | ||
|
|
4d41f03bd5 | ||
|
|
30704a15a7 | ||
|
|
83889178ed | ||
|
|
1d2720bf5e | ||
|
|
c4b6d0eadb | ||
|
|
0c66888691 | ||
|
|
68781387fe | ||
|
|
fd299a0961 | ||
|
|
285a82050c | ||
|
|
2dbb8c55c9 | ||
|
|
effcf39469 | ||
|
|
9db9484863 | ||
|
|
ca813f461b | ||
|
|
bb46cdb2b3 | ||
|
|
dcb10c21a1 | ||
|
|
05ea0ca00e | ||
|
|
c098f282b1 | ||
|
|
ecf82d197c | ||
|
|
9afe75586a | ||
|
|
a1be455202 | ||
|
|
19fb214226 | ||
|
|
28ec898a8c | ||
|
|
467b1bbeeb | ||
|
|
02ab8ce806 | ||
|
|
ce69e620e9 | ||
|
|
1133cf3ef5 | ||
|
|
59a607e303 | ||
|
|
313be3d7a4 | ||
|
|
4fe40fcee0 | ||
|
|
e233fd4fe5 | ||
|
|
9f7683818f | ||
|
|
179e3cb2f5 | ||
|
|
41b960552d | ||
|
|
8304295c48 | ||
|
|
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 | ||
|
|
8332878cdc | ||
|
|
d0ba69ad22 | ||
|
|
31b8834427 | ||
|
|
d0f7a59e9b | ||
|
|
71e7d517a8 | ||
|
|
e6885e9967 | ||
|
|
e2090923db | ||
|
|
46be319976 | ||
|
|
b27bc45cf2 | ||
|
|
3d735281f4 | ||
|
|
8760a0d94d | ||
|
|
2239b59933 | ||
|
|
425a63f59d | ||
|
|
b85725c009 | ||
|
|
17aebc56c1 | ||
|
|
f76b21b02c | ||
|
|
704545a2ec | ||
|
|
dc7b7afc06 | ||
|
|
e478d3c2dc | ||
|
|
c8318058bb | ||
|
|
abca2118e7 | ||
|
|
a8ee41715a | ||
|
|
94f76d6671 | ||
|
|
bf6cc8903c | ||
|
|
1b15e1692a | ||
|
|
017372db25 | ||
|
|
216a0380fe | ||
|
|
71b9e4ff17 | ||
|
|
9b7deb5246 | ||
|
|
a850a73e1a | ||
|
|
c4d9be9e0d | ||
|
|
f31c604b3d | ||
|
|
4c8a50a52b | ||
|
|
b326e60998 | ||
|
|
11bec79a06 | ||
|
|
16eff06c37 | ||
|
|
2911eba236 | ||
|
|
2e607118c3 | ||
|
|
89c723e3e4 | ||
|
|
35fd9de3ff | ||
|
|
6ddcd3954d | ||
|
|
36b0f2e91a | ||
|
|
fe053e26b5 | ||
|
|
269434cfe6 | ||
|
|
88495a24dc | ||
|
|
d131a7c10a | ||
|
|
744a5d703b | ||
|
|
09421b6378 | ||
|
|
21283b554a | ||
|
|
25810b50c1 | ||
|
|
f1e3a59db3 | ||
|
|
a99deb2cb5 | ||
|
|
38d28e0763 | ||
|
|
e09a94bb9e | ||
|
|
a21c5324fd | ||
|
|
4b43acfec0 | ||
|
|
7df151e820 | ||
|
|
5948ffb965 | ||
|
|
bf4e556f67 | ||
|
|
e3f8567690 | ||
|
|
40c7f3e170 | ||
|
|
c506255e0f | ||
|
|
87c6fd4c0f | ||
|
|
19c445d28e | ||
|
|
9119a5209b | ||
|
|
46c8d6e61f | ||
|
|
ea17c2786d | ||
|
|
12ababd911 | ||
|
|
0523845833 | ||
|
|
57794919fa | ||
|
|
f5bb5cf343 | ||
|
|
3eed614dea | ||
|
|
76a295a660 | ||
|
|
082e3fb8df | ||
|
|
a0cab4f563 | ||
|
|
aeb7308e81 | ||
|
|
bb1ebfda83 | ||
|
|
c05c798221 | ||
|
|
55b1bcc6a5 | ||
|
|
d6eddce420 | ||
|
|
4bf057139b | ||
|
|
a1b28b8282 | ||
|
|
d0aaf71770 | ||
|
|
2f31202c6b | ||
|
|
e4cc510712 | ||
|
|
e329bf6865 | ||
|
|
2badcec765 | ||
|
|
e71c13b1a2 | ||
|
|
a959a67ed3 | ||
|
|
a1044af579 | ||
|
|
a64b57451a | ||
|
|
f0e2318cbd | ||
|
|
ebec308fd8 | ||
|
|
ca094587be | ||
|
|
ca3b86c781 | ||
|
|
5a1d0047b9 | ||
|
|
4669854039 | ||
|
|
2eecdc38a4 | ||
|
|
83581b7c1a | ||
|
|
d346f0023d | ||
|
|
47b7a29cbd | ||
|
|
cffc07579d | ||
|
|
0ef268637e | ||
|
|
50f5a76380 | ||
|
|
20ca05dd36 | ||
|
|
5a792b186a | ||
|
|
3f458064a3 | ||
|
|
5269231df0 | ||
|
|
fc8e49994c | ||
|
|
e911d4aa4b | ||
|
|
01f6e70bc5 | ||
|
|
5f1e39a42c | ||
|
|
4f7770e254 | ||
|
|
e8c4c942c0 | ||
|
|
253976d6c0 | ||
|
|
f0571b4122 | ||
|
|
1b71e52e90 | ||
|
|
6d24be23da | ||
|
|
2a45c178fa | ||
|
|
81e214812f | ||
|
|
4d23773a25 | ||
|
|
40a0b69918 | ||
|
|
a7b37c5953 | ||
|
|
03663a5093 | ||
|
|
b08226a850 | ||
|
|
edbae5dc4d | ||
|
|
0f8ad0234b | ||
|
|
661eadc3bd | ||
|
|
50c1290567 | ||
|
|
eaccc9759a | ||
|
|
925214869b | ||
|
|
6a2bfd26d0 | ||
|
|
72a81afb76 | ||
|
|
240abe204c | ||
|
|
7c49196792 | ||
|
|
3a2808cff6 | ||
|
|
005d6cf4cf | ||
|
|
36dff630d6 | ||
|
|
1825869124 | ||
|
|
3cadc90375 | ||
|
|
2c6967d7f9 | ||
|
|
fe866b123a | ||
|
|
cbef1b1e59 | ||
|
|
e21f84932c | ||
|
|
7a679bc328 | ||
|
|
6635dd9abc | ||
|
|
ce164724ea | ||
|
|
a3ef7a7d88 | ||
|
|
71218ef0d3 | ||
|
|
e777b4c6dc | ||
|
|
6815f94180 | ||
|
|
b013acd89d | ||
|
|
f7c2eb6e76 | ||
|
|
3ef9b1b343 | ||
|
|
2224c68959 | ||
|
|
bb7d03d1db | ||
|
|
50036924e8 | ||
|
|
c2c3f7284f | ||
|
|
f6fee53676 | ||
|
|
63b8e8ed23 | ||
|
|
6ae86eda98 | ||
|
|
267d9617b7 | ||
|
|
0a06ccae50 | ||
|
|
8de0fad9f5 | ||
|
|
e05bf6308e | ||
|
|
a20a0cb455 | ||
|
|
d29f7475d2 | ||
|
|
aaa6702863 | ||
|
|
bb928f096a | ||
|
|
9f01d5c5b4 | ||
|
|
11629a931b | ||
|
|
126f825241 | ||
|
|
998cc7bd22 | ||
|
|
3efccaa8f5 | ||
|
|
d57b35ec30 | ||
|
|
e82dab027d | ||
|
|
9350f3983b | ||
|
|
53b123241f | ||
|
|
97286eea1e | ||
|
|
343e24969d | ||
|
|
31c294d998 | ||
|
|
3b161ab30c | ||
|
|
41fd1778a7 | ||
|
|
ac930cf1aa | ||
|
|
e143fc510d | ||
|
|
bea177a4cd | ||
|
|
aa05a4d050 | ||
|
|
a8112ff824 | ||
|
|
a7710c3845 | ||
|
|
cb2e15f8a7 | ||
|
|
23aa8a0543 | ||
|
|
edf7d046eb | ||
|
|
de0b5cc1c2 | ||
|
|
2686e8afea | ||
|
|
d9853ca2be | ||
|
|
b617eb5adf | ||
|
|
ddf38799e2 | ||
|
|
5291d43dc8 | ||
|
|
a634830d85 | ||
|
|
e5d191ca73 | ||
|
|
2371f0fd51 | ||
|
|
cfdce7a96f | ||
|
|
dc8ac01dec | ||
|
|
5f18738b2b | ||
|
|
7b4e4ca2d0 | ||
|
|
01ba4668b6 | ||
|
|
e782d21806 | ||
|
|
00155d61fc | ||
|
|
8f2273a2b4 | ||
|
|
0d0526afa2 | ||
|
|
ac2d07b61a | ||
|
|
d35487f422 | ||
|
|
2749f4a013 | ||
|
|
45c679648e | ||
|
|
5f2f7fc8b9 | ||
|
|
83c79102cf | ||
|
|
8b95292e53 | ||
|
|
3de7a2ddd3 | ||
|
|
8437a6cb4e | ||
|
|
9c4d08c6e1 | ||
|
|
e26096085e | ||
|
|
2f1b2199c5 | ||
|
|
af791db01f | ||
|
|
abcf030d89 | ||
|
|
7840dc73e3 | ||
|
|
df9050400e | ||
|
|
fdd38d6cf8 | ||
|
|
9891fd672f | ||
|
|
92a84ee112 | ||
|
|
992331f17e | ||
|
|
4fb227ed86 | ||
|
|
5a1ddea100 | ||
|
|
fbaa2f9de9 | ||
|
|
97ab9bb194 | ||
|
|
61ac141124 | ||
|
|
d4d49d9df5 | ||
|
|
c60a944aac | ||
|
|
17584c245f | ||
|
|
6e84b694a4 | ||
|
|
34a93171f0 | ||
|
|
678f6ef72f | ||
|
|
ae8187ed15 | ||
|
|
12dd1ac87f | ||
|
|
85c8f00885 | ||
|
|
e7b7ae811f | ||
|
|
a9743b77f6 | ||
|
|
4068871d97 | ||
|
|
f05afcea39 | ||
|
|
688e9daef4 | ||
|
|
64edacffb7 | ||
|
|
743df5373b | ||
|
|
e80084316d | ||
|
|
9dcd427743 | ||
|
|
d17e93384b | ||
|
|
c1ffcf365e | ||
|
|
3040e97222 | ||
|
|
5f063fb0b5 | ||
|
|
a7dadd8671 | ||
|
|
c320be75a7 | ||
|
|
bd7adcbb7e | ||
|
|
1d6d3edec5 | ||
|
|
46bfeb574c | ||
|
|
a1449ee40e | ||
|
|
8cb41b5fa6 | ||
|
|
53475c7390 | ||
|
|
5d8af150a7 | ||
|
|
69499a51a5 | ||
|
|
4c050d7f4b | ||
|
|
533fca9fa3 | ||
|
|
187bf2f7bc | ||
|
|
983a4222ad | ||
|
|
2ea506aeb8 | ||
|
|
5b343d4c72 | ||
|
|
be61ca64d4 | ||
|
|
efe33cf48d | ||
|
|
fe8d46cce5 | ||
|
|
b1f289bce5 | ||
|
|
a8beb80876 | ||
|
|
ff209471d8 | ||
|
|
806f7d0a2b | ||
|
|
6b943caf37 | ||
|
|
4ea2d460f4 | ||
|
|
c84c18f960 | ||
|
|
1402bdab41 | ||
|
|
7082cf277e | ||
|
|
b9310154a7 | ||
|
|
55c34e3fb0 | ||
|
|
68f2202eec | ||
|
|
5057e50bb8 | ||
|
|
23e1a69955 | ||
|
|
b83c6c9d20 | ||
|
|
67deac6d44 | ||
|
|
ea3731162b | ||
|
|
c75e32e722 | ||
|
|
e7b35be5f6 | ||
|
|
5a309266f0 | ||
|
|
05669eaaad | ||
|
|
e91a6e5439 | ||
|
|
43f72a6419 | ||
|
|
6dcacf3b5e | ||
|
|
edad4d1ce7 | ||
|
|
262842c87d | ||
|
|
376f527742 | ||
|
|
c0bbb3849d | ||
|
|
738c25d818 | ||
|
|
027af4d4ee | ||
|
|
6011f4483a | ||
|
|
fc22466e3b | ||
|
|
975e13a313 | ||
|
|
f46732bc0e | ||
|
|
5c5c25e3ad | ||
|
|
53a0bf2d11 | ||
|
|
7b79d98f59 | ||
|
|
1dd2c26f31 | ||
|
|
d14170348d | ||
|
|
9f94b21687 | ||
|
|
cf57e46d69 | ||
|
|
b459001600 | ||
|
|
73267fd6ad | ||
|
|
1019ecfdcf | ||
|
|
81b847faca | ||
|
|
ce4c76cdd2 | ||
|
|
917420e79a | ||
|
|
0b14dc3228 | ||
|
|
cbdaf3272b | ||
|
|
d51ab2b0a7 | ||
|
|
1363e16312 | ||
|
|
f43d0141f3 | ||
|
|
90b3aad83a | ||
|
|
2675aff98a | ||
|
|
09ffa2c66e | ||
|
|
9fba4f02b6 | ||
|
|
59987747e5 | ||
|
|
c40140bbae | ||
|
|
2123b216c0 | ||
|
|
1983f54907 | ||
|
|
8d629ef323 | ||
|
|
f57bee2f4b | ||
|
|
679739683e | ||
|
|
4fcce1f073 | ||
|
|
ff14220e08 | ||
|
|
a7b7a5c3c5 | ||
|
|
b054441f34 | ||
|
|
1e31d26e03 | ||
|
|
ffe515d0e0 | ||
|
|
aad021f521 | ||
|
|
4a986459ee | ||
|
|
9532d0cba4 | ||
|
|
cadc34f3ad | ||
|
|
db23a48b36 | ||
|
|
407cf68e59 | ||
|
|
e0058ca9c5 | ||
|
|
8140af01aa | ||
|
|
98bf696d01 | ||
|
|
e075bb5c8d | ||
|
|
c6baabedef | ||
|
|
6e6998dab7 | ||
|
|
1a29c23263 | ||
|
|
0f87396ab6 | ||
|
|
ffde948860 | ||
|
|
69b5dbdcc3 | ||
|
|
1121517755 | ||
|
|
6879def619 | ||
|
|
5c0f6d0a6f | ||
|
|
d74abbd20e | ||
|
|
120dae4eed | ||
|
|
bb651db2d2 | ||
|
|
e929dde13e | ||
|
|
9d75385bbb | ||
|
|
1c526feec1 | ||
|
|
7df26986de | ||
|
|
5f2d23a12d | ||
|
|
d9e65c0969 | ||
|
|
ec1160924f | ||
|
|
230e8f895d | ||
|
|
af79378734 | ||
|
|
07ce5e0d22 | ||
|
|
9c8565cf21 | ||
|
|
5ad0ea2b5a | ||
|
|
e482053c8a | ||
|
|
945713d886 | ||
|
|
9bb62ad6b5 | ||
|
|
c2bda9fbde | ||
|
|
1d1db62a44 | ||
|
|
39405373f8 | ||
|
|
22a7988d3f | ||
|
|
b2092fafb7 | ||
|
|
cc7b5d8280 | ||
|
|
702d96a738 | ||
|
|
b9f34f1309 | ||
|
|
07724a0ddd | ||
|
|
83c3454685 | ||
|
|
7d263eb733 | ||
|
|
222687d9c5 | ||
|
|
07d3652e30 | ||
|
|
8d5b9d240a | ||
|
|
4f12eba944 | ||
|
|
a7f77d59c1 | ||
|
|
597248130f | ||
|
|
3c2c9cf317 | ||
|
|
e572b9d0cd | ||
|
|
52e9059a8d | ||
|
|
0cb9cff690 | ||
|
|
c0669cb2a5 | ||
|
|
c5902f2473 | ||
|
|
22028602e8 | ||
|
|
bd54608473 | ||
|
|
3741394269 | ||
|
|
6266d2df7e | ||
|
|
01dfba722a | ||
|
|
f8d5f01665 | ||
|
|
ad999d4791 | ||
|
|
6f1b258501 | ||
|
|
f949ddc0ab | ||
|
|
f53007cbf3 | ||
|
|
c287731df9 | ||
|
|
bc32c78d03 | ||
|
|
daee0db7bb | ||
|
|
91fbf4c79b | ||
|
|
54d9ef2f2a | ||
|
|
e056d4502b | ||
|
|
98c2c439aa | ||
|
|
b6068cea6b | ||
|
|
9c9affa719 | ||
|
|
8eb7dd0059 | ||
|
|
a62ad44883 | ||
|
|
2850354070 | ||
|
|
0a4abcbbc8 | ||
|
|
b491c350ae | ||
|
|
1fbe7c54bf | ||
|
|
9d32fc9bd1 | ||
|
|
542612129d | ||
|
|
750f87bb0a | ||
|
|
e168de79c7 | ||
|
|
9bca5a517f | ||
|
|
aa94cfb876 | ||
|
|
52b776b561 | ||
|
|
c74d3a53d4 | ||
|
|
fe7ac80a6c | ||
|
|
e50b334b9a | ||
|
|
a0d8e374fb | ||
|
|
d3a67cb5ae | ||
|
|
e69e98b185 | ||
|
|
5e1499d67b | ||
|
|
e8dad1afeb | ||
|
|
6ce4e31fc8 | ||
|
|
d2d4faf520 | ||
|
|
438de36749 | ||
|
|
df0eef770e | ||
|
|
bbdd495ed5 | ||
|
|
d686172854 | ||
|
|
e1d96cb64e | ||
|
|
d5f94b65b7 | ||
|
|
ec2d0b6b3c | ||
|
|
3a92bf993d | ||
|
|
ec13965fd0 | ||
|
|
ddf747006e | ||
|
|
4382093868 | ||
|
|
a5322850b3 | ||
|
|
407b08975c | ||
|
|
c7067ff5e8 | ||
|
|
9b2384b296 | ||
|
|
b498a22972 | ||
|
|
20e9da5c67 | ||
|
|
ec8974673b | ||
|
|
5e6e7923e4 | ||
|
|
de1b5971e1 | ||
|
|
5c20d0b4d5 | ||
|
|
9df96ac7f1 | ||
|
|
87cd925144 | ||
|
|
fecb796000 | ||
|
|
cfb6c804aa | ||
|
|
11c50c7558 | ||
|
|
34cc7f176e | ||
|
|
b54da9c6af | ||
|
|
f44f86b832 | ||
|
|
4ebf40f582 | ||
|
|
53e4302143 | ||
|
|
cf778eda4f | ||
|
|
bb63429079 | ||
|
|
f7f9a7ae20 | ||
|
|
8699412a4c | ||
|
|
0d7aa19cd1 | ||
|
|
50a7295360 | ||
|
|
e57b6ae98d | ||
|
|
6843970536 | ||
|
|
62425ad3e4 | ||
|
|
e1e217854e | ||
|
|
5bf177b021 | ||
|
|
72dbf2e2b4 | ||
|
|
46c318c6fe | ||
|
|
05bb1b88c3 | ||
|
|
5176ea9fe0 | ||
|
|
36d349acd2 | ||
|
|
4feee983b5 | ||
|
|
9b12e3e389 | ||
|
|
afd3464216 | ||
|
|
8b64446274 | ||
|
|
28aa4c4d1f | ||
|
|
0be3cdc8fb | ||
|
|
f8be484019 | ||
|
|
35f03f092d | ||
|
|
c3d7401ead | ||
|
|
4db7eb9d9e | ||
|
|
fd4efd6104 | ||
|
|
19a35ec6a4 | ||
|
|
2012c0ca1e | ||
|
|
187421c754 | ||
|
|
b3fb86d415 | ||
|
|
88fafd4e30 | ||
|
|
8056932f9c | ||
|
|
c8af003bfc | ||
|
|
4999441a85 | ||
|
|
09b001e795 | ||
|
|
3b3a251008 | ||
|
|
2e4eb9aa39 | ||
|
|
77fd284703 | ||
|
|
0a4517f4b7 | ||
|
|
4395db3206 | ||
|
|
dd5b0abc67 | ||
|
|
466800aa3a | ||
|
|
4328c535a9 | ||
|
|
f9516709da | ||
|
|
5dce722879 | ||
|
|
9324a39d4e | ||
|
|
84904c5206 | ||
|
|
fe4b429fc2 | ||
|
|
f680d0acaf | ||
|
|
4baff5aeb1 | ||
|
|
f25296fb23 | ||
|
|
e717852c73 | ||
|
|
13dc70f649 | ||
|
|
46040a71c3 | ||
|
|
0558b3fc5c | ||
|
|
99b2ab5526 | ||
|
|
e5f3bb6344 | ||
|
|
c7f89ad88e | ||
|
|
e0d9f79445 | ||
|
|
b6dbb69fc4 | ||
|
|
b76fabee65 | ||
|
|
872bcfd1c0 | ||
|
|
b033c13ca2 | ||
|
|
2db188f3a1 | ||
|
|
11de271c8f | ||
|
|
40c800c57c | ||
|
|
91b0540e95 | ||
|
|
ce6d186345 | ||
|
|
32bc4450a7 | ||
|
|
43f31b40ba | ||
|
|
a3a5185b15 | ||
|
|
14a0f180c8 | ||
|
|
cc9cb0b477 | ||
|
|
2cb0e37f50 | ||
|
|
dbd5be55b0 | ||
|
|
f674b4fbd5 | ||
|
|
5a4e8fea81 | ||
|
|
78e02b52ca | ||
|
|
ffdaae90d7 | ||
|
|
c77681ea17 | ||
|
|
d824390167 | ||
|
|
70cf681ff2 | ||
|
|
b004b9ec81 | ||
|
|
657b05fd96 | ||
|
|
caad60da45 | ||
|
|
7d22cf9b45 | ||
|
|
5cb178ca93 | ||
|
|
16788008b6 | ||
|
|
6ec7a33046 | ||
|
|
6af9c2b3ca | ||
|
|
bdc620dab1 | ||
|
|
a88820af31 | ||
|
|
3688f2e114 | ||
|
|
a183958d53 | ||
|
|
1c8a9e91b7 | ||
|
|
325f6c71ff | ||
|
|
6c6c0792ad | ||
|
|
9264f2307c | ||
|
|
87dd328700 | ||
|
|
0cec92dd0f | ||
|
|
e88afa9665 | ||
|
|
3883a81315 | ||
|
|
c919ad079a | ||
|
|
83593aee70 | ||
|
|
ac7cc09694 | ||
|
|
d032e3568b | ||
|
|
c24df037ac | ||
|
|
a2d43b3746 | ||
|
|
5b3b74bd0f | ||
|
|
d24d3b26dc | ||
|
|
5db3cd7781 | ||
|
|
c88af8b081 | ||
|
|
45852ca3e7 | ||
|
|
03ce555104 | ||
|
|
dd0a07624e | ||
|
|
b9b2b77814 | ||
|
|
2366835121 | ||
|
|
42e1dea7d2 | ||
|
|
13d7716b02 | ||
|
|
7ecb9fc738 | ||
|
|
19b15e0d10 | ||
|
|
0b15de461b | ||
|
|
27aba99e6c | ||
|
|
8151bcfd6b | ||
|
|
e8802357e1 | ||
|
|
6e22c004f6 | ||
|
|
20e1caa531 | ||
|
|
32ad3c3db3 | ||
|
|
1f5f8a7dde | ||
|
|
6da1460795 | ||
|
|
b14ae51f71 | ||
|
|
5af8d001ae | ||
|
|
0ca344df5f | ||
|
|
49f568abbd | ||
|
|
3b4e811907 | ||
|
|
d0e9443031 | ||
|
|
f7e9d9ab1f | ||
|
|
7834d6bca7 | ||
|
|
ed50257735 | ||
|
|
f15f525c5c | ||
|
|
e4bff0460d | ||
|
|
5ce3ddee9b | ||
|
|
22bf7a9509 | ||
|
|
842730707c | ||
|
|
a8f13bd956 | ||
|
|
cd5c2a7999 | ||
|
|
fbc94b9e3e | ||
|
|
e766f25d55 | ||
|
|
140ed9a4cb | ||
|
|
60094884cd | ||
|
|
0e8a4d141a | ||
|
|
17b78a6339 | ||
|
|
e99741159b | ||
|
|
6b9603227b | ||
|
|
23e8d282a3 | ||
|
|
611d6bbfc5 | ||
|
|
f26785c0ba | ||
|
|
5bcfb71737 | ||
|
|
4135c4974f | ||
|
|
222196b182 | ||
|
|
86e55c5c1c | ||
|
|
73c068b96f | ||
|
|
f516026540 | ||
|
|
3c5bc842ed | ||
|
|
d8270a66f4 | ||
|
|
123c383eae | ||
|
|
67814faf92 | ||
|
|
ec4a0c8497 | ||
|
|
21cb227bc2 | ||
|
|
1610bdc5dd | ||
|
|
3296a2f7b2 | ||
|
|
2bd91baad0 | ||
|
|
a624cd9b49 | ||
|
|
02afba132f | ||
|
|
99890a1af0 | ||
|
|
437f1f819c | ||
|
|
92a79e6158 | ||
|
|
c9efd0a74f | ||
|
|
9da349748a | ||
|
|
2423cbbbfe | ||
|
|
4833f6d5db | ||
|
|
9db3cb5cb7 | ||
|
|
c14b353a29 | ||
|
|
19d08b55c8 | ||
|
|
39514b3ca0 | ||
|
|
7ea9d48987 | ||
|
|
df3a982141 | ||
|
|
687b4509df | ||
|
|
41ec2e7944 | ||
|
|
1bd3a9144d | ||
|
|
6e852cc99b | ||
|
|
8320dd0b51 | ||
|
|
960d04d172 | ||
|
|
86ea035bdd | ||
|
|
9b6449dcf4 | ||
|
|
4e22ac1a35 | ||
|
|
8a779f6e94 | ||
|
|
d461768ffb | ||
|
|
5d41e328d4 | ||
|
|
fe492904e9 | ||
|
|
168253b851 | ||
|
|
05620a369e | ||
|
|
8e0fe55363 | ||
|
|
59e521c1db | ||
|
|
f32c149738 | ||
|
|
23a35b3c06 | ||
|
|
044f9c5d4f | ||
|
|
54f9625bdc | ||
|
|
ff0693be32 | ||
|
|
53d9ad93e3 | ||
|
|
f5c5570bec | ||
|
|
53f19a6ead | ||
|
|
cfaf15f429 | ||
|
|
9e67f3b4a5 | ||
|
|
4d2185a2d4 | ||
|
|
33f22263ca | ||
|
|
d09aa07d21 | ||
|
|
8afb8ca7eb | ||
|
|
80ed5bf8fb | ||
|
|
81e7b0b320 | ||
|
|
a828c3b5da | ||
|
|
c95e4a13a1 | ||
|
|
726a7e19eb | ||
|
|
8953ddc6e0 | ||
|
|
7ebbd58b00 | ||
|
|
d0095fd0f4 | ||
|
|
66d8d563eb | ||
|
|
4bf96c7eb5 | ||
|
|
f687c25fa9 | ||
|
|
a92412ecac | ||
|
|
8dcafa5b33 | ||
|
|
7a02cb83a7 | ||
|
|
51ce672076 | ||
|
|
7734afc40c | ||
|
|
ee3cd49aa5 | ||
|
|
bf20ff84b5 | ||
|
|
c58302554c | ||
|
|
05ed88aba8 | ||
|
|
9f5cc0442b | ||
|
|
2641a43ad8 | ||
|
|
4a6ab5e9fd | ||
|
|
d1fe17a4db | ||
|
|
7c910165ef | ||
|
|
8c1fddcf8d | ||
|
|
01b4769852 | ||
|
|
a401828ed5 | ||
|
|
ffd54eef6c | ||
|
|
c16e4316d6 | ||
|
|
8b7fe20b7f | ||
|
|
696c1065b6 | ||
|
|
5d690f4147 | ||
|
|
f906641a82 | ||
|
|
89913dfa8c | ||
|
|
468778f67f | ||
|
|
22a22aebe2 | ||
|
|
a2d2ec9b45 | ||
|
|
2695b3516e | ||
|
|
3a9ef8fac0 | ||
|
|
ebad363201 | ||
|
|
11076d52cd | ||
|
|
5eb132063e | ||
|
|
13ab5d3348 | ||
|
|
ce1ddc400f | ||
|
|
2c9d25e853 | ||
|
|
3d76777760 | ||
|
|
24f4dfea04 | ||
|
|
2fc1a0a9dd | ||
|
|
617aba84e4 | ||
|
|
5510c474c7 | ||
|
|
eb2e8a0b40 | ||
|
|
972491c19d | ||
|
|
7358ca4a52 | ||
|
|
61c274045a | ||
|
|
f205140b04 | ||
|
|
1db8e03c86 | ||
|
|
2ecf86c2bc | ||
|
|
999a847e86 | ||
|
|
1f63ce5dee | ||
|
|
0ad1bbea11 | ||
|
|
b2cd78d279 | ||
|
|
d5bb58a0b4 | ||
|
|
7f84936050 | ||
|
|
6adfea0a72 | ||
|
|
10f213bf3d | ||
|
|
6e8c4f6576 | ||
|
|
9779dc0154 | ||
|
|
a2abe31298 | ||
|
|
930d177dd0 | ||
|
|
f3d1b59173 | ||
|
|
14452f3049 | ||
|
|
4119c8647b | ||
|
|
90a94a8c63 | ||
|
|
b0c39ac7ff | ||
|
|
8703e1ff98 | ||
|
|
35886b88d7 | ||
|
|
d583b35717 | ||
|
|
217ffb2f95 | ||
|
|
22f06f582b | ||
|
|
f2b5098fa0 | ||
|
|
0ca3290364 | ||
|
|
43d5b8598b | ||
|
|
f3e1d1defc | ||
|
|
95c03c9373 | ||
|
|
7e0958b4ac | ||
|
|
6a26737508 | ||
|
|
92a92f39c5 | ||
|
|
fc533cd38d | ||
|
|
68e286499d | ||
|
|
f5c1900aad | ||
|
|
6591dd58ca | ||
|
|
54af113363 | ||
|
|
3f1fe814ef | ||
|
|
5a2cebebd1 | ||
|
|
b8009d61b2 | ||
|
|
a61a64bf9e | ||
|
|
7d17c52fea | ||
|
|
f5b15b392b | ||
|
|
8a53846efd | ||
|
|
badc454452 | ||
|
|
a01bb569d1 | ||
|
|
89ff9f8368 | ||
|
|
7f816a2ebc | ||
|
|
39c141651a | ||
|
|
b0ad9bb6f1 | ||
|
|
d135d0f287 | ||
|
|
b183ccf23d | ||
|
|
c2969bc186 | ||
|
|
bd86bfcd22 | ||
|
|
8aec64b855 | ||
|
|
1445bdba37 | ||
|
|
29d08e63b5 | ||
|
|
1173fdea64 | ||
|
|
968430c338 | ||
|
|
3e5bee6faf | ||
|
|
aa613cba73 | ||
|
|
1e510511ae | ||
|
|
1b44faed17 | ||
|
|
c7a485815c | ||
|
|
7f9c870bba | ||
|
|
b5564ef3d3 | ||
|
|
8ce244dd04 | ||
|
|
0f57b93925 | ||
|
|
c90a77a185 | ||
|
|
c6586f19fa | ||
|
|
cbab86ae38 | ||
|
|
17b5f031f1 | ||
|
|
b00b6b9e25 | ||
|
|
fb6b3b0401 | ||
|
|
22ea878fe9 | ||
|
|
abe3dc6039 | ||
|
|
852829b9dc | ||
|
|
407509c985 | ||
|
|
9856b73cb5 | ||
|
|
f42356fbcb | ||
|
|
d0b467671a | ||
|
|
c18c545798 | ||
|
|
693ef293ac | ||
|
|
a006627795 | ||
|
|
0738b184e4 | ||
|
|
42524ba04e | ||
|
|
63fc95b96d | ||
|
|
ab436fc137 | ||
|
|
1546770bfd | ||
|
|
f4b2099488 | ||
|
|
a2c4d68031 | ||
|
|
cfe14f2817 | ||
|
|
a5402ffb69 | ||
|
|
4d24cf5ec4 | ||
|
|
668d354771 | ||
|
|
ad14719b14 | ||
|
|
d9aa0a67d6 | ||
|
|
92bf784f4f | ||
|
|
395b13103a | ||
|
|
628cf56d3c | ||
|
|
ac5582537f | ||
|
|
9aa7a20d96 | ||
|
|
189f02c802 | ||
|
|
2373281c41 | ||
|
|
e8f4c2d36f | ||
|
|
07b6db23c1 | ||
|
|
9a3360e5d0 | ||
|
|
007a278ac8 | ||
|
|
1db7f45370 | ||
|
|
b271e19a23 | ||
|
|
79b6bdfda1 | ||
|
|
38088f28b0 | ||
|
|
dfb8b5f2fa | ||
|
|
9913e0e025 | ||
|
|
ce567ffdde | ||
|
|
5a9913eca5 | ||
|
|
eaf1ace681 | ||
|
|
a2d1f89922 | ||
|
|
7e09beb0c3 | ||
|
|
ebf5cbf1b9 | ||
|
|
d727710d60 | ||
|
|
0e31aeea00 | ||
|
|
2f437a0382 | ||
|
|
3ad4370fa5 | ||
|
|
a3bb9c2877 | ||
|
|
ee7e976084 | ||
|
|
099358d3e5 | ||
|
|
5297273937 | ||
|
|
80cfc9a25b | ||
|
|
2ae4da524e | ||
|
|
bbe7f28545 | ||
|
|
78ddd497ee | ||
|
|
8d044232af | ||
|
|
aa7e85caa7 | ||
|
|
46a8f24400 | ||
|
|
87bc292296 | ||
|
|
ac539ace70 | ||
|
|
a15b13978f | ||
|
|
0c975db0a6 | ||
|
|
cb4fea0240 | ||
|
|
8e7957d440 | ||
|
|
f7bed32c6f | ||
|
|
ef7f2d82c0 | ||
|
|
7aa97a332e | ||
|
|
7c30dde96b | ||
|
|
9cef2a0a8f | ||
|
|
f376683fc3 | ||
|
|
4b61d6e875 | ||
|
|
7d83e350fd | ||
|
|
500ba69548 | ||
|
|
9a422549b1 | ||
|
|
3b48fa455e | ||
|
|
ef013e0639 | ||
|
|
8f8437a88d | ||
|
|
1b091c9b07 | ||
|
|
4801b6f057 | ||
|
|
9078bc2de5 | ||
|
|
b69464dfe9 | ||
|
|
62fa48293a | ||
|
|
b206d0889b | ||
|
|
ee691d81bf | ||
|
|
56876a67cc | ||
|
|
4a0df713aa | ||
|
|
ef801cbfbe | ||
|
|
9378fc88d2 | ||
|
|
f46bfcc3d8 | ||
|
|
ccdb238843 | ||
|
|
f1f61b4e2b | ||
|
|
a44cb745d9 | ||
|
|
f5f5cb023c | ||
|
|
5813e0ce7a | ||
|
|
5a9c2b1e80 | ||
|
|
bda34fdb3b | ||
|
|
426b677eb8 | ||
|
|
67c7e9fd86 | ||
|
|
d8028a8632 | ||
|
|
374743d022 | ||
|
|
cd98ea5008 | ||
|
|
dbda0ed98a | ||
|
|
f5e0ead01c | ||
|
|
44818701bc | ||
|
|
e0f7387dff | ||
|
|
d440a01792 | ||
|
|
665c84ee42 | ||
|
|
e0de96eb4c | ||
|
|
c6ef276811 | ||
|
|
1701aaf78c | ||
|
|
122daa4bfb | ||
|
|
561a9e5275 | ||
|
|
de2453fce9 | ||
|
|
d59d40c118 | ||
|
|
3469df001f | ||
|
|
0d8cfa3031 | ||
|
|
0289586880 | ||
|
|
e46427c7fc | ||
|
|
3ea59d9a8e | ||
|
|
e85dfc6adf | ||
|
|
d0703b78fa | ||
|
|
432e6adf3e | ||
|
|
a057754035 | ||
|
|
0348ace253 | ||
|
|
c5e38203eb | ||
|
|
9ac31d0233 | ||
|
|
9d8d1cd69d | ||
|
|
07a0381f8b | ||
|
|
f841459004 | ||
|
|
78a26fc139 | ||
|
|
9f6628445e | ||
|
|
fa017b5977 | ||
|
|
58f4a970f2 | ||
|
|
021aa8faed | ||
|
|
83f6e037d6 | ||
|
|
baf153434d | ||
|
|
d481bd7993 | ||
|
|
e859c0a6ef | ||
|
|
59a39e66b1 | ||
|
|
fd5ac69a35 | ||
|
|
a940703ae1 | ||
|
|
350729cde8 | ||
|
|
2e14cd6d66 | ||
|
|
f703524f04 | ||
|
|
aa4435c775 | ||
|
|
31a2e368cc | ||
|
|
97e284e65e | ||
|
|
a6baab92f3 | ||
|
|
7c76e0c3ee | ||
|
|
591a4fcf8e | ||
|
|
71dac85600 | ||
|
|
ad90ddd327 | ||
|
|
03f457f3d0 | ||
|
|
a878256367 | ||
|
|
553f78ed55 | ||
|
|
1bc7d2237e | ||
|
|
132222013b | ||
|
|
2008fb552a | ||
|
|
236c034c62 | ||
|
|
f87baf08d3 | ||
|
|
22aa0c2f40 | ||
|
|
88469d4aaa | ||
|
|
1413c5022a | ||
|
|
aa8cdaee22 | ||
|
|
9f6ff54a76 | ||
|
|
e750c747c6 | ||
|
|
9edfe7d9d3 | ||
|
|
c9b7acd22c | ||
|
|
2ba2f0298c | ||
|
|
a24a2b475a | ||
|
|
4005452772 | ||
|
|
d4b7e221f0 | ||
|
|
77c98fd042 | ||
|
|
082872b2f3 | ||
|
|
6253e2e24c | ||
|
|
4216afe62f | ||
|
|
8fec78a5cd | ||
|
|
7ba0a14e97 | ||
|
|
3a442347a5 | ||
|
|
c4f4fd97d6 | ||
|
|
ac0ead1473 | ||
|
|
83cea9475d | ||
|
|
dc6bb7ab1b | ||
|
|
c71f6ba377 | ||
|
|
b1b1ab5350 | ||
|
|
7613b8dbfe | ||
|
|
e4cece6095 | ||
|
|
bcefe8716f | ||
|
|
746b5d8be0 | ||
|
|
f13ecbd9bb | ||
|
|
e839beb73b | ||
|
|
b797cdf91e | ||
|
|
84e4677a94 | ||
|
|
0377a11719 | ||
|
|
d0fa79044a | ||
|
|
f381f8d35a | ||
|
|
92e1e5b893 | ||
|
|
8e8b4dba22 | ||
|
|
767cd55817 | ||
|
|
eb0ef439d6 | ||
|
|
0bf78c0a8a | ||
|
|
12d7e19f32 | ||
|
|
d1c3dd0ee1 | ||
|
|
3dfa99efe1 | ||
|
|
d7bd221a47 | ||
|
|
1b7a3b4a74 | ||
|
|
c8424ed8fd | ||
|
|
150df1ae8e | ||
|
|
5ca9d77176 | ||
|
|
aa89fcc29d | ||
|
|
7ead0de26b | ||
|
|
f22c2690ec | ||
|
|
738bb0eabc | ||
|
|
002a519a17 | ||
|
|
f51128f772 | ||
|
|
d6a0aa7ccf | ||
|
|
ca94a2ddcb | ||
|
|
835ae1217b | ||
|
|
c165969399 | ||
|
|
88c69a06dc | ||
|
|
cd5e7055d2 | ||
|
|
3157593b6b | ||
|
|
c8399a297e | ||
|
|
529cfe2d9a | ||
|
|
50869c6cd2 | ||
|
|
44fcfab9aa | ||
|
|
340fce9f1c | ||
|
|
51bbf93ff2 | ||
|
|
f8d13d79c7 | ||
|
|
b0ed1dc106 | ||
|
|
ef8dde2b70 | ||
|
|
97870c9288 | ||
|
|
c9226aeaaf | ||
|
|
7b30815938 | ||
|
|
517a89fa9c | ||
|
|
cf80073f27 | ||
|
|
43353ca5a4 | ||
|
|
b79b19c470 | ||
|
|
ccdfab378a | ||
|
|
0e6b4df8f1 | ||
|
|
79a4b18ec7 | ||
|
|
e05ee55545 | ||
|
|
ecaddd897e | ||
|
|
8ca6c246a8 | ||
|
|
55d9a0ef2f | ||
|
|
4067e0f25c | ||
|
|
6d78cf6b58 | ||
|
|
df6635c620 | ||
|
|
2c6d239525 | ||
|
|
0f74f6cd60 | ||
|
|
6ee10c03d1 | ||
|
|
587739e95c | ||
|
|
01bace7769 | ||
|
|
dfa10d4ebe | ||
|
|
f691bd5ce1 | ||
|
|
64dbac8138 | ||
|
|
1c3c154d6d | ||
|
|
32201cacda | ||
|
|
5f566b140f | ||
|
|
03890151d7 | ||
|
|
8e4de29409 | ||
|
|
f6f3390490 | ||
|
|
d4cce1b5b9 | ||
|
|
44068500bf | ||
|
|
8ce263669e | ||
|
|
6b4824ffba | ||
|
|
9a8918cb9e | ||
|
|
b1c2440371 | ||
|
|
37b11e614c | ||
|
|
14b6200fd8 | ||
|
|
7966b80476 | ||
|
|
b526ab2746 | ||
|
|
0e0a892b7e | ||
|
|
fe3649cd27 | ||
|
|
e014220508 | ||
|
|
d06fd03dd8 | ||
|
|
0eed0ca11a | ||
|
|
c57ea9e47c | ||
|
|
70b4577dbe | ||
|
|
5f1f55fbe7 | ||
|
|
2d3d46eb34 | ||
|
|
2ce09b6ffd | ||
|
|
d3378a575c | ||
|
|
9b9b5ebb72 | ||
|
|
0f7f800e95 | ||
|
|
457de86819 | ||
|
|
83154eadd3 | ||
|
|
c0a2f77258 | ||
|
|
c240f1b359 | ||
|
|
aa074a2063 | ||
|
|
f008d0bde3 | ||
|
|
0347a7c038 | ||
|
|
75508bccb5 | ||
|
|
593799a988 | ||
|
|
816d7b734c | ||
|
|
5491895a60 | ||
|
|
9394674b63 | ||
|
|
146008a631 | ||
|
|
cc78f0347d | ||
|
|
1f05420745 | ||
|
|
32e2730ec6 | ||
|
|
29c329dc52 | ||
|
|
7d8a7c5c7d | ||
|
|
9e9e6f7ee6 | ||
|
|
fde33fbb30 | ||
|
|
67edc163cb | ||
|
|
fe8e984608 | ||
|
|
7d340e7ef9 | ||
|
|
af19ba6119 | ||
|
|
8666631732 | ||
|
|
ce4b7231e2 | ||
|
|
21641be9d1 | ||
|
|
5f6f33c464 | ||
|
|
4abf669d09 | ||
|
|
187de6c738 | ||
|
|
884c0cf595 | ||
|
|
9f8978bbcf | ||
|
|
bcdf71deb3 | ||
|
|
7bc7b72c61 | ||
|
|
e531e89b4b | ||
|
|
e5e24ef51d | ||
|
|
c4e46c35b5 | ||
|
|
139127f1e4 | ||
|
|
3838d3070d | ||
|
|
8004ff51f0 | ||
|
|
385c42e638 | ||
|
|
6327c4b40c | ||
|
|
45643fbed1 | ||
|
|
6ac1b395cf | ||
|
|
c5b3e8b042 | ||
|
|
c7fabe40ed | ||
|
|
3fb011712b | ||
|
|
6b1a68908d | ||
|
|
d1e83882e5 | ||
|
|
cf845d946e | ||
|
|
4094c94971 | ||
|
|
ae36e35de7 | ||
|
|
a4b5c61e25 | ||
|
|
9caa37c12a | ||
|
|
9871ed955b | ||
|
|
afc2f509a4 | ||
|
|
fe5618c35d | ||
|
|
8619e07d66 | ||
|
|
eb35d6793e | ||
|
|
a25db09aac | ||
|
|
8969a15858 | ||
|
|
377f3f83a2 | ||
|
|
5a3de62c50 | ||
|
|
2c2eb31e18 | ||
|
|
e13b72afca | ||
|
|
b9086b31e6 | ||
|
|
de2db7c2d2 | ||
|
|
238afda9da | ||
|
|
e7d557fd9e | ||
|
|
4fc763cfa2 | ||
|
|
b47f3adbb3 | ||
|
|
862e3c430c | ||
|
|
dc127e2994 | ||
|
|
3c1190e2c3 | ||
|
|
a104d18277 | ||
|
|
6048b1e270 | ||
|
|
0ef2e330e3 | ||
|
|
b417bd5be4 | ||
|
|
7f84191748 | ||
|
|
29f78248dc | ||
|
|
638f8a52d1 | ||
|
|
4432cc2253 | ||
|
|
730144cc26 | ||
|
|
c463d0cf80 | ||
|
|
8eecae92b2 | ||
|
|
6832451561 | ||
|
|
41925b6606 | ||
|
|
211d97ff8a | ||
|
|
8fa953a516 | ||
|
|
7f8c9ffa30 | ||
|
|
f448b6b977 | ||
|
|
e0cfc33fe2 | ||
|
|
76236a0b75 | ||
|
|
0cb9d79044 | ||
|
|
eeaee94e5e | ||
|
|
ddbdac7d97 | ||
|
|
b44a1cd823 | ||
|
|
d6d02b9924 | ||
|
|
3699a57847 | ||
|
|
d45007b501 | ||
|
|
9c256afc1a | ||
|
|
538a1f5909 | ||
|
|
ecac383477 | ||
|
|
8a761d7e3b | ||
|
|
651c4b539a | ||
|
|
81330d32e0 | ||
|
|
3696e81eb4 | ||
|
|
46f28a9de9 | ||
|
|
dcd7ca78fc | ||
|
|
86a38a1c7e | ||
|
|
2d9203ee74 | ||
|
|
8392567962 | ||
|
|
f76102dab5 | ||
|
|
43cf0441db | ||
|
|
792dc83778 | ||
|
|
718b4afbf3 | ||
|
|
13f41f59d6 | ||
|
|
ca5b782106 | ||
|
|
8e7f215514 | ||
|
|
4fc4eb09b0 | ||
|
|
18e3f43df3 | ||
|
|
28b865acf0 | ||
|
|
dd56b2584b | ||
|
|
70c0812606 | ||
|
|
ef5cfd59d4 | ||
|
|
85a695caa1 | ||
|
|
30444057bd | ||
|
|
6eae8e361f | ||
|
|
7c57eb70e8 | ||
|
|
9f4c0ff624 | ||
|
|
60691819b1 |
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"type": "xboard",
|
||||
"panel_url": "https://your-panel.com",
|
||||
"key": "your-node-key",
|
||||
"node_id": 1,
|
||||
"sync_interval": "1m",
|
||||
"report_interval": "1m"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
.env.example
10
.env.example
@@ -1,10 +0,0 @@
|
||||
PANEL_URL=https://your-panel.com
|
||||
PANEL_TOKEN=your_node_key
|
||||
NODE_ID=1
|
||||
NODE_TYPE=v2ray
|
||||
|
||||
# Separate URLs/IDs (Optional)
|
||||
# CONFIG_PANEL_URL=https://config-panel.com
|
||||
# CONFIG_NODE_ID=151
|
||||
# USER_PANEL_URL=https://user-panel.com
|
||||
# USER_NODE_ID=140
|
||||
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
*.sh text eol=lf
|
||||
*.bash text eol=lf
|
||||
*.service text eol=lf
|
||||
*.conf text eol=lf
|
||||
118
.gitea/workflows/autobuild.yml
Normal file
118
.gitea/workflows/autobuild.yml
Normal file
@@ -0,0 +1,118 @@
|
||||
name: Auto Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- stable
|
||||
- testing
|
||||
- unstable
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Override embedded version
|
||||
required: false
|
||||
type: string
|
||||
targets:
|
||||
description: Space separated build targets, for example "linux-amd64 windows-amd64"
|
||||
required: false
|
||||
default: all
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build binaries
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout source
|
||||
shell: bash
|
||||
env:
|
||||
CI_SERVER_URL: ${{ gitea.server_url }}
|
||||
CI_REPOSITORY: ${{ gitea.repository }}
|
||||
CI_REF: ${{ gitea.ref }}
|
||||
CI_SHA: ${{ gitea.sha }}
|
||||
CI_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
workspace="${GITHUB_WORKSPACE:-$PWD}"
|
||||
repo_url="${CI_SERVER_URL}/${CI_REPOSITORY}.git"
|
||||
|
||||
rm -rf "$workspace/.git"
|
||||
git init -b ci "$workspace"
|
||||
git -C "$workspace" remote add origin "$repo_url"
|
||||
|
||||
if [[ -n "${CI_TOKEN}" ]]; then
|
||||
auth_header="Authorization: token ${CI_TOKEN}"
|
||||
git -C "$workspace" -c http.extraHeader="$auth_header" fetch --depth=1 origin "${CI_REF}"
|
||||
else
|
||||
git -C "$workspace" fetch --depth=1 origin "${CI_REF}"
|
||||
fi
|
||||
|
||||
git -C "$workspace" checkout --detach FETCH_HEAD
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.9
|
||||
|
||||
- name: Prepare Go cache
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$PWD/.gitea-cache/go-build" "$PWD/.gitea-cache/gomod"
|
||||
echo "GOCACHE=$PWD/.gitea-cache/go-build" >> "$GITHUB_ENV"
|
||||
echo "GOMODCACHE=$PWD/.gitea-cache/gomod" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Download Go modules
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
go mod download
|
||||
|
||||
- name: Verify Go modules
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
go mod verify
|
||||
|
||||
- name: Resolve build metadata
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
version_input="${{ inputs.version }}"
|
||||
targets_input="${{ inputs.targets }}"
|
||||
|
||||
if [[ -n "$version_input" ]]; then
|
||||
version="$version_input"
|
||||
else
|
||||
version="$(go run ./cmd/internal/read_tag)"
|
||||
if [[ -z "$version" || "$version" == "unknown" ]]; then
|
||||
version="$(git describe --tags --always 2>/dev/null || git rev-parse --short HEAD)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$targets_input" ]]; then
|
||||
targets_input="all"
|
||||
fi
|
||||
|
||||
echo "VERSION=$version" >> "$GITHUB_ENV"
|
||||
echo "BUILD_TARGETS=$targets_input" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
chmod +x ./building-linux.sh
|
||||
rm -rf dist
|
||||
mkdir -p dist
|
||||
|
||||
VERSION="$VERSION" DIST_DIR="$PWD/dist" ./building-linux.sh ${BUILD_TARGETS}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: sing-box-${{ env.VERSION }}
|
||||
path: dist
|
||||
if-no-files-found: error
|
||||
61
.gitignore
vendored
61
.gitignore
vendored
@@ -1,39 +1,24 @@
|
||||
# Binaries
|
||||
sing-box
|
||||
sing-box.exe
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
.codex-go-Cache
|
||||
.codex-gopath
|
||||
.codex-go-Build
|
||||
.codex-go-modcache
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Build & Cache
|
||||
go.sum
|
||||
bin/
|
||||
dist/
|
||||
/var/lib/sing-box/*
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
/var/log/sing-box/*
|
||||
|
||||
# OS
|
||||
/.idea/
|
||||
/vendor/
|
||||
/*.json
|
||||
/*.srs
|
||||
/*.db
|
||||
/site/
|
||||
/bin/
|
||||
/dist/
|
||||
/sing-box
|
||||
/sing-box.exe
|
||||
/build/
|
||||
/*.jar
|
||||
/*.aar
|
||||
/*.xcframework/
|
||||
/experimental/libbox/*.aar
|
||||
/experimental/libbox/*.xcframework/
|
||||
/experimental/libbox/*.nupkg
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Antigravity/Gemini Artifacts
|
||||
.gemini/
|
||||
artifacts/
|
||||
scratch/
|
||||
implementation_plan*.md
|
||||
walkthrough*.md
|
||||
task.md
|
||||
|
||||
V2bX/
|
||||
reference/
|
||||
/config.d/
|
||||
/venv/
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
/.claude/
|
||||
/.codex-*/
|
||||
5
Makefile
5
Makefile
@@ -106,10 +106,13 @@ release_android: lib_android update_android_version build_android upload_android
|
||||
publish_android:
|
||||
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop
|
||||
|
||||
# TODO: find why and remove `-destination 'generic/platform=iOS'`
|
||||
# TODO: remove xcode clean when fix control widget fixed
|
||||
build_ios:
|
||||
cd ../sing-box-for-apple && \
|
||||
rm -rf build/SFI.xcarchive && \
|
||||
xcodebuild archive -scheme SFI -configuration Release -archivePath build/SFI.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
|
||||
xcodebuild clean -scheme SFI && \
|
||||
xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
|
||||
|
||||
upload_ios_app_store:
|
||||
cd ../sing-box-for-apple && \
|
||||
|
||||
24
README.md
24
README.md
@@ -49,7 +49,9 @@
|
||||
- `acmedns`
|
||||
- 安装脚本默认生成:
|
||||
- `/etc/sing-box/config.d/10-base.json`
|
||||
- `/etc/sing-box/config.d/20-outbounds.json`
|
||||
- `/etc/sing-box/config.d/route.json`
|
||||
- `/etc/sing-box/config.d/outbound.json`
|
||||
- 旧版如果遗留 `/etc/sing-box/config.d/20-outbounds.json`,请不要与 `outbound.json` 同时保留,否则可能出现重复 outbound tag 导致启动失败
|
||||
- 安装后的服务名为:
|
||||
- `singbox.service`
|
||||
|
||||
@@ -57,6 +59,8 @@
|
||||
|
||||
- [install.sh](./install.sh)
|
||||
Linux 安装脚本
|
||||
- [update.sh](./update.sh)
|
||||
Linux 升级脚本
|
||||
- [option/xboard.go](./option/xboard.go)
|
||||
`services.xboard` 配置结构
|
||||
- [service/xboard/service.go](./service/xboard/service.go)
|
||||
@@ -68,12 +72,14 @@
|
||||
|
||||
### 1. 编译并安装
|
||||
|
||||
在 Linux 服务器上进入仓库目录:
|
||||
在 Linux 服务器上执行脚本:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://s3.cloudyun.top/downloads/singbox/install.sh | bash
|
||||
```
|
||||
`install.sh` 默认会从 `https://s3.cloudyun.top/downloads/singbox` 下载对应架构的预编译 `sing-box` 二进制,再继续进入面板和服务配置流程。
|
||||
该脚本同时具有更新的功能
|
||||
`update.sh` 会从同一发布地址下载对应架构的 `sing-box` 二进制,并自动重启已检测到的 `singbox` 或 `sing-box` 服务。
|
||||
|
||||
|
||||
脚本会做这些事情:
|
||||
@@ -139,7 +145,13 @@ sing-box -D /var/lib/sing-box -C /etc/sing-box/config.d run
|
||||
- `services`
|
||||
- 基础路由规则
|
||||
|
||||
### `20-outbounds.json`
|
||||
### `route.json`
|
||||
|
||||
- `route.rules`
|
||||
- `route.auto_detect_interface`
|
||||
- common DNS hijack rules
|
||||
|
||||
### `outbound.json`
|
||||
|
||||
放这些内容:
|
||||
|
||||
@@ -157,7 +169,8 @@ sing-box -D /var/lib/sing-box -C /etc/sing-box/config.d run
|
||||
|
||||
- [configs/10-base.single-node.json](./configs/10-base.single-node.json)
|
||||
- [configs/10-base.multi-node.json](./configs/10-base.multi-node.json)
|
||||
- [configs/20-outbounds.example.json](./configs/20-outbounds.example.json)
|
||||
- [configs/route.json](./configs/route.json)
|
||||
- [configs/outbound.json](./configs/outbound.json)
|
||||
|
||||
## `services.xboard` 配置说明
|
||||
|
||||
@@ -362,7 +375,8 @@ Xboard setup error: missing certificate
|
||||
单节点基础配置
|
||||
- [configs/10-base.multi-node.json](./configs/10-base.multi-node.json)
|
||||
多节点基础配置
|
||||
- [configs/20-outbounds.example.json](./configs/20-outbounds.example.json)
|
||||
- [configs/route.json](./configs/route.json)
|
||||
- [configs/outbound.json](./configs/outbound.json)
|
||||
出站配置模板
|
||||
- [configs/panel-response.vless-reality.json](./configs/panel-response.vless-reality.json)
|
||||
VLESS REALITY 面板回包
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package certificate
|
||||
|
||||
type Adapter struct {
|
||||
providerType string
|
||||
providerTag string
|
||||
}
|
||||
|
||||
func NewAdapter(providerType string, providerTag string) Adapter {
|
||||
return Adapter{
|
||||
providerType: providerType,
|
||||
providerTag: providerTag,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) Type() string {
|
||||
return a.providerType
|
||||
}
|
||||
|
||||
func (a *Adapter) Tag() string {
|
||||
return a.providerTag
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
var _ adapter.CertificateProviderManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.CertificateProviderRegistry
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
providers []adapter.CertificateProviderService
|
||||
providerByTag map[string]adapter.CertificateProviderService
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.CertificateProviderRegistry) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
providerByTag: make(map[string]adapter.CertificateProviderService),
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
providers := m.providers
|
||||
m.access.Unlock()
|
||||
for _, provider := range providers {
|
||||
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
||||
m.logger.Trace(stage, " ", name)
|
||||
startTime := time.Now()
|
||||
err := adapter.LegacyStart(provider, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
}
|
||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if !m.started {
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
providers := m.providers
|
||||
m.providers = nil
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
var err error
|
||||
for _, provider := range providers {
|
||||
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
||||
m.logger.Trace("close ", name)
|
||||
startTime := time.Now()
|
||||
monitor.Start("close ", name)
|
||||
err = E.Append(err, provider.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", name)
|
||||
})
|
||||
monitor.Finish()
|
||||
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) CertificateProviders() []adapter.CertificateProviderService {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return m.providers
|
||||
}
|
||||
|
||||
func (m *Manager) Get(tag string) (adapter.CertificateProviderService, bool) {
|
||||
m.access.Lock()
|
||||
provider, found := m.providerByTag[tag]
|
||||
m.access.Unlock()
|
||||
return provider, found
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
provider, found := m.providerByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.providerByTag, tag)
|
||||
index := common.Index(m.providers, func(it adapter.CertificateProviderService) bool {
|
||||
return it == provider
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid certificate provider index")
|
||||
}
|
||||
m.providers = append(m.providers[:index], m.providers[index+1:]...)
|
||||
started := m.started
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return provider.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error {
|
||||
provider, err := m.registry.Create(ctx, logger, tag, providerType, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started {
|
||||
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
m.logger.Trace(stage, " ", name)
|
||||
startTime := time.Now()
|
||||
err = adapter.LegacyStart(provider, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
}
|
||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
}
|
||||
if existsProvider, loaded := m.providerByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = existsProvider.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "close certificate-provider/", existsProvider.Type(), "[", existsProvider.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.providers, func(it adapter.CertificateProviderService) bool {
|
||||
return it == existsProvider
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid certificate provider index")
|
||||
}
|
||||
m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...)
|
||||
}
|
||||
m.providers = append(m.providers, provider)
|
||||
m.providerByTag[tag] = provider
|
||||
return nil
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package certificate
|
||||
|
||||
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.CertificateProviderService, error)
|
||||
|
||||
func Register[Options any](registry *Registry, providerType string, constructor ConstructorFunc[Options]) {
|
||||
registry.register(providerType, func() any {
|
||||
return new(Options)
|
||||
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.CertificateProviderService, error) {
|
||||
var options *Options
|
||||
if rawOptions != nil {
|
||||
options = rawOptions.(*Options)
|
||||
}
|
||||
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
|
||||
})
|
||||
}
|
||||
|
||||
var _ adapter.CertificateProviderRegistry = (*Registry)(nil)
|
||||
|
||||
type (
|
||||
optionsConstructorFunc func() any
|
||||
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.CertificateProviderService, 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(providerType string) (any, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
optionsConstructor, loaded := m.optionsType[providerType]
|
||||
if !loaded {
|
||||
return nil, false
|
||||
}
|
||||
return optionsConstructor(), true
|
||||
}
|
||||
|
||||
func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (adapter.CertificateProviderService, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
constructor, loaded := m.constructor[providerType]
|
||||
if !loaded {
|
||||
return nil, E.New("certificate provider type not found: " + providerType)
|
||||
}
|
||||
return constructor(ctx, logger, tag, options)
|
||||
}
|
||||
|
||||
func (m *Registry) register(providerType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.optionsType[providerType] = optionsConstructor
|
||||
m.constructor[providerType] = constructor
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
type CertificateProvider interface {
|
||||
GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
|
||||
}
|
||||
|
||||
type ACMECertificateProvider interface {
|
||||
CertificateProvider
|
||||
GetACMENextProtos() []string
|
||||
}
|
||||
|
||||
type CertificateProviderService interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
CertificateProvider
|
||||
}
|
||||
|
||||
type CertificateProviderRegistry interface {
|
||||
option.CertificateProviderOptionsRegistry
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (CertificateProviderService, error)
|
||||
}
|
||||
|
||||
type CertificateProviderManager interface {
|
||||
Lifecycle
|
||||
CertificateProviders() []CertificateProviderService
|
||||
Get(tag string) (CertificateProviderService, bool)
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package adapter
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -26,19 +25,44 @@ type DNSRouter interface {
|
||||
|
||||
type DNSClient interface {
|
||||
Start()
|
||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error)
|
||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error)
|
||||
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)
|
||||
ClearCache()
|
||||
}
|
||||
|
||||
type DNSQueryOptions struct {
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
LookupStrategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
DisableOptimisticCache bool
|
||||
RewriteTTL *uint32
|
||||
ClientSubnet netip.Prefix
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
LookupStrategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
RewriteTTL *uint32
|
||||
ClientSubnet netip.Prefix
|
||||
}
|
||||
|
||||
func LookupDNSTransport(manager DNSTransportManager, reference string) (DNSTransport, bool, bool) {
|
||||
transport, loaded := manager.Transport(reference)
|
||||
if loaded {
|
||||
return transport, true, false
|
||||
}
|
||||
switch reference {
|
||||
case C.DNSTypeLocal, C.DNSTypeFakeIP:
|
||||
default:
|
||||
return nil, false, false
|
||||
}
|
||||
var matchedTransport DNSTransport
|
||||
for _, transport := range manager.Transports() {
|
||||
if transport.Type() != reference {
|
||||
continue
|
||||
}
|
||||
if matchedTransport != nil {
|
||||
return nil, false, true
|
||||
}
|
||||
matchedTransport = transport
|
||||
}
|
||||
if matchedTransport != nil {
|
||||
return matchedTransport, true, false
|
||||
}
|
||||
return nil, false, false
|
||||
}
|
||||
|
||||
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
||||
@@ -46,17 +70,19 @@ func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptio
|
||||
return &DNSQueryOptions{}, nil
|
||||
}
|
||||
transportManager := service.FromContext[DNSTransportManager](ctx)
|
||||
transport, loaded := transportManager.Transport(options.Server)
|
||||
transport, loaded, ambiguous := LookupDNSTransport(transportManager, options.Server)
|
||||
if !loaded {
|
||||
if ambiguous {
|
||||
return nil, E.New("domain resolver is ambiguous: " + options.Server)
|
||||
}
|
||||
return nil, E.New("domain resolver not found: " + options.Server)
|
||||
}
|
||||
return &DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: C.DomainStrategy(options.Strategy),
|
||||
DisableCache: options.DisableCache,
|
||||
DisableOptimisticCache: options.DisableOptimisticCache,
|
||||
RewriteTTL: options.RewriteTTL,
|
||||
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
Transport: transport,
|
||||
Strategy: C.DomainStrategy(options.Strategy),
|
||||
DisableCache: options.DisableCache,
|
||||
RewriteTTL: options.RewriteTTL,
|
||||
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -66,13 +92,6 @@ type RDRCStore interface {
|
||||
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
|
||||
}
|
||||
|
||||
type DNSCacheStore interface {
|
||||
LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool)
|
||||
SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error
|
||||
SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger)
|
||||
ClearDNSCache() error
|
||||
}
|
||||
|
||||
type DNSTransport interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
@@ -82,6 +101,11 @@ type DNSTransport interface {
|
||||
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)
|
||||
|
||||
122
adapter/dns.go.834007539997830043
Normal file
122
adapter/dns.go.834007539997830043
Normal file
@@ -0,0 +1,122 @@
|
||||
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)
|
||||
ClearCache()
|
||||
}
|
||||
|
||||
type DNSQueryOptions struct {
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
LookupStrategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
RewriteTTL *uint32
|
||||
ClientSubnet netip.Prefix
|
||||
}
|
||||
|
||||
func LookupDNSTransport(manager DNSTransportManager, reference string) (DNSTransport, bool, bool) {
|
||||
transport, loaded := manager.Transport(reference)
|
||||
if loaded {
|
||||
return transport, true, false
|
||||
}
|
||||
switch reference {
|
||||
case C.DNSTypeLocal, C.DNSTypeFakeIP:
|
||||
default:
|
||||
return nil, false, false
|
||||
}
|
||||
var matchedTransport DNSTransport
|
||||
for _, transport := range manager.Transports() {
|
||||
if transport.Type() != reference {
|
||||
continue
|
||||
}
|
||||
if matchedTransport != nil {
|
||||
return nil, false, true
|
||||
}
|
||||
matchedTransport = transport
|
||||
}
|
||||
if matchedTransport != nil {
|
||||
return matchedTransport, true, false
|
||||
}
|
||||
return nil, false, false
|
||||
}
|
||||
|
||||
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
||||
if options == nil {
|
||||
return &DNSQueryOptions{}, nil
|
||||
}
|
||||
transportManager := service.FromContext[DNSTransportManager](ctx)
|
||||
transport, loaded, ambiguous := LookupDNSTransport(transportManager, options.Server)
|
||||
if !loaded {
|
||||
if ambiguous {
|
||||
return nil, E.New("domain resolver is ambiguous: " + options.Server)
|
||||
}
|
||||
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
|
||||
Reset()
|
||||
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
|
||||
}
|
||||
137
adapter/dns_test.go
Normal file
137
adapter/dns_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type testDNSTransport struct {
|
||||
transportType string
|
||||
tag string
|
||||
}
|
||||
|
||||
func (t *testDNSTransport) Start(stage StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testDNSTransport) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testDNSTransport) Type() string {
|
||||
return t.transportType
|
||||
}
|
||||
|
||||
func (t *testDNSTransport) Tag() string {
|
||||
return t.tag
|
||||
}
|
||||
|
||||
func (t *testDNSTransport) Dependencies() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testDNSTransport) Reset() {
|
||||
}
|
||||
|
||||
func (t *testDNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type testDNSTransportManager struct {
|
||||
transports []DNSTransport
|
||||
transportByTag map[string]DNSTransport
|
||||
fakeIPTransport FakeIPTransport
|
||||
}
|
||||
|
||||
func newTestDNSTransportManager(transports ...DNSTransport) *testDNSTransportManager {
|
||||
manager := &testDNSTransportManager{
|
||||
transports: transports,
|
||||
transportByTag: make(map[string]DNSTransport),
|
||||
}
|
||||
for _, transport := range transports {
|
||||
manager.transportByTag[transport.Tag()] = transport
|
||||
}
|
||||
return manager
|
||||
}
|
||||
|
||||
func (m *testDNSTransportManager) Start(stage StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *testDNSTransportManager) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *testDNSTransportManager) Transports() []DNSTransport {
|
||||
return m.transports
|
||||
}
|
||||
|
||||
func (m *testDNSTransportManager) Transport(tag string) (DNSTransport, bool) {
|
||||
transport, loaded := m.transportByTag[tag]
|
||||
return transport, loaded
|
||||
}
|
||||
|
||||
func (m *testDNSTransportManager) Default() DNSTransport {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *testDNSTransportManager) FakeIP() FakeIPTransport {
|
||||
return m.fakeIPTransport
|
||||
}
|
||||
|
||||
func (m *testDNSTransportManager) Remove(tag string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *testDNSTransportManager) Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestLookupDNSTransportLocalAlias(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
localTransport := &testDNSTransport{
|
||||
transportType: "local",
|
||||
tag: "dns-local",
|
||||
}
|
||||
manager := newTestDNSTransportManager(localTransport)
|
||||
|
||||
transport, loaded, ambiguous := LookupDNSTransport(manager, "local")
|
||||
require.True(t, loaded)
|
||||
require.False(t, ambiguous)
|
||||
require.Same(t, localTransport, transport)
|
||||
}
|
||||
|
||||
func TestLookupDNSTransportExactTagPreferred(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
localTransport := &testDNSTransport{
|
||||
transportType: "local",
|
||||
tag: "local",
|
||||
}
|
||||
manager := newTestDNSTransportManager(localTransport)
|
||||
|
||||
transport, loaded, ambiguous := LookupDNSTransport(manager, "local")
|
||||
require.True(t, loaded)
|
||||
require.False(t, ambiguous)
|
||||
require.Same(t, localTransport, transport)
|
||||
}
|
||||
|
||||
func TestLookupDNSTransportLocalAliasAmbiguous(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
manager := newTestDNSTransportManager(
|
||||
&testDNSTransport{transportType: "local", tag: "dns-local-a"},
|
||||
&testDNSTransport{transportType: "local", tag: "dns-local-b"},
|
||||
)
|
||||
|
||||
transport, loaded, ambiguous := LookupDNSTransport(manager, "local")
|
||||
require.Nil(t, transport)
|
||||
require.False(t, loaded)
|
||||
require.True(t, ambiguous)
|
||||
}
|
||||
@@ -47,12 +47,6 @@ type CacheFile interface {
|
||||
StoreRDRC() bool
|
||||
RDRCStore
|
||||
|
||||
StoreDNS() bool
|
||||
DNSCacheStore
|
||||
|
||||
SetDisableExpire(disableExpire bool)
|
||||
SetOptimisticTimeout(timeout time.Duration)
|
||||
|
||||
LoadMode() string
|
||||
StoreMode(mode string) error
|
||||
LoadSelected(group string) string
|
||||
|
||||
@@ -2,7 +2,6 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
@@ -10,8 +9,6 @@ import (
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type Inbound interface {
|
||||
@@ -81,16 +78,12 @@ type InboundContext struct {
|
||||
FallbackNetworkType []C.InterfaceType
|
||||
FallbackDelay time.Duration
|
||||
|
||||
DestinationAddresses []netip.Addr
|
||||
DNSResponse *dns.Msg
|
||||
DestinationAddressMatchFromResponse bool
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *ConnectionOwner
|
||||
SourceMACAddress net.HardwareAddr
|
||||
SourceHostname string
|
||||
QueryType uint16
|
||||
FakeIP bool
|
||||
DestinationAddresses []netip.Addr
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *ConnectionOwner
|
||||
QueryType uint16
|
||||
FakeIP bool
|
||||
|
||||
// rule cache
|
||||
|
||||
@@ -119,51 +112,6 @@ func (c *InboundContext) ResetRuleMatchCache() {
|
||||
c.DidMatch = false
|
||||
}
|
||||
|
||||
func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr {
|
||||
return DNSResponseAddresses(c.DNSResponse)
|
||||
}
|
||||
|
||||
func DNSResponseAddresses(response *dns.Msg) []netip.Addr {
|
||||
if response == nil || response.Rcode != dns.RcodeSuccess {
|
||||
return nil
|
||||
}
|
||||
addresses := make([]netip.Addr, 0, len(response.Answer))
|
||||
for _, rawRecord := range response.Answer {
|
||||
switch record := rawRecord.(type) {
|
||||
case *dns.A:
|
||||
addr := M.AddrFromIP(record.A)
|
||||
if addr.IsValid() {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
case *dns.AAAA:
|
||||
addr := M.AddrFromIP(record.AAAA)
|
||||
if addr.IsValid() {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
case *dns.HTTPS:
|
||||
for _, value := range record.SVCB.Value {
|
||||
switch hint := value.(type) {
|
||||
case *dns.SVCBIPv4Hint:
|
||||
for _, ip := range hint.Hint {
|
||||
addr := M.AddrFromIP(ip).Unmap()
|
||||
if addr.IsValid() {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
}
|
||||
case *dns.SVCBIPv6Hint:
|
||||
for _, ip := range hint.Hint {
|
||||
addr := M.AddrFromIP(ip)
|
||||
if addr.IsValid() {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
type inboundContextKey struct{}
|
||||
|
||||
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDNSResponseAddressesUnmapsHTTPSIPv4Hints(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ipv4Hint := net.ParseIP("1.1.1.1")
|
||||
require.NotNil(t, ipv4Hint)
|
||||
|
||||
response := &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Response: true,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.HTTPS{
|
||||
SVCB: dns.SVCB{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: dns.Fqdn("example.com"),
|
||||
Rrtype: dns.TypeHTTPS,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 60,
|
||||
},
|
||||
Priority: 1,
|
||||
Target: ".",
|
||||
Value: []dns.SVCBKeyValue{
|
||||
&dns.SVCBIPv4Hint{Hint: []net.IP{ipv4Hint}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
addresses := DNSResponseAddresses(response)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses)
|
||||
require.True(t, addresses[0].Is4())
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type NeighborEntry struct {
|
||||
Address netip.Addr
|
||||
MACAddress net.HardwareAddr
|
||||
Hostname string
|
||||
}
|
||||
|
||||
type NeighborResolver interface {
|
||||
LookupMAC(address netip.Addr) (net.HardwareAddr, bool)
|
||||
LookupHostname(address netip.Addr) (string, bool)
|
||||
Start() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type NeighborUpdateListener interface {
|
||||
UpdateNeighborTable(entries []NeighborEntry)
|
||||
}
|
||||
@@ -36,10 +36,6 @@ type PlatformInterface interface {
|
||||
|
||||
UsePlatformNotification() bool
|
||||
SendNotification(notification *Notification) error
|
||||
|
||||
UsePlatformNeighborResolver() bool
|
||||
StartNeighborMonitor(listener NeighborUpdateListener) error
|
||||
CloseNeighborMonitor(listener NeighborUpdateListener) error
|
||||
}
|
||||
|
||||
type FindConnectionOwnerRequest struct {
|
||||
|
||||
@@ -26,8 +26,6 @@ type Router interface {
|
||||
RuleSet(tag string) (RuleSet, bool)
|
||||
Rules() []Rule
|
||||
NeedFindProcess() bool
|
||||
NeedFindNeighbor() bool
|
||||
NeighborResolver() NeighborResolver
|
||||
AppendTracker(tracker ConnectionTracker)
|
||||
ResetNetwork()
|
||||
}
|
||||
@@ -66,16 +64,10 @@ type RuleSet interface {
|
||||
|
||||
type RuleSetUpdateCallback func(it RuleSet)
|
||||
|
||||
type DNSRuleSetUpdateValidator interface {
|
||||
ValidateRuleSetMetadataUpdate(tag string, metadata RuleSetMetadata) error
|
||||
}
|
||||
|
||||
// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent.
|
||||
type RuleSetMetadata struct {
|
||||
ContainsProcessRule bool
|
||||
ContainsWIFIRule bool
|
||||
ContainsIPCIDRRule bool
|
||||
ContainsDNSQueryTypeRule bool
|
||||
ContainsProcessRule bool
|
||||
ContainsWIFIRule bool
|
||||
ContainsIPCIDRRule bool
|
||||
}
|
||||
type HTTPStartContext struct {
|
||||
ctx context.Context
|
||||
|
||||
@@ -2,8 +2,6 @@ package adapter
|
||||
|
||||
import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type HeadlessRule interface {
|
||||
@@ -20,9 +18,8 @@ type Rule interface {
|
||||
|
||||
type DNSRule interface {
|
||||
Rule
|
||||
LegacyPreMatch(metadata *InboundContext) bool
|
||||
WithAddressLimit() bool
|
||||
MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool
|
||||
MatchAddressLimit(metadata *InboundContext) bool
|
||||
}
|
||||
|
||||
type RuleAction interface {
|
||||
@@ -32,7 +29,7 @@ type RuleAction interface {
|
||||
|
||||
func IsFinalAction(action RuleAction) bool {
|
||||
switch action.Type() {
|
||||
case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate:
|
||||
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import "context"
|
||||
|
||||
type TailscaleEndpoint interface {
|
||||
SubscribeTailscaleStatus(ctx context.Context, fn func(*TailscaleEndpointStatus)) error
|
||||
StartTailscalePing(ctx context.Context, peerIP string, fn func(*TailscalePingResult)) error
|
||||
}
|
||||
|
||||
type TailscalePingResult struct {
|
||||
LatencyMs float64
|
||||
IsDirect bool
|
||||
Endpoint string
|
||||
DERPRegionID int32
|
||||
DERPRegionCode string
|
||||
Error string
|
||||
}
|
||||
|
||||
type TailscaleEndpointStatus struct {
|
||||
BackendState string
|
||||
AuthURL string
|
||||
NetworkName string
|
||||
MagicDNSSuffix string
|
||||
Self *TailscalePeer
|
||||
UserGroups []*TailscaleUserGroup
|
||||
}
|
||||
|
||||
type TailscaleUserGroup struct {
|
||||
UserID int64
|
||||
LoginName string
|
||||
DisplayName string
|
||||
ProfilePicURL string
|
||||
Peers []*TailscalePeer
|
||||
}
|
||||
|
||||
type TailscalePeer struct {
|
||||
HostName string
|
||||
DNSName string
|
||||
OS string
|
||||
TailscaleIPs []string
|
||||
Online bool
|
||||
ExitNode bool
|
||||
ExitNodeOption bool
|
||||
Active bool
|
||||
RxBytes int64
|
||||
TxBytes int64
|
||||
UserID int64
|
||||
KeyExpiry int64
|
||||
}
|
||||
975
api.md
975
api.md
@@ -1,975 +0,0 @@
|
||||
I made it with AI, but it seems great so far
|
||||
# Xboard API Documentation
|
||||
|
||||
This document provides a comprehensive overview of all API endpoints available in the Xboard system, organized by access level.
|
||||
|
||||
## API Base URLs
|
||||
|
||||
- **V1 API**: `/api/v1/`
|
||||
- **V2 API**: `/api/v2/`
|
||||
|
||||
## Authentication
|
||||
|
||||
- **Guest**: No authentication required
|
||||
- **User**: Requires user authentication (`user` middleware)
|
||||
- **Staff**: Requires staff privileges (`staff` middleware)
|
||||
- **Admin**: Requires admin privileges (`admin` middleware)
|
||||
- **Client**: Requires client authentication (`client` middleware)
|
||||
- **Server**: Requires server authentication (`server` middleware)
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Guest APIs (Public - No Authentication Required)
|
||||
|
||||
### Plans
|
||||
- **GET** `/api/v1/guest/plan/fetch`
|
||||
- **Purpose**: Get all available subscription plans for public viewing
|
||||
- **Returns**: List of available plans with pricing, features, and limits
|
||||
- **Data**: Plan ID, name, prices (monthly/quarterly/yearly), transfer limits, speed limits, device limits, capacity info
|
||||
|
||||
### Configuration
|
||||
- **GET** `/api/v1/guest/comm/config`
|
||||
- **Purpose**: Get public configuration settings
|
||||
- **Returns**: Public app configuration
|
||||
- **Data**: ToS URL, email verification settings, invite requirements, reCAPTCHA settings, app description, app URL, logo
|
||||
|
||||
### Payment Webhooks
|
||||
- **GET/POST** `/api/v1/guest/payment/notify/{method}/{uuid}`
|
||||
- **Purpose**: Handle payment notifications from payment providers
|
||||
- **Returns**: Payment processing status
|
||||
- **Data**: Payment confirmation and processing results
|
||||
|
||||
### Telegram Webhooks
|
||||
- **POST** `/api/v1/guest/telegram/webhook`
|
||||
- **Purpose**: Handle Telegram bot webhook events
|
||||
- **Returns**: Webhook processing status
|
||||
- **Data**: Telegram bot event processing results
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Authentication APIs (Passport)
|
||||
|
||||
### V1 Authentication
|
||||
- **POST** `/api/v1/passport/auth/register`
|
||||
- **Purpose**: User registration
|
||||
- **Returns**: Registration success/failure
|
||||
- **Data**: User account creation status
|
||||
|
||||
- **POST** `/api/v1/passport/auth/login`
|
||||
- **Purpose**: User login
|
||||
- **Returns**: Authentication token and user info
|
||||
- **Data**: JWT token, user details, session info
|
||||
|
||||
- **GET** `/api/v1/passport/auth/token2Login`
|
||||
- **Purpose**: Token-based login
|
||||
- **Returns**: Login status
|
||||
- **Data**: Authentication status
|
||||
|
||||
- **POST** `/api/v1/passport/auth/forget`
|
||||
- **Purpose**: Password reset request
|
||||
- **Returns**: Reset email status
|
||||
- **Data**: Password reset confirmation
|
||||
|
||||
- **POST** `/api/v1/passport/auth/getQuickLoginUrl`
|
||||
- **Purpose**: Generate quick login URL
|
||||
- **Returns**: Quick login URL
|
||||
- **Data**: Temporary login URL
|
||||
|
||||
- **POST** `/api/v1/passport/auth/loginWithMailLink`
|
||||
- **Purpose**: Login via email link
|
||||
- **Returns**: Login status
|
||||
- **Data**: Authentication confirmation
|
||||
|
||||
### Communication
|
||||
- **POST** `/api/v1/passport/comm/sendEmailVerify`
|
||||
- **Purpose**: Send email verification
|
||||
- **Returns**: Email sending status
|
||||
- **Data**: Verification email delivery confirmation
|
||||
|
||||
- **POST** `/api/v1/passport/comm/pv`
|
||||
- **Purpose**: Page view tracking
|
||||
- **Returns**: Tracking status
|
||||
- **Data**: Analytics tracking confirmation
|
||||
|
||||
### V2 Authentication
|
||||
Same endpoints as V1 but under `/api/v2/passport/` prefix.
|
||||
|
||||
---
|
||||
|
||||
## 👤 User APIs (Authenticated Users)
|
||||
|
||||
### User Management
|
||||
- **GET** `/api/v1/user/info`
|
||||
- **Purpose**: Get current user information
|
||||
- **Returns**: User profile data
|
||||
- **Data**: Email, transfer limits, login history, subscription status, balance, commission info, telegram ID, avatar URL
|
||||
|
||||
- **POST** `/api/v1/user/update`
|
||||
- **Purpose**: Update user preferences
|
||||
- **Returns**: Update status
|
||||
- **Data**: Updated user preferences (reminders, etc.)
|
||||
|
||||
- **POST** `/api/v1/user/changePassword`
|
||||
- **Purpose**: Change user password
|
||||
- **Returns**: Password change status
|
||||
- **Data**: Password update confirmation
|
||||
|
||||
- **GET** `/api/v1/user/resetSecurity`
|
||||
- **Purpose**: Reset security credentials (UUID, token)
|
||||
- **Returns**: New subscribe URL
|
||||
- **Data**: New subscription URL with updated token
|
||||
|
||||
- **GET** `/api/v1/user/checkLogin`
|
||||
- **Purpose**: Check login status
|
||||
- **Returns**: Login status and permissions
|
||||
- **Data**: Login status, admin privileges
|
||||
|
||||
- **GET** `/api/v1/user/getStat`
|
||||
- **Purpose**: Get user statistics
|
||||
- **Returns**: User statistics summary
|
||||
- **Data**: Pending orders count, open tickets count, invited users count
|
||||
|
||||
- **GET** `/api/v1/user/getSubscribe`
|
||||
- **Purpose**: Get subscription information
|
||||
- **Returns**: Subscription details and URL
|
||||
- **Data**: Plan details, subscription URL, usage stats, reset schedule
|
||||
|
||||
- **POST** `/api/v1/user/transfer`
|
||||
- **Purpose**: Transfer commission to balance
|
||||
- **Returns**: Transfer status
|
||||
- **Data**: Transfer confirmation and updated balances
|
||||
|
||||
- **POST** `/api/v1/user/getQuickLoginUrl`
|
||||
- **Purpose**: Generate quick login URL
|
||||
- **Returns**: Quick login URL
|
||||
- **Data**: Temporary login URL
|
||||
|
||||
### Session Management
|
||||
- **GET** `/api/v1/user/getActiveSession`
|
||||
- **Purpose**: Get active sessions
|
||||
- **Returns**: List of active sessions
|
||||
- **Data**: Session details, login times, IP addresses
|
||||
|
||||
- **POST** `/api/v1/user/removeActiveSession`
|
||||
- **Purpose**: Remove specific session
|
||||
- **Returns**: Session removal status
|
||||
- **Data**: Session termination confirmation
|
||||
|
||||
### Orders & Billing
|
||||
- **POST** `/api/v1/user/order/save`
|
||||
- **Purpose**: Create new order
|
||||
- **Returns**: Order creation status
|
||||
- **Data**: Order details, payment information
|
||||
|
||||
- **POST** `/api/v1/user/order/checkout`
|
||||
- **Purpose**: Checkout order
|
||||
- **Returns**: Payment processing info
|
||||
- **Data**: Payment URL, order status
|
||||
|
||||
- **GET** `/api/v1/user/order/check`
|
||||
- **Purpose**: Check order status
|
||||
- **Returns**: Order status
|
||||
- **Data**: Order processing status
|
||||
|
||||
- **GET** `/api/v1/user/order/detail`
|
||||
- **Purpose**: Get order details
|
||||
- **Returns**: Detailed order information
|
||||
- **Data**: Order items, pricing, status, payment info
|
||||
|
||||
- **GET** `/api/v1/user/order/fetch`
|
||||
- **Purpose**: Get user's orders
|
||||
- **Returns**: List of user orders
|
||||
- **Data**: Order history with status and details
|
||||
|
||||
- **GET** `/api/v1/user/order/getPaymentMethod`
|
||||
- **Purpose**: Get available payment methods
|
||||
- **Returns**: List of payment options
|
||||
- **Data**: Payment providers, fees, availability
|
||||
|
||||
- **POST** `/api/v1/user/order/cancel`
|
||||
- **Purpose**: Cancel order
|
||||
- **Returns**: Cancellation status
|
||||
- **Data**: Order cancellation confirmation
|
||||
|
||||
### Plans
|
||||
- **GET** `/api/v1/user/plan/fetch`
|
||||
- **Purpose**: Get available plans for authenticated user
|
||||
- **Returns**: List of plans with user-specific availability
|
||||
- **Data**: Plans with pricing, user eligibility, renewal options
|
||||
|
||||
### Invitations
|
||||
- **GET** `/api/v1/user/invite/save`
|
||||
- **Purpose**: Generate invitation code
|
||||
- **Returns**: Invitation code
|
||||
- **Data**: Invitation code and sharing info
|
||||
|
||||
- **GET** `/api/v1/user/invite/fetch`
|
||||
- **Purpose**: Get invitation statistics
|
||||
- **Returns**: Invitation data
|
||||
- **Data**: Invitation codes, usage stats, commissions
|
||||
|
||||
- **GET** `/api/v1/user/invite/details`
|
||||
- **Purpose**: Get detailed invitation information
|
||||
- **Returns**: Invitation details
|
||||
- **Data**: Detailed invitation statistics and earnings
|
||||
|
||||
### Support & Communication
|
||||
- **GET** `/api/v1/user/notice/fetch`
|
||||
- **Purpose**: Get user notices
|
||||
- **Returns**: List of notices
|
||||
- **Data**: System announcements, updates, alerts
|
||||
|
||||
- **POST** `/api/v1/user/ticket/save`
|
||||
- **Purpose**: Create support ticket
|
||||
- **Returns**: Ticket creation status
|
||||
- **Data**: Ticket ID and details
|
||||
|
||||
- **GET** `/api/v1/user/ticket/fetch`
|
||||
- **Purpose**: Get user's tickets
|
||||
- **Returns**: List of support tickets
|
||||
- **Data**: Ticket history, status, responses
|
||||
|
||||
- **POST** `/api/v1/user/ticket/reply`
|
||||
- **Purpose**: Reply to ticket
|
||||
- **Returns**: Reply status
|
||||
- **Data**: Reply confirmation
|
||||
|
||||
- **POST** `/api/v1/user/ticket/close`
|
||||
- **Purpose**: Close ticket
|
||||
- **Returns**: Closure status
|
||||
- **Data**: Ticket closure confirmation
|
||||
|
||||
- **POST** `/api/v1/user/ticket/withdraw`
|
||||
- **Purpose**: Withdraw ticket
|
||||
- **Returns**: Withdrawal status
|
||||
- **Data**: Ticket withdrawal confirmation
|
||||
|
||||
### Servers
|
||||
- **GET** `/api/v1/user/server/fetch`
|
||||
- **Purpose**: Get available servers
|
||||
- **Returns**: List of servers user can access
|
||||
- **Data**: Server details, locations, status, protocols
|
||||
|
||||
### Coupons
|
||||
- **POST** `/api/v1/user/coupon/check`
|
||||
- **Purpose**: Validate coupon code
|
||||
- **Returns**: Coupon validity and discount info
|
||||
- **Data**: Coupon details, discount amount, validity
|
||||
|
||||
### Telegram Integration
|
||||
- **GET** `/api/v1/user/telegram/getBotInfo`
|
||||
- **Purpose**: Get Telegram bot information
|
||||
- **Returns**: Bot connection info
|
||||
- **Data**: Bot details, connection status
|
||||
|
||||
### Configuration
|
||||
- **GET** `/api/v1/user/comm/config`
|
||||
- **Purpose**: Get user-specific configuration
|
||||
- **Returns**: User configuration settings
|
||||
- **Data**: User preferences, feature availability
|
||||
|
||||
- **POST** `/api/v1/user/comm/getStripePublicKey`
|
||||
- **Purpose**: Get Stripe public key for payments
|
||||
- **Returns**: Stripe configuration
|
||||
- **Data**: Stripe public key for payment processing
|
||||
|
||||
### Knowledge Base
|
||||
- **GET** `/api/v1/user/knowledge/fetch`
|
||||
- **Purpose**: Get knowledge base articles
|
||||
- **Returns**: List of help articles
|
||||
- **Data**: Articles, categories, content
|
||||
|
||||
- **GET** `/api/v1/user/knowledge/getCategory`
|
||||
- **Purpose**: Get knowledge base categories
|
||||
- **Returns**: List of categories
|
||||
- **Data**: Category structure and organization
|
||||
|
||||
### Statistics
|
||||
- **GET** `/api/v1/user/stat/getTrafficLog`
|
||||
- **Purpose**: Get traffic usage logs
|
||||
- **Returns**: Traffic usage history
|
||||
- **Data**: Traffic logs, usage patterns, timestamps
|
||||
|
||||
### V2 User APIs
|
||||
- **GET** `/api/v2/user/resetSecurity`
|
||||
- **Purpose**: Reset security credentials
|
||||
- **Returns**: New security credentials
|
||||
- **Data**: Updated security tokens
|
||||
|
||||
- **GET** `/api/v2/user/info`
|
||||
- **Purpose**: Get user information (V2)
|
||||
- **Returns**: User profile data
|
||||
- **Data**: Enhanced user profile information
|
||||
|
||||
---
|
||||
|
||||
## 📱 Client APIs (Client Authentication)
|
||||
|
||||
### Subscription
|
||||
- **GET** `/api/v1/client/subscribe`
|
||||
- **Purpose**: Get subscription configuration
|
||||
- **Returns**: Client configuration for VPN apps
|
||||
- **Data**: Server configurations, protocols, connection details
|
||||
|
||||
### App Configuration
|
||||
- **GET** `/api/v1/client/app/getConfig`
|
||||
- **Purpose**: Get app configuration
|
||||
- **Returns**: App configuration settings
|
||||
- **Data**: App settings, features, URLs
|
||||
|
||||
- **GET** `/api/v1/client/app/getVersion`
|
||||
- **Purpose**: Get app version information
|
||||
- **Returns**: Version details
|
||||
- **Data**: Current version, update availability
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Server APIs (Server Authentication)
|
||||
|
||||
### UniProxy
|
||||
- **GET** `/api/v1/server/UniProxy/config`
|
||||
- **Purpose**: Get server configuration
|
||||
- **Returns**: Server configuration
|
||||
- **Data**: Server settings and parameters
|
||||
- **Example Response**:
|
||||
```json
|
||||
{
|
||||
"protocol": "vless",
|
||||
"listen_ip": "0.0.0.0",
|
||||
"server_port": 18443,
|
||||
"network": "tcp",
|
||||
"tls": 2,
|
||||
"server_name": "git.example.com",
|
||||
"dest": "www.cloudflare.com:443",
|
||||
"private_key": "YOUR_REALITY_PRIVATE_KEY",
|
||||
"short_id": "01234567",
|
||||
"accept_proxy_protocol": true,
|
||||
"base_config": {
|
||||
"push_interval": 60,
|
||||
"pull_interval": 60
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Proxy Protocol Note**: Set `accept_proxy_protocol` to `true` only when this node is behind an L4 proxy or load balancer that really sends PROXY protocol headers. Direct client connections will fail if this is enabled without an upstream PROXY sender.
|
||||
|
||||
- **GET** `/api/v1/server/UniProxy/user`
|
||||
- **Purpose**: Get user data for server
|
||||
- **Returns**: User information for server
|
||||
- **Data**: User access rights, quotas, settings
|
||||
|
||||
- **POST** `/api/v1/server/UniProxy/push`
|
||||
- **Purpose**: Push data to server
|
||||
- **Returns**: Push status
|
||||
- **Data**: Data synchronization confirmation
|
||||
|
||||
- **POST** `/api/v1/server/UniProxy/alive`
|
||||
- **Purpose**: Server heartbeat
|
||||
- **Returns**: Alive status
|
||||
- **Data**: Server health status
|
||||
|
||||
- **GET** `/api/v1/server/UniProxy/alivelist`
|
||||
- **Purpose**: Get alive servers list
|
||||
- **Returns**: List of active servers
|
||||
- **Data**: Server status and availability
|
||||
|
||||
- **POST** `/api/v1/server/UniProxy/status`
|
||||
- **Purpose**: Update server status
|
||||
- **Returns**: Status update confirmation
|
||||
- **Data**: Server status update
|
||||
|
||||
### Shadowsocks Tidalab
|
||||
- **GET** `/api/v1/server/ShadowsocksTidalab/user`
|
||||
- **Purpose**: Get user data for Shadowsocks
|
||||
- **Returns**: Shadowsocks user configuration
|
||||
- **Data**: Shadowsocks-specific user settings
|
||||
|
||||
- **POST** `/api/v1/server/ShadowsocksTidalab/submit`
|
||||
- **Purpose**: Submit Shadowsocks data
|
||||
- **Returns**: Submission status
|
||||
- **Data**: Data submission confirmation
|
||||
|
||||
### Trojan Tidalab
|
||||
- **GET** `/api/v1/server/TrojanTidalab/config`
|
||||
- **Purpose**: Get Trojan server configuration
|
||||
- **Returns**: Trojan configuration
|
||||
- **Data**: Trojan server settings
|
||||
|
||||
- **GET** `/api/v1/server/TrojanTidalab/user`
|
||||
- **Purpose**: Get user data for Trojan
|
||||
- **Returns**: Trojan user configuration
|
||||
- **Data**: Trojan-specific user settings
|
||||
|
||||
- **POST** `/api/v1/server/TrojanTidalab/submit`
|
||||
- **Purpose**: Submit Trojan data
|
||||
- **Returns**: Submission status
|
||||
- **Data**: Data submission confirmation
|
||||
|
||||
---
|
||||
|
||||
## 👨💼 Staff APIs (Staff Authentication)
|
||||
|
||||
**Note**: Staff functionality exists but appears to be integrated into admin routes rather than having dedicated staff routes. Staff users have `is_staff` flag and can access certain admin functions with limited permissions.
|
||||
|
||||
### User Management (Staff Level)
|
||||
- Staff can manage non-admin, non-staff users
|
||||
- Limited user update capabilities
|
||||
- Send emails to users
|
||||
- Ban users
|
||||
- Access user information
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Admin APIs (Administrator Authentication)
|
||||
|
||||
### Configuration Management
|
||||
- **GET** `/api/v2/{admin_path}/config/fetch`
|
||||
- **Purpose**: Get system configuration
|
||||
- **Returns**: Complete system settings
|
||||
- **Data**: All configuration parameters
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/config/save`
|
||||
- **Purpose**: Save system configuration
|
||||
- **Returns**: Save status
|
||||
- **Data**: Configuration update confirmation
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/config/getEmailTemplate`
|
||||
- **Purpose**: Get email templates
|
||||
- **Returns**: Email template configurations
|
||||
- **Data**: Email templates and settings
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/config/getThemeTemplate`
|
||||
- **Purpose**: Get theme templates
|
||||
- **Returns**: Theme configurations
|
||||
- **Data**: Available themes and settings
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/config/setTelegramWebhook`
|
||||
- **Purpose**: Configure Telegram webhook
|
||||
- **Returns**: Webhook setup status
|
||||
- **Data**: Telegram integration status
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/config/testSendMail`
|
||||
- **Purpose**: Test email configuration
|
||||
- **Returns**: Email test results
|
||||
- **Data**: Email sending test status
|
||||
|
||||
### Plan Management
|
||||
- **GET** `/api/v2/{admin_path}/plan/fetch`
|
||||
- **Purpose**: Get all plans (admin view)
|
||||
- **Returns**: Complete plan list with admin details
|
||||
- **Data**: All plans with user counts, revenue, admin settings
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/plan/save`
|
||||
- **Purpose**: Create/update plan
|
||||
- **Returns**: Plan save status
|
||||
- **Data**: Plan creation/update confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/plan/drop`
|
||||
- **Purpose**: Delete plan
|
||||
- **Returns**: Deletion status
|
||||
- **Data**: Plan deletion confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/plan/update`
|
||||
- **Purpose**: Update plan details
|
||||
- **Returns**: Update status
|
||||
- **Data**: Plan update confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/plan/sort`
|
||||
- **Purpose**: Reorder plans
|
||||
- **Returns**: Sort status
|
||||
- **Data**: Plan ordering confirmation
|
||||
|
||||
### Server Management
|
||||
#### Server Groups
|
||||
- **GET** `/api/v2/{admin_path}/server/group/fetch`
|
||||
- **Purpose**: Get server groups
|
||||
- **Returns**: List of server groups
|
||||
- **Data**: Group configurations and permissions
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/server/group/save`
|
||||
- **Purpose**: Create/update server group
|
||||
- **Returns**: Group save status
|
||||
- **Data**: Group creation/update confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/server/group/drop`
|
||||
- **Purpose**: Delete server group
|
||||
- **Returns**: Deletion status
|
||||
- **Data**: Group deletion confirmation
|
||||
|
||||
#### Server Routes
|
||||
- **GET** `/api/v2/{admin_path}/server/route/fetch`
|
||||
- **Purpose**: Get server routes
|
||||
- **Returns**: List of server routes
|
||||
- **Data**: Route configurations and rules
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/server/route/save`
|
||||
- **Purpose**: Create/update server route
|
||||
- **Returns**: Route save status
|
||||
- **Data**: Route creation/update confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/server/route/drop`
|
||||
- **Purpose**: Delete server route
|
||||
- **Returns**: Deletion status
|
||||
- **Data**: Route deletion confirmation
|
||||
|
||||
#### Server Management
|
||||
- **GET** `/api/v2/{admin_path}/server/manage/getNodes`
|
||||
- **Purpose**: Get server nodes
|
||||
- **Returns**: List of server nodes
|
||||
- **Data**: Node details, status, configuration
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/server/manage/update`
|
||||
- **Purpose**: Update server
|
||||
- **Returns**: Update status
|
||||
- **Data**: Server update confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/server/manage/save`
|
||||
- **Purpose**: Create server
|
||||
- **Returns**: Creation status
|
||||
- **Data**: Server creation confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/server/manage/drop`
|
||||
- **Purpose**: Delete server
|
||||
- **Returns**: Deletion status
|
||||
- **Data**: Server deletion confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/server/manage/copy`
|
||||
- **Purpose**: Copy server configuration
|
||||
- **Returns**: Copy status
|
||||
- **Data**: Server copy confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/server/manage/sort`
|
||||
- **Purpose**: Reorder servers
|
||||
- **Returns**: Sort status
|
||||
- **Data**: Server ordering confirmation
|
||||
|
||||
### Order Management
|
||||
- **GET/POST** `/api/v2/{admin_path}/order/fetch`
|
||||
- **Purpose**: Get orders with filtering
|
||||
- **Returns**: Paginated order list
|
||||
- **Data**: Order details, user info, payment status
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/order/update`
|
||||
- **Purpose**: Update order
|
||||
- **Returns**: Update status
|
||||
- **Data**: Order update confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/order/assign`
|
||||
- **Purpose**: Assign order to plan
|
||||
- **Returns**: Assignment status
|
||||
- **Data**: Order assignment confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/order/paid`
|
||||
- **Purpose**: Mark order as paid
|
||||
- **Returns**: Payment status
|
||||
- **Data**: Payment confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/order/cancel`
|
||||
- **Purpose**: Cancel order
|
||||
- **Returns**: Cancellation status
|
||||
- **Data**: Order cancellation confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/order/detail`
|
||||
- **Purpose**: Get order details
|
||||
- **Returns**: Detailed order information
|
||||
- **Data**: Complete order information
|
||||
|
||||
### User Management
|
||||
- **GET/POST** `/api/v2/{admin_path}/user/fetch`
|
||||
- **Purpose**: Get users with filtering and pagination
|
||||
- **Returns**: Paginated user list
|
||||
- **Data**: User details, subscription info, usage stats
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/user/update`
|
||||
- **Purpose**: Update user
|
||||
- **Returns**: Update status
|
||||
- **Data**: User update confirmation
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/user/getUserInfoById`
|
||||
- **Purpose**: Get specific user info
|
||||
- **Returns**: User details
|
||||
- **Data**: Complete user information
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/user/generate`
|
||||
- **Purpose**: Generate user account
|
||||
- **Returns**: Generation status
|
||||
- **Data**: New user account details
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/user/dumpCSV`
|
||||
- **Purpose**: Export users to CSV
|
||||
- **Returns**: CSV export
|
||||
- **Data**: User data in CSV format
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/user/sendMail`
|
||||
- **Purpose**: Send email to users
|
||||
- **Returns**: Email sending status
|
||||
- **Data**: Email delivery confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/user/ban`
|
||||
- **Purpose**: Ban users
|
||||
- **Returns**: Ban status
|
||||
- **Data**: User ban confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/user/resetSecret`
|
||||
- **Purpose**: Reset user secrets
|
||||
- **Returns**: Reset status
|
||||
- **Data**: Secret reset confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/user/setInviteUser`
|
||||
- **Purpose**: Set invite relationships
|
||||
- **Returns**: Setting status
|
||||
- **Data**: Invite relationship confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/user/destroy`
|
||||
- **Purpose**: Delete user
|
||||
- **Returns**: Deletion status
|
||||
- **Data**: User deletion confirmation
|
||||
|
||||
### Statistics & Analytics
|
||||
- **GET** `/api/v2/{admin_path}/stat/getOverride`
|
||||
- **Purpose**: Get system overview
|
||||
- **Returns**: System statistics overview
|
||||
- **Data**: Key metrics, revenue, user counts
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/stat/getStats`
|
||||
- **Purpose**: Get detailed statistics
|
||||
- **Returns**: Comprehensive statistics
|
||||
- **Data**: Revenue, usage, growth metrics
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/stat/getServerLastRank`
|
||||
- **Purpose**: Get server performance ranking
|
||||
- **Returns**: Server ranking data
|
||||
- **Data**: Server performance metrics
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/stat/getServerYesterdayRank`
|
||||
- **Purpose**: Get yesterday's server ranking
|
||||
- **Returns**: Historical server data
|
||||
- **Data**: Previous day server metrics
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/stat/getOrder`
|
||||
- **Purpose**: Get order statistics
|
||||
- **Returns**: Order analytics
|
||||
- **Data**: Order trends, revenue data
|
||||
|
||||
- **GET/POST** `/api/v2/{admin_path}/stat/getStatUser`
|
||||
- **Purpose**: Get user statistics
|
||||
- **Returns**: User analytics
|
||||
- **Data**: User growth, activity metrics
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/stat/getRanking`
|
||||
- **Purpose**: Get ranking data
|
||||
- **Returns**: Various rankings
|
||||
- **Data**: User, server, revenue rankings
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/stat/getStatRecord`
|
||||
- **Purpose**: Get statistical records
|
||||
- **Returns**: Historical statistics
|
||||
- **Data**: Historical data records
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/stat/getTrafficRank`
|
||||
- **Purpose**: Get traffic ranking
|
||||
- **Returns**: Traffic usage rankings
|
||||
- **Data**: Traffic usage by users/servers
|
||||
|
||||
### Notice Management
|
||||
- **GET** `/api/v2/{admin_path}/notice/fetch`
|
||||
- **Purpose**: Get system notices
|
||||
- **Returns**: List of notices
|
||||
- **Data**: Notice content, visibility, scheduling
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/notice/save`
|
||||
- **Purpose**: Create notice
|
||||
- **Returns**: Creation status
|
||||
- **Data**: Notice creation confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/notice/update`
|
||||
- **Purpose**: Update notice
|
||||
- **Returns**: Update status
|
||||
- **Data**: Notice update confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/notice/drop`
|
||||
- **Purpose**: Delete notice
|
||||
- **Returns**: Deletion status
|
||||
- **Data**: Notice deletion confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/notice/show`
|
||||
- **Purpose**: Toggle notice visibility
|
||||
- **Returns**: Visibility status
|
||||
- **Data**: Notice visibility confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/notice/sort`
|
||||
- **Purpose**: Reorder notices
|
||||
- **Returns**: Sort status
|
||||
- **Data**: Notice ordering confirmation
|
||||
|
||||
### Ticket Management
|
||||
- **GET/POST** `/api/v2/{admin_path}/ticket/fetch`
|
||||
- **Purpose**: Get support tickets
|
||||
- **Returns**: Paginated ticket list
|
||||
- **Data**: Ticket details, user info, status
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/ticket/reply`
|
||||
- **Purpose**: Reply to ticket
|
||||
- **Returns**: Reply status
|
||||
- **Data**: Ticket reply confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/ticket/close`
|
||||
- **Purpose**: Close ticket
|
||||
- **Returns**: Closure status
|
||||
- **Data**: Ticket closure confirmation
|
||||
|
||||
### Coupon Management
|
||||
- **GET/POST** `/api/v2/{admin_path}/coupon/fetch`
|
||||
- **Purpose**: Get coupons
|
||||
- **Returns**: List of coupons
|
||||
- **Data**: Coupon details, usage statistics
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/coupon/generate`
|
||||
- **Purpose**: Generate coupons
|
||||
- **Returns**: Generation status
|
||||
- **Data**: New coupon codes
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/coupon/drop`
|
||||
- **Purpose**: Delete coupon
|
||||
- **Returns**: Deletion status
|
||||
- **Data**: Coupon deletion confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/coupon/show`
|
||||
- **Purpose**: Toggle coupon visibility
|
||||
- **Returns**: Visibility status
|
||||
- **Data**: Coupon visibility confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/coupon/update`
|
||||
- **Purpose**: Update coupon
|
||||
- **Returns**: Update status
|
||||
- **Data**: Coupon update confirmation
|
||||
|
||||
### Knowledge Base Management
|
||||
- **GET** `/api/v2/{admin_path}/knowledge/fetch`
|
||||
- **Purpose**: Get knowledge articles
|
||||
- **Returns**: List of articles
|
||||
- **Data**: Article content, categories, visibility
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/knowledge/getCategory`
|
||||
- **Purpose**: Get knowledge categories
|
||||
- **Returns**: Category structure
|
||||
- **Data**: Category hierarchy and organization
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/knowledge/save`
|
||||
- **Purpose**: Create/update article
|
||||
- **Returns**: Save status
|
||||
- **Data**: Article save confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/knowledge/show`
|
||||
- **Purpose**: Toggle article visibility
|
||||
- **Returns**: Visibility status
|
||||
- **Data**: Article visibility confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/knowledge/drop`
|
||||
- **Purpose**: Delete article
|
||||
- **Returns**: Deletion status
|
||||
- **Data**: Article deletion confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/knowledge/sort`
|
||||
- **Purpose**: Reorder articles
|
||||
- **Returns**: Sort status
|
||||
- **Data**: Article ordering confirmation
|
||||
|
||||
### Payment Management
|
||||
- **GET** `/api/v2/{admin_path}/payment/fetch`
|
||||
- **Purpose**: Get payment methods
|
||||
- **Returns**: List of payment providers
|
||||
- **Data**: Payment method configurations
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/payment/getPaymentMethods`
|
||||
- **Purpose**: Get available payment methods
|
||||
- **Returns**: Payment method list
|
||||
- **Data**: Available payment options
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/payment/getPaymentForm`
|
||||
- **Purpose**: Get payment form configuration
|
||||
- **Returns**: Form configuration
|
||||
- **Data**: Payment form settings
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/payment/save`
|
||||
- **Purpose**: Save payment method
|
||||
- **Returns**: Save status
|
||||
- **Data**: Payment method save confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/payment/drop`
|
||||
- **Purpose**: Delete payment method
|
||||
- **Returns**: Deletion status
|
||||
- **Data**: Payment method deletion confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/payment/show`
|
||||
- **Purpose**: Toggle payment method visibility
|
||||
- **Returns**: Visibility status
|
||||
- **Data**: Payment method visibility confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/payment/sort`
|
||||
- **Purpose**: Reorder payment methods
|
||||
- **Returns**: Sort status
|
||||
- **Data**: Payment method ordering confirmation
|
||||
|
||||
### System Management
|
||||
- **GET** `/api/v2/{admin_path}/system/getSystemStatus`
|
||||
- **Purpose**: Get system status
|
||||
- **Returns**: System health information
|
||||
- **Data**: Server status, performance metrics
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/system/getQueueStats`
|
||||
- **Purpose**: Get queue statistics
|
||||
- **Returns**: Queue performance data
|
||||
- **Data**: Queue metrics, job statistics
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/system/getQueueWorkload`
|
||||
- **Purpose**: Get queue workload
|
||||
- **Returns**: Current queue workload
|
||||
- **Data**: Queue load and processing times
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/system/getQueueMasters`
|
||||
- **Purpose**: Get queue masters (Horizon)
|
||||
- **Returns**: Queue master status
|
||||
- **Data**: Horizon supervisor information
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/system/getSystemLog`
|
||||
- **Purpose**: Get system logs
|
||||
- **Returns**: System log entries
|
||||
- **Data**: Application logs, errors, events
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/system/getHorizonFailedJobs`
|
||||
- **Purpose**: Get failed jobs
|
||||
- **Returns**: Failed job list
|
||||
- **Data**: Failed job details and errors
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/system/clearSystemLog`
|
||||
- **Purpose**: Clear system logs
|
||||
- **Returns**: Clear status
|
||||
- **Data**: Log clearing confirmation
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/system/getLogClearStats`
|
||||
- **Purpose**: Get log clearing statistics
|
||||
- **Returns**: Log management stats
|
||||
- **Data**: Log storage and clearing metrics
|
||||
|
||||
### Theme Management
|
||||
- **GET** `/api/v2/{admin_path}/theme/getThemes`
|
||||
- **Purpose**: Get available themes
|
||||
- **Returns**: Theme list
|
||||
- **Data**: Theme details, configurations
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/theme/upload`
|
||||
- **Purpose**: Upload theme
|
||||
- **Returns**: Upload status
|
||||
- **Data**: Theme upload confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/theme/delete`
|
||||
- **Purpose**: Delete theme
|
||||
- **Returns**: Deletion status
|
||||
- **Data**: Theme deletion confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/theme/saveThemeConfig`
|
||||
- **Purpose**: Save theme configuration
|
||||
- **Returns**: Save status
|
||||
- **Data**: Theme configuration save confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/theme/getThemeConfig`
|
||||
- **Purpose**: Get theme configuration
|
||||
- **Returns**: Theme configuration
|
||||
- **Data**: Current theme settings
|
||||
|
||||
### Plugin Management
|
||||
- **GET** `/api/v2/{admin_path}/plugin/getPlugins`
|
||||
- **Purpose**: Get installed plugins
|
||||
- **Returns**: Plugin list
|
||||
- **Data**: Plugin details, status, configuration
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/plugin/upload`
|
||||
- **Purpose**: Upload plugin
|
||||
- **Returns**: Upload status
|
||||
- **Data**: Plugin upload confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/plugin/delete`
|
||||
- **Purpose**: Delete plugin
|
||||
- **Returns**: Deletion status
|
||||
- **Data**: Plugin deletion confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/plugin/install`
|
||||
- **Purpose**: Install plugin
|
||||
- **Returns**: Installation status
|
||||
- **Data**: Plugin installation confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/plugin/uninstall`
|
||||
- **Purpose**: Uninstall plugin
|
||||
- **Returns**: Uninstallation status
|
||||
- **Data**: Plugin uninstallation confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/plugin/enable`
|
||||
- **Purpose**: Enable plugin
|
||||
- **Returns**: Enable status
|
||||
- **Data**: Plugin enable confirmation
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/plugin/disable`
|
||||
- **Purpose**: Disable plugin
|
||||
- **Returns**: Disable status
|
||||
- **Data**: Plugin disable confirmation
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/plugin/config`
|
||||
- **Purpose**: Get plugin configuration
|
||||
- **Returns**: Plugin configuration
|
||||
- **Data**: Plugin settings
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/plugin/config`
|
||||
- **Purpose**: Update plugin configuration
|
||||
- **Returns**: Update status
|
||||
- **Data**: Plugin configuration update confirmation
|
||||
|
||||
### Traffic Reset Management
|
||||
- **GET** `/api/v2/{admin_path}/traffic-reset/logs`
|
||||
- **Purpose**: Get traffic reset logs
|
||||
- **Returns**: Reset log entries
|
||||
- **Data**: Traffic reset history and details
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/traffic-reset/stats`
|
||||
- **Purpose**: Get traffic reset statistics
|
||||
- **Returns**: Reset statistics
|
||||
- **Data**: Traffic reset metrics and trends
|
||||
|
||||
- **GET** `/api/v2/{admin_path}/traffic-reset/user/{userId}/history`
|
||||
- **Purpose**: Get user traffic reset history
|
||||
- **Returns**: User-specific reset history
|
||||
- **Data**: Individual user reset records
|
||||
|
||||
- **POST** `/api/v2/{admin_path}/traffic-reset/reset-user`
|
||||
- **Purpose**: Reset user traffic
|
||||
- **Returns**: Reset status
|
||||
- **Data**: User traffic reset confirmation
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
1. **Admin Path**: The `{admin_path}` in V2 admin routes is dynamically generated based on the `secure_path` or `frontend_admin_path` configuration setting.
|
||||
|
||||
2. **Authentication Middleware**:
|
||||
- `user`: Requires valid user authentication
|
||||
- `admin`: Requires admin privileges
|
||||
- `staff`: Requires staff privileges
|
||||
- `client`: Requires client token authentication
|
||||
- `server`: Requires server authentication
|
||||
|
||||
3. **Response Format**: Most endpoints return data in a standardized format:
|
||||
```json
|
||||
{
|
||||
"data": [...], // Actual response data
|
||||
"message": "Success message",
|
||||
"status": true/false
|
||||
}
|
||||
```
|
||||
|
||||
4. **Error Handling**: Failed requests return appropriate HTTP status codes with error messages.
|
||||
|
||||
5. **Pagination**: Many list endpoints support pagination with `current` and `pageSize` parameters.
|
||||
|
||||
6. **Filtering**: Admin endpoints often support filtering and sorting parameters.
|
||||
|
||||
This documentation provides a comprehensive overview of all available API endpoints in the Xboard system. Each endpoint serves specific functionality within the VPN service management platform.
|
||||
191
box.go
191
box.go
@@ -2,12 +2,13 @@ package box
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxCertificate "github.com/sagernet/sing-box/adapter/certificate"
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
@@ -35,21 +36,20 @@ import (
|
||||
var _ adapter.SimpleLifecycle = (*Box)(nil)
|
||||
|
||||
type Box struct {
|
||||
createdAt time.Time
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
network *route.NetworkManager
|
||||
endpoint *endpoint.Manager
|
||||
inbound *inbound.Manager
|
||||
outbound *outbound.Manager
|
||||
service *boxService.Manager
|
||||
certificateProvider *boxCertificate.Manager
|
||||
dnsTransport *dns.TransportManager
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
internalService []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
createdAt time.Time
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
network *route.NetworkManager
|
||||
endpoint *endpoint.Manager
|
||||
inbound *inbound.Manager
|
||||
outbound *outbound.Manager
|
||||
service *boxService.Manager
|
||||
dnsTransport *dns.TransportManager
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
internalService []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
@@ -65,7 +65,6 @@ func Context(
|
||||
endpointRegistry adapter.EndpointRegistry,
|
||||
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||
serviceRegistry adapter.ServiceRegistry,
|
||||
certificateProviderRegistry adapter.CertificateProviderRegistry,
|
||||
) context.Context {
|
||||
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
||||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
||||
@@ -90,10 +89,6 @@ func Context(
|
||||
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
|
||||
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
|
||||
}
|
||||
if service.FromContext[adapter.CertificateProviderRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.CertificateProviderOptionsRegistry](ctx, certificateProviderRegistry)
|
||||
ctx = service.ContextWith[adapter.CertificateProviderRegistry](ctx, certificateProviderRegistry)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -110,7 +105,6 @@ func New(options Options) (*Box, error) {
|
||||
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
||||
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
|
||||
certificateProviderRegistry := service.FromContext[adapter.CertificateProviderRegistry](ctx)
|
||||
|
||||
if endpointRegistry == nil {
|
||||
return nil, E.New("missing endpoint registry in context")
|
||||
@@ -127,9 +121,6 @@ func New(options Options) (*Box, error) {
|
||||
if serviceRegistry == nil {
|
||||
return nil, E.New("missing service registry in context")
|
||||
}
|
||||
if certificateProviderRegistry == nil {
|
||||
return nil, E.New("missing certificate provider registry in context")
|
||||
}
|
||||
|
||||
ctx = pause.WithDefaultManager(ctx)
|
||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||
@@ -187,19 +178,13 @@ func New(options Options) (*Box, error) {
|
||||
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)
|
||||
certificateProviderManager := boxCertificate.NewManager(logFactory.NewLogger("certificate-provider"), certificateProviderRegistry)
|
||||
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)
|
||||
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
|
||||
dnsRouter, err := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize DNS router")
|
||||
}
|
||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
|
||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize network manager")
|
||||
@@ -248,8 +233,15 @@ func New(options Options) (*Box, error) {
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
endpointCtx := ctx
|
||||
if tag != "" {
|
||||
// TODO: remove this
|
||||
endpointCtx = adapter.WithContext(endpointCtx, &adapter.InboundContext{
|
||||
Outbound: tag,
|
||||
})
|
||||
}
|
||||
err = endpointManager.Create(
|
||||
ctx,
|
||||
endpointCtx,
|
||||
router,
|
||||
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
@@ -279,6 +271,32 @@ func New(options Options) (*Box, error) {
|
||||
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
||||
}
|
||||
}
|
||||
for i, outboundOptions := range options.Outbounds {
|
||||
var tag string
|
||||
if outboundOptions.Tag != "" {
|
||||
tag = outboundOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
outboundCtx := ctx
|
||||
if tag != "" {
|
||||
// TODO: remove this
|
||||
outboundCtx = adapter.WithContext(outboundCtx, &adapter.InboundContext{
|
||||
Outbound: tag,
|
||||
})
|
||||
}
|
||||
err = outboundManager.Create(
|
||||
outboundCtx,
|
||||
router,
|
||||
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
outboundOptions.Type,
|
||||
outboundOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
||||
}
|
||||
}
|
||||
for i, serviceOptions := range options.Services {
|
||||
var tag string
|
||||
if serviceOptions.Tag != "" {
|
||||
@@ -297,43 +315,6 @@ func New(options Options) (*Box, error) {
|
||||
return nil, E.Cause(err, "initialize service[", i, "]")
|
||||
}
|
||||
}
|
||||
for i, outboundOptions := range options.Outbounds {
|
||||
var tag string
|
||||
if outboundOptions.Tag != "" {
|
||||
tag = outboundOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = outboundManager.Create(
|
||||
ctx,
|
||||
router,
|
||||
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
outboundOptions.Type,
|
||||
outboundOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
||||
}
|
||||
}
|
||||
for i, certificateProviderOptions := range options.CertificateProviders {
|
||||
var tag string
|
||||
if certificateProviderOptions.Tag != "" {
|
||||
tag = certificateProviderOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = certificateProviderManager.Create(
|
||||
ctx,
|
||||
logFactory.NewLogger(F.ToString("certificate-provider/", certificateProviderOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
certificateProviderOptions.Type,
|
||||
certificateProviderOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize certificate provider[", i, "]")
|
||||
}
|
||||
}
|
||||
outboundManager.Initialize(func() (adapter.Outbound, error) {
|
||||
return direct.NewOutbound(
|
||||
ctx,
|
||||
@@ -359,7 +340,7 @@ func New(options Options) (*Box, error) {
|
||||
}
|
||||
}
|
||||
if needCacheFile {
|
||||
cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||
internalServices = append(internalServices, cacheFile)
|
||||
}
|
||||
@@ -402,27 +383,35 @@ func New(options Options) (*Box, error) {
|
||||
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||
}
|
||||
return &Box{
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
inbound: inboundManager,
|
||||
outbound: outboundManager,
|
||||
dnsTransport: dnsTransportManager,
|
||||
service: serviceManager,
|
||||
certificateProvider: certificateProviderManager,
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
internalService: internalServices,
|
||||
done: make(chan struct{}),
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
inbound: inboundManager,
|
||||
outbound: outboundManager,
|
||||
dnsTransport: dnsTransportManager,
|
||||
service: serviceManager,
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
internalService: internalServices,
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Box) PreStart() error {
|
||||
err := s.preStart()
|
||||
if err != nil {
|
||||
// TODO: remove catch error
|
||||
defer func() {
|
||||
v := recover()
|
||||
if v != nil {
|
||||
println(err.Error())
|
||||
debug.PrintStack()
|
||||
panic("panic on early close: " + fmt.Sprint(v))
|
||||
}
|
||||
}()
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
@@ -433,6 +422,15 @@ func (s *Box) PreStart() error {
|
||||
func (s *Box) Start() error {
|
||||
err := s.start()
|
||||
if err != nil {
|
||||
// TODO: remove catch error
|
||||
defer func() {
|
||||
v := recover()
|
||||
if v != nil {
|
||||
println(err.Error())
|
||||
debug.PrintStack()
|
||||
println("panic on early start: " + fmt.Sprint(v))
|
||||
}
|
||||
}()
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
@@ -452,11 +450,11 @@ func (s *Box) preStart() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service, s.certificateProvider)
|
||||
err = adapter.Start(s.logger, 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(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection, s.router, s.dnsRouter)
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -472,19 +470,11 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.endpoint)
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.certificateProvider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.endpoint, s.certificateProvider, s.inbound, s.service)
|
||||
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -492,7 +482,7 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.endpoint, s.certificateProvider, s.inbound, s.service)
|
||||
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -516,9 +506,8 @@ func (s *Box) Close() error {
|
||||
service adapter.Lifecycle
|
||||
}{
|
||||
{"service", s.service},
|
||||
{"inbound", s.inbound},
|
||||
{"certificate-provider", s.certificateProvider},
|
||||
{"endpoint", s.endpoint},
|
||||
{"inbound", s.inbound},
|
||||
{"outbound", s.outbound},
|
||||
{"router", s.router},
|
||||
{"connection", s.connection},
|
||||
|
||||
@@ -15,7 +15,8 @@ NC='\033[0m'
|
||||
CONFIG_DIR="/etc/sing-box"
|
||||
CONFIG_MERGE_DIR="$CONFIG_DIR/config.d"
|
||||
CONFIG_BASE_FILE="$CONFIG_MERGE_DIR/10-base.json"
|
||||
CONFIG_OUTBOUNDS_FILE="$CONFIG_MERGE_DIR/20-outbounds.json"
|
||||
CONFIG_ROUTE_FILE="$CONFIG_MERGE_DIR/route.json"
|
||||
CONFIG_OUTBOUNDS_FILE="$CONFIG_MERGE_DIR/outbound.json"
|
||||
WORK_DIR="/var/lib/sing-box"
|
||||
BINARY_PATH="/usr/local/bin/sing-box"
|
||||
SERVICE_NAME="singbox"
|
||||
@@ -332,7 +333,12 @@ ${DNS_SERVER_JSON}
|
||||
"services": [
|
||||
${SERVICE_JSON}
|
||||
],
|
||||
"inbounds": [],
|
||||
"inbounds": []
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > "$CONFIG_ROUTE_FILE" <<EOF
|
||||
{
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
@@ -357,6 +363,7 @@ cat > "$CONFIG_OUTBOUNDS_FILE" <<EOF
|
||||
EOF
|
||||
|
||||
echo -e "${GREEN}Base configuration written to $CONFIG_BASE_FILE${NC}"
|
||||
echo -e "${GREEN}Route configuration written to $CONFIG_ROUTE_FILE${NC}"
|
||||
echo -e "${GREEN}Outbound configuration written to $CONFIG_OUTBOUNDS_FILE${NC}"
|
||||
echo -e "${YELLOW}Edit $CONFIG_OUTBOUNDS_FILE when adding custom sing-box outbounds.${NC}"
|
||||
|
||||
|
||||
1
clients/android
Submodule
1
clients/android
Submodule
Submodule clients/android added at ab09918615
1
clients/apple
Submodule
1
clients/apple
Submodule
Submodule clients/apple added at ad7434d676
@@ -82,11 +82,6 @@ func compileRuleSet(sourcePath string) error {
|
||||
}
|
||||
|
||||
func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 {
|
||||
if version == C.RuleSetVersion5 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
||||
return len(rule.PackageNameRegex) > 0
|
||||
}) {
|
||||
version = C.RuleSetVersion4
|
||||
}
|
||||
if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
||||
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
|
||||
len(rule.DefaultInterfaceAddress) > 0
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/networkquality"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
commandNetworkQualityFlagConfigURL string
|
||||
commandNetworkQualityFlagSerial bool
|
||||
commandNetworkQualityFlagMaxRuntime int
|
||||
commandNetworkQualityFlagHTTP3 bool
|
||||
)
|
||||
|
||||
var commandNetworkQuality = &cobra.Command{
|
||||
Use: "networkquality",
|
||||
Short: "Run a network quality test",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := runNetworkQuality()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandNetworkQuality.Flags().StringVar(
|
||||
&commandNetworkQualityFlagConfigURL,
|
||||
"config-url", "",
|
||||
"Network quality test config URL (default: Apple mensura)",
|
||||
)
|
||||
commandNetworkQuality.Flags().BoolVar(
|
||||
&commandNetworkQualityFlagSerial,
|
||||
"serial", false,
|
||||
"Run download and upload tests sequentially instead of in parallel",
|
||||
)
|
||||
commandNetworkQuality.Flags().IntVar(
|
||||
&commandNetworkQualityFlagMaxRuntime,
|
||||
"max-runtime", int(networkquality.DefaultMaxRuntime/time.Second),
|
||||
"Network quality maximum runtime in seconds",
|
||||
)
|
||||
commandNetworkQuality.Flags().BoolVar(
|
||||
&commandNetworkQualityFlagHTTP3,
|
||||
"http3", false,
|
||||
"Use HTTP/3 (QUIC) for measurement traffic",
|
||||
)
|
||||
commandTools.AddCommand(commandNetworkQuality)
|
||||
}
|
||||
|
||||
func runNetworkQuality() error {
|
||||
instance, err := createPreStartedClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer instance.Close()
|
||||
|
||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpClient := networkquality.NewHTTPClient(dialer)
|
||||
defer httpClient.CloseIdleConnections()
|
||||
|
||||
measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(dialer, commandNetworkQualityFlagHTTP3)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "==== NETWORK QUALITY TEST ====")
|
||||
|
||||
result, err := networkquality.Run(networkquality.Options{
|
||||
ConfigURL: commandNetworkQualityFlagConfigURL,
|
||||
HTTPClient: httpClient,
|
||||
NewMeasurementClient: measurementClientFactory,
|
||||
Serial: commandNetworkQualityFlagSerial,
|
||||
MaxRuntime: time.Duration(commandNetworkQualityFlagMaxRuntime) * time.Second,
|
||||
Context: globalCtx,
|
||||
OnProgress: func(p networkquality.Progress) {
|
||||
if !commandNetworkQualityFlagSerial && p.Phase != networkquality.PhaseIdle {
|
||||
fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d Upload: %s RPM: %d",
|
||||
networkquality.FormatBitrate(p.DownloadCapacity), p.DownloadRPM,
|
||||
networkquality.FormatBitrate(p.UploadCapacity), p.UploadRPM)
|
||||
return
|
||||
}
|
||||
switch networkquality.Phase(p.Phase) {
|
||||
case networkquality.PhaseIdle:
|
||||
if p.IdleLatencyMs > 0 {
|
||||
fmt.Fprintf(os.Stderr, "\rIdle Latency: %d ms", p.IdleLatencyMs)
|
||||
} else {
|
||||
fmt.Fprint(os.Stderr, "\rMeasuring idle latency...")
|
||||
}
|
||||
case networkquality.PhaseDownload:
|
||||
fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d",
|
||||
networkquality.FormatBitrate(p.DownloadCapacity), p.DownloadRPM)
|
||||
case networkquality.PhaseUpload:
|
||||
fmt.Fprintf(os.Stderr, "\rUpload: %s RPM: %d",
|
||||
networkquality.FormatBitrate(p.UploadCapacity), p.UploadRPM)
|
||||
}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr)
|
||||
fmt.Fprintln(os.Stderr, strings.Repeat("-", 40))
|
||||
fmt.Fprintf(os.Stderr, "Idle Latency: %d ms\n", result.IdleLatencyMs)
|
||||
fmt.Fprintf(os.Stderr, "Download Capacity: %-20s Accuracy: %s\n", networkquality.FormatBitrate(result.DownloadCapacity), result.DownloadCapacityAccuracy)
|
||||
fmt.Fprintf(os.Stderr, "Upload Capacity: %-20s Accuracy: %s\n", networkquality.FormatBitrate(result.UploadCapacity), result.UploadCapacityAccuracy)
|
||||
fmt.Fprintf(os.Stderr, "Download Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.DownloadRPM), result.DownloadRPMAccuracy)
|
||||
fmt.Fprintf(os.Stderr, "Upload Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.UploadRPM), result.UploadRPMAccuracy)
|
||||
return nil
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/common/stun"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandSTUNFlagServer string
|
||||
|
||||
var commandSTUN = &cobra.Command{
|
||||
Use: "stun",
|
||||
Short: "Run a STUN test",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := runSTUN()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandSTUN.Flags().StringVarP(&commandSTUNFlagServer, "server", "s", stun.DefaultServer, "STUN server address")
|
||||
commandTools.AddCommand(commandSTUN)
|
||||
}
|
||||
|
||||
func runSTUN() error {
|
||||
instance, err := createPreStartedClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer instance.Close()
|
||||
|
||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "==== STUN TEST ====")
|
||||
|
||||
result, err := stun.Run(stun.Options{
|
||||
Server: commandSTUNFlagServer,
|
||||
Dialer: dialer,
|
||||
Context: globalCtx,
|
||||
OnProgress: func(p stun.Progress) {
|
||||
switch p.Phase {
|
||||
case stun.PhaseBinding:
|
||||
if p.ExternalAddr != "" {
|
||||
fmt.Fprintf(os.Stderr, "\rExternal Address: %s (%d ms)", p.ExternalAddr, p.LatencyMs)
|
||||
} else {
|
||||
fmt.Fprint(os.Stderr, "\rSending binding request...")
|
||||
}
|
||||
case stun.PhaseNATMapping:
|
||||
fmt.Fprint(os.Stderr, "\rDetecting NAT mapping behavior...")
|
||||
case stun.PhaseNATFiltering:
|
||||
fmt.Fprint(os.Stderr, "\rDetecting NAT filtering behavior...")
|
||||
}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr)
|
||||
fmt.Fprintf(os.Stderr, "External Address: %s\n", result.ExternalAddr)
|
||||
fmt.Fprintf(os.Stderr, "Latency: %d ms\n", result.LatencyMs)
|
||||
if result.NATTypeSupported {
|
||||
fmt.Fprintf(os.Stderr, "NAT Mapping: %s\n", result.NATMapping)
|
||||
fmt.Fprintf(os.Stderr, "NAT Filtering: %s\n", result.NATFiltering)
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "NAT Type Detection: not supported by server")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -87,12 +87,11 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
}
|
||||
server = dialOptions.DomainResolver.Server
|
||||
dnsQueryOptions = adapter.DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: strategy,
|
||||
DisableCache: dialOptions.DomainResolver.DisableCache,
|
||||
DisableOptimisticCache: dialOptions.DomainResolver.DisableOptimisticCache,
|
||||
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
||||
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||
Transport: transport,
|
||||
Strategy: strategy,
|
||||
DisableCache: dialOptions.DomainResolver.DisableCache,
|
||||
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
||||
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||
}
|
||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||
} else if options.DirectResolver {
|
||||
|
||||
@@ -44,8 +44,8 @@ type createRecordResponse struct {
|
||||
}
|
||||
|
||||
type listRecordsResponse struct {
|
||||
Status apiStatus `json:"status"`
|
||||
Records []apiRecord `json:"records"`
|
||||
Status apiStatus `json:"status"`
|
||||
Records []apiRecord `json:"records"`
|
||||
}
|
||||
|
||||
func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
|
||||
|
||||
@@ -21,6 +21,10 @@ import (
|
||||
)
|
||||
|
||||
func (l *Listener) ListenTCP() (net.Listener, error) {
|
||||
//nolint:staticcheck
|
||||
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
|
||||
return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
|
||||
}
|
||||
var err error
|
||||
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
|
||||
var listenConfig net.ListenConfig
|
||||
@@ -96,18 +100,6 @@ func (l *Listener) loopTCPIn() {
|
||||
l.logger.Error("tcp listener closed: ", err)
|
||||
continue
|
||||
}
|
||||
remoteAddr := conn.RemoteAddr()
|
||||
//nolint:staticcheck
|
||||
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
|
||||
//nolint:staticcheck
|
||||
wrappedConn, wrapErr := wrapProxyProtocolConn(conn, l.listenOptions.ProxyProtocolAcceptNoHeader)
|
||||
if wrapErr != nil {
|
||||
conn.Close()
|
||||
l.logger.Error("process connection from ", remoteAddr, ": PROXY protocol: ", wrapErr)
|
||||
continue
|
||||
}
|
||||
conn = wrappedConn
|
||||
}
|
||||
//nolint:staticcheck
|
||||
metadata.InboundDetour = l.listenOptions.Detour
|
||||
metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap()
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
var errProxyProtocolHeaderNotPresent = errors.New("proxy protocol header not present")
|
||||
|
||||
var proxyProtocolV2Signature = []byte{
|
||||
0x0d, 0x0a, 0x0d, 0x0a,
|
||||
0x00, 0x0d, 0x0a, 0x51,
|
||||
0x55, 0x49, 0x54, 0x0a,
|
||||
}
|
||||
|
||||
type proxyProtocolConn struct {
|
||||
net.Conn
|
||||
reader *bufio.Reader
|
||||
remoteAddr net.Addr
|
||||
}
|
||||
|
||||
func (c *proxyProtocolConn) Read(p []byte) (int, error) {
|
||||
return c.reader.Read(p)
|
||||
}
|
||||
|
||||
func (c *proxyProtocolConn) RemoteAddr() net.Addr {
|
||||
return c.remoteAddr
|
||||
}
|
||||
|
||||
func wrapProxyProtocolConn(conn net.Conn, allowNoHeader bool) (net.Conn, error) {
|
||||
reader := bufio.NewReader(conn)
|
||||
remoteAddr, err := readProxyProtocolRemoteAddr(reader)
|
||||
if err != nil {
|
||||
if allowNoHeader && errors.Is(err, errProxyProtocolHeaderNotPresent) {
|
||||
return &proxyProtocolConn{
|
||||
Conn: conn,
|
||||
reader: reader,
|
||||
remoteAddr: conn.RemoteAddr(),
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if remoteAddr == nil {
|
||||
remoteAddr = conn.RemoteAddr()
|
||||
}
|
||||
return &proxyProtocolConn{
|
||||
Conn: conn,
|
||||
reader: reader,
|
||||
remoteAddr: remoteAddr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readProxyProtocolRemoteAddr(reader *bufio.Reader) (net.Addr, error) {
|
||||
firstByte, err := reader.Peek(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch firstByte[0] {
|
||||
case 'P':
|
||||
return readProxyProtocolV1RemoteAddr(reader)
|
||||
case '\r':
|
||||
signature, err := reader.Peek(len(proxyProtocolV2Signature))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !bytes.Equal(signature, proxyProtocolV2Signature) {
|
||||
return nil, errProxyProtocolHeaderNotPresent
|
||||
}
|
||||
return readProxyProtocolV2RemoteAddr(reader)
|
||||
default:
|
||||
return nil, errProxyProtocolHeaderNotPresent
|
||||
}
|
||||
}
|
||||
|
||||
func readProxyProtocolV1RemoteAddr(reader *bufio.Reader) (net.Addr, error) {
|
||||
prefix, err := reader.Peek(6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !bytes.Equal(prefix, []byte("PROXY ")) {
|
||||
return nil, errProxyProtocolHeaderNotPresent
|
||||
}
|
||||
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) < 2 || line[len(line)-2:] != "\r\n" {
|
||||
return nil, fmt.Errorf("invalid PROXY protocol v1 line ending")
|
||||
}
|
||||
|
||||
fields := bytes.Fields([]byte(line[:len(line)-2]))
|
||||
if len(fields) < 2 {
|
||||
return nil, fmt.Errorf("invalid PROXY protocol v1 header")
|
||||
}
|
||||
if string(fields[1]) == "UNKNOWN" {
|
||||
return nil, nil
|
||||
}
|
||||
if len(fields) != 6 {
|
||||
return nil, fmt.Errorf("invalid PROXY protocol v1 field count")
|
||||
}
|
||||
|
||||
sourceIP := net.ParseIP(string(fields[2]))
|
||||
if sourceIP == nil {
|
||||
return nil, fmt.Errorf("invalid PROXY protocol source ip")
|
||||
}
|
||||
sourcePort, err := parseProxyProtocolPort(fields[4])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &net.TCPAddr{
|
||||
IP: sourceIP,
|
||||
Port: sourcePort,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readProxyProtocolV2RemoteAddr(reader *bufio.Reader) (net.Addr, error) {
|
||||
header := make([]byte, 16)
|
||||
if _, err := io.ReadFull(reader, header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !bytes.Equal(header[:12], proxyProtocolV2Signature) {
|
||||
return nil, errProxyProtocolHeaderNotPresent
|
||||
}
|
||||
|
||||
version := header[12] >> 4
|
||||
command := header[12] & 0x0f
|
||||
if version != 0x2 {
|
||||
return nil, fmt.Errorf("invalid PROXY protocol v2 version")
|
||||
}
|
||||
|
||||
addressDataLen := int(binary.BigEndian.Uint16(header[14:16]))
|
||||
addressData := make([]byte, addressDataLen)
|
||||
if _, err := io.ReadFull(reader, addressData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if command == 0x0 {
|
||||
return nil, nil
|
||||
}
|
||||
if command != 0x1 {
|
||||
return nil, fmt.Errorf("unsupported PROXY protocol v2 command")
|
||||
}
|
||||
|
||||
switch header[13] {
|
||||
case 0x11, 0x12:
|
||||
if len(addressData) < 12 {
|
||||
return nil, fmt.Errorf("short PROXY protocol v2 ipv4 header")
|
||||
}
|
||||
return &net.TCPAddr{
|
||||
IP: net.IP(addressData[:4]),
|
||||
Port: int(binary.BigEndian.Uint16(addressData[8:10])),
|
||||
}, nil
|
||||
case 0x21, 0x22:
|
||||
if len(addressData) < 36 {
|
||||
return nil, fmt.Errorf("short PROXY protocol v2 ipv6 header")
|
||||
}
|
||||
return &net.TCPAddr{
|
||||
IP: net.IP(addressData[:16]),
|
||||
Port: int(binary.BigEndian.Uint16(addressData[32:34])),
|
||||
}, nil
|
||||
case 0x31, 0x32:
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported PROXY protocol v2 family")
|
||||
}
|
||||
}
|
||||
|
||||
func parseProxyProtocolPort(raw []byte) (int, error) {
|
||||
port := 0
|
||||
for _, ch := range raw {
|
||||
if ch < '0' || ch > '9' {
|
||||
return 0, fmt.Errorf("invalid PROXY protocol port")
|
||||
}
|
||||
port = port*10 + int(ch-'0')
|
||||
if port > 65535 {
|
||||
return 0, fmt.Errorf("invalid PROXY protocol port")
|
||||
}
|
||||
}
|
||||
return port, nil
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestReadProxyProtocolV1RemoteAddr(t *testing.T) {
|
||||
reader := bufio.NewReaderSize(newStaticConn("PROXY TCP4 203.0.113.10 192.0.2.1 45678 443\r\npayload"), 128)
|
||||
remoteAddr, err := readProxyProtocolRemoteAddr(reader)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tcpAddr, ok := remoteAddr.(*net.TCPAddr)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected addr type: %T", remoteAddr)
|
||||
}
|
||||
if got := tcpAddr.IP.String(); got != "203.0.113.10" {
|
||||
t.Fatalf("unexpected ip: %s", got)
|
||||
}
|
||||
if tcpAddr.Port != 45678 {
|
||||
t.Fatalf("unexpected port: %d", tcpAddr.Port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadProxyProtocolV2RemoteAddr(t *testing.T) {
|
||||
header := make([]byte, 28)
|
||||
copy(header[:12], proxyProtocolV2Signature)
|
||||
header[12] = 0x21
|
||||
header[13] = 0x11
|
||||
binary.BigEndian.PutUint16(header[14:16], 12)
|
||||
copy(header[16:20], net.ParseIP("198.51.100.12").To4())
|
||||
copy(header[20:24], net.ParseIP("192.0.2.8").To4())
|
||||
binary.BigEndian.PutUint16(header[24:26], 50000)
|
||||
binary.BigEndian.PutUint16(header[26:28], 443)
|
||||
|
||||
reader := bufio.NewReaderSize(newStaticConn(string(header)+"payload"), 128)
|
||||
remoteAddr, err := readProxyProtocolRemoteAddr(reader)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tcpAddr, ok := remoteAddr.(*net.TCPAddr)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected addr type: %T", remoteAddr)
|
||||
}
|
||||
if got := tcpAddr.IP.String(); got != "198.51.100.12" {
|
||||
t.Fatalf("unexpected ip: %s", got)
|
||||
}
|
||||
if tcpAddr.Port != 50000 {
|
||||
t.Fatalf("unexpected port: %d", tcpAddr.Port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapProxyProtocolConnAllowNoHeader(t *testing.T) {
|
||||
rawConn := newStaticConn("hello")
|
||||
conn, err := wrapProxyProtocolConn(rawConn, true)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if conn.RemoteAddr().String() != rawConn.RemoteAddr().String() {
|
||||
t.Fatalf("remote addr changed unexpectedly: %s", conn.RemoteAddr())
|
||||
}
|
||||
}
|
||||
|
||||
type staticConn struct {
|
||||
net.Conn
|
||||
reader *bufio.Reader
|
||||
local net.Addr
|
||||
remote net.Addr
|
||||
}
|
||||
|
||||
func newStaticConn(payload string) *staticConn {
|
||||
return &staticConn{
|
||||
reader: bufio.NewReaderSize(strings.NewReader(payload), len(payload)+16),
|
||||
local: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 443},
|
||||
remote: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 12345},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *staticConn) Read(p []byte) (int, error) {
|
||||
return c.reader.Read(p)
|
||||
}
|
||||
|
||||
func (c *staticConn) Write(p []byte) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (c *staticConn) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *staticConn) LocalAddr() net.Addr {
|
||||
return c.local
|
||||
}
|
||||
|
||||
func (c *staticConn) RemoteAddr() net.Addr {
|
||||
return c.remote
|
||||
}
|
||||
|
||||
func (c *staticConn) SetDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *staticConn) SetReadDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *staticConn) SetWriteDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
package networkquality
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
sBufio "github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func FormatBitrate(bps int64) string {
|
||||
switch {
|
||||
case bps >= 1_000_000_000:
|
||||
return fmt.Sprintf("%.1f Gbps", float64(bps)/1_000_000_000)
|
||||
case bps >= 1_000_000:
|
||||
return fmt.Sprintf("%.1f Mbps", float64(bps)/1_000_000)
|
||||
case bps >= 1_000:
|
||||
return fmt.Sprintf("%.1f Kbps", float64(bps)/1_000)
|
||||
default:
|
||||
return fmt.Sprintf("%d bps", bps)
|
||||
}
|
||||
}
|
||||
|
||||
func NewHTTPClient(dialer N.Dialer) *http.Client {
|
||||
transport := &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
TLSHandshakeTimeout: C.TCPTimeout,
|
||||
}
|
||||
if dialer != nil {
|
||||
transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
}
|
||||
}
|
||||
return &http.Client{Transport: transport}
|
||||
}
|
||||
|
||||
func baseTransportFromClient(client *http.Client) (*http.Transport, error) {
|
||||
if client == nil {
|
||||
return nil, E.New("http client is nil")
|
||||
}
|
||||
if client.Transport == nil {
|
||||
return http.DefaultTransport.(*http.Transport).Clone(), nil
|
||||
}
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
if !ok {
|
||||
return nil, E.New("http client transport must be *http.Transport")
|
||||
}
|
||||
return transport.Clone(), nil
|
||||
}
|
||||
|
||||
func newMeasurementClient(
|
||||
baseClient *http.Client,
|
||||
connectEndpoint string,
|
||||
singleConnection bool,
|
||||
disableKeepAlives bool,
|
||||
readCounters []N.CountFunc,
|
||||
writeCounters []N.CountFunc,
|
||||
) (*http.Client, error) {
|
||||
transport, err := baseTransportFromClient(baseClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transport.DisableCompression = true
|
||||
transport.DisableKeepAlives = disableKeepAlives
|
||||
if singleConnection {
|
||||
transport.MaxConnsPerHost = 1
|
||||
transport.MaxIdleConnsPerHost = 1
|
||||
transport.MaxIdleConns = 1
|
||||
}
|
||||
|
||||
baseDialContext := transport.DialContext
|
||||
if baseDialContext == nil {
|
||||
dialer := &net.Dialer{}
|
||||
baseDialContext = dialer.DialContext
|
||||
}
|
||||
transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) {
|
||||
dialAddr := addr
|
||||
if connectEndpoint != "" {
|
||||
dialAddr = rewriteDialAddress(addr, connectEndpoint)
|
||||
}
|
||||
conn, dialErr := baseDialContext(ctx, network, dialAddr)
|
||||
if dialErr != nil {
|
||||
return nil, dialErr
|
||||
}
|
||||
if len(readCounters) > 0 || len(writeCounters) > 0 {
|
||||
return sBufio.NewCounterConn(conn, readCounters, writeCounters), nil
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
CheckRedirect: baseClient.CheckRedirect,
|
||||
Jar: baseClient.Jar,
|
||||
Timeout: baseClient.Timeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type MeasurementClientFactory func(
|
||||
connectEndpoint string,
|
||||
singleConnection bool,
|
||||
disableKeepAlives bool,
|
||||
readCounters []N.CountFunc,
|
||||
writeCounters []N.CountFunc,
|
||||
) (*http.Client, error)
|
||||
|
||||
func defaultMeasurementClientFactory(baseClient *http.Client) MeasurementClientFactory {
|
||||
return func(connectEndpoint string, singleConnection, disableKeepAlives bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) {
|
||||
return newMeasurementClient(baseClient, connectEndpoint, singleConnection, disableKeepAlives, readCounters, writeCounters)
|
||||
}
|
||||
}
|
||||
|
||||
func NewOptionalHTTP3Factory(dialer N.Dialer, useHTTP3 bool) (MeasurementClientFactory, error) {
|
||||
if !useHTTP3 {
|
||||
return nil, nil
|
||||
}
|
||||
return NewHTTP3MeasurementClientFactory(dialer)
|
||||
}
|
||||
|
||||
func rewriteDialAddress(addr string, connectEndpoint string) string {
|
||||
connectEndpoint = strings.TrimSpace(connectEndpoint)
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return addr
|
||||
}
|
||||
endpointHost, endpointPort, err := net.SplitHostPort(connectEndpoint)
|
||||
if err == nil {
|
||||
host = endpointHost
|
||||
if endpointPort != "" {
|
||||
port = endpointPort
|
||||
}
|
||||
} else if connectEndpoint != "" {
|
||||
host = connectEndpoint
|
||||
}
|
||||
return net.JoinHostPort(host, port)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
//go:build with_quic
|
||||
|
||||
package networkquality
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
sBufio "github.com/sagernet/sing/common/bufio"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) {
|
||||
// singleConnection and disableKeepAlives are not applied:
|
||||
// HTTP/3 multiplexes streams over a single QUIC connection by default.
|
||||
return func(connectEndpoint string, _, _ bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) {
|
||||
transport := &http3.Transport{
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
||||
dialAddr := addr
|
||||
if connectEndpoint != "" {
|
||||
dialAddr = rewriteDialAddress(addr, connectEndpoint)
|
||||
}
|
||||
destination := M.ParseSocksaddr(dialAddr)
|
||||
var udpConn net.Conn
|
||||
var dialErr error
|
||||
if dialer != nil {
|
||||
udpConn, dialErr = dialer.DialContext(ctx, N.NetworkUDP, destination)
|
||||
} else {
|
||||
var netDialer net.Dialer
|
||||
udpConn, dialErr = netDialer.DialContext(ctx, N.NetworkUDP, destination.String())
|
||||
}
|
||||
if dialErr != nil {
|
||||
return nil, dialErr
|
||||
}
|
||||
wrappedConn := udpConn
|
||||
if len(readCounters) > 0 || len(writeCounters) > 0 {
|
||||
wrappedConn = sBufio.NewCounterConn(udpConn, readCounters, writeCounters)
|
||||
}
|
||||
packetConn := sBufio.NewUnbindPacketConn(wrappedConn)
|
||||
quicConn, dialErr := quic.DialEarly(ctx, packetConn, udpConn.RemoteAddr(), tlsCfg, cfg)
|
||||
if dialErr != nil {
|
||||
udpConn.Close()
|
||||
return nil, dialErr
|
||||
}
|
||||
return quicConn, nil
|
||||
},
|
||||
}
|
||||
return &http.Client{Transport: transport}, nil
|
||||
}, nil
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
//go:build !with_quic
|
||||
|
||||
package networkquality
|
||||
|
||||
import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) {
|
||||
return nil, C.ErrQUICNotIncluded
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -46,7 +46,6 @@ const (
|
||||
ruleItemNetworkIsConstrained
|
||||
ruleItemNetworkInterfaceAddress
|
||||
ruleItemDefaultInterfaceAddress
|
||||
ruleItemPackageNameRegex
|
||||
ruleItemFinal uint8 = 0xFF
|
||||
)
|
||||
|
||||
@@ -216,8 +215,6 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
|
||||
rule.ProcessPathRegex, err = readRuleItemString(reader)
|
||||
case ruleItemPackageName:
|
||||
rule.PackageName, err = readRuleItemString(reader)
|
||||
case ruleItemPackageNameRegex:
|
||||
rule.PackageNameRegex, err = readRuleItemString(reader)
|
||||
case ruleItemWIFISSID:
|
||||
rule.WIFISSID, err = readRuleItemString(reader)
|
||||
case ruleItemWIFIBSSID:
|
||||
@@ -397,15 +394,6 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.PackageNameRegex) > 0 {
|
||||
if generateVersion < C.RuleSetVersion5 {
|
||||
return E.New("`package_name_regex` rule item is only supported in version 5 or later")
|
||||
}
|
||||
err = writeRuleItemString(writer, ruleItemPackageNameRegex, rule.PackageNameRegex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.NetworkType) > 0 {
|
||||
if generateVersion < C.RuleSetVersion3 {
|
||||
return E.New("`network_type` rule item is only supported in version 3 or later")
|
||||
|
||||
@@ -1,612 +0,0 @@
|
||||
package stun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
"github.com/sagernet/sing/common/bufio/deadline"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultServer = "stun.voipgate.com:3478"
|
||||
|
||||
magicCookie = 0x2112A442
|
||||
headerSize = 20
|
||||
|
||||
bindingRequest = 0x0001
|
||||
bindingSuccessResponse = 0x0101
|
||||
bindingErrorResponse = 0x0111
|
||||
|
||||
attrMappedAddress = 0x0001
|
||||
attrChangeRequest = 0x0003
|
||||
attrErrorCode = 0x0009
|
||||
attrXORMappedAddress = 0x0020
|
||||
attrOtherAddress = 0x802c
|
||||
|
||||
familyIPv4 = 0x01
|
||||
familyIPv6 = 0x02
|
||||
|
||||
changeIP = 0x04
|
||||
changePort = 0x02
|
||||
|
||||
defaultRTO = 500 * time.Millisecond
|
||||
minRTO = 250 * time.Millisecond
|
||||
maxRetransmit = 2
|
||||
)
|
||||
|
||||
type Phase int32
|
||||
|
||||
const (
|
||||
PhaseBinding Phase = iota
|
||||
PhaseNATMapping
|
||||
PhaseNATFiltering
|
||||
PhaseDone
|
||||
)
|
||||
|
||||
type NATMapping int32
|
||||
|
||||
const (
|
||||
NATMappingUnknown NATMapping = iota
|
||||
_ // reserved
|
||||
NATMappingEndpointIndependent
|
||||
NATMappingAddressDependent
|
||||
NATMappingAddressAndPortDependent
|
||||
)
|
||||
|
||||
func (m NATMapping) String() string {
|
||||
switch m {
|
||||
case NATMappingEndpointIndependent:
|
||||
return "Endpoint Independent"
|
||||
case NATMappingAddressDependent:
|
||||
return "Address Dependent"
|
||||
case NATMappingAddressAndPortDependent:
|
||||
return "Address and Port Dependent"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type NATFiltering int32
|
||||
|
||||
const (
|
||||
NATFilteringUnknown NATFiltering = iota
|
||||
NATFilteringEndpointIndependent
|
||||
NATFilteringAddressDependent
|
||||
NATFilteringAddressAndPortDependent
|
||||
)
|
||||
|
||||
func (f NATFiltering) String() string {
|
||||
switch f {
|
||||
case NATFilteringEndpointIndependent:
|
||||
return "Endpoint Independent"
|
||||
case NATFilteringAddressDependent:
|
||||
return "Address Dependent"
|
||||
case NATFilteringAddressAndPortDependent:
|
||||
return "Address and Port Dependent"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type TransactionID [12]byte
|
||||
|
||||
type Options struct {
|
||||
Server string
|
||||
Dialer N.Dialer
|
||||
Context context.Context
|
||||
OnProgress func(Progress)
|
||||
}
|
||||
|
||||
type Progress struct {
|
||||
Phase Phase
|
||||
ExternalAddr string
|
||||
LatencyMs int32
|
||||
NATMapping NATMapping
|
||||
NATFiltering NATFiltering
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
ExternalAddr string
|
||||
LatencyMs int32
|
||||
NATMapping NATMapping
|
||||
NATFiltering NATFiltering
|
||||
NATTypeSupported bool
|
||||
}
|
||||
|
||||
type parsedResponse struct {
|
||||
xorMappedAddr netip.AddrPort
|
||||
mappedAddr netip.AddrPort
|
||||
otherAddr netip.AddrPort
|
||||
}
|
||||
|
||||
func (r *parsedResponse) externalAddr() (netip.AddrPort, bool) {
|
||||
if r.xorMappedAddr.IsValid() {
|
||||
return r.xorMappedAddr, true
|
||||
}
|
||||
if r.mappedAddr.IsValid() {
|
||||
return r.mappedAddr, true
|
||||
}
|
||||
return netip.AddrPort{}, false
|
||||
}
|
||||
|
||||
type stunAttribute struct {
|
||||
typ uint16
|
||||
value []byte
|
||||
}
|
||||
|
||||
func newTransactionID() TransactionID {
|
||||
var id TransactionID
|
||||
_, _ = rand.Read(id[:])
|
||||
return id
|
||||
}
|
||||
|
||||
func buildBindingRequest(txID TransactionID, attrs ...stunAttribute) []byte {
|
||||
attrLen := 0
|
||||
for _, attr := range attrs {
|
||||
attrLen += 4 + len(attr.value) + paddingLen(len(attr.value))
|
||||
}
|
||||
|
||||
buf := make([]byte, headerSize+attrLen)
|
||||
binary.BigEndian.PutUint16(buf[0:2], bindingRequest)
|
||||
binary.BigEndian.PutUint16(buf[2:4], uint16(attrLen))
|
||||
binary.BigEndian.PutUint32(buf[4:8], magicCookie)
|
||||
copy(buf[8:20], txID[:])
|
||||
|
||||
offset := headerSize
|
||||
for _, attr := range attrs {
|
||||
binary.BigEndian.PutUint16(buf[offset:offset+2], attr.typ)
|
||||
binary.BigEndian.PutUint16(buf[offset+2:offset+4], uint16(len(attr.value)))
|
||||
copy(buf[offset+4:offset+4+len(attr.value)], attr.value)
|
||||
offset += 4 + len(attr.value) + paddingLen(len(attr.value))
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
func changeRequestAttr(flags byte) stunAttribute {
|
||||
return stunAttribute{
|
||||
typ: attrChangeRequest,
|
||||
value: []byte{0, 0, 0, flags},
|
||||
}
|
||||
}
|
||||
|
||||
func parseResponse(data []byte, expectedTxID TransactionID) (*parsedResponse, error) {
|
||||
if len(data) < headerSize {
|
||||
return nil, E.New("response too short")
|
||||
}
|
||||
|
||||
msgType := binary.BigEndian.Uint16(data[0:2])
|
||||
if msgType&0xC000 != 0 {
|
||||
return nil, E.New("invalid STUN message: top 2 bits not zero")
|
||||
}
|
||||
|
||||
cookie := binary.BigEndian.Uint32(data[4:8])
|
||||
if cookie != magicCookie {
|
||||
return nil, E.New("invalid magic cookie")
|
||||
}
|
||||
|
||||
var txID TransactionID
|
||||
copy(txID[:], data[8:20])
|
||||
if txID != expectedTxID {
|
||||
return nil, E.New("transaction ID mismatch")
|
||||
}
|
||||
|
||||
msgLen := int(binary.BigEndian.Uint16(data[2:4]))
|
||||
if msgLen > len(data)-headerSize {
|
||||
return nil, E.New("message length exceeds data")
|
||||
}
|
||||
|
||||
attrData := data[headerSize : headerSize+msgLen]
|
||||
|
||||
if msgType == bindingErrorResponse {
|
||||
return nil, parseErrorResponse(attrData)
|
||||
}
|
||||
if msgType != bindingSuccessResponse {
|
||||
return nil, E.New("unexpected message type: ", fmt.Sprintf("0x%04x", msgType))
|
||||
}
|
||||
|
||||
resp := &parsedResponse{}
|
||||
offset := 0
|
||||
for offset+4 <= len(attrData) {
|
||||
attrType := binary.BigEndian.Uint16(attrData[offset : offset+2])
|
||||
attrLen := int(binary.BigEndian.Uint16(attrData[offset+2 : offset+4]))
|
||||
if offset+4+attrLen > len(attrData) {
|
||||
break
|
||||
}
|
||||
attrValue := attrData[offset+4 : offset+4+attrLen]
|
||||
|
||||
switch attrType {
|
||||
case attrXORMappedAddress:
|
||||
addr, err := parseXORMappedAddress(attrValue, txID)
|
||||
if err == nil {
|
||||
resp.xorMappedAddr = addr
|
||||
}
|
||||
case attrMappedAddress:
|
||||
addr, err := parseMappedAddress(attrValue)
|
||||
if err == nil {
|
||||
resp.mappedAddr = addr
|
||||
}
|
||||
case attrOtherAddress:
|
||||
addr, err := parseMappedAddress(attrValue)
|
||||
if err == nil {
|
||||
resp.otherAddr = addr
|
||||
}
|
||||
}
|
||||
|
||||
offset += 4 + attrLen + paddingLen(attrLen)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func parseErrorResponse(data []byte) error {
|
||||
offset := 0
|
||||
for offset+4 <= len(data) {
|
||||
attrType := binary.BigEndian.Uint16(data[offset : offset+2])
|
||||
attrLen := int(binary.BigEndian.Uint16(data[offset+2 : offset+4]))
|
||||
if offset+4+attrLen > len(data) {
|
||||
break
|
||||
}
|
||||
if attrType == attrErrorCode && attrLen >= 4 {
|
||||
attrValue := data[offset+4 : offset+4+attrLen]
|
||||
class := int(attrValue[2] & 0x07)
|
||||
number := int(attrValue[3])
|
||||
code := class*100 + number
|
||||
if attrLen > 4 {
|
||||
return E.New("STUN error ", code, ": ", string(attrValue[4:]))
|
||||
}
|
||||
return E.New("STUN error ", code)
|
||||
}
|
||||
offset += 4 + attrLen + paddingLen(attrLen)
|
||||
}
|
||||
return E.New("STUN error response")
|
||||
}
|
||||
|
||||
func parseXORMappedAddress(data []byte, txID TransactionID) (netip.AddrPort, error) {
|
||||
if len(data) < 4 {
|
||||
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS too short")
|
||||
}
|
||||
|
||||
family := data[1]
|
||||
xPort := binary.BigEndian.Uint16(data[2:4])
|
||||
port := xPort ^ uint16(magicCookie>>16)
|
||||
|
||||
switch family {
|
||||
case familyIPv4:
|
||||
if len(data) < 8 {
|
||||
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv4 too short")
|
||||
}
|
||||
var ip [4]byte
|
||||
binary.BigEndian.PutUint32(ip[:], binary.BigEndian.Uint32(data[4:8])^magicCookie)
|
||||
return netip.AddrPortFrom(netip.AddrFrom4(ip), port), nil
|
||||
case familyIPv6:
|
||||
if len(data) < 20 {
|
||||
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv6 too short")
|
||||
}
|
||||
var ip [16]byte
|
||||
var xorKey [16]byte
|
||||
binary.BigEndian.PutUint32(xorKey[0:4], magicCookie)
|
||||
copy(xorKey[4:16], txID[:])
|
||||
for i := range 16 {
|
||||
ip[i] = data[4+i] ^ xorKey[i]
|
||||
}
|
||||
return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil
|
||||
default:
|
||||
return netip.AddrPort{}, E.New("unknown address family: ", family)
|
||||
}
|
||||
}
|
||||
|
||||
func parseMappedAddress(data []byte) (netip.AddrPort, error) {
|
||||
if len(data) < 4 {
|
||||
return netip.AddrPort{}, E.New("MAPPED-ADDRESS too short")
|
||||
}
|
||||
|
||||
family := data[1]
|
||||
port := binary.BigEndian.Uint16(data[2:4])
|
||||
|
||||
switch family {
|
||||
case familyIPv4:
|
||||
if len(data) < 8 {
|
||||
return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv4 too short")
|
||||
}
|
||||
return netip.AddrPortFrom(
|
||||
netip.AddrFrom4([4]byte{data[4], data[5], data[6], data[7]}), port,
|
||||
), nil
|
||||
case familyIPv6:
|
||||
if len(data) < 20 {
|
||||
return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv6 too short")
|
||||
}
|
||||
var ip [16]byte
|
||||
copy(ip[:], data[4:20])
|
||||
return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil
|
||||
default:
|
||||
return netip.AddrPort{}, E.New("unknown address family: ", family)
|
||||
}
|
||||
}
|
||||
|
||||
func roundTrip(conn net.PacketConn, addr net.Addr, txID TransactionID, attrs []stunAttribute, rto time.Duration) (*parsedResponse, time.Duration, error) {
|
||||
request := buildBindingRequest(txID, attrs...)
|
||||
currentRTO := rto
|
||||
retransmitCount := 0
|
||||
|
||||
sendTime := time.Now()
|
||||
_, err := conn.WriteTo(request, addr)
|
||||
if err != nil {
|
||||
return nil, 0, E.Cause(err, "send STUN request")
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
err = conn.SetReadDeadline(sendTime.Add(currentRTO))
|
||||
if err != nil {
|
||||
return nil, 0, E.Cause(err, "set read deadline")
|
||||
}
|
||||
|
||||
n, _, readErr := conn.ReadFrom(buf)
|
||||
if readErr != nil {
|
||||
if E.IsTimeout(readErr) && retransmitCount < maxRetransmit {
|
||||
retransmitCount++
|
||||
currentRTO *= 2
|
||||
sendTime = time.Now()
|
||||
_, err = conn.WriteTo(request, addr)
|
||||
if err != nil {
|
||||
return nil, 0, E.Cause(err, "retransmit STUN request")
|
||||
}
|
||||
continue
|
||||
}
|
||||
return nil, 0, E.Cause(readErr, "read STUN response")
|
||||
}
|
||||
|
||||
if n < headerSize || buf[0]&0xC0 != 0 ||
|
||||
binary.BigEndian.Uint32(buf[4:8]) != magicCookie {
|
||||
continue
|
||||
}
|
||||
var receivedTxID TransactionID
|
||||
copy(receivedTxID[:], buf[8:20])
|
||||
if receivedTxID != txID {
|
||||
continue
|
||||
}
|
||||
|
||||
latency := time.Since(sendTime)
|
||||
|
||||
resp, parseErr := parseResponse(buf[:n], txID)
|
||||
if parseErr != nil {
|
||||
return nil, 0, parseErr
|
||||
}
|
||||
|
||||
return resp, latency, nil
|
||||
}
|
||||
}
|
||||
|
||||
func Run(options Options) (*Result, error) {
|
||||
ctx := options.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
server := options.Server
|
||||
if server == "" {
|
||||
server = DefaultServer
|
||||
}
|
||||
serverSocksaddr := M.ParseSocksaddr(server)
|
||||
if serverSocksaddr.Port == 0 {
|
||||
serverSocksaddr.Port = 3478
|
||||
}
|
||||
|
||||
reportProgress := options.OnProgress
|
||||
if reportProgress == nil {
|
||||
reportProgress = func(Progress) {}
|
||||
}
|
||||
|
||||
var (
|
||||
packetConn net.PacketConn
|
||||
serverAddr net.Addr
|
||||
err error
|
||||
)
|
||||
|
||||
if options.Dialer != nil {
|
||||
packetConn, err = options.Dialer.ListenPacket(ctx, serverSocksaddr)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create UDP socket")
|
||||
}
|
||||
serverAddr = serverSocksaddr
|
||||
} else {
|
||||
serverUDPAddr, resolveErr := net.ResolveUDPAddr("udp", serverSocksaddr.String())
|
||||
if resolveErr != nil {
|
||||
return nil, E.Cause(resolveErr, "resolve STUN server")
|
||||
}
|
||||
packetConn, err = net.ListenPacket("udp", "")
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create UDP socket")
|
||||
}
|
||||
serverAddr = serverUDPAddr
|
||||
}
|
||||
defer func() {
|
||||
_ = packetConn.Close()
|
||||
}()
|
||||
if deadline.NeedAdditionalReadDeadline(packetConn) {
|
||||
packetConn = deadline.NewPacketConn(bufio.NewPacketConn(packetConn))
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
rto := defaultRTO
|
||||
|
||||
// Phase 1: Binding
|
||||
reportProgress(Progress{Phase: PhaseBinding})
|
||||
|
||||
txID := newTransactionID()
|
||||
resp, latency, err := roundTrip(packetConn, serverAddr, txID, nil, rto)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "binding request")
|
||||
}
|
||||
|
||||
rto = max(minRTO, 3*latency)
|
||||
|
||||
externalAddr, ok := resp.externalAddr()
|
||||
if !ok {
|
||||
return nil, E.New("no mapped address in response")
|
||||
}
|
||||
|
||||
result := &Result{
|
||||
ExternalAddr: externalAddr.String(),
|
||||
LatencyMs: int32(latency.Milliseconds()),
|
||||
}
|
||||
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseBinding,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
})
|
||||
|
||||
otherAddr := resp.otherAddr
|
||||
if !otherAddr.IsValid() {
|
||||
result.NATTypeSupported = false
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseDone,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
result.NATTypeSupported = true
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result, nil
|
||||
default:
|
||||
}
|
||||
|
||||
// Phase 2: NAT Mapping Detection (RFC 5780 Section 4.3)
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseNATMapping,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
})
|
||||
|
||||
result.NATMapping = detectNATMapping(
|
||||
packetConn, serverSocksaddr.Port, externalAddr, otherAddr, rto,
|
||||
)
|
||||
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseNATMapping,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
NATMapping: result.NATMapping,
|
||||
})
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result, nil
|
||||
default:
|
||||
}
|
||||
|
||||
// Phase 3: NAT Filtering Detection (RFC 5780 Section 4.4)
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseNATFiltering,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
NATMapping: result.NATMapping,
|
||||
})
|
||||
|
||||
result.NATFiltering = detectNATFiltering(packetConn, serverAddr, rto)
|
||||
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseDone,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
NATMapping: result.NATMapping,
|
||||
NATFiltering: result.NATFiltering,
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func detectNATMapping(
|
||||
conn net.PacketConn,
|
||||
serverPort uint16,
|
||||
externalAddr netip.AddrPort,
|
||||
otherAddr netip.AddrPort,
|
||||
rto time.Duration,
|
||||
) NATMapping {
|
||||
// Mapping Test II: Send to other_ip:server_port
|
||||
testIIAddr := net.UDPAddrFromAddrPort(
|
||||
netip.AddrPortFrom(otherAddr.Addr(), serverPort),
|
||||
)
|
||||
txID2 := newTransactionID()
|
||||
resp2, _, err := roundTrip(conn, testIIAddr, txID2, nil, rto)
|
||||
if err != nil {
|
||||
return NATMappingUnknown
|
||||
}
|
||||
|
||||
externalAddr2, ok := resp2.externalAddr()
|
||||
if !ok {
|
||||
return NATMappingUnknown
|
||||
}
|
||||
|
||||
if externalAddr == externalAddr2 {
|
||||
return NATMappingEndpointIndependent
|
||||
}
|
||||
|
||||
// Mapping Test III: Send to other_ip:other_port
|
||||
testIIIAddr := net.UDPAddrFromAddrPort(otherAddr)
|
||||
txID3 := newTransactionID()
|
||||
resp3, _, err := roundTrip(conn, testIIIAddr, txID3, nil, rto)
|
||||
if err != nil {
|
||||
return NATMappingUnknown
|
||||
}
|
||||
|
||||
externalAddr3, ok := resp3.externalAddr()
|
||||
if !ok {
|
||||
return NATMappingUnknown
|
||||
}
|
||||
|
||||
if externalAddr2 == externalAddr3 {
|
||||
return NATMappingAddressDependent
|
||||
}
|
||||
return NATMappingAddressAndPortDependent
|
||||
}
|
||||
|
||||
func detectNATFiltering(
|
||||
conn net.PacketConn,
|
||||
serverAddr net.Addr,
|
||||
rto time.Duration,
|
||||
) NATFiltering {
|
||||
// Filtering Test II: Request response from different IP and port
|
||||
txID := newTransactionID()
|
||||
_, _, err := roundTrip(conn, serverAddr, txID,
|
||||
[]stunAttribute{changeRequestAttr(changeIP | changePort)}, rto)
|
||||
if err == nil {
|
||||
return NATFilteringEndpointIndependent
|
||||
}
|
||||
|
||||
// Filtering Test III: Request response from different port only
|
||||
txID = newTransactionID()
|
||||
_, _, err = roundTrip(conn, serverAddr, txID,
|
||||
[]stunAttribute{changeRequestAttr(changePort)}, rto)
|
||||
if err == nil {
|
||||
return NATFilteringAddressDependent
|
||||
}
|
||||
|
||||
return NATFilteringAddressAndPortDependent
|
||||
}
|
||||
|
||||
func paddingLen(n int) int {
|
||||
if n%4 == 0 {
|
||||
return 0
|
||||
}
|
||||
return 4 - n%4
|
||||
}
|
||||
@@ -40,6 +40,37 @@ func (w *acmeWrapper) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type acmeLogWriter struct {
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
func (w *acmeLogWriter) Write(p []byte) (n int, err error) {
|
||||
logLine := strings.ReplaceAll(string(p), " ", ": ")
|
||||
switch {
|
||||
case strings.HasPrefix(logLine, "error: "):
|
||||
w.logger.Error(logLine[7:])
|
||||
case strings.HasPrefix(logLine, "warn: "):
|
||||
w.logger.Warn(logLine[6:])
|
||||
case strings.HasPrefix(logLine, "info: "):
|
||||
w.logger.Info(logLine[6:])
|
||||
case strings.HasPrefix(logLine, "debug: "):
|
||||
w.logger.Debug(logLine[7:])
|
||||
default:
|
||||
w.logger.Debug(logLine)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *acmeLogWriter) Sync() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func encoderConfig() zapcore.EncoderConfig {
|
||||
config := zap.NewProductionEncoderConfig()
|
||||
config.TimeKey = zapcore.OmitKey
|
||||
return config
|
||||
}
|
||||
|
||||
func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
|
||||
var acmeServer string
|
||||
switch options.Provider {
|
||||
@@ -62,8 +93,8 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
|
||||
storage = certmagic.Default.Storage
|
||||
}
|
||||
zapLogger := zap.New(zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(ACMEEncoderConfig()),
|
||||
&ACMELogWriter{Logger: logger},
|
||||
zapcore.NewConsoleEncoder(encoderConfig()),
|
||||
&acmeLogWriter{logger: logger},
|
||||
zap.DebugLevel,
|
||||
))
|
||||
config := &certmagic.Config{
|
||||
@@ -140,7 +171,7 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
|
||||
} else {
|
||||
tlsConfig = &tls.Config{
|
||||
GetCertificate: config.GetCertificate,
|
||||
NextProtos: []string{C.ACMETLS1Protocol},
|
||||
NextProtos: []string{ACMETLS1Protocol},
|
||||
}
|
||||
}
|
||||
return tlsConfig, &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package constant
|
||||
package tls
|
||||
|
||||
const ACMETLS1Protocol = "acme-tls/1"
|
||||
@@ -1,41 +0,0 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
type ACMELogWriter struct {
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
func (w *ACMELogWriter) Write(p []byte) (n int, err error) {
|
||||
logLine := strings.ReplaceAll(string(p), " ", ": ")
|
||||
switch {
|
||||
case strings.HasPrefix(logLine, "error: "):
|
||||
w.Logger.Error(logLine[7:])
|
||||
case strings.HasPrefix(logLine, "warn: "):
|
||||
w.Logger.Warn(logLine[6:])
|
||||
case strings.HasPrefix(logLine, "info: "):
|
||||
w.Logger.Info(logLine[6:])
|
||||
case strings.HasPrefix(logLine, "debug: "):
|
||||
w.Logger.Debug(logLine[7:])
|
||||
default:
|
||||
w.Logger.Debug(logLine)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *ACMELogWriter) Sync() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ACMEEncoderConfig() zapcore.EncoderConfig {
|
||||
config := zap.NewProductionEncoderConfig()
|
||||
config.TimeKey = zapcore.OmitKey
|
||||
return config
|
||||
}
|
||||
@@ -32,10 +32,6 @@ type RealityServerConfig struct {
|
||||
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||
var tlsConfig utls.RealityConfig
|
||||
|
||||
if options.CertificateProvider != nil {
|
||||
return nil, E.New("certificate_provider is unavailable in reality")
|
||||
}
|
||||
//nolint:staticcheck
|
||||
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
||||
return nil, E.New("acme is unavailable in reality")
|
||||
}
|
||||
|
||||
@@ -13,87 +13,19 @@ import (
|
||||
"github.com/sagernet/fswatch"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var errInsecureUnused = E.New("tls: insecure unused")
|
||||
|
||||
type managedCertificateProvider interface {
|
||||
adapter.CertificateProvider
|
||||
adapter.SimpleLifecycle
|
||||
}
|
||||
|
||||
type sharedCertificateProvider struct {
|
||||
tag string
|
||||
manager adapter.CertificateProviderManager
|
||||
provider adapter.CertificateProviderService
|
||||
}
|
||||
|
||||
func (p *sharedCertificateProvider) Start() error {
|
||||
provider, found := p.manager.Get(p.tag)
|
||||
if !found {
|
||||
return E.New("certificate provider not found: ", p.tag)
|
||||
}
|
||||
p.provider = provider
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *sharedCertificateProvider) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *sharedCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return p.provider.GetCertificate(hello)
|
||||
}
|
||||
|
||||
func (p *sharedCertificateProvider) GetACMENextProtos() []string {
|
||||
return getACMENextProtos(p.provider)
|
||||
}
|
||||
|
||||
type inlineCertificateProvider struct {
|
||||
provider adapter.CertificateProviderService
|
||||
}
|
||||
|
||||
func (p *inlineCertificateProvider) Start() error {
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
err := adapter.LegacyStart(p.provider, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *inlineCertificateProvider) Close() error {
|
||||
return p.provider.Close()
|
||||
}
|
||||
|
||||
func (p *inlineCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return p.provider.GetCertificate(hello)
|
||||
}
|
||||
|
||||
func (p *inlineCertificateProvider) GetACMENextProtos() []string {
|
||||
return getACMENextProtos(p.provider)
|
||||
}
|
||||
|
||||
func getACMENextProtos(provider adapter.CertificateProvider) []string {
|
||||
if acmeProvider, isACME := provider.(adapter.ACMECertificateProvider); isACME {
|
||||
return acmeProvider.GetACMENextProtos()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type STDServerConfig struct {
|
||||
access sync.RWMutex
|
||||
config *tls.Config
|
||||
logger log.Logger
|
||||
certificateProvider managedCertificateProvider
|
||||
acmeService adapter.SimpleLifecycle
|
||||
certificate []byte
|
||||
key []byte
|
||||
@@ -121,17 +53,18 @@ func (c *STDServerConfig) SetServerName(serverName string) {
|
||||
func (c *STDServerConfig) NextProtos() []string {
|
||||
c.access.RLock()
|
||||
defer c.access.RUnlock()
|
||||
if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol {
|
||||
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
|
||||
return c.config.NextProtos[1:]
|
||||
} else {
|
||||
return c.config.NextProtos
|
||||
}
|
||||
return c.config.NextProtos
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
config := c.config.Clone()
|
||||
if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol {
|
||||
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
|
||||
config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
|
||||
} else {
|
||||
config.NextProtos = nextProto
|
||||
@@ -139,18 +72,6 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
||||
c.config = config
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) hasACMEALPN() bool {
|
||||
if c.acmeService != nil {
|
||||
return true
|
||||
}
|
||||
if c.certificateProvider != nil {
|
||||
if acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider); isACME {
|
||||
return len(acmeProvider.GetACMENextProtos()) > 0
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) STDConfig() (*STDConfig, error) {
|
||||
return c.config, nil
|
||||
}
|
||||
@@ -170,39 +91,15 @@ func (c *STDServerConfig) Clone() Config {
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) Start() error {
|
||||
if c.certificateProvider != nil {
|
||||
err := c.certificateProvider.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider); isACME {
|
||||
nextProtos := acmeProvider.GetACMENextProtos()
|
||||
if len(nextProtos) > 0 {
|
||||
c.access.Lock()
|
||||
config := c.config.Clone()
|
||||
mergedNextProtos := append([]string{}, nextProtos...)
|
||||
for _, nextProto := range config.NextProtos {
|
||||
if !common.Contains(mergedNextProtos, nextProto) {
|
||||
mergedNextProtos = append(mergedNextProtos, nextProto)
|
||||
}
|
||||
}
|
||||
config.NextProtos = mergedNextProtos
|
||||
c.config = config
|
||||
c.access.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.acmeService != nil {
|
||||
err := c.acmeService.Start()
|
||||
return c.acmeService.Start()
|
||||
} else {
|
||||
err := c.startWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
c.logger.Warn("create fsnotify watcher: ", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err := c.startWatcher()
|
||||
if err != nil {
|
||||
c.logger.Warn("create fsnotify watcher: ", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) startWatcher() error {
|
||||
@@ -306,34 +203,23 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) Close() error {
|
||||
return common.Close(c.certificateProvider, c.acmeService, c.watcher)
|
||||
if c.acmeService != nil {
|
||||
return c.acmeService.Close()
|
||||
}
|
||||
if c.watcher != nil {
|
||||
return c.watcher.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||
if !options.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
//nolint:staticcheck
|
||||
if options.CertificateProvider != nil && options.ACME != nil {
|
||||
return nil, E.New("certificate_provider and acme are mutually exclusive")
|
||||
}
|
||||
var tlsConfig *tls.Config
|
||||
var certificateProvider managedCertificateProvider
|
||||
var acmeService adapter.SimpleLifecycle
|
||||
var err error
|
||||
if options.CertificateProvider != nil {
|
||||
certificateProvider, err = newCertificateProvider(ctx, logger, options.CertificateProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig = &tls.Config{
|
||||
GetCertificate: certificateProvider.GetCertificate,
|
||||
}
|
||||
if options.Insecure {
|
||||
return nil, errInsecureUnused
|
||||
}
|
||||
} else if options.ACME != nil && len(options.ACME.Domain) > 0 { //nolint:staticcheck
|
||||
deprecated.Report(ctx, deprecated.OptionInlineACME)
|
||||
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
||||
//nolint:staticcheck
|
||||
tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME))
|
||||
if err != nil {
|
||||
@@ -386,7 +272,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
||||
certificate []byte
|
||||
key []byte
|
||||
)
|
||||
if certificateProvider == nil && acmeService == nil {
|
||||
if acmeService == nil {
|
||||
if len(options.Certificate) > 0 {
|
||||
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
||||
} else if options.CertificatePath != "" {
|
||||
@@ -474,7 +360,6 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
||||
serverConfig := &STDServerConfig{
|
||||
config: tlsConfig,
|
||||
logger: logger,
|
||||
certificateProvider: certificateProvider,
|
||||
acmeService: acmeService,
|
||||
certificate: certificate,
|
||||
key: key,
|
||||
@@ -484,8 +369,8 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
||||
echKeyPath: echKeyPath,
|
||||
}
|
||||
serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
serverConfig.access.RLock()
|
||||
defer serverConfig.access.RUnlock()
|
||||
serverConfig.access.Lock()
|
||||
defer serverConfig.access.Unlock()
|
||||
return serverConfig.config, nil
|
||||
}
|
||||
var config ServerConfig = serverConfig
|
||||
@@ -502,27 +387,3 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func newCertificateProvider(ctx context.Context, logger log.ContextLogger, options *option.CertificateProviderOptions) (managedCertificateProvider, error) {
|
||||
if options.IsShared() {
|
||||
manager := service.FromContext[adapter.CertificateProviderManager](ctx)
|
||||
if manager == nil {
|
||||
return nil, E.New("missing certificate provider manager in context")
|
||||
}
|
||||
return &sharedCertificateProvider{
|
||||
tag: options.Tag,
|
||||
manager: manager,
|
||||
}, nil
|
||||
}
|
||||
registry := service.FromContext[adapter.CertificateProviderRegistry](ctx)
|
||||
if registry == nil {
|
||||
return nil, E.New("missing certificate provider registry in context")
|
||||
}
|
||||
provider, err := registry.Create(ctx, logger, "", options.Type, options.Options)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create inline certificate provider")
|
||||
}
|
||||
return &inlineCertificateProvider{
|
||||
provider: provider,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -37,14 +37,5 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"inbounds": [],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"action": "hijack-dns"
|
||||
}
|
||||
],
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
"inbounds": []
|
||||
}
|
||||
|
||||
@@ -29,14 +29,5 @@
|
||||
"node_id": 286
|
||||
}
|
||||
],
|
||||
"inbounds": [],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"action": "hijack-dns"
|
||||
}
|
||||
],
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
"inbounds": []
|
||||
}
|
||||
|
||||
12
configs/outbound.json
Normal file
12
configs/outbound.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"tag": "block"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
configs/route.json
Normal file
11
configs/route.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"action": "hijack-dns"
|
||||
}
|
||||
],
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
@@ -17,24 +17,25 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
DNSTypeLegacy = "legacy"
|
||||
DNSTypeUDP = "udp"
|
||||
DNSTypeTCP = "tcp"
|
||||
DNSTypeTLS = "tls"
|
||||
DNSTypeHTTPS = "https"
|
||||
DNSTypeQUIC = "quic"
|
||||
DNSTypeHTTP3 = "h3"
|
||||
DNSTypeLocal = "local"
|
||||
DNSTypeHosts = "hosts"
|
||||
DNSTypeFakeIP = "fakeip"
|
||||
DNSTypeDHCP = "dhcp"
|
||||
DNSTypeTailscale = "tailscale"
|
||||
DNSTypeLegacy = "legacy"
|
||||
DNSTypeLegacyRcode = "legacy_rcode"
|
||||
DNSTypeUDP = "udp"
|
||||
DNSTypeTCP = "tcp"
|
||||
DNSTypeTLS = "tls"
|
||||
DNSTypeHTTPS = "https"
|
||||
DNSTypeQUIC = "quic"
|
||||
DNSTypeHTTP3 = "h3"
|
||||
DNSTypeLocal = "local"
|
||||
DNSTypeHosts = "hosts"
|
||||
DNSTypeFakeIP = "fakeip"
|
||||
DNSTypeDHCP = "dhcp"
|
||||
DNSTypeTailscale = "tailscale"
|
||||
)
|
||||
|
||||
const (
|
||||
DNSProviderAliDNS = "alidns"
|
||||
DNSProviderCloudflare = "cloudflare"
|
||||
DNSProviderACMEDNS = "acmedns"
|
||||
DNSProviderAliDNS = "alidns"
|
||||
DNSProviderCloudflare = "cloudflare"
|
||||
DNSProviderACMEDNS = "acmedns"
|
||||
DNSProviderTencentCloud = "tencentcloud"
|
||||
DNSProviderDNSPod = "dnspod"
|
||||
)
|
||||
|
||||
@@ -1,40 +1,37 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
TypeTun = "tun"
|
||||
TypeRedirect = "redirect"
|
||||
TypeTProxy = "tproxy"
|
||||
TypeDirect = "direct"
|
||||
TypeBlock = "block"
|
||||
TypeDNS = "dns"
|
||||
TypeSOCKS = "socks"
|
||||
TypeHTTP = "http"
|
||||
TypeMixed = "mixed"
|
||||
TypeShadowsocks = "shadowsocks"
|
||||
TypeVMess = "vmess"
|
||||
TypeTrojan = "trojan"
|
||||
TypeNaive = "naive"
|
||||
TypeWireGuard = "wireguard"
|
||||
TypeHysteria = "hysteria"
|
||||
TypeTor = "tor"
|
||||
TypeSSH = "ssh"
|
||||
TypeShadowTLS = "shadowtls"
|
||||
TypeAnyTLS = "anytls"
|
||||
TypeShadowsocksR = "shadowsocksr"
|
||||
TypeVLESS = "vless"
|
||||
TypeTUIC = "tuic"
|
||||
TypeHysteria2 = "hysteria2"
|
||||
TypeTailscale = "tailscale"
|
||||
TypeCloudflared = "cloudflared"
|
||||
TypeDERP = "derp"
|
||||
TypeResolved = "resolved"
|
||||
TypeSSMAPI = "ssm-api"
|
||||
TypeCCM = "ccm"
|
||||
TypeOCM = "ocm"
|
||||
TypeOOMKiller = "oom-killer"
|
||||
TypeACME = "acme"
|
||||
TypeCloudflareOriginCA = "cloudflare-origin-ca"
|
||||
TypeXBoard = "xboard"
|
||||
TypeTun = "tun"
|
||||
TypeRedirect = "redirect"
|
||||
TypeTProxy = "tproxy"
|
||||
TypeDirect = "direct"
|
||||
TypeBlock = "block"
|
||||
TypeDNS = "dns"
|
||||
TypeSOCKS = "socks"
|
||||
TypeHTTP = "http"
|
||||
TypeMixed = "mixed"
|
||||
TypeShadowsocks = "shadowsocks"
|
||||
TypeVMess = "vmess"
|
||||
TypeTrojan = "trojan"
|
||||
TypeNaive = "naive"
|
||||
TypeWireGuard = "wireguard"
|
||||
TypeHysteria = "hysteria"
|
||||
TypeTor = "tor"
|
||||
TypeSSH = "ssh"
|
||||
TypeShadowTLS = "shadowtls"
|
||||
TypeAnyTLS = "anytls"
|
||||
TypeShadowsocksR = "shadowsocksr"
|
||||
TypeVLESS = "vless"
|
||||
TypeTUIC = "tuic"
|
||||
TypeHysteria2 = "hysteria2"
|
||||
TypeTailscale = "tailscale"
|
||||
TypeDERP = "derp"
|
||||
TypeResolved = "resolved"
|
||||
TypeSSMAPI = "ssm-api"
|
||||
TypeCCM = "ccm"
|
||||
TypeOCM = "ocm"
|
||||
TypeOOMKiller = "oom-killer"
|
||||
TypeXBoard = "xboard"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -92,8 +89,6 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "AnyTLS"
|
||||
case TypeTailscale:
|
||||
return "Tailscale"
|
||||
case TypeCloudflared:
|
||||
return "Cloudflared"
|
||||
case TypeSelector:
|
||||
return "Selector"
|
||||
case TypeURLTest:
|
||||
|
||||
@@ -23,15 +23,12 @@ const (
|
||||
RuleSetVersion2
|
||||
RuleSetVersion3
|
||||
RuleSetVersion4
|
||||
RuleSetVersion5
|
||||
RuleSetVersionCurrent = RuleSetVersion5
|
||||
RuleSetVersionCurrent = RuleSetVersion4
|
||||
)
|
||||
|
||||
const (
|
||||
RuleActionTypeRoute = "route"
|
||||
RuleActionTypeRouteOptions = "route-options"
|
||||
RuleActionTypeEvaluate = "evaluate"
|
||||
RuleActionTypeRespond = "respond"
|
||||
RuleActionTypeDirect = "direct"
|
||||
RuleActionTypeBypass = "bypass"
|
||||
RuleActionTypeReject = "reject"
|
||||
|
||||
@@ -87,17 +87,12 @@ func (s *StartedService) newInstance(profileContent string, overrideOptions *Ove
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.oomKillerEnabled {
|
||||
if s.oomKiller && C.IsIos {
|
||||
if !common.Any(options.Services, func(it option.Service) bool {
|
||||
return it.Type == C.TypeOOMKiller
|
||||
}) {
|
||||
oomOptions := &option.OOMKillerServiceOptions{
|
||||
KillerDisabled: s.oomKillerDisabled,
|
||||
MemoryLimitOverride: s.oomMemoryLimit,
|
||||
}
|
||||
options.Services = append(options.Services, option.Service{
|
||||
Type: C.TypeOOMKiller,
|
||||
Options: oomOptions,
|
||||
Type: C.TypeOOMKiller,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,5 @@ type PlatformHandler interface {
|
||||
ServiceReload() error
|
||||
SystemProxyStatus() (*SystemProxyStatus, error)
|
||||
SetSystemProxyEnabled(enabled bool) error
|
||||
TriggerNativeCrash() error
|
||||
WriteDebugMessage(message string)
|
||||
}
|
||||
|
||||
@@ -6,20 +6,14 @@ import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/networkquality"
|
||||
"github.com/sagernet/sing-box/common/stun"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/clashapi"
|
||||
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/protocol/group"
|
||||
"github.com/sagernet/sing-box/service/oomkiller"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/batch"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
@@ -30,8 +24,6 @@ import (
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
@@ -40,12 +32,10 @@ var _ StartedServiceServer = (*StartedService)(nil)
|
||||
type StartedService struct {
|
||||
ctx context.Context
|
||||
// platform adapter.PlatformInterface
|
||||
handler PlatformHandler
|
||||
debug bool
|
||||
logMaxLines int
|
||||
oomKillerEnabled bool
|
||||
oomKillerDisabled bool
|
||||
oomMemoryLimit uint64
|
||||
handler PlatformHandler
|
||||
debug bool
|
||||
logMaxLines int
|
||||
oomKiller bool
|
||||
// workingDirectory string
|
||||
// tempDirectory string
|
||||
// userID int
|
||||
@@ -74,12 +64,10 @@ type StartedService struct {
|
||||
type ServiceOptions struct {
|
||||
Context context.Context
|
||||
// Platform adapter.PlatformInterface
|
||||
Handler PlatformHandler
|
||||
Debug bool
|
||||
LogMaxLines int
|
||||
OOMKillerEnabled bool
|
||||
OOMKillerDisabled bool
|
||||
OOMMemoryLimit uint64
|
||||
Handler PlatformHandler
|
||||
Debug bool
|
||||
LogMaxLines int
|
||||
OOMKiller bool
|
||||
// WorkingDirectory string
|
||||
// TempDirectory string
|
||||
// UserID int
|
||||
@@ -91,12 +79,10 @@ func NewStartedService(options ServiceOptions) *StartedService {
|
||||
s := &StartedService{
|
||||
ctx: options.Context,
|
||||
// platform: options.Platform,
|
||||
handler: options.Handler,
|
||||
debug: options.Debug,
|
||||
logMaxLines: options.LogMaxLines,
|
||||
oomKillerEnabled: options.OOMKillerEnabled,
|
||||
oomKillerDisabled: options.OOMKillerDisabled,
|
||||
oomMemoryLimit: options.OOMMemoryLimit,
|
||||
handler: options.Handler,
|
||||
debug: options.Debug,
|
||||
logMaxLines: options.LogMaxLines,
|
||||
oomKiller: options.OOMKiller,
|
||||
// workingDirectory: options.WorkingDirectory,
|
||||
// tempDirectory: options.TempDirectory,
|
||||
// userID: options.UserID,
|
||||
@@ -696,42 +682,7 @@ func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *Set
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) TriggerDebugCrash(ctx context.Context, request *DebugCrashRequest) (*emptypb.Empty, error) {
|
||||
if !s.debug {
|
||||
return nil, status.Error(codes.PermissionDenied, "debug crash trigger unavailable")
|
||||
}
|
||||
if request == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "missing debug crash request")
|
||||
}
|
||||
switch request.Type {
|
||||
case DebugCrashRequest_GO:
|
||||
time.AfterFunc(200*time.Millisecond, func() {
|
||||
*(*int)(unsafe.Pointer(uintptr(0))) = 0
|
||||
})
|
||||
case DebugCrashRequest_NATIVE:
|
||||
err := s.handler.TriggerNativeCrash()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, status.Error(codes.InvalidArgument, "unknown debug crash type")
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) TriggerOOMReport(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
instance := s.Instance()
|
||||
if instance == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "service not started")
|
||||
}
|
||||
reporter := service.FromContext[oomkiller.OOMReporter](instance.ctx)
|
||||
if reporter == nil {
|
||||
return nil, status.Error(codes.Unavailable, "OOM reporter not available")
|
||||
}
|
||||
return &emptypb.Empty{}, reporter.WriteReport(memory.Total())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[ConnectionEvents]) error {
|
||||
@@ -1068,12 +1019,9 @@ func (s *StartedService) GetDeprecatedWarnings(ctx context.Context, empty *empty
|
||||
return &DeprecatedWarnings{
|
||||
Warnings: common.Map(notes, func(it deprecated.Note) *DeprecatedWarning {
|
||||
return &DeprecatedWarning{
|
||||
Message: it.Message(),
|
||||
Impending: it.Impending(),
|
||||
MigrationLink: it.MigrationLink,
|
||||
Description: it.Description,
|
||||
DeprecatedVersion: it.DeprecatedVersion,
|
||||
ScheduledVersion: it.ScheduledVersion,
|
||||
Message: it.Message(),
|
||||
Impending: it.Impending(),
|
||||
MigrationLink: it.MigrationLink,
|
||||
}
|
||||
}),
|
||||
}, nil
|
||||
@@ -1085,386 +1033,6 @@ func (s *StartedService) GetStartedAt(ctx context.Context, empty *emptypb.Empty)
|
||||
return &StartedAt{StartedAt: s.startedAt.UnixMilli()}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.ServerStreamingServer[OutboundList]) error {
|
||||
err := s.waitForStarted(server.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
subscription, done, err := s.urlTestObserver.Subscribe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.urlTestObserver.UnSubscribe(subscription)
|
||||
for {
|
||||
s.serviceAccess.RLock()
|
||||
if s.serviceStatus.Status != ServiceStatus_STARTED {
|
||||
s.serviceAccess.RUnlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
historyStorage := boxService.urlTestHistoryStorage
|
||||
var list OutboundList
|
||||
for _, ob := range boxService.instance.Outbound().Outbounds() {
|
||||
item := &GroupItem{
|
||||
Tag: ob.Tag(),
|
||||
Type: ob.Type(),
|
||||
}
|
||||
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ob)); history != nil {
|
||||
item.UrlTestTime = history.Time.Unix()
|
||||
item.UrlTestDelay = int32(history.Delay)
|
||||
}
|
||||
list.Outbounds = append(list.Outbounds, item)
|
||||
}
|
||||
for _, ep := range boxService.instance.Endpoint().Endpoints() {
|
||||
item := &GroupItem{
|
||||
Tag: ep.Tag(),
|
||||
Type: ep.Type(),
|
||||
}
|
||||
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ep)); history != nil {
|
||||
item.UrlTestTime = history.Time.Unix()
|
||||
item.UrlTestDelay = int32(history.Delay)
|
||||
}
|
||||
list.Outbounds = append(list.Outbounds, item)
|
||||
}
|
||||
err = server.Send(&list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case <-subscription:
|
||||
case <-s.ctx.Done():
|
||||
return s.ctx.Err()
|
||||
case <-server.Context().Done():
|
||||
return server.Context().Err()
|
||||
case <-done:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveOutbound(instance *Instance, tag string) (adapter.Outbound, error) {
|
||||
if tag == "" {
|
||||
return instance.instance.Outbound().Default(), nil
|
||||
}
|
||||
outbound, loaded := instance.instance.Outbound().Outbound(tag)
|
||||
if !loaded {
|
||||
return nil, E.New("outbound not found: ", tag)
|
||||
}
|
||||
return outbound, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) StartNetworkQualityTest(
|
||||
request *NetworkQualityTestRequest,
|
||||
server grpc.ServerStreamingServer[NetworkQualityTestProgress],
|
||||
) error {
|
||||
err := s.waitForStarted(server.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.serviceAccess.RLock()
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
|
||||
outbound, err := resolveOutbound(boxService, request.OutboundTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0)
|
||||
httpClient := networkquality.NewHTTPClient(resolvedDialer)
|
||||
defer httpClient.CloseIdleConnections()
|
||||
|
||||
measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(resolvedDialer, request.Http3)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, nqErr := networkquality.Run(networkquality.Options{
|
||||
ConfigURL: request.ConfigURL,
|
||||
HTTPClient: httpClient,
|
||||
NewMeasurementClient: measurementClientFactory,
|
||||
Serial: request.Serial,
|
||||
MaxRuntime: time.Duration(request.MaxRuntimeSeconds) * time.Second,
|
||||
Context: server.Context(),
|
||||
OnProgress: func(p networkquality.Progress) {
|
||||
_ = server.Send(&NetworkQualityTestProgress{
|
||||
Phase: int32(p.Phase),
|
||||
DownloadCapacity: p.DownloadCapacity,
|
||||
UploadCapacity: p.UploadCapacity,
|
||||
DownloadRPM: p.DownloadRPM,
|
||||
UploadRPM: p.UploadRPM,
|
||||
IdleLatencyMs: p.IdleLatencyMs,
|
||||
ElapsedMs: p.ElapsedMs,
|
||||
DownloadCapacityAccuracy: int32(p.DownloadCapacityAccuracy),
|
||||
UploadCapacityAccuracy: int32(p.UploadCapacityAccuracy),
|
||||
DownloadRPMAccuracy: int32(p.DownloadRPMAccuracy),
|
||||
UploadRPMAccuracy: int32(p.UploadRPMAccuracy),
|
||||
})
|
||||
},
|
||||
})
|
||||
if nqErr != nil {
|
||||
return server.Send(&NetworkQualityTestProgress{
|
||||
IsFinal: true,
|
||||
Error: nqErr.Error(),
|
||||
})
|
||||
}
|
||||
return server.Send(&NetworkQualityTestProgress{
|
||||
Phase: int32(networkquality.PhaseDone),
|
||||
DownloadCapacity: result.DownloadCapacity,
|
||||
UploadCapacity: result.UploadCapacity,
|
||||
DownloadRPM: result.DownloadRPM,
|
||||
UploadRPM: result.UploadRPM,
|
||||
IdleLatencyMs: result.IdleLatencyMs,
|
||||
IsFinal: true,
|
||||
DownloadCapacityAccuracy: int32(result.DownloadCapacityAccuracy),
|
||||
UploadCapacityAccuracy: int32(result.UploadCapacityAccuracy),
|
||||
DownloadRPMAccuracy: int32(result.DownloadRPMAccuracy),
|
||||
UploadRPMAccuracy: int32(result.UploadRPMAccuracy),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *StartedService) StartSTUNTest(
|
||||
request *STUNTestRequest,
|
||||
server grpc.ServerStreamingServer[STUNTestProgress],
|
||||
) error {
|
||||
err := s.waitForStarted(server.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.serviceAccess.RLock()
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
|
||||
outbound, err := resolveOutbound(boxService, request.OutboundTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0)
|
||||
|
||||
result, stunErr := stun.Run(stun.Options{
|
||||
Server: request.Server,
|
||||
Dialer: resolvedDialer,
|
||||
Context: server.Context(),
|
||||
OnProgress: func(p stun.Progress) {
|
||||
_ = server.Send(&STUNTestProgress{
|
||||
Phase: int32(p.Phase),
|
||||
ExternalAddr: p.ExternalAddr,
|
||||
LatencyMs: p.LatencyMs,
|
||||
NatMapping: int32(p.NATMapping),
|
||||
NatFiltering: int32(p.NATFiltering),
|
||||
})
|
||||
},
|
||||
})
|
||||
if stunErr != nil {
|
||||
return server.Send(&STUNTestProgress{
|
||||
IsFinal: true,
|
||||
Error: stunErr.Error(),
|
||||
})
|
||||
}
|
||||
return server.Send(&STUNTestProgress{
|
||||
Phase: int32(stun.PhaseDone),
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
NatMapping: int32(result.NATMapping),
|
||||
NatFiltering: int32(result.NATFiltering),
|
||||
IsFinal: true,
|
||||
NatTypeSupported: result.NATTypeSupported,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeTailscaleStatus(
|
||||
_ *emptypb.Empty,
|
||||
server grpc.ServerStreamingServer[TailscaleStatusUpdate],
|
||||
) error {
|
||||
err := s.waitForStarted(server.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.serviceAccess.RLock()
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
|
||||
endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx)
|
||||
if endpointManager == nil {
|
||||
return status.Error(codes.FailedPrecondition, "endpoint manager not available")
|
||||
}
|
||||
|
||||
type tailscaleEndpoint struct {
|
||||
tag string
|
||||
provider adapter.TailscaleEndpoint
|
||||
}
|
||||
var endpoints []tailscaleEndpoint
|
||||
for _, endpoint := range endpointManager.Endpoints() {
|
||||
if endpoint.Type() != C.TypeTailscale {
|
||||
continue
|
||||
}
|
||||
provider, loaded := endpoint.(adapter.TailscaleEndpoint)
|
||||
if !loaded {
|
||||
continue
|
||||
}
|
||||
endpoints = append(endpoints, tailscaleEndpoint{
|
||||
tag: endpoint.Tag(),
|
||||
provider: provider,
|
||||
})
|
||||
}
|
||||
if len(endpoints) == 0 {
|
||||
return status.Error(codes.NotFound, "no Tailscale endpoint found")
|
||||
}
|
||||
|
||||
type taggedStatus struct {
|
||||
tag string
|
||||
status *adapter.TailscaleEndpointStatus
|
||||
}
|
||||
updates := make(chan taggedStatus, len(endpoints))
|
||||
ctx, cancel := context.WithCancel(server.Context())
|
||||
defer cancel()
|
||||
|
||||
var waitGroup sync.WaitGroup
|
||||
for _, endpoint := range endpoints {
|
||||
waitGroup.Add(1)
|
||||
go func(tag string, provider adapter.TailscaleEndpoint) {
|
||||
defer waitGroup.Done()
|
||||
_ = provider.SubscribeTailscaleStatus(ctx, func(endpointStatus *adapter.TailscaleEndpointStatus) {
|
||||
select {
|
||||
case updates <- taggedStatus{tag: tag, status: endpointStatus}:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
})
|
||||
}(endpoint.tag, endpoint.provider)
|
||||
}
|
||||
|
||||
go func() {
|
||||
waitGroup.Wait()
|
||||
close(updates)
|
||||
}()
|
||||
|
||||
var tags []string
|
||||
statuses := make(map[string]*adapter.TailscaleEndpointStatus, len(endpoints))
|
||||
for update := range updates {
|
||||
if _, exists := statuses[update.tag]; !exists {
|
||||
tags = append(tags, update.tag)
|
||||
}
|
||||
statuses[update.tag] = update.status
|
||||
protoEndpoints := make([]*TailscaleEndpointStatus, 0, len(statuses))
|
||||
for _, tag := range tags {
|
||||
protoEndpoints = append(protoEndpoints, tailscaleEndpointStatusToProto(tag, statuses[tag]))
|
||||
}
|
||||
sendErr := server.Send(&TailscaleStatusUpdate{
|
||||
Endpoints: protoEndpoints,
|
||||
})
|
||||
if sendErr != nil {
|
||||
return sendErr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStatus) *TailscaleEndpointStatus {
|
||||
userGroups := make([]*TailscaleUserGroup, len(s.UserGroups))
|
||||
for i, group := range s.UserGroups {
|
||||
peers := make([]*TailscalePeer, len(group.Peers))
|
||||
for j, peer := range group.Peers {
|
||||
peers[j] = tailscalePeerToProto(peer)
|
||||
}
|
||||
userGroups[i] = &TailscaleUserGroup{
|
||||
UserID: group.UserID,
|
||||
LoginName: group.LoginName,
|
||||
DisplayName: group.DisplayName,
|
||||
ProfilePicURL: group.ProfilePicURL,
|
||||
Peers: peers,
|
||||
}
|
||||
}
|
||||
result := &TailscaleEndpointStatus{
|
||||
EndpointTag: tag,
|
||||
BackendState: s.BackendState,
|
||||
AuthURL: s.AuthURL,
|
||||
NetworkName: s.NetworkName,
|
||||
MagicDNSSuffix: s.MagicDNSSuffix,
|
||||
UserGroups: userGroups,
|
||||
}
|
||||
if s.Self != nil {
|
||||
result.Self = tailscalePeerToProto(s.Self)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func tailscalePeerToProto(peer *adapter.TailscalePeer) *TailscalePeer {
|
||||
return &TailscalePeer{
|
||||
HostName: peer.HostName,
|
||||
DnsName: peer.DNSName,
|
||||
Os: peer.OS,
|
||||
TailscaleIPs: peer.TailscaleIPs,
|
||||
Online: peer.Online,
|
||||
ExitNode: peer.ExitNode,
|
||||
ExitNodeOption: peer.ExitNodeOption,
|
||||
Active: peer.Active,
|
||||
RxBytes: peer.RxBytes,
|
||||
TxBytes: peer.TxBytes,
|
||||
KeyExpiry: peer.KeyExpiry,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StartedService) StartTailscalePing(
|
||||
request *TailscalePingRequest,
|
||||
server grpc.ServerStreamingServer[TailscalePingResponse],
|
||||
) error {
|
||||
err := s.waitForStarted(server.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.serviceAccess.RLock()
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
|
||||
endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx)
|
||||
if endpointManager == nil {
|
||||
return status.Error(codes.FailedPrecondition, "endpoint manager not available")
|
||||
}
|
||||
|
||||
var provider adapter.TailscaleEndpoint
|
||||
if request.EndpointTag != "" {
|
||||
endpoint, loaded := endpointManager.Get(request.EndpointTag)
|
||||
if !loaded {
|
||||
return status.Error(codes.NotFound, "endpoint not found: "+request.EndpointTag)
|
||||
}
|
||||
if endpoint.Type() != C.TypeTailscale {
|
||||
return status.Error(codes.InvalidArgument, "endpoint is not Tailscale: "+request.EndpointTag)
|
||||
}
|
||||
pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint)
|
||||
if !loaded {
|
||||
return status.Error(codes.FailedPrecondition, "endpoint does not support ping")
|
||||
}
|
||||
provider = pingProvider
|
||||
} else {
|
||||
for _, endpoint := range endpointManager.Endpoints() {
|
||||
if endpoint.Type() != C.TypeTailscale {
|
||||
continue
|
||||
}
|
||||
pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint)
|
||||
if loaded {
|
||||
provider = pingProvider
|
||||
break
|
||||
}
|
||||
}
|
||||
if provider == nil {
|
||||
return status.Error(codes.NotFound, "no Tailscale endpoint found")
|
||||
}
|
||||
}
|
||||
|
||||
return provider.StartTailscalePing(server.Context(), request.PeerIP, func(result *adapter.TailscalePingResult) {
|
||||
_ = server.Send(&TailscalePingResponse{
|
||||
LatencyMs: result.LatencyMs,
|
||||
IsDirect: result.IsDirect,
|
||||
Endpoint: result.Endpoint,
|
||||
DerpRegionID: result.DERPRegionID,
|
||||
DerpRegionCode: result.DERPRegionCode,
|
||||
Error: result.Error,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,20 +26,12 @@ service StartedService {
|
||||
|
||||
rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
|
||||
rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {}
|
||||
rpc TriggerDebugCrash(DebugCrashRequest) returns(google.protobuf.Empty) {}
|
||||
rpc TriggerOOMReport(google.protobuf.Empty) returns(google.protobuf.Empty) {}
|
||||
|
||||
rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream ConnectionEvents) {}
|
||||
rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {}
|
||||
rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {}
|
||||
rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {}
|
||||
rpc GetStartedAt(google.protobuf.Empty) returns(StartedAt) {}
|
||||
|
||||
rpc SubscribeOutbounds(google.protobuf.Empty) returns (stream OutboundList) {}
|
||||
rpc StartNetworkQualityTest(NetworkQualityTestRequest) returns (stream NetworkQualityTestProgress) {}
|
||||
rpc StartSTUNTest(STUNTestRequest) returns (stream STUNTestProgress) {}
|
||||
rpc SubscribeTailscaleStatus(google.protobuf.Empty) returns (stream TailscaleStatusUpdate) {}
|
||||
rpc StartTailscalePing(TailscalePingRequest) returns (stream TailscalePingResponse) {}
|
||||
}
|
||||
|
||||
message ServiceStatus {
|
||||
@@ -149,15 +141,6 @@ message SetSystemProxyEnabledRequest {
|
||||
bool enabled = 1;
|
||||
}
|
||||
|
||||
message DebugCrashRequest {
|
||||
enum Type {
|
||||
GO = 0;
|
||||
NATIVE = 1;
|
||||
}
|
||||
|
||||
Type type = 1;
|
||||
}
|
||||
|
||||
message SubscribeConnectionsRequest {
|
||||
int64 interval = 1;
|
||||
}
|
||||
@@ -227,105 +210,8 @@ message DeprecatedWarning {
|
||||
string message = 1;
|
||||
bool impending = 2;
|
||||
string migrationLink = 3;
|
||||
string description = 4;
|
||||
string deprecatedVersion = 5;
|
||||
string scheduledVersion = 6;
|
||||
}
|
||||
|
||||
message StartedAt {
|
||||
int64 startedAt = 1;
|
||||
}
|
||||
|
||||
message OutboundList {
|
||||
repeated GroupItem outbounds = 1;
|
||||
}
|
||||
|
||||
message NetworkQualityTestRequest {
|
||||
string configURL = 1;
|
||||
string outboundTag = 2;
|
||||
bool serial = 3;
|
||||
int32 maxRuntimeSeconds = 4;
|
||||
bool http3 = 5;
|
||||
}
|
||||
|
||||
message NetworkQualityTestProgress {
|
||||
int32 phase = 1;
|
||||
int64 downloadCapacity = 2;
|
||||
int64 uploadCapacity = 3;
|
||||
int32 downloadRPM = 4;
|
||||
int32 uploadRPM = 5;
|
||||
int32 idleLatencyMs = 6;
|
||||
int64 elapsedMs = 7;
|
||||
bool isFinal = 8;
|
||||
string error = 9;
|
||||
int32 downloadCapacityAccuracy = 10;
|
||||
int32 uploadCapacityAccuracy = 11;
|
||||
int32 downloadRPMAccuracy = 12;
|
||||
int32 uploadRPMAccuracy = 13;
|
||||
}
|
||||
|
||||
message STUNTestRequest {
|
||||
string server = 1;
|
||||
string outboundTag = 2;
|
||||
}
|
||||
|
||||
message STUNTestProgress {
|
||||
int32 phase = 1;
|
||||
string externalAddr = 2;
|
||||
int32 latencyMs = 3;
|
||||
int32 natMapping = 4;
|
||||
int32 natFiltering = 5;
|
||||
bool isFinal = 6;
|
||||
string error = 7;
|
||||
bool natTypeSupported = 8;
|
||||
}
|
||||
|
||||
message TailscaleStatusUpdate {
|
||||
repeated TailscaleEndpointStatus endpoints = 1;
|
||||
}
|
||||
|
||||
message TailscaleEndpointStatus {
|
||||
string endpointTag = 1;
|
||||
string backendState = 2;
|
||||
string authURL = 3;
|
||||
string networkName = 4;
|
||||
string magicDNSSuffix = 5;
|
||||
TailscalePeer self = 6;
|
||||
repeated TailscaleUserGroup userGroups = 7;
|
||||
}
|
||||
|
||||
message TailscaleUserGroup {
|
||||
int64 userID = 1;
|
||||
string loginName = 2;
|
||||
string displayName = 3;
|
||||
string profilePicURL = 4;
|
||||
repeated TailscalePeer peers = 5;
|
||||
}
|
||||
|
||||
message TailscalePeer {
|
||||
string hostName = 1;
|
||||
string dnsName = 2;
|
||||
string os = 3;
|
||||
repeated string tailscaleIPs = 4;
|
||||
bool online = 5;
|
||||
bool exitNode = 6;
|
||||
bool exitNodeOption = 7;
|
||||
bool active = 8;
|
||||
int64 rxBytes = 9;
|
||||
int64 txBytes = 10;
|
||||
int64 keyExpiry = 11;
|
||||
}
|
||||
|
||||
message TailscalePingRequest {
|
||||
string endpointTag = 1;
|
||||
string peerIP = 2;
|
||||
}
|
||||
|
||||
message TailscalePingResponse {
|
||||
double latencyMs = 1;
|
||||
bool isDirect = 2;
|
||||
string endpoint = 3;
|
||||
int32 derpRegionID = 4;
|
||||
string derpRegionCode = 5;
|
||||
string error = 6;
|
||||
}
|
||||
}
|
||||
@@ -15,34 +15,27 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService"
|
||||
StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService"
|
||||
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
|
||||
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
|
||||
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
|
||||
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
|
||||
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
|
||||
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
|
||||
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
|
||||
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
|
||||
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
|
||||
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
|
||||
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
|
||||
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
|
||||
StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus"
|
||||
StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled"
|
||||
StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash"
|
||||
StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport"
|
||||
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
|
||||
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
|
||||
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
|
||||
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
|
||||
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
|
||||
StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds"
|
||||
StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest"
|
||||
StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest"
|
||||
StartedService_SubscribeTailscaleStatus_FullMethodName = "/daemon.StartedService/SubscribeTailscaleStatus"
|
||||
StartedService_StartTailscalePing_FullMethodName = "/daemon.StartedService/StartTailscalePing"
|
||||
StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService"
|
||||
StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService"
|
||||
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
|
||||
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
|
||||
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
|
||||
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
|
||||
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
|
||||
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
|
||||
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
|
||||
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
|
||||
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
|
||||
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
|
||||
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
|
||||
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
|
||||
StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus"
|
||||
StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled"
|
||||
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
|
||||
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
|
||||
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
|
||||
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
|
||||
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
|
||||
)
|
||||
|
||||
// StartedServiceClient is the client API for StartedService service.
|
||||
@@ -65,18 +58,11 @@ type StartedServiceClient interface {
|
||||
SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error)
|
||||
SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error)
|
||||
CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error)
|
||||
GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error)
|
||||
SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error)
|
||||
StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error)
|
||||
StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error)
|
||||
SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error)
|
||||
StartTailscalePing(ctx context.Context, in *TailscalePingRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscalePingResponse], error)
|
||||
}
|
||||
|
||||
type startedServiceClient struct {
|
||||
@@ -292,26 +278,6 @@ func (c *startedServiceClient) SetSystemProxyEnabled(ctx context.Context, in *Se
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *startedServiceClient) TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, StartedService_TriggerDebugCrash_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *startedServiceClient) TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, StartedService_TriggerOOMReport_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...)
|
||||
@@ -371,101 +337,6 @@ func (c *startedServiceClient) GetStartedAt(ctx context.Context, in *emptypb.Emp
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *startedServiceClient) SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[6], StartedService_SubscribeOutbounds_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[emptypb.Empty, OutboundList]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_SubscribeOutboundsClient = grpc.ServerStreamingClient[OutboundList]
|
||||
|
||||
func (c *startedServiceClient) StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[7], StartedService_StartNetworkQualityTest_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[NetworkQualityTestRequest, NetworkQualityTestProgress]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_StartNetworkQualityTestClient = grpc.ServerStreamingClient[NetworkQualityTestProgress]
|
||||
|
||||
func (c *startedServiceClient) StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[8], StartedService_StartSTUNTest_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[STUNTestRequest, STUNTestProgress]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_StartSTUNTestClient = grpc.ServerStreamingClient[STUNTestProgress]
|
||||
|
||||
func (c *startedServiceClient) SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[9], StartedService_SubscribeTailscaleStatus_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[emptypb.Empty, TailscaleStatusUpdate]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_SubscribeTailscaleStatusClient = grpc.ServerStreamingClient[TailscaleStatusUpdate]
|
||||
|
||||
func (c *startedServiceClient) StartTailscalePing(ctx context.Context, in *TailscalePingRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscalePingResponse], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[10], StartedService_StartTailscalePing_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[TailscalePingRequest, TailscalePingResponse]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_StartTailscalePingClient = grpc.ServerStreamingClient[TailscalePingResponse]
|
||||
|
||||
// StartedServiceServer is the server API for StartedService service.
|
||||
// All implementations must embed UnimplementedStartedServiceServer
|
||||
// for forward compatibility.
|
||||
@@ -486,18 +357,11 @@ type StartedServiceServer interface {
|
||||
SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error)
|
||||
GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error)
|
||||
SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error)
|
||||
TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error)
|
||||
TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||
SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error
|
||||
CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error)
|
||||
CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||
GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error)
|
||||
GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error)
|
||||
SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error
|
||||
StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error
|
||||
StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error
|
||||
SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error
|
||||
StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error
|
||||
mustEmbedUnimplementedStartedServiceServer()
|
||||
}
|
||||
|
||||
@@ -572,14 +436,6 @@ func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context,
|
||||
return nil, status.Error(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method TriggerDebugCrash not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method TriggerOOMReport not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error {
|
||||
return status.Error(codes.Unimplemented, "method SubscribeConnections not implemented")
|
||||
}
|
||||
@@ -599,26 +455,6 @@ func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context,
|
||||
func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error {
|
||||
return status.Error(codes.Unimplemented, "method SubscribeOutbounds not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error {
|
||||
return status.Error(codes.Unimplemented, "method StartNetworkQualityTest not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error {
|
||||
return status.Error(codes.Unimplemented, "method StartSTUNTest not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error {
|
||||
return status.Error(codes.Unimplemented, "method SubscribeTailscaleStatus not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error {
|
||||
return status.Error(codes.Unimplemented, "method StartTailscalePing not implemented")
|
||||
}
|
||||
func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {}
|
||||
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
@@ -893,42 +729,6 @@ func _StartedService_SetSystemProxyEnabled_Handler(srv interface{}, ctx context.
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StartedService_TriggerDebugCrash_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DebugCrashRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StartedServiceServer).TriggerDebugCrash(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: StartedService_TriggerDebugCrash_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StartedServiceServer).TriggerDebugCrash(ctx, req.(*DebugCrashRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StartedService_TriggerOOMReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(emptypb.Empty)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StartedServiceServer).TriggerOOMReport(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: StartedService_TriggerOOMReport_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StartedServiceServer).TriggerOOMReport(ctx, req.(*emptypb.Empty))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StartedService_SubscribeConnections_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(SubscribeConnectionsRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
@@ -1012,61 +812,6 @@ func _StartedService_GetStartedAt_Handler(srv interface{}, ctx context.Context,
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StartedService_SubscribeOutbounds_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(emptypb.Empty)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(StartedServiceServer).SubscribeOutbounds(m, &grpc.GenericServerStream[emptypb.Empty, OutboundList]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_SubscribeOutboundsServer = grpc.ServerStreamingServer[OutboundList]
|
||||
|
||||
func _StartedService_StartNetworkQualityTest_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(NetworkQualityTestRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(StartedServiceServer).StartNetworkQualityTest(m, &grpc.GenericServerStream[NetworkQualityTestRequest, NetworkQualityTestProgress]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_StartNetworkQualityTestServer = grpc.ServerStreamingServer[NetworkQualityTestProgress]
|
||||
|
||||
func _StartedService_StartSTUNTest_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(STUNTestRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(StartedServiceServer).StartSTUNTest(m, &grpc.GenericServerStream[STUNTestRequest, STUNTestProgress]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_StartSTUNTestServer = grpc.ServerStreamingServer[STUNTestProgress]
|
||||
|
||||
func _StartedService_SubscribeTailscaleStatus_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(emptypb.Empty)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(StartedServiceServer).SubscribeTailscaleStatus(m, &grpc.GenericServerStream[emptypb.Empty, TailscaleStatusUpdate]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_SubscribeTailscaleStatusServer = grpc.ServerStreamingServer[TailscaleStatusUpdate]
|
||||
|
||||
func _StartedService_StartTailscalePing_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(TailscalePingRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(StartedServiceServer).StartTailscalePing(m, &grpc.GenericServerStream[TailscalePingRequest, TailscalePingResponse]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_StartTailscalePingServer = grpc.ServerStreamingServer[TailscalePingResponse]
|
||||
|
||||
// StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -1118,14 +863,6 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "SetSystemProxyEnabled",
|
||||
Handler: _StartedService_SetSystemProxyEnabled_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "TriggerDebugCrash",
|
||||
Handler: _StartedService_TriggerDebugCrash_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "TriggerOOMReport",
|
||||
Handler: _StartedService_TriggerOOMReport_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CloseConnection",
|
||||
Handler: _StartedService_CloseConnection_Handler,
|
||||
@@ -1174,31 +911,6 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
|
||||
Handler: _StartedService_SubscribeConnections_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "SubscribeOutbounds",
|
||||
Handler: _StartedService_SubscribeOutbounds_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "StartNetworkQualityTest",
|
||||
Handler: _StartedService_StartNetworkQualityTest_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "StartSTUNTest",
|
||||
Handler: _StartedService_StartSTUNTest_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "SubscribeTailscaleStatus",
|
||||
Handler: _StartedService_SubscribeTailscaleStatus_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "StartTailscalePing",
|
||||
Handler: _StartedService_StartTailscalePing_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "daemon/started_service.proto",
|
||||
}
|
||||
|
||||
595
dns/client.go
595
dns/client.go
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/task"
|
||||
"github.com/sagernet/sing/contrab/freelru"
|
||||
"github.com/sagernet/sing/contrab/maphash"
|
||||
@@ -30,63 +32,59 @@ var (
|
||||
var _ adapter.DNSClient = (*Client)(nil)
|
||||
|
||||
type Client struct {
|
||||
ctx context.Context
|
||||
timeout time.Duration
|
||||
disableCache bool
|
||||
disableExpire bool
|
||||
optimisticTimeout time.Duration
|
||||
cacheCapacity uint32
|
||||
clientSubnet netip.Prefix
|
||||
rdrc adapter.RDRCStore
|
||||
initRDRCFunc func() adapter.RDRCStore
|
||||
dnsCache adapter.DNSCacheStore
|
||||
initDNSCacheFunc func() adapter.DNSCacheStore
|
||||
logger logger.ContextLogger
|
||||
cache freelru.Cache[dnsCacheKey, *dns.Msg]
|
||||
cacheLock compatible.Map[dnsCacheKey, chan struct{}]
|
||||
backgroundRefresh compatible.Map[dnsCacheKey, struct{}]
|
||||
timeout time.Duration
|
||||
disableCache bool
|
||||
disableExpire bool
|
||||
independentCache bool
|
||||
clientSubnet netip.Prefix
|
||||
rdrc adapter.RDRCStore
|
||||
initRDRCFunc func() adapter.RDRCStore
|
||||
logger logger.ContextLogger
|
||||
cache freelru.Cache[dns.Question, *dns.Msg]
|
||||
cacheLock compatible.Map[dns.Question, chan struct{}]
|
||||
transportCache freelru.Cache[transportCacheKey, *dns.Msg]
|
||||
transportCacheLock compatible.Map[dns.Question, chan struct{}]
|
||||
}
|
||||
|
||||
type ClientOptions struct {
|
||||
Context context.Context
|
||||
Timeout time.Duration
|
||||
DisableCache bool
|
||||
DisableExpire bool
|
||||
OptimisticTimeout time.Duration
|
||||
CacheCapacity uint32
|
||||
ClientSubnet netip.Prefix
|
||||
RDRC func() adapter.RDRCStore
|
||||
DNSCache func() adapter.DNSCacheStore
|
||||
Logger logger.ContextLogger
|
||||
Timeout time.Duration
|
||||
DisableCache bool
|
||||
DisableExpire bool
|
||||
IndependentCache bool
|
||||
CacheCapacity uint32
|
||||
ClientSubnet netip.Prefix
|
||||
RDRC func() adapter.RDRCStore
|
||||
Logger logger.ContextLogger
|
||||
}
|
||||
|
||||
func NewClient(options ClientOptions) *Client {
|
||||
cacheCapacity := options.CacheCapacity
|
||||
if cacheCapacity < 1024 {
|
||||
cacheCapacity = 1024
|
||||
}
|
||||
client := &Client{
|
||||
ctx: options.Context,
|
||||
timeout: options.Timeout,
|
||||
disableCache: options.DisableCache,
|
||||
disableExpire: options.DisableExpire,
|
||||
optimisticTimeout: options.OptimisticTimeout,
|
||||
cacheCapacity: cacheCapacity,
|
||||
clientSubnet: options.ClientSubnet,
|
||||
initRDRCFunc: options.RDRC,
|
||||
initDNSCacheFunc: options.DNSCache,
|
||||
logger: options.Logger,
|
||||
timeout: options.Timeout,
|
||||
disableCache: options.DisableCache,
|
||||
disableExpire: options.DisableExpire,
|
||||
independentCache: options.IndependentCache,
|
||||
clientSubnet: options.ClientSubnet,
|
||||
initRDRCFunc: options.RDRC,
|
||||
logger: options.Logger,
|
||||
}
|
||||
if client.timeout == 0 {
|
||||
client.timeout = C.DNSTimeout
|
||||
}
|
||||
if !client.disableCache && client.initDNSCacheFunc == nil {
|
||||
client.initializeMemoryCache()
|
||||
cacheCapacity := options.CacheCapacity
|
||||
if cacheCapacity < 1024 {
|
||||
cacheCapacity = 1024
|
||||
}
|
||||
if !client.disableCache {
|
||||
if !client.independentCache {
|
||||
client.cache = common.Must1(freelru.NewSharded[dns.Question, *dns.Msg](cacheCapacity, maphash.NewHasher[dns.Question]().Hash32))
|
||||
} else {
|
||||
client.transportCache = common.Must1(freelru.NewSharded[transportCacheKey, *dns.Msg](cacheCapacity, maphash.NewHasher[transportCacheKey]().Hash32))
|
||||
}
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
type dnsCacheKey struct {
|
||||
type transportCacheKey struct {
|
||||
dns.Question
|
||||
transportTag string
|
||||
}
|
||||
@@ -95,19 +93,6 @@ func (c *Client) Start() {
|
||||
if c.initRDRCFunc != nil {
|
||||
c.rdrc = c.initRDRCFunc()
|
||||
}
|
||||
if c.initDNSCacheFunc != nil {
|
||||
c.dnsCache = c.initDNSCacheFunc()
|
||||
}
|
||||
if c.dnsCache == nil {
|
||||
c.initializeMemoryCache()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) initializeMemoryCache() {
|
||||
if c.disableCache || c.cache != nil {
|
||||
return
|
||||
}
|
||||
c.cache = common.Must1(freelru.NewSharded[dnsCacheKey, *dns.Msg](c.cacheCapacity, maphash.NewHasher[dnsCacheKey]().Hash32))
|
||||
}
|
||||
|
||||
func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
|
||||
@@ -124,38 +109,7 @@ func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func computeTimeToLive(response *dns.Msg) uint32 {
|
||||
var timeToLive uint32
|
||||
if len(response.Answer) == 0 {
|
||||
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
|
||||
return soaTTL
|
||||
}
|
||||
}
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
||||
timeToLive = record.Header().Ttl
|
||||
}
|
||||
}
|
||||
}
|
||||
return timeToLive
|
||||
}
|
||||
|
||||
func normalizeTTL(response *dns.Msg, timeToLive uint32) {
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
record.Header().Ttl = timeToLive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error) {
|
||||
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
|
||||
if len(message.Question) == 0 {
|
||||
if c.logger != nil {
|
||||
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
||||
@@ -169,7 +123,13 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
}
|
||||
return FixedResponseStatus(message, dns.RcodeSuccess), nil
|
||||
}
|
||||
message = c.prepareExchangeMessage(message, options)
|
||||
clientSubnet := options.ClientSubnet
|
||||
if !clientSubnet.IsValid() {
|
||||
clientSubnet = c.clientSubnet
|
||||
}
|
||||
if clientSubnet.IsValid() {
|
||||
message = SetClientSubnet(message, clientSubnet)
|
||||
}
|
||||
|
||||
isSimpleRequest := len(message.Question) == 1 &&
|
||||
len(message.Ns) == 0 &&
|
||||
@@ -181,32 +141,40 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
!options.ClientSubnet.IsValid()
|
||||
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
|
||||
if !disableCache {
|
||||
cacheKey := dnsCacheKey{Question: question, transportTag: transport.Tag()}
|
||||
cond, loaded := c.cacheLock.LoadOrStore(cacheKey, make(chan struct{}))
|
||||
if loaded {
|
||||
select {
|
||||
case <-cond:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
if c.cache != nil {
|
||||
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
|
||||
if loaded {
|
||||
select {
|
||||
case <-cond:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
} else {
|
||||
defer func() {
|
||||
c.cacheLock.Delete(question)
|
||||
close(cond)
|
||||
}()
|
||||
}
|
||||
} else if c.transportCache != nil {
|
||||
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
|
||||
if loaded {
|
||||
select {
|
||||
case <-cond:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
} else {
|
||||
defer func() {
|
||||
c.transportCacheLock.Delete(question)
|
||||
close(cond)
|
||||
}()
|
||||
}
|
||||
} else {
|
||||
defer func() {
|
||||
c.cacheLock.Delete(cacheKey)
|
||||
close(cond)
|
||||
}()
|
||||
}
|
||||
response, ttl, isStale := c.loadResponse(question, transport)
|
||||
response, ttl := c.loadResponse(question, transport)
|
||||
if response != nil {
|
||||
if isStale && !options.DisableOptimisticCache {
|
||||
c.backgroundRefreshDNS(transport, question, message.Copy(), options, responseChecker)
|
||||
logOptimisticResponse(c.logger, ctx, response)
|
||||
response.Id = message.Id
|
||||
return response, nil
|
||||
} else if !isStale {
|
||||
logCachedResponse(c.logger, ctx, response, ttl)
|
||||
response.Id = message.Id
|
||||
return response, nil
|
||||
}
|
||||
logCachedResponse(c.logger, ctx, response, ttl)
|
||||
response.Id = message.Id
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,17 +190,62 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
return nil, ErrResponseRejectedCached
|
||||
}
|
||||
}
|
||||
response, err := c.exchangeToTransport(ctx, transport, message)
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
response, err := transport.Exchange(ctx, message)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var rcodeError RcodeError
|
||||
if errors.As(err, &rcodeError) {
|
||||
response = FixedResponseStatus(message, int(rcodeError))
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
/*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
|
||||
validResponse := response
|
||||
loop:
|
||||
for {
|
||||
var (
|
||||
addresses int
|
||||
queryCNAME string
|
||||
)
|
||||
for _, rawRR := range validResponse.Answer {
|
||||
switch rr := rawRR.(type) {
|
||||
case *dns.A:
|
||||
break loop
|
||||
case *dns.AAAA:
|
||||
break loop
|
||||
case *dns.CNAME:
|
||||
queryCNAME = rr.Target
|
||||
}
|
||||
}
|
||||
if queryCNAME == "" {
|
||||
break
|
||||
}
|
||||
exMessage := *message
|
||||
exMessage.Question = []dns.Question{{
|
||||
Name: queryCNAME,
|
||||
Qtype: question.Qtype,
|
||||
}}
|
||||
validResponse, err = c.Exchange(ctx, transport, &exMessage, options, responseChecker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if validResponse != response {
|
||||
response.Answer = append(response.Answer, validResponse.Answer...)
|
||||
}
|
||||
}*/
|
||||
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
|
||||
if responseChecker != nil {
|
||||
var rejected bool
|
||||
// TODO: add accept_any rule and support to check response instead of addresses
|
||||
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
||||
rejected = true
|
||||
} else if len(response.Answer) == 0 {
|
||||
rejected = !responseChecker(nil)
|
||||
} else {
|
||||
rejected = !responseChecker(response)
|
||||
rejected = !responseChecker(MessageToAddresses(response))
|
||||
}
|
||||
if rejected {
|
||||
if !disableCache && c.rdrc != nil {
|
||||
@@ -242,7 +255,54 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
return response, ErrResponseRejected
|
||||
}
|
||||
}
|
||||
timeToLive := applyResponseOptions(question, response, options)
|
||||
if question.Qtype == dns.TypeHTTPS {
|
||||
if options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only {
|
||||
for _, rr := range response.Answer {
|
||||
https, isHTTPS := rr.(*dns.HTTPS)
|
||||
if !isHTTPS {
|
||||
continue
|
||||
}
|
||||
content := https.SVCB
|
||||
content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool {
|
||||
if options.Strategy == C.DomainStrategyIPv4Only {
|
||||
return it.Key() != dns.SVCB_IPV6HINT
|
||||
} else {
|
||||
return it.Key() != dns.SVCB_IPV4HINT
|
||||
}
|
||||
})
|
||||
https.SVCB = content
|
||||
}
|
||||
}
|
||||
}
|
||||
var timeToLive uint32
|
||||
if len(response.Answer) == 0 {
|
||||
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
|
||||
timeToLive = soaTTL
|
||||
}
|
||||
}
|
||||
if timeToLive == 0 {
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
||||
timeToLive = record.Header().Ttl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if options.RewriteTTL != nil {
|
||||
timeToLive = *options.RewriteTTL
|
||||
}
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
record.Header().Ttl = timeToLive
|
||||
}
|
||||
}
|
||||
if !disableCache {
|
||||
c.storeCache(transport, question, response, timeToLive)
|
||||
}
|
||||
@@ -261,7 +321,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
|
||||
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
||||
domain = FqdnToDomain(domain)
|
||||
dnsName := dns.Fqdn(domain)
|
||||
var strategy C.DomainStrategy
|
||||
@@ -308,12 +368,8 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
|
||||
func (c *Client) ClearCache() {
|
||||
if c.cache != nil {
|
||||
c.cache.Purge()
|
||||
}
|
||||
if c.dnsCache != nil {
|
||||
err := c.dnsCache.ClearDNSCache()
|
||||
if err != nil && c.logger != nil {
|
||||
c.logger.Warn("clear DNS cache: ", err)
|
||||
}
|
||||
} else if c.transportCache != nil {
|
||||
c.transportCache.Purge()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,44 +385,46 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
|
||||
if timeToLive == 0 {
|
||||
return
|
||||
}
|
||||
if c.dnsCache != nil {
|
||||
packed, err := message.Pack()
|
||||
if err == nil {
|
||||
expireAt := time.Now().Add(time.Second * time.Duration(timeToLive))
|
||||
c.dnsCache.SaveDNSCacheAsync(transport.Tag(), question.Name, question.Qtype, packed, expireAt, c.logger)
|
||||
}
|
||||
return
|
||||
}
|
||||
if c.cache == nil {
|
||||
return
|
||||
}
|
||||
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
|
||||
if c.disableExpire {
|
||||
c.cache.Add(key, message.Copy())
|
||||
if !c.independentCache {
|
||||
c.cache.Add(question, message.Copy())
|
||||
} else {
|
||||
c.transportCache.Add(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
}, message.Copy())
|
||||
}
|
||||
} else {
|
||||
c.cache.AddWithLifetime(key, message.Copy(), time.Second*time.Duration(timeToLive))
|
||||
if !c.independentCache {
|
||||
c.cache.AddWithLifetime(question, message.Copy(), time.Second*time.Duration(timeToLive))
|
||||
} else {
|
||||
c.transportCache.AddWithLifetime(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
}, message.Copy(), time.Second*time.Duration(timeToLive))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
|
||||
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
||||
question := dns.Question{
|
||||
Name: name,
|
||||
Qtype: qType,
|
||||
Qclass: dns.ClassINET,
|
||||
}
|
||||
disableCache := c.disableCache || options.DisableCache
|
||||
if !disableCache {
|
||||
cachedAddresses, err := c.questionCache(question, transport)
|
||||
if err != ErrNotCached {
|
||||
return cachedAddresses, err
|
||||
}
|
||||
}
|
||||
message := dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
RecursionDesired: true,
|
||||
},
|
||||
Question: []dns.Question{question},
|
||||
}
|
||||
disableCache := c.disableCache || options.DisableCache
|
||||
if !disableCache {
|
||||
cachedAddresses, err := c.questionCache(ctx, transport, &message, options, responseChecker)
|
||||
if err != ErrNotCached {
|
||||
return cachedAddresses, err
|
||||
}
|
||||
}
|
||||
response, err := c.Exchange(ctx, transport, &message, options, responseChecker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -377,181 +435,120 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran
|
||||
return MessageToAddresses(response), nil
|
||||
}
|
||||
|
||||
func (c *Client) questionCache(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
|
||||
question := message.Question[0]
|
||||
response, _, isStale := c.loadResponse(question, transport)
|
||||
func (c *Client) questionCache(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) {
|
||||
response, _ := c.loadResponse(question, transport)
|
||||
if response == nil {
|
||||
return nil, ErrNotCached
|
||||
}
|
||||
if isStale {
|
||||
if options.DisableOptimisticCache {
|
||||
return nil, ErrNotCached
|
||||
}
|
||||
c.backgroundRefreshDNS(transport, question, c.prepareExchangeMessage(message.Copy(), options), options, responseChecker)
|
||||
logOptimisticResponse(c.logger, ctx, response)
|
||||
}
|
||||
if response.Rcode != dns.RcodeSuccess {
|
||||
return nil, RcodeError(response.Rcode)
|
||||
}
|
||||
return MessageToAddresses(response), nil
|
||||
}
|
||||
|
||||
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) {
|
||||
if c.dnsCache != nil {
|
||||
return c.loadPersistentResponse(question, transport)
|
||||
}
|
||||
if c.cache == nil {
|
||||
return nil, 0, false
|
||||
}
|
||||
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
|
||||
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int) {
|
||||
var (
|
||||
response *dns.Msg
|
||||
loaded bool
|
||||
)
|
||||
if c.disableExpire {
|
||||
response, loaded := c.cache.Get(key)
|
||||
if !loaded {
|
||||
return nil, 0, false
|
||||
}
|
||||
return response.Copy(), 0, false
|
||||
}
|
||||
response, expireAt, loaded := c.cache.GetWithLifetimeNoExpire(key)
|
||||
if !loaded {
|
||||
return nil, 0, false
|
||||
}
|
||||
timeNow := time.Now()
|
||||
if timeNow.After(expireAt) {
|
||||
if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) {
|
||||
response = response.Copy()
|
||||
normalizeTTL(response, 1)
|
||||
return response, 0, true
|
||||
}
|
||||
c.cache.Remove(key)
|
||||
return nil, 0, false
|
||||
}
|
||||
nowTTL := int(expireAt.Sub(timeNow).Seconds())
|
||||
if nowTTL < 0 {
|
||||
nowTTL = 0
|
||||
}
|
||||
response = response.Copy()
|
||||
normalizeTTL(response, uint32(nowTTL))
|
||||
return response, nowTTL, false
|
||||
}
|
||||
|
||||
func (c *Client) loadPersistentResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) {
|
||||
rawMessage, expireAt, loaded := c.dnsCache.LoadDNSCache(transport.Tag(), question.Name, question.Qtype)
|
||||
if !loaded {
|
||||
return nil, 0, false
|
||||
}
|
||||
response := new(dns.Msg)
|
||||
err := response.Unpack(rawMessage)
|
||||
if err != nil {
|
||||
return nil, 0, false
|
||||
}
|
||||
if c.disableExpire {
|
||||
return response, 0, false
|
||||
}
|
||||
timeNow := time.Now()
|
||||
if timeNow.After(expireAt) {
|
||||
if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) {
|
||||
normalizeTTL(response, 1)
|
||||
return response, 0, true
|
||||
}
|
||||
return nil, 0, false
|
||||
}
|
||||
nowTTL := int(expireAt.Sub(timeNow).Seconds())
|
||||
if nowTTL < 0 {
|
||||
nowTTL = 0
|
||||
}
|
||||
normalizeTTL(response, uint32(nowTTL))
|
||||
return response, nowTTL, false
|
||||
}
|
||||
|
||||
func applyResponseOptions(question dns.Question, response *dns.Msg, options adapter.DNSQueryOptions) uint32 {
|
||||
if question.Qtype == dns.TypeHTTPS && (options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only) {
|
||||
for _, rr := range response.Answer {
|
||||
https, isHTTPS := rr.(*dns.HTTPS)
|
||||
if !isHTTPS {
|
||||
continue
|
||||
}
|
||||
content := https.SVCB
|
||||
content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool {
|
||||
if options.Strategy == C.DomainStrategyIPv4Only {
|
||||
return it.Key() != dns.SVCB_IPV6HINT
|
||||
}
|
||||
return it.Key() != dns.SVCB_IPV4HINT
|
||||
if !c.independentCache {
|
||||
response, loaded = c.cache.Get(question)
|
||||
} else {
|
||||
response, loaded = c.transportCache.Get(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
})
|
||||
https.SVCB = content
|
||||
}
|
||||
}
|
||||
timeToLive := computeTimeToLive(response)
|
||||
if options.RewriteTTL != nil {
|
||||
timeToLive = *options.RewriteTTL
|
||||
}
|
||||
normalizeTTL(response, timeToLive)
|
||||
return timeToLive
|
||||
}
|
||||
|
||||
func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) {
|
||||
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
|
||||
_, loaded := c.backgroundRefresh.LoadOrStore(key, struct{}{})
|
||||
if loaded {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer c.backgroundRefresh.Delete(key)
|
||||
ctx := contextWithTransportTag(c.ctx, transport.Tag())
|
||||
response, err := c.exchangeToTransport(ctx, transport, message)
|
||||
if err != nil {
|
||||
if c.logger != nil {
|
||||
c.logger.Debug("optimistic refresh failed for ", FqdnToDomain(question.Name), ": ", err)
|
||||
}
|
||||
return
|
||||
if !loaded {
|
||||
return nil, 0
|
||||
}
|
||||
if responseChecker != nil {
|
||||
var rejected bool
|
||||
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
||||
rejected = true
|
||||
return response.Copy(), 0
|
||||
} else {
|
||||
var expireAt time.Time
|
||||
if !c.independentCache {
|
||||
response, expireAt, loaded = c.cache.GetWithLifetime(question)
|
||||
} else {
|
||||
response, expireAt, loaded = c.transportCache.GetWithLifetime(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
})
|
||||
}
|
||||
if !loaded {
|
||||
return nil, 0
|
||||
}
|
||||
timeNow := time.Now()
|
||||
if timeNow.After(expireAt) {
|
||||
if !c.independentCache {
|
||||
c.cache.Remove(question)
|
||||
} else {
|
||||
rejected = !responseChecker(response)
|
||||
c.transportCache.Remove(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
})
|
||||
}
|
||||
if rejected {
|
||||
if c.rdrc != nil {
|
||||
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
|
||||
}
|
||||
return
|
||||
}
|
||||
} else if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
||||
return
|
||||
return nil, 0
|
||||
}
|
||||
timeToLive := applyResponseOptions(question, response, options)
|
||||
c.storeCache(transport, question, response, timeToLive)
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *Client) prepareExchangeMessage(message *dns.Msg, options adapter.DNSQueryOptions) *dns.Msg {
|
||||
clientSubnet := options.ClientSubnet
|
||||
if !clientSubnet.IsValid() {
|
||||
clientSubnet = c.clientSubnet
|
||||
var originTTL int
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
|
||||
originTTL = int(record.Header().Ttl)
|
||||
}
|
||||
}
|
||||
}
|
||||
nowTTL := int(expireAt.Sub(timeNow).Seconds())
|
||||
if nowTTL < 0 {
|
||||
nowTTL = 0
|
||||
}
|
||||
response = response.Copy()
|
||||
if originTTL > 0 {
|
||||
duration := uint32(originTTL - nowTTL)
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
record.Header().Ttl = record.Header().Ttl - duration
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
record.Header().Ttl = uint32(nowTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
return response, nowTTL
|
||||
}
|
||||
if clientSubnet.IsValid() {
|
||||
message = SetClientSubnet(message, clientSubnet)
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg) (*dns.Msg, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
response, err := transport.Exchange(ctx, message)
|
||||
if err == nil {
|
||||
return response, nil
|
||||
}
|
||||
var rcodeError RcodeError
|
||||
if errors.As(err, &rcodeError) {
|
||||
return FixedResponseStatus(message, int(rcodeError)), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func MessageToAddresses(response *dns.Msg) []netip.Addr {
|
||||
return adapter.DNSResponseAddresses(response)
|
||||
if response == nil || response.Rcode != dns.RcodeSuccess {
|
||||
return nil
|
||||
}
|
||||
addresses := make([]netip.Addr, 0, len(response.Answer))
|
||||
for _, rawAnswer := range response.Answer {
|
||||
switch answer := rawAnswer.(type) {
|
||||
case *dns.A:
|
||||
addresses = append(addresses, M.AddrFromIP(answer.A))
|
||||
case *dns.AAAA:
|
||||
addresses = append(addresses, M.AddrFromIP(answer.AAAA))
|
||||
case *dns.HTTPS:
|
||||
for _, value := range answer.SVCB.Value {
|
||||
if value.Key() == dns.SVCB_IPV4HINT || value.Key() == dns.SVCB_IPV6HINT {
|
||||
addresses = append(addresses, common.Map(strings.Split(value.String(), ","), M.ParseAddr)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
func wrapError(err error) error {
|
||||
|
||||
@@ -22,19 +22,6 @@ func logCachedResponse(logger logger.ContextLogger, ctx context.Context, respons
|
||||
}
|
||||
}
|
||||
|
||||
func logOptimisticResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg) {
|
||||
if logger == nil || len(response.Question) == 0 {
|
||||
return
|
||||
}
|
||||
domain := FqdnToDomain(response.Question[0].Name)
|
||||
logger.DebugContext(ctx, "optimistic ", domain, " ", dns.RcodeToString[response.Rcode])
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
logger.InfoContext(ctx, "optimistic ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) {
|
||||
if logger == nil || len(response.Question) == 0 {
|
||||
return
|
||||
|
||||
@@ -5,11 +5,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
RcodeSuccess RcodeError = mDNS.RcodeSuccess
|
||||
RcodeServerFailure RcodeError = mDNS.RcodeServerFailure
|
||||
RcodeFormatError RcodeError = mDNS.RcodeFormatError
|
||||
RcodeNameError RcodeError = mDNS.RcodeNameError
|
||||
RcodeRefused RcodeError = mDNS.RcodeRefused
|
||||
RcodeSuccess RcodeError = mDNS.RcodeSuccess
|
||||
RcodeFormatError RcodeError = mDNS.RcodeFormatError
|
||||
RcodeNameError RcodeError = mDNS.RcodeNameError
|
||||
RcodeRefused RcodeError = mDNS.RcodeRefused
|
||||
)
|
||||
|
||||
type RcodeError int
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReproLookupWithRulesUsesRequestStrategy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}
|
||||
var qTypes []uint16
|
||||
router := newTestRouter(t, nil, &fakeDNSTransportManager{
|
||||
defaultTransport: defaultTransport,
|
||||
transports: map[string]adapter.DNSTransport{
|
||||
"default": defaultTransport,
|
||||
},
|
||||
}, &fakeDNSClient{
|
||||
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
qTypes = append(qTypes, message.Question[0].Qtype)
|
||||
if message.Question[0].Qtype == mDNS.TypeA {
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil
|
||||
}
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::1")}, 60), nil
|
||||
},
|
||||
})
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{
|
||||
Strategy: C.DomainStrategyIPv4Only,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []uint16{mDNS.TypeA}, qTypes)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addresses)
|
||||
}
|
||||
|
||||
func TestReproLogicalMatchResponseIPCIDR(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
transportManager := &fakeDNSTransportManager{
|
||||
defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
|
||||
transports: map[string]adapter.DNSTransport{
|
||||
"upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP},
|
||||
"selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP},
|
||||
"default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
|
||||
},
|
||||
}
|
||||
client := &fakeDNSClient{
|
||||
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
switch transport.Tag() {
|
||||
case "upstream":
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil
|
||||
case "selected":
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil
|
||||
default:
|
||||
return nil, E.New("unexpected transport")
|
||||
}
|
||||
},
|
||||
}
|
||||
rules := []option.DNSRule{
|
||||
{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
RawDefaultDNSRule: option.RawDefaultDNSRule{
|
||||
Domain: badoption.Listable[string]{"example.com"},
|
||||
},
|
||||
DNSRuleAction: option.DNSRuleAction{
|
||||
Action: C.RuleActionTypeEvaluate,
|
||||
RouteOptions: option.DNSRouteActionOptions{Server: "upstream"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: C.RuleTypeLogical,
|
||||
LogicalOptions: option.LogicalDNSRule{
|
||||
RawLogicalDNSRule: option.RawLogicalDNSRule{
|
||||
Mode: C.LogicalTypeOr,
|
||||
Rules: []option.DNSRule{{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
RawDefaultDNSRule: option.RawDefaultDNSRule{
|
||||
MatchResponse: true,
|
||||
IPCIDR: badoption.Listable[string]{"1.1.1.0/24"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
DNSRuleAction: option.DNSRuleAction{
|
||||
Action: C.RuleActionTypeRoute,
|
||||
RouteOptions: option.DNSRouteActionOptions{Server: "selected"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
router := newTestRouter(t, rules, transportManager, client)
|
||||
|
||||
response, err := router.Exchange(context.Background(), &mDNS.Msg{
|
||||
Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)},
|
||||
}, adapter.DNSQueryOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response))
|
||||
}
|
||||
859
dns/router.go
859
dns/router.go
File diff suppressed because it is too large
Load Diff
464
dns/router.go.7853130561294400935
Normal file
464
dns/router.go.7853130561294400935
Normal file
@@ -0,0 +1,464 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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-box/option"
|
||||
R "github.com/sagernet/sing-box/route/rule"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/contrab/freelru"
|
||||
"github.com/sagernet/sing/contrab/maphash"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var _ adapter.DNSRouter = (*Router)(nil)
|
||||
|
||||
type Router struct {
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
transport adapter.DNSTransportManager
|
||||
outbound adapter.OutboundManager
|
||||
client adapter.DNSClient
|
||||
rules []adapter.DNSRule
|
||||
defaultDomainStrategy C.DomainStrategy
|
||||
dnsReverseMapping freelru.Cache[netip.Addr, string]
|
||||
platformInterface adapter.PlatformInterface
|
||||
}
|
||||
|
||||
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {
|
||||
router := &Router{
|
||||
ctx: ctx,
|
||||
logger: logFactory.NewLogger("dns"),
|
||||
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
||||
outbound: service.FromContext[adapter.OutboundManager](ctx),
|
||||
rules: make([]adapter.DNSRule, 0, len(options.Rules)),
|
||||
defaultDomainStrategy: C.DomainStrategy(options.Strategy),
|
||||
}
|
||||
router.client = NewClient(ClientOptions{
|
||||
DisableCache: options.DNSClientOptions.DisableCache,
|
||||
DisableExpire: options.DNSClientOptions.DisableExpire,
|
||||
IndependentCache: options.DNSClientOptions.IndependentCache,
|
||||
CacheCapacity: options.DNSClientOptions.CacheCapacity,
|
||||
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}),
|
||||
RDRC: func() adapter.RDRCStore {
|
||||
cacheFile := service.FromContext[adapter.CacheFile](ctx)
|
||||
if cacheFile == nil {
|
||||
return nil
|
||||
}
|
||||
if !cacheFile.StoreRDRC() {
|
||||
return nil
|
||||
}
|
||||
return cacheFile
|
||||
},
|
||||
Logger: router.logger,
|
||||
})
|
||||
if options.ReverseMapping {
|
||||
router.dnsReverseMapping = common.Must1(freelru.NewSharded[netip.Addr, string](1024, maphash.NewHasher[netip.Addr]().Hash32))
|
||||
}
|
||||
return router
|
||||
}
|
||||
|
||||
func (r *Router) Initialize(rules []option.DNSRule) error {
|
||||
for i, ruleOptions := range rules {
|
||||
dnsRule, err := R.NewDNSRule(r.ctx, r.logger, ruleOptions, true)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse dns rule[", i, "]")
|
||||
}
|
||||
r.rules = append(r.rules, dnsRule)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) Start(stage adapter.StartStage) error {
|
||||
monitor := taskmonitor.New(r.logger, C.StartTimeout)
|
||||
switch stage {
|
||||
case adapter.StartStateStart:
|
||||
monitor.Start("initialize DNS client")
|
||||
r.client.Start()
|
||||
monitor.Finish()
|
||||
|
||||
for i, rule := range r.rules {
|
||||
monitor.Start("initialize DNS rule[", i, "]")
|
||||
err := rule.Start()
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize DNS rule[", i, "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) Close() error {
|
||||
monitor := taskmonitor.New(r.logger, C.StopTimeout)
|
||||
var err error
|
||||
for i, rule := range r.rules {
|
||||
monitor.Start("close dns rule[", i, "]")
|
||||
err = E.Append(err, rule.Close(), func(err error) error {
|
||||
return E.Cause(err, "close dns rule[", i, "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) {
|
||||
metadata := adapter.ContextFrom(ctx)
|
||||
if metadata == nil {
|
||||
panic("no context")
|
||||
}
|
||||
var currentRuleIndex int
|
||||
if ruleIndex != -1 {
|
||||
currentRuleIndex = ruleIndex + 1
|
||||
}
|
||||
for ; currentRuleIndex < len(r.rules); currentRuleIndex++ {
|
||||
currentRule := r.rules[currentRuleIndex]
|
||||
if currentRule.WithAddressLimit() && !isAddressQuery {
|
||||
continue
|
||||
}
|
||||
metadata.ResetRuleCache()
|
||||
if currentRule.Match(metadata) {
|
||||
displayRuleIndex := currentRuleIndex
|
||||
if displayRuleIndex != -1 {
|
||||
displayRuleIndex += displayRuleIndex + 1
|
||||
}
|
||||
ruleDescription := currentRule.String()
|
||||
if ruleDescription != "" {
|
||||
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] ", currentRule, " => ", currentRule.Action())
|
||||
} else {
|
||||
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
|
||||
}
|
||||
switch action := currentRule.Action().(type) {
|
||||
case *R.RuleActionDNSRoute:
|
||||
transport, loaded, ambiguous := adapter.LookupDNSTransport(r.transport, action.Server)
|
||||
if !loaded {
|
||||
if ambiguous {
|
||||
r.logger.ErrorContext(ctx, "transport is ambiguous: ", action.Server)
|
||||
} else {
|
||||
r.logger.ErrorContext(ctx, "transport not found: ", action.Server)
|
||||
}
|
||||
continue
|
||||
}
|
||||
isFakeIP := transport.Type() == C.DNSTypeFakeIP
|
||||
if isFakeIP && !allowFakeIP {
|
||||
continue
|
||||
}
|
||||
if action.Strategy != C.DomainStrategyAsIS {
|
||||
options.Strategy = action.Strategy
|
||||
}
|
||||
if isFakeIP || action.DisableCache {
|
||||
options.DisableCache = true
|
||||
}
|
||||
if action.RewriteTTL != nil {
|
||||
options.RewriteTTL = action.RewriteTTL
|
||||
}
|
||||
if action.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = action.ClientSubnet
|
||||
}
|
||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = legacyTransport.LegacyStrategy()
|
||||
}
|
||||
if !options.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||
}
|
||||
}
|
||||
return transport, currentRule, currentRuleIndex
|
||||
case *R.RuleActionDNSRouteOptions:
|
||||
if action.Strategy != C.DomainStrategyAsIS {
|
||||
options.Strategy = action.Strategy
|
||||
}
|
||||
if action.DisableCache {
|
||||
options.DisableCache = true
|
||||
}
|
||||
if action.RewriteTTL != nil {
|
||||
options.RewriteTTL = action.RewriteTTL
|
||||
}
|
||||
if action.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = action.ClientSubnet
|
||||
}
|
||||
case *R.RuleActionReject:
|
||||
return nil, currentRule, currentRuleIndex
|
||||
case *R.RuleActionPredefined:
|
||||
return nil, currentRule, currentRuleIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
transport := r.transport.Default()
|
||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = legacyTransport.LegacyStrategy()
|
||||
}
|
||||
if !options.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||
}
|
||||
}
|
||||
return transport, nil, -1
|
||||
}
|
||||
|
||||
func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) {
|
||||
if len(message.Question) != 1 {
|
||||
r.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
||||
responseMessage := mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: message.Id,
|
||||
Response: true,
|
||||
Rcode: mDNS.RcodeFormatError,
|
||||
},
|
||||
Question: message.Question,
|
||||
}
|
||||
return &responseMessage, nil
|
||||
}
|
||||
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
|
||||
var (
|
||||
response *mDNS.Msg
|
||||
transport adapter.DNSTransport
|
||||
err error
|
||||
)
|
||||
var metadata *adapter.InboundContext
|
||||
ctx, metadata = adapter.ExtendContext(ctx)
|
||||
metadata.Destination = M.Socksaddr{}
|
||||
metadata.QueryType = message.Question[0].Qtype
|
||||
switch metadata.QueryType {
|
||||
case mDNS.TypeA:
|
||||
metadata.IPVersion = 4
|
||||
case mDNS.TypeAAAA:
|
||||
metadata.IPVersion = 6
|
||||
}
|
||||
metadata.Domain = FqdnToDomain(message.Question[0].Name)
|
||||
if options.Transport != nil {
|
||||
transport = options.Transport
|
||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = legacyTransport.LegacyStrategy()
|
||||
}
|
||||
if !options.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||
}
|
||||
}
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
response, err = r.client.Exchange(ctx, transport, message, options, nil)
|
||||
} else {
|
||||
var (
|
||||
rule adapter.DNSRule
|
||||
ruleIndex int
|
||||
)
|
||||
ruleIndex = -1
|
||||
for {
|
||||
dnsCtx := adapter.OverrideContext(ctx)
|
||||
dnsOptions := options
|
||||
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
|
||||
if rule != nil {
|
||||
switch action := rule.Action().(type) {
|
||||
case *R.RuleActionReject:
|
||||
switch action.Method {
|
||||
case C.RuleActionRejectMethodDefault:
|
||||
return &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: message.Id,
|
||||
Rcode: mDNS.RcodeRefused,
|
||||
Response: true,
|
||||
},
|
||||
Question: []mDNS.Question{message.Question[0]},
|
||||
}, nil
|
||||
case C.RuleActionRejectMethodDrop:
|
||||
return nil, tun.ErrDrop
|
||||
}
|
||||
case *R.RuleActionPredefined:
|
||||
return action.Response(message), nil
|
||||
}
|
||||
}
|
||||
responseCheck := addressLimitResponseCheck(rule, metadata)
|
||||
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
||||
dnsOptions.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
|
||||
var rejected bool
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrResponseRejectedCached) {
|
||||
rejected = true
|
||||
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
|
||||
} else if errors.Is(err, ErrResponseRejected) {
|
||||
rejected = true
|
||||
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
|
||||
} else if len(message.Question) > 0 {
|
||||
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
|
||||
} else {
|
||||
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
|
||||
}
|
||||
}
|
||||
if responseCheck != nil && rejected {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.dnsReverseMapping != nil && len(message.Question) > 0 && response != nil && len(response.Answer) > 0 {
|
||||
if transport == nil || transport.Type() != C.DNSTypeFakeIP {
|
||||
for _, answer := range response.Answer {
|
||||
switch record := answer.(type) {
|
||||
case *mDNS.A:
|
||||
r.dnsReverseMapping.AddWithLifetime(M.AddrFromIP(record.A), FqdnToDomain(record.Hdr.Name), time.Duration(record.Hdr.Ttl)*time.Second)
|
||||
case *mDNS.AAAA:
|
||||
r.dnsReverseMapping.AddWithLifetime(M.AddrFromIP(record.AAAA), FqdnToDomain(record.Hdr.Name), time.Duration(record.Hdr.Ttl)*time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
||||
var (
|
||||
responseAddrs []netip.Addr
|
||||
err error
|
||||
)
|
||||
printResult := func() {
|
||||
if err == nil && len(responseAddrs) == 0 {
|
||||
err = E.New("empty result")
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrResponseRejectedCached) {
|
||||
r.logger.DebugContext(ctx, "response rejected for ", domain, " (cached)")
|
||||
} else if errors.Is(err, ErrResponseRejected) {
|
||||
r.logger.DebugContext(ctx, "response rejected for ", domain)
|
||||
} else {
|
||||
r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
err = E.Cause(err, "lookup ", domain)
|
||||
}
|
||||
}
|
||||
r.logger.DebugContext(ctx, "lookup domain ", domain)
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
metadata.Destination = M.Socksaddr{}
|
||||
metadata.Domain = FqdnToDomain(domain)
|
||||
if options.Transport != nil {
|
||||
transport := options.Transport
|
||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = legacyTransport.LegacyStrategy()
|
||||
}
|
||||
if !options.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||
}
|
||||
}
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
responseAddrs, err = r.client.Lookup(ctx, transport, domain, options, nil)
|
||||
} else {
|
||||
var (
|
||||
transport adapter.DNSTransport
|
||||
rule adapter.DNSRule
|
||||
ruleIndex int
|
||||
)
|
||||
ruleIndex = -1
|
||||
for {
|
||||
dnsCtx := adapter.OverrideContext(ctx)
|
||||
dnsOptions := options
|
||||
transport, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true, &dnsOptions)
|
||||
if rule != nil {
|
||||
switch action := rule.Action().(type) {
|
||||
case *R.RuleActionReject:
|
||||
return nil, &R.RejectedError{Cause: action.Error(ctx)}
|
||||
case *R.RuleActionPredefined:
|
||||
responseAddrs = nil
|
||||
if action.Rcode != mDNS.RcodeSuccess {
|
||||
err = RcodeError(action.Rcode)
|
||||
} else {
|
||||
err = nil
|
||||
for _, answer := range action.Answer {
|
||||
switch record := answer.(type) {
|
||||
case *mDNS.A:
|
||||
responseAddrs = append(responseAddrs, M.AddrFromIP(record.A))
|
||||
case *mDNS.AAAA:
|
||||
responseAddrs = append(responseAddrs, M.AddrFromIP(record.AAAA))
|
||||
}
|
||||
}
|
||||
}
|
||||
goto response
|
||||
}
|
||||
}
|
||||
responseCheck := addressLimitResponseCheck(rule, metadata)
|
||||
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
||||
dnsOptions.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
responseAddrs, err = r.client.Lookup(dnsCtx, transport, domain, dnsOptions, responseCheck)
|
||||
if responseCheck == nil || err == nil {
|
||||
break
|
||||
}
|
||||
printResult()
|
||||
}
|
||||
}
|
||||
response:
|
||||
printResult()
|
||||
if len(responseAddrs) > 0 {
|
||||
r.logger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(responseAddrs), " "))
|
||||
}
|
||||
return responseAddrs, err
|
||||
}
|
||||
|
||||
func isAddressQuery(message *mDNS.Msg) bool {
|
||||
for _, question := range message.Question {
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA || question.Qtype == mDNS.TypeHTTPS {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func addressLimitResponseCheck(rule adapter.DNSRule, metadata *adapter.InboundContext) func(responseAddrs []netip.Addr) bool {
|
||||
if rule == nil || !rule.WithAddressLimit() {
|
||||
return nil
|
||||
}
|
||||
responseMetadata := *metadata
|
||||
return func(responseAddrs []netip.Addr) bool {
|
||||
checkMetadata := responseMetadata
|
||||
checkMetadata.DestinationAddresses = responseAddrs
|
||||
return rule.MatchAddressLimit(&checkMetadata)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) ClearCache() {
|
||||
r.client.ClearCache()
|
||||
if r.platformInterface != nil {
|
||||
r.platformInterface.ClearDNSCache()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) LookupReverseMapping(ip netip.Addr) (string, bool) {
|
||||
if r.dnsReverseMapping == nil {
|
||||
return "", false
|
||||
}
|
||||
domain, loaded := r.dnsReverseMapping.Get(ip)
|
||||
return domain, loaded
|
||||
}
|
||||
|
||||
func (r *Router) ResetNetwork() {
|
||||
r.ClearCache()
|
||||
for _, transport := range r.transport.Transports() {
|
||||
transport.Reset()
|
||||
}
|
||||
}
|
||||
2547
dns/router_test.go
2547
dns/router_test.go
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,8 @@ package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
@@ -12,6 +14,7 @@ import (
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
@@ -32,8 +35,10 @@ type Transport struct {
|
||||
logger logger.ContextLogger
|
||||
hosts *hosts.File
|
||||
dialer N.Dialer
|
||||
preferGo bool
|
||||
fallback bool
|
||||
dhcpTransport dhcpTransport
|
||||
resolver net.Resolver
|
||||
}
|
||||
|
||||
type dhcpTransport interface {
|
||||
@@ -47,12 +52,14 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transportAdapter := dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options)
|
||||
return &Transport{
|
||||
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
|
||||
TransportAdapter: transportAdapter,
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
hosts: hosts.NewFile(hosts.DefaultPath),
|
||||
dialer: transportDialer,
|
||||
preferGo: options.PreferGo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -99,11 +106,35 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
|
||||
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||
}
|
||||
}
|
||||
if t.fallback && t.dhcpTransport != nil {
|
||||
dhcpServers := t.dhcpTransport.Fetch()
|
||||
if len(dhcpServers) > 0 {
|
||||
return t.dhcpTransport.Exchange0(ctx, message, dhcpServers)
|
||||
if !t.fallback {
|
||||
return t.exchange(ctx, message, question.Name)
|
||||
}
|
||||
if t.dhcpTransport != nil {
|
||||
dhcpTransports := t.dhcpTransport.Fetch()
|
||||
if len(dhcpTransports) > 0 {
|
||||
return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports)
|
||||
}
|
||||
}
|
||||
return t.exchange(ctx, message, question.Name)
|
||||
if t.preferGo {
|
||||
// Assuming the user knows what they are doing, we still execute the query which will fail.
|
||||
return t.exchange(ctx, message, question.Name)
|
||||
}
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||
var network string
|
||||
if question.Qtype == mDNS.TypeA {
|
||||
network = "ip4"
|
||||
} else {
|
||||
network = "ip6"
|
||||
}
|
||||
addresses, err := t.resolver.LookupNetIP(ctx, network, question.Name)
|
||||
if err != nil {
|
||||
var dnsError *net.DNSError
|
||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
||||
return nil, dns.RcodeRefused
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||
}
|
||||
return nil, E.New("only A and AAAA queries are supported on Apple platforms when using TUN and DHCP unavailable.")
|
||||
}
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
//go:build darwin
|
||||
|
||||
package local
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
#include <dns.h>
|
||||
#include <resolv.h>
|
||||
|
||||
static void *cgo_dns_open_super() {
|
||||
return (void *)dns_open(NULL);
|
||||
}
|
||||
|
||||
static void cgo_dns_close(void *opaque) {
|
||||
if (opaque != NULL) dns_free((dns_handle_t)opaque);
|
||||
}
|
||||
|
||||
static int cgo_dns_search(void *opaque, const char *name, int class, int type,
|
||||
unsigned char *answer, int anslen) {
|
||||
dns_handle_t handle = (dns_handle_t)opaque;
|
||||
struct sockaddr_storage from;
|
||||
uint32_t fromlen = sizeof(from);
|
||||
return dns_search(handle, name, class, type, (char *)answer, anslen, (struct sockaddr *)&from, &fromlen);
|
||||
}
|
||||
|
||||
static void *cgo_res_init() {
|
||||
res_state state = calloc(1, sizeof(struct __res_state));
|
||||
if (state == NULL) return NULL;
|
||||
if (res_ninit(state) != 0) {
|
||||
free(state);
|
||||
return NULL;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
static void cgo_res_destroy(void *opaque) {
|
||||
res_state state = (res_state)opaque;
|
||||
res_ndestroy(state);
|
||||
free(state);
|
||||
}
|
||||
|
||||
static int cgo_res_nsearch(void *opaque, const char *dname, int class, int type,
|
||||
unsigned char *answer, int anslen,
|
||||
int timeout_seconds,
|
||||
int *out_h_errno) {
|
||||
res_state state = (res_state)opaque;
|
||||
state->retrans = timeout_seconds;
|
||||
state->retry = 1;
|
||||
int n = res_nsearch(state, dname, class, type, answer, anslen);
|
||||
if (n < 0) {
|
||||
*out_h_errno = state->res_h_errno;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
boxC "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const (
|
||||
darwinResolverHostNotFound = 1
|
||||
darwinResolverTryAgain = 2
|
||||
darwinResolverNoRecovery = 3
|
||||
darwinResolverNoData = 4
|
||||
|
||||
darwinResolverMaxPacketSize = 65535
|
||||
)
|
||||
|
||||
var errDarwinNeedLargerBuffer = errors.New("darwin resolver response truncated")
|
||||
|
||||
func darwinLookupSystemDNS(name string, class, qtype, timeoutSeconds int) (*mDNS.Msg, error) {
|
||||
response, err := darwinSearchWithSystemRouting(name, class, qtype)
|
||||
if err == nil {
|
||||
return response, nil
|
||||
}
|
||||
fallbackResponse, fallbackErr := darwinSearchWithResolv(name, class, qtype, timeoutSeconds)
|
||||
if fallbackErr == nil || fallbackResponse != nil {
|
||||
return fallbackResponse, fallbackErr
|
||||
}
|
||||
return nil, E.Errors(
|
||||
E.Cause(err, "dns_search"),
|
||||
E.Cause(fallbackErr, "res_nsearch"),
|
||||
)
|
||||
}
|
||||
|
||||
func darwinSearchWithSystemRouting(name string, class, qtype int) (*mDNS.Msg, error) {
|
||||
handle := C.cgo_dns_open_super()
|
||||
if handle == nil {
|
||||
return nil, E.New("dns_open failed")
|
||||
}
|
||||
defer C.cgo_dns_close(handle)
|
||||
|
||||
cName := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cName))
|
||||
|
||||
bufSize := 1232
|
||||
for {
|
||||
answer := make([]byte, bufSize)
|
||||
n := C.cgo_dns_search(handle, cName, C.int(class), C.int(qtype),
|
||||
(*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)))
|
||||
if n <= 0 {
|
||||
return nil, E.New("dns_search failed for ", name)
|
||||
}
|
||||
if int(n) > bufSize {
|
||||
bufSize = int(n)
|
||||
continue
|
||||
}
|
||||
return unpackDarwinResolverMessage(answer[:int(n)], "dns_search")
|
||||
}
|
||||
}
|
||||
|
||||
func darwinSearchWithResolv(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) {
|
||||
state := C.cgo_res_init()
|
||||
if state == nil {
|
||||
return nil, E.New("res_ninit failed")
|
||||
}
|
||||
defer C.cgo_res_destroy(state)
|
||||
|
||||
cName := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cName))
|
||||
|
||||
bufSize := 1232
|
||||
for {
|
||||
answer := make([]byte, bufSize)
|
||||
var hErrno C.int
|
||||
n := C.cgo_res_nsearch(state, cName, C.int(class), C.int(qtype),
|
||||
(*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)),
|
||||
C.int(timeoutSeconds),
|
||||
&hErrno)
|
||||
if n >= 0 {
|
||||
if int(n) > bufSize {
|
||||
bufSize = int(n)
|
||||
continue
|
||||
}
|
||||
return unpackDarwinResolverMessage(answer[:int(n)], "res_nsearch")
|
||||
}
|
||||
response, err := handleDarwinResolvFailure(name, answer, int(hErrno))
|
||||
if err == nil {
|
||||
return response, nil
|
||||
}
|
||||
if errors.Is(err, errDarwinNeedLargerBuffer) && bufSize < darwinResolverMaxPacketSize {
|
||||
bufSize *= 2
|
||||
if bufSize > darwinResolverMaxPacketSize {
|
||||
bufSize = darwinResolverMaxPacketSize
|
||||
}
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func unpackDarwinResolverMessage(packet []byte, source string) (*mDNS.Msg, error) {
|
||||
var response mDNS.Msg
|
||||
err := response.Unpack(packet)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "unpack ", source, " response")
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func handleDarwinResolvFailure(name string, answer []byte, hErrno int) (*mDNS.Msg, error) {
|
||||
response, err := unpackDarwinResolverMessage(answer, "res_nsearch failure")
|
||||
if err == nil && response.Response {
|
||||
if response.Truncated && len(answer) < darwinResolverMaxPacketSize {
|
||||
return nil, errDarwinNeedLargerBuffer
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
return nil, darwinResolverHErrno(name, hErrno)
|
||||
}
|
||||
|
||||
func darwinResolverHErrno(name string, hErrno int) error {
|
||||
switch hErrno {
|
||||
case darwinResolverHostNotFound:
|
||||
return dns.RcodeNameError
|
||||
case darwinResolverTryAgain:
|
||||
return dns.RcodeServerFailure
|
||||
case darwinResolverNoRecovery:
|
||||
return dns.RcodeServerFailure
|
||||
case darwinResolverNoData:
|
||||
return dns.RcodeSuccess
|
||||
default:
|
||||
return E.New("res_nsearch: unknown error ", hErrno, " for ", name)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
question := message.Question[0]
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
|
||||
if len(addresses) > 0 {
|
||||
return dns.FixedResponse(message.Id, question, addresses, boxC.DefaultDNSTTL), nil
|
||||
}
|
||||
}
|
||||
if t.fallback && t.dhcpTransport != nil {
|
||||
dhcpServers := t.dhcpTransport.Fetch()
|
||||
if len(dhcpServers) > 0 {
|
||||
return t.dhcpTransport.Exchange0(ctx, message, dhcpServers)
|
||||
}
|
||||
}
|
||||
name := question.Name
|
||||
timeoutSeconds := int(boxC.DNSTimeout / time.Second)
|
||||
if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
|
||||
remaining := time.Until(deadline)
|
||||
if remaining <= 0 {
|
||||
return nil, context.DeadlineExceeded
|
||||
}
|
||||
seconds := int(remaining.Seconds())
|
||||
if seconds < 1 {
|
||||
seconds = 1
|
||||
}
|
||||
timeoutSeconds = seconds
|
||||
}
|
||||
type resolvResult struct {
|
||||
response *mDNS.Msg
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan resolvResult, 1)
|
||||
go func() {
|
||||
response, err := darwinLookupSystemDNS(name, int(question.Qclass), int(question.Qtype), timeoutSeconds)
|
||||
resultCh <- resolvResult{response, err}
|
||||
}()
|
||||
var result resolvResult
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case result = <-resultCh:
|
||||
}
|
||||
if result.err != nil {
|
||||
var rcodeError dns.RcodeError
|
||||
if errors.As(result.err, &rcodeError) {
|
||||
return dns.FixedResponseStatus(message, int(rcodeError)), nil
|
||||
}
|
||||
return nil, result.err
|
||||
}
|
||||
result.response.Id = message.Id
|
||||
return result.response, nil
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
//go:build !windows
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
var _ adapter.LegacyDNSTransport = (*TransportAdapter)(nil)
|
||||
|
||||
type TransportAdapter struct {
|
||||
transportType string
|
||||
transportTag string
|
||||
dependencies []string
|
||||
strategy C.DomainStrategy
|
||||
clientSubnet netip.Prefix
|
||||
}
|
||||
|
||||
func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter {
|
||||
@@ -27,6 +35,8 @@ func NewTransportAdapterWithLocalOptions(transportType string, transportTag stri
|
||||
transportType: transportType,
|
||||
transportTag: transportTag,
|
||||
dependencies: dependencies,
|
||||
strategy: C.DomainStrategy(localOptions.LegacyStrategy),
|
||||
clientSubnet: localOptions.LegacyClientSubnet,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +45,15 @@ func NewTransportAdapterWithRemoteOptions(transportType string, transportTag str
|
||||
if remoteOptions.DomainResolver != nil && remoteOptions.DomainResolver.Server != "" {
|
||||
dependencies = append(dependencies, remoteOptions.DomainResolver.Server)
|
||||
}
|
||||
if remoteOptions.LegacyAddressResolver != "" {
|
||||
dependencies = append(dependencies, remoteOptions.LegacyAddressResolver)
|
||||
}
|
||||
return TransportAdapter{
|
||||
transportType: transportType,
|
||||
transportTag: transportTag,
|
||||
dependencies: dependencies,
|
||||
strategy: C.DomainStrategy(remoteOptions.LegacyStrategy),
|
||||
clientSubnet: remoteOptions.LegacyClientSubnet,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,3 +68,11 @@ func (a *TransportAdapter) Tag() string {
|
||||
func (a *TransportAdapter) Dependencies() []string {
|
||||
return a.dependencies
|
||||
}
|
||||
|
||||
func (a *TransportAdapter) LegacyStrategy() C.DomainStrategy {
|
||||
return a.strategy
|
||||
}
|
||||
|
||||
func (a *TransportAdapter) LegacyClientSubnet() netip.Prefix {
|
||||
return a.clientSubnet
|
||||
}
|
||||
|
||||
@@ -2,25 +2,107 @@ package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (N.Dialer, error) {
|
||||
return dialer.NewWithOptions(dialer.Options{
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
DirectResolver: true,
|
||||
})
|
||||
if options.LegacyDefaultDialer {
|
||||
return dialer.NewDefaultOutbound(ctx), nil
|
||||
} else {
|
||||
return dialer.NewWithOptions(dialer.Options{
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
DirectResolver: true,
|
||||
LegacyDNSDialer: options.Legacy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions) (N.Dialer, error) {
|
||||
return dialer.NewWithOptions(dialer.Options{
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
RemoteIsDomain: options.ServerIsDomain(),
|
||||
DirectResolver: true,
|
||||
})
|
||||
if options.LegacyDefaultDialer {
|
||||
transportDialer := dialer.NewDefaultOutbound(ctx)
|
||||
if options.LegacyAddressResolver != "" {
|
||||
transport := service.FromContext[adapter.DNSTransportManager](ctx)
|
||||
resolverTransport, loaded, ambiguous := adapter.LookupDNSTransport(transport, options.LegacyAddressResolver)
|
||||
if !loaded {
|
||||
if ambiguous {
|
||||
return nil, E.New("address resolver is ambiguous: ", options.LegacyAddressResolver)
|
||||
}
|
||||
return nil, E.New("address resolver not found: ", options.LegacyAddressResolver)
|
||||
}
|
||||
transportDialer = newTransportDialer(transportDialer, service.FromContext[adapter.DNSRouter](ctx), resolverTransport, C.DomainStrategy(options.LegacyAddressStrategy), time.Duration(options.LegacyAddressFallbackDelay))
|
||||
} else if options.ServerIsDomain() {
|
||||
return nil, E.New("missing address resolver for server: ", options.Server)
|
||||
}
|
||||
return transportDialer, nil
|
||||
} else {
|
||||
return dialer.NewWithOptions(dialer.Options{
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
RemoteIsDomain: options.ServerIsDomain(),
|
||||
DirectResolver: true,
|
||||
LegacyDNSDialer: options.Legacy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type legacyTransportDialer struct {
|
||||
dialer N.Dialer
|
||||
dnsRouter adapter.DNSRouter
|
||||
transport adapter.DNSTransport
|
||||
strategy C.DomainStrategy
|
||||
fallbackDelay time.Duration
|
||||
}
|
||||
|
||||
func newTransportDialer(dialer N.Dialer, dnsRouter adapter.DNSRouter, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) *legacyTransportDialer {
|
||||
return &legacyTransportDialer{
|
||||
dialer,
|
||||
dnsRouter,
|
||||
transport,
|
||||
strategy,
|
||||
fallbackDelay,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *legacyTransportDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
if destination.IsIP() {
|
||||
return d.dialer.DialContext(ctx, network, destination)
|
||||
}
|
||||
addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{
|
||||
Transport: d.transport,
|
||||
Strategy: d.strategy,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
|
||||
}
|
||||
|
||||
func (d *legacyTransportDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
if destination.IsIP() {
|
||||
return d.dialer.ListenPacket(ctx, destination)
|
||||
}
|
||||
addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{
|
||||
Transport: d.transport,
|
||||
Strategy: d.strategy,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, _, err := N.ListenSerial(ctx, d.dialer, destination, addresses)
|
||||
return conn, err
|
||||
}
|
||||
|
||||
func (d *legacyTransportDialer) Upstream() any {
|
||||
return d.dialer
|
||||
}
|
||||
|
||||
108
dns/transport_dialer.go.6124520021629164267
Normal file
108
dns/transport_dialer.go.6124520021629164267
Normal file
@@ -0,0 +1,108 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (N.Dialer, error) {
|
||||
if options.LegacyDefaultDialer {
|
||||
return dialer.NewDefaultOutbound(ctx), nil
|
||||
} else {
|
||||
return dialer.NewWithOptions(dialer.Options{
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
DirectResolver: true,
|
||||
LegacyDNSDialer: options.Legacy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions) (N.Dialer, error) {
|
||||
if options.LegacyDefaultDialer {
|
||||
transportDialer := dialer.NewDefaultOutbound(ctx)
|
||||
if options.LegacyAddressResolver != "" {
|
||||
transport := service.FromContext[adapter.DNSTransportManager](ctx)
|
||||
resolverTransport, loaded, ambiguous := adapter.LookupDNSTransport(transport, options.LegacyAddressResolver)
|
||||
if !loaded {
|
||||
if ambiguous {
|
||||
return nil, E.New("address resolver is ambiguous: ", options.LegacyAddressResolver)
|
||||
}
|
||||
return nil, E.New("address resolver not found: ", options.LegacyAddressResolver)
|
||||
}
|
||||
transportDialer = newTransportDialer(transportDialer, service.FromContext[adapter.DNSRouter](ctx), resolverTransport, C.DomainStrategy(options.LegacyAddressStrategy), time.Duration(options.LegacyAddressFallbackDelay))
|
||||
} else if options.ServerIsDomain() {
|
||||
return nil, E.New("missing address resolver for server: ", options.Server)
|
||||
}
|
||||
return transportDialer, nil
|
||||
} else {
|
||||
return dialer.NewWithOptions(dialer.Options{
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
RemoteIsDomain: options.ServerIsDomain(),
|
||||
DirectResolver: true,
|
||||
LegacyDNSDialer: options.Legacy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type legacyTransportDialer struct {
|
||||
dialer N.Dialer
|
||||
dnsRouter adapter.DNSRouter
|
||||
transport adapter.DNSTransport
|
||||
strategy C.DomainStrategy
|
||||
fallbackDelay time.Duration
|
||||
}
|
||||
|
||||
func newTransportDialer(dialer N.Dialer, dnsRouter adapter.DNSRouter, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) *legacyTransportDialer {
|
||||
return &legacyTransportDialer{
|
||||
dialer,
|
||||
dnsRouter,
|
||||
transport,
|
||||
strategy,
|
||||
fallbackDelay,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *legacyTransportDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
if destination.IsIP() {
|
||||
return d.dialer.DialContext(ctx, network, destination)
|
||||
}
|
||||
addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{
|
||||
Transport: d.transport,
|
||||
Strategy: d.strategy,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
|
||||
}
|
||||
|
||||
func (d *legacyTransportDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
if destination.IsIP() {
|
||||
return d.dialer.ListenPacket(ctx, destination)
|
||||
}
|
||||
addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{
|
||||
Transport: d.transport,
|
||||
Strategy: d.strategy,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, _, err := N.ListenSerial(ctx, d.dialer, destination, addresses)
|
||||
return conn, err
|
||||
}
|
||||
|
||||
func (d *legacyTransportDialer) Upstream() any {
|
||||
return d.dialer
|
||||
}
|
||||
@@ -2,143 +2,28 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.14.0-alpha.12
|
||||
|
||||
* Fix fake-ip DNS server should return SUCCESS when another address type is not configured
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.8
|
||||
|
||||
* Update naiveproxy to v147.0.7727.49-1
|
||||
* Fix fake-ip DNS server should return SUCCESS when another address type is not configured
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.14.0-alpha.11
|
||||
|
||||
* Add optimistic DNS cache **1**
|
||||
* Update NaiveProxy to 147.0.7727.49
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Optimistic DNS cache returns an expired cached response immediately while
|
||||
refreshing it in the background, reducing tail latency for repeated
|
||||
queries. Enabled via [`optimistic`](/configuration/dns/#optimistic)
|
||||
in DNS options, and can be persisted across restarts with the new
|
||||
[`store_dns`](/configuration/experimental/cache-file/#store_dns) cache
|
||||
file option. A per-query
|
||||
[`disable_optimistic_cache`](/configuration/dns/rule_action/#disable_optimistic_cache)
|
||||
field is also available on DNS rule actions and the `resolve` route rule
|
||||
action.
|
||||
|
||||
This deprecates the `independent_cache` DNS option (the DNS cache now
|
||||
always keys by transport) and the `store_rdrc` cache file option
|
||||
(replaced by `store_dns`); both will be removed in sing-box 1.16.0.
|
||||
See [Migration](/migration/#migrate-independent-dns-cache).
|
||||
|
||||
#### 1.14.0-alpha.10
|
||||
|
||||
* Add `evaluate` DNS rule action and Response Match Fields **1**
|
||||
* `ip_version` and `query_type` now also take effect on internal DNS lookups **2**
|
||||
* Add `package_name_regex` route, DNS and headless rule item **3**
|
||||
* Add cloudflared inbound **4**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Response Match Fields
|
||||
([`response_rcode`](/configuration/dns/rule/#response_rcode),
|
||||
[`response_answer`](/configuration/dns/rule/#response_answer),
|
||||
[`response_ns`](/configuration/dns/rule/#response_ns),
|
||||
and [`response_extra`](/configuration/dns/rule/#response_extra))
|
||||
match the evaluated DNS response. They are gated by the new
|
||||
[`match_response`](/configuration/dns/rule/#match_response) field and
|
||||
populated by a preceding
|
||||
[`evaluate`](/configuration/dns/rule_action/#evaluate) DNS rule action;
|
||||
the evaluated response can also be returned directly by a
|
||||
[`respond`](/configuration/dns/rule_action/#respond) action.
|
||||
|
||||
This deprecates the Legacy Address Filter Fields (`ip_cidr`,
|
||||
`ip_is_private` without `match_response`) in DNS rules, the Legacy
|
||||
`strategy` DNS rule action option, and the Legacy
|
||||
`rule_set_ip_cidr_accept_empty` DNS rule item; all three will be removed
|
||||
in sing-box 1.16.0.
|
||||
See [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
||||
|
||||
**2**:
|
||||
|
||||
`ip_version` and `query_type` in DNS rules, together with `query_type` in
|
||||
referenced rule-sets, now take effect on every DNS rule evaluation,
|
||||
including matches from internal domain resolutions that do not target a
|
||||
specific DNS server (for example a `resolve` route rule action without
|
||||
`server` set). In earlier versions they were silently ignored in that
|
||||
path. Combining these fields with any of the legacy DNS fields deprecated
|
||||
in **1** in the same DNS configuration is no longer supported and is
|
||||
rejected at startup.
|
||||
See [Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules).
|
||||
|
||||
**3**:
|
||||
|
||||
See [Route Rule](/configuration/route/rule/#package_name_regex),
|
||||
[DNS Rule](/configuration/dns/rule/#package_name_regex) and
|
||||
[Headless Rule](/configuration/rule-set/headless-rule/#package_name_regex).
|
||||
|
||||
**4**:
|
||||
|
||||
See [Cloudflared](/configuration/inbound/cloudflared/).
|
||||
|
||||
#### 1.13.7
|
||||
|
||||
* Fixes and improvement
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.6
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.14.0-alpha.8
|
||||
|
||||
* Add BBR profile and hop interval randomization for Hysteria2 **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
See [Hysteria2 Inbound](/configuration/inbound/hysteria2/#bbr_profile) and [Hysteria2 Outbound](/configuration/outbound/hysteria2/#bbr_profile).
|
||||
|
||||
#### 1.14.0-alpha.8
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.5
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.14.0-alpha.7
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.4
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.14.0-alpha.4
|
||||
|
||||
* Refactor ACME support to certificate provider system **1**
|
||||
* Add Cloudflare Origin CA certificate provider **2**
|
||||
* Add Tailscale certificate provider **3**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
See [Certificate Provider](/configuration/shared/certificate-provider/) and [Migration](/migration/#migrate-inline-acme-to-certificate-provider).
|
||||
|
||||
**2**:
|
||||
|
||||
See [Cloudflare Origin CA](/configuration/shared/certificate-provider/cloudflare-origin-ca).
|
||||
|
||||
**3**:
|
||||
|
||||
See [Tailscale](/configuration/shared/certificate-provider/tailscale).
|
||||
|
||||
#### 1.13.3
|
||||
|
||||
* Add OpenWrt and Alpine APK packages to release **1**
|
||||
@@ -163,59 +48,6 @@ from [SagerNet/go](https://github.com/SagerNet/go).
|
||||
|
||||
See [OCM](/configuration/service/ocm).
|
||||
|
||||
#### 1.12.24
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.14.0-alpha.2
|
||||
|
||||
* Add OpenWrt and Alpine APK packages to release **1**
|
||||
* Backport to macOS 10.13 High Sierra **2**
|
||||
* OCM service: Add WebSocket support for Responses API **3**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
|
||||
|
||||
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
|
||||
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
|
||||
|
||||
**2**:
|
||||
|
||||
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
|
||||
macOS 10.13 High Sierra, built using Go 1.25 with patches
|
||||
from [SagerNet/go](https://github.com/SagerNet/go).
|
||||
|
||||
**3**:
|
||||
|
||||
See [OCM](/configuration/service/ocm).
|
||||
|
||||
#### 1.14.0-alpha.1
|
||||
|
||||
* Add `source_mac_address` and `source_hostname` rule items **1**
|
||||
* Add `include_mac_address` and `exclude_mac_address` TUN options **2**
|
||||
* Update NaiveProxy to 145.0.7632.159 **3**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
New rule items for matching LAN devices by MAC address and hostname via neighbor resolution.
|
||||
Supported on Linux, macOS, or in graphical clients on Android and macOS.
|
||||
|
||||
See [Route Rule](/configuration/route/rule/#source_mac_address), [DNS Rule](/configuration/dns/rule/#source_mac_address) and [Neighbor Resolution](/configuration/shared/neighbor/).
|
||||
|
||||
**2**:
|
||||
|
||||
Limit or exclude devices from TUN routing by MAC address.
|
||||
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
|
||||
|
||||
See [TUN](/configuration/inbound/tun/#include_mac_address).
|
||||
|
||||
**3**:
|
||||
|
||||
This is not an official update from NaiveProxy. Instead, it's a Chromium codebase update maintained by Project S.
|
||||
|
||||
#### 1.13.2
|
||||
|
||||
* Fixes and improvements
|
||||
@@ -837,7 +669,7 @@ DNS servers are refactored for better performance and scalability.
|
||||
|
||||
See [DNS server](/configuration/dns/server/).
|
||||
|
||||
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-server-formats).
|
||||
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-servers).
|
||||
|
||||
Compatibility for old formats will be removed in sing-box 1.14.0.
|
||||
|
||||
@@ -1307,7 +1139,7 @@ DNS servers are refactored for better performance and scalability.
|
||||
|
||||
See [DNS server](/configuration/dns/server/).
|
||||
|
||||
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-server-formats).
|
||||
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-servers).
|
||||
|
||||
Compatibility for old formats will be removed in sing-box 1.14.0.
|
||||
|
||||
@@ -2143,7 +1975,7 @@ See [Migration](/migration/#process_path-format-update-on-windows).
|
||||
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
|
||||
if using this method.
|
||||
|
||||
See [Legacy Address Filter Fields](/configuration/dns/rule#legacy-address-filter-fields).
|
||||
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
|
||||
|
||||
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
|
||||
|
||||
@@ -2157,7 +1989,7 @@ the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users
|
||||
**5**:
|
||||
|
||||
The new feature allows you to cache the check results of
|
||||
[Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) until expiration.
|
||||
[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration.
|
||||
|
||||
**6**:
|
||||
|
||||
@@ -2338,7 +2170,7 @@ See [TUN](/configuration/inbound/tun) inbound.
|
||||
**1**:
|
||||
|
||||
The new feature allows you to cache the check results of
|
||||
[Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) until expiration.
|
||||
[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration.
|
||||
|
||||
#### 1.9.0-alpha.7
|
||||
|
||||
@@ -2385,7 +2217,7 @@ See [Migration](/migration/#process_path-format-update-on-windows).
|
||||
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
|
||||
if using this method.
|
||||
|
||||
See [Legacy Address Filter Fields](/configuration/dns/rule#legacy-address-filter-fields).
|
||||
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
|
||||
|
||||
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ SFA provides an unprivileged TUN implementation through Android VpnService.
|
||||
| `process_path` | :material-close: | No permission |
|
||||
| `process_path_regex` | :material-close: | No permission |
|
||||
| `package_name` | :material-check: | / |
|
||||
| `package_name_regex` | :material-check: | / |
|
||||
| `user` | :material-close: | Use `package_name` instead |
|
||||
| `user_id` | :material-close: | Use `package_name` instead |
|
||||
| `wifi_ssid` | :material-check: | Fine location permission required |
|
||||
|
||||
@@ -44,7 +44,6 @@ SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension
|
||||
| `process_path` | :material-close: | No permission |
|
||||
| `process_path_regex` | :material-close: | No permission |
|
||||
| `package_name` | :material-close: | / |
|
||||
| `package_name_regex` | :material-close: | / |
|
||||
| `user` | :material-close: | No permission |
|
||||
| `user_id` | :material-close: | No permission |
|
||||
| `wifi_ssid` | :material-alert: | Only supported on iOS |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
icon: material/note-remove
|
||||
icon: material/delete-clock
|
||||
---
|
||||
|
||||
!!! failure "Removed in sing-box 1.14.0"
|
||||
!!! failure "Deprecated in sing-box 1.12.0"
|
||||
|
||||
Legacy fake-ip configuration is deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-server-formats).
|
||||
Legacy fake-ip configuration is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers).
|
||||
|
||||
### Structure
|
||||
|
||||
@@ -26,6 +26,6 @@ Enable FakeIP service.
|
||||
|
||||
IPv4 address range for FakeIP.
|
||||
|
||||
#### inet6_range
|
||||
#### inet6_address
|
||||
|
||||
IPv6 address range for FakeIP.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
icon: material/note-remove
|
||||
icon: material/delete-clock
|
||||
---
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 移除"
|
||||
!!! failure "已在 sing-box 1.12.0 废弃"
|
||||
|
||||
旧的 fake-ip 配置已在 sing-box 1.12.0 废弃且已在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
||||
旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
||||
|
||||
### 结构
|
||||
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-delete-clock: [independent_cache](#independent_cache)
|
||||
:material-plus: [optimistic](#optimistic)
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-decagram: [servers](#servers)
|
||||
@@ -30,7 +25,6 @@ icon: material/alert-decagram
|
||||
"disable_expire": false,
|
||||
"independent_cache": false,
|
||||
"cache_capacity": 0,
|
||||
"optimistic": false, // or {}
|
||||
"reverse_mapping": false,
|
||||
"client_subnet": "",
|
||||
"fakeip": {}
|
||||
@@ -45,7 +39,7 @@ icon: material/alert-decagram
|
||||
|----------|---------------------------------|
|
||||
| `server` | List of [DNS Server](./server/) |
|
||||
| `rules` | List of [DNS Rule](./rule/) |
|
||||
| `fakeip` | :material-note-remove: [FakeIP](./fakeip/) |
|
||||
| `fakeip` | [FakeIP](./fakeip/) |
|
||||
|
||||
#### final
|
||||
|
||||
@@ -63,20 +57,12 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
||||
|
||||
Disable dns cache.
|
||||
|
||||
Conflict with `optimistic`.
|
||||
|
||||
#### disable_expire
|
||||
|
||||
Disable dns cache expire.
|
||||
|
||||
Conflict with `optimistic`.
|
||||
|
||||
#### independent_cache
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
`independent_cache` is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-independent-dns-cache).
|
||||
|
||||
Make each DNS server's cache independent for special purposes. If enabled, will slightly degrade performance.
|
||||
|
||||
#### cache_capacity
|
||||
@@ -87,34 +73,6 @@ LRU cache capacity.
|
||||
|
||||
Value less than 1024 will be ignored.
|
||||
|
||||
#### optimistic
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Enable optimistic DNS caching. When a cached DNS entry has expired but is still within the timeout window,
|
||||
the stale response is returned immediately while a background refresh is triggered.
|
||||
|
||||
Conflict with `disable_cache` and `disable_expire`.
|
||||
|
||||
Accepts a boolean or an object. When set to `true`, the default timeout of `3d` is used.
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"timeout": "3d"
|
||||
}
|
||||
```
|
||||
|
||||
##### enabled
|
||||
|
||||
Enable optimistic DNS caching.
|
||||
|
||||
##### timeout
|
||||
|
||||
The maximum time an expired cache entry can be served optimistically.
|
||||
|
||||
`3d` is used by default.
|
||||
|
||||
#### reverse_mapping
|
||||
|
||||
Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing.
|
||||
@@ -130,4 +88,4 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
|
||||
|
||||
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||
|
||||
Can be overridden by `servers.[].client_subnet` or `rules.[].client_subnet`.
|
||||
Can be overrides by `servers.[].client_subnet` or `rules.[].client_subnet`.
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-delete-clock: [independent_cache](#independent_cache)
|
||||
:material-plus: [optimistic](#optimistic)
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-decagram: [servers](#servers)
|
||||
@@ -30,7 +25,6 @@ icon: material/alert-decagram
|
||||
"disable_expire": false,
|
||||
"independent_cache": false,
|
||||
"cache_capacity": 0,
|
||||
"optimistic": false, // or {}
|
||||
"reverse_mapping": false,
|
||||
"client_subnet": "",
|
||||
"fakeip": {}
|
||||
@@ -62,20 +56,12 @@ icon: material/alert-decagram
|
||||
|
||||
禁用 DNS 缓存。
|
||||
|
||||
与 `optimistic` 冲突。
|
||||
|
||||
#### disable_expire
|
||||
|
||||
禁用 DNS 缓存过期。
|
||||
|
||||
与 `optimistic` 冲突。
|
||||
|
||||
#### independent_cache
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
`independent_cache` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除,参阅[迁移指南](/zh/migration/#迁移-independent-dns-cache)。
|
||||
|
||||
使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。
|
||||
|
||||
#### cache_capacity
|
||||
@@ -86,34 +72,6 @@ LRU 缓存容量。
|
||||
|
||||
小于 1024 的值将被忽略。
|
||||
|
||||
#### optimistic
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
启用乐观 DNS 缓存。当缓存的 DNS 条目已过期但仍在超时窗口内时,
|
||||
立即返回过期的响应,同时在后台触发刷新。
|
||||
|
||||
与 `disable_cache` 和 `disable_expire` 冲突。
|
||||
|
||||
接受布尔值或对象。当设置为 `true` 时,使用默认超时 `3d`。
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"timeout": "3d"
|
||||
}
|
||||
```
|
||||
|
||||
##### enabled
|
||||
|
||||
启用乐观 DNS 缓存。
|
||||
|
||||
##### timeout
|
||||
|
||||
过期缓存条目可被乐观提供的最长时间。
|
||||
|
||||
默认使用 `3d`。
|
||||
|
||||
#### reverse_mapping
|
||||
|
||||
在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。
|
||||
@@ -130,6 +88,6 @@ LRU 缓存容量。
|
||||
|
||||
可以被 `servers.[].client_subnet` 或 `rules.[].client_subnet` 覆盖。
|
||||
|
||||
#### fakeip :material-note-remove:
|
||||
#### fakeip
|
||||
|
||||
[FakeIP](./fakeip/) 设置。
|
||||
|
||||
@@ -2,20 +2,6 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [source_mac_address](#source_mac_address)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
:material-plus: [match_response](#match_response)
|
||||
:material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
|
||||
:material-plus: [response_rcode](#response_rcode)
|
||||
:material-plus: [response_answer](#response_answer)
|
||||
:material-plus: [response_ns](#response_ns)
|
||||
:material-plus: [response_extra](#response_extra)
|
||||
:material-plus: [package_name_regex](#package_name_regex)
|
||||
:material-alert: [ip_version](#ip_version)
|
||||
:material-alert: [query_type](#query_type)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
@@ -103,6 +89,12 @@ icon: material/alert-decagram
|
||||
"192.168.0.1"
|
||||
],
|
||||
"source_ip_is_private": false,
|
||||
"ip_cidr": [
|
||||
"10.0.0.0/24",
|
||||
"192.168.0.1"
|
||||
],
|
||||
"ip_is_private": false,
|
||||
"ip_accept_any": false,
|
||||
"source_port": [
|
||||
12345
|
||||
],
|
||||
@@ -132,9 +124,6 @@ icon: material/alert-decagram
|
||||
"package_name": [
|
||||
"com.termux"
|
||||
],
|
||||
"package_name_regex": [
|
||||
"^com\\.termux.*"
|
||||
],
|
||||
"user": [
|
||||
"sekai"
|
||||
],
|
||||
@@ -160,12 +149,6 @@ icon: material/alert-decagram
|
||||
"default_interface_address": [
|
||||
"2000::/3"
|
||||
],
|
||||
"source_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"source_hostname": [
|
||||
"my-device"
|
||||
],
|
||||
"wifi_ssid": [
|
||||
"My WIFI"
|
||||
],
|
||||
@@ -177,17 +160,7 @@ icon: material/alert-decagram
|
||||
"geosite-cn"
|
||||
],
|
||||
"rule_set_ip_cidr_match_source": false,
|
||||
"match_response": false,
|
||||
"ip_cidr": [
|
||||
"10.0.0.0/24",
|
||||
"192.168.0.1"
|
||||
],
|
||||
"ip_is_private": false,
|
||||
"ip_accept_any": false,
|
||||
"response_rcode": "",
|
||||
"response_answer": [],
|
||||
"response_ns": [],
|
||||
"response_extra": [],
|
||||
"rule_set_ip_cidr_accept_empty": false,
|
||||
"invert": false,
|
||||
"outbound": [
|
||||
"direct"
|
||||
@@ -196,8 +169,7 @@ icon: material/alert-decagram
|
||||
"server": "local",
|
||||
|
||||
// Deprecated
|
||||
|
||||
"rule_set_ip_cidr_accept_empty": false,
|
||||
|
||||
"rule_set_ipcidr_match_source": false,
|
||||
"geosite": [
|
||||
"cn"
|
||||
@@ -245,46 +217,12 @@ Tags of [Inbound](/configuration/inbound/).
|
||||
|
||||
#### ip_version
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
This field now also applies when a DNS rule is matched from an internal
|
||||
domain resolution that does not target a specific DNS server, such as a
|
||||
[`resolve`](../../route/rule_action/#resolve) route rule action without a
|
||||
`server` set. In earlier versions, only DNS queries received from a
|
||||
client evaluated this field. See
|
||||
[Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules)
|
||||
for the full list.
|
||||
|
||||
Setting this field makes the DNS rule incompatible in the same DNS
|
||||
configuration with Legacy Address Filter Fields in DNS rules, the Legacy
|
||||
`strategy` DNS rule action option, and the Legacy
|
||||
`rule_set_ip_cidr_accept_empty` DNS rule item. To combine with
|
||||
address-based filtering, use the [`evaluate`](../rule_action/#evaluate)
|
||||
action and [`match_response`](#match_response).
|
||||
|
||||
4 (A DNS query) or 6 (AAAA DNS query).
|
||||
|
||||
Not limited if empty.
|
||||
|
||||
#### query_type
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
This field now also applies when a DNS rule is matched from an internal
|
||||
domain resolution that does not target a specific DNS server, such as a
|
||||
[`resolve`](../../route/rule_action/#resolve) route rule action without a
|
||||
`server` set. In earlier versions, only DNS queries received from a
|
||||
client evaluated this field. See
|
||||
[Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules)
|
||||
for the full list.
|
||||
|
||||
Setting this field makes the DNS rule incompatible in the same DNS
|
||||
configuration with Legacy Address Filter Fields in DNS rules, the Legacy
|
||||
`strategy` DNS rule action option, and the Legacy
|
||||
`rule_set_ip_cidr_accept_empty` DNS rule item. To combine with
|
||||
address-based filtering, use the [`evaluate`](../rule_action/#evaluate)
|
||||
action and [`match_response`](#match_response).
|
||||
|
||||
DNS query type. Values can be integers or type name strings.
|
||||
|
||||
#### network
|
||||
@@ -387,12 +325,6 @@ Match process path using regular expression.
|
||||
|
||||
Match android package name.
|
||||
|
||||
#### package_name_regex
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Match android package name using regular expression.
|
||||
|
||||
#### user
|
||||
|
||||
!!! quote ""
|
||||
@@ -476,26 +408,6 @@ Matches network interface (same values as `network_type`) address.
|
||||
|
||||
Match default interface address.
|
||||
|
||||
#### source_mac_address
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||
|
||||
Match source device MAC address.
|
||||
|
||||
#### source_hostname
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||
|
||||
Match source device hostname from DHCP leases.
|
||||
|
||||
#### wifi_ssid
|
||||
|
||||
!!! quote ""
|
||||
@@ -534,25 +446,6 @@ Make `ip_cidr` rule items in rule-sets match the source IP.
|
||||
|
||||
Make `ip_cidr` rule items in rule-sets match the source IP.
|
||||
|
||||
#### match_response
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Enable response-based matching. When enabled, this rule matches against the evaluated response
|
||||
(set by a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action)
|
||||
instead of only matching the original query.
|
||||
|
||||
The evaluated response can also be returned directly by a later [`respond`](/configuration/dns/rule_action/#respond) action.
|
||||
|
||||
Required for Response Match Fields (`response_rcode`, `response_answer`, `response_ns`, `response_extra`).
|
||||
Also required for `ip_cidr`, `ip_is_private`, and `ip_accept_any` when used with `evaluate` or Response Match Fields.
|
||||
|
||||
#### ip_accept_any
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
Match when the DNS query response contains at least one address.
|
||||
|
||||
#### invert
|
||||
|
||||
Invert match result.
|
||||
@@ -597,12 +490,7 @@ See [DNS Rule Actions](../rule_action/) for details.
|
||||
|
||||
Moved to [DNS Rule Action](../rule_action#route).
|
||||
|
||||
### Legacy Address Filter Fields
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Legacy Address Filter Fields are deprecated and will be removed in sing-box 1.16.0,
|
||||
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
||||
### Address Filter Fields
|
||||
|
||||
Only takes effect for address requests (A/AAAA/HTTPS). When the query results do not match the address filtering rule items, the current rule will be skipped.
|
||||
|
||||
@@ -628,61 +516,23 @@ Match GeoIP with query response.
|
||||
|
||||
Match IP CIDR with query response.
|
||||
|
||||
As a Legacy Address Filter Field, deprecated. Use with `match_response` instead,
|
||||
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
||||
|
||||
#### ip_is_private
|
||||
|
||||
!!! question "Since sing-box 1.9.0"
|
||||
|
||||
Match private IP with query response.
|
||||
|
||||
As a Legacy Address Filter Field, deprecated. Use with `match_response` instead,
|
||||
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
||||
|
||||
#### rule_set_ip_cidr_accept_empty
|
||||
|
||||
!!! question "Since sing-box 1.10.0"
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
`rule_set_ip_cidr_accept_empty` is deprecated and will be removed in sing-box 1.16.0,
|
||||
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
||||
|
||||
Make `ip_cidr` rules in rule-sets accept empty query response.
|
||||
|
||||
### Response Match Fields
|
||||
#### ip_accept_any
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
Match fields for the evaluated response. Require `match_response` to be set to `true`
|
||||
and a preceding rule with [`evaluate`](/configuration/dns/rule_action/#evaluate) action to populate the response.
|
||||
|
||||
That evaluated response may also be returned directly by a later [`respond`](/configuration/dns/rule_action/#respond) action.
|
||||
|
||||
#### response_rcode
|
||||
|
||||
Match DNS response code.
|
||||
|
||||
Accepted values are the same as in the [predefined action rcode](/configuration/dns/rule_action/#rcode).
|
||||
|
||||
#### response_answer
|
||||
|
||||
Match DNS answer records.
|
||||
|
||||
Record format is the same as in [predefined action answer](/configuration/dns/rule_action/#answer).
|
||||
|
||||
#### response_ns
|
||||
|
||||
Match DNS name server records.
|
||||
|
||||
Record format is the same as in [predefined action ns](/configuration/dns/rule_action/#ns).
|
||||
|
||||
#### response_extra
|
||||
|
||||
Match DNS extra records.
|
||||
|
||||
Record format is the same as in [predefined action extra](/configuration/dns/rule_action/#extra).
|
||||
Match any IP with query response.
|
||||
|
||||
### Logical Fields
|
||||
|
||||
|
||||
@@ -2,20 +2,6 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [source_mac_address](#source_mac_address)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
:material-plus: [match_response](#match_response)
|
||||
:material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
|
||||
:material-plus: [response_rcode](#response_rcode)
|
||||
:material-plus: [response_answer](#response_answer)
|
||||
:material-plus: [response_ns](#response_ns)
|
||||
:material-plus: [response_extra](#response_extra)
|
||||
:material-plus: [package_name_regex](#package_name_regex)
|
||||
:material-alert: [ip_version](#ip_version)
|
||||
:material-alert: [query_type](#query_type)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
@@ -103,6 +89,12 @@ icon: material/alert-decagram
|
||||
"192.168.0.1"
|
||||
],
|
||||
"source_ip_is_private": false,
|
||||
"ip_cidr": [
|
||||
"10.0.0.0/24",
|
||||
"192.168.0.1"
|
||||
],
|
||||
"ip_is_private": false,
|
||||
"ip_accept_any": false,
|
||||
"source_port": [
|
||||
12345
|
||||
],
|
||||
@@ -132,9 +124,6 @@ icon: material/alert-decagram
|
||||
"package_name": [
|
||||
"com.termux"
|
||||
],
|
||||
"package_name_regex": [
|
||||
"^com\\.termux.*"
|
||||
],
|
||||
"user": [
|
||||
"sekai"
|
||||
],
|
||||
@@ -160,12 +149,6 @@ icon: material/alert-decagram
|
||||
"default_interface_address": [
|
||||
"2000::/3"
|
||||
],
|
||||
"source_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"source_hostname": [
|
||||
"my-device"
|
||||
],
|
||||
"wifi_ssid": [
|
||||
"My WIFI"
|
||||
],
|
||||
@@ -177,17 +160,7 @@ icon: material/alert-decagram
|
||||
"geosite-cn"
|
||||
],
|
||||
"rule_set_ip_cidr_match_source": false,
|
||||
"match_response": false,
|
||||
"ip_cidr": [
|
||||
"10.0.0.0/24",
|
||||
"192.168.0.1"
|
||||
],
|
||||
"ip_is_private": false,
|
||||
"ip_accept_any": false,
|
||||
"response_rcode": "",
|
||||
"response_answer": [],
|
||||
"response_ns": [],
|
||||
"response_extra": [],
|
||||
"rule_set_ip_cidr_accept_empty": false,
|
||||
"invert": false,
|
||||
"outbound": [
|
||||
"direct"
|
||||
@@ -196,8 +169,6 @@ icon: material/alert-decagram
|
||||
"server": "local",
|
||||
|
||||
// 已弃用
|
||||
|
||||
"rule_set_ip_cidr_accept_empty": false,
|
||||
"rule_set_ipcidr_match_source": false,
|
||||
"geosite": [
|
||||
"cn"
|
||||
@@ -245,38 +216,12 @@ icon: material/alert-decagram
|
||||
|
||||
#### ip_version
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效,
|
||||
例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。
|
||||
此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅
|
||||
[迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。
|
||||
|
||||
在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与
|
||||
旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项,
|
||||
或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。如需与
|
||||
基于地址的筛选组合,请使用 [`evaluate`](../rule_action/#evaluate) 动作和
|
||||
[`match_response`](#match_response)。
|
||||
|
||||
4 (A DNS 查询) 或 6 (AAAA DNS 查询)。
|
||||
|
||||
默认不限制。
|
||||
|
||||
#### query_type
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效,
|
||||
例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。
|
||||
此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅
|
||||
[迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。
|
||||
|
||||
在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与
|
||||
旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项,
|
||||
或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。如需与
|
||||
基于地址的筛选组合,请使用 [`evaluate`](../rule_action/#evaluate) 动作和
|
||||
[`match_response`](#match_response)。
|
||||
|
||||
DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
#### network
|
||||
@@ -379,12 +324,6 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
匹配 Android 应用包名。
|
||||
|
||||
#### package_name_regex
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
使用正则表达式匹配 Android 应用包名。
|
||||
|
||||
#### user
|
||||
|
||||
!!! quote ""
|
||||
@@ -468,26 +407,6 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
匹配默认接口地址。
|
||||
|
||||
#### source_mac_address
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||
|
||||
匹配源设备 MAC 地址。
|
||||
|
||||
#### source_hostname
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||
|
||||
匹配源设备从 DHCP 租约获取的主机名。
|
||||
|
||||
#### wifi_ssid
|
||||
|
||||
!!! quote ""
|
||||
@@ -526,23 +445,6 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
使规则集中的 `ip_cidr` 规则匹配源 IP。
|
||||
|
||||
#### match_response
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
启用响应匹配。启用后,此规则将匹配已评估的响应(由前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作设置),而不仅是匹配原始查询。
|
||||
|
||||
该已评估的响应也可以被后续的 [`respond`](/zh/configuration/dns/rule_action/#respond) 动作直接返回。
|
||||
|
||||
响应匹配字段(`response_rcode`、`response_answer`、`response_ns`、`response_extra`)需要此选项。
|
||||
当与 `evaluate` 或响应匹配字段一起使用时,`ip_cidr`、`ip_is_private` 和 `ip_accept_any` 也需要此选项。
|
||||
|
||||
#### ip_accept_any
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
当 DNS 查询响应包含至少一个地址时匹配。
|
||||
|
||||
#### invert
|
||||
|
||||
反选匹配结果。
|
||||
@@ -587,12 +489,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
已移动到 [DNS 规则动作](../rule_action#route).
|
||||
|
||||
### 旧版地址筛选字段
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
旧版地址筛选字段已废弃,且将在 sing-box 1.16.0 中被移除,
|
||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
||||
### 地址筛选字段
|
||||
|
||||
仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
|
||||
|
||||
@@ -619,62 +516,24 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
与查询响应匹配 IP CIDR。
|
||||
|
||||
作为旧版地址筛选字段已废弃。请改为配合 `match_response` 使用,
|
||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
||||
|
||||
#### ip_is_private
|
||||
|
||||
!!! question "自 sing-box 1.9.0 起"
|
||||
|
||||
与查询响应匹配非公开 IP。
|
||||
|
||||
作为旧版地址筛选字段已废弃。请改为配合 `match_response` 使用,
|
||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
||||
#### ip_accept_any
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
匹配任意 IP。
|
||||
|
||||
#### rule_set_ip_cidr_accept_empty
|
||||
|
||||
!!! question "自 sing-box 1.10.0 起"
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
`rule_set_ip_cidr_accept_empty` 已废弃且将在 sing-box 1.16.0 中被移除,
|
||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
||||
|
||||
使规则集中的 `ip_cidr` 规则接受空查询响应。
|
||||
|
||||
### 响应匹配字段
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
已评估的响应的匹配字段。需要将 `match_response` 设为 `true`,
|
||||
且需要前序规则使用 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作来填充响应。
|
||||
|
||||
该已评估的响应也可以被后续的 [`respond`](/zh/configuration/dns/rule_action/#respond) 动作直接返回。
|
||||
|
||||
#### response_rcode
|
||||
|
||||
匹配 DNS 响应码。
|
||||
|
||||
接受的值与 [predefined 动作 rcode](/zh/configuration/dns/rule_action/#rcode) 中相同。
|
||||
|
||||
#### response_answer
|
||||
|
||||
匹配 DNS 应答记录。
|
||||
|
||||
记录格式与 [predefined 动作 answer](/zh/configuration/dns/rule_action/#answer) 中相同。
|
||||
|
||||
#### response_ns
|
||||
|
||||
匹配 DNS 名称服务器记录。
|
||||
|
||||
记录格式与 [predefined 动作 ns](/zh/configuration/dns/rule_action/#ns) 中相同。
|
||||
|
||||
#### response_extra
|
||||
|
||||
匹配 DNS 额外记录。
|
||||
|
||||
记录格式与 [predefined 动作 extra](/zh/configuration/dns/rule_action/#extra) 中相同。
|
||||
|
||||
### 逻辑字段
|
||||
|
||||
#### type
|
||||
|
||||
@@ -2,13 +2,6 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-delete-clock: [strategy](#strategy)
|
||||
:material-plus: [evaluate](#evaluate)
|
||||
:material-plus: [respond](#respond)
|
||||
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [strategy](#strategy)
|
||||
@@ -24,7 +17,6 @@ icon: material/new-box
|
||||
"server": "",
|
||||
"strategy": "",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
@@ -42,10 +34,6 @@ Tag of target server.
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
`strategy` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0.
|
||||
|
||||
Set domain strategy for this query.
|
||||
|
||||
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
||||
@@ -54,12 +42,6 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
||||
|
||||
Disable cache and save cache in this query.
|
||||
|
||||
#### disable_optimistic_cache
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Disable optimistic DNS caching in this query.
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
Rewrite TTL in DNS responses.
|
||||
@@ -70,75 +52,7 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
|
||||
|
||||
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||
|
||||
Will override `dns.client_subnet`.
|
||||
|
||||
### evaluate
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "evaluate",
|
||||
"server": "",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
```
|
||||
|
||||
`evaluate` sends a DNS query to the specified server and saves the evaluated response for subsequent rules
|
||||
to match against using [`match_response`](/configuration/dns/rule/#match_response) and response fields.
|
||||
Unlike `route`, it does **not** terminate rule evaluation.
|
||||
|
||||
Only allowed on top-level DNS rules (not inside logical sub-rules).
|
||||
Rules that use [`match_response`](/configuration/dns/rule/#match_response) or Response Match Fields
|
||||
require a preceding top-level rule with `evaluate` action. A rule's own `evaluate` action
|
||||
does not satisfy this requirement, because matching happens before the action runs.
|
||||
|
||||
#### server
|
||||
|
||||
==Required==
|
||||
|
||||
Tag of target server.
|
||||
|
||||
#### disable_cache
|
||||
|
||||
Disable cache and save cache in this query.
|
||||
|
||||
#### disable_optimistic_cache
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Disable optimistic DNS caching in this query.
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
Rewrite TTL in DNS responses.
|
||||
|
||||
#### client_subnet
|
||||
|
||||
Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default.
|
||||
|
||||
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||
|
||||
Will override `dns.client_subnet`.
|
||||
|
||||
### respond
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "respond"
|
||||
}
|
||||
```
|
||||
|
||||
`respond` terminates rule evaluation and returns the evaluated response from a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action.
|
||||
|
||||
This action does not send a new DNS query and has no extra options.
|
||||
|
||||
Only allowed after a preceding top-level `evaluate` rule. If the action is reached without an evaluated response at runtime, the request fails with an error instead of falling through to later rules.
|
||||
Will overrides `dns.client_subnet`.
|
||||
|
||||
### route-options
|
||||
|
||||
@@ -146,7 +60,6 @@ Only allowed after a preceding top-level `evaluate` rule. If the action is reach
|
||||
{
|
||||
"action": "route-options",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
|
||||
@@ -2,13 +2,6 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-delete-clock: [strategy](#strategy)
|
||||
:material-plus: [evaluate](#evaluate)
|
||||
:material-plus: [respond](#respond)
|
||||
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [strategy](#strategy)
|
||||
@@ -24,7 +17,6 @@ icon: material/new-box
|
||||
"server": "",
|
||||
"strategy": "",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
@@ -42,10 +34,6 @@ icon: material/new-box
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
`strategy` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
为此查询设置域名策略。
|
||||
|
||||
可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。
|
||||
@@ -54,12 +42,6 @@ icon: material/new-box
|
||||
|
||||
在此查询中禁用缓存。
|
||||
|
||||
#### disable_optimistic_cache
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
在此查询中禁用乐观 DNS 缓存。
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
重写 DNS 回应中的 TTL。
|
||||
@@ -72,79 +54,12 @@ icon: material/new-box
|
||||
|
||||
将覆盖 `dns.client_subnet`.
|
||||
|
||||
### evaluate
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "evaluate",
|
||||
"server": "",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
```
|
||||
|
||||
`evaluate` 向指定服务器发送 DNS 查询并保存已评估的响应,供后续规则通过 [`match_response`](/zh/configuration/dns/rule/#match_response) 和响应字段进行匹配。与 `route` 不同,它**不会**终止规则评估。
|
||||
|
||||
仅允许在顶层 DNS 规则中使用(不可在逻辑子规则内部使用)。
|
||||
使用 [`match_response`](/zh/configuration/dns/rule/#match_response) 或响应匹配字段的规则,
|
||||
需要位于更早的顶层 `evaluate` 规则之后。规则自身的 `evaluate` 动作不能满足这个条件,
|
||||
因为匹配发生在动作执行之前。
|
||||
|
||||
#### server
|
||||
|
||||
==必填==
|
||||
|
||||
目标 DNS 服务器的标签。
|
||||
|
||||
#### disable_cache
|
||||
|
||||
在此查询中禁用缓存。
|
||||
|
||||
#### disable_optimistic_cache
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
在此查询中禁用乐观 DNS 缓存。
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
重写 DNS 回应中的 TTL。
|
||||
|
||||
#### client_subnet
|
||||
|
||||
默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
|
||||
|
||||
如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。
|
||||
|
||||
将覆盖 `dns.client_subnet`.
|
||||
|
||||
### respond
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "respond"
|
||||
}
|
||||
```
|
||||
|
||||
`respond` 会终止规则评估,并直接返回前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作保存的已评估的响应。
|
||||
|
||||
此动作不会发起新的 DNS 查询,也没有额外选项。
|
||||
|
||||
只能用于前面已有顶层 `evaluate` 规则的场景。如果运行时命中该动作时没有已评估的响应,则请求会直接返回错误,而不是继续匹配后续规则。
|
||||
|
||||
### route-options
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "route-options",
|
||||
"disable_cache": false,
|
||||
"disable_optimistic_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
@@ -169,7 +84,7 @@ icon: material/new-box
|
||||
- `default`: 返回 REFUSED。
|
||||
- `drop`: 丢弃请求。
|
||||
|
||||
默认使用 `default`。
|
||||
默认使用 `defualt`。
|
||||
|
||||
#### no_drop
|
||||
|
||||
|
||||
@@ -73,55 +73,24 @@ Example:
|
||||
|
||||
=== "Use hosts if available"
|
||||
|
||||
=== ":material-card-multiple: sing-box 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
...
|
||||
},
|
||||
{
|
||||
"type": "hosts",
|
||||
"tag": "hosts"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"action": "evaluate",
|
||||
"server": "hosts"
|
||||
},
|
||||
{
|
||||
"match_response": true,
|
||||
"ip_accept_any": true,
|
||||
"action": "respond"
|
||||
}
|
||||
]
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
...
|
||||
},
|
||||
{
|
||||
"type": "hosts",
|
||||
"tag": "hosts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-remove: sing-box < 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
...
|
||||
},
|
||||
{
|
||||
"type": "hosts",
|
||||
"tag": "hosts"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "hosts"
|
||||
}
|
||||
]
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "hosts"
|
||||
}
|
||||
}
|
||||
```
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -73,55 +73,24 @@ hosts 文件路径列表。
|
||||
|
||||
=== "如果可用则使用 hosts"
|
||||
|
||||
=== ":material-card-multiple: sing-box 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
...
|
||||
},
|
||||
{
|
||||
"type": "hosts",
|
||||
"tag": "hosts"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"action": "evaluate",
|
||||
"server": "hosts"
|
||||
},
|
||||
{
|
||||
"match_response": true,
|
||||
"ip_accept_any": true,
|
||||
"action": "respond"
|
||||
}
|
||||
]
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
...
|
||||
},
|
||||
{
|
||||
"type": "hosts",
|
||||
"tag": "hosts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-remove: sing-box < 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
...
|
||||
},
|
||||
{
|
||||
"type": "hosts",
|
||||
"tag": "hosts"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "hosts"
|
||||
}
|
||||
]
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "hosts"
|
||||
}
|
||||
}
|
||||
```
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ The type of the DNS server.
|
||||
|
||||
| Type | Format |
|
||||
|-----------------|---------------------------|
|
||||
| empty (default) | :material-note-remove: [Legacy](./legacy/) |
|
||||
| empty (default) | [Legacy](./legacy/) |
|
||||
| `local` | [Local](./local/) |
|
||||
| `hosts` | [Hosts](./hosts/) |
|
||||
| `tcp` | [TCP](./tcp/) |
|
||||
|
||||
@@ -29,7 +29,7 @@ DNS 服务器的类型。
|
||||
|
||||
| 类型 | 格式 |
|
||||
|-----------------|---------------------------|
|
||||
| empty (default) | :material-note-remove: [Legacy](./legacy/) |
|
||||
| empty (default) | [Legacy](./legacy/) |
|
||||
| `local` | [Local](./local/) |
|
||||
| `hosts` | [Hosts](./hosts/) |
|
||||
| `tcp` | [TCP](./tcp/) |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
icon: material/note-remove
|
||||
icon: material/delete-clock
|
||||
---
|
||||
|
||||
!!! failure "Removed in sing-box 1.14.0"
|
||||
!!! failure "Deprecated in sing-box 1.12.0"
|
||||
|
||||
Legacy DNS servers are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-server-formats).
|
||||
Legacy DNS servers is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers).
|
||||
|
||||
!!! quote "Changes in sing-box 1.9.0"
|
||||
|
||||
@@ -108,6 +108,6 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
|
||||
|
||||
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||
|
||||
Can be overridden by `rules.[].client_subnet`.
|
||||
Can be overrides by `rules.[].client_subnet`.
|
||||
|
||||
Will override `dns.client_subnet`.
|
||||
Will overrides `dns.client_subnet`.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
icon: material/note-remove
|
||||
icon: material/delete-clock
|
||||
---
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 移除"
|
||||
!!! failure "Deprecated in sing-box 1.12.0"
|
||||
|
||||
旧的 DNS 服务器配置已在 sing-box 1.12.0 废弃且已在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
||||
旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
||||
|
||||
!!! quote "sing-box 1.9.0 中的更改"
|
||||
|
||||
|
||||
@@ -43,62 +43,29 @@ If not enabled, `NXDOMAIN` will be returned for requests that do not match searc
|
||||
|
||||
=== "Split DNS only"
|
||||
|
||||
=== ":material-card-multiple: sing-box 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
},
|
||||
{
|
||||
"type": "resolved",
|
||||
"tag": "resolved",
|
||||
"service": "resolved"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"action": "evaluate",
|
||||
"server": "resolved"
|
||||
},
|
||||
{
|
||||
"match_response": true,
|
||||
"ip_accept_any": true,
|
||||
"action": "respond"
|
||||
}
|
||||
]
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
},
|
||||
{
|
||||
"type": "resolved",
|
||||
"tag": "resolved",
|
||||
"service": "resolved"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-remove: sing-box < 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
},
|
||||
{
|
||||
"type": "resolved",
|
||||
"tag": "resolved",
|
||||
"service": "resolved"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "resolved"
|
||||
}
|
||||
]
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "resolved"
|
||||
}
|
||||
}
|
||||
```
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Use as global DNS"
|
||||
|
||||
|
||||
@@ -42,62 +42,29 @@ icon: material/new-box
|
||||
|
||||
=== "仅分割 DNS"
|
||||
|
||||
=== ":material-card-multiple: sing-box 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
},
|
||||
{
|
||||
"type": "resolved",
|
||||
"tag": "resolved",
|
||||
"service": "resolved"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"action": "evaluate",
|
||||
"server": "resolved"
|
||||
},
|
||||
{
|
||||
"match_response": true,
|
||||
"ip_accept_any": true,
|
||||
"action": "respond"
|
||||
}
|
||||
]
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
},
|
||||
{
|
||||
"type": "resolved",
|
||||
"tag": "resolved",
|
||||
"service": "resolved"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-remove: sing-box < 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
},
|
||||
{
|
||||
"type": "resolved",
|
||||
"tag": "resolved",
|
||||
"service": "resolved"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "resolved"
|
||||
}
|
||||
]
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "resolved"
|
||||
}
|
||||
}
|
||||
```
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "用作全局 DNS"
|
||||
|
||||
|
||||
@@ -42,62 +42,29 @@ if not enabled, `NXDOMAIN` will be returned for non-Tailscale domain queries.
|
||||
|
||||
=== "MagicDNS only"
|
||||
|
||||
=== ":material-card-multiple: sing-box 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
},
|
||||
{
|
||||
"type": "tailscale",
|
||||
"tag": "ts",
|
||||
"endpoint": "ts-ep"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"action": "evaluate",
|
||||
"server": "ts"
|
||||
},
|
||||
{
|
||||
"match_response": true,
|
||||
"ip_accept_any": true,
|
||||
"action": "respond"
|
||||
}
|
||||
]
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
},
|
||||
{
|
||||
"type": "tailscale",
|
||||
"tag": "ts",
|
||||
"endpoint": "ts-ep"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-remove: sing-box < 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
},
|
||||
{
|
||||
"type": "tailscale",
|
||||
"tag": "ts",
|
||||
"endpoint": "ts-ep"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "ts"
|
||||
}
|
||||
]
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Use as global DNS"
|
||||
|
||||
|
||||
@@ -42,62 +42,29 @@ icon: material/new-box
|
||||
|
||||
=== "仅 MagicDNS"
|
||||
|
||||
=== ":material-card-multiple: sing-box 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
},
|
||||
{
|
||||
"type": "tailscale",
|
||||
"tag": "ts",
|
||||
"endpoint": "ts-ep"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"action": "evaluate",
|
||||
"server": "ts"
|
||||
},
|
||||
{
|
||||
"match_response": true,
|
||||
"ip_accept_any": true,
|
||||
"action": "respond"
|
||||
}
|
||||
]
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
},
|
||||
{
|
||||
"type": "tailscale",
|
||||
"tag": "ts",
|
||||
"endpoint": "ts-ep"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-remove: sing-box < 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
},
|
||||
{
|
||||
"type": "tailscale",
|
||||
"tag": "ts",
|
||||
"endpoint": "ts-ep"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "ts"
|
||||
}
|
||||
]
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "用作全局 DNS"
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-delete-clock: [store_rdrc](#store_rdrc)
|
||||
:material-plus: [store_dns](#store_dns)
|
||||
|
||||
!!! quote "Changes in sing-box 1.9.0"
|
||||
|
||||
:material-plus: [store_rdrc](#store_rdrc)
|
||||
@@ -19,8 +14,7 @@
|
||||
"cache_id": "",
|
||||
"store_fakeip": false,
|
||||
"store_rdrc": false,
|
||||
"rdrc_timeout": "",
|
||||
"store_dns": false
|
||||
"rdrc_timeout": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -48,13 +42,9 @@ Store fakeip in the cache file
|
||||
|
||||
#### store_rdrc
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
`store_rdrc` is deprecated and will be removed in sing-box 1.16.0, check [Migration](/migration/#migrate-store-rdrc).
|
||||
|
||||
Store rejected DNS response cache in the cache file
|
||||
|
||||
The check results of [Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields)
|
||||
The check results of [Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields)
|
||||
will be cached until expiration.
|
||||
|
||||
#### rdrc_timeout
|
||||
@@ -62,9 +52,3 @@ will be cached until expiration.
|
||||
Timeout of rejected DNS response cache.
|
||||
|
||||
`7d` is used by default.
|
||||
|
||||
#### store_dns
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Store DNS cache in the cache file.
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
!!! question "自 sing-box 1.8.0 起"
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-delete-clock: [store_rdrc](#store_rdrc)
|
||||
:material-plus: [store_dns](#store_dns)
|
||||
|
||||
!!! quote "sing-box 1.9.0 中的更改"
|
||||
|
||||
:material-plus: [store_rdrc](#store_rdrc)
|
||||
@@ -19,8 +14,7 @@
|
||||
"cache_id": "",
|
||||
"store_fakeip": false,
|
||||
"store_rdrc": false,
|
||||
"rdrc_timeout": "",
|
||||
"store_dns": false
|
||||
"rdrc_timeout": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -46,22 +40,12 @@
|
||||
|
||||
#### store_rdrc
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
`store_rdrc` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除,参阅[迁移指南](/zh/migration/#迁移-store_rdrc)。
|
||||
|
||||
将拒绝的 DNS 响应缓存存储在缓存文件中。
|
||||
|
||||
[旧版地址筛选字段](/zh/configuration/dns/rule/#旧版地址筛选字段) 的检查结果将被缓存至过期。
|
||||
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#地址筛选字段) 的检查结果将被缓存至过期。
|
||||
|
||||
#### rdrc_timeout
|
||||
|
||||
拒绝的 DNS 响应缓存超时。
|
||||
|
||||
默认使用 `7d`。
|
||||
|
||||
#### store_dns
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
将 DNS 缓存存储在缓存文件中。
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
`cloudflared` inbound runs an embedded Cloudflare Tunnel client and routes all
|
||||
incoming tunnel traffic (TCP, UDP, ICMP) through sing-box's routing engine.
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "cloudflared",
|
||||
"tag": "",
|
||||
|
||||
"token": "",
|
||||
"ha_connections": 0,
|
||||
"protocol": "",
|
||||
"post_quantum": false,
|
||||
"edge_ip_version": 0,
|
||||
"datagram_version": "",
|
||||
"grace_period": "",
|
||||
"region": "",
|
||||
"control_dialer": {
|
||||
... // Dial Fields
|
||||
},
|
||||
"tunnel_dialer": {
|
||||
... // Dial Fields
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### token
|
||||
|
||||
==Required==
|
||||
|
||||
Base64-encoded tunnel token from the Cloudflare Zero Trust dashboard
|
||||
(`Networks → Tunnels → Install connector`).
|
||||
|
||||
#### ha_connections
|
||||
|
||||
Number of high-availability connections to the Cloudflare edge.
|
||||
|
||||
Capped by the number of discovered edge addresses.
|
||||
|
||||
#### protocol
|
||||
|
||||
Transport protocol for edge connections.
|
||||
|
||||
One of `quic` `http2`.
|
||||
|
||||
#### post_quantum
|
||||
|
||||
Enable post-quantum key exchange on the control connection.
|
||||
|
||||
#### edge_ip_version
|
||||
|
||||
IP version used when connecting to the Cloudflare edge.
|
||||
|
||||
One of `0` (automatic) `4` `6`.
|
||||
|
||||
#### datagram_version
|
||||
|
||||
Datagram protocol version used for UDP proxying over QUIC.
|
||||
|
||||
One of `v2` `v3`. Only meaningful when `protocol` is `quic`.
|
||||
|
||||
#### grace_period
|
||||
|
||||
Graceful shutdown window for in-flight edge connections.
|
||||
|
||||
#### region
|
||||
|
||||
Cloudflare edge region selector.
|
||||
|
||||
Conflict with endpoints embedded in `token`.
|
||||
|
||||
#### control_dialer
|
||||
|
||||
[Dial Fields](/configuration/shared/dial/) used when the tunnel client dials the
|
||||
Cloudflare control plane.
|
||||
|
||||
#### tunnel_dialer
|
||||
|
||||
[Dial Fields](/configuration/shared/dial/) used when the tunnel client dials the
|
||||
Cloudflare edge data plane.
|
||||
@@ -1,89 +0,0 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
`cloudflared` 入站运行一个内嵌的 Cloudflare Tunnel 客户端,并将所有传入的隧道流量
|
||||
(TCP、UDP、ICMP)通过 sing-box 的路由引擎转发。
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "cloudflared",
|
||||
"tag": "",
|
||||
|
||||
"token": "",
|
||||
"ha_connections": 0,
|
||||
"protocol": "",
|
||||
"post_quantum": false,
|
||||
"edge_ip_version": 0,
|
||||
"datagram_version": "",
|
||||
"grace_period": "",
|
||||
"region": "",
|
||||
"control_dialer": {
|
||||
... // 拨号字段
|
||||
},
|
||||
"tunnel_dialer": {
|
||||
... // 拨号字段
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### token
|
||||
|
||||
==必填==
|
||||
|
||||
来自 Cloudflare Zero Trust 仪表板的 Base64 编码隧道令牌
|
||||
(`Networks → Tunnels → Install connector`)。
|
||||
|
||||
#### ha_connections
|
||||
|
||||
到 Cloudflare edge 的高可用连接数。
|
||||
|
||||
上限为已发现的 edge 地址数量。
|
||||
|
||||
#### protocol
|
||||
|
||||
edge 连接使用的传输协议。
|
||||
|
||||
`quic` `http2` 之一。
|
||||
|
||||
#### post_quantum
|
||||
|
||||
在控制连接上启用后量子密钥交换。
|
||||
|
||||
#### edge_ip_version
|
||||
|
||||
连接 Cloudflare edge 时使用的 IP 版本。
|
||||
|
||||
`0`(自动)`4` `6` 之一。
|
||||
|
||||
#### datagram_version
|
||||
|
||||
通过 QUIC 进行 UDP 代理时使用的数据报协议版本。
|
||||
|
||||
`v2` `v3` 之一。仅在 `protocol` 为 `quic` 时有效。
|
||||
|
||||
#### grace_period
|
||||
|
||||
正在处理的 edge 连接的优雅关闭窗口。
|
||||
|
||||
#### region
|
||||
|
||||
Cloudflare edge 区域选择器。
|
||||
|
||||
与 `token` 中嵌入的 endpoint 冲突。
|
||||
|
||||
#### control_dialer
|
||||
|
||||
隧道客户端拨向 Cloudflare 控制面时使用的
|
||||
[拨号字段](/zh/configuration/shared/dial/)。
|
||||
|
||||
#### tunnel_dialer
|
||||
|
||||
隧道客户端拨向 Cloudflare edge 数据面时使用的
|
||||
[拨号字段](/zh/configuration/shared/dial/)。
|
||||
@@ -2,10 +2,6 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [bbr_profile](#bbr_profile)
|
||||
|
||||
!!! quote "Changes in sing-box 1.11.0"
|
||||
|
||||
:material-alert: [masquerade](#masquerade)
|
||||
@@ -35,7 +31,6 @@ icon: material/alert-decagram
|
||||
"ignore_client_bandwidth": false,
|
||||
"tls": {},
|
||||
"masquerade": "", // or {}
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false
|
||||
}
|
||||
```
|
||||
@@ -146,14 +141,6 @@ Fixed response headers.
|
||||
|
||||
Fixed response content.
|
||||
|
||||
#### bbr_profile
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
BBR congestion control algorithm profile, one of `conservative` `standard` `aggressive`.
|
||||
|
||||
`standard` is used by default.
|
||||
|
||||
#### brutal_debug
|
||||
|
||||
Enable debug information logging for Hysteria Brutal CC.
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [bbr_profile](#bbr_profile)
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-alert: [masquerade](#masquerade)
|
||||
@@ -35,7 +31,6 @@ icon: material/alert-decagram
|
||||
"ignore_client_bandwidth": false,
|
||||
"tls": {},
|
||||
"masquerade": "", // 或 {}
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false
|
||||
}
|
||||
```
|
||||
@@ -143,14 +138,6 @@ HTTP3 服务器认证失败时的行为 (对象配置)。
|
||||
|
||||
固定响应内容。
|
||||
|
||||
#### bbr_profile
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
BBR 拥塞控制算法配置,可选 `conservative` `standard` `aggressive`。
|
||||
|
||||
默认使用 `standard`。
|
||||
|
||||
#### brutal_debug
|
||||
|
||||
启用 Hysteria Brutal CC 的调试信息日志记录。
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
| `tun` | [Tun](./tun/) | :material-close: |
|
||||
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
||||
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
||||
| `cloudflared` | [Cloudflared](./cloudflared/) | :material-close: |
|
||||
|
||||
#### tag
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user