Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f2f019f16 | ||
|
|
86bf3df0fd | ||
|
|
daf30bff6e | ||
|
|
b5bc4a853d | ||
|
|
144e306562 | ||
|
|
52bb26680a | ||
|
|
0b27ac2885 | ||
|
|
23d55373a4 | ||
|
|
d375d7e455 | ||
|
|
1e40350c89 | ||
|
|
e31bde1836 | ||
|
|
86222771c5 | ||
|
|
d114cdd20f | ||
|
|
df874b85be | ||
|
|
8d475dc3fc | ||
|
|
40c857748f | ||
|
|
afbba5d928 | ||
|
|
fc3914baf5 | ||
|
|
4923a13c5d | ||
|
|
d55e63b88e | ||
|
|
7b506e25a7 | ||
|
|
7fb42c3b73 | ||
|
|
dc11298558 | ||
|
|
021f523431 | ||
|
|
346f32d3d0 | ||
|
|
f24735a86f | ||
|
|
7756a15cbe | ||
|
|
741460e1e4 | ||
|
|
13412895c5 | ||
|
|
425a24273b | ||
|
|
4e61612944 | ||
|
|
15e5d89f70 | ||
|
|
46f1b57367 | ||
|
|
9cb3833216 | ||
|
|
7205517160 | ||
|
|
be2034dd06 | ||
|
|
f0d00d6161 | ||
|
|
034f5066cd | ||
|
|
9869ddaa4b | ||
|
|
5ea6766238 | ||
|
|
5efbc8587f | ||
|
|
7632cd64da | ||
|
|
ba86e47cc6 | ||
|
|
d742fa34aa | ||
|
|
800c840bb5 | ||
|
|
16b666ad25 | ||
|
|
ca17e856a6 | ||
|
|
d87624b353 | ||
|
|
a7ab60e0e4 | ||
|
|
8909e88484 | ||
|
|
3d0d2bed9e | ||
|
|
7ef023fcdd | ||
|
|
69d476ad13 | ||
|
|
cf3fcb75b5 | ||
|
|
17195a5cc4 | ||
|
|
22bcfebe3e | ||
|
|
47a2fb19a6 | ||
|
|
62e43855cb | ||
|
|
5efbf4853a | ||
|
|
3e0bde68b8 | ||
|
|
bda06ebb61 | ||
|
|
be0859aca6 | ||
|
|
b2f1b37336 | ||
|
|
583fb45939 | ||
|
|
a343a907a9 | ||
|
|
b1ac64f40f | ||
|
|
122fb9b54e | ||
|
|
09780c1698 | ||
|
|
e533b48244 | ||
|
|
c0bd050742 | ||
|
|
5edc4cb1cd | ||
|
|
e7709f1cb5 | ||
|
|
c0f42ebe8c | ||
|
|
9d965cae64 | ||
|
|
da28c8badc | ||
|
|
e38b1354b3 | ||
|
|
793b57c2a1 | ||
|
|
4a25ca934b | ||
|
|
13f2ebfe12 | ||
|
|
9ba364ff60 | ||
|
|
ba653da9a0 | ||
|
|
64c4d73044 | ||
|
|
e0745f4bab | ||
|
|
e3ca7ffa54 | ||
|
|
df97f3a64d | ||
|
|
00957e5f9d | ||
|
|
bfa3383aaa | ||
|
|
73bea01f40 | ||
|
|
0378191783 | ||
|
|
d5fa6a608d | ||
|
|
811f151155 | ||
|
|
8428a89d2d | ||
|
|
513695297c | ||
|
|
bfbfa2ef8d | ||
|
|
8e1d079142 | ||
|
|
55f15bb6f0 | ||
|
|
96fd39649a | ||
|
|
74fc8b300d | ||
|
|
baeee40b79 | ||
|
|
4342c8d7a2 | ||
|
|
1178b312fa | ||
|
|
362aa7a9cd | ||
|
|
12a7b5a5c5 | ||
|
|
4eba9b07b6 | ||
|
|
1b48029bdc | ||
|
|
3542e944cb | ||
|
|
852d1c9e14 | ||
|
|
4958394469 | ||
|
|
41b6d65604 | ||
|
|
aae30894dd | ||
|
|
81d169abfc | ||
|
|
9c6c210e89 | ||
|
|
d1c6dcf754 | ||
|
|
97c8c4f55a | ||
|
|
ed8df2d58f | ||
|
|
f66010e6f9 | ||
|
|
d5c4700d32 | ||
|
|
969ecfc4ca | ||
|
|
8f862997eb | ||
|
|
b20075e3dc | ||
|
|
eb3b5aae51 | ||
|
|
af6b6ab6f1 | ||
|
|
5a1668c753 | ||
|
|
820d9095d3 | ||
|
|
2fb41ccbba | ||
|
|
b4666be696 | ||
|
|
4688ad74ad | ||
|
|
f7ea78d4f0 | ||
|
|
ac112440c3 | ||
|
|
958b246f05 | ||
|
|
263f4c3bc9 | ||
|
|
ffddc517e1 | ||
|
|
5cd0a3e846 | ||
|
|
f4319c4d4f | ||
|
|
0091a535d5 | ||
|
|
d7a5fb8d66 | ||
|
|
f63054e937 | ||
|
|
efc043abbb | ||
|
|
40c6de8e31 | ||
|
|
2db655bd6d | ||
|
|
c49c56612b | ||
|
|
6ca074abae | ||
|
|
84430055ab | ||
|
|
432fcb3fc3 | ||
|
|
fae32361f2 | ||
|
|
bcb2e512d4 | ||
|
|
82ca04a8a7 | ||
|
|
2ef3b72224 | ||
|
|
6d319cba1d | ||
|
|
3687519ef3 | ||
|
|
3a4ac59467 | ||
|
|
1cfc135df3 | ||
|
|
5b35c51da9 | ||
|
|
ec7ddd3bad | ||
|
|
6f3e708679 | ||
|
|
869e1b89f5 | ||
|
|
9e0a3b6936 | ||
|
|
c6cb1a77d0 | ||
|
|
83010861ba | ||
|
|
daa53e5168 | ||
|
|
51befdbf87 | ||
|
|
8311b11713 | ||
|
|
19c80c7b9c | ||
|
|
a879dd1b14 | ||
|
|
a8feb9ac2b | ||
|
|
c5fbd29c0e | ||
|
|
26b1794723 | ||
|
|
371b4b70a3 | ||
|
|
b2cc38ee63 | ||
|
|
79b562cdc9 | ||
|
|
e3f089251c | ||
|
|
cf6dcbc054 | ||
|
|
2cf2b0fcac | ||
|
|
aa0cca3bb6 | ||
|
|
fb59f01058 | ||
|
|
e91a0da70a | ||
|
|
9cc617ae4c | ||
|
|
e4b0f1f1bb | ||
|
|
443c3ca0b3 | ||
|
|
55a0e5952c | ||
|
|
1dff388717 | ||
|
|
61c741f887 | ||
|
|
01dd9a05c3 | ||
|
|
8c19a2293c | ||
|
|
a1bec48dc9 | ||
|
|
7e289865b2 | ||
|
|
742c7edd57 | ||
|
|
b71a2889ef | ||
|
|
bcd75d6ce3 | ||
|
|
d4c1b0e867 | ||
|
|
b037ea9c3f | ||
|
|
b5f475cd4c | ||
|
|
eaa4d2c7b8 | ||
|
|
e160d9b048 | ||
|
|
0aeea39fbe | ||
|
|
e000636d83 | ||
|
|
8e4dc508bb | ||
|
|
e2684a93de | ||
|
|
1d89ddbb16 | ||
|
|
2bfdd44759 | ||
|
|
77966916c4 | ||
|
|
26b7455c1e | ||
|
|
8922e7b991 | ||
|
|
e6ac31fb20 | ||
|
|
c8f3c5d6aa | ||
|
|
330659e449 | ||
|
|
80043df292 | ||
|
|
ecd1ea6f8c | ||
|
|
694b8d349d | ||
|
|
ef44027f57 | ||
|
|
f3db348b01 | ||
|
|
c4eacf4591 | ||
|
|
59d4475743 | ||
|
|
22b4c4be2c | ||
|
|
5f31583a84 | ||
|
|
1d25240d8c | ||
|
|
eeb507d6ea | ||
|
|
9e9916efa5 | ||
|
|
db6b9e3684 | ||
|
|
ff24332e23 | ||
|
|
d4ff0b1767 | ||
|
|
5716f7f16b | ||
|
|
e5bd8f9e24 | ||
|
|
b56bcfb4b0 | ||
|
|
fb95b4827c | ||
|
|
a8f7226195 | ||
|
|
e6ee485352 | ||
|
|
73291a3a1c | ||
|
|
f737708f45 | ||
|
|
aa24d09aa2 | ||
|
|
fe4e77979d | ||
|
|
7a26640c26 | ||
|
|
5a777959e3 | ||
|
|
3512a80597 | ||
|
|
011770a601 | ||
|
|
6475724d2e | ||
|
|
85e9029577 | ||
|
|
b6e292cce3 | ||
|
|
c58140fb47 | ||
|
|
aebb7facfa | ||
|
|
1e2124cb99 | ||
|
|
e1cbd07d1f | ||
|
|
7750e81168 | ||
|
|
bf3edbd28f | ||
|
|
cd2cf56358 | ||
|
|
21f4a944a7 | ||
|
|
9617005136 | ||
|
|
c85d1d41b3 | ||
|
|
9e3c9228bb | ||
|
|
acd7c85ff6 | ||
|
|
8727221513 | ||
|
|
cdedaf3f63 | ||
|
|
ffe5644ddc | ||
|
|
ccc684a9ab | ||
|
|
977e502150 | ||
|
|
518d26b25f | ||
|
|
101f416268 | ||
|
|
ffa08d1c43 | ||
|
|
cf3f9169b7 | ||
|
|
8343cd5e76 | ||
|
|
005b321f62 | ||
|
|
53264f67bf | ||
|
|
f8b34e3c86 | ||
|
|
ce1bdac2bc | ||
|
|
bd8f01fb26 | ||
|
|
b590700540 | ||
|
|
48c5c23f9b | ||
|
|
f4f591d14c | ||
|
|
0c16e2211b | ||
|
|
4bfea06a12 | ||
|
|
057ee9f2c5 | ||
|
|
7f48ca54a3 | ||
|
|
ee5227130c | ||
|
|
2e0d9a2b54 | ||
|
|
c5d732773f | ||
|
|
88a45d1156 | ||
|
|
4e651a72f7 | ||
|
|
7c563153ae | ||
|
|
cb81c0df85 | ||
|
|
9c316ea01c | ||
|
|
541fc664e3 | ||
|
|
18478b7c4b | ||
|
|
650323faef | ||
|
|
ed131272d4 | ||
|
|
39b056c87a | ||
|
|
c19cd1bff3 | ||
|
|
37531507db | ||
|
|
ca9b4c58b1 | ||
|
|
4341bcba5d | ||
|
|
0be4ac1fa5 | ||
|
|
28cd6da502 | ||
|
|
0712ef762d | ||
|
|
eee7d7a1ed | ||
|
|
4c58def0db | ||
|
|
c6a32e4467 | ||
|
|
30f0ff16ca | ||
|
|
38d117ee44 | ||
|
|
7aba65ea32 | ||
|
|
fe4dff5df0 | ||
|
|
2bc51daa98 | ||
|
|
838b6101b9 | ||
|
|
056c9da781 | ||
|
|
2a656d6a0c | ||
|
|
43a650f9ab | ||
|
|
88a55859ac | ||
|
|
d686c8721f | ||
|
|
0a718163fd | ||
|
|
53f279f5ff | ||
|
|
ae6d929f4a | ||
|
|
bb82b3a5b0 | ||
|
|
70b122fb91 | ||
|
|
67cba2c326 | ||
|
|
b86692d009 | ||
|
|
28e645a277 | ||
|
|
1f2517c731 | ||
|
|
b44053f496 | ||
|
|
5b9ac65477 | ||
|
|
d726d46a00 | ||
|
|
1273426009 | ||
|
|
b50744690e | ||
|
|
55b93454dc | ||
|
|
89cc75f674 | ||
|
|
6bb2fd9a15 | ||
|
|
8ab98bba8f | ||
|
|
26d002bc2b | ||
|
|
71679e889a | ||
|
|
7485f5f64e | ||
|
|
bbe8f9f810 | ||
|
|
eba9504fc2 | ||
|
|
67ac9b00ff | ||
|
|
3ffa6214ca | ||
|
|
6f278ab167 | ||
|
|
f10b45a67c | ||
|
|
cc8f35787e | ||
|
|
8f1786fa23 | ||
|
|
70dddeace3 | ||
|
|
8cc9da9d6d | ||
|
|
5292b87275 | ||
|
|
87b7b7ed7c | ||
|
|
999a486928 | ||
|
|
627e989faa | ||
|
|
af95312949 | ||
|
|
a452c34390 | ||
|
|
4d5330fa0a | ||
|
|
5e48626cb9 | ||
|
|
ad7dc3a129 | ||
|
|
92fab5aafa | ||
|
|
841d525913 | ||
|
|
d2efbbef04 | ||
|
|
971ef82679 | ||
|
|
020bf04ec4 | ||
|
|
4d91582fd8 | ||
|
|
e9b4dbce6e | ||
|
|
00fd02c739 | ||
|
|
c0d2045e52 | ||
|
|
835cd407bf | ||
|
|
f5ba5bb146 | ||
|
|
7a694257d9 | ||
|
|
67abf4446d | ||
|
|
7035a3fef4 | ||
|
|
4445916ba7 | ||
|
|
a102a8bfc7 | ||
|
|
c9e8c35e77 | ||
|
|
1a1be8138a | ||
|
|
e06e8a9e8a | ||
|
|
56fd6e4ab6 | ||
|
|
215db09925 | ||
|
|
9fff5e4fec | ||
|
|
802d3f78d7 | ||
|
|
3593035eb9 | ||
|
|
757d76c9da | ||
|
|
445e68ddd1 | ||
|
|
b540ec3f46 | ||
|
|
5c90431876 | ||
|
|
793889c3b7 | ||
|
|
eb42086f9c | ||
|
|
d0efc40efb | ||
|
|
ae704d1d5f | ||
|
|
525dfd9fc1 | ||
|
|
18bd178bbd | ||
|
|
088155f6f3 | ||
|
|
b750faa66f | ||
|
|
ef3309814d | ||
|
|
b87a05b457 | ||
|
|
754439f03c | ||
|
|
2145ef40b9 | ||
|
|
a3806e0190 | ||
|
|
0ceb58586b | ||
|
|
719a1fe7cf | ||
|
|
671b8d5a0c | ||
|
|
e29206aef9 | ||
|
|
3299a77da3 | ||
|
|
0804fd6632 | ||
|
|
ea76114d50 | ||
|
|
9304d3b227 | ||
|
|
78004de5e5 | ||
|
|
5b7384fddd | ||
|
|
08a92a53c3 | ||
|
|
34560af141 | ||
|
|
2e7e0088dd | ||
|
|
d23366ea84 | ||
|
|
df7eb47593 | ||
|
|
839a28a3d5 | ||
|
|
9c6d1dabdf | ||
|
|
e6ec7f405c | ||
|
|
8f37d4ef7c | ||
|
|
c37af8c1be | ||
|
|
489661a2ce | ||
|
|
fa3e208668 | ||
|
|
4d240efde9 | ||
|
|
d9bcbd9b31 | ||
|
|
35ff9b82fc | ||
|
|
a511abb613 | ||
|
|
1eec27b5ff | ||
|
|
1de7777a71 | ||
|
|
975ca8bd9c | ||
|
|
e43537939a | ||
|
|
0087ac3ffc | ||
|
|
7de4b33dd1 | ||
|
|
8ffc2f12e4 | ||
|
|
37b24164b6 | ||
|
|
8cdb27d43d | ||
|
|
efa17a7c10 | ||
|
|
6d14e9e441 | ||
|
|
e3e406dcde | ||
|
|
d0a6c93c2c | ||
|
|
84bfac144c | ||
|
|
9eddb4b072 | ||
|
|
4fca0f40fe | ||
|
|
43b9e6e6e9 | ||
|
|
583c768f40 | ||
|
|
b1b2421561 | ||
|
|
3d610c0f0f | ||
|
|
2ec88da823 | ||
|
|
5514de1187 | ||
|
|
e70eed74e2 | ||
|
|
7dc5988620 | ||
|
|
354a4e1d7b | ||
|
|
5409c5bbe7 | ||
|
|
33ff9554cd | ||
|
|
975b4e7664 | ||
|
|
1f6a715939 | ||
|
|
8e7a8de5e5 | ||
|
|
4f53fccd25 | ||
|
|
876d550f68 | ||
|
|
2660ed5fda | ||
|
|
50c6f5ae6c | ||
|
|
85f0091056 | ||
|
|
e25cd9be37 | ||
|
|
1fb5ca9475 | ||
|
|
7f3a9c021c | ||
|
|
0427b48d75 | ||
|
|
0b729b99e7 | ||
|
|
940238f158 | ||
|
|
3f6c7ba1d2 | ||
|
|
0025973453 | ||
|
|
c3a217c9d2 | ||
|
|
13c2e72871 | ||
|
|
3c65594030 | ||
|
|
f85b031402 | ||
|
|
ac3e994682 | ||
|
|
139f6b3c4c | ||
|
|
a4bb555fac | ||
|
|
d0cfc49806 | ||
|
|
01e491ec07 | ||
|
|
bf021a9ead | ||
|
|
70e69a382e | ||
|
|
cd26d9f669 | ||
|
|
4fd0253e99 | ||
|
|
ebab70ca3b | ||
|
|
ae4a158e36 | ||
|
|
760a1e6306 | ||
|
|
fded8b1de0 | ||
|
|
762d5cd392 | ||
|
|
09ac79b9f3 | ||
|
|
16f6fb0c59 | ||
|
|
385e790600 | ||
|
|
95e4e5a931 | ||
|
|
e1bfec6fe2 | ||
|
|
dde7a4dff1 | ||
|
|
40601bd05b | ||
|
|
72d5ed908e | ||
|
|
72673a9d52 | ||
|
|
327ccdcf38 | ||
|
|
8c2f96d1aa | ||
|
|
34ba0bc95b | ||
|
|
ed162c2e66 | ||
|
|
40b5fe9a54 | ||
|
|
5a98fac395 | ||
|
|
0bab14cd72 | ||
|
|
b407cfd9d4 | ||
|
|
25dcdc652a | ||
|
|
950cb04534 | ||
|
|
c07d1286ef | ||
|
|
8ddd153022 | ||
|
|
870353c499 | ||
|
|
ecebbecd3b | ||
|
|
f39fbb2ce2 | ||
|
|
ec56c0bc45 | ||
|
|
20a6025075 | ||
|
|
707963c0d9 | ||
|
|
3c7837692e | ||
|
|
f890812577 | ||
|
|
47f3efe71b | ||
|
|
6d88b10b14 | ||
|
|
d34a51739f | ||
|
|
a6773aa549 | ||
|
|
0314c66635 | ||
|
|
3fb172b4d2 | ||
|
|
96fc19b803 | ||
|
|
9f7ba8ab8f | ||
|
|
e592e9f29a | ||
|
|
4608bca998 | ||
|
|
b5dfc7374c | ||
|
|
b469f8197a | ||
|
|
0a38a8ef4a | ||
|
|
e75be7801f | ||
|
|
6c49bb1865 | ||
|
|
f9c24bc205 | ||
|
|
d7c3179c6e | ||
|
|
b0fd37949a | ||
|
|
29994b663a | ||
|
|
fc397c35c5 | ||
|
|
0f2b214918 | ||
|
|
fec885c427 | ||
|
|
5a2fd4465c | ||
|
|
83d1ecc4da | ||
|
|
7c6daf7c56 | ||
|
|
28fe6257be | ||
|
|
99430983bc | ||
|
|
d758a4958f | ||
|
|
95b12dda5a | ||
|
|
2675cf2d00 | ||
|
|
72be46e8fa | ||
|
|
c5580feb64 | ||
|
|
7e3819be86 | ||
|
|
f0302f2be7 | ||
|
|
b5f60f843d | ||
|
|
6bdfb8b01f | ||
|
|
ef1d81a2a1 | ||
|
|
739b4ee106 | ||
|
|
6a038e8a88 | ||
|
|
72ea8a9f76 | ||
|
|
44d93648ee | ||
|
|
75f7865769 | ||
|
|
01e3ad99ca | ||
|
|
3c0d85c9db | ||
|
|
b38991a14e | ||
|
|
465269566b | ||
|
|
f103fc13d9 | ||
|
|
e5917fad4e | ||
|
|
de8c89eb03 | ||
|
|
c142db301a | ||
|
|
8dc8c7d9e2 | ||
|
|
2b909e04ea | ||
|
|
e130c3f2e4 | ||
|
|
3ad754879f | ||
|
|
fd2b3768e1 | ||
|
|
67cff12c76 | ||
|
|
c5ea7848b3 | ||
|
|
34365a096e | ||
|
|
d880dfbbca | ||
|
|
b46a200f8d | ||
|
|
81490d0662 | ||
|
|
3d1e841cc5 | ||
|
|
f52936a103 | ||
|
|
23f69ce6a4 | ||
|
|
f84ae228fc | ||
|
|
74c716ccaa | ||
|
|
445b02b2ca | ||
|
|
bb17ffa9fc | ||
|
|
389ea709ce | ||
|
|
c2f535ead4 | ||
|
|
0318f55322 | ||
|
|
1f4340e82f | ||
|
|
ed08707c98 | ||
|
|
7397abcb94 | ||
|
|
98d321f8ac | ||
|
|
e78b0ef869 | ||
|
|
8d654330ac | ||
|
|
00d61333d3 | ||
|
|
03b55b61e7 | ||
|
|
745e44cc87 | ||
|
|
24213a874a | ||
|
|
155f8a2ba2 | ||
|
|
568dca6f9c | ||
|
|
673c34cf5a | ||
|
|
2050ed78d0 | ||
|
|
2632c44195 | ||
|
|
5449eabf2a | ||
|
|
dd5b00faf4 | ||
|
|
0caec3e4da | ||
|
|
e48e62cac0 | ||
|
|
06ebda2e2f | ||
|
|
53c449b9fb | ||
|
|
51e0fac72c | ||
|
|
32b1fe0893 | ||
|
|
2af3b82e32 | ||
|
|
eca1231831 | ||
|
|
e833c2a28b | ||
|
|
8b89a037e8 | ||
|
|
1e821a03fe | ||
|
|
66051967fe | ||
|
|
a63778854f | ||
|
|
4aea0821dd | ||
|
|
08546925cc | ||
|
|
d0f26d9303 | ||
|
|
2a5d5ea4df | ||
|
|
b69b122c8d | ||
|
|
55a39491cb | ||
|
|
1194ee1c2d | ||
|
|
c23b544c34 | ||
|
|
9d76b86f49 | ||
|
|
bb0ccca3e5 | ||
|
|
306817ae9a | ||
|
|
d2ec60e108 | ||
|
|
e016aeddeb | ||
|
|
a4419a31fd | ||
|
|
34e4e907a9 | ||
|
|
2f4a097787 | ||
|
|
f3de00be37 | ||
|
|
4cf61f0d4a | ||
|
|
4e5915f98e | ||
|
|
870eca9e9f | ||
|
|
25ed41caf5 | ||
|
|
4bb72b5606 | ||
|
|
c4d8ea4fec | ||
|
|
8588c9201a | ||
|
|
dd2236c697 | ||
|
|
bc7c4d8cd0 | ||
|
|
aed54f7318 | ||
|
|
86600c6315 | ||
|
|
3f47f37470 | ||
|
|
1324e6163e | ||
|
|
89093167c6 | ||
|
|
15ad92aef2 | ||
|
|
6cdea38284 | ||
|
|
9d455e22fa | ||
|
|
4fc3ff8ce8 | ||
|
|
88e6de9d7e | ||
|
|
e948dbfcc1 | ||
|
|
8aca5851f2 | ||
|
|
18da94bf33 | ||
|
|
1ac2e1c8e3 | ||
|
|
a78b759741 | ||
|
|
b5c3726e67 | ||
|
|
efee3707da | ||
|
|
bbd3453f36 | ||
|
|
0bf42c53cc | ||
|
|
2134bc9139 | ||
|
|
4df8d7e976 | ||
|
|
70708b34cc | ||
|
|
949003ee1b | ||
|
|
db9df1df94 | ||
|
|
4dca25db86 | ||
|
|
d87a440c04 | ||
|
|
55efd62798 | ||
|
|
70a41275c1 | ||
|
|
dd941681ce | ||
|
|
9824d0adaa | ||
|
|
d2291628e0 | ||
|
|
7ab8cad1af | ||
|
|
2c017e0fc5 | ||
|
|
d9453589ac | ||
|
|
e344372616 | ||
|
|
63821e56bc | ||
|
|
1be64223c8 | ||
|
|
a08a8e7f4c | ||
|
|
b31996230d | ||
|
|
1e836501a8 | ||
|
|
d4e59ffc40 | ||
|
|
37ceb77bf6 | ||
|
|
ba3da97ad4 | ||
|
|
984ed8f6cf | ||
|
|
c7895963e4 | ||
|
|
a0ece6ad4d | ||
|
|
d0a3a40a0f | ||
|
|
ff5ee8a05e | ||
|
|
a50bcf3087 | ||
|
|
e0b364d3e2 | ||
|
|
2496cf51c3 | ||
|
|
7b4a01e7fb | ||
|
|
3f9a1d8f2e | ||
|
|
0b927bcc91 | ||
|
|
92397bf7b6 | ||
|
|
d1e2e1db2b | ||
|
|
783ba50c9e | ||
|
|
aca9a0e35b | ||
|
|
fb8d262554 | ||
|
|
bd60cfc2a0 | ||
|
|
06afd221d5 | ||
|
|
0171fb35a4 | ||
|
|
99c47813c3 | ||
|
|
82f5dfd569 | ||
|
|
6d7edcd486 | ||
|
|
9f273dc887 | ||
|
|
ac9cfa5040 | ||
|
|
1b03223537 | ||
|
|
0467b0a3dc | ||
|
|
ba75167238 | ||
|
|
51e7daa26f | ||
|
|
2ff653cc6f | ||
|
|
cfe4d080d5 | ||
|
|
9b28ecde8e | ||
|
|
096ed39d23 | ||
|
|
6ea3adcef8 | ||
|
|
4342be29d7 | ||
|
|
1609c97574 | ||
|
|
f07b3ee9c6 | ||
|
|
2058dbc470 | ||
|
|
6964fb71fc | ||
|
|
a8bb4ee7e5 | ||
|
|
3fcd74ce4e | ||
|
|
2b7ff0efc5 | ||
|
|
5833541a6e | ||
|
|
54c6418f97 | ||
|
|
fc9aac42b4 | ||
|
|
89b43684d8 | ||
|
|
31b26222d3 | ||
|
|
e4df03053e | ||
|
|
833e7eca22 | ||
|
|
b7d85ad2ff | ||
|
|
8793560e12 | ||
|
|
58e0e48d59 | ||
|
|
ad4cbbea6d | ||
|
|
db660ee3b1 | ||
|
|
ae54a872ce | ||
|
|
2aa686f7ad | ||
|
|
ce10bf5e60 | ||
|
|
28ae9c447a | ||
|
|
ff6da9bbec | ||
|
|
198c239399 | ||
|
|
0fbbea963f | ||
|
|
51165c54f5 |
@@ -1,77 +1,96 @@
|
||||
[target.x86_64-unknown-linux-musl]
|
||||
linker = "rust-lld"
|
||||
rustflags = ["-C", "linker-flavor=ld.lld"]
|
||||
# region Native
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
linker = "aarch64-linux-musl-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.'cfg(all(windows, target_env = "msvc"))']
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.mipsel-unknown-linux-musl]
|
||||
linker = "mipsel-linux-muslsf-gcc"
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = [
|
||||
"-C",
|
||||
"target-feature=+crt-static",
|
||||
"-L",
|
||||
"./musl_gcc/mipsel-linux-muslsf-cross/mipsel-linux-muslsf/lib",
|
||||
"-L",
|
||||
"./musl_gcc/mipsel-linux-muslsf-cross/lib/gcc/mipsel-linux-muslsf/11.2.1",
|
||||
"-l",
|
||||
"atomic",
|
||||
"-l",
|
||||
"ctz",
|
||||
"opt-level=z",
|
||||
"--cfg",
|
||||
'getrandom_backend="wasm_js"',
|
||||
]
|
||||
|
||||
[target.mips-unknown-linux-musl]
|
||||
linker = "mips-linux-muslsf-gcc"
|
||||
rustflags = [
|
||||
"-C",
|
||||
"target-feature=+crt-static",
|
||||
"-L",
|
||||
"./musl_gcc/mips-linux-muslsf-cross/mips-linux-muslsf/lib",
|
||||
"-L",
|
||||
"./musl_gcc/mips-linux-muslsf-cross/lib/gcc/mips-linux-muslsf/11.2.1",
|
||||
"-l",
|
||||
"atomic",
|
||||
"-l",
|
||||
"ctz",
|
||||
]
|
||||
# region
|
||||
|
||||
# region CI
|
||||
|
||||
[target.x86_64-unknown-linux-musl]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.riscv64gc-unknown-linux-musl]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.armv7-unknown-linux-musleabihf]
|
||||
linker = "armv7l-linux-musleabihf-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.armv7-unknown-linux-musleabi]
|
||||
linker = "armv7m-linux-musleabi-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.arm-unknown-linux-musleabihf]
|
||||
linker = "arm-linux-musleabihf-gcc"
|
||||
rustflags = [
|
||||
"-C",
|
||||
"target-feature=+crt-static",
|
||||
"-L",
|
||||
"./musl_gcc/arm-linux-musleabihf-cross/arm-linux-musleabihf/lib",
|
||||
"-L",
|
||||
"./musl_gcc/arm-linux-musleabihf-cross/lib/gcc/arm-linux-musleabihf/11.2.1",
|
||||
"-l",
|
||||
"atomic",
|
||||
]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.arm-unknown-linux-musleabi]
|
||||
linker = "arm-linux-musleabi-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.loongarch64-unknown-linux-musl]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.mipsel-unknown-linux-musl]
|
||||
linker = "mipsel-unknown-linux-muslsf-gcc"
|
||||
rustflags = [
|
||||
"-C",
|
||||
"target-feature=+crt-static",
|
||||
"-L",
|
||||
"./musl_gcc/arm-linux-musleabi-cross/arm-linux-musleabi/lib",
|
||||
"./musl_gcc/mipsel-unknown-linux-muslsf/mipsel-unknown-linux-muslsf/lib",
|
||||
"-L",
|
||||
"./musl_gcc/arm-linux-musleabi-cross/lib/gcc/arm-linux-musleabi/11.2.1",
|
||||
"./musl_gcc/mipsel-unknown-linux-muslsf/mipsel-unknown-linux-muslsf/sysroot/usr/lib",
|
||||
"-L",
|
||||
"./musl_gcc/mipsel-unknown-linux-muslsf/lib/gcc/mipsel-unknown-linux-muslsf/15.1.0",
|
||||
"-l",
|
||||
"atomic",
|
||||
"-l",
|
||||
"ctz",
|
||||
"-l",
|
||||
"gcc",
|
||||
]
|
||||
|
||||
[target.mips-unknown-linux-musl]
|
||||
linker = "mips-unknown-linux-muslsf-gcc"
|
||||
rustflags = [
|
||||
"-C",
|
||||
"target-feature=+crt-static",
|
||||
"-L",
|
||||
"./musl_gcc/mips-unknown-linux-muslsf/mips-unknown-linux-muslsf/lib",
|
||||
"-L",
|
||||
"./musl_gcc/mips-unknown-linux-muslsf/mips-unknown-linux-muslsf/sysroot/usr/lib",
|
||||
"-L",
|
||||
"./musl_gcc/mips-unknown-linux-muslsf/lib/gcc/mips-unknown-linux-muslsf/15.1.0",
|
||||
"-l",
|
||||
"atomic",
|
||||
"-l",
|
||||
"ctz",
|
||||
"-l",
|
||||
"gcc",
|
||||
]
|
||||
|
||||
[target.aarch64-unknown-linux-ohos]
|
||||
ar = "/usr/local/ohos-sdk/linux/native/llvm/bin/llvm-ar"
|
||||
linker = "/home/runner/sdk/native/llvm/aarch64-unknown-linux-ohos-clang.sh"
|
||||
|
||||
[target.aarch64-unknown-linux-ohos.env]
|
||||
PKG_CONFIG_PATH = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib/pkgconfig:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib/pkgconfig"
|
||||
PKG_CONFIG_LIBDIR = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib"
|
||||
PKG_CONFIG_SYSROOT_DIR = "/usr/local/ohos-sdk/linux/native/sysroot"
|
||||
SYSROOT = "/usr/local/ohos-sdk/linux/native/sysroot"
|
||||
|
||||
# endregion
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
PROFILE=$(cat .flake-profile 2>/dev/null)
|
||||
use flake .#${PROFILE}
|
||||
@@ -0,0 +1,135 @@
|
||||
# Copyright 2024-present Easytier Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: 🐞 问题报告 / Bug Report
|
||||
title: '[bug] '
|
||||
description: 报告一个问题 / Report a bug
|
||||
labels: ['type: bug', 'status: needs triage']
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 在提交问题之前 / First of all
|
||||
1. 请先搜索有关此问题的 [现有问题](https://github.com/EasyTier/EasyTier/issues?q=is%3Aissue)。
|
||||
1. Please search for [existing issues](https://github.com/EasyTier/EasyTier/issues?q=is%3Aissue) about this problem first.
|
||||
2. 请确保所使用的 Easytier 版本都是最新的。
|
||||
2. Make sure that all Easytier versions are up-to-date.
|
||||
3. 请确保这是 EasyTier 的问题,而不是你正在使用的其他内容引起的问题。
|
||||
3. Make sure it's an issue with EasyTier and not something else you are using.
|
||||
4. 请记得遵守我们的社区准则并保持友好态度。
|
||||
4. Remember to follow our community guidelines and be friendly.
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 问题简要描述 / Brief Description
|
||||
description: 对问题的简要描述,包括期望的行为和实际发生的情况。 / A brief description of the issue, including expected vs actual behavior.
|
||||
placeholder: |
|
||||
例如:节点 A 无法连接到节点 B,期望能够正常建立连接
|
||||
Example: Node A cannot connect to Node B, expected to establish connection normally
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: environment-info
|
||||
attributes:
|
||||
label: 环境信息 / Environment Information
|
||||
description: 请提供网络拓扑、节点信息和系统环境详情。 / Please provide network topology, node information and system environment details.
|
||||
placeholder: |
|
||||
**EasyTier 版本(非常重要)/ EasyTier Version (Very Important):** v1.2.0
|
||||
|
||||
**网络拓扑 / Network Topology:**
|
||||
- 节点 A (10.1.1.1): Windows 11 Pro 22H2, Wifi,有 IPV6 地址
|
||||
- 节点 B (10.1.1.2): Ubuntu 22.04.3 LTS (Linux 5.15.0-72-generic), 公网 IP
|
||||
- 节点 C (10.1.1.3): macOS Ventura 13.4.1, 5G 流量,无 IPV6 地址
|
||||
|
||||
**Network Topology:**
|
||||
- Node A (10.1.1.1): Windows 11 Pro 22H2, Wifi, has IPV6 address
|
||||
- Node B (10.1.1.2): Ubuntu 22.04.3 LTS (Linux 5.15.0-72-generic), public IP
|
||||
- Node C (10.1.1.3): macOS Ventura 13.4.1, 5G traffic, no IPV6 address
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: node-configs
|
||||
attributes:
|
||||
label: 节点配置 / Node Configurations
|
||||
description: 请提供每个节点的配置文件或启动参数。 / Please provide configuration files or startup parameters for each node.
|
||||
placeholder: |
|
||||
**节点 A 配置 / Node A Config:**
|
||||
```
|
||||
easytier-core --config-file config.toml
|
||||
```
|
||||
|
||||
**节点 B 配置 / Node B Config:**
|
||||
```
|
||||
easytier-core --ipv4 10.1.1.2 --peers tcp://1.2.3.4:11010
|
||||
```
|
||||
|
||||
请贴出完整的配置文件内容或命令行参数
|
||||
Please paste complete configuration file contents or command line arguments
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: 日志信息 / Log Information
|
||||
description: 请提供相关的日志信息,包括 GUI 的事件日志或命令行的控制台输出。 / Please provide relevant log information, including GUI event logs or command line console output.
|
||||
placeholder: |
|
||||
请粘贴相关的日志信息:
|
||||
- GUI 用户:请提供事件日志中的错误信息
|
||||
- 命令行用户:请提供控制台输出的详细日志
|
||||
- 一般情况下,提供默认输出的事件日志即可
|
||||
- 如果能提供 --file-log-level debug 输出的日志,会更方便 debug
|
||||
|
||||
Please paste relevant log information:
|
||||
- GUI users: Please provide error messages from event logs
|
||||
- CLI users: Please provide detailed console output logs
|
||||
- Default log output is usually sufficient
|
||||
- If possible, logs with --file-log-level debug would be more helpful for debugging
|
||||
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: 重现步骤 / Reproduction Steps
|
||||
description: 请提供详细的步骤来重现这个问题。 / Please provide detailed steps to reproduce this issue.
|
||||
placeholder: |
|
||||
1. 启动节点 A,使用配置 xxx / Start Node A with config xxx
|
||||
2. 启动节点 B,使用配置 yyy / Start Node B with config yyy
|
||||
3. 尝试从节点 A ping 节点 B / Try to ping Node B from Node A
|
||||
4. 观察到错误:xxx / Observe error: xxx
|
||||
|
||||
请提供详细的操作步骤,以便我们能够重现问题
|
||||
Please provide detailed steps so we can reproduce the issue
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: 预期结果 / Expected Behavior
|
||||
description: 清楚地描述您期望发生的事情。 / A clear description of what you expected to happen.
|
||||
placeholder: |
|
||||
例如:节点 A 应该能够成功 ping 通节点 B,延迟在 100ms 以内
|
||||
Example: Node A should be able to ping Node B successfully with latency under 100ms
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: 额外信息 / Additional Context
|
||||
description: 在这里添加关于问题的任何其他上下文信息。 / Add any other context about the problem here.
|
||||
placeholder: |
|
||||
例如:
|
||||
- 这个问题是否在特定时间出现?
|
||||
- 是否有网络环境的特殊配置?
|
||||
- 是否尝试过其他解决方案?
|
||||
|
||||
Example:
|
||||
- Does this issue occur at specific times?
|
||||
- Are there any special network environment configurations?
|
||||
- Have you tried any other solutions?
|
||||
@@ -0,0 +1,179 @@
|
||||
# Copyright 2024-present Easytier Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: 💡 新功能请求 / Feature Request
|
||||
title: '[feat] '
|
||||
description: 提出一个想法 / Suggest an idea
|
||||
labels: ['type: feature request']
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 提交功能请求前请注意 / Before Submitting
|
||||
1. 请先搜索 [现有的功能请求](https://github.com/EasyTier/EasyTier/issues?q=is%3Aissue+label%3A%22type%3A+feature+request%22) 确保您的想法尚未被提出。
|
||||
1. Please search [existing feature requests](https://github.com/EasyTier/EasyTier/issues?q=is%3Aissue+label%3A%22type%3A+feature+request%22) to ensure your idea hasn't been suggested already.
|
||||
2. 请确保这个功能确实适合 EasyTier 项目的目标和范围。
|
||||
2. Please ensure this feature fits within EasyTier's goals and scope.
|
||||
3. 考虑这个功能是否能让更多用户受益,而不只是解决个人需求。
|
||||
3. Consider whether this feature would benefit many users, not just personal needs.
|
||||
|
||||
- type: dropdown
|
||||
id: feature-category
|
||||
attributes:
|
||||
label: 功能类别 / Feature Category
|
||||
description: 请选择这个功能请求属于哪个类别 / Please select which category this feature request belongs to
|
||||
options:
|
||||
- 网络连接 / Network Connectivity
|
||||
- 安全和加密 / Security & Encryption
|
||||
- 性能优化 / Performance Optimization
|
||||
- 用户界面 / User Interface
|
||||
- 配置管理 / Configuration Management
|
||||
- 监控和日志 / Monitoring & Logging
|
||||
- 平台支持 / Platform Support
|
||||
- API 和集成 / API & Integration
|
||||
- 其他 / Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: use-case
|
||||
attributes:
|
||||
label: 使用场景 / Use Case
|
||||
description: 描述您希望这个功能解决的具体使用场景或问题 / Describe the specific use case or problem you want this feature to solve
|
||||
placeholder: |
|
||||
例如:
|
||||
- 作为企业用户,我需要在多个分支机构之间建立安全的网络连接
|
||||
- 作为开发者,我希望能够通过 API 监控网络状态
|
||||
- 作为系统管理员,我需要更详细的连接日志来排查问题
|
||||
|
||||
Example:
|
||||
- As an enterprise user, I need to establish secure network connections between multiple branch offices
|
||||
- As a developer, I want to monitor network status through APIs
|
||||
- As a system administrator, I need more detailed connection logs for troubleshooting
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: current-limitations
|
||||
attributes:
|
||||
label: 当前限制 / Current Limitations
|
||||
description: 描述当前 EasyTier 的哪些限制阻止了您实现这个使用场景 / Describe what current limitations in EasyTier prevent you from achieving this use case
|
||||
placeholder: |
|
||||
例如:
|
||||
- 目前不支持基于用户角色的访问控制
|
||||
- 缺少对 IPv6 的完整支持
|
||||
- 没有提供 REST API 来获取网络状态
|
||||
|
||||
Example:
|
||||
- Currently lacks role-based access control
|
||||
- Missing complete IPv6 support
|
||||
- No REST API available for network status
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposed-solution
|
||||
attributes:
|
||||
label: 建议的解决方案 / Proposed Solution
|
||||
description: 详细描述您希望添加的功能以及它应该如何工作 / Describe in detail the feature you'd like to add and how it should work
|
||||
placeholder: |
|
||||
请描述:
|
||||
- 功能的具体实现方式
|
||||
- 用户界面或 API 设计
|
||||
- 配置选项和参数
|
||||
- 与现有功能的集成方式
|
||||
|
||||
Please describe:
|
||||
- Specific implementation approach
|
||||
- User interface or API design
|
||||
- Configuration options and parameters
|
||||
- Integration with existing features
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: benefits
|
||||
attributes:
|
||||
label: 预期收益 / Expected Benefits
|
||||
description: 说明这个功能会带来什么好处,会影响哪些用户群体 / Explain what benefits this feature would bring and which user groups it would affect
|
||||
placeholder: |
|
||||
例如:
|
||||
- 提高网络连接的稳定性和性能
|
||||
- 简化大规模部署的管理复杂度
|
||||
- 增强企业用户的安全性需求
|
||||
- 降低新用户的学习成本
|
||||
|
||||
Example:
|
||||
- Improve network connection stability and performance
|
||||
- Simplify management complexity for large-scale deployments
|
||||
- Enhance security requirements for enterprise users
|
||||
- Reduce learning curve for new users
|
||||
|
||||
- type: textarea
|
||||
id: technical-considerations
|
||||
attributes:
|
||||
label: 技术考虑 / Technical Considerations
|
||||
description: 如果您了解技术细节,请分享相关的技术考虑或约束 / If you have technical knowledge, please share relevant technical considerations or constraints
|
||||
placeholder: |
|
||||
例如:
|
||||
- 可能需要修改网络协议栈
|
||||
- 需要考虑跨平台兼容性
|
||||
- 可能影响现有性能
|
||||
- 依赖第三方库或协议
|
||||
|
||||
Example:
|
||||
- May require modifications to network protocol stack
|
||||
- Cross-platform compatibility needs consideration
|
||||
- Potential impact on existing performance
|
||||
- Dependencies on third-party libraries or protocols
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: 备选方案 / Alternative Solutions
|
||||
description: 您是否考虑过其他解决方案?是否有现有的替代方案? / Have you considered other solutions? Are there existing alternatives?
|
||||
placeholder: |
|
||||
例如:
|
||||
- 使用第三方工具 X 可以部分解决,但缺少 Y 功能
|
||||
- 通过脚本workaround可以实现,但不够优雅
|
||||
- 其他类似项目 Z 有这个功能,可以参考其实现
|
||||
|
||||
Example:
|
||||
- Third-party tool X can partially solve this, but lacks Y functionality
|
||||
- Can be achieved through script workarounds, but not elegant
|
||||
- Similar project Z has this feature, could reference its implementation
|
||||
|
||||
- type: textarea
|
||||
id: implementation-priority
|
||||
attributes:
|
||||
label: 实现优先级 / Implementation Priority
|
||||
description: 这个功能对您有多重要?是否有时间要求? / How important is this feature to you? Any time requirements?
|
||||
placeholder: |
|
||||
例如:
|
||||
- 高优先级:阻碍了我们的生产部署
|
||||
- 中优先级:会显著改善用户体验
|
||||
- 低优先级:锦上添花的功能
|
||||
|
||||
Example:
|
||||
- High priority: Blocking our production deployment
|
||||
- Medium priority: Would significantly improve user experience
|
||||
- Low priority: Nice-to-have feature
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: 补充信息 / Additional Context
|
||||
description: 添加任何其他相关信息,如截图、链接、参考资料等 / Add any other relevant information such as screenshots, links, or references
|
||||
placeholder: |
|
||||
例如:
|
||||
- 相关的 RFC 或技术规范
|
||||
- 其他项目的实现示例
|
||||
- 用户调研或反馈数据
|
||||
- 设计草图或流程图
|
||||
|
||||
Example:
|
||||
- Relevant RFCs or technical specifications
|
||||
- Implementation examples from other projects
|
||||
- User research or feedback data
|
||||
- Design sketches or flowcharts
|
||||
@@ -0,0 +1,98 @@
|
||||
name: prepare-build
|
||||
author: Luna
|
||||
description: Prepare build environment
|
||||
inputs:
|
||||
target:
|
||||
description: 'The target to build for'
|
||||
required: false
|
||||
pnpm:
|
||||
description: 'Whether to run pnpm build'
|
||||
required: true
|
||||
default: 'true'
|
||||
pnpm-build-filter:
|
||||
description: 'The filter argument for pnpm build (e.g. ./easytier-web/*)'
|
||||
required: false
|
||||
default: './easytier-web/*'
|
||||
gui:
|
||||
description: 'Whether to prepare the GUI build environment'
|
||||
required: true
|
||||
default: 'true'
|
||||
token:
|
||||
description: 'GitHub token, used by setup-protoc action'
|
||||
required: false
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- run: mkdir -p easytier-gui/dist
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -qqy build-essential mold musl-tools
|
||||
shell: bash
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
version: '35.1'
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ inputs.token }}
|
||||
|
||||
- name: Verify protoc version
|
||||
run: |
|
||||
version="$(protoc --version | tr -d '\r')"
|
||||
test "$version" = "libprotoc 35.1"
|
||||
shell: bash
|
||||
|
||||
- name: Setup Frontend Environment
|
||||
if: ${{ inputs.pnpm == 'true' }}
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
with:
|
||||
token: ${{ inputs.token }}
|
||||
build-filter: ${{ inputs.pnpm-build-filter }}
|
||||
|
||||
- name: Install GUI dependencies (Linux)
|
||||
if: ${{ inputs.gui == 'true' && runner.os == 'Linux' }}
|
||||
run: |
|
||||
sudo apt-get install -qq xdg-utils \
|
||||
libappindicator3-dev \
|
||||
libgtk-3-dev \
|
||||
librsvg2-dev \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libxdo-dev
|
||||
shell: bash
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: 1.95
|
||||
target: ${{ !contains(inputs.target, 'mips') && inputs.target || '' }}
|
||||
components: ${{ contains(inputs.target, 'mips') && 'rust-src' || '' }}
|
||||
cache: false
|
||||
rustflags: ''
|
||||
|
||||
- name: Install Rust (MIPS)
|
||||
if: ${{ contains(inputs.target, 'mips') }}
|
||||
run: |
|
||||
MUSL_TARGET=${{ inputs.target }}sf
|
||||
mkdir -p ./musl_gcc
|
||||
wget --inet4-only -c https://github.com/cross-tools/musl-cross/releases/download/20250520/${MUSL_TARGET}.tar.xz -P ./musl_gcc/
|
||||
tar xf ./musl_gcc/${MUSL_TARGET}.tar.xz -C ./musl_gcc/
|
||||
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/bin/*gcc /usr/bin/
|
||||
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/include/ /usr/include/musl-cross
|
||||
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/${MUSL_TARGET}/sysroot/ ./musl_gcc/sysroot
|
||||
sudo chmod -R a+rwx ./musl_gcc
|
||||
|
||||
if [[ -d "./musl_gcc/sysroot" ]]; then
|
||||
echo "BINDGEN_EXTRA_CLANG_ARGS=--sysroot=$(readlink -f ./musl_gcc/sysroot)" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
cd "$PWD/musl_gcc/${MUSL_TARGET}/lib/gcc/${MUSL_TARGET}/15.1.0" || exit 255
|
||||
# for panic-abort
|
||||
cp libgcc_eh.a libunwind.a
|
||||
|
||||
# for mimalloc
|
||||
ar x libgcc.a _ctzsi2.o _clz.o _bswapsi2.o
|
||||
ar rcs libctz.a _ctzsi2.o _clz.o _bswapsi2.o
|
||||
shell: bash
|
||||
@@ -0,0 +1,75 @@
|
||||
name: 'Setup pnpm'
|
||||
author: Luna
|
||||
description: 'Setup Node.js, pnpm, and install dependencies'
|
||||
|
||||
inputs:
|
||||
token:
|
||||
description: 'GitHub token, used by setup-protoc action'
|
||||
required: false
|
||||
build-filter:
|
||||
description: 'The filter argument for pnpm build (e.g. ./easytier-web/*)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: 1.95
|
||||
target: wasm32-unknown-unknown
|
||||
cache: false
|
||||
rustflags: ''
|
||||
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: wasm-pack
|
||||
|
||||
- uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
version: '35.1'
|
||||
repo-token: ${{ inputs.token }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install and build
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm -r install
|
||||
if [ -n "${{ inputs.build-filter }}" ]; then
|
||||
echo "Building with filter: ${{ inputs.build-filter }}"
|
||||
pnpm -r --workspace-concurrency=1 --filter "${{ inputs.build-filter }}" build
|
||||
else
|
||||
echo "No build filter provided, building all packages"
|
||||
pnpm -r --workspace-concurrency=1 build
|
||||
fi
|
||||
|
||||
- name: Bundle config generator with web frontend
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f easytier-web/frontend/dist/index.html ] && [ -f easytier-web/config-generator/dist/index.html ]; then
|
||||
mkdir -p easytier-web/frontend/dist/config-generator
|
||||
cp -R easytier-web/config-generator/dist/. easytier-web/frontend/dist/config-generator/
|
||||
fi
|
||||
@@ -1,26 +1,36 @@
|
||||
FROM alpine:latest AS builder
|
||||
FROM alpine:latest AS base
|
||||
FROM base AS builder
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY . /tmp/artifacts
|
||||
RUN mkdir -p /tmp/output; \
|
||||
cd /tmp/artifacts; \
|
||||
ARTIFACT_ARCH=""; \
|
||||
WORKDIR /tmp/output
|
||||
RUN ARTIFACT_ARCH=""; \
|
||||
if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||
ARTIFACT_ARCH="x86_64"; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then \
|
||||
ARTIFACT_ARCH="armhf"; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
||||
ARTIFACT_ARCH="armv7hf"; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||
ARTIFACT_ARCH="aarch64"; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/riscv64" ]; then \
|
||||
ARTIFACT_ARCH="riscv64"; \
|
||||
else \
|
||||
echo "Unsupported architecture: $TARGETARCH"; \
|
||||
echo "Unsupported architecture: $TARGETPLATFORM"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
cp /tmp/artifacts/easytier-linux-${ARTIFACT_ARCH}/* /tmp/output;
|
||||
|
||||
FROM alpine:latest
|
||||
FROM base
|
||||
|
||||
RUN apk add --no-cache tzdata tini
|
||||
WORKDIR /app
|
||||
COPY --from=builder --chmod=755 /tmp/output/* /usr/local/bin
|
||||
|
||||
# users can use "-e TZ=xxx" to adjust it
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# tcp
|
||||
EXPOSE 11010/tcp
|
||||
# udp
|
||||
@@ -32,4 +42,7 @@ EXPOSE 11011/tcp
|
||||
# wss
|
||||
EXPOSE 11012/tcp
|
||||
|
||||
ENTRYPOINT ["easytier-core"]
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=5 \
|
||||
CMD ["/usr/local/bin/easytier-cli", "--rpc-portal", "127.0.0.1:15888", "--output", "json", "node", "info"]
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--", "easytier-core"]
|
||||
|
||||
@@ -2,9 +2,14 @@ name: EasyTier Core
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop", "main"]
|
||||
branches: [ "develop", "main", "releases/**" ]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
branches: [ "develop", "main" ]
|
||||
types: [ opened, synchronize, reopened, ready_for_review ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -18,47 +23,83 @@ jobs:
|
||||
pre_job:
|
||||
# continue-on-error: true # Uncomment once integration is finished
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
# do not skip push on branch starts with releases/
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: 'never'
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh"]'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-core/**", "easytier-proto/**", ".github/workflows/core.yml", ".github/actions/**", "easytier-web/**"]'
|
||||
build_web:
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Frontend Environment
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
build-filter: './easytier-web/*'
|
||||
|
||||
- name: Archive artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: easytier-web-dashboard
|
||||
path: |
|
||||
easytier-web/frontend/dist/*
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- TARGET: aarch64-unknown-linux-musl
|
||||
OS: ubuntu-latest
|
||||
ARTIFACT_NAME: linux-aarch64
|
||||
- TARGET: x86_64-unknown-linux-musl
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-x86_64
|
||||
- TARGET: mips-unknown-linux-musl
|
||||
OS: ubuntu-latest
|
||||
ARTIFACT_NAME: linux-mips
|
||||
- TARGET: mipsel-unknown-linux-musl
|
||||
OS: ubuntu-latest
|
||||
ARTIFACT_NAME: linux-mipsel
|
||||
- TARGET: aarch64-unknown-linux-musl
|
||||
OS: ubuntu-24.04-arm
|
||||
ARTIFACT_NAME: linux-aarch64
|
||||
|
||||
- TARGET: riscv64gc-unknown-linux-musl
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-riscv64
|
||||
- TARGET: loongarch64-unknown-linux-musl
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-loongarch64
|
||||
|
||||
- TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-armv7hf
|
||||
- TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-armv7
|
||||
- TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-armhf
|
||||
- TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-arm
|
||||
|
||||
- TARGET: mips-unknown-linux-musl
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-mips
|
||||
- TARGET: mipsel-unknown-linux-musl
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-mipsel
|
||||
|
||||
- TARGET: x86_64-unknown-freebsd
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: freebsd-13.2-x86_64
|
||||
BSD_VERSION: 13.2
|
||||
|
||||
- TARGET: x86_64-apple-darwin
|
||||
OS: macos-latest
|
||||
ARTIFACT_NAME: macos-x86_64
|
||||
@@ -69,6 +110,12 @@ jobs:
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
OS: windows-latest
|
||||
ARTIFACT_NAME: windows-x86_64
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
OS: windows-latest
|
||||
ARTIFACT_NAME: windows-i686
|
||||
- TARGET: aarch64-pc-windows-msvc
|
||||
OS: windows-11-arm
|
||||
ARTIFACT_NAME: windows-arm64
|
||||
|
||||
runs-on: ${{ matrix.OS }}
|
||||
env:
|
||||
@@ -76,99 +123,180 @@ jobs:
|
||||
TARGET: ${{ matrix.TARGET }}
|
||||
OS: ${{ matrix.OS }}
|
||||
OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||
needs: pre_job
|
||||
needs:
|
||||
- pre_job
|
||||
- build_web
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
|
||||
- name: Cargo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
./target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Install rust target
|
||||
run: bash ./.github/workflows/install_rust.sh
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v2
|
||||
with:
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Core & Cli
|
||||
- name: Set current ref as env variable
|
||||
run: |
|
||||
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips
|
||||
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
|
||||
|
||||
- name: Download web artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: easytier-web-dashboard
|
||||
path: easytier-web/frontend/dist/
|
||||
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
target: ${{ matrix.TARGET }}
|
||||
gui: true
|
||||
pnpm: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
# The prefix cache key, this can be changed to start a new cache manually.
|
||||
# default: "v0-rust"
|
||||
prefix-key: ""
|
||||
shared-key: "core-registry"
|
||||
cache-targets: "false"
|
||||
|
||||
- uses: mlugg/setup-zig@v2
|
||||
if: ${{ contains(matrix.OS, 'ubuntu') }}
|
||||
with:
|
||||
version: 0.16.0
|
||||
use-cache: true
|
||||
|
||||
- uses: taiki-e/install-action@v2
|
||||
if: ${{ contains(matrix.OS, 'ubuntu') }}
|
||||
with:
|
||||
tool: cargo-zigbuild
|
||||
|
||||
- name: Build
|
||||
if: ${{ !contains(matrix.TARGET, 'mips') }}
|
||||
run: |
|
||||
if [[ "$TARGET" == *windows* ]]; then
|
||||
SUFFIX=.exe
|
||||
else
|
||||
cargo build --release --verbose --target $TARGET
|
||||
SUFFIX=""
|
||||
fi
|
||||
|
||||
- name: Install UPX
|
||||
if: ${{ matrix.OS != 'macos-latest' }}
|
||||
uses: crazy-max/ghaction-upx@v3
|
||||
with:
|
||||
version: latest
|
||||
install-only: true
|
||||
if [[ "$TARGET" =~ (x86_64-unknown-linux-musl|aarch64-unknown-linux-musl|windows|darwin) ]]; then
|
||||
BUILD=build
|
||||
else
|
||||
BUILD=zigbuild
|
||||
fi
|
||||
|
||||
if [[ "$TARGET" =~ ^(riscv64|loongarch64|aarch64).*$ || "$TARGET" =~ (freebsd|windows) ]]; then
|
||||
FEATURES="mimalloc"
|
||||
else
|
||||
FEATURES="jemalloc"
|
||||
fi
|
||||
|
||||
cargo $BUILD --release --target $TARGET --package=easytier-web --features=embed
|
||||
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./target/$TARGET/release/easytier-web-embed"$SUFFIX"
|
||||
|
||||
cargo $BUILD --release --target $TARGET --features=$FEATURES
|
||||
|
||||
- name: Build (MIPS)
|
||||
if: ${{ contains(matrix.TARGET, 'mips') }}
|
||||
env:
|
||||
RUSTC_BOOTSTRAP: 1
|
||||
run: |
|
||||
cargo build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc
|
||||
|
||||
- name: Compress
|
||||
run: |
|
||||
mkdir -p ./artifacts/objects/
|
||||
|
||||
# windows is the only OS using a different convention for executable file name
|
||||
if [[ $OS =~ ^windows.*$ ]]; then
|
||||
SUFFIX=.exe
|
||||
cp easytier/third_party/Packet.dll ./artifacts/objects/
|
||||
cp easytier/third_party/wintun.dll ./artifacts/objects/
|
||||
case $TARGET in
|
||||
x86_64*) ARCH_DIR=x86_64 ;;
|
||||
i686*) ARCH_DIR=i686 ;;
|
||||
aarch64*) ARCH_DIR=arm64 ;;
|
||||
esac
|
||||
if [[ -n "$ARCH_DIR" ]]; then
|
||||
find "easytier/third_party/${ARCH_DIR}" -maxdepth 1 -type f \( -name "*.dll" -o -name "*.sys" \) -exec cp {} ./artifacts/objects/ \;
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
||||
TAG=$GITHUB_REF_NAME
|
||||
else
|
||||
TAG=$GITHUB_SHA
|
||||
fi
|
||||
|
||||
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ (loongarch|freebsd) ]]; then
|
||||
HOST_ARCH=$(uname -m)
|
||||
case $HOST_ARCH in
|
||||
x86_64) UPX_ARCH="amd64" ;;
|
||||
aarch64) UPX_ARCH="arm64" ;;
|
||||
*) UPX_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
if [[ $OS =~ ^ubuntu.*$ ]]; then
|
||||
upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX"
|
||||
upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX"
|
||||
UPX_VERSION=4.2.4
|
||||
UPX_PKG="upx-${UPX_VERSION}-${UPX_ARCH}_linux"
|
||||
curl -L "https://github.com/upx/upx/releases/download/v${UPX_VERSION}/${UPX_PKG}.tar.xz" -s | tar xJvf -
|
||||
cp "${UPX_PKG}/upx" .
|
||||
UPX_BIN=./upx
|
||||
fi
|
||||
|
||||
mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
|
||||
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
|
||||
for BIN in ./target/$TARGET/release/easytier-{core,cli,web,web-embed}"$SUFFIX"; do
|
||||
if [[ -f "$BIN" ]]; then
|
||||
if [[ -n "$UPX_BIN" ]]; then
|
||||
$UPX_BIN --lzma --best "$BIN" || true
|
||||
fi
|
||||
|
||||
mv "$BIN" ./artifacts/objects/
|
||||
fi
|
||||
done
|
||||
|
||||
mv ./artifacts/objects/* ./artifacts/
|
||||
rm -rf ./artifacts/objects/
|
||||
|
||||
- name: Archive artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: easytier-${{ matrix.ARTIFACT_NAME }}
|
||||
path: |
|
||||
./artifacts/*
|
||||
|
||||
- name: Upload OSS
|
||||
if: ${{ env.OSS_BUCKET != '' }}
|
||||
uses: Menci/upload-to-oss@main
|
||||
with:
|
||||
access-key-id: ${{ secrets.ALIYUN_OSS_ACCESS_ID }}
|
||||
access-key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY }}
|
||||
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
|
||||
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||
local-path: ./artifacts/
|
||||
remote-path: /easytier-releases/${{ github.sha }}/
|
||||
no-delete-remote-files: true
|
||||
retry: 5
|
||||
core-result:
|
||||
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||
build_magisk:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre_job
|
||||
- build
|
||||
needs: [ pre_job, build_web, build ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v5 # 必须先检出代码才能获取模块配置
|
||||
|
||||
# 下载二进制文件到独立目录
|
||||
- name: Download Linux aarch64 binaries
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: easytier-linux-aarch64
|
||||
path: ./downloaded-binaries/ # 独立目录避免冲突
|
||||
|
||||
# 将二进制文件复制到 Magisk 模块目录
|
||||
- name: Prepare binaries
|
||||
run: |
|
||||
mkdir -p ./easytier-contrib/easytier-magisk/
|
||||
cp ./downloaded-binaries/easytier-core ./easytier-contrib/easytier-magisk/
|
||||
cp ./downloaded-binaries/easytier-cli ./easytier-contrib/easytier-magisk/
|
||||
cp ./downloaded-binaries/easytier-web ./easytier-contrib/easytier-magisk/
|
||||
|
||||
# 上传生成的模块
|
||||
- name: Upload Magisk Module
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Easytier-Magisk
|
||||
path: |
|
||||
./easytier-contrib/easytier-magisk
|
||||
!./easytier-contrib/easytier-magisk/build.sh
|
||||
!./easytier-contrib/easytier-magisk/magisk_update.json
|
||||
if-no-files-found: error
|
||||
|
||||
core-result:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre_job, build_web, build, build_magisk ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: needs.build.result != 'success'
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
@@ -11,13 +11,18 @@ on:
|
||||
image_tag:
|
||||
description: 'Tag for this image build'
|
||||
type: string
|
||||
default: 'v1.2.0'
|
||||
default: 'v2.6.4'
|
||||
required: true
|
||||
mark_latest:
|
||||
description: 'Mark this image as latest'
|
||||
type: boolean
|
||||
default: false
|
||||
required: true
|
||||
mark_unstable:
|
||||
description: 'Mark this image as unstable'
|
||||
type: boolean
|
||||
default: false
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
@@ -26,7 +31,14 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
-
|
||||
name: Validate inputs
|
||||
run: |
|
||||
if [[ "${{ inputs.mark_latest }}" == "true" && "${{ inputs.mark_unstable }}" == "true" ]]; then
|
||||
echo "Error: mark_latest and mark_unstable cannot both be true"
|
||||
exit 1
|
||||
fi
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -39,9 +51,15 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: login github container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Download artifact
|
||||
id: download-artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.run_id }}
|
||||
@@ -50,12 +68,36 @@ jobs:
|
||||
- name: List files
|
||||
run: |
|
||||
ls -l -R .
|
||||
- name: Prepare Docker tags
|
||||
id: tags
|
||||
run: |
|
||||
# Base tags with version
|
||||
DOCKERHUB_TAGS="easytier/easytier:${{ inputs.image_tag }}"
|
||||
GHCR_TAGS="ghcr.io/easytier/easytier:${{ inputs.image_tag }}"
|
||||
|
||||
# Add latest tags if requested
|
||||
if [[ "${{ inputs.mark_latest }}" == "true" ]]; then
|
||||
DOCKERHUB_TAGS="${DOCKERHUB_TAGS},easytier/easytier:latest"
|
||||
GHCR_TAGS="${GHCR_TAGS},ghcr.io/easytier/easytier:latest"
|
||||
fi
|
||||
|
||||
# Add unstable tags if requested
|
||||
if [[ "${{ inputs.mark_unstable }}" == "true" ]]; then
|
||||
DOCKERHUB_TAGS="${DOCKERHUB_TAGS},easytier/easytier:unstable"
|
||||
GHCR_TAGS="${GHCR_TAGS},ghcr.io/easytier/easytier:unstable"
|
||||
fi
|
||||
|
||||
# Combine all tags
|
||||
ALL_TAGS="${DOCKERHUB_TAGS},${GHCR_TAGS}"
|
||||
|
||||
echo "tags=${ALL_TAGS}" >> $GITHUB_OUTPUT
|
||||
echo "Generated tags: ${ALL_TAGS}"
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./docker_context
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
file: .github/workflows/Dockerfile
|
||||
tags: easytier/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }},
|
||||
tags: ${{ steps.tags.outputs.tags }}
|
||||
|
||||
@@ -2,10 +2,15 @@ name: EasyTier GUI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop", "main"]
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -18,30 +23,32 @@ jobs:
|
||||
pre_job:
|
||||
# continue-on-error: true # Uncomment once integration is finished
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: 'never'
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/workflows/install_rust.sh"]'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-core/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/actions/**", "easytier-web/frontend-lib/**"]'
|
||||
build-gui:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- TARGET: aarch64-unknown-linux-musl
|
||||
OS: ubuntu-latest
|
||||
GUI_TARGET: aarch64-unknown-linux-gnu
|
||||
ARTIFACT_NAME: linux-aarch64
|
||||
- TARGET: x86_64-unknown-linux-musl
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-24.04
|
||||
GUI_TARGET: x86_64-unknown-linux-gnu
|
||||
ARTIFACT_NAME: linux-x86_64
|
||||
- TARGET: aarch64-unknown-linux-musl
|
||||
OS: ubuntu-24.04-arm
|
||||
GUI_TARGET: aarch64-unknown-linux-gnu
|
||||
ARTIFACT_NAME: linux-aarch64
|
||||
|
||||
- TARGET: x86_64-apple-darwin
|
||||
OS: macos-latest
|
||||
@@ -56,6 +63,14 @@ jobs:
|
||||
OS: windows-latest
|
||||
GUI_TARGET: x86_64-pc-windows-msvc
|
||||
ARTIFACT_NAME: windows-x86_64
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
OS: windows-latest
|
||||
GUI_TARGET: i686-pc-windows-msvc
|
||||
ARTIFACT_NAME: windows-i686
|
||||
- TARGET: aarch64-pc-windows-msvc
|
||||
OS: windows-11-arm
|
||||
GUI_TARGET: aarch64-pc-windows-msvc
|
||||
ARTIFACT_NAME: windows-arm64
|
||||
|
||||
runs-on: ${{ matrix.OS }}
|
||||
env:
|
||||
@@ -67,96 +82,128 @@ jobs:
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
- name: Set current ref as env variable
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
target: ${{ matrix.TARGET }}
|
||||
gui: true
|
||||
pnpm: true
|
||||
pnpm-build-filter: ''
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install frontend dependencies
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
# The prefix cache key, this can be changed to start a new cache manually.
|
||||
# default: "v0-rust"
|
||||
prefix-key: ""
|
||||
shared-key: "gui-registry"
|
||||
cache-targets: "false"
|
||||
|
||||
- name: copy correct DLLs
|
||||
if: ${{ contains(matrix.GUI_TARGET, 'windows') }}
|
||||
run: |
|
||||
(cd easytier-gui; pnpm install)
|
||||
(cd tauri-plugin-vpnservice; pnpm install; pnpm build)
|
||||
case $TARGET in
|
||||
x86_64*) ARCH_DIR=x86_64 ;;
|
||||
i686*) ARCH_DIR=i686 ;;
|
||||
aarch64*) ARCH_DIR=arm64 ;;
|
||||
esac
|
||||
if [[ -n "$ARCH_DIR" ]]; then
|
||||
find "./easytier/third_party/${ARCH_DIR}" -maxdepth 1 -type f \( -name "*.dll" -o -name "*.sys" \) -exec cp {} ./easytier-gui/src-tauri/ \;
|
||||
fi
|
||||
|
||||
- name: Cargo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
./target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Install rust target
|
||||
run: bash ./.github/workflows/install_rust.sh
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v2
|
||||
with:
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install GUI cross compile (aarch64 only)
|
||||
if: ${{ matrix.TARGET == 'aarch64-unknown-linux-musl' }}
|
||||
- name: Validate macOS signing secrets
|
||||
if: ${{ contains(matrix.GUI_TARGET, 'darwin') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
# see https://tauri.app/v1/guides/building/linux/
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted" | sudo tee /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
|
||||
sudo dpkg --add-architecture arm64
|
||||
sudo apt-get update && sudo apt-get upgrade -y
|
||||
sudo apt install gcc-aarch64-linux-gnu
|
||||
sudo apt install libwebkit2gtk-4.1-dev:arm64
|
||||
sudo apt install libssl-dev:arm64
|
||||
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
|
||||
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV"
|
||||
missing=()
|
||||
for name in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do
|
||||
if [[ -z "${!name}" ]]; then
|
||||
missing+=("$name")
|
||||
fi
|
||||
done
|
||||
if (( ${#missing[@]} )); then
|
||||
printf 'Missing macOS signing secret(s): %s\n' "${missing[*]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build GUI
|
||||
if: ${{ matrix.GUI_TARGET != '' }}
|
||||
if: ${{ matrix.GUI_TARGET != '' && (!contains(matrix.GUI_TARGET, 'darwin') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) }}
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
with:
|
||||
projectPath: ./easytier-gui
|
||||
# https://tauri.app/v1/guides/building/linux/#cross-compiling-tauri-applications-for-arm-based-devices
|
||||
args: --verbose --target ${{ matrix.GUI_TARGET }} ${{ matrix.OS == 'ubuntu-latest' && contains(matrix.TARGET, 'aarch64') && '--bundles deb' || '' }}
|
||||
args: --verbose --target ${{ matrix.GUI_TARGET }}
|
||||
|
||||
- name: Compress
|
||||
- name: Build GUI (signed and notarized)
|
||||
if: ${{ contains(matrix.GUI_TARGET, 'darwin') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
timeout-minutes: 60
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
with:
|
||||
projectPath: ./easytier-gui
|
||||
args: --verbose --target ${{ matrix.GUI_TARGET }}
|
||||
|
||||
- name: Notarize and staple macOS DMG
|
||||
if: ${{ contains(matrix.GUI_TARGET, 'darwin') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
dmg_dir="./target/$GUI_TARGET/release/bundle/dmg"
|
||||
if [[ ! -d "$dmg_dir" ]]; then
|
||||
printf 'macOS DMG directory not found: %s\n' "$dmg_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dmgs=()
|
||||
while IFS= read -r dmg; do
|
||||
dmgs+=("$dmg")
|
||||
done < <(find "$dmg_dir" -maxdepth 1 -type f -name "*.dmg" | sort)
|
||||
if (( ${#dmgs[@]} == 0 )); then
|
||||
printf 'No macOS DMG found in %s\n' "$dmg_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for dmg in "${dmgs[@]}"; do
|
||||
printf 'Verifying signed DMG: %s\n' "$dmg"
|
||||
codesign --verify --verbose=4 "$dmg"
|
||||
codesign -dv --verbose=4 "$dmg"
|
||||
|
||||
printf 'Notarizing DMG: %s\n' "$dmg"
|
||||
xcrun notarytool submit "$dmg" \
|
||||
--apple-id "$APPLE_ID" \
|
||||
--password "$APPLE_PASSWORD" \
|
||||
--team-id "$APPLE_TEAM_ID" \
|
||||
--wait \
|
||||
--timeout 40m
|
||||
|
||||
printf 'Stapling DMG: %s\n' "$dmg"
|
||||
xcrun stapler staple "$dmg"
|
||||
xcrun stapler validate "$dmg"
|
||||
done
|
||||
|
||||
- name: Collect artifact
|
||||
run: |
|
||||
mkdir -p ./artifacts/objects/
|
||||
|
||||
@@ -165,48 +212,33 @@ jobs:
|
||||
else
|
||||
TAG=$GITHUB_SHA
|
||||
fi
|
||||
|
||||
# copy gui bundle, gui is built without specific target
|
||||
if [[ $OS =~ ^windows.*$ ]]; then
|
||||
if [[ $GUI_TARGET =~ windows ]]; then
|
||||
mv ./target/$GUI_TARGET/release/bundle/nsis/*.exe ./artifacts/objects/
|
||||
elif [[ $OS =~ ^macos.*$ ]]; then
|
||||
elif [[ $GUI_TARGET =~ darwin ]]; then
|
||||
mv ./target/$GUI_TARGET/release/bundle/dmg/*.dmg ./artifacts/objects/
|
||||
elif [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^mips.*$ ]]; then
|
||||
elif [[ $GUI_TARGET =~ linux ]]; then
|
||||
mv ./target/$GUI_TARGET/release/bundle/deb/*.deb ./artifacts/objects/
|
||||
if [[ $GUI_TARGET =~ ^x86_64.*$ ]]; then
|
||||
# currently only x86 appimage is supported
|
||||
mv ./target/$GUI_TARGET/release/bundle/appimage/*.AppImage ./artifacts/objects/
|
||||
fi
|
||||
mv ./target/$GUI_TARGET/release/bundle/rpm/*.rpm ./artifacts/objects/
|
||||
mv ./target/$GUI_TARGET/release/bundle/appimage/*.AppImage ./artifacts/objects/
|
||||
fi
|
||||
|
||||
mv ./artifacts/objects/* ./artifacts/
|
||||
rm -rf ./artifacts/objects/
|
||||
|
||||
- name: Archive artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: easytier-gui-${{ matrix.ARTIFACT_NAME }}
|
||||
path: |
|
||||
./artifacts/*
|
||||
|
||||
- name: Upload OSS
|
||||
if: ${{ env.OSS_BUCKET != '' }}
|
||||
uses: Menci/upload-to-oss@main
|
||||
with:
|
||||
access-key-id: ${{ secrets.ALIYUN_OSS_ACCESS_ID }}
|
||||
access-key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY }}
|
||||
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
|
||||
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||
local-path: ./artifacts/
|
||||
remote-path: /easytier-releases/${{ github.sha }}/gui
|
||||
no-delete-remote-files: true
|
||||
retry: 5
|
||||
gui-result:
|
||||
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre_job
|
||||
- build-gui
|
||||
needs: [ pre_job, build-gui ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: needs.build-gui.result != 'success'
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# env needed:
|
||||
# - TARGET
|
||||
# - GUI_TARGET
|
||||
# - OS
|
||||
|
||||
# dependencies are only needed on ubuntu as that's the only place where
|
||||
# we make cross-compilation
|
||||
if [[ $OS =~ ^ubuntu.*$ ]]; then
|
||||
sudo apt-get update && sudo apt-get install -qq crossbuild-essential-arm64 crossbuild-essential-armhf musl-tools libappindicator3-dev
|
||||
# for easytier-gui
|
||||
if [[ $GUI_TARGET != '' && $GUI_TARGET =~ ^x86_64.*$ ]]; then
|
||||
sudo apt install -qq libwebkit2gtk-4.1-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libgtk-3-dev \
|
||||
librsvg2-dev \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
patchelf
|
||||
fi
|
||||
# curl -s musl.cc | grep mipsel
|
||||
case $TARGET in
|
||||
mipsel-unknown-linux-musl)
|
||||
MUSL_URI=mipsel-linux-muslsf
|
||||
;;
|
||||
mips-unknown-linux-musl)
|
||||
MUSL_URI=mips-linux-muslsf
|
||||
;;
|
||||
aarch64-unknown-linux-musl)
|
||||
MUSL_URI=aarch64-linux-musl
|
||||
;;
|
||||
armv7-unknown-linux-musleabihf)
|
||||
MUSL_URI=armv7l-linux-musleabihf
|
||||
;;
|
||||
armv7-unknown-linux-musleabi)
|
||||
MUSL_URI=armv7m-linux-musleabi
|
||||
;;
|
||||
arm-unknown-linux-musleabihf)
|
||||
MUSL_URI=arm-linux-musleabihf
|
||||
;;
|
||||
arm-unknown-linux-musleabi)
|
||||
MUSL_URI=arm-linux-musleabi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -n "$MUSL_URI" ]; then
|
||||
mkdir -p ./musl_gcc
|
||||
wget -c https://musl.cc/${MUSL_URI}-cross.tgz -P ./musl_gcc/
|
||||
tar zxf ./musl_gcc/${MUSL_URI}-cross.tgz -C ./musl_gcc/
|
||||
sudo ln -s $(pwd)/musl_gcc/${MUSL_URI}-cross/bin/*gcc /usr/bin/
|
||||
fi
|
||||
fi
|
||||
|
||||
# see https://github.com/rust-lang/rustup/issues/3709
|
||||
rustup set auto-self-update disable
|
||||
rustup install 1.77
|
||||
rustup default 1.77
|
||||
|
||||
# mips/mipsel cannot add target from rustup, need compile by ourselves
|
||||
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||
cd "$PWD/musl_gcc/${MUSL_URI}-cross/lib/gcc/${MUSL_URI}/11.2.1" || exit 255
|
||||
# for panic-abort
|
||||
cp libgcc_eh.a libunwind.a
|
||||
|
||||
# for mimalloc
|
||||
ar x libgcc.a _ctzsi2.o _clz.o _bswapsi2.o
|
||||
ar rcs libctz.a _ctzsi2.o _clz.o _bswapsi2.o
|
||||
|
||||
rustup toolchain install nightly-x86_64-unknown-linux-gnu
|
||||
rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
|
||||
|
||||
# https://github.com/rust-lang/rust/issues/128808
|
||||
# remove it after Cargo or rustc fix this.
|
||||
RUST_LIB_SRC=$HOME/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/
|
||||
if [[ -f $RUST_LIB_SRC/library/Cargo.lock && ! -f $RUST_LIB_SRC/Cargo.lock ]]; then
|
||||
cp -f $RUST_LIB_SRC/library/Cargo.lock $RUST_LIB_SRC/Cargo.lock
|
||||
fi
|
||||
else
|
||||
rustup target add $TARGET
|
||||
if [[ $GUI_TARGET != '' ]]; then
|
||||
rustup target add $GUI_TARGET
|
||||
fi
|
||||
fi
|
||||
@@ -2,10 +2,15 @@ name: EasyTier Mobile
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop", "main"]
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -18,112 +23,90 @@ jobs:
|
||||
pre_job:
|
||||
# continue-on-error: true # Uncomment once integration is finished
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: 'never'
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/workflows/install_rust.sh"]'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-core/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/actions/**"]'
|
||||
build-mobile:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- TARGET: android
|
||||
OS: ubuntu-latest
|
||||
ARTIFACT_NAME: android
|
||||
runs-on: ${{ matrix.OS }}
|
||||
- TARGET: aarch64-linux-android
|
||||
ARCH: aarch64
|
||||
- TARGET: armv7-linux-androideabi
|
||||
ARCH: armv7
|
||||
- TARGET: i686-linux-android
|
||||
ARCH: i686
|
||||
- TARGET: x86_64-linux-android
|
||||
ARCH: x86_64
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NAME: easytier
|
||||
TARGET: ${{ matrix.TARGET }}
|
||||
OS: ${{ matrix.OS }}
|
||||
ARCH: ${{ matrix.ARCH }}
|
||||
OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set current ref as env variable
|
||||
run: |
|
||||
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'oracle'
|
||||
java-version: '20'
|
||||
java-version: '21'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
with:
|
||||
cmdline-tools-version: 11076708
|
||||
packages: 'build-tools;34.0.0 ndk;26.0.10792818 tools platform-tools platforms;android-34 '
|
||||
cmdline-tools-version: 12.0
|
||||
packages: 'build-tools;34.0.0 ndk;26.0.10792818 platform-tools platforms;android-34 '
|
||||
|
||||
- name: Setup Android Environment
|
||||
run: |
|
||||
echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH
|
||||
echo "$ANDROID_HOME/ndk/26.0.10792818/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_PATH
|
||||
echo "NDK_HOME=$ANDROID_HOME/ndk/26.0.10792818/" > $GITHUB_ENV
|
||||
echo "NDK_HOME=$ANDROID_HOME/ndk/26.0.10792818/" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
node-version: 21
|
||||
target: ${{ matrix.TARGET }}
|
||||
gui: false
|
||||
pnpm: true
|
||||
pnpm-build-filter: ''
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
version: 9
|
||||
run_install: false
|
||||
# The prefix cache key, this can be changed to start a new cache manually.
|
||||
# default: "v0-rust"
|
||||
prefix-key: ""
|
||||
shared-key: "gui-registry"
|
||||
cache-targets: "false"
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: |
|
||||
(cd easytier-gui; pnpm install)
|
||||
(cd tauri-plugin-vpnservice; pnpm install; pnpm build)
|
||||
|
||||
- name: Cargo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
./target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Install rust target
|
||||
run: |
|
||||
bash ./.github/workflows/install_rust.sh
|
||||
rustup target add aarch64-linux-android
|
||||
rustup target add armv7-linux-androideabi
|
||||
rustup target add i686-linux-android
|
||||
rustup target add x86_64-linux-android
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v2
|
||||
with:
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Android
|
||||
- name: Build
|
||||
run: |
|
||||
cd easytier-gui
|
||||
pnpm tauri android build
|
||||
pnpm tauri android build --apk --target "$ARCH" --split-per-abi
|
||||
|
||||
- name: Compress
|
||||
- name: Collect artifact
|
||||
run: |
|
||||
mkdir -p ./artifacts/objects/
|
||||
mv easytier-gui/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk ./artifacts/objects/
|
||||
mv easytier-gui/src-tauri/gen/android/app/build/outputs/apk/*/release/*.apk ./artifacts/objects/
|
||||
|
||||
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
||||
TAG=$GITHUB_REF_NAME
|
||||
@@ -131,35 +114,21 @@ jobs:
|
||||
TAG=$GITHUB_SHA
|
||||
fi
|
||||
|
||||
mv ./artifacts/objects/* ./artifacts
|
||||
mv ./artifacts/objects/* ./artifacts/
|
||||
rm -rf ./artifacts/objects/
|
||||
|
||||
- name: Archive artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: easytier-gui-${{ matrix.ARTIFACT_NAME }}
|
||||
name: easytier-mobile-android-${{ matrix.ARCH }}
|
||||
path: |
|
||||
./artifacts/*
|
||||
|
||||
- name: Upload OSS
|
||||
if: ${{ env.OSS_BUCKET != '' }}
|
||||
uses: Menci/upload-to-oss@main
|
||||
with:
|
||||
access-key-id: ${{ secrets.ALIYUN_OSS_ACCESS_ID }}
|
||||
access-key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY }}
|
||||
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
|
||||
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||
local-path: ./artifacts/
|
||||
remote-path: /easytier-releases/${{ github.sha }}/mobile
|
||||
no-delete-remote-files: true
|
||||
retry: 5
|
||||
mobile-result:
|
||||
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre_job
|
||||
- build-mobile
|
||||
needs: [ pre_job, build-mobile ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: needs.build-mobile.result != 'success'
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Nix Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "develop"]
|
||||
paths:
|
||||
- "**/*.nix"
|
||||
- "flake.lock"
|
||||
- "rust-toolchain.toml"
|
||||
pull_request:
|
||||
branches: ["main", "develop"]
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- "**/*.nix"
|
||||
- "flake.lock"
|
||||
- "rust-toolchain.toml"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-full-shell:
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v27
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
|
||||
- name: Magic Nix Cache
|
||||
uses: DeterminateSystems/magic-nix-cache-action@v6
|
||||
|
||||
- name: Warm up full devShell
|
||||
run: nix develop .#full --command true
|
||||
|
||||
- name: Cargo check in flake environment
|
||||
run: nix develop .#full --command cargo check
|
||||
|
||||
- name: Cargo build in flake environment
|
||||
run: nix develop .#full --command cargo build
|
||||
@@ -0,0 +1,205 @@
|
||||
name: ohos
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop, main, "releases/**", "ohos/**"]
|
||||
tags:
|
||||
- "v*"
|
||||
- "!*-pre"
|
||||
pull_request:
|
||||
branches: [develop, main, "ohos/**"]
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish:
|
||||
description: Publish this non-main branch and dispatch downstream builds
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
ohos:
|
||||
name: ohos
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Rust
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
target: aarch64-unknown-linux-ohos
|
||||
gui: false
|
||||
pnpm: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up HarmonyOS
|
||||
uses: ErBWs/setup-ohos@v1
|
||||
|
||||
- name: Install ohrs
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: ohrs
|
||||
|
||||
- name: Build HAR
|
||||
id: package
|
||||
env:
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get install -qqy \
|
||||
pkg-config curl libgl1-mesa-dev expect llvm clang lldb lld
|
||||
rustup component add rustfmt
|
||||
cargo fmt --all --manifest-path \
|
||||
easytier-contrib/easytier-ohrs/Cargo.toml -- --check
|
||||
|
||||
cargo_version=$(cargo metadata --format-version 1 --no-deps \
|
||||
--manifest-path easytier/Cargo.toml | jq -r '.packages[0].version')
|
||||
last_tag=$(git describe --tags --abbrev=0 HEAD 2>/dev/null || true)
|
||||
if [ -n "$last_tag" ]; then
|
||||
base_version=$(printf '%s\n' "$cargo_version" "${last_tag#v}" \
|
||||
| sort -V | tail -n 1)
|
||||
commit_count=$(git rev-list --count "$last_tag..HEAD")
|
||||
else
|
||||
base_version=$cargo_version
|
||||
commit_count=0
|
||||
fi
|
||||
|
||||
source_branch=${GITHUB_HEAD_REF:-}
|
||||
if [ -z "$source_branch" ]; then
|
||||
if [ "$GITHUB_REF_TYPE" = branch ]; then
|
||||
source_branch=$GITHUB_REF_NAME
|
||||
else
|
||||
source_branch=${DEFAULT_BRANCH:-main}
|
||||
fi
|
||||
fi
|
||||
branch_id=$(printf '%s' "$source_branch" \
|
||||
| tr '[:upper:]' '[:lower:]' \
|
||||
| sed -E 's/[^a-z0-9-]+/-/g; s/^-+//; s/-+$//' \
|
||||
| cut -c1-64)
|
||||
branch_id=${branch_id:-main}
|
||||
|
||||
package_name=easytier-ohrs
|
||||
package_version="${base_version}-${branch_id}-${commit_count}-${GITHUB_RUN_NUMBER}-${GITHUB_RUN_ATTEMPT}-g$(git rev-parse --short=8 HEAD)"
|
||||
echo "name=$package_name" >> "$GITHUB_OUTPUT"
|
||||
echo "EASYTIER_PACKAGE_NAME=$package_name" >> "$GITHUB_ENV"
|
||||
echo "EASYTIER_VERSION=$package_version" >> "$GITHUB_ENV"
|
||||
|
||||
package_dir=easytier-contrib/easytier-ohrs/package
|
||||
jq --arg name "$package_name" --arg version "$package_version" \
|
||||
'.name = $name | .version = $version' \
|
||||
"$package_dir/oh-package.json5" > "$package_dir/oh-package.tmp.json5"
|
||||
mv "$package_dir/oh-package.tmp.json5" "$package_dir/oh-package.json5"
|
||||
{
|
||||
echo "## $package_name $package_version"
|
||||
echo
|
||||
echo "- Core version: $base_version"
|
||||
echo "- Core commit: $GITHUB_SHA"
|
||||
git log -1 --pretty=format:'- %s'
|
||||
echo
|
||||
} > "$package_dir/CHANGELOG.md"
|
||||
|
||||
sudo mkdir -p "$OHOS_NDK_HOME/native/llvm"
|
||||
sudo tee "$OHOS_NDK_HOME/native/llvm/aarch64-unknown-linux-ohos-clang.sh" >/dev/null <<'EOF'
|
||||
#!/bin/sh
|
||||
exec "$OHOS_NDK_HOME/native/llvm/bin/clang" \
|
||||
-target aarch64-linux-ohos \
|
||||
--sysroot="$OHOS_NDK_HOME/native/sysroot" \
|
||||
-D__MUSL__ "$@"
|
||||
EOF
|
||||
sudo chmod +x \
|
||||
"$OHOS_NDK_HOME/native/llvm/aarch64-unknown-linux-ohos-clang.sh"
|
||||
|
||||
cd easytier-contrib/easytier-ohrs
|
||||
source env.sh
|
||||
ohrs build --release --arch aarch
|
||||
ohrs artifact
|
||||
mv package.har "$package_name.har"
|
||||
|
||||
- name: Upload HAR
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ steps.package.outputs.name }}
|
||||
path: easytier-contrib/easytier-ohrs/${{ steps.package.outputs.name }}.har
|
||||
retention-days: 5
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Publish and dispatch
|
||||
if: >-
|
||||
(github.event_name == 'push' &&
|
||||
github.ref_type == 'branch' &&
|
||||
github.ref_name == 'main' &&
|
||||
github.event.forced != true) ||
|
||||
(github.event_name == 'workflow_dispatch' &&
|
||||
github.ref_type == 'branch' &&
|
||||
(github.ref_name == 'main' || inputs.publish))
|
||||
working-directory: easytier-contrib/easytier-ohrs
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CODEARTS_PRIVATE_OHPM: ${{ secrets.CODEARTS_PRIVATE_OHPM }}
|
||||
DOWNSTREAM_DISPATCH_TOKEN: ${{ secrets.DOWNSTREAM_DISPATCH_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "$GITHUB_EVENT_NAME" = push ]; then
|
||||
pull_requests=$(gh api \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls")
|
||||
if ! jq -e \
|
||||
--arg repository "$GITHUB_REPOSITORY" \
|
||||
--arg branch "$GITHUB_REF_NAME" \
|
||||
--arg sha "$GITHUB_SHA" \
|
||||
'any(.[];
|
||||
.merged_at != null and
|
||||
.base.repo.full_name == $repository and
|
||||
.base.ref == $branch and
|
||||
.merge_commit_sha == $sha)' \
|
||||
<<< "$pull_requests" >/dev/null; then
|
||||
echo "Direct push: HAR built without publishing."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$HOME/.ohpm"
|
||||
umask 077
|
||||
printf '%s' "$CODEARTS_PRIVATE_OHPM" > "$HOME/.ohpm/.ohpmrc"
|
||||
trap 'rm -f "$HOME/.ohpm/.ohpmrc"' EXIT
|
||||
ohpm publish "$EASYTIER_PACKAGE_NAME.har"
|
||||
|
||||
payload=$(jq -nc \
|
||||
--arg repository "$GITHUB_REPOSITORY" \
|
||||
--arg ref "refs/heads/$GITHUB_REF_NAME" \
|
||||
--arg package "$EASYTIER_PACKAGE_NAME" \
|
||||
'{
|
||||
event_type: "core-har-published",
|
||||
client_payload: {
|
||||
core_repository: $repository,
|
||||
core_ref: $ref,
|
||||
package_name: $package
|
||||
}
|
||||
}')
|
||||
for repository in \
|
||||
FrankHan052176/EasyTier-ArkTS \
|
||||
FrankHan052176/easytier-pro-app; do
|
||||
curl --fail-with-body --silent --show-error \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $DOWNSTREAM_DISPATCH_TOKEN" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"$GITHUB_API_URL/repos/$repository/dispatches" \
|
||||
--data "$payload"
|
||||
done
|
||||
@@ -6,22 +6,19 @@ on:
|
||||
core_run_id:
|
||||
description: 'The run id of EasyTier-Core Action in EasyTier repo'
|
||||
type: number
|
||||
default: 10322498549
|
||||
required: true
|
||||
gui_run_id:
|
||||
description: 'The run id of EasyTier-GUI Action in EasyTier repo'
|
||||
type: number
|
||||
default: 10322498557
|
||||
required: true
|
||||
mobile_run_id:
|
||||
description: 'The run id of EasyTier-Mobile Action in EasyTier repo'
|
||||
type: number
|
||||
default: 10322498555
|
||||
required: true
|
||||
version:
|
||||
description: 'version for this release'
|
||||
description: 'Version for this release'
|
||||
type: string
|
||||
default: 'v1.2.2'
|
||||
default: 'v2.6.4'
|
||||
required: true
|
||||
make_latest:
|
||||
description: 'Mark this release as latest'
|
||||
@@ -34,48 +31,57 @@ permissions:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: contains('["KKRainbow"]', github.actor)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Download Core Artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.core_run_id }}
|
||||
repo: EasyTier/EasyTier
|
||||
repo: ${{ github.repository }}
|
||||
path: release_assets
|
||||
|
||||
- name: Download GUI Artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.gui_run_id }}
|
||||
repo: EasyTier/EasyTier
|
||||
path: release_assets
|
||||
repo: ${{ github.repository }}
|
||||
path: release_assets_nozip
|
||||
|
||||
- name: Download GUI Artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
- name: Download Mobile Artifact
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.mobile_run_id }}
|
||||
repo: EasyTier/EasyTier
|
||||
path: release_assets
|
||||
repo: ${{ github.repository }}
|
||||
path: release_assets_nozip
|
||||
|
||||
- name: Zip release assets
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
mkdir zipped_assets
|
||||
|
||||
find release_assets_nozip -type f -exec mv {} zipped_assets \;
|
||||
ls -l -R ./zipped_assets
|
||||
|
||||
cd release_assets
|
||||
ls -l -R ./
|
||||
chmod -R 755 .
|
||||
|
||||
mkdir ../zipped_assets
|
||||
for x in `ls`; do
|
||||
zip ../zipped_assets/$x-${VERSION}.zip $x/*;
|
||||
if [ "$x" = "Easytier-Magisk" ]; then
|
||||
# for Easytier-Magisk, make sure files are in the root of the zip
|
||||
cd $x;
|
||||
zip -r ../../zipped_assets/$x-${VERSION}.zip .;
|
||||
cd ..;
|
||||
else
|
||||
zip -r ../zipped_assets/$x-${VERSION}.zip $x;
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Release
|
||||
@@ -86,4 +92,4 @@ jobs:
|
||||
files: |
|
||||
./zipped_assets/*
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag_name: ${{ inputs.version }}
|
||||
tag_name: ${{ inputs.version }}
|
||||
|
||||
@@ -2,12 +2,18 @@ name: EasyTier Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop", "main"]
|
||||
branches: [ "develop", "main" ]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
branches: [ "develop", "main" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# RUSTC_WRAPPER: "sccache"
|
||||
# SCCACHE_GHA_ENABLED: "true"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -28,40 +34,138 @@ jobs:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: 'never'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml"]'
|
||||
test:
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-core/**", "easytier-proto/**", "easytier-web/**", "easytier-gui/src-tauri/**", "easytier-contrib/**", ".github/workflows/test.yml", ".github/actions/**"]'
|
||||
|
||||
check:
|
||||
name: Run linters & check
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v2
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
gui: true
|
||||
pnpm: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
components: rustfmt,clippy
|
||||
rustflags: ''
|
||||
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
|
||||
- name: Check formatting
|
||||
if: ${{ !cancelled() }}
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Check Clippy
|
||||
if: ${{ !cancelled() }}
|
||||
run: cargo clippy --all-targets --features full --all -- -D warnings
|
||||
|
||||
- name: Check features
|
||||
if: ${{ !cancelled() }}
|
||||
run: cargo hack check --package easytier --each-feature --exclude-features macos-ne --verbose
|
||||
|
||||
- name: Check Cargo.lock is up to date
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
if ! cargo metadata --format-version 1 --locked > /dev/null; then
|
||||
echo "::error::Cargo.lock is out of date. Run cargo generate-lockfile or cargo build locally, then commit Cargo.lock."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pre-test:
|
||||
name: Build test
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
gui: true
|
||||
pnpm: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Archive test
|
||||
run: >-
|
||||
cargo nextest archive --archive-file tests.tar.zst
|
||||
--package easytier --package easytier-core --features full
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: tests
|
||||
path: tests.tar.zst
|
||||
retention-days: 1
|
||||
|
||||
test_matrix:
|
||||
name: Test (${{ matrix.name }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre_job, pre-test ]
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: "easytier"
|
||||
opts: "-E 'not test(tests::three_node)' --test-threads 1 --no-fail-fast"
|
||||
|
||||
- name: "three_node"
|
||||
opts: "-E 'test(tests::three_node) and not test(subnet_proxy_three_node_test)' --test-threads 1 --no-fail-fast"
|
||||
|
||||
- name: "three_node::subnet_proxy_three_node_test"
|
||||
opts: "-E 'test(subnet_proxy_three_node_test)' --test-threads 1 --no-fail-fast"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup tools for test
|
||||
run: sudo apt install bridge-utils
|
||||
- name: Setup upnpd for test
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y miniupnpd miniupnpd-iptables iptables
|
||||
|
||||
- name: Setup system for test
|
||||
run: |
|
||||
sudo modprobe br_netfilter
|
||||
sudo modprobe tun
|
||||
if [ ! -e /dev/net/tun ]; then
|
||||
sudo mkdir -p /dev/net
|
||||
sudo mknod /dev/net/tun c 10 200
|
||||
fi
|
||||
sudo sysctl net.bridge.bridge-nf-call-iptables=0
|
||||
sudo sysctl net.bridge.bridge-nf-call-ip6tables=0
|
||||
sudo sysctl net.ipv6.conf.lo.disable_ipv6=0
|
||||
sudo ip addr add 2001:db8::2/64 dev lo
|
||||
|
||||
- name: Cargo cache
|
||||
uses: actions/cache@v4
|
||||
- uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Download tests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
./target
|
||||
key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }}
|
||||
name: tests
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
sudo -E env "PATH=$PATH" cargo test --no-default-features --features=full --verbose
|
||||
sudo chown -R $USER:$USER ./target
|
||||
sudo chown -R $USER:$USER ~/.cargo
|
||||
sudo prlimit --pid $$ --nofile=1048576:1048576
|
||||
sudo -E env "PATH=$PATH" EASYTIER_LINUX_BPF_INTEGRATION=required \
|
||||
cargo nextest run --archive-file tests.tar.zst ${{ matrix.opts }}
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre_job, check, test_matrix ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
@@ -11,6 +11,8 @@ target-*/
|
||||
*.pdb
|
||||
|
||||
.vscode
|
||||
/.idea
|
||||
/.direnv/
|
||||
|
||||
# perf & flamegraph
|
||||
perf.data
|
||||
@@ -29,3 +31,21 @@ musl_gcc
|
||||
|
||||
# log
|
||||
easytier-panic.log
|
||||
|
||||
# web
|
||||
node_modules
|
||||
easytier-web/frontend-lib/src/generated/
|
||||
easytier-web/config-generator/dist/
|
||||
easytier-web/config-generator/src/generated/
|
||||
|
||||
.vite
|
||||
|
||||
easytier-gui/src-tauri/*.dll
|
||||
easytier-gui/src-tauri/*.sys
|
||||
/easytier-contrib/easytier-ohrs/dist/
|
||||
|
||||
.direnv
|
||||
.flake-profile
|
||||
|
||||
# contrib
|
||||
go.sum
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# EasyTier Domain Context
|
||||
|
||||
## Module layers
|
||||
|
||||
`easytier-core` layers dependencies from `foundation` upward through the
|
||||
portable networking domains. `foundation` contains infrastructure Modules
|
||||
that have no dependency on a networking domain and may be used by any higher
|
||||
layer.
|
||||
|
||||
## Operation broker
|
||||
|
||||
An operation broker owns the lifecycle of asynchronous work submitted by an
|
||||
external caller to core. It allocates opaque operation IDs, arbitrates
|
||||
completion, cancellation, and disposal, retains terminal outcomes, and
|
||||
publishes a batch-drainable completion queue.
|
||||
|
||||
The broker does not interpret operation kinds, outcomes, resources, wire
|
||||
formats, or domain errors. Each domain Module owns those semantics and composes
|
||||
the broker under the same lock as any state that must change atomically with an
|
||||
operation transition.
|
||||
|
||||
Host capability operations use a separate seam. They turn Host readiness into
|
||||
Rust task wakeups and do not share the caller-to-core broker state machine.
|
||||
|
||||
## Compact compatibility Host
|
||||
|
||||
A compact compatibility Host retains accepted values in the authoritative TOML
|
||||
model for management readback, while the shared host-aware normalization path
|
||||
omits capabilities that the compact runtime cannot execute. Omitted settings
|
||||
are silent no-ops and must not be advertised as live network capabilities.
|
||||
@@ -0,0 +1,236 @@
|
||||
# Contributing to EasyTier
|
||||
|
||||
[中文版](CONTRIBUTING_zh.md)
|
||||
|
||||
Thank you for your interest in contributing to EasyTier! This document provides guidelines and instructions for contributing to the project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Development Environment Setup](#development-environment-setup)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation Steps](#installation-steps)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Build Guide](#build-guide)
|
||||
- [Building Core](#building-core)
|
||||
- [Building GUI](#building-gui)
|
||||
- [Building Mobile](#building-mobile)
|
||||
- [Development Workflow](#development-workflow)
|
||||
- [Testing Guidelines](#testing-guidelines)
|
||||
- [Pull Request Guidelines](#pull-request-guidelines)
|
||||
- [Additional Resources](#additional-resources)
|
||||
|
||||
## Development Environment Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
#### Required Tools
|
||||
- Node.js v21 or higher
|
||||
- pnpm v9 or higher
|
||||
- Rust toolchain (version 1.95)
|
||||
- LLVM and Clang
|
||||
- Protoc (Protocol Buffers compiler)
|
||||
|
||||
#### Platform-Specific Dependencies
|
||||
|
||||
**Linux (Ubuntu/Debian)**
|
||||
```bash
|
||||
# Core build dependencies
|
||||
sudo apt-get update && sudo apt-get install -y \
|
||||
musl-tools \
|
||||
llvm \
|
||||
clang \
|
||||
protobuf-compiler
|
||||
|
||||
# GUI build dependencies
|
||||
sudo apt install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libgtk-3-dev \
|
||||
librsvg2-dev \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
libappindicator3-dev \
|
||||
patchelf
|
||||
|
||||
# Testing dependencies
|
||||
sudo apt install -y bridge-utils
|
||||
```
|
||||
|
||||
**For Cross-Compilation**
|
||||
- musl-cross toolchain (for MIPS and other architectures)
|
||||
- Additional setup may be required (see `.github/workflows/` for details)
|
||||
|
||||
**For Android Development**
|
||||
- Java 20
|
||||
- Android SDK (Build Tools 34.0.0)
|
||||
- Android NDK (26.0.10792818)
|
||||
|
||||
### Installation Steps
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/EasyTier/EasyTier.git
|
||||
cd EasyTier
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
# Install Rust toolchain
|
||||
rustup install 1.95
|
||||
rustup default 1.95
|
||||
|
||||
# Install project dependencies
|
||||
pnpm -r install
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
easytier/ # Core functionality and libraries
|
||||
easytier-web/ # Web dashboard and frontend
|
||||
easytier-gui/ # Desktop GUI application
|
||||
.github/workflows/ # CI/CD configuration files
|
||||
```
|
||||
|
||||
## Build Guide
|
||||
|
||||
### Building Core
|
||||
|
||||
```bash
|
||||
# Standard build
|
||||
cargo build --release
|
||||
|
||||
# Platform-specific builds
|
||||
cargo build --release --target x86_64-unknown-linux-musl # Linux x86_64
|
||||
cargo build --release --target aarch64-unknown-linux-musl # Linux ARM64
|
||||
cargo build --release --target x86_64-apple-darwin # macOS x86_64
|
||||
cargo build --release --target aarch64-apple-darwin # macOS M1/M2
|
||||
cargo build --release --target x86_64-pc-windows-msvc # Windows x86_64
|
||||
```
|
||||
|
||||
Build artifacts: `target/[target-triple]/release/`
|
||||
|
||||
### Building the WASI core
|
||||
|
||||
```bash
|
||||
script/build-wasi-core.sh
|
||||
```
|
||||
|
||||
This builds the `easytier-core` Go-host profile for `wasm32-wasip1`, then
|
||||
optimizes it with the pinned official Binaryen release. Binaryen is downloaded
|
||||
once into `target/binaryen/` and verified by SHA-256; set `WASM_OPT` to use an
|
||||
existing matching binary.
|
||||
|
||||
### Building GUI
|
||||
|
||||
```bash
|
||||
# 1. Build frontend
|
||||
pnpm -r build
|
||||
|
||||
# 2. Build GUI application
|
||||
cd easytier-gui
|
||||
|
||||
# Linux
|
||||
pnpm tauri build --target x86_64-unknown-linux-gnu
|
||||
|
||||
# macOS
|
||||
pnpm tauri build --target x86_64-apple-darwin # Intel
|
||||
pnpm tauri build --target aarch64-apple-darwin # Apple Silicon
|
||||
|
||||
# Windows
|
||||
pnpm tauri build --target x86_64-pc-windows-msvc # x64
|
||||
```
|
||||
|
||||
Build artifacts: `easytier-gui/src-tauri/target/release/bundle/`
|
||||
|
||||
### Building Mobile
|
||||
|
||||
```bash
|
||||
# 1. Install Android targets
|
||||
rustup target add aarch64-linux-android
|
||||
rustup target add armv7-linux-androideabi
|
||||
rustup target add i686-linux-android
|
||||
rustup target add x86_64-linux-android
|
||||
|
||||
# 2. Build Android application
|
||||
cd easytier-gui
|
||||
pnpm tauri android build
|
||||
```
|
||||
|
||||
Build artifacts: `easytier-gui/src-tauri/gen/android/app/build/outputs/apk/universal/release/`
|
||||
|
||||
### Build Notes
|
||||
|
||||
1. Cross-compilation for ARM/MIPS requires additional setup
|
||||
2. Windows builds need correct DLL files
|
||||
3. Check `.github/workflows/` for detailed build configurations
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. Create a feature branch from `develop`:
|
||||
```bash
|
||||
git checkout develop
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
|
||||
2. Make your changes following our coding standards
|
||||
|
||||
3. Write or update tests as needed
|
||||
|
||||
4. Use conventional commit messages:
|
||||
```
|
||||
feat: add new feature
|
||||
fix: resolve bug
|
||||
docs: update documentation
|
||||
test: add tests
|
||||
chore: update dependencies
|
||||
```
|
||||
|
||||
5. Submit a pull request to `develop`
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Configure system (Linux)
|
||||
sudo modprobe br_netfilter
|
||||
sudo sysctl net.bridge.bridge-nf-call-iptables=0
|
||||
sudo sysctl net.bridge.bridge-nf-call-ip6tables=0
|
||||
|
||||
# Run tests
|
||||
cargo test --no-default-features --features=full --verbose
|
||||
```
|
||||
|
||||
### Test Requirements
|
||||
|
||||
- Write tests for new features
|
||||
- Maintain existing test coverage
|
||||
- Tests should be isolated and repeatable
|
||||
- Include both unit and integration tests
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
1. Target the `develop` branch
|
||||
2. Ensure all tests pass
|
||||
3. Include clear description and purpose
|
||||
4. Reference related issues
|
||||
5. Keep changes focused and atomic
|
||||
6. Update documentation as needed
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Issue Tracker](https://github.com/EasyTier/EasyTier/issues)
|
||||
- [Project Documentation](https://github.com/EasyTier/EasyTier/wiki)
|
||||
|
||||
## Questions or Need Help?
|
||||
|
||||
Feel free to:
|
||||
- Open an issue for questions
|
||||
- Join our community discussions
|
||||
- Reach out to maintainers
|
||||
|
||||
Thank you for contributing to EasyTier!
|
||||
@@ -0,0 +1,233 @@
|
||||
# EasyTier 贡献指南
|
||||
|
||||
[English Version](CONTRIBUTING.md)
|
||||
|
||||
感谢您对 EasyTier 项目的关注!本文档提供了参与项目贡献的指南和说明。
|
||||
|
||||
## 目录
|
||||
|
||||
- [EasyTier 贡献指南](#easytier-贡献指南)
|
||||
- [目录](#目录)
|
||||
- [开发环境配置](#开发环境配置)
|
||||
- [前置要求](#前置要求)
|
||||
- [必需工具](#必需工具)
|
||||
- [平台特定依赖](#平台特定依赖)
|
||||
- [安装步骤](#安装步骤)
|
||||
- [项目结构](#项目结构)
|
||||
- [构建指南](#构建指南)
|
||||
- [构建核心组件](#构建核心组件)
|
||||
- [构建桌面应用](#构建桌面应用)
|
||||
- [构建移动应用](#构建移动应用)
|
||||
- [构建注意事项](#构建注意事项)
|
||||
- [开发工作流](#开发工作流)
|
||||
- [测试指南](#测试指南)
|
||||
- [运行测试](#运行测试)
|
||||
- [测试要求](#测试要求)
|
||||
- [Pull Request 规范](#pull-request-规范)
|
||||
- [其他资源](#其他资源)
|
||||
- [需要帮助?](#需要帮助)
|
||||
|
||||
## 开发环境配置
|
||||
|
||||
### 前置要求
|
||||
|
||||
#### 必需工具
|
||||
- Node.js v21 或更高版本
|
||||
- pnpm v9 或更高版本
|
||||
- Rust 工具链(版本 1.95)
|
||||
- LLVM 和 Clang
|
||||
- Protoc(Protocol Buffers 编译器)
|
||||
|
||||
#### 平台特定依赖
|
||||
|
||||
**Linux (Ubuntu/Debian)**
|
||||
```bash
|
||||
# 核心构建依赖
|
||||
sudo apt-get update && sudo apt-get install -y \
|
||||
musl-tools \
|
||||
llvm \
|
||||
clang \
|
||||
protobuf-compiler
|
||||
|
||||
# GUI 构建依赖
|
||||
sudo apt install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libgtk-3-dev \
|
||||
librsvg2-dev \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
libappindicator3-dev \
|
||||
patchelf
|
||||
|
||||
# 测试依赖
|
||||
sudo apt install -y bridge-utils
|
||||
```
|
||||
|
||||
**交叉编译依赖**
|
||||
- musl-cross 工具链(用于 MIPS 和其他架构)
|
||||
- 可能需要额外配置(详见 `.github/workflows/` 目录)
|
||||
|
||||
**Android 开发依赖**
|
||||
- Java 20
|
||||
- Android SDK(Build Tools 34.0.0)
|
||||
- Android NDK(26.0.10792818)
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. 克隆仓库:
|
||||
```bash
|
||||
git clone https://github.com/EasyTier/EasyTier.git
|
||||
cd EasyTier
|
||||
```
|
||||
|
||||
2. 安装依赖:
|
||||
```bash
|
||||
# 安装 Rust 工具链
|
||||
rustup install 1.95
|
||||
rustup default 1.95
|
||||
|
||||
# 安装项目依赖
|
||||
pnpm -r install
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
easytier/ # 核心功能和库
|
||||
easytier-web/ # Web 仪表盘和前端
|
||||
easytier-gui/ # 桌面 GUI 应用
|
||||
.github/workflows/ # CI/CD 配置文件
|
||||
```
|
||||
|
||||
## 构建指南
|
||||
|
||||
### 构建核心组件
|
||||
|
||||
```bash
|
||||
# 标准构建
|
||||
cargo build --release
|
||||
|
||||
# 特定平台构建
|
||||
cargo build --release --target x86_64-unknown-linux-musl # Linux x86_64
|
||||
cargo build --release --target aarch64-unknown-linux-musl # Linux ARM64
|
||||
cargo build --release --target x86_64-apple-darwin # macOS x86_64
|
||||
cargo build --release --target aarch64-apple-darwin # macOS M1/M2
|
||||
cargo build --release --target x86_64-pc-windows-msvc # Windows x86_64
|
||||
```
|
||||
|
||||
构建产物位置:`target/[target-triple]/release/`
|
||||
|
||||
### 构建桌面应用
|
||||
|
||||
```bash
|
||||
# 1. 构建前端
|
||||
pnpm -r build
|
||||
|
||||
# 2. 构建 GUI 应用
|
||||
cd easytier-gui
|
||||
|
||||
# Linux
|
||||
pnpm tauri build --target x86_64-unknown-linux-gnu
|
||||
|
||||
# macOS
|
||||
pnpm tauri build --target x86_64-apple-darwin # Intel
|
||||
pnpm tauri build --target aarch64-apple-darwin # Apple Silicon
|
||||
|
||||
# Windows
|
||||
pnpm tauri build --target x86_64-pc-windows-msvc # x64
|
||||
```
|
||||
|
||||
构建产物位置:`easytier-gui/src-tauri/target/release/bundle/`
|
||||
|
||||
### 构建移动应用
|
||||
|
||||
```bash
|
||||
# 1. 安装 Android 目标平台
|
||||
rustup target add aarch64-linux-android
|
||||
rustup target add armv7-linux-androideabi
|
||||
rustup target add i686-linux-android
|
||||
rustup target add x86_64-linux-android
|
||||
|
||||
# 2. 构建 Android 应用
|
||||
cd easytier-gui
|
||||
pnpm tauri android build
|
||||
```
|
||||
|
||||
构建产物位置:`easytier-gui/src-tauri/gen/android/app/build/outputs/apk/universal/release/`
|
||||
|
||||
### 构建注意事项
|
||||
|
||||
1. ARM/MIPS 的交叉编译需要额外配置
|
||||
2. Windows 构建需要正确的 DLL 文件
|
||||
3. 详细构建配置请参考 `.github/workflows/` 目录
|
||||
|
||||
## 开发工作流
|
||||
|
||||
1. 从 `develop` 分支创建特性分支:
|
||||
```bash
|
||||
git checkout develop
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
|
||||
2. 按照代码规范进行修改
|
||||
|
||||
3. 编写或更新测试
|
||||
|
||||
4. 使用规范的提交信息:
|
||||
```
|
||||
feat: 添加新功能
|
||||
fix: 修复问题
|
||||
docs: 更新文档
|
||||
test: 添加测试
|
||||
chore: 更新依赖
|
||||
```
|
||||
|
||||
5. 提交 Pull Request 到 `develop` 分支
|
||||
|
||||
## 测试指南
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 配置系统(Linux)
|
||||
sudo modprobe br_netfilter
|
||||
sudo sysctl net.bridge.bridge-nf-call-iptables=0
|
||||
sudo sysctl net.bridge.bridge-nf-call-ip6tables=0
|
||||
|
||||
# 运行测试
|
||||
cargo test --no-default-features --features=full --verbose
|
||||
```
|
||||
|
||||
### 测试要求
|
||||
|
||||
- 为新功能编写测试
|
||||
- 维护现有测试覆盖率
|
||||
- 测试应该是独立且可重复的
|
||||
- 包含单元测试和集成测试
|
||||
|
||||
## Pull Request 规范
|
||||
|
||||
1. 目标分支为 `develop`
|
||||
2. 确保所有测试通过
|
||||
3. 包含清晰的描述和目的
|
||||
4. 关联相关的 issues
|
||||
5. 保持变更的原子性和聚焦性
|
||||
6. 及时更新相关文档
|
||||
|
||||
## 其他资源
|
||||
|
||||
- [问题追踪](https://github.com/EasyTier/EasyTier/issues)
|
||||
- [项目文档](https://github.com/EasyTier/EasyTier/wiki)
|
||||
|
||||
## 需要帮助?
|
||||
|
||||
欢迎:
|
||||
- 提出问题
|
||||
- 参与社区讨论
|
||||
- 联系维护者
|
||||
|
||||
感谢您为 EasyTier 做出贡献!
|
||||
@@ -1,13 +1,37 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["easytier", "easytier-gui/src-tauri"]
|
||||
default-members = ["easytier"]
|
||||
members = [
|
||||
"easytier-core",
|
||||
"easytier-proto",
|
||||
"easytier",
|
||||
"easytier-gui/src-tauri",
|
||||
"easytier-web",
|
||||
"easytier-contrib/easytier-mini",
|
||||
"easytier-contrib/easytier-ffi",
|
||||
"easytier-contrib/easytier-uptime",
|
||||
"easytier-contrib/easytier-android-jni",
|
||||
]
|
||||
default-members = ["easytier", "easytier-web"]
|
||||
exclude = [
|
||||
"easytier-contrib/easytier-ohrs", # it needs ohrs sdk
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
rust-version = "1.95"
|
||||
|
||||
[profile.dev]
|
||||
panic = "unwind"
|
||||
debug = 2
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
opt-level = 3
|
||||
strip = true
|
||||
|
||||
[profile.mini]
|
||||
inherits = "release"
|
||||
opt-level = "z"
|
||||
strip = "symbols"
|
||||
|
||||
@@ -4,84 +4,54 @@
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"name": "core",
|
||||
"path": "easytier"
|
||||
},
|
||||
{
|
||||
"name": "gui",
|
||||
"path": "easytier-gui"
|
||||
},
|
||||
{
|
||||
"path": "easytier"
|
||||
"name": "web",
|
||||
"path": "easytier-web"
|
||||
},
|
||||
{
|
||||
"name": "ffi",
|
||||
"path": "easytier-contrib/easytier-ffi"
|
||||
},
|
||||
{
|
||||
"name": "magisk",
|
||||
"path": "easytier-contrib/easytier-magisk"
|
||||
},
|
||||
{
|
||||
"name": "openharmony",
|
||||
"path": "easytier-contrib/easytier-ohrs"
|
||||
},
|
||||
{
|
||||
"name": "uptime",
|
||||
"path": "easytier-contrib/easytier-uptime"
|
||||
},
|
||||
{
|
||||
"name": "vpnservice",
|
||||
"path": "tauri-plugin-vpnservice"
|
||||
},
|
||||
{
|
||||
"name": "rpc-build",
|
||||
"path": "easytier-rpc-build"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"eslint.experimental.useFlatConfig": true,
|
||||
"i18n-ally.sourceLanguage": "cn",
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.sortKeys": true,
|
||||
// Disable the default formatter
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "modifications",
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnType": true,
|
||||
"[nix]": {
|
||||
"editor.formatOnSave": false,
|
||||
},
|
||||
"eslint.rules.customizations": [
|
||||
{
|
||||
"rule": "style/*",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "style/eol-last",
|
||||
"severity": "error"
|
||||
},
|
||||
{
|
||||
"rule": "format/*",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-indent",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-spacing",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-spaces",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-order",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-dangle",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-newline",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*quotes",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*semi",
|
||||
"severity": "off"
|
||||
}
|
||||
],
|
||||
"eslint.validate": [
|
||||
"code-workspace",
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml",
|
||||
"toml",
|
||||
"gql",
|
||||
"graphql"
|
||||
],
|
||||
"i18n-ally.localesPaths": [
|
||||
"easytier-gui/locales"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,73 +1,165 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
0. Additional Definitions.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
4. Combined Works.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
d) Do one of the following:
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
Copyright 2023 sunsijie
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
5. Combined Libraries.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
|
||||
@@ -1,215 +1,247 @@
|
||||
# EasyTier
|
||||
|
||||
[](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
|
||||
[](https://github.com/EasyTier/EasyTier/commits/main)
|
||||
[](https://github.com/EasyTier/EasyTier/issues)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
|
||||
|
||||
# EasyTier
|
||||
|
||||
[](https://github.com/EasyTier/EasyTier/releases)
|
||||
[](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
|
||||
[](https://github.com/EasyTier/EasyTier/commits/main)
|
||||
[](https://github.com/EasyTier/EasyTier/issues)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/test.yml)
|
||||
[](https://deepwiki.com/EasyTier/EasyTier)
|
||||
|
||||
[简体中文](/README_CN.md) | [English](/README.md)
|
||||
|
||||
**Please visit the [EasyTier Official Website](https://www.easytier.top/en/) to view the full documentation.**
|
||||
|
||||
EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.
|
||||
> ✨ A simple, secure, decentralized virtual private network solution powered by Rust and Tokio
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/image-5.png" width="300">
|
||||
<img src="assets/image-4.png" width="300">
|
||||
<img src="assets/config-page.png" width="300" alt="config page">
|
||||
<img src="assets/running-page.png" width="300" alt="running page">
|
||||
</p>
|
||||
|
||||
## Features
|
||||
|
||||
- **Decentralized**: No need to rely on centralized services, nodes are equal and independent.
|
||||
- **Safe**: Use WireGuard protocol to encrypt data.
|
||||
- **High Performance**: Full-link zero-copy, with performance comparable to mainstream networking software.
|
||||
- **Cross-platform**: Supports MacOS/Linux/Windows, will support IOS and Android in the future. The executable file is statically linked, making deployment simple.
|
||||
- **Networking without public IP**: Supports networking using shared public nodes, refer to [Configuration Guide](#Networking-without-public-IP)
|
||||
- **NAT traversal**: Supports UDP-based NAT traversal, able to establish stable connections even in complex network environments.
|
||||
- **Subnet Proxy (Point-to-Network)**: Nodes can expose accessible network segments as proxies to the VPN subnet, allowing other nodes to access these subnets through the node.
|
||||
- **Smart Routing**: Selects links based on traffic to reduce latency and increase throughput.
|
||||
- **TCP Support**: Provides reliable data transmission through concurrent TCP links when UDP is limited, optimizing performance.
|
||||
- **High Availability**: Supports multi-path and switches to healthy paths when high packet loss or network errors are detected.
|
||||
- **IPv6 Support**: Supports networking using IPv6.
|
||||
- **Multiple Protocol Types**: Supports communication between nodes using protocols such as WebSocket and QUIC.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Download the precompiled binary file**
|
||||
|
||||
Visit the [GitHub Release page](https://github.com/EasyTier/EasyTier/releases) to download the binary file suitable for your operating system. Release includes both command-line programs and GUI programs in the compressed package.
|
||||
|
||||
2. **Install via crates.io**
|
||||
```sh
|
||||
cargo install easytier
|
||||
```
|
||||
|
||||
3. **Install from source code**
|
||||
```sh
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git
|
||||
```
|
||||
📚 **[Full Documentation](https://easytier.cn/en/)** | 🖥️ **[Web Console](https://easytier.cn/web)** | 📝 **[Download Releases](https://github.com/EasyTier/EasyTier/releases)** | 🧩 **[Third Party Tools](https://easytier.cn/en/guide/installation_gui.html#third-party-graphical-interfaces)** | ❤️ **[Sponsor](#sponsor)**
|
||||
|
||||
4. **Install by Docker Compose**
|
||||
## Features
|
||||
|
||||
Please visit the [EasyTier Official Website](https://www.easytier.top/en/) to view the full documentation.
|
||||
### Core Features
|
||||
|
||||
5. **Install by script (For Linux Only)**
|
||||
```sh
|
||||
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/easytier.sh" && bash /tmp/easytier.sh install
|
||||
```
|
||||
|
||||
You can also uninstall/update Easytier by the command "uninstall" or "update" of this script
|
||||
- 🔒 **Decentralized**: Nodes are equal and independent, no centralized services required
|
||||
- 🚀 **Easy to Use**: Multiple operation methods via web, client, and command line
|
||||
- 🌍 **Cross-Platform**: Supports Win/MacOS/Linux/FreeBSD/Android and X86/ARM/MIPS architectures
|
||||
- 🔐 **Secure**: AES-GCM or WireGuard encryption, prevents man-in-the-middle attacks
|
||||
|
||||
## Quick Start
|
||||
### Advanced Capabilities
|
||||
|
||||
> The following text only describes the use of the command-line tool; the GUI program can be configured by referring to the following concepts.
|
||||
|
||||
Make sure EasyTier is installed according to the [Installation Guide](#Installation), and both easytier-core and easytier-cli commands are available.
|
||||
|
||||
### Two-node Networking
|
||||
|
||||
Assuming the network topology of the two nodes is as follows
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph Node A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
end
|
||||
|
||||
nodea <-----> nodeb
|
||||
|
||||
```
|
||||
|
||||
1. Execute on Node A:
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.1
|
||||
```
|
||||
Successful execution of the command will print the following.
|
||||
|
||||

|
||||
|
||||
2. Execute on Node B
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
3. Test Connectivity
|
||||
|
||||
The two nodes should connect successfully and be able to communicate within the virtual subnet
|
||||
```sh
|
||||
ping 10.144.144.2
|
||||
```
|
||||
|
||||
Use easytier-cli to view node information in the subnet
|
||||
```sh
|
||||
easytier-cli peer
|
||||
```
|
||||

|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||

|
||||
- 🔌 **Efficient NAT Traversal**: Supports UDP and IPv6 traversal, works with NAT4-NAT4 networks
|
||||
- 🌐 **Subnet Proxy**: Nodes can share subnets for other nodes to access
|
||||
- 🔄 **Intelligent Routing**: Latency priority and automatic route selection for best network experience
|
||||
- ⚡ **High Performance**: Zero-copy throughout the entire link, supports TCP/UDP/WSS/WG protocols
|
||||
|
||||
---
|
||||
|
||||
### Multi-node Networking
|
||||
|
||||
Based on the two-node networking example just now, if more nodes need to join the virtual network, you can use the following command.
|
||||
|
||||
```
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
The `--peers` parameter can fill in the listening address of any node already in the virtual network.
|
||||
|
||||
---
|
||||
|
||||
### Subnet Proxy (Point-to-Network) Configuration
|
||||
|
||||
Assuming the network topology is as follows, Node B wants to share its accessible subnet 10.1.1.0/24 with other nodes.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph Node A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
end
|
||||
|
||||
id1[[10.1.1.0/24]]
|
||||
|
||||
nodea <--> nodeb <-.-> id1
|
||||
|
||||
```
|
||||
|
||||
Then the startup parameters for Node B's easytier are (new -n parameter)
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
|
||||
```
|
||||
|
||||
Subnet proxy information will automatically sync to each node in the virtual network, and each node will automatically configure the corresponding route. Node A can check whether the subnet proxy is effective through the following command.
|
||||
|
||||
1. Check whether the routing information has been synchronized, the proxy_cidrs column shows the proxied subnets.
|
||||
|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||

|
||||
|
||||
2. Test whether Node A can access nodes under the proxied subnet
|
||||
|
||||
```sh
|
||||
ping 10.1.1.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Networking without Public IP
|
||||
|
||||
EasyTier supports networking using shared public nodes. The currently deployed shared public node is ``tcp://easytier.public.kkrainbow.top:11010``.
|
||||
|
||||
When using shared nodes, each node entering the network needs to provide the same ``--network-name`` and ``--network-secret`` parameters as the unique identifier of the network.
|
||||
|
||||
Taking two nodes as an example, Node A executes:
|
||||
|
||||
```sh
|
||||
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -e tcp://easytier.public.kkrainbow.top:11010
|
||||
```
|
||||
|
||||
Node B executes
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://easytier.public.kkrainbow.top:11010
|
||||
```
|
||||
|
||||
After the command is successfully executed, Node A can access Node B through the virtual IP 10.144.144.2.
|
||||
|
||||
### Use EasyTier with WireGuard Client
|
||||
### Network Optimization
|
||||
|
||||
EasyTier can be used as a WireGuard server to allow any device with WireGuard client installed to access the EasyTier network. For platforms currently unsupported by EasyTier (such as iOS, Android, etc.), this method can be used to connect to the EasyTier network.
|
||||
- 📊 **UDP Loss Resistance**: KCP/QUIC proxy optimizes latency and bandwidth in high packet loss environments
|
||||
- 🔧 **Web Management**: Easy configuration and monitoring through web interface
|
||||
- 🛠️ **Zero Config**: Simple deployment with statically linked executables
|
||||
|
||||
Assuming the network topology is as follows:
|
||||
## Quick Start
|
||||
|
||||
### 📥 Installation
|
||||
|
||||
Choose the installation method that best suits your needs:
|
||||
|
||||
Linux (Recommended):
|
||||
```bash
|
||||
curl -fsSL "https://github.com/EasyTier/EasyTier/blob/main/script/install.sh?raw=true" | sudo bash -s install
|
||||
```
|
||||
|
||||
Homebrew (MacOS/Linux):
|
||||
```bash
|
||||
brew tap brewforge/chinese
|
||||
brew install --cask easytier-gui
|
||||
```
|
||||
|
||||
Windows (Recommended, run with administrator privileges):
|
||||
```powershell
|
||||
irm "https://github.com/EasyTier/EasyTier/blob/main/script/install.ps1?raw=true" | iex
|
||||
```
|
||||
|
||||
Install via cargo (Latest development version):
|
||||
```bash
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
```
|
||||
|
||||
[Install pre-built binary](https://github.com/EasyTier/EasyTier/releases) (Recommended, All platforms supported)
|
||||
|
||||
[Install via Docker](https://easytier.cn/en/guide/installation.html#installation-methods)
|
||||
|
||||
[Install OpenWrt ipk package](https://github.com/EasyTier/luci-app-easytier)
|
||||
|
||||
Additional steps:
|
||||
|
||||
[One-Click Register Service](https://easytier.cn/en/guide/network/oneclick-install-as-service.html) (Automatically start when the system boots and run in the background)
|
||||
|
||||
### 🚀 Basic Usage
|
||||
|
||||
#### Quick Networking with Shared Nodes
|
||||
|
||||
EasyTier supports quick networking using shared public nodes. When you don't have a public IP, you can use the free shared nodes provided by the EasyTier community. Nodes will automatically attempt NAT traversal and establish P2P connections. When P2P fails, data will be relayed through shared nodes.
|
||||
|
||||
When using shared nodes, each node entering the network needs to provide the same `--network-name` and `--network-secret` parameters as the unique identifier of the network.
|
||||
|
||||
Taking two nodes as an example (Please use more complex network name to avoid conflicts):
|
||||
|
||||
1. Run on Node A:
|
||||
|
||||
```bash
|
||||
# Run with administrator privileges
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<SharedNodeIP>:11010
|
||||
```
|
||||
|
||||
2. Run on Node B:
|
||||
|
||||
```bash
|
||||
# Run with administrator privileges
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<SharedNodeIP>:11010
|
||||
```
|
||||
|
||||
After successful execution, you can check the network status using `easytier-cli`:
|
||||
|
||||
```text
|
||||
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
|
||||
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.2-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.6.2-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.2-70e69a38~ |
|
||||
```
|
||||
|
||||
You can test connectivity between nodes:
|
||||
|
||||
```bash
|
||||
# Test connectivity
|
||||
ping 10.126.126.1
|
||||
ping 10.126.126.2
|
||||
```
|
||||
|
||||
Note: If you cannot ping through, it may be that the firewall is blocking incoming traffic. Please turn off the firewall or add allow rules.
|
||||
|
||||
To improve availability, you can connect to multiple shared nodes simultaneously:
|
||||
|
||||
```bash
|
||||
# Connect to multiple shared nodes
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<SharedNodeIP1>:11010 -p udp://<SharedNodeIP2>:11010
|
||||
```
|
||||
|
||||
Once your network is set up successfully, you can easily configure it to start automatically on system boot. Refer to the [One-Click Register Service guide](https://easytier.cn/en/guide/network/oneclick-install-as-service.html) for step-by-step instructions on registering EasyTier as a system service.
|
||||
|
||||
#### Decentralized Networking
|
||||
|
||||
EasyTier is fundamentally decentralized, with no distinction between server and client. As long as one device can communicate with any node in the virtual network, it can join the virtual network. Here's how to set up a decentralized network:
|
||||
|
||||
1. Start First Node (Node A):
|
||||
|
||||
```bash
|
||||
# Start the first node
|
||||
sudo easytier-core -i 10.144.144.1
|
||||
```
|
||||
|
||||
After startup, this node will listen on the following ports by default:
|
||||
- TCP: 11010
|
||||
- UDP: 11010
|
||||
- WebSocket: 11011
|
||||
- WebSocket SSL: 11012
|
||||
- WireGuard: 11013
|
||||
|
||||
2. Connect Second Node (Node B):
|
||||
|
||||
```bash
|
||||
# Connect to the first node using its public IP
|
||||
sudo easytier-core -i 10.144.144.2 -p udp://FIRST_NODE_PUBLIC_IP:11010
|
||||
```
|
||||
|
||||
3. Verify Connection:
|
||||
|
||||
```bash
|
||||
# Test connectivity
|
||||
ping 10.144.144.2
|
||||
|
||||
# View connected peers
|
||||
easytier-cli peer
|
||||
|
||||
# View routing information
|
||||
easytier-cli route
|
||||
|
||||
# View local node information
|
||||
easytier-cli node
|
||||
```
|
||||
|
||||
For more nodes to join the network, they can connect to any existing node in the network using the `-p` parameter:
|
||||
|
||||
```bash
|
||||
# Connect to any existing node using its public IP
|
||||
sudo easytier-core -i 10.144.144.3 -p udp://ANY_EXISTING_NODE_PUBLIC_IP:11010
|
||||
```
|
||||
|
||||
### 🔍 Advanced Features
|
||||
|
||||
#### Subnet Proxy
|
||||
|
||||
Assuming the network topology is as follows, Node B wants to share its accessible subnet 10.1.1.0/24 with other nodes:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
ios[[iPhone \n WireGuard Installed]]
|
||||
|
||||
subgraph Node A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
subgraph Node A Public IP 22.1.1.1
|
||||
nodea[EasyTier<br/>10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
nodeb[EasyTier<br/>10.144.144.2]
|
||||
end
|
||||
|
||||
id1[[10.1.1.0/24]]
|
||||
|
||||
nodea <--> nodeb <-.-> id1
|
||||
```
|
||||
|
||||
To share a subnet, add the `-n` parameter when starting EasyTier:
|
||||
|
||||
```bash
|
||||
# Share subnet 10.1.1.0/24 with other nodes
|
||||
sudo easytier-core -i 10.144.144.2 -n 10.1.1.0/24
|
||||
```
|
||||
|
||||
Subnet proxy information will automatically sync to each node in the virtual network, and each node will automatically configure the corresponding route. You can verify the subnet proxy setup:
|
||||
|
||||
1. Check if the routing information has been synchronized (the proxy_cidrs column shows the proxied subnets):
|
||||
|
||||
```bash
|
||||
# View routing information
|
||||
easytier-cli route
|
||||
```
|
||||
|
||||

|
||||
|
||||
2. Test if you can access nodes in the proxied subnet:
|
||||
|
||||
```bash
|
||||
# Test connectivity to proxied subnet
|
||||
ping 10.1.1.2
|
||||
```
|
||||
|
||||
#### WireGuard Integration
|
||||
|
||||
EasyTier can act as a WireGuard server, allowing any device with a WireGuard client (including iOS and Android) to access the EasyTier network. Here's an example setup:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
ios[[iPhone<br/>WireGuard Installed]]
|
||||
|
||||
subgraph Node A Public IP 22.1.1.1
|
||||
nodea[EasyTier<br/>10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier<br/>10.144.144.2]
|
||||
end
|
||||
|
||||
id1[[10.1.1.0/24]]
|
||||
@@ -217,80 +249,78 @@ id1[[10.1.1.0/24]]
|
||||
ios <-.-> nodea <--> nodeb <-.-> id1
|
||||
```
|
||||
|
||||
To enable an iPhone to access the EasyTier network through Node A, the following configuration can be applied:
|
||||
1. Start EasyTier with WireGuard portal enabled:
|
||||
|
||||
Include the --vpn-portal parameter in the easytier-core command on Node A to specify the port that the WireGuard service listens on and the subnet used by the WireGuard network.
|
||||
|
||||
```
|
||||
# The following parameters mean: listen on port 0.0.0.0:11013, and use the 10.14.14.0/24 subnet for WireGuard
|
||||
sudo easytier-core --ipv4 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
|
||||
```bash
|
||||
# Listen on 0.0.0.0:11013 and use 10.14.14.0/24 subnet for WireGuard clients
|
||||
sudo easytier-core -i 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
|
||||
```
|
||||
|
||||
After successfully starting easytier-core, use easytier-cli to obtain the WireGuard client configuration.
|
||||
2. Get WireGuard client configuration:
|
||||
|
||||
```
|
||||
$> easytier-cli vpn-portal
|
||||
portal_name: wireguard
|
||||
|
||||
############### client_config_start ###############
|
||||
|
||||
[Interface]
|
||||
PrivateKey = 9VDvlaIC9XHUvRuE06hD2CEDrtGF+0lDthgr9SZfIho=
|
||||
Address = 10.14.14.0/32 # should assign an ip from this cidr manually
|
||||
|
||||
[Peer]
|
||||
PublicKey = zhrZQg4QdPZs8CajT3r4fmzcNsWpBL9ImQCUsnlXyGM=
|
||||
AllowedIPs = 10.144.144.0/24,10.14.14.0/24
|
||||
Endpoint = 0.0.0.0:11013 # should be the public ip(or domain) of the vpn server
|
||||
PersistentKeepalive = 25
|
||||
|
||||
############### client_config_end ###############
|
||||
|
||||
connected_clients:
|
||||
[]
|
||||
```bash
|
||||
# Get WireGuard client configuration
|
||||
easytier-cli vpn-portal
|
||||
```
|
||||
|
||||
Before using the Client Config, you need to modify the Interface Address and Peer Endpoint to the client's IP and the IP of the EasyTier node, respectively. Import the configuration file into the WireGuard client to access the EasyTier network.
|
||||
3. In the output configuration:
|
||||
- Set `Interface.Address` to an available IP from the WireGuard subnet
|
||||
- Set `Peer.Endpoint` to the public IP/domain of your EasyTier node
|
||||
- Import the modified configuration into your WireGuard client
|
||||
|
||||
# Self-Hosted Public Server
|
||||
#### Self-Hosted Public Shared Node
|
||||
|
||||
Each node can act as a relay node for other users' networks. Simply start EasyTier without any parameters.
|
||||
|
||||
### Configurations
|
||||
|
||||
You can use ``easytier-core --help`` to view all configuration items
|
||||
|
||||
|
||||
# Roadmap
|
||||
|
||||
- [ ] Improve documentation and user guides.
|
||||
- [ ] Support features such as encryption, TCP hole punching, etc.
|
||||
- [ ] Support Android, IOS and other mobile platforms.
|
||||
- [ ] Support Web configuration management.
|
||||
|
||||
# Community and Contribution
|
||||
|
||||
We welcome and encourage community contributions! If you want to get involved, please submit a [GitHub PR](https://github.com/EasyTier/EasyTier/pulls). Detailed contribution guidelines can be found in [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md).
|
||||
|
||||
# Related Projects and Resources
|
||||
|
||||
- [ZeroTier](https://www.zerotier.com/): A global virtual network for connecting devices.
|
||||
- [TailScale](https://tailscale.com/): A VPN solution aimed at simplifying network configuration.
|
||||
- [vpncloud](https://github.com/dswd/vpncloud): A P2P Mesh VPN
|
||||
- [Candy](https://github.com/lanthora/candy): A reliable, low-latency, and anti-censorship virtual private network
|
||||
|
||||
# License
|
||||
|
||||
EasyTier is released under the [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE).
|
||||
|
||||
# Contact
|
||||
|
||||
- Ask questions or report problems: [GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
|
||||
- Discussion and exchange: [GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
|
||||
- Telegram:https://t.me/easytier
|
||||
- QQ Group: 949700262
|
||||
You can run your own public shared node to help other nodes discover each other. A public shared node is just a regular EasyTier network (with same network name and secret) that other networks can connect to.
|
||||
|
||||
# Sponsor
|
||||
To run a public shared node:
|
||||
|
||||
<img src="assets/image-8.png" width="300">
|
||||
<img src="assets/image-9.png" width="300">
|
||||
```bash
|
||||
# No need to specify IPv4 address for public shared nodes
|
||||
sudo easytier-core --network-name mysharednode --network-secret mysharednode
|
||||
```
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [ZeroTier](https://www.zerotier.com/): A global virtual network for connecting devices.
|
||||
- [TailScale](https://tailscale.com/): A VPN solution aimed at simplifying network configuration.
|
||||
|
||||
### Contact Us
|
||||
|
||||
- 💬 **[Telegram Group](https://t.me/easytier)**
|
||||
- 👥 **[QQ Group]**
|
||||
- No.1 [949700262](https://qm.qq.com/q/wFoTUChqZW)
|
||||
- No.2 [837676408](https://qm.qq.com/q/4V33DrfgHe)
|
||||
- No.3 [957189589](https://qm.qq.com/q/YNyTQjwlai)
|
||||
|
||||
## License
|
||||
|
||||
EasyTier is released under the [LGPL-3.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE).
|
||||
|
||||
## Sponsor
|
||||
|
||||
CDN acceleration and security protection for this project are sponsored by Tencent EdgeOne.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://edgeone.ai/?from=github" target="_blank">
|
||||
<img src="assets/edgeone.png" width="200" alt="EdgeOne Logo">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Special thanks to [Langlang Cloud](https://langlangy.cn/?i26c5a5) and [RainCloud](https://www.rainyun.com/NjM0NzQ1_) for sponsoring our public servers.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
|
||||
<img src="assets/langlang.png" width="200">
|
||||
</a>
|
||||
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
|
||||
<img src="assets/raincloud.png" width="200">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
If you find EasyTier helpful, please consider sponsoring us. Software development and maintenance require a lot of time and effort, and your sponsorship will help us better maintain and improve EasyTier.
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/wechat.png" width="200">
|
||||
<img src="assets/alipay.png" width="200">
|
||||
</p>
|
||||
|
||||
@@ -1,218 +1,245 @@
|
||||
# EasyTier
|
||||
|
||||
[](https://github.com/EasyTier/EasyTier/releases)
|
||||
[](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
|
||||
[](https://github.com/EasyTier/EasyTier/commits/main)
|
||||
[](https://github.com/EasyTier/EasyTier/issues)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/test.yml)
|
||||
[](https://deepwiki.com/EasyTier/EasyTier)
|
||||
|
||||
[简体中文](/README_CN.md) | [English](/README.md)
|
||||
|
||||
**请访问 [EasyTier 官网](https://www.easytier.top/) 以查看完整的文档。**
|
||||
|
||||
一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
|
||||
> ✨ 一个由 Rust 和 Tokio 驱动的简单、安全、去中心化的异地组网方案
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/image-6.png" width="300">
|
||||
<img src="assets/image-7.png" width="300">
|
||||
<img src="assets/config-page.png" width="300" alt="配置页面">
|
||||
<img src="assets/running-page.png" width="300" alt="运行页面">
|
||||
</p>
|
||||
|
||||
## 特点
|
||||
📚 **[完整文档](https://easytier.cn)** | 🖥️ **[Web 控制台](https://easytier.cn/web)** | 📝 **[下载发布版本](https://github.com/EasyTier/EasyTier/releases)** | 🧩 **[第三方工具](https://easytier.cn/guide/installation_gui.html#%E7%AC%AC%E4%B8%89%E6%96%B9%E5%9B%BE%E5%BD%A2%E7%95%8C%E9%9D%A2)** | ❤️ **[赞助](#赞助)**
|
||||
|
||||
- **去中心化**:无需依赖中心化服务,节点平等且独立。
|
||||
- **安全**:支持利用 WireGuard 加密通信,也支持 AES-GCM 加密保护中转流量。
|
||||
- **高性能**:全链路零拷贝,性能与主流组网软件相当。
|
||||
- **跨平台**:支持 MacOS/Linux/Windows,未来将支持 IOS 和 Android。可执行文件静态链接,部署简单。
|
||||
- **无公网 IP 组网**:支持利用共享的公网节点组网,可参考 [配置指南](#无公网IP组网)
|
||||
- **NAT 穿透**:支持基于 UDP 的 NAT 穿透,即使在复杂的网络环境下也能建立稳定的连接。
|
||||
- **子网代理(点对网)**:节点可以将可访问的网段作为代理暴露给 VPN 子网,允许其他节点通过该节点访问这些子网。
|
||||
- **智能路由**:根据流量智能选择链路,减少延迟,提高吞吐量。
|
||||
- **TCP 支持**:在 UDP 受限的情况下,通过并发 TCP 链接提供可靠的数据传输,优化性能。
|
||||
- **高可用性**:支持多路径和在检测到高丢包率或网络错误时切换到健康路径。
|
||||
- **IPV6 支持**:支持利用 IPV6 组网。
|
||||
- **多协议类型**: 支持使用 WebSocket、QUIC 等协议进行节点间通信。
|
||||
## 特性
|
||||
|
||||
## 安装
|
||||
### 核心特性
|
||||
|
||||
1. **下载预编译的二进制文件**
|
||||
- 🔒 **去中心化**:节点平等且独立,无需中心化服务
|
||||
- 🚀 **易于使用**:支持通过网页、客户端和命令行多种操作方式
|
||||
- 🌍 **跨平台**:支持 Win/MacOS/Linux/FreeBSD/Android 和 X86/ARM/MIPS 架构
|
||||
- 🔐 **安全**:AES-GCM 或 WireGuard 加密,防止中间人攻击
|
||||
|
||||
访问 [GitHub Release 页面](https://github.com/EasyTier/EasyTier/releases) 下载适用于您操作系统的二进制文件。Release 压缩包中同时包含命令行程序和图形界面程序。
|
||||
### 高级功能
|
||||
|
||||
2. **通过 crates.io 安装**
|
||||
```sh
|
||||
cargo install easytier
|
||||
```
|
||||
- 🔌 **高效 NAT 穿透**:支持 UDP 和 IPv6 穿透,可在 NAT4-NAT4 网络中工作
|
||||
- 🌐 **子网代理**:节点可以共享子网供其他节点访问
|
||||
- 🔄 **智能路由**:延迟优先和自动路由选择,提供最佳网络体验
|
||||
- ⚡ **高性能**:整个链路零拷贝,支持 TCP/UDP/WSS/WG 协议
|
||||
|
||||
### 网络优化
|
||||
|
||||
3. **通过源码安装**
|
||||
```sh
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git
|
||||
```
|
||||
|
||||
4. **通过Docker Compose安装**
|
||||
|
||||
请访问 [EasyTier 官网](https://www.easytier.top/) 以查看完整的文档。
|
||||
|
||||
|
||||
5. **使用一键脚本安装 (仅适用于 Linux)**
|
||||
```sh
|
||||
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/easytier.sh" && bash /tmp/easytier.sh install
|
||||
```
|
||||
|
||||
使用本脚本安装的 Easytier 可以使用脚本的 uninstall/update 对其卸载/升级
|
||||
- 📊 **UDP 丢包抗性**:KCP/QUIC 代理在高丢包环境下优化延迟和带宽
|
||||
- 🔧 **Web 管理**:通过 Web 界面轻松配置和监控
|
||||
- 🛠️ **零配置**:静态链接的可执行文件,简单部署
|
||||
|
||||
## 快速开始
|
||||
|
||||
> 下文仅描述命令行工具的使用,图形界面程序可参考下述概念自行配置。
|
||||
### 📥 安装
|
||||
|
||||
确保已按照 [安装指南](#安装) 安装 EasyTier,并且 easytier-core 和 easytier-cli 两个命令都已经可用。
|
||||
选择最适合您需求的安装方式:
|
||||
|
||||
### 双节点组网
|
||||
Linux(推荐):
|
||||
```bash
|
||||
curl -fsSL "https://github.com/EasyTier/EasyTier/blob/main/script/install.sh?raw=true" | sudo bash -s install
|
||||
```
|
||||
|
||||
假设双节点的网络拓扑如下
|
||||
Homebrew(MacOS/Linux):
|
||||
```bash
|
||||
brew tap brewforge/chinese
|
||||
brew install --cask easytier-gui
|
||||
```
|
||||
|
||||
Windows(推荐,请以管理员权限运行):
|
||||
```powershell
|
||||
irm "https://github.com/EasyTier/EasyTier/blob/main/script/install.ps1?raw=true" | iex
|
||||
```
|
||||
|
||||
通过 cargo 安装(最新开发版本):
|
||||
```bash
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
```
|
||||
|
||||
[下载预编译文件](https://github.com/EasyTier/EasyTier/releases)(推荐,支持所有平台)
|
||||
|
||||
[通过 Docker 安装](https://easytier.cn/guide/installation.html#%E5%AE%89%E8%A3%85%E6%96%B9%E5%BC%8F)
|
||||
|
||||
[安装 OpenWrt ipk 软件包](https://github.com/EasyTier/luci-app-easytier)
|
||||
|
||||
附加步骤:
|
||||
|
||||
[一键注册系统服务](https://easytier.cn/guide/network/oneclick-install-as-service.html)(系统启动时自动后台运行)
|
||||
|
||||
### 🚀 基本用法
|
||||
|
||||
#### 使用共享节点快速组网
|
||||
|
||||
EasyTier 支持使用共享节点快速组网。当您没有公网 IP 时,可以使用公共共享节点。节点会自动尝试 NAT 穿透并建立 P2P 连接。当 P2P 失败时,数据将通过共享节点中继。
|
||||
|
||||
使用共享节点时,每个进入网络的节点需要提供相同的 `--network-name` 和 `--network-secret` 参数作为网络的唯一标识符。
|
||||
|
||||
以两个节点为例(请使用更复杂的网络名称以避免冲突):
|
||||
|
||||
1. 在节点 A 上运行:
|
||||
|
||||
```bash
|
||||
# 以管理员权限运行
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<共享节点IP>:11010
|
||||
```
|
||||
|
||||
2. 在节点 B 上运行:
|
||||
|
||||
```bash
|
||||
# 以管理员权限运行
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<共享节点IP>:11010
|
||||
```
|
||||
|
||||
执行成功后,可以使用 `easytier-cli` 检查网络状态:
|
||||
|
||||
```text
|
||||
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
|
||||
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.2-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.6.2-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.2-70e69a38~ |
|
||||
```
|
||||
|
||||
您可以测试节点之间的连通性:
|
||||
|
||||
```bash
|
||||
# 测试连通性
|
||||
ping 10.126.126.1
|
||||
ping 10.126.126.2
|
||||
```
|
||||
|
||||
注意:如果无法 ping 通,可能是防火墙阻止了入站流量。请关闭防火墙或添加允许规则。
|
||||
|
||||
为了提高可用性,您可以同时连接多个共享节点:
|
||||
|
||||
```bash
|
||||
# 连接多个共享节点
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<公共节点IP>:11010 -p udp://<公共节点IP>:11010
|
||||
```
|
||||
|
||||
#### 去中心化组网
|
||||
|
||||
EasyTier 本质上是去中心化的,没有服务器和客户端的区分。只要一个设备能与虚拟网络中的任何节点通信,它就可以加入虚拟网络。以下是如何设置去中心化网络:
|
||||
|
||||
1. 启动第一个节点(节点 A):
|
||||
|
||||
```bash
|
||||
# 启动第一个节点
|
||||
sudo easytier-core -i 10.144.144.1
|
||||
```
|
||||
|
||||
启动后,该节点将默认监听以下端口:
|
||||
- TCP:11010
|
||||
- UDP:11010
|
||||
- WebSocket:11011
|
||||
- WebSocket SSL:11012
|
||||
- WireGuard:11013
|
||||
|
||||
2. 连接第二个节点(节点 B):
|
||||
|
||||
```bash
|
||||
# 使用第一个节点的公网 IP 连接
|
||||
sudo easytier-core -i 10.144.144.2 -p udp://第一个节点的公网IP:11010
|
||||
```
|
||||
|
||||
3. 验证连接:
|
||||
|
||||
```bash
|
||||
# 测试连通性
|
||||
ping 10.144.144.2
|
||||
|
||||
# 查看已连接的对等节点
|
||||
easytier-cli peer
|
||||
|
||||
# 查看路由信息
|
||||
easytier-cli route
|
||||
|
||||
# 查看本地节点信息
|
||||
easytier-cli node
|
||||
```
|
||||
|
||||
更多节点要加入网络,可以使用 `-p` 参数连接到网络中的任何现有节点:
|
||||
|
||||
```bash
|
||||
# 使用任何现有节点的公网 IP 连接
|
||||
sudo easytier-core -i 10.144.144.3 -p udp://任何现有节点的公网IP:11010
|
||||
```
|
||||
|
||||
### 🔍 高级功能
|
||||
|
||||
#### 子网代理
|
||||
|
||||
假设网络拓扑如下,节点 B 想要与其他节点共享其可访问的子网 10.1.1.0/24:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph 节点 A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
subgraph 节点 A 公网 IP 22.1.1.1
|
||||
nodea[EasyTier<br/>10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph 节点 B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
end
|
||||
|
||||
nodea <-----> nodeb
|
||||
|
||||
```
|
||||
|
||||
1. 在节点 A 上执行:
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.1
|
||||
```
|
||||
命令执行成功会有如下打印。
|
||||
|
||||

|
||||
|
||||
2. 在节点 B 执行
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
3. 测试联通性
|
||||
|
||||
两个节点应成功连接并能够在虚拟子网内通信
|
||||
```sh
|
||||
ping 10.144.144.2
|
||||
```
|
||||
|
||||
使用 easytier-cli 查看子网中的节点信息
|
||||
```sh
|
||||
easytier-cli peer
|
||||
```
|
||||

|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 多节点组网
|
||||
|
||||
基于刚才的双节点组网例子,如果有更多的节点需要加入虚拟网络,可以使用如下命令。
|
||||
|
||||
```
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
其中 `--peers ` 参数可以填写任意一个已经在虚拟网络中的节点的监听地址。
|
||||
|
||||
---
|
||||
|
||||
### 子网代理(点对网)配置
|
||||
|
||||
假设网络拓扑如下,节点 B 想将其可访问的子网 10.1.1.0/24 共享给其他节点。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph 节点 A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph 节点 B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
nodeb[EasyTier<br/>10.144.144.2]
|
||||
end
|
||||
|
||||
id1[[10.1.1.0/24]]
|
||||
|
||||
nodea <--> nodeb <-.-> id1
|
||||
|
||||
```
|
||||
|
||||
则节点 B 的 easytier 启动参数为(新增 -n 参数)
|
||||
要共享子网,在启动 EasyTier 时添加 `-n` 参数:
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
|
||||
```bash
|
||||
# 与其他节点共享子网 10.1.1.0/24
|
||||
sudo easytier-core -i 10.144.144.2 -n 10.1.1.0/24
|
||||
```
|
||||
|
||||
子网代理信息会自动同步到虚拟网络的每个节点,各个节点会自动配置相应的路由,节点 A 可以通过如下命令检查子网代理是否生效。
|
||||
子网代理信息将自动同步到虚拟网络中的每个节点,每个节点将自动配置相应的路由。您可以验证子网代理设置:
|
||||
|
||||
1. 检查路由信息是否已经同步,proxy_cidrs 列展示了被代理的子网。
|
||||
1. 检查路由信息是否已同步(proxy_cidrs 列显示代理的子网):
|
||||
|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||

|
||||
|
||||
2. 测试节点 A 是否可访问被代理子网下的节点
|
||||
|
||||
```sh
|
||||
ping 10.1.1.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 无公网IP组网
|
||||
|
||||
EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://easytier.public.kkrainbow.top:11010``。
|
||||
|
||||
使用共享节点时,需要每个入网节点提供相同的 ``--network-name`` 和 ``--network-secret`` 参数,作为网络的唯一标识。
|
||||
|
||||
以双节点为例,节点 A 执行:
|
||||
|
||||
```sh
|
||||
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -e tcp://easytier.public.kkrainbow.top:11010
|
||||
```bash
|
||||
# 查看路由信息
|
||||
easytier-cli route
|
||||
```
|
||||
|
||||
节点 B 执行
|
||||

|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://easytier.public.kkrainbow.top:11010
|
||||
2. 测试是否可以访问代理子网中的节点:
|
||||
|
||||
```bash
|
||||
# 测试到代理子网的连通性
|
||||
ping 10.1.1.2
|
||||
```
|
||||
|
||||
命令执行成功后,节点 A 即可通过虚拟 IP 10.144.144.2 访问节点 B。
|
||||
#### WireGuard 集成
|
||||
|
||||
---
|
||||
|
||||
### 使用 WireGuard 客户端接入
|
||||
|
||||
EasyTier 可以用作 WireGuard 服务端,让任意安装了 WireGuard 客户端的设备访问 EasyTier 网络。对于目前 EasyTier 不支持的平台 (如 iOS、Android 等),可以使用这种方式接入 EasyTier 网络。
|
||||
|
||||
假设网络拓扑如下:
|
||||
EasyTier 可以作为 WireGuard 服务器,允许任何安装了 WireGuard 客户端的设备(包括 iOS 和 Android)访问 EasyTier 网络。以下是设置示例:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
ios[[iPhone \n 安装 WireGuard]]
|
||||
ios[[iPhone<br/>已安装 WireGuard]]
|
||||
|
||||
subgraph 节点 A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
subgraph 节点 A 公网 IP 22.1.1.1
|
||||
nodea[EasyTier<br/>10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph 节点 B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
nodeb[EasyTier<br/>10.144.144.2]
|
||||
end
|
||||
|
||||
id1[[10.1.1.0/24]]
|
||||
@@ -220,82 +247,79 @@ id1[[10.1.1.0/24]]
|
||||
ios <-.-> nodea <--> nodeb <-.-> id1
|
||||
```
|
||||
|
||||
我们需要 iPhone 通过节点 A 访问 EasyTier 网络,则可进行如下配置:
|
||||
1. 启动启用 WireGuard 门户的 EasyTier:
|
||||
|
||||
在节点 A 的 easytier-core 命令中,加入 --vpn-portal 参数,指定 WireGuard 服务监听的端口,以及 WireGuard 网络使用的网段。
|
||||
|
||||
```
|
||||
# 以下参数的含义为: 监听 0.0.0.0:11013 端口,WireGuard 使用 10.14.14.0/24 网段
|
||||
sudo easytier-core --ipv4 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
|
||||
```bash
|
||||
# 在 0.0.0.0:11013 上监听,并使用 10.14.14.0/24 子网作为 WireGuard 客户端
|
||||
sudo easytier-core -i 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
|
||||
```
|
||||
|
||||
easytier-core 启动成功后,使用 easytier-cli 获取 WireGuard Client 的配置。
|
||||
2. 获取 WireGuard 客户端配置:
|
||||
|
||||
```
|
||||
$> easytier-cli vpn-portal
|
||||
portal_name: wireguard
|
||||
|
||||
############### client_config_start ###############
|
||||
|
||||
[Interface]
|
||||
PrivateKey = 9VDvlaIC9XHUvRuE06hD2CEDrtGF+0lDthgr9SZfIho=
|
||||
Address = 10.14.14.0/32 # should assign an ip from this cidr manually
|
||||
|
||||
[Peer]
|
||||
PublicKey = zhrZQg4QdPZs8CajT3r4fmzcNsWpBL9ImQCUsnlXyGM=
|
||||
AllowedIPs = 10.144.144.0/24,10.14.14.0/24
|
||||
Endpoint = 0.0.0.0:11013 # should be the public ip(or domain) of the vpn server
|
||||
PersistentKeepalive = 25
|
||||
|
||||
############### client_config_end ###############
|
||||
|
||||
connected_clients:
|
||||
[]
|
||||
```bash
|
||||
# 获取 WireGuard 客户端配置
|
||||
easytier-cli vpn-portal
|
||||
```
|
||||
|
||||
使用 Client Config 前,需要将 Interface Address 和 Peer Endpoint 分别修改为客户端的 IP 和 EasyTier 节点的 IP。将配置文件导入 WireGuard 客户端,即可访问 EasyTier 网络。
|
||||
3. 在输出配置中:
|
||||
- 将 `Interface.Address` 设置为 WireGuard 子网中的可用 IP
|
||||
- 将 `Peer.Endpoint` 设置为您的 EasyTier 节点的公网 IP/域名
|
||||
- 将修改后的配置导入到您的 WireGuard 客户端
|
||||
|
||||
---
|
||||
#### 自建公共共享节点
|
||||
|
||||
### 自建公共中转服务器
|
||||
您可以运行自己的公共共享节点来帮助其他节点相互发现。公共共享节点只是一个普通的 EasyTier 网络(具有相同的网络名称和密钥),其他网络可以连接到它。
|
||||
|
||||
每个节点都可作为其他用户网络的中转节点。不带任何参数直接启动 EasyTier 即可。
|
||||
要运行公共共享节点:
|
||||
|
||||
### 其他配置
|
||||
```bash
|
||||
# 公共共享节点无需指定 IPv4 地址
|
||||
sudo easytier-core --network-name mysharednode --network-secret mysharednode
|
||||
```
|
||||
|
||||
可使用 ``easytier-core --help`` 查看全部配置项
|
||||
网络设置成功后,您可以轻松配置它以在系统启动时自动启动。请参阅 [一键注册服务指南](https://easytier.cn/en/guide/network/oneclick-install-as-service.html) 了解如何将 EasyTier 注册为系统服务。
|
||||
|
||||
## 相关项目
|
||||
|
||||
# 路线图
|
||||
- [ZeroTier](https://www.zerotier.com/):用于连接设备的全球虚拟网络。
|
||||
- [TailScale](https://tailscale.com/):旨在简化网络配置的 VPN 解决方案。
|
||||
|
||||
- [ ] 完善文档和用户指南。
|
||||
- [ ] 支持 TCP 打洞等特性。
|
||||
- [ ] 支持 Android、IOS 等移动平台。
|
||||
- [ ] 支持 Web 配置管理。
|
||||
### 联系我们
|
||||
|
||||
# 社区和贡献
|
||||
- 💬 **[Telegram 群组](https://t.me/easytier)**
|
||||
- 👥 **QQ 群**
|
||||
- 一群 [949700262](https://qm.qq.com/q/wFoTUChqZW)
|
||||
- 二群 [837676408](https://qm.qq.com/q/4V33DrfgHe)
|
||||
- 三群 [957189589](https://qm.qq.com/q/YNyTQjwlai)
|
||||
|
||||
我们欢迎并鼓励社区贡献!如果你想参与进来,请提交 [GitHub PR](https://github.com/EasyTier/EasyTier/pulls)。详细的贡献指南可以在 [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md) 中找到。
|
||||
## 许可证
|
||||
|
||||
# 相关项目和资源
|
||||
EasyTier 在 [LGPL-3.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE) 许可下发布。
|
||||
|
||||
- [ZeroTier](https://www.zerotier.com/): 一个全球虚拟网络,用于连接设备。
|
||||
- [TailScale](https://tailscale.com/): 一个旨在简化网络配置的 VPN 解决方案。
|
||||
- [vpncloud](https://github.com/dswd/vpncloud): 一个 P2P Mesh VPN
|
||||
- [Candy](https://github.com/lanthora/candy): 可靠、低延迟、抗审查的虚拟专用网络
|
||||
## 赞助
|
||||
|
||||
# 许可证
|
||||
本项目的 CDN 加速和安全防护由腾讯云 EdgeOne 赞助。
|
||||
|
||||
EasyTier 根据 [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE) 许可证发布。
|
||||
<p align="center">
|
||||
<a href="https://edgeone.ai/?from=github" target="_blank">
|
||||
<img src="assets/edgeone.png" width="200">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# 联系方式
|
||||
特别感谢 [浪浪云](https://langlangy.cn/?i26c5a5) 和 [雨云](https://www.rainyun.com/NjM0NzQ1_) 赞助我们的公共服务器。
|
||||
|
||||
- 提问或报告问题:[GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
|
||||
- 讨论和交流:[GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
|
||||
- QQ 群: 949700262
|
||||
- Telegram:https://t.me/easytier
|
||||
<p align="center">
|
||||
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
|
||||
<img src="assets/langlang.png" width="200">
|
||||
</a>
|
||||
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
|
||||
<img src="assets/raincloud.png" width="200">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# 赞助
|
||||
如果您觉得 EasyTier 有帮助,请考虑赞助我们。软件开发和维护需要大量的时间和精力,您的赞助将帮助我们更好地维护和改进 EasyTier。
|
||||
|
||||
<img src="assets/image-8.png" width="300">
|
||||
<img src="assets/image-9.png" width="300">
|
||||
<p align="center">
|
||||
<img src="assets/wechat.png" width="200">
|
||||
<img src="assets/alipay.png" width="200">
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# Android build environment
|
||||
{
|
||||
pkgs,
|
||||
nixpkgs,
|
||||
system,
|
||||
}:
|
||||
|
||||
let
|
||||
androidEnv = pkgs.callPackage "${nixpkgs}/pkgs/development/mobile/androidenv" {
|
||||
inherit pkgs;
|
||||
licenseAccepted = true;
|
||||
};
|
||||
|
||||
includeAuto = pkgs.stdenv.hostPlatform.isx86_64 || pkgs.stdenv.hostPlatform.isDarwin;
|
||||
ndkVersion = "26.1.10909125";
|
||||
ndkVersions = [ ndkVersion ];
|
||||
|
||||
sdkArgs = {
|
||||
includeNDK = true;
|
||||
includeSources = true;
|
||||
includeSystemImages = false;
|
||||
includeEmulator = false;
|
||||
inherit ndkVersions;
|
||||
useGoogleAPIs = true;
|
||||
useGoogleTVAddOns = true;
|
||||
buildToolsVersions = [ "34.0.0" ];
|
||||
numLatestPlatformVersions = 10;
|
||||
includeExtras = [
|
||||
"extras;google;gcm"
|
||||
]
|
||||
++ pkgs.lib.optionals includeAuto [
|
||||
"extras;google;auto"
|
||||
];
|
||||
extraLicenses = [
|
||||
"android-sdk-preview-license"
|
||||
"android-googletv-license"
|
||||
"android-sdk-arm-dbt-license"
|
||||
"google-gdk-license"
|
||||
"intel-android-extra-license"
|
||||
"intel-android-sysimage-license"
|
||||
"mips-android-sysimage-license"
|
||||
];
|
||||
};
|
||||
|
||||
androidComposition = androidEnv.composeAndroidPackages sdkArgs;
|
||||
androidSdk = androidComposition.androidsdk;
|
||||
platformTools = androidComposition.platform-tools;
|
||||
cmake = androidComposition.cmake;
|
||||
ndkHostTag =
|
||||
if pkgs.stdenv.isLinux then
|
||||
"linux-x86_64"
|
||||
else if pkgs.stdenv.isDarwin then
|
||||
"darwin-x86_64"
|
||||
else
|
||||
"";
|
||||
ndkToolchain = "${androidSdk}/libexec/android-sdk/ndk/${ndkVersion}/toolchains/llvm/prebuilt/${ndkHostTag}";
|
||||
in
|
||||
{
|
||||
inherit
|
||||
androidSdk
|
||||
platformTools
|
||||
cmake
|
||||
ndkToolchain
|
||||
ndkVersion
|
||||
;
|
||||
|
||||
# List of packages required for Android development
|
||||
packages = [
|
||||
pkgs.jdk # openjdk 21
|
||||
androidSdk
|
||||
platformTools
|
||||
cmake
|
||||
pkgs.glibc_multi.dev
|
||||
];
|
||||
|
||||
# Provide Rust extensions/targets for use by the upper-level flake
|
||||
rust = {
|
||||
extensions = [ "rust-std" ];
|
||||
targets = [
|
||||
"aarch64-linux-android"
|
||||
"armv7-linux-androideabi"
|
||||
"i686-linux-android"
|
||||
"x86_64-linux-android"
|
||||
];
|
||||
};
|
||||
|
||||
# Android environment variables and shellHook
|
||||
envVars = {
|
||||
LANG = "C.UTF-8";
|
||||
LC_ALL = "C.UTF-8";
|
||||
JAVA_HOME = "${pkgs.jdk}/lib/openjdk";
|
||||
ANDROID_SDK_ROOT = "${androidSdk}/libexec/android-sdk";
|
||||
ANDROID_NDK_ROOT = "\${ANDROID_SDK_ROOT}/ndk-bundle";
|
||||
NDK_HOME = "${androidSdk}/libexec/android-sdk/ndk/${ndkVersion}";
|
||||
LIBCLANG_PATH = "${ndkToolchain}/lib";
|
||||
KCP_SYS_EXTRA_HEADER_PATH = "${ndkToolchain}/lib/clang/19/include:${pkgs.glibc_multi.dev}/include";
|
||||
ZSTD_SYS_STATIC = "1";
|
||||
BINDGEN_EXTRA_CLANG_ARGS = "--sysroot=${ndkToolchain}/sysroot -I${ndkToolchain}/lib/clang/17/include ";
|
||||
|
||||
shellHook = ''
|
||||
echo "Android environment activated"
|
||||
export GRADLE_OPTS="-Dorg.gradle.project.android.aapt2FromMavenOverride=$(echo "$ANDROID_SDK_ROOT/build-tools/"*"/aapt2")"
|
||||
cmake_root="$(echo "$ANDROID_SDK_ROOT/cmake/"*/)"
|
||||
export PATH="$cmake_root/bin:$PATH"
|
||||
|
||||
unset NIX_CFLAGS_COMPILE
|
||||
unset NIX_CFLAGS_COMPILE_FOR_BUILD
|
||||
|
||||
cat <<EOF > easytier-gui/local.properties
|
||||
sdk.dir=$ANDROID_SDK_ROOT
|
||||
ndk.dir=$ANDROID_NDK_ROOT
|
||||
cmake.dir=$cmake_root
|
||||
EOF
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,529 @@
|
||||
# EasyTier Core Architecture
|
||||
|
||||
## Status and scope
|
||||
|
||||
This document describes the current architecture after the portable-core
|
||||
refactor. It is the source of truth for ownership, dependency direction,
|
||||
feature boundaries, and validation. It intentionally records the resulting
|
||||
design rather than the migration history.
|
||||
|
||||
The refactor has three principal crate roles:
|
||||
|
||||
- `easytier-core` owns portable EasyTier configuration, protocol state,
|
||||
routing, peer state, connectivity orchestration, packet processing, and
|
||||
instance lifecycle.
|
||||
- `easytier` is the native composition root. It owns operating-system
|
||||
resources, native protocol engines, process integration, CLI and native
|
||||
presentation.
|
||||
- `easytier-proto` owns generated protobuf and RPC types, descriptor data, and
|
||||
the feature slices needed by core and presentation users.
|
||||
|
||||
`easytier-core` is designed to compile without direct operating-system network
|
||||
access. It supports native hosts through Rust traits and has a target-only WASI
|
||||
adapter and ABI implementation under `easytier-core/src/wasi`.
|
||||
|
||||
This architecture does not require compatibility with old internal module
|
||||
paths. Wire compatibility, configuration compatibility, management semantics,
|
||||
and externally used application behaviour remain compatibility requirements.
|
||||
|
||||
## Architectural vocabulary
|
||||
|
||||
The following terms have specific meanings in this document:
|
||||
|
||||
- **Module**: an interface and the implementation hidden behind it.
|
||||
- **Host**: the process or runtime embedding core and owning platform
|
||||
resources.
|
||||
- **Host capability**: an operation core may request but must not implement
|
||||
with direct OS calls.
|
||||
- **Adapter**: a concrete implementation of a Host capability or protocol
|
||||
extension.
|
||||
- **Composition root**: code that creates core configuration, Host Adapters,
|
||||
instances, and process-level services.
|
||||
- **Runtime configuration**: the authoritative normalized state used after an
|
||||
instance starts.
|
||||
- **Packet plane**: portable packet classification, routing, transformation,
|
||||
proxy/NAT state, and forwarding decisions.
|
||||
|
||||
New abstractions should pass a deletion test: deleting a useful deep Module
|
||||
should force non-trivial policy or lifecycle logic to reappear in multiple
|
||||
callers. A pass-through wrapper with no independent invariant is not an
|
||||
architectural boundary.
|
||||
|
||||
## Crate dependency direction
|
||||
|
||||
The principal dependency direction is:
|
||||
|
||||
```text
|
||||
easytier-proto <- easytier-core <- easytier
|
||||
```
|
||||
|
||||
Presentation crates and platform integrations consume these crates. Portable
|
||||
policy must not move outward merely because one current consumer is native.
|
||||
Conversely, core must not absorb an OS mechanism or a protocol engine whose
|
||||
dependencies cannot satisfy the core target contract.
|
||||
|
||||
### `easytier-proto`
|
||||
|
||||
The protobuf crate is split by public Cargo features:
|
||||
|
||||
- `core` provides the common wire messages, peer RPC messages, generated RPC
|
||||
runtime, and descriptor bytes needed by core.
|
||||
- `api` adds management API messages.
|
||||
- protocol-specific features add only their generated message modules.
|
||||
- `json-rpc` enables the well-known protobuf JSON types used by the management
|
||||
plane.
|
||||
- `full` is the compatibility aggregate used by complete products.
|
||||
|
||||
The core crate depends on `easytier-proto` with default features disabled and
|
||||
enables only `core`, adding API or JSON-RPC types through its own management
|
||||
features.
|
||||
|
||||
The main core/native path has no `prost-reflect` dependency. OSPF route
|
||||
reflection uses the focused wire editor in
|
||||
`peers/route/route_peer_wire.rs`. It retains the original encoded
|
||||
`RoutePeerInfo`, replaces only the fields credential filtering is allowed to
|
||||
change, and leaves all other top-level and nested fields intact. This is
|
||||
required so unknown fields survive mixed-version, multi-hop propagation.
|
||||
Generated Rust types remain responsible for normal message construction and
|
||||
validation.
|
||||
|
||||
Descriptor sets are still generated and embedded by `easytier-proto`; removing
|
||||
runtime reflection did not remove descriptor data used by configuration and
|
||||
RPC tooling. The OHOS integration has its own schema service and dependency
|
||||
policy and is outside this replacement.
|
||||
|
||||
### `easytier-core`
|
||||
|
||||
Core owns portable behaviour and exposes capability seams. Its normal
|
||||
dependencies use Tokio runtime, time, synchronization, and I/O traits without
|
||||
requiring the full Tokio feature set.
|
||||
|
||||
Core may depend on optional portable engines when their owning feature is
|
||||
enabled. It does not create real native TCP/UDP sockets, alter routes, open a
|
||||
TUN device, enter a network namespace, configure system DNS, manage a service,
|
||||
or invoke UPnP/NAT-PMP directly.
|
||||
|
||||
### `easytier`
|
||||
|
||||
The native crate owns:
|
||||
|
||||
- process startup, shutdown, signals, service management, and allocators;
|
||||
- filesystem configuration input and persistence;
|
||||
- real TCP/UDP, DNS, TUN, raw-socket, route, interface, namespace, and socket
|
||||
option operations;
|
||||
- UPnP and NAT-PMP operations;
|
||||
- Unix and FakeTCP resources;
|
||||
- WebSocket/WSS, QUIC, WireGuard, and KCP concrete engines;
|
||||
- native Magic DNS serving and system DNS integration;
|
||||
- CLI, web, GUI, FFI, and native management presentation.
|
||||
|
||||
Native code may translate values and assemble Adapters. It must not maintain a
|
||||
second peer graph, reproduce core routing or hole-punch policy, or invent an
|
||||
alternative instance lifecycle.
|
||||
|
||||
## Internal core layers
|
||||
|
||||
The physical module layout follows this downward order:
|
||||
|
||||
```text
|
||||
foundation
|
||||
<- config / packet
|
||||
<- socket
|
||||
<- host
|
||||
<- tunnel
|
||||
<- listener / connectivity
|
||||
<- peers / rpc
|
||||
<- gateway
|
||||
<- instance
|
||||
<- management
|
||||
```
|
||||
|
||||
`process_runtime` is a process- or module-scoped owner shared by instances.
|
||||
`wasi` is target integration and is compiled only for tests or the WASI target;
|
||||
it is not an additional portable domain layer.
|
||||
|
||||
### Foundation
|
||||
|
||||
`foundation/` contains task supervision, the time facade, rate limiting,
|
||||
statistics primitives, and the domain-neutral external operation broker. The
|
||||
broker owns asynchronous operation lifecycle and completion storage while the
|
||||
calling domain owns operation kinds, outcomes, resources, and errors.
|
||||
Foundation must not depend on a domain layer.
|
||||
|
||||
### Configuration and packets
|
||||
|
||||
`config/` owns:
|
||||
|
||||
- the complete `TomlConfig` model;
|
||||
- parsing, serialization, and validation;
|
||||
- OS-independent defaults;
|
||||
- peer, encryption, gateway, and API input models;
|
||||
- normalized runtime snapshots and the live runtime configuration store.
|
||||
|
||||
The Host supplies platform facts through `CoreInstanceHostConfig`. Core applies
|
||||
the policy that combines those facts with TOML input. This is especially
|
||||
important for a WASI build: the compile-time guest target cannot be used as a
|
||||
proxy for the Host operating system.
|
||||
|
||||
`packet/` owns EasyTier packet structures, compression, STUN and hole-punch
|
||||
wire codecs. It does not own socket I/O or connection policy.
|
||||
|
||||
### Socket and Host seams
|
||||
|
||||
`socket/` contains transport-neutral primitives:
|
||||
|
||||
- `SocketContext`, including IP-family policy, optional socket mark, and an
|
||||
opaque network-namespace token;
|
||||
- virtual TCP socket, listener, and factory traits;
|
||||
- virtual UDP socket and factory traits;
|
||||
- UDP session multiplexing, classification, and lifecycle;
|
||||
- in-process Ring sockets.
|
||||
|
||||
`host/` is the single home of Host capability seams:
|
||||
|
||||
- DNS and DNS record resolution;
|
||||
- connector environment observations;
|
||||
- packet ingress and egress;
|
||||
- Host socket operation bridges and handle-based TCP/UDP/listener adapters.
|
||||
|
||||
Core owns scheduling, backpressure, cancellation, UDP session state, and
|
||||
protocol state even when each actual operation crosses a Host Adapter. A Host
|
||||
Adapter owns the real resource and performs the OS operation.
|
||||
|
||||
The native `NativeHostRuntime` is process-wide and does not retain an instance
|
||||
`GlobalCtx`, namespace guard, socket mark, or connectivity state. Differences
|
||||
between instances travel in each request's `SocketContext`. A narrow
|
||||
instance-host projection may expose listener and interface facts, but it does
|
||||
not become another socket factory.
|
||||
|
||||
### Tunnel and listener
|
||||
|
||||
A socket is a raw communication endpoint. A Tunnel is an EasyTier connection
|
||||
created by adding framing, metadata, handshakes, and protocol lifecycle.
|
||||
|
||||
Core owns:
|
||||
|
||||
- raw TCP framing and upgrade;
|
||||
- UDP tunnel/session framing and classification;
|
||||
- Ring Tunnel identity and registry state;
|
||||
- encryption and secure-datagram policy that is portable;
|
||||
- client/server protocol selection interfaces;
|
||||
- listener planning, optional/required listener policy, retry, accept
|
||||
scheduling, running-listener registry, and orderly shutdown.
|
||||
|
||||
Native protocol Adapters own WebSocket/WSS, QUIC, WireGuard, and KCP engines.
|
||||
Unix and FakeTCP are socket resources that feed a core protocol upgrader; they
|
||||
are not independent owners of EasyTier peer state.
|
||||
|
||||
Each protocol registration must provide a coherent client/server Adapter.
|
||||
Unavailable configured transports must be rejected during validation or
|
||||
protocol selection in the standard runtime, rather than silently falling back
|
||||
to another transport. A compact compatibility Host may instead retain the
|
||||
desired value for management readback and omit it from normalized runtime
|
||||
state; it must not advertise or partially activate the unavailable transport.
|
||||
|
||||
### Connectivity
|
||||
|
||||
`connectivity/` owns:
|
||||
|
||||
- manual connection and endpoint discovery policy;
|
||||
- direct candidate selection;
|
||||
- retry, backoff, blacklists, and listener reuse;
|
||||
- STUN requests, responses, probing, NAT inference, and published endpoint
|
||||
state;
|
||||
- TCP and UDP hole-punch state machines;
|
||||
- UDP port-mapping policy and lease lifecycle;
|
||||
- conversion of successful sockets into protocol-upgrade requests.
|
||||
|
||||
The Host owns DNS execution, socket syscalls, interface enumeration, bind
|
||||
device/mark/namespace operations, and concrete UPnP/NAT-PMP calls. STUN-only
|
||||
hole punching remains available when the Host does not supply a port-mapping
|
||||
Adapter.
|
||||
|
||||
Some connectivity files intentionally implement peer-facing adapter traits for
|
||||
`PeerManagerCore`. These are localized integration edges between adjacent
|
||||
domains, not permission for lower socket or Host layers to depend on peers.
|
||||
|
||||
### Peers and RPC
|
||||
|
||||
`peers/` is the authoritative owner of:
|
||||
|
||||
- admission and connection sessions;
|
||||
- peer maps and connection lifecycle;
|
||||
- ACL and whitelist decisions;
|
||||
- OSPF route calculation and graph algorithms;
|
||||
- peer and credential RPC registration;
|
||||
- foreign-network admission, identity, relay, and lifecycle;
|
||||
- peer-center state and public IPv6 policy;
|
||||
- traffic metrics and peer snapshots.
|
||||
|
||||
Submodules progress from kernel types and utilities, through ACL/context,
|
||||
connection state, route state, manager services, and finally foreign-network
|
||||
and peer-center composition. Callers consume the public surface declared by
|
||||
the domain rather than reaching into a parallel native peer owner.
|
||||
|
||||
`rpc/` owns the peer-flavoured RPC transport, packet fragmentation, client and
|
||||
server lifecycle, handler registry, and standalone listener/client lifecycle.
|
||||
Generated service descriptors and message types remain in `easytier-proto`.
|
||||
|
||||
### Gateway
|
||||
|
||||
`gateway/` owns portable packet-plane features:
|
||||
|
||||
- proxy CIDR state and monitoring policy;
|
||||
- packet parsing, reassembly, NAT/proxy state, and TCP/UDP/ICMP decisions;
|
||||
- the smoltcp-backed portable dataplane selected by its feature;
|
||||
- SOCKS5 framing, authentication, association, routing, and session state;
|
||||
- wrapped-transport planning and session state used by KCP and QUIC Adapters;
|
||||
- DHCP allocation policy;
|
||||
- Magic DNS route and response policy;
|
||||
- VPN portal client/session policy;
|
||||
- UDP broadcast classification and rewrite policy.
|
||||
|
||||
TUN, raw sockets, transparent-destination lookup, concrete protocol engines,
|
||||
native DNS servers, namespace operations, and route application stay in native
|
||||
Adapters.
|
||||
|
||||
Optional gateway capabilities are selected by cohesive Modules. Disabled
|
||||
implementations retain stable lifecycle calls and report unsupported
|
||||
configuration in the standard runtime. A compact compatibility Host may
|
||||
silently normalize those settings to no-ops while preserving the desired TOML
|
||||
model; disabled implementations do not duplicate portable policy.
|
||||
|
||||
The instance-scoped `DataPlaneSession` composes the foundation operation broker
|
||||
under the same session lock as its resource and quota state. The broker owns
|
||||
generic completion, cancellation, free, drain, and take transitions. The data
|
||||
plane retains TCP/UDP resource ownership, operation metadata, route deadlines,
|
||||
and error semantics.
|
||||
|
||||
The proposed restructuring of the smoltcp data plane, SOCKS5 and port-forward
|
||||
Adapters, portable KCP engine, event-driven FFI/WASI completion model, and Go
|
||||
Host integration is tracked in
|
||||
[`data-plane-runtime-plan.md`](data-plane-runtime-plan.md). That document is a
|
||||
future implementation plan; this document remains the source of truth for the
|
||||
currently implemented architecture until the plan is completed.
|
||||
|
||||
### Instance and management
|
||||
|
||||
`CoreInstance::new(CoreInstanceConfig, CoreHostAdapters)` is the sole direct
|
||||
construction path for a normalized instance. `CoreInstance::from_toml` uses
|
||||
the same normalization and construction path. Core constructs the peer graph,
|
||||
runtime store, STUN collector, connectivity managers, listener runtime, packet
|
||||
plane, gateway runtimes, and lifecycle owners.
|
||||
|
||||
A core instance:
|
||||
|
||||
- owns all mutable portable state for one network;
|
||||
- is one-shot after `stop`;
|
||||
- exposes one complete `start` and one `stop` lifecycle interface;
|
||||
- starts Modules in a fixed serial composition order without cross-Module
|
||||
started flags or staged activation;
|
||||
- installs initial ACL, proxy CIDR, and manual-peer inputs before startup;
|
||||
- serializes lifecycle operations with one instance-level operation lock;
|
||||
- owns cooperative cancellation and component shutdown order;
|
||||
- exposes `CorePacketPlane` as the narrow packet/route projection used by Host
|
||||
dataplane Adapters;
|
||||
- treats its normalized runtime store as authoritative after construction.
|
||||
|
||||
`CoreHostAdapters` contains the required Host, DNS, packet sink, and
|
||||
`CoreProcessRuntime`, plus optional protocol and platform capabilities. The
|
||||
bundle carries capabilities, not preconstructed portable managers.
|
||||
|
||||
Each Module owns partial-start cleanup for its internal resources.
|
||||
`CoreInstance` has one outer cancellation and recovery path for the complete
|
||||
serial startup. `Running` therefore means the Host runtime and every enabled
|
||||
portable Module have started successfully; there is no separate post-Host
|
||||
activation state. Host packet tasks stop before PeerManager resources are
|
||||
cleared.
|
||||
|
||||
`InstanceManager<F>` is the canonical UUID-indexed instance collection for one
|
||||
Host composition. Its `InstanceFactory` constructs one complete record before
|
||||
the manager performs an atomic uniqueness check. The manager owns collection
|
||||
membership; it does not own startup order, persistence, daemon policy, cached
|
||||
errors, ABI handles, or RPC projections.
|
||||
|
||||
`management/` consumes the canonical manager and instances. It owns:
|
||||
|
||||
- stable UUID/name selection;
|
||||
- read-only instance and peer management RPC;
|
||||
- full process mutation and configuration transactions when enabled;
|
||||
- persistence and logger-control capability interfaces;
|
||||
- management listener/client lifecycle and JSON-RPC presentation.
|
||||
|
||||
There is one process-level management entry. Instances and the manager do not
|
||||
depend on management response projections.
|
||||
|
||||
## Process-scoped state
|
||||
|
||||
`CoreProcessRuntime` owns portable resources shared across instances in one
|
||||
process or instantiated module:
|
||||
|
||||
- the Ring Tunnel registry and namespace;
|
||||
- a reference-counted protected TCP-port registry.
|
||||
|
||||
The composition root creates and shares one runtime. Management listener ports
|
||||
are protected before bind and held by leases after the concrete port is known.
|
||||
Native and target adapters supply bound resources but do not implement a
|
||||
second protected-port registry.
|
||||
|
||||
Process-global capability objects may contain stateless or shared platform
|
||||
mechanisms. They must not contain instance-specific peer, route,
|
||||
configuration, or connectivity state.
|
||||
|
||||
## Runtime configuration authority
|
||||
|
||||
`TomlConfig` is the authoritative desired configuration used for management
|
||||
readback and patch transactions. Compact Hosts keep unsupported accepted values
|
||||
there so controllers observe the configuration they submitted.
|
||||
|
||||
The separately typed, normalized core runtime store is authoritative for live
|
||||
behavior:
|
||||
|
||||
- peer feature flags and routing policy;
|
||||
- listeners and initial peers;
|
||||
- ACL and whitelist inputs;
|
||||
- manual and VPN portal CIDRs;
|
||||
- gateway and connectivity settings;
|
||||
- runtime configuration patches.
|
||||
|
||||
Host persistence is an effect following a successful core transaction. A Host
|
||||
Adapter must not call back into an instance to obtain a hidden configuration
|
||||
snapshot while core is applying an operation.
|
||||
|
||||
Non-serializable resources such as TUN descriptors, packet sinks, execution
|
||||
domains, and native protocol engines are construction context, not TOML
|
||||
fields.
|
||||
|
||||
## Logging
|
||||
|
||||
The main native runtime uses a small logger implemented in
|
||||
`easytier/src/common/log`:
|
||||
|
||||
- `log` records and `tracing` events share console and file sinks;
|
||||
- timestamps, compact formatting, optional terminal colours, `NO_COLOR`, and
|
||||
basic `RUST_LOG` target/level filters are implemented directly;
|
||||
- file rotation uses the existing EasyTier rolling appender;
|
||||
- management RPC can reload the file level;
|
||||
- an atomic maximum-level gate rejects disabled events before target matching
|
||||
or file-filter locking;
|
||||
- concurrent file-level reload serializes the filter and atomic-level update.
|
||||
|
||||
File logging and no-file logging are separate selected backends. The default
|
||||
tracing backend records events and deliberately ignores span trees. The
|
||||
optional `tracing` feature selects the tokio-console subscriber integration;
|
||||
only that diagnostic profile pulls the main crate's `tracing-subscriber` and
|
||||
`console-subscriber` dependencies.
|
||||
|
||||
Contrib applications and platform integrations may have independent logging
|
||||
requirements and are not implicitly wired to the native process logger.
|
||||
|
||||
## Feature model
|
||||
|
||||
Features represent coherent capabilities, not arbitrary source fragments.
|
||||
Important core feature relationships are:
|
||||
|
||||
- `management-rpc` enables generated management API types and read-only
|
||||
management services.
|
||||
- `management` adds configuration writes, full management composition, rich
|
||||
errors, and JSON-RPC.
|
||||
- `proxy-packet` enables portable packet parsing/proxy machinery and the
|
||||
required smoltcp packet features.
|
||||
- `proxy-smoltcp-stack` adds the async TCP/UDP smoltcp stack.
|
||||
- `dns-resolver` is the shared Hickory resolver leaf used by endpoint
|
||||
discovery and Magic DNS without coupling either capability to the other.
|
||||
- `endpoint-discovery` adds HTTPS endpoint discovery dependencies.
|
||||
- `magic-dns` enables its DNS server, management wire messages, and portable
|
||||
packet-query integration.
|
||||
- `tcp-hole-punch` enables the TCP hole-punch runtime.
|
||||
- `dhcp-ipv4`, `public-ipv6-provider`, `vpn-portal`,
|
||||
`wrapped-transport`, and `proxy-cidr-monitor` are independent gateway or
|
||||
platform-policy leaves.
|
||||
- `extended-services` is the compatibility aggregate for those leaves.
|
||||
- encryption and compression engines remain independently selectable.
|
||||
|
||||
The native crate maps product features to the core and protocol features it
|
||||
actually consumes. A protocol feature must not accidentally enable unrelated
|
||||
gateway or management capabilities.
|
||||
|
||||
Production feature and platform selection belongs at Module or Adapter
|
||||
boundaries rather than inside shared implementations. The logger demonstrates
|
||||
the intended pattern: file and tracing variants are complete backend modules
|
||||
with one stable interface, so shared event processing contains no feature
|
||||
branches.
|
||||
|
||||
## Module boundaries
|
||||
|
||||
The dependency directions in this document define the intended module
|
||||
boundaries. Changes that require a new upward edge must first define a stable
|
||||
lower-layer interface or explicitly revise this architecture.
|
||||
|
||||
Modules are `pub(crate)` by default. Each domain's `mod.rs` declares its
|
||||
outward surface. Public visibility is used for real cross-crate Host,
|
||||
configuration, management, packet-plane, or test-support interfaces.
|
||||
|
||||
## Architectural invariants
|
||||
|
||||
1. Portable EasyTier policy has one owner in `easytier-core`.
|
||||
2. Core does not perform real OS socket, DNS, TUN, route, filesystem
|
||||
configuration, process, or service-manager operations.
|
||||
3. Host-OS policy is runtime input; a WASI compile target is not Host policy.
|
||||
4. Every real socket and DNS operation crosses a Host capability seam.
|
||||
5. Core owns socket scheduling, backpressure, protocol state, and cancellation.
|
||||
6. Dial, accept, and hole-punch paths produce sockets before protocol upgrade.
|
||||
7. Peer admission consumes upgraded transports and does not create OS
|
||||
resources.
|
||||
8. Each instance owns its mutable peer, route, connectivity, gateway, and
|
||||
runtime configuration state.
|
||||
9. One Host composition has one canonical UUID-to-instance manager.
|
||||
10. Process-level runtimes do not capture instance state.
|
||||
11. `CoreInstance::new` is the sole normalized direct construction entry.
|
||||
12. The manager owns membership, not lifecycle or presentation.
|
||||
13. Management consumes the manager; the manager does not return management
|
||||
projections.
|
||||
14. Unknown protobuf fields in reflected route information survive forwarding
|
||||
and credential filtering.
|
||||
15. Feature selection is localized at cohesive Module/Adapter boundaries.
|
||||
16. The standard runtime rejects unsupported configured capabilities. Compact
|
||||
compatibility Hosts may preserve them as runtime no-ops, but never change
|
||||
wire protocol, advertise them, or silently fall back to an unsafe mode.
|
||||
|
||||
## Validation
|
||||
|
||||
Changes to these boundaries should run, at minimum:
|
||||
|
||||
```text
|
||||
cargo fmt --all -- --check
|
||||
cargo check -p easytier-core -p easytier-proto -p easytier --features full
|
||||
cargo test -p easytier-core --lib
|
||||
```
|
||||
|
||||
Feature work should add focused checks for the changed no-default, isolated,
|
||||
default, full, and cross-target profiles. Socket, TUN, namespace, protocol
|
||||
engine, and multi-node changes require the relevant Docker integration tests.
|
||||
WASI ABI or Adapter changes require a `wasm32-wasip1` build and target-side
|
||||
tests. These compiler-resolved profiles are the authority for feature and
|
||||
target boundaries.
|
||||
|
||||
CI path filters include `easytier-core`, `easytier-proto`, native, web, GUI
|
||||
Tauri, and contrib. The archived Rust test suite contains both `easytier` and
|
||||
`easytier-core`.
|
||||
|
||||
## Known limitations and debt
|
||||
|
||||
- Some production feature and platform gates still select fields or statements
|
||||
inside shared implementations. New code should prefer complete Module or
|
||||
Adapter variants, and existing cases should move only when their owning
|
||||
Module is changed.
|
||||
- Connectivity retains localized Adapter implementations that name
|
||||
`PeerManagerCore`; further decoupling requires an interface extraction, not
|
||||
a visibility-only move.
|
||||
- Native Linux namespace guards exist in paths that can cross async suspension.
|
||||
Because `setns` is thread-local, those operations should eventually be kept
|
||||
on one non-migrating execution context.
|
||||
- QUIC session retirement after failed or exhausted accepted sessions remains
|
||||
separate native-engine correctness work; it must preserve multiple
|
||||
connections sharing one QUIC endpoint/session.
|
||||
|
||||
These limitations are not reasons to add fallback owners or parallel state.
|
||||
Fixes should preserve the ownership rules above and address the responsible
|
||||
Module directly.
|
||||
@@ -0,0 +1,65 @@
|
||||
# HarmonyOS HAR delivery
|
||||
|
||||
The `ohos` workflow builds the Core HAR on pushes, pull requests, tags, and
|
||||
manual runs. Every successful run retains a short-lived HAR artifact, while
|
||||
publication to the private OHPM registry is deliberately restricted:
|
||||
|
||||
- A push to `main` publishes only when the pushed SHA is the merge commit of a
|
||||
pull request targeting `main` and the push is not forced.
|
||||
- A manual run on `main` publishes by default.
|
||||
- A manual run on another branch publishes only when its `publish` input is
|
||||
enabled.
|
||||
- Direct pushes, pull requests, tags, and ordinary non-main branch builds do
|
||||
not publish.
|
||||
|
||||
## Package identity
|
||||
|
||||
All branches publish the same private package name, `easytier-ohrs`. The
|
||||
source branch is encoded in the package version instead of the package name:
|
||||
|
||||
```text
|
||||
<core-version>-<branch-id>-<commits-since-tag>-<run-number>-<run-attempt>-g<short-sha>
|
||||
```
|
||||
|
||||
`branch-id` is a lowercase, OHPM-safe form of the source branch. Publishing a
|
||||
new version advances the registry's `latest` version. After publication, Core
|
||||
sends the `core-har-published` repository dispatch to the ArkTS and Pro
|
||||
repositories. The payload contains only `core_repository`, `core_ref`, and
|
||||
`package_name`.
|
||||
|
||||
## App install sequence
|
||||
|
||||
ArkTS and Pro use the same three OHPM commands:
|
||||
|
||||
```bash
|
||||
ohpm uninstall "$CORE_HAR_PACKAGE"
|
||||
ohpm install "$CORE_HAR_PACKAGE@latest" \
|
||||
--registry "$CORE_HAR_REGISTRY"
|
||||
ohpm install
|
||||
```
|
||||
|
||||
The App workflow then reads the installed version from:
|
||||
|
||||
```text
|
||||
oh_modules/<package_name>/oh-package.json5
|
||||
```
|
||||
|
||||
The existing `oh-package-lock.json5` and `oh_modules` directory are not
|
||||
manually deleted. Because the package name remains `easytier-ohrs`, downstream
|
||||
source imports do not need to be rewritten.
|
||||
|
||||
## Secrets
|
||||
|
||||
Core requires:
|
||||
|
||||
- `CODEARTS_PRIVATE_OHPM`: publish-capable OHPM configuration.
|
||||
- `DOWNSTREAM_DISPATCH_TOKEN`: permission to dispatch both App repositories.
|
||||
|
||||
ArkTS and Pro require:
|
||||
|
||||
- `CODEARTS_PRIVATE_OHPM_READ`: read-only private OHPM authentication.
|
||||
- `SIGNING_REPOSITORY_TOKEN`: read access to the corresponding private signing
|
||||
repository.
|
||||
|
||||
Signing and AppGallery Connect credentials remain downstream application
|
||||
concerns and are not passed through the Core dispatch payload.
|
||||
@@ -0,0 +1,176 @@
|
||||
# QUIC TCP Proxy 内存对比(2026-07-27)
|
||||
|
||||
## 结论
|
||||
|
||||
在相同的双节点 network namespace 环境中,当前分支相对 2.6.4:
|
||||
|
||||
- 空闲且未建立 TCP proxy 连接时,两端合计 USS 从 15.95 MiB
|
||||
降至 11.66 MiB,下降 26.9%;
|
||||
- 66 条空闲 TCP proxy 连接时,两端合计 USS 从 19.45 MiB
|
||||
降至 13.85 MiB,下降 28.8%;
|
||||
- 固定 1 Gbit/s 的单流 TCP proxy 传输中,两端平均 USS 从
|
||||
20.69 MiB 降至 14.75 MiB,下降 28.7%,同步峰值从
|
||||
21.50 MiB 降至 15.02 MiB;
|
||||
- 从 0 增长到 66 条空闲连接推算,每条连接在两个 core 上合计
|
||||
增加约 33.9 KiB USS;2.6.4 为 54.2 KiB,下降 37.4%;
|
||||
- 当前分支的匿名内存下降约 40% 至 44%,说明堆和连接缓冲区开销
|
||||
确实降低。
|
||||
|
||||
当前分支的 RSS 比 2.6.4 高约 5% 至 11%,但这部分差异没有出现在
|
||||
Anonymous 中,主要体现为非匿名或共享驻留页。PSS 在高连接数及
|
||||
固定吞吐场景基本持平,USS 和 Anonymous 则显著更低。因此不能
|
||||
只根据 RSS 判断发生了内存回退。
|
||||
|
||||
## 测试对象
|
||||
|
||||
| 版本 | 标识 | 二进制 |
|
||||
|---|---|---|
|
||||
| 当前分支 | commit `9e2ed33aeb37`,版本 `2.6.4-9e2ed33a` | `target/x86_64-unknown-linux-musl/release/easytier-core` |
|
||||
| 2.6.4 | 版本 `2.6.4-8428a89d` | `/data/tickets/easytier/easytier-linux-x86_64/easytier-core` |
|
||||
|
||||
当前分支使用以下命令重新构建,确保被测二进制准确对应 HEAD:
|
||||
|
||||
```console
|
||||
cargo build --release \
|
||||
--target x86_64-unknown-linux-musl \
|
||||
-p easytier \
|
||||
--features jemalloc \
|
||||
--bin easytier-core \
|
||||
--bin easytier-cli
|
||||
```
|
||||
|
||||
两个二进制均为 stripped static PIE。当前分支明确使用 musl 和
|
||||
jemalloc。
|
||||
|
||||
## 测试拓扑
|
||||
|
||||
- 两个 `easytier-core` 分别运行在独立的 network namespace;
|
||||
- namespace 通过 Linux bridge 和 veth 连接;
|
||||
- underlay 地址为 `10.251.89.10/24` 和 `10.251.89.11/24`;
|
||||
- EasyTier 虚拟地址为 `10.144.144.1/24` 和
|
||||
`10.144.144.2/24`;
|
||||
- 两个节点之间使用 UDP listener 建立 EasyTier peer 连接;
|
||||
- 源节点启用 `--enable-quic-proxy true`;
|
||||
- 两个节点均保留默认 QUIC input;
|
||||
- TCP client 从 `10.144.144.1` 访问绑定在
|
||||
`10.144.144.2` 上的 server;
|
||||
- `tcp_proxy_connect` 指标的 `protocol` 标签确认为 `QUIC`;
|
||||
- 66 条连接场景通过两端各 132 个 established TCP socket 条目
|
||||
确认当前连接数。
|
||||
|
||||
## 采样口径
|
||||
|
||||
数据读取自 `/proc/<pid>/smaps_rollup`:
|
||||
|
||||
- RSS:进程映射的全部驻留页,包含共享代码页;
|
||||
- PSS:共享页按共享进程数量分摊后的驻留内存;
|
||||
- USS:`Private_Clean + Private_Dirty`,表示进程独占内存;
|
||||
- Anonymous:匿名页,主要反映堆、栈和运行时缓冲区。
|
||||
|
||||
空闲场景每隔 2 秒采样一次,共 5 次,表格记录均值。固定吞吐场景
|
||||
持续 20 秒,每隔 2 秒采样一次,共 8 次,同时记录均值和峰值。
|
||||
所有容量单位均为 MiB。
|
||||
|
||||
## 空闲连接结果
|
||||
|
||||
以下数据均为两个 EasyTier core 的合计值:
|
||||
|
||||
| 当前连接数 | 当前 RSS | 2.6.4 RSS | 当前 PSS | 2.6.4 PSS | 当前 USS | 2.6.4 USS | USS 变化 | 当前 Anonymous | 2.6.4 Anonymous |
|
||||
|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 0 | 40.42 | 36.33 | 26.03 | 24.50 | 11.66 | 15.95 | -26.9% | 8.89 | 15.62 |
|
||||
| 1 | 41.52 | 38.46 | 26.49 | 25.98 | 11.46 | 16.82 | -31.9% | 9.08 | 16.23 |
|
||||
| 10 | 41.63 | 38.79 | 26.59 | 26.32 | 11.56 | 17.15 | -32.6% | 9.19 | 16.55 |
|
||||
| 66 | 43.68 | 41.11 | 28.76 | 28.63 | 13.85 | 19.45 | -28.8% | 11.22 | 18.79 |
|
||||
|
||||
0 条和 1 条连接之间的小幅反向波动属于分配器回收和采样时序噪声,
|
||||
不能解释为连接产生负开销。使用 0 到 66 条连接的跨度估算单位
|
||||
连接成本更稳定。
|
||||
|
||||
### 分节点 USS
|
||||
|
||||
| 当前连接数 | 当前源端 | 当前目的端 | 2.6.4 源端 | 2.6.4 目的端 |
|
||||
|---:|---:|---:|---:|---:|
|
||||
| 0 | 5.45 | 6.21 | 8.07 | 7.88 |
|
||||
| 1 | 5.43 | 6.03 | 8.68 | 8.14 |
|
||||
| 10 | 5.49 | 6.07 | 8.90 | 8.25 |
|
||||
| 66 | 6.60 | 7.25 | 10.19 | 9.25 |
|
||||
|
||||
### 单位连接增量
|
||||
|
||||
以 0 到 66 条连接的 USS 增量计算:
|
||||
|
||||
| 版本 | 两端 USS 增量 | 每连接两端合计 | 每连接单端平均 |
|
||||
|---|---:|---:|---:|
|
||||
| 当前分支 | 2.19 MiB | 33.9 KiB | 17.0 KiB |
|
||||
| 2.6.4 | 3.50 MiB | 54.2 KiB | 27.1 KiB |
|
||||
|
||||
当前分支的每连接独占内存增量下降约 37.4%。
|
||||
|
||||
## 固定 1 Gbit/s 活跃流量
|
||||
|
||||
为排除两个版本最大吞吐不同造成的缓冲区差异,使用
|
||||
`iperf3 -b 1G -P 1 -t 20` 将两个版本都限制为 1 Gbit/s。
|
||||
两次测试均实际完成 2.33 GiB 传输,接收端报告 1000 Mbit/s。
|
||||
|
||||
### 平均值
|
||||
|
||||
| 版本 | 节点 | RSS | PSS | USS | Anonymous |
|
||||
|---|---|---:|---:|---:|---:|
|
||||
| 当前分支 | 源端 | 22.89 | 15.41 | 7.94 | 6.88 |
|
||||
| 当前分支 | 目的端 | 21.77 | 14.29 | 6.82 | 5.27 |
|
||||
| 当前分支 | 两端合计 | 44.67 | 29.70 | 14.75 | 12.14 |
|
||||
| 2.6.4 | 源端 | 22.59 | 16.34 | 11.70 | 11.35 |
|
||||
| 2.6.4 | 目的端 | 19.95 | 13.67 | 8.99 | 8.77 |
|
||||
| 2.6.4 | 两端合计 | 42.53 | 30.01 | 20.69 | 20.13 |
|
||||
|
||||
### 对比
|
||||
|
||||
| 指标 | 当前分支 | 2.6.4 | 变化 |
|
||||
|---|---:|---:|---:|
|
||||
| 两端平均 RSS | 44.67 | 42.53 | +5.0% |
|
||||
| 两端平均 PSS | 29.70 | 30.01 | -1.0% |
|
||||
| 两端平均 USS | 14.75 | 20.69 | -28.7% |
|
||||
| 两端平均 Anonymous | 12.14 | 20.13 | -39.7% |
|
||||
| 两端同步峰值 USS | 15.02 | 21.50 | -30.2% |
|
||||
|
||||
## 分节点原始统计
|
||||
|
||||
下表保留各场景所有样本计算出的均值;`max_uss` 是该节点采样期间
|
||||
的最大 USS。
|
||||
|
||||
| 版本 | 场景 | 节点 | 样本数 | mean_rss | mean_pss | mean_uss | mean_anon | max_uss |
|
||||
|---|---|---|---:|---:|---:|---:|---:|---:|
|
||||
| 当前 | 0 连接 | 源端 | 5 | 19.830 | 12.636 | 5.451 | 4.314 | 5.582 |
|
||||
| 当前 | 0 连接 | 目的端 | 5 | 20.587 | 13.393 | 6.208 | 4.579 | 6.320 |
|
||||
| 当前 | 1 连接 | 源端 | 5 | 20.463 | 12.944 | 5.432 | 4.401 | 5.465 |
|
||||
| 当前 | 1 连接 | 目的端 | 5 | 21.061 | 13.541 | 6.030 | 4.682 | 6.051 |
|
||||
| 当前 | 10 连接 | 源端 | 5 | 20.522 | 13.002 | 5.491 | 4.459 | 5.496 |
|
||||
| 当前 | 10 连接 | 目的端 | 5 | 21.105 | 13.585 | 6.073 | 4.726 | 6.086 |
|
||||
| 当前 | 66 连接 | 源端 | 5 | 21.513 | 14.050 | 6.595 | 5.498 | 6.672 |
|
||||
| 当前 | 66 连接 | 目的端 | 5 | 22.169 | 14.706 | 7.251 | 5.723 | 7.375 |
|
||||
| 当前 | 1 Gbit/s | 源端 | 8 | 22.893 | 15.410 | 7.936 | 6.877 | 8.188 |
|
||||
| 当前 | 1 Gbit/s | 目的端 | 8 | 21.773 | 14.291 | 6.816 | 5.266 | 6.832 |
|
||||
| 2.6.4 | 0 连接 | 源端 | 5 | 18.278 | 12.355 | 8.071 | 7.876 | 8.328 |
|
||||
| 2.6.4 | 0 连接 | 目的端 | 5 | 18.048 | 12.144 | 7.880 | 7.747 | 8.203 |
|
||||
| 2.6.4 | 1 连接 | 源端 | 5 | 19.535 | 13.279 | 8.676 | 8.262 | 8.727 |
|
||||
| 2.6.4 | 1 连接 | 目的端 | 5 | 18.920 | 12.705 | 8.143 | 7.971 | 8.191 |
|
||||
| 2.6.4 | 10 连接 | 源端 | 5 | 19.762 | 13.506 | 8.902 | 8.473 | 8.910 |
|
||||
| 2.6.4 | 10 连接 | 目的端 | 5 | 19.027 | 12.812 | 8.250 | 8.078 | 8.297 |
|
||||
| 2.6.4 | 66 连接 | 源端 | 5 | 21.069 | 14.805 | 10.194 | 9.702 | 10.320 |
|
||||
| 2.6.4 | 66 连接 | 目的端 | 5 | 20.045 | 13.822 | 9.252 | 9.088 | 9.293 |
|
||||
| 2.6.4 | 1 Gbit/s | 源端 | 8 | 22.588 | 16.343 | 11.697 | 11.354 | 12.258 |
|
||||
| 2.6.4 | 1 Gbit/s | 目的端 | 8 | 19.946 | 13.670 | 8.993 | 8.774 | 9.277 |
|
||||
|
||||
## 解释和限制
|
||||
|
||||
1. 以固定 1 Gbit/s 场景为例,当前分支 RSS 增加 5.0%,但
|
||||
Anonymous 下降 39.7%,PSS 下降 1.0%。这说明差异主要体现
|
||||
在非匿名或共享驻留页;本次没有保存逐 VMA 数据,因此不进一步
|
||||
将它归因到某一个具体映射。
|
||||
2. 两个相同版本进程运行在同一宿主机时会共享可执行文件代码页,
|
||||
所以 PSS 比 RSS 更适合估算该测试拓扑的宿主机总成本,USS 和
|
||||
Anonymous 更适合判断 EasyTier 私有堆及缓冲区的变化。
|
||||
3. 这是一轮受控 A/B 测试,而不是长期统计分布。数值可用于确认
|
||||
差异方向和量级;若作为发布门禁,应固定机器负载并增加多轮重复。
|
||||
4. 本文只比较 QUIC TCP proxy 内存,不使用未限速吞吐结果推断性能,
|
||||
避免吞吐差异污染内存结论。
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "easytier-android-jni"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
jni = "0.21"
|
||||
once_cell = "1.18.0"
|
||||
log = "0.4"
|
||||
android_logger = "0.13"
|
||||
serde = { version = "1.0.220", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
easytier = { path = "../../easytier" }
|
||||
easytier-ffi = { path = "../easytier-ffi", default-features = false }
|
||||
@@ -0,0 +1,282 @@
|
||||
# EasyTier Android JNI
|
||||
|
||||
这是 EasyTier 的 Android JNI 绑定库,允许 Android 应用程序调用 EasyTier 的网络功能。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🚀 完整的 EasyTier FFI 接口封装
|
||||
- 📱 原生 Android JNI 支持
|
||||
- 🔧 支持多种 Android 架构 (arm64-v8a, armeabi-v7a, x86, x86_64)
|
||||
- 🛡️ 类型安全的 Java 接口
|
||||
- 🔌 支持通过 JSON 调用已暴露的 EasyTier RPC 查询/管理接口
|
||||
- 📝 详细的错误处理和日志记录
|
||||
|
||||
## 支持的架构
|
||||
|
||||
- `arm64-v8a` (aarch64-linux-android)
|
||||
- `armeabi-v7a` (armv7-linux-androideabi)
|
||||
- `x86` (i686-linux-android)
|
||||
- `x86_64` (x86_64-linux-android)
|
||||
|
||||
## 构建要求
|
||||
|
||||
### 系统要求
|
||||
|
||||
- Rust 1.70+
|
||||
- Android NDK r21+
|
||||
- Linux/macOS 开发环境
|
||||
|
||||
### 环境设置
|
||||
|
||||
1. **安装 Rust**
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source ~/.cargo/env
|
||||
```
|
||||
|
||||
2. **安装 Android NDK**
|
||||
- 下载 Android NDK: https://developer.android.com/ndk/downloads
|
||||
- 解压到合适的目录
|
||||
- 设置环境变量:
|
||||
```bash
|
||||
export ANDROID_NDK_ROOT=/path/to/android-ndk
|
||||
```
|
||||
|
||||
3. **添加 Android 目标**
|
||||
```bash
|
||||
rustup target add aarch64-linux-android
|
||||
rustup target add armv7-linux-androideabi
|
||||
rustup target add i686-linux-android
|
||||
rustup target add x86_64-linux-android
|
||||
```
|
||||
|
||||
## 构建步骤
|
||||
|
||||
1. **克隆项目并进入目录**
|
||||
```bash
|
||||
cd /path/to/EasyTier/easytier-contrib/easytier-android-jni
|
||||
```
|
||||
|
||||
2. **运行构建脚本**
|
||||
```bash
|
||||
./build.sh
|
||||
```
|
||||
|
||||
3. **构建完成后,库文件将生成在 `target/android/` 目录下**
|
||||
```
|
||||
target/android/
|
||||
├── arm64-v8a/
|
||||
│ └── libeasytier_android_jni.so
|
||||
├── armeabi-v7a/
|
||||
│ └── libeasytier_android_jni.so
|
||||
├── x86/
|
||||
│ └── libeasytier_android_jni.so
|
||||
└── x86_64/
|
||||
└── libeasytier_android_jni.so
|
||||
```
|
||||
|
||||
## Android 项目集成
|
||||
|
||||
### 1. 复制库文件
|
||||
|
||||
将生成的 `.so` 文件复制到您的 Android 项目中:
|
||||
|
||||
```
|
||||
your-android-project/
|
||||
└── src/main/
|
||||
├── jniLibs/
|
||||
│ ├── arm64-v8a/
|
||||
│ │ └── libeasytier_android_jni.so
|
||||
│ ├── armeabi-v7a/
|
||||
│ │ └── libeasytier_android_jni.so
|
||||
│ ├── x86/
|
||||
│ │ └── libeasytier_android_jni.so
|
||||
│ └── x86_64/
|
||||
│ └── libeasytier_android_jni.so
|
||||
└── java/
|
||||
└── com/easytier/jni/
|
||||
└── EasyTierJNI.java
|
||||
```
|
||||
|
||||
### 2. 复制 Java 接口
|
||||
|
||||
将 `java/com/easytier/jni/EasyTierJNI.java` 复制到您的 Android 项目的相应包路径下。
|
||||
|
||||
### 3. 添加权限
|
||||
|
||||
在 `AndroidManifest.xml` 中添加必要的权限:
|
||||
|
||||
```xml
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基本使用
|
||||
|
||||
```java
|
||||
import com.easytier.jni.EasyTierJNI;
|
||||
import java.util.Map;
|
||||
|
||||
public class EasyTierManager {
|
||||
|
||||
// 初始化网络实例
|
||||
public void startNetwork() {
|
||||
String config = """
|
||||
inst_name = "my_instance"
|
||||
network = "my_network"
|
||||
""";
|
||||
|
||||
try {
|
||||
// 解析配置
|
||||
int result = EasyTierJNI.parseConfig(config);
|
||||
if (result != 0) {
|
||||
String error = EasyTierJNI.getLastError();
|
||||
throw new RuntimeException("配置解析失败: " + error);
|
||||
}
|
||||
|
||||
// 启动网络实例
|
||||
result = EasyTierJNI.runNetworkInstance(config);
|
||||
if (result != 0) {
|
||||
String error = EasyTierJNI.getLastError();
|
||||
throw new RuntimeException("网络实例启动失败: " + error);
|
||||
}
|
||||
|
||||
System.out.println("EasyTier 网络实例启动成功");
|
||||
|
||||
} catch (RuntimeException e) {
|
||||
System.err.println("启动失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 获取网络信息
|
||||
public void getNetworkInfo() {
|
||||
try {
|
||||
Map<String, String> infos = EasyTierJNI.collectNetworkInfosAsMap(10);
|
||||
for (Map.Entry<String, String> entry : infos.entrySet()) {
|
||||
System.out.println(entry.getKey() + ": " + entry.getValue());
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
System.err.println("获取网络信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 停止所有实例
|
||||
public void stopNetwork() {
|
||||
try {
|
||||
int result = EasyTierJNI.stopAllInstances();
|
||||
if (result == 0) {
|
||||
System.out.println("所有网络实例已停止");
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
System.err.println("停止网络失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 通用 JSON RPC
|
||||
|
||||
`EasyTierJNI.callJsonRpc(serviceName, methodName, domainName, payloadJson)` 可以调用已暴露的
|
||||
EasyTier RPC 服务,payload 和返回值均为 protobuf JSON。该接口不支持
|
||||
`api.manage.WebClientService`;实例启动、保留、删除、信息收集仍使用专用 JNI API。
|
||||
|
||||
```java
|
||||
String response = EasyTierJNI.callJsonRpc(
|
||||
"api.logger.LoggerRpcService",
|
||||
"get_logger_config",
|
||||
"{}"
|
||||
);
|
||||
```
|
||||
|
||||
### VPN 服务集成
|
||||
|
||||
如果您要在 Android VPN 服务中使用:
|
||||
|
||||
```java
|
||||
public class EasyTierVpnService extends VpnService {
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
// 建立 VPN 连接
|
||||
ParcelFileDescriptor vpnInterface = establishVpnInterface();
|
||||
|
||||
if (vpnInterface != null) {
|
||||
int fd = vpnInterface.getFd();
|
||||
|
||||
// 设置 TUN 文件描述符
|
||||
try {
|
||||
EasyTierJNI.setTunFd("my_instance", fd);
|
||||
} catch (RuntimeException e) {
|
||||
Log.e("EasyTier", "设置 TUN FD 失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
private ParcelFileDescriptor establishVpnInterface() {
|
||||
Builder builder = new Builder();
|
||||
builder.setMtu(1500);
|
||||
builder.addAddress("10.0.0.2", 24);
|
||||
builder.addRoute("0.0.0.0", 0);
|
||||
builder.setSession("EasyTier VPN");
|
||||
|
||||
return builder.establish();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### EasyTierJNI 类方法
|
||||
|
||||
| 方法 | 描述 | 参数 | 返回值 |
|
||||
|------|------|------|--------|
|
||||
| `parseConfig(String config)` | 解析 TOML 配置 | config: 配置字符串 | 0=成功, -1=失败 |
|
||||
| `runNetworkInstance(String config)` | 启动网络实例 | config: 配置字符串 | 0=成功, -1=失败 |
|
||||
| `setTunFd(String instanceName, int fd)` | 设置 TUN 文件描述符 | instanceName: 实例名, fd: 文件描述符 | 0=成功, -1=失败 |
|
||||
| `retainNetworkInstance(String[] names)` | 保留指定实例 | names: 实例名数组 | 0=成功, -1=失败 |
|
||||
| `collectNetworkInfos(int maxLength)` | 收集网络信息 | maxLength: 最大条目数 | 信息字符串数组 |
|
||||
| `collectNetworkInfosAsMap(int maxLength)` | 收集网络信息为 Map | maxLength: 最大条目数 | Map<String, String> |
|
||||
| `getLastError()` | 获取最后错误 | 无 | 错误消息字符串 |
|
||||
| `stopAllInstances()` | 停止所有实例 | 无 | 0=成功, -1=失败 |
|
||||
| `retainSingleInstance(String name)` | 保留单个实例 | name: 实例名 | 0=成功, -1=失败 |
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **构建失败: "Android NDK not found"**
|
||||
- 确保设置了 `ANDROID_NDK_ROOT` 环境变量
|
||||
- 检查 NDK 路径是否正确
|
||||
|
||||
2. **运行时错误: "java.lang.UnsatisfiedLinkError"**
|
||||
- 确保 `.so` 文件放在正确的 `jniLibs` 目录下
|
||||
- 检查目标架构是否匹配
|
||||
|
||||
3. **配置解析失败**
|
||||
- 检查 TOML 配置格式是否正确
|
||||
- 使用 `getLastError()` 获取详细错误信息
|
||||
|
||||
### 调试技巧
|
||||
|
||||
- 启用 Android 日志查看 JNI 层的日志输出
|
||||
- 使用 `adb logcat -s EasyTier-JNI` 查看相关日志
|
||||
- 检查 `getLastError()` 返回的错误信息
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目遵循与 EasyTier 主项目相同的许可证。
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request 来改进这个项目。
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [EasyTier 主项目](https://github.com/EasyTier/EasyTier)
|
||||
- [Android NDK 文档](https://developer.android.com/ndk)
|
||||
- [Rust JNI 文档](https://docs.rs/jni/)
|
||||
@@ -0,0 +1,17 @@
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
fn main() {
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
if !matches!(target_os.as_str(), "android" | "linux") {
|
||||
return;
|
||||
}
|
||||
|
||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let exports = manifest_dir.join("exports.map");
|
||||
println!("cargo:rerun-if-changed={}", exports.display());
|
||||
println!(
|
||||
"cargo:rustc-cdylib-link-arg=-Wl,--version-script={}",
|
||||
exports.display()
|
||||
);
|
||||
println!("cargo:rustc-cdylib-link-arg=-Wl,--exclude-libs,ALL");
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/bin/bash
|
||||
|
||||
# EasyTier Android JNI 构建脚本
|
||||
# 用于编译适用于 Android 平台的 JNI 库
|
||||
# 使用 cargo-ndk 工具简化 Android 编译过程
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
|
||||
echo -e "${GREEN}EasyTier Android JNI 构建脚本 (使用 cargo-ndk)${NC}"
|
||||
echo "=============================================="
|
||||
|
||||
# 检查 Rust 是否安装
|
||||
if ! command -v rustc &> /dev/null; then
|
||||
echo -e "${RED}错误: 未找到 Rust 编译器,请先安装 Rust${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 cargo 是否安装
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
echo -e "${RED}错误: 未找到 Cargo,请先安装 Rust 工具链${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 cargo-ndk 是否安装
|
||||
if ! cargo ndk --version &> /dev/null; then
|
||||
echo -e "${YELLOW}cargo-ndk 未安装,正在安装...${NC}"
|
||||
cargo install cargo-ndk
|
||||
if ! cargo ndk --version &> /dev/null; then
|
||||
echo -e "${RED}错误: cargo-ndk 安装失败${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}cargo-ndk 版本: $(cargo ndk --version)${NC}"
|
||||
|
||||
# Android 目标架构映射 (cargo-ndk 使用的架构名称)
|
||||
# ANDROID_TARGETS=("arm64-v8a" "armeabi-v7a" "x86" "x86_64")
|
||||
ANDROID_TARGETS=("arm64-v8a")
|
||||
|
||||
# Android 架构到 Rust target 的映射
|
||||
declare -A TARGET_MAP
|
||||
TARGET_MAP["arm64-v8a"]="aarch64-linux-android"
|
||||
TARGET_MAP["armeabi-v7a"]="armv7-linux-androideabi"
|
||||
TARGET_MAP["x86"]="i686-linux-android"
|
||||
TARGET_MAP["x86_64"]="x86_64-linux-android"
|
||||
|
||||
# 检查并安装所需的 Rust target
|
||||
echo -e "${YELLOW}检查并安装 Android 目标架构...${NC}"
|
||||
for android_target in "${ANDROID_TARGETS[@]}"; do
|
||||
rust_target="${TARGET_MAP[$android_target]}"
|
||||
if ! rustup target list --installed | grep -q "$rust_target"; then
|
||||
echo -e "${YELLOW}安装目标架构: $rust_target (for $android_target)${NC}"
|
||||
rustup target add "$rust_target"
|
||||
else
|
||||
echo -e "${GREEN}目标架构已安装: $rust_target (for $android_target)${NC}"
|
||||
fi
|
||||
done
|
||||
|
||||
# 创建输出目录
|
||||
OUTPUT_DIR="./target/android"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# 构建函数
|
||||
build_for_target() {
|
||||
local android_target=$1
|
||||
echo -e "${YELLOW}构建目标: $android_target${NC}"
|
||||
|
||||
# 首先构建 easytier-ffi
|
||||
echo -e "${YELLOW}构建 easytier-ffi for $android_target${NC}"
|
||||
(cd $REPO_ROOT/easytier-contrib/easytier-ffi && cargo ndk -t $android_target build --release)
|
||||
|
||||
# 构建 JNI 库
|
||||
cargo ndk -t $android_target build --release
|
||||
|
||||
# 复制库文件到输出目录
|
||||
# cargo-ndk 使用 Rust target 名称作为目录名,而不是 Android 架构名称
|
||||
rust_target="${TARGET_MAP[$android_target]}"
|
||||
mkdir -p "$OUTPUT_DIR/$android_target"
|
||||
cp "$REPO_ROOT/target/$rust_target/release/libeasytier_android_jni.so" "$OUTPUT_DIR/$android_target/"
|
||||
cp "$REPO_ROOT/target/$rust_target/release/libeasytier_ffi.so" "$OUTPUT_DIR/$android_target/"
|
||||
echo -e "${GREEN}库文件已复制到: $OUTPUT_DIR/$android_target/${NC}"
|
||||
}
|
||||
|
||||
# 检查 Android NDK (cargo-ndk 会自动处理 NDK 路径)
|
||||
if [ -z "$ANDROID_NDK_ROOT" ] && [ -z "$ANDROID_NDK_HOME" ] && [ -z "$NDK_HOME" ]; then
|
||||
echo -e "${YELLOW}警告: 未设置 Android NDK 环境变量${NC}"
|
||||
echo "cargo-ndk 将尝试自动检测 NDK 路径"
|
||||
echo "如果构建失败,请设置以下环境变量之一:"
|
||||
echo " - ANDROID_NDK_ROOT"
|
||||
echo " - ANDROID_NDK_HOME"
|
||||
echo " - NDK_HOME"
|
||||
else
|
||||
if [ -n "$ANDROID_NDK_ROOT" ]; then
|
||||
echo -e "${GREEN}使用 Android NDK: $ANDROID_NDK_ROOT${NC}"
|
||||
elif [ -n "$ANDROID_NDK_HOME" ]; then
|
||||
echo -e "${GREEN}使用 Android NDK: $ANDROID_NDK_HOME${NC}"
|
||||
elif [ -n "$NDK_HOME" ]; then
|
||||
echo -e "${GREEN}使用 Android NDK: $NDK_HOME${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 构建所有目标
|
||||
echo -e "${YELLOW}开始构建所有目标架构...${NC}"
|
||||
for target in "${ANDROID_TARGETS[@]}"; do
|
||||
build_for_target "$target"
|
||||
done
|
||||
|
||||
echo -e "${GREEN}构建完成!${NC}"
|
||||
echo -e "${GREEN}所有库文件已生成到: $OUTPUT_DIR${NC}"
|
||||
echo ""
|
||||
echo "目录结构:"
|
||||
ls -la "$OUTPUT_DIR"/*/
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}使用说明:${NC}"
|
||||
echo "1. 将生成的 .so 文件复制到您的 Android 项目的 src/main/jniLibs/ 目录下"
|
||||
echo "2. 将 java/com/easytier/jni/EasyTierJNI.java 复制到您的 Android 项目中"
|
||||
echo "3. 在您的 Android 代码中调用 EasyTierJNI 类的方法"
|
||||
echo ""
|
||||
echo -e "${GREEN}注意: 此脚本使用 cargo-ndk 工具,无需手动设置复杂的环境变量${NC}"
|
||||
echo -e "${GREEN}cargo-ndk 会自动处理交叉编译所需的工具链配置${NC}"
|
||||
@@ -0,0 +1,56 @@
|
||||
# EasyTier Android JNI 示例配置文件
|
||||
# 这是一个基本的配置示例,展示如何配置 EasyTier 网络实例
|
||||
|
||||
# 实例名称 (必需)
|
||||
inst_name = "android_instance"
|
||||
|
||||
# 网络名称 (必需)
|
||||
network = "my_easytier_network"
|
||||
|
||||
# 网络密钥 (可选,用于网络加密)
|
||||
# network_secret = "your_secret_key_here"
|
||||
|
||||
# 监听地址 (可选)
|
||||
# listeners = ["tcp://0.0.0.0:11010", "udp://0.0.0.0:11010"]
|
||||
|
||||
# 对等节点地址 (可选)
|
||||
# peers = ["tcp://peer1.example.com:11010", "udp://peer2.example.com:11010"]
|
||||
|
||||
# 虚拟 IP 地址 (可选)
|
||||
# ipv4 = "10.144.144.1"
|
||||
|
||||
# 主机名 (可选)
|
||||
# hostname = "android-device"
|
||||
|
||||
# 启用 IPv6 (可选)
|
||||
# ipv6 = "fd00::1"
|
||||
|
||||
# 代理网络 (可选)
|
||||
# proxy_networks = ["192.168.1.0/24"]
|
||||
|
||||
# 退出节点 (可选)
|
||||
# exit_nodes = ["peer1"]
|
||||
|
||||
# 启用加密 (可选)
|
||||
# enable_encryption = true
|
||||
|
||||
# 启用 IPv4 转发 (可选)
|
||||
# enable_ipv4 = true
|
||||
|
||||
# 启用 IPv6 转发 (可选)
|
||||
# enable_ipv6 = false
|
||||
|
||||
# MTU 设置 (可选)
|
||||
# mtu = 1420
|
||||
|
||||
# 日志级别 (可选: error, warn, info, debug, trace)
|
||||
# log_level = "info"
|
||||
|
||||
# 禁用 P2P (可选)
|
||||
# disable_p2p = false
|
||||
|
||||
# 使用多路径 (可选)
|
||||
# use_multi_path = true
|
||||
|
||||
# 延迟优先 (可选)
|
||||
# latency_first = false
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
global:
|
||||
Java_com_easytier_jni_EasyTierJNI_*;
|
||||
local:
|
||||
*;
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.easytier.jni
|
||||
|
||||
fun interface ConfigServerEventCallback {
|
||||
fun onEvent(eventJson: String)
|
||||
}
|
||||
|
||||
/** EasyTier JNI 接口类 提供 Android 应用调用 EasyTier 核心网络功能的接口 */
|
||||
object EasyTierJNI {
|
||||
init {
|
||||
// 加载本地库
|
||||
System.loadLibrary("easytier_android_jni")
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 TUN 文件描述符
|
||||
* @param instanceName 实例名称
|
||||
* @param fd TUN 文件描述符
|
||||
* @return 0 表示成功,-1 表示失败
|
||||
* @throws RuntimeException 当操作失败时抛出异常
|
||||
*/
|
||||
@JvmStatic external fun setTunFd(instanceName: String, fd: Int): Int
|
||||
|
||||
/**
|
||||
* 解析配置字符串
|
||||
* @param config TOML 格式的配置字符串
|
||||
* @return 0 表示成功,-1 表示失败
|
||||
* @throws RuntimeException 当配置解析失败时抛出异常
|
||||
*/
|
||||
@JvmStatic external fun parseConfig(config: String): Int
|
||||
|
||||
/**
|
||||
* 运行网络实例
|
||||
* @param config TOML 格式的配置字符串
|
||||
* @return 0 表示成功,-1 表示失败
|
||||
* @throws RuntimeException 当实例启动失败时抛出异常
|
||||
*/
|
||||
@JvmStatic external fun runNetworkInstance(config: String): Int
|
||||
|
||||
/**
|
||||
* 启动配置服务器客户端
|
||||
* @param url 配置服务器 URL
|
||||
* @param hostname 主机名,传入 null 使用系统主机名
|
||||
* @param machineId 稳定机器 ID,由调用方负责持久化
|
||||
* @param secureMode 是否启用 secure mode
|
||||
* @param callback 远程配置应用/删除事件回调
|
||||
* @return 0 表示成功,-1 表示失败
|
||||
* @throws RuntimeException 当客户端启动失败时抛出异常
|
||||
*/
|
||||
@JvmStatic
|
||||
external fun startConfigServerClient(
|
||||
url: String,
|
||||
hostname: String?,
|
||||
machineId: String,
|
||||
secureMode: Boolean,
|
||||
callback: ConfigServerEventCallback?
|
||||
): Int
|
||||
|
||||
/**
|
||||
* 停止配置服务器客户端
|
||||
* @return 0 表示成功,-1 表示失败
|
||||
* @throws RuntimeException 当客户端停止失败时抛出异常
|
||||
*/
|
||||
@JvmStatic external fun stopConfigServerClient(): Int
|
||||
|
||||
/** 查询配置服务器客户端是否已连接 */
|
||||
@JvmStatic external fun isConfigServerClientConnected(): Boolean
|
||||
|
||||
/**
|
||||
* 保留指定的网络实例,停止其他实例
|
||||
* @param instanceNames 要保留的实例名称数组,传入 null 或空数组将停止所有实例
|
||||
* @return 0 表示成功,-1 表示失败
|
||||
* @throws RuntimeException 当操作失败时抛出异常
|
||||
*/
|
||||
@JvmStatic external fun retainNetworkInstance(instanceNames: Array<String>?): Int
|
||||
|
||||
/**
|
||||
* 收集网络信息
|
||||
* @param maxLength 最大返回条目数
|
||||
* @return 包含网络信息的 JSON 字符串
|
||||
* @throws RuntimeException 当操作失败时抛出异常
|
||||
*/
|
||||
@JvmStatic external fun collectNetworkInfos(maxLength: Int): String?
|
||||
|
||||
/**
|
||||
* 列出当前运行的实例名称和实例 ID。
|
||||
* @param maxLength 最大返回条目数
|
||||
* @return JSON 对象,key 为 instance name,value 为 instance id
|
||||
* @throws RuntimeException 当操作失败时抛出异常
|
||||
*/
|
||||
@JvmStatic external fun listInstances(maxLength: Int): String?
|
||||
|
||||
/**
|
||||
* 调用暴露的 EasyTier RPC 方法,输入和输出均为 protobuf JSON 字符串。
|
||||
*
|
||||
* 不支持 api.manage.WebClientService;实例启动、保留、删除、信息收集请继续使用专用 JNI API。
|
||||
* payloadJson 需要包含目标 RPC 所需的 instance selector。
|
||||
*
|
||||
* @param serviceName RPC 服务名,例如 api.instance.PeerManageRpcService
|
||||
* @param methodName RPC 方法名,支持 snake_case 或 proto 方法名
|
||||
* @param domainName 仅 TcpProxyRpcService 使用;传 null 或空字符串默认 tcp
|
||||
* @param payloadJson protobuf JSON 请求体
|
||||
* @return protobuf JSON 响应体
|
||||
* @throws RuntimeException 当 RPC 调用失败时抛出异常
|
||||
*/
|
||||
@JvmStatic
|
||||
external fun callJsonRpc(
|
||||
serviceName: String,
|
||||
methodName: String,
|
||||
domainName: String?,
|
||||
payloadJson: String
|
||||
): String?
|
||||
|
||||
/**
|
||||
* 调用不需要 domainName 的 EasyTier RPC 方法。
|
||||
*/
|
||||
@JvmStatic
|
||||
fun callJsonRpc(serviceName: String, methodName: String, payloadJson: String): String? {
|
||||
return callJsonRpc(serviceName, methodName, null, payloadJson)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后的错误消息
|
||||
* @return 错误消息字符串,如果没有错误则返回 null
|
||||
*/
|
||||
@JvmStatic external fun getLastError(): String?
|
||||
|
||||
/**
|
||||
* 便利方法:停止所有网络实例
|
||||
* @return 0 表示成功,-1 表示失败
|
||||
* @throws RuntimeException 当操作失败时抛出异常
|
||||
*/
|
||||
@JvmStatic
|
||||
fun stopAllInstances(): Int {
|
||||
return retainNetworkInstance(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* 便利方法:停止指定实例外的所有实例
|
||||
* @param instanceName 要保留的实例名称
|
||||
* @return 0 表示成功,-1 表示失败
|
||||
* @throws RuntimeException 当操作失败时抛出异常
|
||||
*/
|
||||
@JvmStatic
|
||||
fun retainSingleInstance(instanceName: String): Int {
|
||||
return retainNetworkInstance(arrayOf(instanceName))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package com.easytier.jni
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.wire.WireJsonAdapterFactory
|
||||
import common.Ipv4Inet
|
||||
import web.NetworkInstanceRunningInfoMap
|
||||
|
||||
fun parseIpv4InetToString(inet: Ipv4Inet?): String? {
|
||||
val addr = inet?.address?.addr ?: return null
|
||||
val networkLength = inet.network_length
|
||||
|
||||
// 将 int32 转换为 IPv4 字符串
|
||||
val ip =
|
||||
String.format(
|
||||
"%d.%d.%d.%d",
|
||||
(addr shr 24) and 0xFF,
|
||||
(addr shr 16) and 0xFF,
|
||||
(addr shr 8) and 0xFF,
|
||||
addr and 0xFF
|
||||
)
|
||||
|
||||
return "$ip/$networkLength"
|
||||
}
|
||||
|
||||
/** EasyTier 管理类 负责管理 EasyTier 实例的生命周期、监控网络状态变化、控制 VpnService */
|
||||
class EasyTierManager(
|
||||
private val activity: Activity,
|
||||
private val instanceName: String,
|
||||
private val networkConfig: String
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "EasyTierManager"
|
||||
private const val MONITOR_INTERVAL = 3000L // 3秒监控间隔
|
||||
}
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var isRunning = false
|
||||
private var currentIpv4: String? = null
|
||||
private var currentProxyCidrs: List<String> = emptyList()
|
||||
private var vpnServiceIntent: Intent? = null
|
||||
|
||||
// JSON 解析器
|
||||
private val moshi = Moshi.Builder().add(WireJsonAdapterFactory()).build()
|
||||
private val adapter = moshi.adapter(NetworkInstanceRunningInfoMap::class.java)
|
||||
|
||||
// 监控任务
|
||||
private val monitorRunnable =
|
||||
object : Runnable {
|
||||
override fun run() {
|
||||
if (isRunning) {
|
||||
monitorNetworkStatus()
|
||||
handler.postDelayed(this, MONITOR_INTERVAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 启动 EasyTier 实例和监控 */
|
||||
fun start() {
|
||||
if (isRunning) {
|
||||
Log.w(TAG, "EasyTier 实例已经在运行中")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 启动 EasyTier 实例
|
||||
val result = EasyTierJNI.runNetworkInstance(networkConfig)
|
||||
if (result == 0) {
|
||||
isRunning = true
|
||||
Log.i(TAG, "EasyTier 实例启动成功: $instanceName")
|
||||
|
||||
// 开始监控网络状态
|
||||
handler.post(monitorRunnable)
|
||||
} else {
|
||||
Log.e(TAG, "EasyTier 实例启动失败: $result")
|
||||
val error = EasyTierJNI.getLastError()
|
||||
Log.e(TAG, "错误信息: $error")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "启动 EasyTier 实例时发生异常", e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 停止 EasyTier 实例和监控 */
|
||||
fun stop() {
|
||||
if (!isRunning) {
|
||||
Log.w(TAG, "EasyTier 实例未在运行")
|
||||
return
|
||||
}
|
||||
|
||||
isRunning = false
|
||||
|
||||
// 停止监控任务
|
||||
handler.removeCallbacks(monitorRunnable)
|
||||
|
||||
try {
|
||||
// 停止 VpnService
|
||||
stopVpnService()
|
||||
|
||||
// 停止 EasyTier 实例
|
||||
EasyTierJNI.stopAllInstances()
|
||||
Log.i(TAG, "EasyTier 实例已停止: $instanceName")
|
||||
|
||||
// 重置状态
|
||||
currentIpv4 = null
|
||||
currentProxyCidrs = emptyList()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "停止 EasyTier 实例时发生异常", e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 监控网络状态 */
|
||||
private fun monitorNetworkStatus() {
|
||||
try {
|
||||
val infosJson = EasyTierJNI.collectNetworkInfos(10)
|
||||
if (infosJson.isNullOrEmpty()) {
|
||||
Log.d(TAG, "未获取到网络信息")
|
||||
return
|
||||
}
|
||||
|
||||
val networkInfoMap = parseNetworkInfo(infosJson)
|
||||
val networkInfo = networkInfoMap?.map?.get(instanceName)
|
||||
|
||||
if (networkInfo == null) {
|
||||
Log.d(TAG, "未找到实例 $instanceName 的网络信息")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "网络信息: $networkInfo")
|
||||
|
||||
// 检查实例是否正在运行
|
||||
if (!networkInfo.running) {
|
||||
Log.w(TAG, "EasyTier 实例未运行: ${networkInfo.error_msg}")
|
||||
return
|
||||
}
|
||||
|
||||
val newIpv4Inet = networkInfo.my_node_info?.virtual_ipv4
|
||||
|
||||
if (newIpv4Inet == null) {
|
||||
Log.w(TAG, "EasyTier No Ipv4: $networkInfo")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前节点的 IPv4 地址
|
||||
val newIpv4 = parseIpv4InetToString(newIpv4Inet)
|
||||
|
||||
// 获取所有节点的 proxy_cidrs
|
||||
val newProxyCidrs = mutableListOf<String>()
|
||||
networkInfo.routes?.forEach { route ->
|
||||
route.proxy_cidrs?.let { cidrs -> newProxyCidrs.addAll(cidrs) }
|
||||
}
|
||||
|
||||
// 检查是否有变化
|
||||
val ipv4Changed = newIpv4 != currentIpv4
|
||||
val proxyCidrsChanged = newProxyCidrs != currentProxyCidrs
|
||||
|
||||
if (ipv4Changed || proxyCidrsChanged) {
|
||||
Log.i(TAG, "网络状态发生变化:")
|
||||
Log.i(TAG, " IPv4: $currentIpv4 -> $newIpv4")
|
||||
Log.i(TAG, " Proxy CIDRs: $currentProxyCidrs -> $newProxyCidrs")
|
||||
|
||||
// 更新状态
|
||||
currentIpv4 = newIpv4
|
||||
currentProxyCidrs = newProxyCidrs.toList()
|
||||
|
||||
// 重启 VpnService
|
||||
if (newIpv4 != null) {
|
||||
restartVpnService(newIpv4, newProxyCidrs)
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "网络状态无变化 - IPv4: $currentIpv4, Proxy CIDRs: ${currentProxyCidrs.size} 个")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "监控网络状态时发生异常", e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析网络信息 JSON */
|
||||
private fun parseNetworkInfo(jsonString: String): NetworkInstanceRunningInfoMap? {
|
||||
return try {
|
||||
adapter.fromJson(jsonString)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "解析网络信息失败", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/** 重启 VpnService */
|
||||
private fun restartVpnService(ipv4: String, proxyCidrs: List<String>) {
|
||||
try {
|
||||
// 先停止现有的 VpnService
|
||||
stopVpnService()
|
||||
|
||||
// 启动新的 VpnService
|
||||
startVpnService(ipv4, proxyCidrs)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "重启 VpnService 时发生异常", e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 启动 VpnService */
|
||||
private fun startVpnService(ipv4: String, proxyCidrs: List<String>) {
|
||||
try {
|
||||
val intent = Intent(activity, EasyTierVpnService::class.java)
|
||||
intent.putExtra("ipv4_address", ipv4)
|
||||
intent.putStringArrayListExtra("proxy_cidrs", ArrayList(proxyCidrs))
|
||||
intent.putExtra("instance_name", instanceName)
|
||||
|
||||
activity.startService(intent)
|
||||
vpnServiceIntent = intent
|
||||
|
||||
Log.i(TAG, "VpnService 已启动 - IPv4: $ipv4, Proxy CIDRs: $proxyCidrs")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "启动 VpnService 时发生异常", e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 停止 VpnService */
|
||||
private fun stopVpnService() {
|
||||
try {
|
||||
vpnServiceIntent?.let { intent ->
|
||||
activity.stopService(intent)
|
||||
Log.i(TAG, "VpnService 已停止")
|
||||
}
|
||||
vpnServiceIntent = null
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "停止 VpnService 时发生异常", e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取当前状态信息 */
|
||||
fun getStatus(): EasyTierStatus {
|
||||
return EasyTierStatus(
|
||||
isRunning = isRunning,
|
||||
instanceName = instanceName,
|
||||
currentIpv4 = currentIpv4,
|
||||
currentProxyCidrs = currentProxyCidrs.toList()
|
||||
)
|
||||
}
|
||||
|
||||
/** 状态数据类 */
|
||||
data class EasyTierStatus(
|
||||
val isRunning: Boolean,
|
||||
val instanceName: String,
|
||||
val currentIpv4: String?,
|
||||
val currentProxyCidrs: List<String>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.easytier.jni
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class EasyTierVpnService : VpnService() {
|
||||
|
||||
private var vpnInterface: ParcelFileDescriptor? = null
|
||||
private var isRunning = false
|
||||
private var instanceName: String? = null
|
||||
|
||||
companion object {
|
||||
private const val TAG = "EasyTierVpnService"
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "VPN Service created")
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
// 获取传入的参数
|
||||
val ipv4Address = intent?.getStringExtra("ipv4_address")
|
||||
val proxyCidrs = intent?.getStringArrayListExtra("proxy_cidrs") ?: arrayListOf()
|
||||
instanceName = intent?.getStringExtra("instance_name")
|
||||
|
||||
if (ipv4Address == null || instanceName == null) {
|
||||
Log.e(TAG, "缺少必要参数: ipv4Address=$ipv4Address, instanceName=$instanceName")
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
Log.i(
|
||||
TAG,
|
||||
"启动 VPN Service - IPv4: $ipv4Address, Proxy CIDRs: $proxyCidrs, Instance: $instanceName"
|
||||
)
|
||||
|
||||
thread {
|
||||
try {
|
||||
setupVpnInterface(ipv4Address, proxyCidrs)
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "VPN 设置失败", t)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun setupVpnInterface(ipv4Address: String, proxyCidrs: List<String>) {
|
||||
try {
|
||||
// 解析 IPv4 地址和网络长度
|
||||
val (ip, networkLength) = parseIpv4Address(ipv4Address)
|
||||
|
||||
// 1. 准备 VpnService.Builder
|
||||
val builder = Builder()
|
||||
builder.setSession("EasyTier VPN")
|
||||
.addAddress(ip, networkLength)
|
||||
.addDnsServer("223.5.5.5")
|
||||
.addDnsServer("114.114.114.114")
|
||||
.addDisallowedApplication("com.easytier.easytiervpn")
|
||||
|
||||
// 2. 添加路由表 - 为每个 proxy CIDR 添加路由
|
||||
proxyCidrs.forEach { cidr ->
|
||||
try {
|
||||
val (routeIp, routeLength) = parseCidr(cidr)
|
||||
builder.addRoute(routeIp, routeLength)
|
||||
Log.d(TAG, "添加路由: $routeIp/$routeLength")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "解析 CIDR 失败: $cidr", e)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 构建虚拟网络接口
|
||||
vpnInterface = builder.establish()
|
||||
|
||||
if (vpnInterface == null) {
|
||||
Log.e(TAG, "创建 VPN 接口失败")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "VPN 接口创建成功")
|
||||
|
||||
// 4. 将 TUN 文件描述符传递给 EasyTier
|
||||
instanceName?.let { name ->
|
||||
val fd = vpnInterface!!.fd
|
||||
val result = EasyTierJNI.setTunFd(name, fd)
|
||||
if (result == 0) {
|
||||
Log.i(TAG, "TUN 文件描述符设置成功: $fd")
|
||||
} else {
|
||||
Log.e(TAG, "TUN 文件描述符设置失败: $result")
|
||||
}
|
||||
}
|
||||
|
||||
isRunning = true
|
||||
|
||||
// 5. 保持服务运行
|
||||
while (isRunning && vpnInterface != null) {
|
||||
Thread.sleep(1000)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "VPN 接口设置过程中发生错误", t)
|
||||
} finally {
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析 IPv4 地址,返回 IP 和网络长度 */
|
||||
private fun parseIpv4Address(ipv4Address: String): Pair<String, Int> {
|
||||
return if (ipv4Address.contains("/")) {
|
||||
val parts = ipv4Address.split("/")
|
||||
Pair(parts[0], parts[1].toInt())
|
||||
} else {
|
||||
// 默认使用 /24 网络
|
||||
Pair(ipv4Address, 24)
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析 CIDR,返回 IP 和网络长度 */
|
||||
private fun parseCidr(cidr: String): Pair<String, Int> {
|
||||
val parts = cidr.split("/")
|
||||
if (parts.size != 2) {
|
||||
throw IllegalArgumentException("无效的 CIDR 格式: $cidr")
|
||||
}
|
||||
return Pair(parts[0], parts[1].toInt())
|
||||
}
|
||||
|
||||
private fun cleanup() {
|
||||
isRunning = false
|
||||
vpnInterface?.close()
|
||||
vpnInterface = null
|
||||
Log.i(TAG, "VPN 接口已清理")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Log.d(TAG, "VPN Service destroyed")
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# 使用说明
|
||||
|
||||
1. 需要将 proto 文件放入 app/src/main/proto
|
||||
2. android/gradle/libs.versions.toml 中加入依赖
|
||||
|
||||
```
|
||||
# Wire 核心运行时
|
||||
android-wire-runtime = { group = "com.squareup.wire", name = "wire-runtime", version = "5.3.11" }
|
||||
moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
|
||||
android-wire-moshi-adapter = { group = "com.squareup.wire", name = "wire-moshi-adapter", version = "5.3.11" }
|
||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.9.0" }
|
||||
```
|
||||
|
||||
3. build.gradle.kts 中加入
|
||||
|
||||
```
|
||||
plugins {
|
||||
...
|
||||
alias(libs.plugins.wire)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
...
|
||||
implementation(libs.android.wire.runtime)
|
||||
implementation(libs.android.wire.moshi.adapter)
|
||||
implementation(libs.moshi)
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
wire {
|
||||
kotlin {
|
||||
rpcRole = "none"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. 调用 easytier-contrib/easytier-android-jni/build.sh 生成 jni 和 ffi 的 so 文件。
|
||||
并将生成的 so 文件放到 android/app/src/main/jniLibs/arm64-v8a 目录下。
|
||||
|
||||
5. 使用 EasyTierManager 可以拉起 EasyTier 实例并启动 Android VpnService 组件。
|
||||
@@ -0,0 +1,124 @@
|
||||
use std::{
|
||||
ffi::{CStr, c_char, c_void},
|
||||
sync::{Arc, Mutex, MutexGuard},
|
||||
};
|
||||
|
||||
use easytier_ffi::ConfigServerEventCallback;
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{GlobalRef, JObject, JValue};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::error;
|
||||
|
||||
pub(crate) struct JniConfigServerCallback {
|
||||
java_vm: jni::JavaVM,
|
||||
callback: GlobalRef,
|
||||
}
|
||||
|
||||
static CONFIG_SERVER_CALLBACK: Lazy<Mutex<Option<Arc<JniConfigServerCallback>>>> =
|
||||
Lazy::new(|| Mutex::new(None));
|
||||
|
||||
pub(crate) fn lock_callback_storage()
|
||||
-> Result<MutexGuard<'static, Option<Arc<JniConfigServerCallback>>>, String> {
|
||||
CONFIG_SERVER_CALLBACK
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock config server callback: {}", e))
|
||||
}
|
||||
|
||||
pub(crate) fn new_callback(
|
||||
env: &mut JNIEnv,
|
||||
callback: &JObject,
|
||||
) -> Result<Arc<JniConfigServerCallback>, String> {
|
||||
let java_vm = env
|
||||
.get_java_vm()
|
||||
.map_err(|e| format!("Failed to get JavaVM: {:?}", e))?;
|
||||
let callback = env
|
||||
.new_global_ref(callback)
|
||||
.map_err(|e| format!("Failed to create callback global ref: {:?}", e))?;
|
||||
Ok(Arc::new(JniConfigServerCallback { java_vm, callback }))
|
||||
}
|
||||
|
||||
pub(crate) fn callback_fn(
|
||||
callback: &Option<Arc<JniConfigServerCallback>>,
|
||||
) -> ConfigServerEventCallback {
|
||||
callback
|
||||
.as_ref()
|
||||
.map(|_| config_server_event_callback as unsafe extern "C" fn(*const c_char, *mut c_void))
|
||||
}
|
||||
|
||||
pub(crate) fn user_data(callback: &Option<Arc<JniConfigServerCallback>>) -> *mut c_void {
|
||||
callback
|
||||
.as_ref()
|
||||
.map(|callback| Arc::as_ptr(callback) as *mut c_void)
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
impl JniConfigServerCallback {
|
||||
fn clear_pending_exception(
|
||||
env: &mut JNIEnv,
|
||||
context: &str,
|
||||
error: &dyn std::fmt::Debug,
|
||||
) -> String {
|
||||
match env.exception_check() {
|
||||
Ok(true) => {
|
||||
if let Err(clear_err) = env.exception_clear() {
|
||||
return format!(
|
||||
"{}: {:?}; failed to clear pending Java exception: {:?}",
|
||||
context, error, clear_err
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(check_err) => {
|
||||
return format!(
|
||||
"{}: {:?}; failed to check pending Java exception: {:?}",
|
||||
context, error, check_err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
format!("{}: {:?}", context, error)
|
||||
}
|
||||
|
||||
fn on_event(&self, event_json: *const c_char) -> Result<(), String> {
|
||||
let event_json = unsafe { CStr::from_ptr(event_json) }
|
||||
.to_str()
|
||||
.map_err(|e| format!("Invalid config server event JSON: {:?}", e))?;
|
||||
let mut env = self
|
||||
.java_vm
|
||||
.attach_current_thread()
|
||||
.map_err(|e| format!("Failed to attach callback thread: {:?}", e))?;
|
||||
let event_json = env.new_string(event_json).map_err(|e| {
|
||||
Self::clear_pending_exception(&mut env, "Failed to create event string", &e)
|
||||
})?;
|
||||
|
||||
if let Err(e) = env.call_method(
|
||||
self.callback.as_obj(),
|
||||
"onEvent",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[JValue::from(&event_json)],
|
||||
) {
|
||||
return Err(Self::clear_pending_exception(
|
||||
&mut env,
|
||||
"Failed to call config server callback",
|
||||
&e,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn config_server_event_callback(
|
||||
event_json: *const c_char,
|
||||
user_data: *mut c_void,
|
||||
) {
|
||||
if event_json.is_null() || user_data.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
let callback = unsafe { &*(user_data as *const JniConfigServerCallback) };
|
||||
|
||||
if let Err(error) = callback.on_event(event_json) {
|
||||
error::set_callback_error(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
use std::ptr;
|
||||
|
||||
use easytier_ffi::{
|
||||
in_config_server_callback, is_config_server_client_connected, start_config_server_client,
|
||||
stop_config_server_client,
|
||||
};
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JObject, JString};
|
||||
use jni::sys::{JNI_FALSE, JNI_TRUE, jboolean, jint};
|
||||
|
||||
use crate::{
|
||||
callback, error,
|
||||
strings::{jstring_to_cstring, optional_jstring_to_cstring},
|
||||
};
|
||||
|
||||
pub(crate) fn start_config_server_client_jni(
|
||||
env: &mut JNIEnv,
|
||||
config_server_url: JString,
|
||||
hostname: JString,
|
||||
machine_id: JString,
|
||||
secure_mode: jboolean,
|
||||
callback_obj: JObject,
|
||||
) -> jint {
|
||||
if in_config_server_callback() {
|
||||
error::throw_exception(
|
||||
env,
|
||||
"Cannot start config server client from config server callback",
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
|
||||
let config_server_url = match jstring_to_cstring(env, &config_server_url) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
error::throw_exception(env, &format!("Invalid config server URL: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let hostname = match optional_jstring_to_cstring(env, &hostname) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
error::throw_exception(env, &format!("Invalid hostname: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let machine_id = match jstring_to_cstring(env, &machine_id) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
error::throw_exception(env, &format!("Invalid machine ID: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let callback_ref = if callback_obj.is_null() {
|
||||
None
|
||||
} else {
|
||||
match callback::new_callback(env, &callback_obj) {
|
||||
Ok(state) => Some(state),
|
||||
Err(e) => {
|
||||
error::throw_exception(env, &e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut callback_guard = match callback::lock_callback_storage() {
|
||||
Ok(guard) => guard,
|
||||
Err(e) => {
|
||||
error::throw_exception(env, &e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
if callback_guard.is_none() {
|
||||
error::clear_callback_error();
|
||||
}
|
||||
|
||||
let callback_fn = callback::callback_fn(&callback_ref);
|
||||
let user_data = callback::user_data(&callback_ref);
|
||||
let result = unsafe {
|
||||
start_config_server_client(
|
||||
config_server_url.as_ptr(),
|
||||
hostname
|
||||
.as_ref()
|
||||
.map(|value| value.as_ptr())
|
||||
.unwrap_or(ptr::null()),
|
||||
machine_id.as_ptr(),
|
||||
secure_mode == JNI_TRUE,
|
||||
callback_fn,
|
||||
user_data,
|
||||
)
|
||||
};
|
||||
if result != 0 {
|
||||
if let Some(error_msg) = error::get_last_error() {
|
||||
error::throw_exception(env, &error_msg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
*callback_guard = callback_ref;
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn stop_config_server_client_jni(mut env: JNIEnv, _class: JClass) -> jint {
|
||||
if in_config_server_callback() {
|
||||
let result = stop_config_server_client();
|
||||
if result != 0
|
||||
&& let Some(error_msg) = error::get_last_error()
|
||||
{
|
||||
error::throw_exception(&mut env, &error_msg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
let mut callback_guard = match callback::lock_callback_storage() {
|
||||
Ok(guard) => guard,
|
||||
Err(e) => {
|
||||
error::throw_exception(&mut env, &e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let result = stop_config_server_client();
|
||||
if result != 0 {
|
||||
if let Some(error_msg) = error::get_last_error() {
|
||||
error::throw_exception(&mut env, &error_msg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
*callback_guard = None;
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn is_config_server_client_connected_jni(_env: JNIEnv, _class: JClass) -> jboolean {
|
||||
if is_config_server_client_connected() != 0 {
|
||||
JNI_TRUE
|
||||
} else {
|
||||
JNI_FALSE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
use std::{
|
||||
ffi::{CStr, c_char},
|
||||
ptr,
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use easytier_ffi::{free_string, get_error_msg};
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::JClass;
|
||||
use jni::sys::jstring;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static JNI_CALLBACK_ERROR: Lazy<Mutex<Option<String>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
pub(crate) fn set_callback_error(error: String) {
|
||||
log::error!("{}", error);
|
||||
if let Ok(mut guard) = JNI_CALLBACK_ERROR.lock() {
|
||||
*guard = Some(error);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_callback_error() {
|
||||
if let Ok(mut guard) = JNI_CALLBACK_ERROR.lock() {
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn take_callback_error() -> Option<String> {
|
||||
JNI_CALLBACK_ERROR
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|mut guard| guard.take())
|
||||
}
|
||||
|
||||
fn get_ffi_last_error() -> Option<String> {
|
||||
unsafe {
|
||||
let mut error_ptr: *const c_char = ptr::null();
|
||||
get_error_msg(&mut error_ptr);
|
||||
if error_ptr.is_null() {
|
||||
None
|
||||
} else {
|
||||
let error_cstr = CStr::from_ptr(error_ptr);
|
||||
let error_str = error_cstr.to_string_lossy().into_owned();
|
||||
free_string(error_ptr);
|
||||
Some(error_str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_last_error() -> Option<String> {
|
||||
match (get_ffi_last_error(), take_callback_error()) {
|
||||
(Some(ffi_error), Some(callback_error)) => Some(format!(
|
||||
"{}; config server callback error: {}",
|
||||
ffi_error, callback_error
|
||||
)),
|
||||
(Some(ffi_error), None) => Some(ffi_error),
|
||||
(None, Some(callback_error)) => Some(callback_error),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn throw_exception(env: &mut JNIEnv, message: &str) {
|
||||
let _ = env.throw_new("java/lang/RuntimeException", message);
|
||||
}
|
||||
|
||||
pub(crate) fn get_last_error_jni(env: JNIEnv, _class: JClass) -> jstring {
|
||||
match get_last_error() {
|
||||
Some(error) => match env.new_string(&error) {
|
||||
Ok(jstr) => jstr.into_raw(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
},
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
use std::{
|
||||
ffi::{CStr, c_char},
|
||||
ptr,
|
||||
};
|
||||
|
||||
use easytier_ffi::{call_json_rpc, free_string};
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JString};
|
||||
use jni::sys::jstring;
|
||||
|
||||
use crate::{
|
||||
error::{get_last_error, throw_exception},
|
||||
strings::{jstring_to_cstring, optional_jstring_to_cstring},
|
||||
};
|
||||
|
||||
pub(crate) fn call_json_rpc_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
service_name: JString,
|
||||
method_name: JString,
|
||||
domain_name: JString,
|
||||
payload_json: JString,
|
||||
) -> jstring {
|
||||
let service_name_cstr = match jstring_to_cstring(&mut env, &service_name) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid service name: {}", e));
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let method_name_cstr = match jstring_to_cstring(&mut env, &method_name) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid method name: {}", e));
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let domain_name_cstr = match optional_jstring_to_cstring(&mut env, &domain_name) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid domain name: {}", e));
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let payload_json_cstr = match jstring_to_cstring(&mut env, &payload_json) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid payload JSON: {}", e));
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
|
||||
let domain_name_ptr = domain_name_cstr
|
||||
.as_ref()
|
||||
.map_or(ptr::null(), |cstr| cstr.as_ptr());
|
||||
let mut response_ptr: *const c_char = ptr::null();
|
||||
let result = unsafe {
|
||||
call_json_rpc(
|
||||
service_name_cstr.as_ptr(),
|
||||
method_name_cstr.as_ptr(),
|
||||
domain_name_ptr,
|
||||
payload_json_cstr.as_ptr(),
|
||||
&mut response_ptr,
|
||||
)
|
||||
};
|
||||
|
||||
if result != 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
if response_ptr.is_null() {
|
||||
throw_exception(&mut env, "JSON RPC returned a null response");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let response = unsafe { CStr::from_ptr(response_ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
free_string(response_ptr);
|
||||
|
||||
match env.new_string(&response) {
|
||||
Ok(jstr) => jstr.into_raw(),
|
||||
Err(_) => {
|
||||
throw_exception(&mut env, "Failed to create JSON RPC response string");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
//! JNI facade for Android callers of EasyTier.
|
||||
//!
|
||||
//! This file intentionally lists every Java-visible native method exported by
|
||||
//! `libeasytier_android_jni.so`. The implementation details live in sibling
|
||||
//! modules so this facade stays readable as an API map.
|
||||
//!
|
||||
//! Network management APIs:
|
||||
//! - `setTunFd(instanceName, fd)`: attach an Android TUN fd to an instance.
|
||||
//! - `parseConfig(config)`: validate TOML config text.
|
||||
//! - `runNetworkInstance(config)`: start a local network instance.
|
||||
//! - `retainNetworkInstance(instanceNames)`: retain named instances and stop the rest.
|
||||
//! - `listInstances()`: return running instance names and IDs as JSON.
|
||||
//! - `collectNetworkInfos()`: return running instance info as a JSON string.
|
||||
//! - `callJsonRpc(...)`: call an exposed EasyTier RPC service with JSON payload.
|
||||
//!
|
||||
//! Config server client APIs:
|
||||
//! - `startConfigServerClient(url, hostname, machineId, secureMode, callback)`:
|
||||
//! start the managed remote config client.
|
||||
//! - `stopConfigServerClient()`: stop the managed client and release its Java callback.
|
||||
//! - `isConfigServerClientConnected()`: return whether the managed client is connected.
|
||||
//!
|
||||
//! Error API:
|
||||
//! - `getLastError()`: return the latest FFI/JNI error string for the calling thread.
|
||||
//!
|
||||
mod callback;
|
||||
mod config_server_api;
|
||||
mod error;
|
||||
mod json_rpc_api;
|
||||
mod logger;
|
||||
mod network_api;
|
||||
mod strings;
|
||||
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JObject, JObjectArray, JString};
|
||||
use jni::sys::{jboolean, jint, jstring};
|
||||
|
||||
/// Attach a TUN file descriptor to an EasyTier network instance.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.setTunFd(instanceName: String, fd: Int): Int`
|
||||
///
|
||||
/// `instanceName` must name an instance known to the shared FFI instance cache.
|
||||
/// The `fd` must be a valid Android TUN file descriptor. On failure this
|
||||
/// returns `-1` and throws `RuntimeException` with the FFI error message when
|
||||
/// one is available.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
inst_name: JString,
|
||||
fd: jint,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
network_api::set_tun_fd_jni(env, class, inst_name, fd)
|
||||
}
|
||||
|
||||
/// Validate a TOML network config string.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.parseConfig(config: String): Int`
|
||||
///
|
||||
/// This only validates the config text; it does not start or mutate any
|
||||
/// instance. On failure this returns `-1` and throws `RuntimeException`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
config: JString,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
network_api::parse_config_jni(env, class, config)
|
||||
}
|
||||
|
||||
/// Start one local EasyTier network instance from TOML config text.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.runNetworkInstance(config: String): Int`
|
||||
///
|
||||
/// The instance name in the config must be unique in the FFI instance cache.
|
||||
/// On failure this returns `-1` and throws `RuntimeException`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
config: JString,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
network_api::run_network_instance_jni(env, class, config)
|
||||
}
|
||||
|
||||
/// Retain the named network instances and stop all other instances.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.retainNetworkInstance(instanceNames: Array<String>?): Int`
|
||||
///
|
||||
/// Passing `null` or an empty array stops all instances. Null elements inside a
|
||||
/// non-empty array are invalid. On failure this returns `-1` and throws
|
||||
/// `RuntimeException`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
instance_names: JObjectArray,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
network_api::retain_network_instance_jni(env, class, instance_names)
|
||||
}
|
||||
|
||||
/// Collect running network instance information.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.collectNetworkInfos(maxLength: Int): String?`
|
||||
///
|
||||
/// Returns a JSON string containing `NetworkInstanceRunningInfoMap`, or null if
|
||||
/// collection fails. `maxLength` limits how many FFI entries are collected. On
|
||||
/// failure this throws `RuntimeException` when an error message is available.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
max_length: jint,
|
||||
) -> jstring {
|
||||
logger::init();
|
||||
network_api::collect_network_infos_jni(env, class, max_length)
|
||||
}
|
||||
|
||||
/// List running network instance names and IDs.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.listInstances(maxLength: Int): String?`
|
||||
///
|
||||
/// Returns a JSON object whose keys are instance names and whose values are
|
||||
/// instance ID strings. On failure this returns null and throws
|
||||
/// `RuntimeException`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_listInstances(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
max_length: jint,
|
||||
) -> jstring {
|
||||
logger::init();
|
||||
network_api::list_instances_jni(env, class, max_length)
|
||||
}
|
||||
|
||||
/// Call an exposed EasyTier RPC method using protobuf JSON.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.callJsonRpc(serviceName, methodName, domainName, payloadJson): String?`
|
||||
///
|
||||
/// Instance lifecycle management RPCs are intentionally not exposed here. Use
|
||||
/// the dedicated EasyTierJNI instance APIs for start/retain/delete/collect.
|
||||
/// `payloadJson` must include any `instance` selector required by the target
|
||||
/// RPC. On failure this returns null and throws `RuntimeException`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_callJsonRpc(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
service_name: JString,
|
||||
method_name: JString,
|
||||
domain_name: JString,
|
||||
payload_json: JString,
|
||||
) -> jstring {
|
||||
logger::init();
|
||||
json_rpc_api::call_json_rpc_jni(
|
||||
env,
|
||||
class,
|
||||
service_name,
|
||||
method_name,
|
||||
domain_name,
|
||||
payload_json,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the latest FFI/JNI error string for the calling thread.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.getLastError(): String?`
|
||||
///
|
||||
/// This combines the FFI thread-local error with any pending config-server Java
|
||||
/// callback error. It returns null when no error is available.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_getLastError(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
) -> jstring {
|
||||
error::get_last_error_jni(env, class)
|
||||
}
|
||||
|
||||
/// Start the managed config-server client.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.startConfigServerClient(url, hostname, machineId, secureMode, callback): Int`
|
||||
///
|
||||
/// JNI only converts Java values and keeps the Java callback alive. The FFI
|
||||
/// layer owns singleton lifecycle, config-server/data-plane mutual exclusion,
|
||||
/// remote instance tracking, and callback event timing. If `callback` is
|
||||
/// non-null, each remote apply/delete event is delivered to
|
||||
/// `ConfigServerEventCallback.onEvent(eventJson)`.
|
||||
///
|
||||
/// On failure this returns `-1` and throws `RuntimeException`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_startConfigServerClient(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
config_server_url: JString,
|
||||
hostname: JString,
|
||||
machine_id: JString,
|
||||
secure_mode: jboolean,
|
||||
callback: JObject,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
config_server_api::start_config_server_client_jni(
|
||||
&mut env,
|
||||
config_server_url,
|
||||
hostname,
|
||||
machine_id,
|
||||
secure_mode,
|
||||
callback,
|
||||
)
|
||||
}
|
||||
|
||||
/// Stop the managed config-server client.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.stopConfigServerClient(): Int`
|
||||
///
|
||||
/// The FFI layer performs the actual stop and managed instance cleanup. JNI
|
||||
/// releases the Java callback reference after FFI stop succeeds. On failure
|
||||
/// this returns `-1` and throws `RuntimeException`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_stopConfigServerClient(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
config_server_api::stop_config_server_client_jni(env, class)
|
||||
}
|
||||
|
||||
/// Report whether the managed config-server client is connected.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.isConfigServerClientConnected(): Boolean`
|
||||
///
|
||||
/// Returns `JNI_TRUE` only when the FFI config-server client exists and reports
|
||||
/// connected.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_isConfigServerClientConnected(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
) -> jboolean {
|
||||
logger::init();
|
||||
config_server_api::is_config_server_client_connected_jni(env, class)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static LOGGER_INIT: Lazy<()> = Lazy::new(|| {
|
||||
android_logger::init_once(
|
||||
android_logger::Config::default()
|
||||
.with_max_level(log::LevelFilter::Debug)
|
||||
.with_tag("EasyTier-JNI"),
|
||||
);
|
||||
});
|
||||
|
||||
pub(crate) fn init() {
|
||||
Lazy::force(&LOGGER_INIT);
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
use std::{ffi::CStr, ptr};
|
||||
|
||||
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
|
||||
use easytier_ffi::{
|
||||
KeyValuePair, collect_network_infos, free_string, list_instance, parse_config,
|
||||
retain_network_instance, run_network_instance, set_tun_fd,
|
||||
};
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JObjectArray, JString};
|
||||
use jni::sys::{jint, jstring};
|
||||
|
||||
use crate::{
|
||||
error::{get_last_error, throw_exception},
|
||||
strings::jstring_to_cstring,
|
||||
};
|
||||
|
||||
pub(crate) fn set_tun_fd_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
inst_name: JString,
|
||||
fd: jint,
|
||||
) -> jint {
|
||||
let inst_name_cstr = match jstring_to_cstring(&mut env, &inst_name) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid instance name: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
unsafe {
|
||||
let result = set_tun_fd(inst_name_cstr.as_ptr(), fd);
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_config_jni(mut env: JNIEnv, _class: JClass, config: JString) -> jint {
|
||||
let config_cstr = match jstring_to_cstring(&mut env, &config) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid config string: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
unsafe {
|
||||
let result = parse_config(config_cstr.as_ptr());
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run_network_instance_jni(mut env: JNIEnv, _class: JClass, config: JString) -> jint {
|
||||
let config_cstr = match jstring_to_cstring(&mut env, &config) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid config string: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
unsafe {
|
||||
let result = run_network_instance(config_cstr.as_ptr());
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn retain_network_instance_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
instance_names: JObjectArray,
|
||||
) -> jint {
|
||||
if instance_names.is_null() {
|
||||
return retain_all(&mut env);
|
||||
}
|
||||
|
||||
let array_length = match env.get_array_length(&instance_names) {
|
||||
Ok(len) => len as usize,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Failed to get array length: {:?}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if array_length == 0 {
|
||||
return retain_all(&mut env);
|
||||
}
|
||||
|
||||
let mut c_strings = Vec::with_capacity(array_length);
|
||||
let mut c_string_ptrs = Vec::with_capacity(array_length);
|
||||
|
||||
for i in 0..array_length {
|
||||
let java_string = match env.get_object_array_element(&instance_names, i as i32) {
|
||||
Ok(obj) => obj,
|
||||
Err(e) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Failed to get array element {}: {:?}", i, e),
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if java_string.is_null() {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Invalid instance name at index {}: null", i),
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
|
||||
let jstring = JString::from(java_string);
|
||||
let c_string = match jstring_to_cstring(&mut env, &jstring) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Invalid instance name at index {}: {}", i, e),
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
c_string_ptrs.push(c_string.as_ptr());
|
||||
c_strings.push(c_string);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let result = retain_network_instance(c_string_ptrs.as_ptr(), c_string_ptrs.len());
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn retain_all(env: &mut JNIEnv) -> jint {
|
||||
unsafe {
|
||||
let result = retain_network_instance(ptr::null(), 0);
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn collect_network_infos_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
max_length: jint,
|
||||
) -> jstring {
|
||||
let max_length = max_length.max(0) as usize;
|
||||
let mut infos = vec![
|
||||
KeyValuePair {
|
||||
key: ptr::null(),
|
||||
value: ptr::null(),
|
||||
};
|
||||
max_length
|
||||
];
|
||||
|
||||
unsafe {
|
||||
let count = collect_network_infos(infos.as_mut_ptr(), max_length);
|
||||
if count < 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let mut ret = NetworkInstanceRunningInfoMap::default();
|
||||
for info in infos.iter().take(count as usize) {
|
||||
let key_ptr = info.key;
|
||||
let val_ptr = info.value;
|
||||
if key_ptr.is_null() || val_ptr.is_null() {
|
||||
break;
|
||||
}
|
||||
|
||||
let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
|
||||
let val = CStr::from_ptr(val_ptr).to_string_lossy().into_owned();
|
||||
free_string(key_ptr);
|
||||
free_string(val_ptr);
|
||||
let value = match serde_json::from_str::<NetworkInstanceRunningInfo>(&val) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
throw_exception(&mut env, "Failed to parse JSON");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
ret.map.insert(key, value);
|
||||
}
|
||||
|
||||
let json_str = serde_json::to_string(&ret).unwrap_or_else(|_| "{}".to_string());
|
||||
match env.new_string(&json_str) {
|
||||
Ok(jstr) => jstr.into_raw(),
|
||||
Err(_) => {
|
||||
throw_exception(&mut env, "Failed to create JSON string");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn list_instances_jni(mut env: JNIEnv, _class: JClass, max_length: jint) -> jstring {
|
||||
let max_length = max_length.max(0) as usize;
|
||||
let mut infos = vec![
|
||||
KeyValuePair {
|
||||
key: ptr::null(),
|
||||
value: ptr::null(),
|
||||
};
|
||||
max_length
|
||||
];
|
||||
|
||||
unsafe {
|
||||
let count = list_instance(infos.as_mut_ptr(), max_length);
|
||||
if count < 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let mut ret = serde_json::Map::new();
|
||||
for info in infos.iter().take(count as usize) {
|
||||
let key_ptr = info.key;
|
||||
let val_ptr = info.value;
|
||||
if key_ptr.is_null() || val_ptr.is_null() {
|
||||
break;
|
||||
}
|
||||
|
||||
let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
|
||||
let val = CStr::from_ptr(val_ptr).to_string_lossy().into_owned();
|
||||
free_string(key_ptr);
|
||||
free_string(val_ptr);
|
||||
ret.insert(key, serde_json::Value::String(val));
|
||||
}
|
||||
|
||||
let json_str = serde_json::Value::Object(ret).to_string();
|
||||
match env.new_string(&json_str) {
|
||||
Ok(jstr) => jstr.into_raw(),
|
||||
Err(_) => {
|
||||
throw_exception(&mut env, "Failed to create instance list JSON string");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use std::ffi::CString;
|
||||
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::JString;
|
||||
|
||||
pub(crate) fn jstring_to_cstring(env: &mut JNIEnv, jstr: &JString) -> Result<CString, String> {
|
||||
let java_str = env
|
||||
.get_string(jstr)
|
||||
.map_err(|e| format!("Failed to get string: {:?}", e))?;
|
||||
let rust_str = java_str.to_str().map_err(|_| "Invalid UTF-8".to_string())?;
|
||||
CString::new(rust_str).map_err(|_| "String contains null byte".to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn optional_jstring_to_cstring(
|
||||
env: &mut JNIEnv,
|
||||
jstr: &JString,
|
||||
) -> Result<Option<CString>, String> {
|
||||
if jstr.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
jstring_to_cstring(env, jstr).map(Some)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "easytier-ffi"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["c-abi", "ffi-dataplane"]
|
||||
c-abi = []
|
||||
ffi-dataplane = [
|
||||
"easytier/ffi-dataplane",
|
||||
"easytier-core/proxy-smoltcp-stack",
|
||||
]
|
||||
macos-ne = ["easytier/macos-ne"]
|
||||
|
||||
[dependencies]
|
||||
easytier = { path = "../../easytier", features = ["tracing-log"] }
|
||||
easytier-core = { path = "../../easytier-core" }
|
||||
|
||||
once_cell = "1.18.0"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "io-util", "time", "sync", "macros"] }
|
||||
async-trait = "0.1"
|
||||
log = "0.4"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
uuid = "1.17.0"
|
||||
|
||||
[build-dependencies]
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
|
||||
"win7",
|
||||
] }
|
||||
@@ -0,0 +1,108 @@
|
||||
# Native data-plane ABI v3
|
||||
|
||||
The native data-plane ABI is a thin adapter over the instance-owned
|
||||
`DataPlaneSession`. It does not own sockets, operation state, completion
|
||||
queues, routing policy, or timeouts.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Every immediate call returns `0` on success or a negative
|
||||
`DataPlaneErrorKind` value on failure.
|
||||
- `data_plane_completion_wait` returns `1` when a completion is ready, `0` on
|
||||
timeout or session close, and a negative error value on failure.
|
||||
- `data_plane_completion_drain` returns a non-negative descriptor count or a
|
||||
negative error value.
|
||||
- Handle zero is invalid.
|
||||
- `timeout_ms == UINT64_MAX` means no deadline.
|
||||
- TCP connect/bind/accept and UDP bind timeouts start when submission is
|
||||
accepted.
|
||||
- TCP streams and UDP sockets have persistent read and write deadlines.
|
||||
`data_plane_resource_deadline_set` replaces the selected directions'
|
||||
deadlines immediately, including for active operations. An expired deadline
|
||||
remains expired until it is replaced or cleared with `UINT64_MAX`.
|
||||
- Deadline direction `1` selects reads, `2` selects writes, and `3` selects
|
||||
both.
|
||||
- Request and write bytes are copied before a submit call returns.
|
||||
- Socket-address fields use native-endian integers. Address bytes are in
|
||||
network order. ABI v3 accepts IPv4 only.
|
||||
|
||||
`DataPlaneSocketAddr` is:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
uint16_t family; /* 4 */
|
||||
uint16_t port;
|
||||
uint8_t address[16]; /* IPv4 uses the first four bytes */
|
||||
} DataPlaneSocketAddr;
|
||||
```
|
||||
|
||||
`DataPlaneCompletion` is:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
uint64_t operation_id;
|
||||
uint16_t operation_kind;
|
||||
uint16_t status; /* 0 or DataPlaneErrorKind */
|
||||
} DataPlaneCompletion;
|
||||
```
|
||||
|
||||
## Lifecycle
|
||||
|
||||
One native session may be open for an EasyTier instance at a time:
|
||||
|
||||
```text
|
||||
data_plane_session_open
|
||||
-> set resource deadlines
|
||||
-> submit operations
|
||||
-> completion_wait
|
||||
-> completion_drain
|
||||
-> typed result_take
|
||||
-> resource_close / operation_free
|
||||
data_plane_session_close
|
||||
```
|
||||
|
||||
Closing a native session cancels and discards its outstanding operations and
|
||||
resources and wakes a thread blocked in `data_plane_completion_wait`.
|
||||
|
||||
The resource and operation IDs returned by the ABI belong to that session.
|
||||
They must always be passed together with the same session handle.
|
||||
|
||||
## Completion and result ownership
|
||||
|
||||
Submission returns an operation ID immediately. Completion descriptors carry
|
||||
only the operation ID, operation kind, and terminal status. Draining a
|
||||
descriptor makes its typed result available but does not consume it.
|
||||
|
||||
`data_plane_result_size` reports the TCP-read or UDP-receive payload size.
|
||||
Typed result-take functions consume the result exactly once. If a supplied
|
||||
buffer is too small, they return `-BufferTooSmall` and leave the result
|
||||
available for a later call.
|
||||
|
||||
Call `data_plane_operation_free` when a drained result is intentionally
|
||||
abandoned. Call `data_plane_resource_close` for TCP streams, listeners, and
|
||||
UDP sockets.
|
||||
|
||||
## Operation kinds
|
||||
|
||||
| Value | Operation |
|
||||
| ---: | --- |
|
||||
| 1 | TCP connect |
|
||||
| 2 | TCP bind |
|
||||
| 3 | TCP accept |
|
||||
| 4 | TCP read |
|
||||
| 5 | TCP write |
|
||||
| 6 | UDP bind |
|
||||
| 7 | UDP receive |
|
||||
| 8 | UDP send |
|
||||
|
||||
The exported function families are:
|
||||
|
||||
- `data_plane_tcp_*_submit`
|
||||
- `data_plane_udp_*_submit`
|
||||
- `data_plane_resource_deadline_set`
|
||||
- `data_plane_completion_wait`
|
||||
- `data_plane_completion_drain`
|
||||
- `data_plane_*_result_take`
|
||||
- `data_plane_operation_cancel`
|
||||
- `data_plane_operation_free`
|
||||
- `data_plane_resource_close`
|
||||
@@ -0,0 +1,8 @@
|
||||
fn main() {
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
||||
|
||||
if target_os == "windows" && (target_arch == "x86" || target_arch == "x86_64") {
|
||||
thunk::thunk();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
public class EasyTierFFI
|
||||
{
|
||||
// 导入 DLL 函数
|
||||
private const string DllName = "easytier_ffi.dll";
|
||||
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern int parse_config([MarshalAs(UnmanagedType.LPStr)] string cfgStr);
|
||||
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern int run_network_instance([MarshalAs(UnmanagedType.LPStr)] string cfgStr);
|
||||
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern int retain_network_instance(IntPtr instNames, int length);
|
||||
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern int collect_network_infos(IntPtr infos, int maxLength);
|
||||
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern void get_error_msg(out IntPtr errorMsg);
|
||||
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern void free_string(IntPtr str);
|
||||
|
||||
// 定义 KeyValuePair 结构体
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct KeyValuePair
|
||||
{
|
||||
public IntPtr Key;
|
||||
public IntPtr Value;
|
||||
}
|
||||
|
||||
// 解析配置
|
||||
public static void ParseConfig(string config)
|
||||
{
|
||||
if (string.IsNullOrEmpty(config))
|
||||
{
|
||||
throw new ArgumentException("Configuration string cannot be null or empty.");
|
||||
}
|
||||
|
||||
int result = parse_config(config);
|
||||
if (result < 0)
|
||||
{
|
||||
throw new Exception(GetErrorMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 启动网络实例
|
||||
public static void RunNetworkInstance(string config)
|
||||
{
|
||||
if (string.IsNullOrEmpty(config))
|
||||
{
|
||||
throw new ArgumentException("Configuration string cannot be null or empty.");
|
||||
}
|
||||
|
||||
int result = run_network_instance(config);
|
||||
if (result < 0)
|
||||
{
|
||||
throw new Exception(GetErrorMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 保留网络实例
|
||||
public static void RetainNetworkInstances(string[] instanceNames)
|
||||
{
|
||||
IntPtr[] namePointers = null;
|
||||
IntPtr namesPtr = IntPtr.Zero;
|
||||
|
||||
try
|
||||
{
|
||||
if (instanceNames != null && instanceNames.Length > 0)
|
||||
{
|
||||
namePointers = new IntPtr[instanceNames.Length];
|
||||
for (int i = 0; i < instanceNames.Length; i++)
|
||||
{
|
||||
if (string.IsNullOrEmpty(instanceNames[i]))
|
||||
{
|
||||
throw new ArgumentException("Instance name cannot be null or empty.");
|
||||
}
|
||||
namePointers[i] = Marshal.StringToHGlobalAnsi(instanceNames[i]);
|
||||
}
|
||||
|
||||
namesPtr = Marshal.AllocHGlobal(Marshal.SizeOf<IntPtr>() * namePointers.Length);
|
||||
Marshal.Copy(namePointers, 0, namesPtr, namePointers.Length);
|
||||
}
|
||||
|
||||
int result = retain_network_instance(namesPtr, instanceNames?.Length ?? 0);
|
||||
if (result < 0)
|
||||
{
|
||||
throw new Exception(GetErrorMessage());
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (namePointers != null)
|
||||
{
|
||||
foreach (var ptr in namePointers)
|
||||
{
|
||||
if (ptr != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (namesPtr != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(namesPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 收集网络信息
|
||||
public static KeyValuePair<string, string>[] CollectNetworkInfos(int maxLength)
|
||||
{
|
||||
IntPtr buffer = Marshal.AllocHGlobal(Marshal.SizeOf<KeyValuePair>() * maxLength);
|
||||
try
|
||||
{
|
||||
int count = collect_network_infos(buffer, maxLength);
|
||||
if (count < 0)
|
||||
{
|
||||
throw new Exception(GetErrorMessage());
|
||||
}
|
||||
|
||||
var result = new KeyValuePair<string, string>[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var kv = Marshal.PtrToStructure<KeyValuePair>(buffer + i * Marshal.SizeOf<KeyValuePair>());
|
||||
string key = Marshal.PtrToStringAnsi(kv.Key);
|
||||
string value = Marshal.PtrToStringAnsi(kv.Value);
|
||||
|
||||
// 释放由 FFI 分配的字符串内存
|
||||
free_string(kv.Key);
|
||||
free_string(kv.Value);
|
||||
|
||||
result[i] = new KeyValuePair<string, string>(key, value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取错误信息
|
||||
private static string GetErrorMessage()
|
||||
{
|
||||
get_error_msg(out IntPtr errorMsgPtr);
|
||||
if (errorMsgPtr == IntPtr.Zero)
|
||||
{
|
||||
return "Unknown error";
|
||||
}
|
||||
|
||||
string errorMsg = Marshal.PtrToStringAnsi(errorMsgPtr);
|
||||
free_string(errorMsgPtr); // 释放错误信息字符串
|
||||
return errorMsg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h> // for sleep
|
||||
|
||||
// FFI struct and function declarations
|
||||
typedef struct {
|
||||
const char* key;
|
||||
const char* value;
|
||||
} KeyValuePair;
|
||||
|
||||
typedef void (*config_server_event_callback)(
|
||||
const char* event_json,
|
||||
void* user_data
|
||||
);
|
||||
|
||||
extern int parse_config(const char* cfg_str);
|
||||
extern int run_network_instance(const char* cfg_str);
|
||||
extern void get_error_msg(const char** out);
|
||||
extern void free_string(const char* s);
|
||||
extern int collect_network_infos(KeyValuePair* infos, size_t max_length);
|
||||
extern int start_config_server_client(
|
||||
const char* config_server_url,
|
||||
const char* hostname,
|
||||
const char* machine_id,
|
||||
bool secure_mode,
|
||||
config_server_event_callback callback,
|
||||
void* user_data
|
||||
);
|
||||
extern int stop_config_server_client(void);
|
||||
extern int is_config_server_client_connected(void);
|
||||
|
||||
static void on_config_server_event(const char* event_json, void* user_data) {
|
||||
(void)user_data;
|
||||
printf("config server event: %s\n", event_json);
|
||||
}
|
||||
|
||||
int main() {
|
||||
const char* config = "inst_name = \"test\"\nnetwork = \"test_network\"\n";
|
||||
int ret;
|
||||
|
||||
// 调用 parse_config
|
||||
ret = parse_config(config);
|
||||
if (ret != 0) {
|
||||
const char* err = NULL;
|
||||
get_error_msg(&err);
|
||||
if (err) {
|
||||
printf("parse_config error: %s\n", err);
|
||||
free_string(err);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
printf("parse_config success\n");
|
||||
|
||||
// 调用 run_network_instance
|
||||
ret = run_network_instance(config);
|
||||
if (ret != 0) {
|
||||
const char* err = NULL;
|
||||
get_error_msg(&err);
|
||||
if (err) {
|
||||
printf("run_network_instance error: %s\n", err);
|
||||
free_string(err);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
printf("run_network_instance success\n");
|
||||
|
||||
// 周期性调用 collect_network_infos 并打印
|
||||
const size_t max_infos = 8;
|
||||
KeyValuePair* infos = (KeyValuePair*)malloc(sizeof(KeyValuePair) * max_infos);
|
||||
if (!infos) {
|
||||
fprintf(stderr, "malloc failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; ++i) { // 循环5次作为示例
|
||||
memset(infos, 0, sizeof(KeyValuePair) * max_infos);
|
||||
int count = collect_network_infos(infos, max_infos);
|
||||
if (count < 0) {
|
||||
const char* err = NULL;
|
||||
get_error_msg(&err);
|
||||
if (err) {
|
||||
printf("collect_network_infos error: %s\n", err);
|
||||
free_string(err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
printf("collect_network_infos: %d instance(s)\n", count);
|
||||
for (int j = 0; j < count; ++j) {
|
||||
printf(" [%d] key: %s\n value: %s\n", j, infos[j].key, infos[j].value);
|
||||
free_string(infos[j].key);
|
||||
free_string(infos[j].value);
|
||||
}
|
||||
sleep(1);
|
||||
}
|
||||
free(infos);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
github.com/go-webgpu/goffi v0.4.1 h1:2hQH5XXloxTyTtIleYv+Rajlwzp6UOETURhSZ5+zJxU=
|
||||
github.com/go-webgpu/goffi v0.4.1/go.mod h1:wfoxNsJkU+5RFbV1kNN1kunhc1lFHuJKK3zpgx08/uM=
|
||||
@@ -0,0 +1,482 @@
|
||||
use std::{
|
||||
cell::Cell,
|
||||
collections::HashSet,
|
||||
ffi::{CString, c_char, c_int, c_void},
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use easytier::{
|
||||
common::{
|
||||
MachineIdOptions,
|
||||
config::{ConfigLoader as _, TomlConfigLoader},
|
||||
},
|
||||
web_client::{WebClient, WebClientHooks, parse_config_server_endpoint, run_web_client},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
data_plane::remove_data_plane_sessions_by_instance_ids,
|
||||
error::set_error_msg,
|
||||
state::{ffi_context, resolve_instance_id_by_name},
|
||||
strings::{c_str_to_string, optional_c_str_to_string},
|
||||
types::ConfigServerEventCallback,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static IN_CONFIG_SERVER_CALLBACK: Cell<bool> = const { Cell::new(false) };
|
||||
}
|
||||
|
||||
static CONFIG_SERVER_CLIENT: once_cell::sync::Lazy<Mutex<Option<ManagedConfigServerClient>>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(None));
|
||||
static CONFIG_SERVER_CLIENT_ACTIVE: once_cell::sync::Lazy<AtomicBool> =
|
||||
once_cell::sync::Lazy::new(|| AtomicBool::new(false));
|
||||
static CONFIG_SERVER_CLIENT_STOPPING: once_cell::sync::Lazy<AtomicBool> =
|
||||
once_cell::sync::Lazy::new(|| AtomicBool::new(false));
|
||||
static LAST_CONFIG_SERVER_CALLBACK_ERROR: once_cell::sync::Lazy<Mutex<Option<String>>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(None));
|
||||
|
||||
pub(crate) struct ConfigServerCallbackScope;
|
||||
|
||||
impl ConfigServerCallbackScope {
|
||||
pub(crate) fn enter() -> Self {
|
||||
IN_CONFIG_SERVER_CALLBACK.with(|in_callback| in_callback.set(true));
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConfigServerCallbackScope {
|
||||
fn drop(&mut self) {
|
||||
IN_CONFIG_SERVER_CALLBACK.with(|in_callback| in_callback.set(false));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_config_server_callback() -> bool {
|
||||
IN_CONFIG_SERVER_CALLBACK.with(Cell::get)
|
||||
}
|
||||
|
||||
fn config_server_machine_id_options(machine_id: String) -> MachineIdOptions {
|
||||
MachineIdOptions {
|
||||
explicit_machine_id: Some(machine_id),
|
||||
state_dir: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_config_server_client_options(
|
||||
config_server_url_s: &str,
|
||||
machine_id: &str,
|
||||
) -> Result<(), String> {
|
||||
if machine_id.trim().is_empty() {
|
||||
return Err("machine_id is empty".to_string());
|
||||
}
|
||||
|
||||
parse_config_server_endpoint(config_server_url_s)
|
||||
.map(|_| ())
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
struct ManagedConfigServerClient {
|
||||
client: WebClient,
|
||||
hooks: Arc<ManagedConfigServerClientHooks>,
|
||||
}
|
||||
|
||||
pub(crate) struct ManagedConfigServerClientHooks {
|
||||
pub(crate) instance_ids: Mutex<HashSet<Uuid>>,
|
||||
callback_delivery: Mutex<()>,
|
||||
stopping: AtomicBool,
|
||||
callback: ConfigServerEventCallback,
|
||||
user_data: usize,
|
||||
}
|
||||
|
||||
impl ManagedConfigServerClientHooks {
|
||||
pub(crate) fn new(callback: ConfigServerEventCallback, user_data: *mut c_void) -> Self {
|
||||
Self {
|
||||
instance_ids: Mutex::new(HashSet::new()),
|
||||
callback_delivery: Mutex::new(()),
|
||||
stopping: AtomicBool::new(false),
|
||||
callback,
|
||||
user_data: user_data as usize,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn tracked_instance_ids(&self) -> Vec<Uuid> {
|
||||
self.instance_ids
|
||||
.lock()
|
||||
.map(|guard| guard.iter().copied().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn remove_tracked_instance_ids(&self, ids: &[Uuid]) -> Result<Vec<Uuid>, String> {
|
||||
let mut guard = self.instance_ids.lock().map_err(|err| err.to_string())?;
|
||||
Ok(ids
|
||||
.iter()
|
||||
.filter_map(|id| guard.remove(id).then_some(*id))
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn validate_instance_name(&self, inst_name: &str, inst_id: Uuid) -> Result<(), String> {
|
||||
if let Some(existing_id) =
|
||||
resolve_instance_id_by_name(inst_name).map_err(|error| error.to_string())?
|
||||
&& existing_id != inst_id
|
||||
{
|
||||
return Err(format!("instance name {} already exists", inst_name));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn start_stopping(&self) -> Vec<Uuid> {
|
||||
let _delivery_guard = if in_config_server_callback() {
|
||||
None
|
||||
} else {
|
||||
self.callback_delivery.lock().ok()
|
||||
};
|
||||
let mut guard = match self.instance_ids.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
self.stopping.store(true, Ordering::Release);
|
||||
guard.drain().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn note_callback_error(&self, error: String) {
|
||||
log::warn!("config server event callback failed: {}", error);
|
||||
if let Ok(mut guard) = LAST_CONFIG_SERVER_CALLBACK_ERROR.lock() {
|
||||
*guard = Some(error);
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_event_with_delivery_locked(
|
||||
&self,
|
||||
event: &str,
|
||||
instance_id: Uuid,
|
||||
) -> Result<(), String> {
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(callback) = self.callback else {
|
||||
return Ok(());
|
||||
};
|
||||
let instance_name = ffi_context()
|
||||
.manager
|
||||
.instance(instance_id)
|
||||
.map(|instance| instance.instance_name().to_owned())
|
||||
.unwrap_or_default();
|
||||
let network_name = ffi_context()
|
||||
.manager
|
||||
.config(instance_id)
|
||||
.map(|config| config.get_network_identity().network_name)
|
||||
.unwrap_or_default();
|
||||
let event_json = serde_json::json!({
|
||||
"event": event,
|
||||
"success": true,
|
||||
"instance_id": instance_id.to_string(),
|
||||
"instance_name": instance_name,
|
||||
"network_name": network_name,
|
||||
"error": null,
|
||||
})
|
||||
.to_string();
|
||||
let event_json = CString::new(event_json).map_err(|err| err.to_string())?;
|
||||
let _callback_scope = ConfigServerCallbackScope::enter();
|
||||
unsafe {
|
||||
callback(event_json.as_ptr(), self.user_data as *mut c_void);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_event(&self, event: &str, instance_id: Uuid) -> Result<(), String> {
|
||||
let _delivery_guard = self
|
||||
.callback_delivery
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?;
|
||||
self.emit_event_with_delivery_locked(event, instance_id)
|
||||
}
|
||||
|
||||
fn wait_for_callback_delivery(&self) {
|
||||
if in_config_server_callback() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(guard) = self.callback_delivery.lock() {
|
||||
drop(guard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl WebClientHooks for ManagedConfigServerClientHooks {
|
||||
fn manages_remote_config_instances(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn pre_run_network_instance(&self, cfg: &TomlConfigLoader) -> Result<(), String> {
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
return Err("config server client is stopping".to_string());
|
||||
}
|
||||
|
||||
let inst_name = cfg.get_inst_name();
|
||||
let inst_id = cfg.get_id();
|
||||
|
||||
self.validate_instance_name(&inst_name, inst_id)
|
||||
}
|
||||
|
||||
async fn post_run_network_instance(&self, id: &Uuid) -> Result<(), String> {
|
||||
let _delivery_guard = self
|
||||
.callback_delivery
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?;
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
return Err("config server client is stopping".to_string());
|
||||
}
|
||||
let Some(inst_name) = ffi_context()
|
||||
.manager
|
||||
.instance(*id)
|
||||
.map(|instance| instance.instance_name().to_owned())
|
||||
else {
|
||||
return Err(format!("instance {} not found after start", id));
|
||||
};
|
||||
|
||||
self.instance_ids
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?
|
||||
.insert(*id);
|
||||
if let Err(error) = self.validate_instance_name(&inst_name, *id) {
|
||||
self.remove_tracked_instance_ids(&[*id])?;
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
remove_data_plane_sessions_by_instance_ids(&[*id]);
|
||||
|
||||
if let Err(err) = self.emit_event_with_delivery_locked("run_network_instance", *id) {
|
||||
self.note_callback_error(err);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn post_remove_network_instances(&self, ids: &[Uuid]) -> Result<(), String> {
|
||||
let removed_ids = self.remove_tracked_instance_ids(ids)?;
|
||||
remove_data_plane_sessions_by_instance_ids(&removed_ids);
|
||||
|
||||
for id in removed_ids {
|
||||
if let Err(err) = self.emit_event("delete_network_instance", id) {
|
||||
self.note_callback_error(err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_config_server_tracked_instance_ids(ids: &[Uuid]) {
|
||||
if ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(guard) = CONFIG_SERVER_CLIENT.lock()
|
||||
&& let Some(managed) = guard.as_ref()
|
||||
&& let Err(err) = managed.hooks.remove_tracked_instance_ids(ids)
|
||||
{
|
||||
log::warn!("failed to remove config server tracked ids: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wait_for_config_server_delivery() {
|
||||
let hooks = CONFIG_SERVER_CLIENT
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.as_ref().map(|managed| managed.hooks.clone()));
|
||||
if let Some(hooks) = hooks {
|
||||
hooks.wait_for_callback_delivery();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn last_callback_error() -> Option<String> {
|
||||
LAST_CONFIG_SERVER_CALLBACK_ERROR
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn clear_last_callback_error() {
|
||||
if let Ok(mut guard) = LAST_CONFIG_SERVER_CALLBACK_ERROR.lock() {
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn is_config_server_active_or_stopping() -> bool {
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.load(Ordering::Acquire)
|
||||
|| CONFIG_SERVER_CLIENT_STOPPING.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_active_for_test(active: bool) {
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(active, Ordering::Release);
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Start the config server client.
|
||||
///
|
||||
/// `config_server_url` must be a valid null-terminated UTF-8 string.
|
||||
/// `hostname` may be null; if non-null it must be a valid null-terminated UTF-8 string.
|
||||
/// `machine_id` must be a valid null-terminated UTF-8 string.
|
||||
/// `event_json` passed to `callback` is valid only during that callback invocation.
|
||||
pub(crate) unsafe fn start_config_server_client(
|
||||
config_server_url: *const c_char,
|
||||
hostname: *const c_char,
|
||||
machine_id: *const c_char,
|
||||
secure_mode: bool,
|
||||
callback: ConfigServerEventCallback,
|
||||
user_data: *mut c_void,
|
||||
) -> c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot start config server client from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let config_server_url = match unsafe { c_str_to_string(config_server_url, "config_server_url") }
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let hostname = match unsafe { optional_c_str_to_string(hostname, "hostname") } {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let machine_id = match unsafe { c_str_to_string(machine_id, "machine_id") } {
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
Ok(value) => value,
|
||||
};
|
||||
if let Err(err) = validate_config_server_client_options(&config_server_url, &machine_id) {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
let mut guard = match CONFIG_SERVER_CLIENT.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock config server client: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if guard.is_some() {
|
||||
set_error_msg("config server client already exists");
|
||||
return -1;
|
||||
}
|
||||
if CONFIG_SERVER_CLIENT_STOPPING.load(Ordering::Acquire) {
|
||||
set_error_msg("config server client is stopping");
|
||||
return -1;
|
||||
}
|
||||
clear_last_callback_error();
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
let data_plane_usage_guard = match crate::data_plane::lock_for_config_server_start() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(true, Ordering::Release);
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
drop(data_plane_usage_guard);
|
||||
|
||||
let hooks = Arc::new(ManagedConfigServerClientHooks::new(callback, user_data));
|
||||
let client = match ffi_context().runtime.block_on(run_web_client(
|
||||
&config_server_url,
|
||||
config_server_machine_id_options(machine_id),
|
||||
hostname,
|
||||
secure_mode,
|
||||
ffi_context().manager.clone(),
|
||||
Some(hooks.clone()),
|
||||
)) {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||
set_error_msg(&format!("failed to start config server client: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
*guard = Some(ManagedConfigServerClient { client, hooks });
|
||||
0
|
||||
}
|
||||
|
||||
pub(crate) fn stop_config_server_client() -> c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot stop config server client from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let guard = match CONFIG_SERVER_CLIENT.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock config server client: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(managed) = guard.as_ref() else {
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||
return 0;
|
||||
};
|
||||
if CONFIG_SERVER_CLIENT_STOPPING.swap(true, Ordering::AcqRel) {
|
||||
set_error_msg("config server client is stopping");
|
||||
return -1;
|
||||
}
|
||||
let hooks = managed.hooks.clone();
|
||||
// Keep the client discoverable until the canonical transaction drains its
|
||||
// tracking. Earlier removals must still retire IDs from these same hooks.
|
||||
drop(guard);
|
||||
|
||||
let delete_result = ffi_context().runtime.block_on(
|
||||
ffi_context()
|
||||
.process_management
|
||||
.delete_owned_network_instances_selected_by(|| hooks.start_stopping()),
|
||||
);
|
||||
let managed = match CONFIG_SERVER_CLIENT.lock() {
|
||||
Ok(mut guard) => guard.take(),
|
||||
Err(err) => {
|
||||
hooks.wait_for_callback_delivery();
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||
set_error_msg(&format!("failed to lock config server client: {err}"));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
drop(managed);
|
||||
hooks.wait_for_callback_delivery();
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||
CONFIG_SERVER_CLIENT_STOPPING.store(false, Ordering::Release);
|
||||
|
||||
if let Err(err) = delete_result {
|
||||
set_error_msg(&format!(
|
||||
"failed to delete config server instances: {}",
|
||||
err
|
||||
));
|
||||
return -1;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
pub(crate) fn is_config_server_client_connected() -> c_int {
|
||||
CONFIG_SERVER_CLIENT
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.as_ref().map(|managed| managed.client.is_connected()))
|
||||
.map(i32::from)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
@@ -0,0 +1,685 @@
|
||||
use std::{
|
||||
ffi::{c_char, c_int, c_uchar},
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
ptr,
|
||||
};
|
||||
|
||||
use easytier_core::gateway::DataPlaneErrorKind;
|
||||
|
||||
use super::session::{self, NativeDataPlaneError, NativeDataPlaneResult};
|
||||
use crate::{
|
||||
error::set_error_msg,
|
||||
strings::c_str_to_string,
|
||||
types::{DataPlaneCompletion, DataPlaneSocketAddr},
|
||||
};
|
||||
|
||||
pub const DATA_PLANE_DEADLINE_READ: u32 = 1 << 0;
|
||||
pub const DATA_PLANE_DEADLINE_WRITE: u32 = 1 << 1;
|
||||
|
||||
fn failure(error: NativeDataPlaneError) -> c_int {
|
||||
set_error_msg(&error.message);
|
||||
-(error.kind as c_int)
|
||||
}
|
||||
|
||||
fn status(result: NativeDataPlaneResult<()>) -> c_int {
|
||||
match result {
|
||||
Ok(()) => 0,
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid(message: impl Into<String>) -> NativeDataPlaneError {
|
||||
NativeDataPlaneError {
|
||||
kind: DataPlaneErrorKind::Io,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn socket_addr(address: DataPlaneSocketAddr) -> NativeDataPlaneResult<SocketAddr> {
|
||||
let ip = match address.family {
|
||||
4 => IpAddr::V4(Ipv4Addr::new(
|
||||
address.address[0],
|
||||
address.address[1],
|
||||
address.address[2],
|
||||
address.address[3],
|
||||
)),
|
||||
6 => {
|
||||
return Err(NativeDataPlaneError {
|
||||
kind: DataPlaneErrorKind::AddressFamilyUnsupported,
|
||||
message: "IPv6 is not supported by data-plane ABI v3".to_string(),
|
||||
});
|
||||
}
|
||||
family => {
|
||||
return Err(NativeDataPlaneError {
|
||||
kind: DataPlaneErrorKind::AddressFamilyUnsupported,
|
||||
message: format!("unsupported address family {family}"),
|
||||
});
|
||||
}
|
||||
};
|
||||
Ok(SocketAddr::new(ip, address.port))
|
||||
}
|
||||
|
||||
fn ffi_socket_addr(address: SocketAddr) -> DataPlaneSocketAddr {
|
||||
match address.ip() {
|
||||
IpAddr::V4(ip) => {
|
||||
let mut bytes = [0; 16];
|
||||
bytes[..4].copy_from_slice(&ip.octets());
|
||||
DataPlaneSocketAddr {
|
||||
family: 4,
|
||||
port: address.port(),
|
||||
address: bytes,
|
||||
}
|
||||
}
|
||||
IpAddr::V6(ip) => DataPlaneSocketAddr {
|
||||
family: 6,
|
||||
port: address.port(),
|
||||
address: ip.octets(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn copy_input(ptr: *const c_uchar, len: u32) -> NativeDataPlaneResult<Vec<u8>> {
|
||||
if len == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if ptr.is_null() {
|
||||
return Err(invalid("input buffer is null"));
|
||||
}
|
||||
Ok(unsafe { std::slice::from_raw_parts(ptr, len as usize) }.to_vec())
|
||||
}
|
||||
|
||||
unsafe fn output_slice<'a>(ptr: *mut c_uchar, len: u32) -> NativeDataPlaneResult<&'a mut [u8]> {
|
||||
if len == 0 {
|
||||
return Ok(&mut []);
|
||||
}
|
||||
if ptr.is_null() {
|
||||
return Err(invalid("output buffer is null"));
|
||||
}
|
||||
Ok(unsafe { std::slice::from_raw_parts_mut(ptr, len as usize) })
|
||||
}
|
||||
|
||||
fn write_operation(
|
||||
out_operation: *mut u64,
|
||||
submit: impl FnOnce() -> NativeDataPlaneResult<u64>,
|
||||
) -> c_int {
|
||||
if out_operation.is_null() {
|
||||
return failure(invalid("out_operation is null"));
|
||||
}
|
||||
match submit() {
|
||||
Ok(operation) => {
|
||||
unsafe {
|
||||
*out_operation = operation;
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// If non-null, `inst_name` must point to a valid NUL-terminated string.
|
||||
/// `out_session` must be null or point to writable, properly aligned storage
|
||||
/// for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_session_open(
|
||||
inst_name: *const c_char,
|
||||
out_session: *mut u64,
|
||||
) -> c_int {
|
||||
if out_session.is_null() {
|
||||
return failure(invalid("out_session is null"));
|
||||
}
|
||||
unsafe {
|
||||
*out_session = 0;
|
||||
}
|
||||
let inst_name = match unsafe { c_str_to_string(inst_name, "inst_name") } {
|
||||
Ok(inst_name) => inst_name,
|
||||
Err(error) => return failure(invalid(error)),
|
||||
};
|
||||
match session::open(&inst_name) {
|
||||
Ok(handle) => {
|
||||
unsafe {
|
||||
*out_session = handle;
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_session_close(session: u64) -> c_int {
|
||||
status(super::session::close(session))
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_operation` must be null or point to writable, properly aligned
|
||||
/// storage for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_connect_submit(
|
||||
session: u64,
|
||||
peer_addr: DataPlaneSocketAddr,
|
||||
timeout_ms: u64,
|
||||
out_operation: *mut u64,
|
||||
) -> c_int {
|
||||
let peer_addr = match socket_addr(peer_addr) {
|
||||
Ok(address) => address,
|
||||
Err(error) => return failure(error),
|
||||
};
|
||||
write_operation(out_operation, || {
|
||||
super::session::submit_tcp_connect(session, peer_addr, timeout_ms)
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_operation` must be null or point to writable, properly aligned
|
||||
/// storage for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_bind_submit(
|
||||
session: u64,
|
||||
local_port: u16,
|
||||
timeout_ms: u64,
|
||||
out_operation: *mut u64,
|
||||
) -> c_int {
|
||||
write_operation(out_operation, || {
|
||||
super::session::submit_tcp_bind(session, local_port, timeout_ms)
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_operation` must be null or point to writable, properly aligned
|
||||
/// storage for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_accept_submit(
|
||||
session: u64,
|
||||
listener: u64,
|
||||
timeout_ms: u64,
|
||||
out_operation: *mut u64,
|
||||
) -> c_int {
|
||||
write_operation(out_operation, || {
|
||||
super::session::submit_tcp_accept(session, listener, timeout_ms)
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_operation` must be null or point to writable, properly aligned
|
||||
/// storage for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_read_submit(
|
||||
session: u64,
|
||||
stream: u64,
|
||||
max_len: u32,
|
||||
out_operation: *mut u64,
|
||||
) -> c_int {
|
||||
write_operation(out_operation, || {
|
||||
super::session::submit_tcp_read(session, stream, max_len)
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// When `len` is nonzero, `data` must point to `len` readable bytes.
|
||||
/// `out_operation` must be null or point to writable, properly aligned
|
||||
/// storage for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_write_submit(
|
||||
session: u64,
|
||||
stream: u64,
|
||||
data: *const c_uchar,
|
||||
len: u32,
|
||||
out_operation: *mut u64,
|
||||
) -> c_int {
|
||||
let data = match unsafe { copy_input(data, len) } {
|
||||
Ok(data) => data,
|
||||
Err(error) => return failure(error),
|
||||
};
|
||||
write_operation(out_operation, || {
|
||||
super::session::submit_tcp_write(session, stream, data)
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_operation` must be null or point to writable, properly aligned
|
||||
/// storage for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_bind_submit(
|
||||
session: u64,
|
||||
local_port: u16,
|
||||
timeout_ms: u64,
|
||||
out_operation: *mut u64,
|
||||
) -> c_int {
|
||||
write_operation(out_operation, || {
|
||||
super::session::submit_udp_bind(session, local_port, timeout_ms)
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_operation` must be null or point to writable, properly aligned
|
||||
/// storage for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_receive_submit(
|
||||
session: u64,
|
||||
socket: u64,
|
||||
max_len: u32,
|
||||
out_operation: *mut u64,
|
||||
) -> c_int {
|
||||
write_operation(out_operation, || {
|
||||
super::session::submit_udp_receive(session, socket, max_len)
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// When `len` is nonzero, `data` must point to `len` readable bytes.
|
||||
/// `out_operation` must be null or point to writable, properly aligned
|
||||
/// storage for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_send_submit(
|
||||
session: u64,
|
||||
socket: u64,
|
||||
peer_addr: DataPlaneSocketAddr,
|
||||
data: *const c_uchar,
|
||||
len: u32,
|
||||
out_operation: *mut u64,
|
||||
) -> c_int {
|
||||
let peer_addr = match socket_addr(peer_addr) {
|
||||
Ok(address) => address,
|
||||
Err(error) => return failure(error),
|
||||
};
|
||||
let data = match unsafe { copy_input(data, len) } {
|
||||
Ok(data) => data,
|
||||
Err(error) => return failure(error),
|
||||
};
|
||||
write_operation(out_operation, || {
|
||||
super::session::submit_udp_send(session, socket, peer_addr, data)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_resource_deadline_set(
|
||||
session: u64,
|
||||
resource: u64,
|
||||
direction: u32,
|
||||
timeout_ms: u64,
|
||||
) -> c_int {
|
||||
let read = direction & DATA_PLANE_DEADLINE_READ != 0;
|
||||
let write = direction & DATA_PLANE_DEADLINE_WRITE != 0;
|
||||
if direction == 0 || direction & !(DATA_PLANE_DEADLINE_READ | DATA_PLANE_DEADLINE_WRITE) != 0 {
|
||||
return failure(invalid(format!("invalid deadline direction {direction}")));
|
||||
}
|
||||
status(super::session::set_resource_deadline(
|
||||
session, resource, read, write, timeout_ms,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_operation_cancel(session: u64, operation: u64) -> c_int {
|
||||
status(super::session::cancel_operation(session, operation))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_operation_free(session: u64, operation: u64) -> c_int {
|
||||
status(super::session::free_operation(session, operation))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_resource_close(session: u64, resource: u64) -> c_int {
|
||||
status(super::session::close_resource(session, resource))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_completion_wait(session: u64, timeout_ms: u64) -> c_int {
|
||||
match super::session::completion_wait(session, timeout_ms) {
|
||||
Ok(true) => 1,
|
||||
Ok(false) => 0,
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// When `capacity` is nonzero, `completions` must point to writable, properly
|
||||
/// aligned storage for `capacity` consecutive [`DataPlaneCompletion`] values.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_completion_drain(
|
||||
session: u64,
|
||||
completions: *mut DataPlaneCompletion,
|
||||
capacity: u32,
|
||||
) -> c_int {
|
||||
if capacity != 0 && completions.is_null() {
|
||||
return failure(invalid("completions is null"));
|
||||
}
|
||||
let drained = match super::session::drain_completions(session, capacity as usize) {
|
||||
Ok(drained) => drained,
|
||||
Err(error) => return failure(error),
|
||||
};
|
||||
for (index, completion) in drained.iter().enumerate() {
|
||||
unsafe {
|
||||
ptr::write(
|
||||
completions.add(index),
|
||||
DataPlaneCompletion {
|
||||
operation_id: completion.operation_id.get(),
|
||||
operation_kind: completion.kind as u16,
|
||||
status: completion.status.code(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
drained.len() as c_int
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_size` must be null or point to writable, properly aligned storage for
|
||||
/// one `u32`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_result_size(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
out_size: *mut u32,
|
||||
) -> c_int {
|
||||
if out_size.is_null() {
|
||||
return failure(invalid("out_size is null"));
|
||||
}
|
||||
match super::session::result_size(session, operation) {
|
||||
Ok(size) => match u32::try_from(size) {
|
||||
Ok(size) => {
|
||||
unsafe {
|
||||
*out_size = size;
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(_) => failure(invalid("data-plane result size exceeds u32")),
|
||||
},
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Each output pointer must be null or point to writable, properly aligned
|
||||
/// storage for its pointee type. Non-null output locations must not overlap.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_connect_result_take(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
out_stream: *mut u64,
|
||||
out_local_addr: *mut DataPlaneSocketAddr,
|
||||
out_peer_addr: *mut DataPlaneSocketAddr,
|
||||
) -> c_int {
|
||||
if out_stream.is_null() || out_local_addr.is_null() || out_peer_addr.is_null() {
|
||||
return failure(invalid("TCP connect result output pointer is null"));
|
||||
}
|
||||
match super::session::take_tcp_connect(session, operation) {
|
||||
Ok(result) => {
|
||||
unsafe {
|
||||
*out_stream = result.stream;
|
||||
*out_local_addr = ffi_socket_addr(result.local_addr);
|
||||
*out_peer_addr = ffi_socket_addr(result.peer_addr);
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Each output pointer must be null or point to writable, properly aligned
|
||||
/// storage for its pointee type. Non-null output locations must not overlap.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_bind_result_take(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
out_listener: *mut u64,
|
||||
out_local_addr: *mut DataPlaneSocketAddr,
|
||||
) -> c_int {
|
||||
if out_listener.is_null() || out_local_addr.is_null() {
|
||||
return failure(invalid("TCP bind result output pointer is null"));
|
||||
}
|
||||
match super::session::take_tcp_bind(session, operation) {
|
||||
Ok(result) => {
|
||||
unsafe {
|
||||
*out_listener = result.listener;
|
||||
*out_local_addr = ffi_socket_addr(result.local_addr);
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Each output pointer must be null or point to writable, properly aligned
|
||||
/// storage for its pointee type. Non-null output locations must not overlap.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_accept_result_take(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
out_stream: *mut u64,
|
||||
out_local_addr: *mut DataPlaneSocketAddr,
|
||||
out_peer_addr: *mut DataPlaneSocketAddr,
|
||||
) -> c_int {
|
||||
if out_stream.is_null() || out_local_addr.is_null() || out_peer_addr.is_null() {
|
||||
return failure(invalid("TCP accept result output pointer is null"));
|
||||
}
|
||||
match super::session::take_tcp_accept(session, operation) {
|
||||
Ok(result) => {
|
||||
unsafe {
|
||||
*out_stream = result.stream;
|
||||
*out_local_addr = ffi_socket_addr(result.local_addr);
|
||||
*out_peer_addr = ffi_socket_addr(result.peer_addr);
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// When `capacity` is nonzero, `data` must point to `capacity` writable bytes.
|
||||
/// Each scalar output pointer must be null or point to writable, properly
|
||||
/// aligned storage for its pointee type. Non-null output ranges must not
|
||||
/// overlap.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_read_result_take(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
data: *mut c_uchar,
|
||||
capacity: u32,
|
||||
out_len: *mut u32,
|
||||
out_eof: *mut bool,
|
||||
) -> c_int {
|
||||
if out_len.is_null() || out_eof.is_null() {
|
||||
return failure(invalid("TCP read result output pointer is null"));
|
||||
}
|
||||
let data = match unsafe { output_slice(data, capacity) } {
|
||||
Ok(data) => data,
|
||||
Err(error) => return failure(error),
|
||||
};
|
||||
match super::session::take_tcp_read(session, operation, data) {
|
||||
Ok(result) => {
|
||||
unsafe {
|
||||
*out_len = result.len as u32;
|
||||
*out_eof = result.eof;
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_len` must be null or point to writable, properly aligned storage for
|
||||
/// one `u32`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_write_result_take(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
out_len: *mut u32,
|
||||
) -> c_int {
|
||||
if out_len.is_null() {
|
||||
return failure(invalid("out_len is null"));
|
||||
}
|
||||
match super::session::take_tcp_write(session, operation) {
|
||||
Ok(len) => match u32::try_from(len) {
|
||||
Ok(len) => {
|
||||
unsafe {
|
||||
*out_len = len;
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(_) => failure(invalid("TCP write result exceeds u32")),
|
||||
},
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Each output pointer must be null or point to writable, properly aligned
|
||||
/// storage for its pointee type. Non-null output locations must not overlap.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_bind_result_take(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
out_socket: *mut u64,
|
||||
out_local_addr: *mut DataPlaneSocketAddr,
|
||||
) -> c_int {
|
||||
if out_socket.is_null() || out_local_addr.is_null() {
|
||||
return failure(invalid("UDP bind result output pointer is null"));
|
||||
}
|
||||
match super::session::take_udp_bind(session, operation) {
|
||||
Ok(result) => {
|
||||
unsafe {
|
||||
*out_socket = result.socket;
|
||||
*out_local_addr = ffi_socket_addr(result.local_addr);
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// When `capacity` is nonzero, `data` must point to `capacity` writable bytes.
|
||||
/// Each scalar output pointer must be null or point to writable, properly
|
||||
/// aligned storage for its pointee type. Non-null output ranges must not
|
||||
/// overlap.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_receive_result_take(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
data: *mut c_uchar,
|
||||
capacity: u32,
|
||||
out_len: *mut u32,
|
||||
out_peer_addr: *mut DataPlaneSocketAddr,
|
||||
out_truncated: *mut bool,
|
||||
) -> c_int {
|
||||
if out_len.is_null() || out_peer_addr.is_null() || out_truncated.is_null() {
|
||||
return failure(invalid("UDP receive result output pointer is null"));
|
||||
}
|
||||
let data = match unsafe { output_slice(data, capacity) } {
|
||||
Ok(data) => data,
|
||||
Err(error) => return failure(error),
|
||||
};
|
||||
match super::session::take_udp_receive(session, operation, data) {
|
||||
Ok(result) => {
|
||||
unsafe {
|
||||
*out_len = result.len as u32;
|
||||
*out_peer_addr = ffi_socket_addr(result.peer_addr);
|
||||
*out_truncated = result.truncated;
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_len` must be null or point to writable, properly aligned storage for
|
||||
/// one `u32`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_send_result_take(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
out_len: *mut u32,
|
||||
) -> c_int {
|
||||
if out_len.is_null() {
|
||||
return failure(invalid("out_len is null"));
|
||||
}
|
||||
match super::session::take_udp_send(session, operation) {
|
||||
Ok(len) => match u32::try_from(len) {
|
||||
Ok(len) => {
|
||||
unsafe {
|
||||
*out_len = len;
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(_) => failure(invalid("UDP send result exceeds u32")),
|
||||
},
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn socket_address_round_trip() {
|
||||
let address = "127.0.0.1:1234".parse::<SocketAddr>().unwrap();
|
||||
assert_eq!(socket_addr(ffi_socket_addr(address)).unwrap(), address);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_is_rejected_by_v3() {
|
||||
let error = socket_addr(ffi_socket_addr(
|
||||
"[2001:db8::1]:4321".parse::<SocketAddr>().unwrap(),
|
||||
))
|
||||
.unwrap_err();
|
||||
assert_eq!(error.kind, DataPlaneErrorKind::AddressFamilyUnsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_address_family_is_stable() {
|
||||
let error = socket_addr(DataPlaneSocketAddr {
|
||||
family: 9,
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap_err();
|
||||
assert_eq!(error.kind, DataPlaneErrorKind::AddressFamilyUnsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_deadline_direction_is_rejected_before_session_lookup() {
|
||||
let invalid = -(DataPlaneErrorKind::Io as c_int);
|
||||
assert_eq!(data_plane_resource_deadline_set(u64::MAX, 1, 0, 0), invalid);
|
||||
assert_eq!(data_plane_resource_deadline_set(u64::MAX, 1, 4, 0), invalid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_operation_output_does_not_submit() {
|
||||
let submitted = std::cell::Cell::new(false);
|
||||
|
||||
assert_eq!(
|
||||
write_operation(std::ptr::null_mut(), || {
|
||||
submitted.set(true);
|
||||
Ok(1)
|
||||
}),
|
||||
-(DataPlaneErrorKind::Io as c_int)
|
||||
);
|
||||
assert!(!submitted.get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//! Native C ABI adapter for the instance-scoped data-plane operation broker.
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
mod abi;
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
mod session;
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub use abi::*;
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) use session::{
|
||||
lock_for_config_server_start, remove_data_plane_sessions_by_instance_ids,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "ffi-dataplane"))]
|
||||
pub(crate) fn remove_data_plane_sessions_by_instance_ids(_ids: &[uuid::Uuid]) {}
|
||||
@@ -0,0 +1,646 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::SocketAddr,
|
||||
sync::{
|
||||
Arc, Mutex, RwLock,
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use easytier::instance::host::NativeInstanceHost;
|
||||
use easytier_core::gateway::{
|
||||
DataPlaneCompletionDescriptor, DataPlaneError, DataPlaneErrorKind, DataPlaneOperationId,
|
||||
DataPlaneOperationKind, DataPlaneOperationResult, DataPlaneResourceId, DataPlaneSession,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
config_server::{in_config_server_callback, is_config_server_active_or_stopping},
|
||||
state::{ffi_context, resolve_instance_id_by_name},
|
||||
};
|
||||
|
||||
type CoreDataPlaneSession = DataPlaneSession<NativeInstanceHost>;
|
||||
|
||||
static NEXT_SESSION_HANDLE: AtomicU64 = AtomicU64::new(1);
|
||||
static SESSIONS: once_cell::sync::Lazy<Mutex<HashMap<u64, Arc<NativeDataPlaneSession>>>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
static DATA_PLANE_USAGE_LOCK: once_cell::sync::Lazy<RwLock<()>> =
|
||||
once_cell::sync::Lazy::new(|| RwLock::new(()));
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct NativeDataPlaneError {
|
||||
pub(super) kind: DataPlaneErrorKind,
|
||||
pub(super) message: String,
|
||||
}
|
||||
|
||||
impl NativeDataPlaneError {
|
||||
fn new(kind: DataPlaneErrorKind, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid(message: impl Into<String>) -> Self {
|
||||
Self::new(DataPlaneErrorKind::Io, message)
|
||||
}
|
||||
|
||||
fn closed(message: impl Into<String>) -> Self {
|
||||
Self::new(DataPlaneErrorKind::HandleClosed, message)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DataPlaneError> for NativeDataPlaneError {
|
||||
fn from(error: DataPlaneError) -> Self {
|
||||
Self::new(error.kind(), error.message())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) type NativeDataPlaneResult<T> = Result<T, NativeDataPlaneError>;
|
||||
|
||||
pub(super) struct TcpConnectResult {
|
||||
pub(super) stream: u64,
|
||||
pub(super) local_addr: SocketAddr,
|
||||
pub(super) peer_addr: SocketAddr,
|
||||
}
|
||||
|
||||
pub(super) struct TcpBindResult {
|
||||
pub(super) listener: u64,
|
||||
pub(super) local_addr: SocketAddr,
|
||||
}
|
||||
|
||||
pub(super) struct TcpAcceptResult {
|
||||
pub(super) stream: u64,
|
||||
pub(super) local_addr: SocketAddr,
|
||||
pub(super) peer_addr: SocketAddr,
|
||||
}
|
||||
|
||||
pub(super) struct TcpReadResult {
|
||||
pub(super) len: usize,
|
||||
pub(super) eof: bool,
|
||||
}
|
||||
|
||||
pub(super) struct UdpBindResult {
|
||||
pub(super) socket: u64,
|
||||
pub(super) local_addr: SocketAddr,
|
||||
}
|
||||
|
||||
pub(super) struct UdpReceiveResult {
|
||||
pub(super) len: usize,
|
||||
pub(super) peer_addr: SocketAddr,
|
||||
pub(super) truncated: bool,
|
||||
}
|
||||
|
||||
struct NativeDataPlaneSession {
|
||||
instance_id: Uuid,
|
||||
runtime: tokio::runtime::Handle,
|
||||
core: Arc<CoreDataPlaneSession>,
|
||||
submit_gate: Mutex<()>,
|
||||
closed: AtomicBool,
|
||||
}
|
||||
|
||||
impl NativeDataPlaneSession {
|
||||
fn close(&self) {
|
||||
let _gate = self
|
||||
.submit_gate
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
if self.closed.swap(true, Ordering::AcqRel) {
|
||||
return;
|
||||
}
|
||||
self.core.discard_all();
|
||||
}
|
||||
|
||||
fn call<T>(
|
||||
&self,
|
||||
call: impl FnOnce(&Arc<CoreDataPlaneSession>) -> Result<T, DataPlaneError>,
|
||||
) -> NativeDataPlaneResult<T> {
|
||||
let _gate = self
|
||||
.submit_gate
|
||||
.lock()
|
||||
.map_err(|error| NativeDataPlaneError::invalid(error.to_string()))?;
|
||||
if self.closed.load(Ordering::Acquire) {
|
||||
return Err(NativeDataPlaneError::closed(
|
||||
"native data-plane session is closed",
|
||||
));
|
||||
}
|
||||
let _runtime = self.runtime.enter();
|
||||
call(&self.core).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn submit(
|
||||
&self,
|
||||
submit: impl FnOnce(&Arc<CoreDataPlaneSession>) -> Result<DataPlaneOperationId, DataPlaneError>,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
self.call(submit).map(DataPlaneOperationId::get)
|
||||
}
|
||||
}
|
||||
|
||||
fn sessions()
|
||||
-> NativeDataPlaneResult<std::sync::MutexGuard<'static, HashMap<u64, Arc<NativeDataPlaneSession>>>>
|
||||
{
|
||||
SESSIONS
|
||||
.lock()
|
||||
.map_err(|error| NativeDataPlaneError::invalid(error.to_string()))
|
||||
}
|
||||
|
||||
fn get_session(handle: u64) -> NativeDataPlaneResult<Arc<NativeDataPlaneSession>> {
|
||||
if handle == 0 {
|
||||
return Err(NativeDataPlaneError::closed(
|
||||
"native data-plane session handle is invalid",
|
||||
));
|
||||
}
|
||||
let session = sessions()?
|
||||
.get(&handle)
|
||||
.cloned()
|
||||
.ok_or_else(|| NativeDataPlaneError::closed("native data-plane session is closed"))?;
|
||||
if session.closed.load(Ordering::Acquire) {
|
||||
return Err(NativeDataPlaneError::closed(
|
||||
"native data-plane session is closed",
|
||||
));
|
||||
}
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
fn next_session_handle(
|
||||
sessions: &HashMap<u64, Arc<NativeDataPlaneSession>>,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
for _ in 0..sessions.len().saturating_add(2) {
|
||||
let handle = NEXT_SESSION_HANDLE.fetch_add(1, Ordering::Relaxed);
|
||||
if handle != 0 && !sessions.contains_key(&handle) {
|
||||
return Ok(handle);
|
||||
}
|
||||
}
|
||||
Err(NativeDataPlaneError::new(
|
||||
DataPlaneErrorKind::ResourceLimit,
|
||||
"native data-plane session handle space is exhausted",
|
||||
))
|
||||
}
|
||||
|
||||
fn reject_data_plane_use() -> NativeDataPlaneResult<()> {
|
||||
if in_config_server_callback() {
|
||||
Err(NativeDataPlaneError::invalid(
|
||||
"cannot use data plane from config server callback",
|
||||
))
|
||||
} else if is_config_server_active_or_stopping() {
|
||||
Err(NativeDataPlaneError::invalid(
|
||||
"cannot use data plane while config server client is active",
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn open(inst_name: &str) -> NativeDataPlaneResult<u64> {
|
||||
reject_data_plane_use()?;
|
||||
let _usage = DATA_PLANE_USAGE_LOCK
|
||||
.read()
|
||||
.map_err(|error| NativeDataPlaneError::invalid(error.to_string()))?;
|
||||
reject_data_plane_use()?;
|
||||
|
||||
let instance_id = resolve_instance_id_by_name(inst_name)
|
||||
.map_err(NativeDataPlaneError::invalid)?
|
||||
.ok_or_else(|| NativeDataPlaneError::closed("instance not found"))?;
|
||||
let manager = &ffi_context().manager;
|
||||
let core = manager.data_plane_session(&instance_id).ok_or_else(|| {
|
||||
NativeDataPlaneError::closed("instance data-plane session is unavailable")
|
||||
})?;
|
||||
let runtime = manager
|
||||
.data_plane_runtime_handle(&instance_id)
|
||||
.ok_or_else(|| NativeDataPlaneError::closed("instance runtime is unavailable"))?;
|
||||
|
||||
let mut sessions = sessions()?;
|
||||
if sessions
|
||||
.values()
|
||||
.any(|session| session.instance_id == instance_id)
|
||||
{
|
||||
return Err(NativeDataPlaneError::new(
|
||||
DataPlaneErrorKind::ResourceLimit,
|
||||
"instance already has an open native data-plane session",
|
||||
));
|
||||
}
|
||||
let handle = next_session_handle(&sessions)?;
|
||||
sessions.insert(
|
||||
handle,
|
||||
Arc::new(NativeDataPlaneSession {
|
||||
instance_id,
|
||||
runtime,
|
||||
core,
|
||||
submit_gate: Mutex::new(()),
|
||||
closed: AtomicBool::new(false),
|
||||
}),
|
||||
);
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
pub(super) fn close(handle: u64) -> NativeDataPlaneResult<()> {
|
||||
let _usage = DATA_PLANE_USAGE_LOCK
|
||||
.read()
|
||||
.map_err(|error| NativeDataPlaneError::invalid(error.to_string()))?;
|
||||
let mut sessions = sessions()?;
|
||||
let session = sessions
|
||||
.remove(&handle)
|
||||
.ok_or_else(|| NativeDataPlaneError::closed("native data-plane session is closed"))?;
|
||||
// Keep the registry locked until the shared core namespace is empty. An
|
||||
// open for the same instance must not publish a replacement session before
|
||||
// this old wrapper finishes discarding its operations and resources.
|
||||
session.close();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn timeout(timeout_ms: u64) -> Option<Duration> {
|
||||
(timeout_ms != u64::MAX).then(|| Duration::from_millis(timeout_ms))
|
||||
}
|
||||
|
||||
fn operation_id(raw: u64) -> NativeDataPlaneResult<DataPlaneOperationId> {
|
||||
DataPlaneOperationId::from_raw(raw)
|
||||
.ok_or_else(|| NativeDataPlaneError::closed("data-plane operation handle is invalid"))
|
||||
}
|
||||
|
||||
fn resource_id(raw: u64) -> NativeDataPlaneResult<DataPlaneResourceId> {
|
||||
DataPlaneResourceId::from_raw(raw)
|
||||
.ok_or_else(|| NativeDataPlaneError::closed("data-plane resource handle is invalid"))
|
||||
}
|
||||
|
||||
pub(super) fn submit_tcp_connect(
|
||||
session: u64,
|
||||
peer_addr: SocketAddr,
|
||||
timeout_ms: u64,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
get_session(session)?.submit(|core| core.submit_tcp_connect(peer_addr, timeout(timeout_ms)))
|
||||
}
|
||||
|
||||
pub(super) fn submit_tcp_bind(
|
||||
session: u64,
|
||||
local_port: u16,
|
||||
timeout_ms: u64,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
get_session(session)?.submit(|core| core.submit_tcp_bind(local_port, timeout(timeout_ms)))
|
||||
}
|
||||
|
||||
pub(super) fn submit_tcp_accept(
|
||||
session: u64,
|
||||
listener: u64,
|
||||
timeout_ms: u64,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
let listener = resource_id(listener)?;
|
||||
get_session(session)?.submit(|core| core.submit_tcp_accept(listener, timeout(timeout_ms)))
|
||||
}
|
||||
|
||||
pub(super) fn submit_tcp_read(
|
||||
session: u64,
|
||||
stream: u64,
|
||||
max_len: u32,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
let stream = resource_id(stream)?;
|
||||
get_session(session)?.submit(|core| core.submit_tcp_read(stream, max_len as usize))
|
||||
}
|
||||
|
||||
pub(super) fn submit_tcp_write(
|
||||
session: u64,
|
||||
stream: u64,
|
||||
data: Vec<u8>,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
let stream = resource_id(stream)?;
|
||||
get_session(session)?.submit(|core| core.submit_tcp_write(stream, data))
|
||||
}
|
||||
|
||||
pub(super) fn submit_udp_bind(
|
||||
session: u64,
|
||||
local_port: u16,
|
||||
timeout_ms: u64,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
get_session(session)?.submit(|core| core.submit_udp_bind(local_port, timeout(timeout_ms)))
|
||||
}
|
||||
|
||||
pub(super) fn submit_udp_receive(
|
||||
session: u64,
|
||||
socket: u64,
|
||||
max_len: u32,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
let socket = resource_id(socket)?;
|
||||
get_session(session)?.submit(|core| core.submit_udp_receive(socket, max_len as usize))
|
||||
}
|
||||
|
||||
pub(super) fn submit_udp_send(
|
||||
session: u64,
|
||||
socket: u64,
|
||||
peer_addr: SocketAddr,
|
||||
data: Vec<u8>,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
let socket = resource_id(socket)?;
|
||||
get_session(session)?.submit(|core| core.submit_udp_send(socket, peer_addr, data))
|
||||
}
|
||||
|
||||
pub(super) fn set_resource_deadline(
|
||||
session: u64,
|
||||
resource: u64,
|
||||
read: bool,
|
||||
write: bool,
|
||||
timeout_ms: u64,
|
||||
) -> NativeDataPlaneResult<()> {
|
||||
let resource = resource_id(resource)?;
|
||||
get_session(session)?
|
||||
.call(|core| core.set_resource_deadline(resource, read, write, timeout(timeout_ms)))
|
||||
}
|
||||
|
||||
pub(super) fn cancel_operation(session: u64, operation: u64) -> NativeDataPlaneResult<()> {
|
||||
let operation = operation_id(operation)?;
|
||||
get_session(session)?.core.cancel_operation(operation);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn free_operation(session: u64, operation: u64) -> NativeDataPlaneResult<()> {
|
||||
let operation = operation_id(operation)?;
|
||||
get_session(session)?.core.free_operation(operation);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn close_resource(session: u64, resource: u64) -> NativeDataPlaneResult<()> {
|
||||
let resource = resource_id(resource)?;
|
||||
get_session(session)?.core.close_resource(resource);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn completion_wait(session: u64, timeout_ms: u64) -> NativeDataPlaneResult<bool> {
|
||||
let session = get_session(session)?;
|
||||
let ready = session.core.completion_wait(timeout(timeout_ms));
|
||||
Ok(ready && !session.closed.load(Ordering::Acquire))
|
||||
}
|
||||
|
||||
pub(super) fn drain_completions(
|
||||
session: u64,
|
||||
max_count: usize,
|
||||
) -> NativeDataPlaneResult<Vec<DataPlaneCompletionDescriptor>> {
|
||||
Ok(get_session(session)?.core.drain_completions(max_count))
|
||||
}
|
||||
|
||||
pub(super) fn result_size(session: u64, operation: u64) -> NativeDataPlaneResult<usize> {
|
||||
let operation = operation_id(operation)?;
|
||||
get_session(session)?
|
||||
.core
|
||||
.result_payload_bytes(operation)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn take_result<T>(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
expected: DataPlaneOperationKind,
|
||||
take: impl FnOnce(&DataPlaneOperationResult) -> Option<T>,
|
||||
) -> NativeDataPlaneResult<T> {
|
||||
let operation = operation_id(operation)?;
|
||||
let session = get_session(session)?;
|
||||
let actual = session.core.operation_kind(operation)?;
|
||||
if actual != expected {
|
||||
return Err(NativeDataPlaneError::invalid(format!(
|
||||
"operation kind mismatch: expected {expected:?}, got {actual:?}"
|
||||
)));
|
||||
}
|
||||
let result = session.core.take_result_with(operation, |outcome| {
|
||||
Some(match outcome {
|
||||
Ok(result) => take(result).ok_or_else(|| {
|
||||
NativeDataPlaneError::invalid("data-plane result variant does not match operation")
|
||||
}),
|
||||
Err(kind) => Err(NativeDataPlaneError::new(
|
||||
*kind,
|
||||
format!("data-plane operation failed with {kind:?}"),
|
||||
)),
|
||||
})
|
||||
})?;
|
||||
result
|
||||
.ok_or_else(|| NativeDataPlaneError::invalid("data-plane result could not be consumed"))?
|
||||
}
|
||||
|
||||
pub(super) fn take_tcp_connect(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
) -> NativeDataPlaneResult<TcpConnectResult> {
|
||||
take_result(
|
||||
session,
|
||||
operation,
|
||||
DataPlaneOperationKind::TcpConnect,
|
||||
|result| match result {
|
||||
DataPlaneOperationResult::TcpConnected {
|
||||
stream,
|
||||
local_addr,
|
||||
peer_addr,
|
||||
} => Some(TcpConnectResult {
|
||||
stream: stream.get(),
|
||||
local_addr: *local_addr,
|
||||
peer_addr: *peer_addr,
|
||||
}),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn take_tcp_bind(session: u64, operation: u64) -> NativeDataPlaneResult<TcpBindResult> {
|
||||
take_result(
|
||||
session,
|
||||
operation,
|
||||
DataPlaneOperationKind::TcpBind,
|
||||
|result| match result {
|
||||
DataPlaneOperationResult::TcpBound {
|
||||
listener,
|
||||
local_addr,
|
||||
} => Some(TcpBindResult {
|
||||
listener: listener.get(),
|
||||
local_addr: *local_addr,
|
||||
}),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn take_tcp_accept(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
) -> NativeDataPlaneResult<TcpAcceptResult> {
|
||||
take_result(
|
||||
session,
|
||||
operation,
|
||||
DataPlaneOperationKind::TcpAccept,
|
||||
|result| match result {
|
||||
DataPlaneOperationResult::TcpAccepted {
|
||||
stream,
|
||||
local_addr,
|
||||
peer_addr,
|
||||
} => Some(TcpAcceptResult {
|
||||
stream: stream.get(),
|
||||
local_addr: *local_addr,
|
||||
peer_addr: *peer_addr,
|
||||
}),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn take_tcp_read(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
output: &mut [u8],
|
||||
) -> NativeDataPlaneResult<TcpReadResult> {
|
||||
let required = result_size(session, operation)?;
|
||||
if output.len() < required {
|
||||
return Err(NativeDataPlaneError::new(
|
||||
DataPlaneErrorKind::BufferTooSmall,
|
||||
format!(
|
||||
"TCP read result requires {required} bytes, buffer has {}",
|
||||
output.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
take_result(
|
||||
session,
|
||||
operation,
|
||||
DataPlaneOperationKind::TcpRead,
|
||||
|result| match result {
|
||||
DataPlaneOperationResult::TcpRead { data, eof } => {
|
||||
output[..data.len()].copy_from_slice(data);
|
||||
Some(TcpReadResult {
|
||||
len: data.len(),
|
||||
eof: *eof,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn take_tcp_write(session: u64, operation: u64) -> NativeDataPlaneResult<usize> {
|
||||
take_result(
|
||||
session,
|
||||
operation,
|
||||
DataPlaneOperationKind::TcpWrite,
|
||||
|result| match result {
|
||||
DataPlaneOperationResult::TcpWritten { len } => Some(*len),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn take_udp_bind(session: u64, operation: u64) -> NativeDataPlaneResult<UdpBindResult> {
|
||||
take_result(
|
||||
session,
|
||||
operation,
|
||||
DataPlaneOperationKind::UdpBind,
|
||||
|result| match result {
|
||||
DataPlaneOperationResult::UdpBound { socket, local_addr } => Some(UdpBindResult {
|
||||
socket: socket.get(),
|
||||
local_addr: *local_addr,
|
||||
}),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn take_udp_receive(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
output: &mut [u8],
|
||||
) -> NativeDataPlaneResult<UdpReceiveResult> {
|
||||
let required = result_size(session, operation)?;
|
||||
if output.len() < required {
|
||||
return Err(NativeDataPlaneError::new(
|
||||
DataPlaneErrorKind::BufferTooSmall,
|
||||
format!(
|
||||
"UDP receive result requires {required} bytes, buffer has {}",
|
||||
output.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
take_result(
|
||||
session,
|
||||
operation,
|
||||
DataPlaneOperationKind::UdpReceive,
|
||||
|result| match result {
|
||||
DataPlaneOperationResult::UdpReceived {
|
||||
data,
|
||||
peer_addr,
|
||||
truncated,
|
||||
} => {
|
||||
output[..data.len()].copy_from_slice(data);
|
||||
Some(UdpReceiveResult {
|
||||
len: data.len(),
|
||||
peer_addr: *peer_addr,
|
||||
truncated: *truncated,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn take_udp_send(session: u64, operation: u64) -> NativeDataPlaneResult<usize> {
|
||||
take_result(
|
||||
session,
|
||||
operation,
|
||||
DataPlaneOperationKind::UdpSend,
|
||||
|result| match result {
|
||||
DataPlaneOperationResult::UdpSent { len } => Some(*len),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_data_plane_sessions_by_instance_ids(ids: &[Uuid]) {
|
||||
if ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
let _usage = DATA_PLANE_USAGE_LOCK
|
||||
.write()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
let removed = {
|
||||
let mut sessions = SESSIONS.lock().unwrap_or_else(|error| error.into_inner());
|
||||
let handles = sessions
|
||||
.iter()
|
||||
.filter_map(|(handle, session)| ids.contains(&session.instance_id).then_some(*handle))
|
||||
.collect::<Vec<_>>();
|
||||
handles
|
||||
.into_iter()
|
||||
.filter_map(|handle| sessions.remove(&handle))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
for session in removed {
|
||||
session.close();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn lock_for_config_server_start()
|
||||
-> Result<std::sync::RwLockWriteGuard<'static, ()>, String> {
|
||||
let guard = DATA_PLANE_USAGE_LOCK
|
||||
.write()
|
||||
.map_err(|error| format!("failed to lock data plane usage: {error}"))?;
|
||||
if !SESSIONS
|
||||
.lock()
|
||||
.map_err(|error| format!("failed to lock data-plane sessions: {error}"))?
|
||||
.is_empty()
|
||||
{
|
||||
return Err("cannot start config server client while data plane is in use".to_string());
|
||||
}
|
||||
Ok(guard)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{sync::mpsc, time::Duration};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn config_server_start_waits_for_session_open_or_close() {
|
||||
let read_guard = DATA_PLANE_USAGE_LOCK.read().unwrap();
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
let waiter = std::thread::spawn(move || {
|
||||
let _write_guard = lock_for_config_server_start().unwrap();
|
||||
done_tx.send(()).unwrap();
|
||||
});
|
||||
|
||||
assert!(done_rx.recv_timeout(Duration::from_millis(100)).is_err());
|
||||
drop(read_guard);
|
||||
done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
waiter.join().unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ffi::{CString, c_char},
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
// # Thread Safety
|
||||
// set_error_msg and get_error_msg must be called on the same thread to
|
||||
// get correct error. And since `Handle::block_on` polls the top-level
|
||||
// future on the calling thread, set_error_msg always runs on the same
|
||||
// thread as the corresponding get_error_msg.
|
||||
static ERROR_MSG: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
pub(crate) fn set_error_msg(msg: &str) {
|
||||
ERROR_MSG.with(|cell| {
|
||||
let mut buf = cell.borrow_mut();
|
||||
buf.clear();
|
||||
buf.extend_from_slice(msg.as_bytes());
|
||||
});
|
||||
}
|
||||
|
||||
fn thread_local_error_msg() -> Option<String> {
|
||||
ERROR_MSG.with(|cell| {
|
||||
let buf = cell.borrow();
|
||||
if buf.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(String::from_utf8_lossy(&buf).into_owned())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn get_error_msg(out: *mut *const c_char) {
|
||||
let msg = match (
|
||||
thread_local_error_msg(),
|
||||
crate::config_server::last_callback_error(),
|
||||
) {
|
||||
(Some(error), Some(callback_error)) => Some(format!(
|
||||
"{}; config server callback error: {}",
|
||||
error, callback_error
|
||||
)),
|
||||
(Some(error), None) => Some(error),
|
||||
(None, Some(callback_error)) => {
|
||||
Some(format!("config server callback error: {}", callback_error))
|
||||
}
|
||||
(None, None) => None,
|
||||
};
|
||||
let cstr = msg.and_then(|msg| CString::new(msg).ok());
|
||||
unsafe {
|
||||
*out = match cstr {
|
||||
Some(s) => s.into_raw() as *const c_char,
|
||||
None => std::ptr::null(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn free_string(s: *const c_char) {
|
||||
if s.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
let _ = CString::from_raw(s as *mut c_char);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
use std::ffi::{CString, c_char, c_int};
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "android",
|
||||
target_os = "ios",
|
||||
all(target_os = "macos", feature = "macos-ne"),
|
||||
target_env = "ohos"
|
||||
))]
|
||||
use easytier::common::config::ConfigLoader as _;
|
||||
use easytier::common::config::{ConfigFileControl, TomlConfigLoader};
|
||||
|
||||
use crate::{
|
||||
config_server::{in_config_server_callback, wait_for_config_server_delivery},
|
||||
error::set_error_msg,
|
||||
state::{ffi_context, resolve_instance_id_by_name},
|
||||
types::KeyValuePair,
|
||||
};
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "android",
|
||||
target_os = "ios",
|
||||
all(target_os = "macos", feature = "macos-ne"),
|
||||
target_env = "ohos"
|
||||
))]
|
||||
fn mobile_tun_sources_for_legacy_set_tun_fd(inst_id: uuid::Uuid) -> Result<(), String> {
|
||||
let config = ffi_context()
|
||||
.manager
|
||||
.config(inst_id)
|
||||
.ok_or_else(|| format!("instance config unavailable: {inst_id}"))?;
|
||||
let flags = config.get_flags();
|
||||
if flags.dev_name.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"set_tun_fd legacy API cannot attach shared mobile TUN dev_name={} without tun sources",
|
||||
flags.dev_name
|
||||
))
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Set the tun fd
|
||||
pub(crate) unsafe fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_int {
|
||||
let inst_name = unsafe {
|
||||
assert!(!inst_name.is_null());
|
||||
std::ffi::CStr::from_ptr(inst_name)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
};
|
||||
let inst_id = match resolve_instance_id_by_name(&inst_name) {
|
||||
Ok(Some(instance_id)) => instance_id,
|
||||
Ok(None) => {
|
||||
set_error_msg(&format!("instance not found: {inst_name}"));
|
||||
return -1;
|
||||
}
|
||||
Err(error) => {
|
||||
set_error_msg(&error.to_string());
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "android",
|
||||
target_os = "ios",
|
||||
all(target_os = "macos", feature = "macos-ne"),
|
||||
target_env = "ohos"
|
||||
))]
|
||||
if let Err(error) = mobile_tun_sources_for_legacy_set_tun_fd(inst_id) {
|
||||
set_error_msg(&error);
|
||||
return -1;
|
||||
}
|
||||
|
||||
match ffi_context().manager.attach_tun_fd(inst_id, fd) {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to set tun fd: {}", e));
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Parse the config
|
||||
pub(crate) unsafe fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
||||
let cfg_str = unsafe {
|
||||
assert!(!cfg_str.is_null());
|
||||
std::ffi::CStr::from_ptr(cfg_str)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
};
|
||||
|
||||
if let Err(e) = TomlConfigLoader::new_from_str(&cfg_str) {
|
||||
set_error_msg(&format!("failed to parse config: {:?}", e));
|
||||
return -1;
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Run the network instance
|
||||
pub(crate) unsafe fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot run network instance from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let cfg_str = unsafe {
|
||||
assert!(!cfg_str.is_null());
|
||||
std::ffi::CStr::from_ptr(cfg_str)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
};
|
||||
let cfg = match TomlConfigLoader::new_from_str(&cfg_str) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to parse config: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
wait_for_config_server_delivery();
|
||||
if let Err(e) = ffi_context().runtime.block_on(
|
||||
ffi_context()
|
||||
.process_management
|
||||
.run_owned_network_instance(cfg, ConfigFileControl::STATIC_CONFIG),
|
||||
) {
|
||||
set_error_msg(&format!("failed to start instance: {}", e));
|
||||
return -1;
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
unsafe fn parse_instance_names(
|
||||
inst_names: *const *const c_char,
|
||||
length: usize,
|
||||
) -> Option<Vec<String>> {
|
||||
if length == 0 {
|
||||
return Some(Vec::new());
|
||||
}
|
||||
if inst_names.is_null() {
|
||||
set_error_msg("inst_names is null");
|
||||
return None;
|
||||
}
|
||||
|
||||
let names = unsafe { std::slice::from_raw_parts(inst_names, length) };
|
||||
let mut parsed = Vec::with_capacity(length);
|
||||
for (index, &name) in names.iter().enumerate() {
|
||||
if name.is_null() {
|
||||
set_error_msg(&format!("inst_names[{}] is null", index));
|
||||
return None;
|
||||
}
|
||||
parsed.push(
|
||||
unsafe { std::ffi::CStr::from_ptr(name) }
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
);
|
||||
}
|
||||
Some(parsed)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Retain the network instance
|
||||
pub(crate) unsafe fn retain_network_instance(
|
||||
inst_names: *const *const std::ffi::c_char,
|
||||
length: usize,
|
||||
) -> std::ffi::c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot retain network instances from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
wait_for_config_server_delivery();
|
||||
let retained_names = if length == 0 {
|
||||
Vec::new()
|
||||
} else {
|
||||
let Some(inst_names) = (unsafe { parse_instance_names(inst_names, length) }) else {
|
||||
return -1;
|
||||
};
|
||||
inst_names
|
||||
};
|
||||
|
||||
if let Err(error) = ffi_context().runtime.block_on(
|
||||
ffi_context()
|
||||
.process_management
|
||||
.retain_owned_network_instances_by_name(retained_names),
|
||||
) {
|
||||
set_error_msg(&format!("failed to retain instances: {error}"));
|
||||
return -1;
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Delete named network instances.
|
||||
pub(crate) unsafe fn delete_network_instance(
|
||||
inst_names: *const *const std::ffi::c_char,
|
||||
length: usize,
|
||||
) -> std::ffi::c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot delete network instances from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
wait_for_config_server_delivery();
|
||||
if length == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let Some(inst_names) = (unsafe { parse_instance_names(inst_names, length) }) else {
|
||||
return -1;
|
||||
};
|
||||
|
||||
if let Err(error) = ffi_context().runtime.block_on(
|
||||
ffi_context()
|
||||
.process_management
|
||||
.delete_owned_network_instances_by_name(inst_names),
|
||||
) {
|
||||
set_error_msg(&format!("failed to delete instances: {error}"));
|
||||
return -1;
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Collect the network infos
|
||||
pub(crate) unsafe fn collect_network_infos(
|
||||
infos: *mut KeyValuePair,
|
||||
max_length: usize,
|
||||
) -> std::ffi::c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot collect network infos from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if max_length == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let infos = unsafe {
|
||||
assert!(!infos.is_null());
|
||||
std::slice::from_raw_parts_mut(infos, max_length)
|
||||
};
|
||||
|
||||
let collected_infos = match ffi_context().manager.collect_network_infos_sync() {
|
||||
Ok(infos) => infos,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to collect network infos: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let mut index = 0;
|
||||
for (instance_id, value) in collected_infos.iter() {
|
||||
if index >= max_length {
|
||||
break;
|
||||
}
|
||||
let Some(key) = ffi_context()
|
||||
.manager
|
||||
.instance(*instance_id)
|
||||
.map(|instance| instance.instance_name().to_owned())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// convert value to json string
|
||||
let value = match serde_json::to_string(&value) {
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to serialize instance info: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
infos[index] = KeyValuePair {
|
||||
key: std::ffi::CString::new(key).unwrap().into_raw(),
|
||||
value: std::ffi::CString::new(value).unwrap().into_raw(),
|
||||
};
|
||||
index += 1;
|
||||
}
|
||||
|
||||
index as std::ffi::c_int
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// List the instance names and IDs known by the FFI instance manager.
|
||||
pub(crate) unsafe fn list_instance(infos: *mut KeyValuePair, max_length: usize) -> std::ffi::c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot list instances from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if max_length == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if infos.is_null() {
|
||||
set_error_msg("infos is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let infos = unsafe { std::slice::from_raw_parts_mut(infos, max_length) };
|
||||
let mut instances = ffi_context()
|
||||
.manager
|
||||
.instance_ids()
|
||||
.into_iter()
|
||||
.filter_map(|id| {
|
||||
ffi_context()
|
||||
.manager
|
||||
.instance(id)
|
||||
.map(|instance| (instance.instance_name().to_owned(), id))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
instances.sort_by(|(left_name, left_id), (right_name, right_id)| {
|
||||
left_name
|
||||
.cmp(right_name)
|
||||
.then_with(|| left_id.to_string().cmp(&right_id.to_string()))
|
||||
});
|
||||
|
||||
let encoded_instances = match instances
|
||||
.into_iter()
|
||||
.take(max_length)
|
||||
.map(|(name, id)| {
|
||||
let key = CString::new(name)
|
||||
.map_err(|err| format!("failed to encode instance name: {}", err))?;
|
||||
let value = CString::new(id.to_string())
|
||||
.map_err(|err| format!("failed to encode instance id: {}", err))?;
|
||||
Ok((key, value))
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let count = encoded_instances.len();
|
||||
for (index, (key, value)) in encoded_instances.into_iter().enumerate() {
|
||||
infos[index] = KeyValuePair {
|
||||
key: key.into_raw(),
|
||||
value: value.into_raw(),
|
||||
};
|
||||
}
|
||||
|
||||
count as std::ffi::c_int
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
use std::{
|
||||
ffi::{CString, c_char, c_int},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config_server::in_config_server_callback,
|
||||
error::set_error_msg,
|
||||
state::ffi_context,
|
||||
strings::{c_str_to_string, optional_c_str_to_string},
|
||||
};
|
||||
|
||||
/// # Safety
|
||||
/// See `crate::call_json_rpc`.
|
||||
pub(crate) unsafe fn call_json_rpc(
|
||||
service_name: *const c_char,
|
||||
method_name: *const c_char,
|
||||
domain_name: *const c_char,
|
||||
payload_json: *const c_char,
|
||||
out_response_json: *mut *const c_char,
|
||||
) -> c_int {
|
||||
if out_response_json.is_null() {
|
||||
set_error_msg("out_response_json is null");
|
||||
return -1;
|
||||
}
|
||||
unsafe {
|
||||
*out_response_json = std::ptr::null();
|
||||
}
|
||||
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot call JSON RPC from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let service_name = match unsafe { c_str_to_string(service_name, "service_name") } {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let method_name = match unsafe { c_str_to_string(method_name, "method_name") } {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let domain_name = match unsafe { optional_c_str_to_string(domain_name, "domain_name") } {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let payload_json = match unsafe { c_str_to_string(payload_json, "payload_json") } {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let payload = match serde_json::from_str::<serde_json::Value>(&payload_json) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to parse payload_json: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let response =
|
||||
match ffi_context()
|
||||
.runtime
|
||||
.block_on(easytier_core::management::call_management_json_rpc(
|
||||
&ffi_context().manager,
|
||||
Arc::new(easytier::rpc_service::logger::NativeLoggerControl),
|
||||
&service_name,
|
||||
&method_name,
|
||||
domain_name.as_deref(),
|
||||
payload,
|
||||
)) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("RPC Error: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let response_json = match serde_json::to_string(&response) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to serialize RPC response: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let response_json = match CString::new(response_json) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to allocate RPC response: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
unsafe {
|
||||
*out_response_json = response_json.into_raw();
|
||||
}
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
//! C ABI facade for EasyTier.
|
||||
//!
|
||||
//! The exported API is intentionally kept in this file so C users and JNI
|
||||
//! bindings can see the full callable surface without reading the internal
|
||||
//! implementation modules.
|
||||
//!
|
||||
//! Network management APIs:
|
||||
//! - `parse_config`: validate a TOML network config string.
|
||||
//! - `run_network_instance`: start one local network instance from TOML.
|
||||
//! - `retain_network_instance`: keep named instances and stop all others.
|
||||
//! - `delete_network_instance`: stop named local network instances.
|
||||
//! - `list_instance`: list running instance names and IDs.
|
||||
//! - `collect_network_infos`: collect running instance info as key/value pairs.
|
||||
//! - `set_tun_fd`: attach a TUN file descriptor to a named instance.
|
||||
//! - `call_json_rpc`: call an exposed EasyTier RPC service with JSON payload.
|
||||
//!
|
||||
//! Config server client APIs:
|
||||
//! - `start_config_server_client`: start the managed remote config client.
|
||||
//! - `stop_config_server_client`: stop the remote config client and its managed instances.
|
||||
//! - `is_config_server_client_connected`: report whether the client is connected.
|
||||
//!
|
||||
//! Data plane APIs, enabled by the `ffi-dataplane` feature:
|
||||
//! - `data_plane_session_open` / `data_plane_session_close`: own one instance session.
|
||||
//! - `data_plane_*_submit`: submit non-blocking TCP and UDP operations.
|
||||
//! - `data_plane_completion_wait` / `data_plane_completion_drain`: await completions.
|
||||
//! - `data_plane_*_result_take`: consume typed operation results.
|
||||
//! - `data_plane_operation_cancel` / `data_plane_operation_free`: control operations.
|
||||
//! - `data_plane_resource_close`: close streams, listeners, and UDP sockets.
|
||||
//!
|
||||
//! Shared FFI helper APIs:
|
||||
//! - `get_error_msg`: copy the last FFI or config-server callback error message.
|
||||
//! - `free_string`: release strings allocated by this library.
|
||||
|
||||
mod config_server;
|
||||
mod data_plane;
|
||||
mod error;
|
||||
mod instance_api;
|
||||
mod json_rpc;
|
||||
mod state;
|
||||
mod strings;
|
||||
mod types;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use config_server::{in_config_server_callback, validate_config_server_client_options};
|
||||
pub use types::{
|
||||
ConfigServerEventCallback, DataPlaneCompletion, DataPlaneSocketAddr, KeyValuePair,
|
||||
};
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
// ===== Network Management API =====
|
||||
|
||||
/// Validate a TOML network config string.
|
||||
///
|
||||
/// This only parses and validates the config. It does not start an instance and
|
||||
/// does not change global FFI state.
|
||||
///
|
||||
/// # Safety
|
||||
/// `cfg_str` must be a non-null pointer to a null-terminated UTF-8 string.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns `0` if the config parses successfully, or `-1` on failure. On
|
||||
/// failure, call `get_error_msg` on the same thread to retrieve details.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn parse_config(cfg_str: *const c_char) -> c_int {
|
||||
unsafe { instance_api::parse_config(cfg_str) }
|
||||
}
|
||||
|
||||
/// Start one local EasyTier network instance from a TOML config string.
|
||||
///
|
||||
/// The config's `inst_name` must be unique among instances started through this
|
||||
/// FFI layer. This API is mutually exclusive with config-server callback
|
||||
/// execution and will fail if called from a config-server event callback.
|
||||
///
|
||||
/// # Safety
|
||||
/// `cfg_str` must be a non-null pointer to a null-terminated UTF-8 string.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns `0` after the instance is started and registered in the FFI name
|
||||
/// cache, or `-1` on failure. On failure, call `get_error_msg` on the same
|
||||
/// thread to retrieve details.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn run_network_instance(cfg_str: *const c_char) -> c_int {
|
||||
unsafe { instance_api::run_network_instance(cfg_str) }
|
||||
}
|
||||
|
||||
/// Keep the named network instances and stop all other instances.
|
||||
///
|
||||
/// Passing `length == 0` stops all instances. When `length > 0`, `inst_names`
|
||||
/// must point to an array of `length` non-null C strings. Instances that are not
|
||||
/// retained are removed from the FFI name cache and any related data-plane
|
||||
/// handles are closed.
|
||||
///
|
||||
/// This API fails if called from a config-server event callback.
|
||||
///
|
||||
/// # Safety
|
||||
/// If `length > 0`, `inst_names` must be a non-null pointer to an array of
|
||||
/// `length` non-null pointers to null-terminated UTF-8 strings.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns `0` on success, or `-1` on failure. On failure, call
|
||||
/// `get_error_msg` on the same thread to retrieve details.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn retain_network_instance(
|
||||
inst_names: *const *const c_char,
|
||||
length: usize,
|
||||
) -> c_int {
|
||||
unsafe { instance_api::retain_network_instance(inst_names, length) }
|
||||
}
|
||||
|
||||
/// Stop the named network instances.
|
||||
///
|
||||
/// Passing `length == 0` is a no-op. When `length > 0`, `inst_names` must point
|
||||
/// to an array of `length` non-null C strings. Unknown names are ignored.
|
||||
/// Removed instances are also removed from the FFI name cache and any related
|
||||
/// data-plane handles are closed.
|
||||
///
|
||||
/// This API fails if called from a config-server event callback.
|
||||
///
|
||||
/// # Safety
|
||||
/// If `length > 0`, `inst_names` must be a non-null pointer to an array of
|
||||
/// `length` non-null pointers to null-terminated UTF-8 strings.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns `0` on success, or `-1` on failure. On failure, call
|
||||
/// `get_error_msg` on the same thread to retrieve details.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn delete_network_instance(
|
||||
inst_names: *const *const c_char,
|
||||
length: usize,
|
||||
) -> c_int {
|
||||
unsafe { instance_api::delete_network_instance(inst_names, length) }
|
||||
}
|
||||
|
||||
/// List running network instance names and IDs.
|
||||
///
|
||||
/// Writes up to `max_length` entries into `infos`. Each returned key is the
|
||||
/// instance name and each returned value is the instance ID string. Returned
|
||||
/// key/value strings are allocated by this library and must be released with
|
||||
/// `free_string`.
|
||||
///
|
||||
/// This API fails if called from a config-server event callback.
|
||||
///
|
||||
/// # Safety
|
||||
/// If `max_length > 0`, `infos` must be a non-null pointer to writable storage
|
||||
/// for at least `max_length` `KeyValuePair` values.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns the number of entries written, or `-1` on failure. On failure, call
|
||||
/// `get_error_msg` on the same thread to retrieve details.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn list_instance(infos: *mut KeyValuePair, max_length: usize) -> c_int {
|
||||
unsafe { instance_api::list_instance(infos, max_length) }
|
||||
}
|
||||
|
||||
/// Collect running network instance information.
|
||||
///
|
||||
/// Writes up to `max_length` entries into `infos`. Each returned key is the
|
||||
/// instance name and each returned value is a JSON string containing that
|
||||
/// instance's running information. Returned key/value strings are allocated by
|
||||
/// this library and must be released with `free_string`.
|
||||
///
|
||||
/// This API fails if called from a config-server event callback.
|
||||
///
|
||||
/// # Safety
|
||||
/// If `max_length > 0`, `infos` must be a non-null pointer to writable storage
|
||||
/// for at least `max_length` `KeyValuePair` values.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns the number of entries written, or `-1` on failure. On failure, call
|
||||
/// `get_error_msg` on the same thread to retrieve details.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn collect_network_infos(
|
||||
infos: *mut KeyValuePair,
|
||||
max_length: usize,
|
||||
) -> c_int {
|
||||
unsafe { instance_api::collect_network_infos(infos, max_length) }
|
||||
}
|
||||
|
||||
/// Attach a TUN file descriptor to a named network instance.
|
||||
///
|
||||
/// The instance must already have been registered in the FFI name cache by
|
||||
/// `run_network_instance` or by a managed config-server remote start event.
|
||||
///
|
||||
/// # Safety
|
||||
/// `inst_name` must be a non-null pointer to a null-terminated UTF-8 string.
|
||||
/// `fd` must be a valid TUN file descriptor owned by the caller.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns `0` if the descriptor is accepted by the instance, or `-1` on
|
||||
/// failure.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_int {
|
||||
unsafe { instance_api::set_tun_fd(inst_name, fd) }
|
||||
}
|
||||
|
||||
/// Call an exposed EasyTier RPC method using protobuf JSON.
|
||||
///
|
||||
/// This generic bridge intentionally excludes instance lifecycle management
|
||||
/// RPCs. Use the dedicated FFI APIs for starting, retaining, deleting, and
|
||||
/// collecting instances. `payload_json` must contain the protobuf JSON request,
|
||||
/// including any `instance` selector required by the target RPC.
|
||||
///
|
||||
/// `domain_name` may be null or empty. It is only used by
|
||||
/// `api.instance.TcpProxyRpcService`; null or empty defaults to `tcp`, and the
|
||||
/// only accepted explicit values are `tcp`, `kcp_src`, `kcp_dst`, `quic_src`,
|
||||
/// and `quic_dst`.
|
||||
///
|
||||
/// On success, writes a newly allocated JSON response string to
|
||||
/// `out_response_json`. The caller must release it with `free_string`.
|
||||
///
|
||||
/// This API fails if called from a config-server event callback.
|
||||
///
|
||||
/// # Safety
|
||||
/// `service_name`, `method_name`, `payload_json`, and `out_response_json` must
|
||||
/// be non-null. String pointers must point to null-terminated UTF-8 strings.
|
||||
/// `domain_name` may be null.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns `0` on success, or `-1` on failure. On failure, call
|
||||
/// `get_error_msg` on the same thread to retrieve details.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn call_json_rpc(
|
||||
service_name: *const c_char,
|
||||
method_name: *const c_char,
|
||||
domain_name: *const c_char,
|
||||
payload_json: *const c_char,
|
||||
out_response_json: *mut *const c_char,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
json_rpc::call_json_rpc(
|
||||
service_name,
|
||||
method_name,
|
||||
domain_name,
|
||||
payload_json,
|
||||
out_response_json,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Config Server Client API =====
|
||||
|
||||
/// Start the managed config-server client.
|
||||
///
|
||||
/// The client reuses EasyTier's web-client path and applies remote config
|
||||
/// changes through the shared `NativeInstanceManager`. Successful remote run
|
||||
/// and delete operations are delivered to `callback` as JSON event strings, one
|
||||
/// callback per affected instance. The event string is valid only for the
|
||||
/// duration of the callback; callers must copy it if they need to keep it.
|
||||
///
|
||||
/// The config-server client is mutually exclusive with the FFI data plane. If a
|
||||
/// data-plane handle exists or is being created, this function returns `-1`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `config_server_url` and `machine_id` must be non-null pointers to
|
||||
/// null-terminated UTF-8 strings. `hostname` may be null; when non-null it must
|
||||
/// also point to a null-terminated UTF-8 string. `user_data` is passed back to
|
||||
/// `callback` unchanged and must remain valid for the callback's expectations.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns `0` after the client starts successfully, or `-1` on failure. On
|
||||
/// failure, call `get_error_msg` on the same thread to retrieve details.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn start_config_server_client(
|
||||
config_server_url: *const c_char,
|
||||
hostname: *const c_char,
|
||||
machine_id: *const c_char,
|
||||
secure_mode: bool,
|
||||
callback: ConfigServerEventCallback,
|
||||
user_data: *mut c_void,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
config_server::start_config_server_client(
|
||||
config_server_url,
|
||||
hostname,
|
||||
machine_id,
|
||||
secure_mode,
|
||||
callback,
|
||||
user_data,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop the managed config-server client.
|
||||
///
|
||||
/// This stops the client, removes instances tracked as remote config-server
|
||||
/// instances, waits for in-flight callback delivery when safe to do so, and
|
||||
/// releases the config-server/data-plane mutual exclusion state.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns `0` if no client exists or if the active client is stopped
|
||||
/// successfully. Returns `-1` on failure. On failure, call `get_error_msg` on
|
||||
/// the same thread to retrieve details.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn stop_config_server_client() -> c_int {
|
||||
config_server::stop_config_server_client()
|
||||
}
|
||||
|
||||
/// Report whether the managed config-server client is currently connected.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns `1` when a client exists and reports connected, otherwise returns
|
||||
/// `0`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn is_config_server_client_connected() -> c_int {
|
||||
config_server::is_config_server_client_connected()
|
||||
}
|
||||
|
||||
// ===== Data Plane API =====
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub use data_plane::{
|
||||
DATA_PLANE_DEADLINE_READ, DATA_PLANE_DEADLINE_WRITE, data_plane_completion_drain,
|
||||
data_plane_completion_wait, data_plane_operation_cancel, data_plane_operation_free,
|
||||
data_plane_resource_close, data_plane_resource_deadline_set, data_plane_result_size,
|
||||
data_plane_session_close, data_plane_session_open, data_plane_tcp_accept_result_take,
|
||||
data_plane_tcp_accept_submit, data_plane_tcp_bind_result_take, data_plane_tcp_bind_submit,
|
||||
data_plane_tcp_connect_result_take, data_plane_tcp_connect_submit,
|
||||
data_plane_tcp_read_result_take, data_plane_tcp_read_submit, data_plane_tcp_write_result_take,
|
||||
data_plane_tcp_write_submit, data_plane_udp_bind_result_take, data_plane_udp_bind_submit,
|
||||
data_plane_udp_receive_result_take, data_plane_udp_receive_submit,
|
||||
data_plane_udp_send_result_take, data_plane_udp_send_submit,
|
||||
};
|
||||
|
||||
// ===== Shared FFI Helper API =====
|
||||
|
||||
/// Return the last FFI error message.
|
||||
///
|
||||
/// API failures are stored in a thread-local buffer, so call this on the same
|
||||
/// thread that received a negative status or another documented failure
|
||||
/// sentinel. Config-server
|
||||
/// callback delivery failures may happen on a runtime thread; those are stored
|
||||
/// globally and are included here so direct FFI callers can still retrieve the
|
||||
/// last callback error. If there is no error message, this writes a null pointer
|
||||
/// to `out`.
|
||||
///
|
||||
/// The returned string is allocated by this library and must be released with
|
||||
/// `free_string`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `out` must be a non-null pointer to writable storage for one C string
|
||||
/// pointer.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn get_error_msg(out: *mut *const c_char) {
|
||||
unsafe { error::get_error_msg(out) }
|
||||
}
|
||||
|
||||
/// Release a C string allocated by this library.
|
||||
///
|
||||
/// Use this for strings returned through `get_error_msg`,
|
||||
/// `collect_network_infos`, and data-plane address output parameters. Passing a
|
||||
/// null pointer is allowed and has no effect.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn free_string(s: *const c_char) {
|
||||
error::free_string(s)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use easytier::instance::factory::{
|
||||
NativeInstanceManager, NativeProcessManagement, native_instance_manager_with_runtime,
|
||||
native_process_management,
|
||||
};
|
||||
use tokio::runtime::{Builder, Runtime};
|
||||
|
||||
struct FfiOwnedInstanceHooks;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl easytier_core::management::InstanceMutationHooks for FfiOwnedInstanceHooks {
|
||||
async fn post_remove_network_instances(
|
||||
&self,
|
||||
instance_ids: &[uuid::Uuid],
|
||||
) -> Result<(), String> {
|
||||
crate::config_server::remove_config_server_tracked_instance_ids(instance_ids);
|
||||
crate::data_plane::remove_data_plane_sessions_by_instance_ids(instance_ids);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct FfiContext {
|
||||
pub(crate) runtime: Runtime,
|
||||
pub(crate) manager: Arc<NativeInstanceManager>,
|
||||
pub(crate) process_management: NativeProcessManagement,
|
||||
}
|
||||
|
||||
impl FfiContext {
|
||||
fn new() -> Self {
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("tokio runtime for easytier-ffi");
|
||||
let manager = Arc::new(native_instance_manager_with_runtime(
|
||||
runtime.handle().clone(),
|
||||
));
|
||||
let process_management =
|
||||
native_process_management(manager.clone(), Arc::new(FfiOwnedInstanceHooks));
|
||||
Self {
|
||||
runtime,
|
||||
manager,
|
||||
process_management,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static FFI_CONTEXT: once_cell::sync::Lazy<FfiContext> = once_cell::sync::Lazy::new(FfiContext::new);
|
||||
|
||||
pub(crate) fn ffi_context() -> &'static FfiContext {
|
||||
&FFI_CONTEXT
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_instance_id_by_name(inst_name: &str) -> Result<Option<uuid::Uuid>, String> {
|
||||
easytier_core::management::resolve_optional_instance_by_name(
|
||||
ffi_context().manager.as_ref(),
|
||||
inst_name,
|
||||
)
|
||||
.map(|instance| instance.map(|instance| instance.instance_id()))
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn find_instance_id_by_name(inst_name: &str) -> Option<uuid::Uuid> {
|
||||
resolve_instance_id_by_name(inst_name).ok().flatten()
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use std::ffi::{CStr, c_char};
|
||||
|
||||
pub(crate) unsafe fn c_str_to_string(ptr: *const c_char, name: &str) -> Result<String, String> {
|
||||
if ptr.is_null() {
|
||||
return Err(format!("{} is null", name));
|
||||
}
|
||||
|
||||
unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map(|value| value.to_string())
|
||||
.map_err(|err| format!("{} is not valid UTF-8: {}", name, err))
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn optional_c_str_to_string(
|
||||
ptr: *const c_char,
|
||||
name: &str,
|
||||
) -> Result<Option<String>, String> {
|
||||
if ptr.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
unsafe { c_str_to_string(ptr, name) }.map(Some)
|
||||
}
|
||||
@@ -0,0 +1,746 @@
|
||||
use crate::{
|
||||
config_server::{
|
||||
ConfigServerCallbackScope, ManagedConfigServerClientHooks, set_active_for_test,
|
||||
},
|
||||
state::{ffi_context, find_instance_id_by_name},
|
||||
*,
|
||||
};
|
||||
use easytier::{
|
||||
common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader},
|
||||
web_client::WebClientHooks,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
ffi::{CStr, CString, c_char, c_int, c_void},
|
||||
sync::{Mutex, mpsc},
|
||||
time::Duration,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn test_parse_config() {
|
||||
let cfg_str = r#"
|
||||
inst_name = "test"
|
||||
network = "test_network"
|
||||
"#;
|
||||
let cstr = std::ffi::CString::new(cfg_str).unwrap();
|
||||
unsafe {
|
||||
assert_eq!(parse_config(cstr.as_ptr()), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_network_instance() {
|
||||
let cfg_str = r#"
|
||||
inst_name = "test"
|
||||
network = "test_network"
|
||||
"#;
|
||||
let cstr = std::ffi::CString::new(cfg_str).unwrap();
|
||||
unsafe {
|
||||
assert_eq!(run_network_instance(cstr.as_ptr()), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_error_msg_returns_config_server_callback_error() {
|
||||
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
|
||||
let callback_error = format!("callback delivery failed {}", Uuid::new_v4());
|
||||
crate::config_server::clear_last_callback_error();
|
||||
hooks.note_callback_error(callback_error.clone());
|
||||
|
||||
unsafe {
|
||||
let mut error_ptr: *const c_char = std::ptr::null();
|
||||
get_error_msg(&mut error_ptr);
|
||||
assert!(!error_ptr.is_null());
|
||||
let error_msg = CStr::from_ptr(error_ptr).to_string_lossy().into_owned();
|
||||
free_string(error_ptr);
|
||||
assert!(error_msg.contains(&callback_error));
|
||||
}
|
||||
|
||||
crate::config_server::clear_last_callback_error();
|
||||
}
|
||||
|
||||
unsafe extern "C" fn record_config_server_event(event_json: *const c_char, user_data: *mut c_void) {
|
||||
let events = unsafe { &*(user_data as *const Mutex<Vec<String>>) };
|
||||
events.lock().unwrap().push(
|
||||
unsafe { CStr::from_ptr(event_json) }
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
);
|
||||
}
|
||||
|
||||
fn take_last_error() -> Option<String> {
|
||||
unsafe {
|
||||
let mut error_ptr: *const c_char = std::ptr::null();
|
||||
get_error_msg(&mut error_ptr);
|
||||
if error_ptr.is_null() {
|
||||
None
|
||||
} else {
|
||||
let error = CStr::from_ptr(error_ptr).to_string_lossy().into_owned();
|
||||
free_string(error_ptr);
|
||||
Some(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn free_key_value_pairs(infos: &[KeyValuePair]) {
|
||||
for info in infos {
|
||||
free_string(info.key);
|
||||
free_string(info.value);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_instance_returns_instance_names_and_ids() {
|
||||
let instance_id = Uuid::new_v4();
|
||||
let instance_name = format!("list-instance-{}", instance_id);
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(instance_id);
|
||||
cfg.set_inst_name(instance_name.clone());
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
|
||||
let mut infos = vec![
|
||||
KeyValuePair {
|
||||
key: std::ptr::null(),
|
||||
value: std::ptr::null(),
|
||||
};
|
||||
16
|
||||
];
|
||||
let count = unsafe { list_instance(infos.as_mut_ptr(), infos.len()) };
|
||||
assert!(count > 0);
|
||||
|
||||
let mut found = false;
|
||||
for info in infos.iter().take(count as usize) {
|
||||
let key = unsafe { CStr::from_ptr(info.key) }.to_string_lossy();
|
||||
let value = unsafe { CStr::from_ptr(info.value) }.to_string_lossy();
|
||||
if key == instance_name {
|
||||
assert_eq!(value, instance_id.to_string());
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
free_key_value_pairs(&infos[..count as usize]);
|
||||
ffi_context()
|
||||
.runtime
|
||||
.block_on(
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id]),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(found);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_instance_allows_zero_length() {
|
||||
assert_eq!(unsafe { list_instance(std::ptr::null_mut(), 0) }, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_instance_rejects_null_output_pointer() {
|
||||
assert_eq!(unsafe { list_instance(std::ptr::null_mut(), 1) }, -1);
|
||||
assert!(take_last_error().unwrap().contains("infos is null"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_json_rpc_returns_logger_response() {
|
||||
let service = CString::new("api.logger.LoggerRpcService").unwrap();
|
||||
let method = CString::new("get_logger_config").unwrap();
|
||||
let payload = CString::new("{}").unwrap();
|
||||
let mut response_ptr: *const c_char = std::ptr::null();
|
||||
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
call_json_rpc(
|
||||
service.as_ptr(),
|
||||
method.as_ptr(),
|
||||
std::ptr::null(),
|
||||
payload.as_ptr(),
|
||||
&mut response_ptr,
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert!(!response_ptr.is_null());
|
||||
let response = unsafe { CStr::from_ptr(response_ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
free_string(response_ptr);
|
||||
let response: Value = serde_json::from_str(&response).unwrap();
|
||||
assert!(response.get("level").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_json_rpc_rejects_instance_management_service() {
|
||||
let service = CString::new("api.manage.WebClientService").unwrap();
|
||||
let method = CString::new("list_network_instance").unwrap();
|
||||
let payload = CString::new("{}").unwrap();
|
||||
let mut response_ptr: *const c_char = std::ptr::null();
|
||||
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
call_json_rpc(
|
||||
service.as_ptr(),
|
||||
method.as_ptr(),
|
||||
std::ptr::null(),
|
||||
payload.as_ptr(),
|
||||
&mut response_ptr,
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert!(response_ptr.is_null());
|
||||
assert!(take_last_error().unwrap().contains("not exposed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_json_rpc_rejects_malformed_payload_json() {
|
||||
let service = CString::new("api.logger.LoggerRpcService").unwrap();
|
||||
let method = CString::new("get_logger_config").unwrap();
|
||||
let payload = CString::new("{").unwrap();
|
||||
let mut response_ptr: *const c_char = std::ptr::null();
|
||||
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
call_json_rpc(
|
||||
service.as_ptr(),
|
||||
method.as_ptr(),
|
||||
std::ptr::null(),
|
||||
payload.as_ptr(),
|
||||
&mut response_ptr,
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert!(response_ptr.is_null());
|
||||
assert!(
|
||||
take_last_error()
|
||||
.unwrap()
|
||||
.contains("failed to parse payload_json")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_json_rpc_rejects_null_output_pointer() {
|
||||
let service = CString::new("api.logger.LoggerRpcService").unwrap();
|
||||
let method = CString::new("get_logger_config").unwrap();
|
||||
let payload = CString::new("{}").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
call_json_rpc(
|
||||
service.as_ptr(),
|
||||
method.as_ptr(),
|
||||
std::ptr::null(),
|
||||
payload.as_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert!(
|
||||
take_last_error()
|
||||
.unwrap()
|
||||
.contains("out_response_json is null")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_emit_run_event() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
&events as *const _ as *mut c_void,
|
||||
);
|
||||
let instance_id = Uuid::new_v4();
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(instance_id);
|
||||
let inst_name = format!("test-{}", instance_id);
|
||||
cfg.set_inst_name(inst_name.clone());
|
||||
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
|
||||
hooks.post_run_network_instance(&instance_id).await.unwrap();
|
||||
|
||||
let duplicate_cfg = TomlConfigLoader::default();
|
||||
duplicate_cfg.set_inst_name(inst_name);
|
||||
duplicate_cfg.set_id(Uuid::new_v4());
|
||||
assert!(
|
||||
hooks
|
||||
.pre_run_network_instance(&duplicate_cfg)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
assert_eq!(hooks.tracked_instance_ids(), vec![instance_id]);
|
||||
let events = events.lock().unwrap().clone();
|
||||
assert_eq!(events.len(), 1);
|
||||
let event: Value = serde_json::from_str(&events[0]).unwrap();
|
||||
assert_eq!(event["event"], "run_network_instance");
|
||||
assert_eq!(event["success"], true);
|
||||
assert_eq!(event["instance_id"], instance_id.to_string());
|
||||
assert!(event["error"].is_null());
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id])
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_emit_delete_events_for_tracked_instances() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
&events as *const _ as *mut c_void,
|
||||
);
|
||||
let instance_id_1 = Uuid::new_v4();
|
||||
let instance_id_2 = Uuid::new_v4();
|
||||
let unknown_instance_id = Uuid::new_v4();
|
||||
for id in [instance_id_1, instance_id_2] {
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(id);
|
||||
cfg.set_inst_name(format!("test-{}", id));
|
||||
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
hooks
|
||||
.post_run_network_instance(&instance_id_1)
|
||||
.await
|
||||
.unwrap();
|
||||
hooks
|
||||
.post_run_network_instance(&instance_id_2)
|
||||
.await
|
||||
.unwrap();
|
||||
events.lock().unwrap().clear();
|
||||
|
||||
hooks
|
||||
.post_remove_network_instances(&[instance_id_1, unknown_instance_id, instance_id_2])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(hooks.tracked_instance_ids().is_empty());
|
||||
let events = events.lock().unwrap().clone();
|
||||
assert_eq!(events.len(), 2);
|
||||
let event_ids = events
|
||||
.iter()
|
||||
.map(|event| {
|
||||
let event: Value = serde_json::from_str(event).unwrap();
|
||||
assert_eq!(event["event"], "delete_network_instance");
|
||||
assert_eq!(event["success"], true);
|
||||
assert!(event["error"].is_null());
|
||||
event["instance_id"].as_str().unwrap().to_string()
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(
|
||||
event_ids,
|
||||
HashSet::from([instance_id_1.to_string(), instance_id_2.to_string()])
|
||||
);
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id_1, instance_id_2])
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_ignore_untracked_instance_without_event() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
&events as *const _ as *mut c_void,
|
||||
);
|
||||
let local_id = Uuid::new_v4();
|
||||
|
||||
hooks
|
||||
.post_remove_network_instances(&[local_id])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(events.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_reject_duplicate_instance_name() {
|
||||
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
|
||||
let inst_name = format!("test-{}", Uuid::new_v4());
|
||||
let existing_id = Uuid::new_v4();
|
||||
let new_id = Uuid::new_v4();
|
||||
let existing_cfg = TomlConfigLoader::default();
|
||||
existing_cfg.set_inst_name(inst_name.clone());
|
||||
existing_cfg.set_id(existing_id);
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(existing_cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_inst_name(inst_name.clone());
|
||||
cfg.set_id(new_id);
|
||||
|
||||
assert!(hooks.pre_run_network_instance(&cfg).await.is_err());
|
||||
assert_eq!(find_instance_id_by_name(&inst_name), Some(existing_id));
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([existing_id])
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_remove_overwritten_id_before_duplicate_name_error() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
&events as *const _ as *mut c_void,
|
||||
);
|
||||
let old_name = format!("old-{}", Uuid::new_v4());
|
||||
let duplicate_name = format!("duplicate-{}", Uuid::new_v4());
|
||||
let overwritten_id = Uuid::new_v4();
|
||||
let duplicate_id = Uuid::new_v4();
|
||||
hooks.instance_ids.lock().unwrap().insert(overwritten_id);
|
||||
for (id, name) in [
|
||||
(overwritten_id, old_name.clone()),
|
||||
(duplicate_id, duplicate_name.clone()),
|
||||
] {
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(id);
|
||||
cfg.set_inst_name(name);
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
}
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([overwritten_id])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
hooks
|
||||
.post_remove_network_instances(&[overwritten_id])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_inst_name(duplicate_name.clone());
|
||||
cfg.set_id(overwritten_id);
|
||||
|
||||
assert!(hooks.pre_run_network_instance(&cfg).await.is_err());
|
||||
assert!(hooks.tracked_instance_ids().is_empty());
|
||||
assert!(find_instance_id_by_name(&old_name).is_none());
|
||||
assert_eq!(
|
||||
find_instance_id_by_name(&duplicate_name),
|
||||
Some(duplicate_id)
|
||||
);
|
||||
assert_eq!(events.lock().unwrap().len(), 1);
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([duplicate_id])
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_remove_tracked_state_before_overwrite_retry() {
|
||||
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
|
||||
let inst_name = format!("test-{}", Uuid::new_v4());
|
||||
let instance_id = Uuid::new_v4();
|
||||
hooks.instance_ids.lock().unwrap().insert(instance_id);
|
||||
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_inst_name(inst_name.clone());
|
||||
cfg.set_id(instance_id);
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg.clone(), ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
hooks
|
||||
.post_remove_network_instances(&[instance_id])
|
||||
.await
|
||||
.unwrap();
|
||||
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||
|
||||
assert!(hooks.tracked_instance_ids().is_empty());
|
||||
assert!(find_instance_id_by_name(&inst_name).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_reject_post_run_after_external_delete() {
|
||||
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
|
||||
let instance_id = Uuid::new_v4();
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(instance_id);
|
||||
cfg.set_inst_name(format!("test-{}", instance_id));
|
||||
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(hooks.post_run_network_instance(&instance_id).await.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_instance_id_by_name_resolves_uncommitted_manager_instance_name() {
|
||||
let instance_id = Uuid::new_v4();
|
||||
let inst_name = format!("test-{}", instance_id);
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(instance_id);
|
||||
cfg.set_inst_name(inst_name.clone());
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(find_instance_id_by_name(&inst_name), Some(instance_id));
|
||||
ffi_context()
|
||||
.runtime
|
||||
.block_on(
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id]),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_network_instance_removes_only_named_instances() {
|
||||
let keep_id = Uuid::new_v4();
|
||||
let delete_id = Uuid::new_v4();
|
||||
let keep_name = format!("keep-{}", keep_id);
|
||||
let delete_name = format!("delete-{}", delete_id);
|
||||
|
||||
for (id, name) in [
|
||||
(keep_id, keep_name.clone()),
|
||||
(delete_id, delete_name.clone()),
|
||||
] {
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(id);
|
||||
cfg.set_inst_name(name.clone());
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let delete_name = CString::new(delete_name.clone()).unwrap();
|
||||
let inst_names = [delete_name.as_ptr()];
|
||||
assert_eq!(
|
||||
unsafe { delete_network_instance(inst_names.as_ptr(), inst_names.len()) },
|
||||
0
|
||||
);
|
||||
|
||||
assert_eq!(find_instance_id_by_name(&keep_name), Some(keep_id));
|
||||
assert!(find_instance_id_by_name(delete_name.to_str().unwrap()).is_none());
|
||||
|
||||
ffi_context()
|
||||
.runtime
|
||||
.block_on(ffi_context().manager.delete_network_instances([keep_id]))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retain_and_delete_network_instance_reject_invalid_name_pointers() {
|
||||
assert_eq!(unsafe { retain_network_instance(std::ptr::null(), 1) }, -1);
|
||||
assert_eq!(unsafe { delete_network_instance(std::ptr::null(), 1) }, -1);
|
||||
|
||||
let inst_names = [std::ptr::null()];
|
||||
assert_eq!(
|
||||
unsafe { retain_network_instance(inst_names.as_ptr(), inst_names.len()) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { delete_network_instance(inst_names.as_ptr(), inst_names.len()) },
|
||||
-1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffi_process_management_uses_manager_mutation_lock() {
|
||||
let manager_guard = ffi_context().manager.mutation_lock().blocking_lock_owned();
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
let waiter = std::thread::spawn(move || {
|
||||
ffi_context()
|
||||
.runtime
|
||||
.block_on(
|
||||
ffi_context()
|
||||
.process_management
|
||||
.delete_owned_network_instances(Vec::new()),
|
||||
)
|
||||
.unwrap();
|
||||
done_tx.send(()).unwrap();
|
||||
});
|
||||
|
||||
assert!(done_rx.recv_timeout(Duration::from_millis(100)).is_err());
|
||||
drop(manager_guard);
|
||||
done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
waiter.join().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_reject_late_runs_for_core_rollback() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
&events as *const _ as *mut c_void,
|
||||
);
|
||||
hooks.start_stopping();
|
||||
|
||||
assert!(
|
||||
hooks
|
||||
.post_run_network_instance(&Uuid::new_v4())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
assert!(hooks.tracked_instance_ids().is_empty());
|
||||
assert!(events.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_network_instance_rejects_an_ambiguous_name() {
|
||||
let duplicate_name = format!("duplicate-{}", Uuid::new_v4());
|
||||
let instance_ids = [Uuid::new_v4(), Uuid::new_v4()];
|
||||
for instance_id in instance_ids {
|
||||
let config = TomlConfigLoader::default();
|
||||
config.set_id(instance_id);
|
||||
config.set_inst_name(duplicate_name.clone());
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(config, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let duplicate_name = CString::new(duplicate_name).unwrap();
|
||||
let names = [duplicate_name.as_ptr()];
|
||||
assert_eq!(
|
||||
unsafe { delete_network_instance(names.as_ptr(), names.len()) },
|
||||
-1
|
||||
);
|
||||
assert!(take_last_error().unwrap().contains("2 instances match"));
|
||||
assert!(
|
||||
instance_ids
|
||||
.iter()
|
||||
.all(|id| ffi_context().manager.instance(*id).is_some())
|
||||
);
|
||||
|
||||
ffi_context()
|
||||
.runtime
|
||||
.block_on(
|
||||
ffi_context()
|
||||
.process_management
|
||||
.delete_owned_network_instances(instance_ids.to_vec()),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_server_callback_context_rejects_nested_blocking_ffi_calls() {
|
||||
let _callback_scope = ConfigServerCallbackScope::enter();
|
||||
assert_eq!(is_config_server_client_connected(), 0);
|
||||
let service = CString::new("api.logger.LoggerRpcService").unwrap();
|
||||
let method = CString::new("get_logger_config").unwrap();
|
||||
let payload = CString::new("{}").unwrap();
|
||||
let mut response_ptr: *const c_char = std::ptr::null();
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
call_json_rpc(
|
||||
service.as_ptr(),
|
||||
method.as_ptr(),
|
||||
std::ptr::null(),
|
||||
payload.as_ptr(),
|
||||
&mut response_ptr,
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert!(response_ptr.is_null());
|
||||
assert_eq!(
|
||||
unsafe { collect_network_infos(std::ptr::null_mut(), 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(unsafe { list_instance(std::ptr::null_mut(), 0) }, -1);
|
||||
let cfg = CString::new("inst_name = \"callback-test\"\nlisteners = []").unwrap();
|
||||
assert_eq!(unsafe { run_network_instance(cfg.as_ptr()) }, -1);
|
||||
assert_eq!(unsafe { retain_network_instance(std::ptr::null(), 0) }, -1);
|
||||
assert_eq!(unsafe { delete_network_instance(std::ptr::null(), 0) }, -1);
|
||||
let url = CString::new("ring://test/token").unwrap();
|
||||
let machine_id = CString::new("test-machine").unwrap();
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
start_config_server_client(
|
||||
url.as_ptr(),
|
||||
std::ptr::null(),
|
||||
machine_id.as_ptr(),
|
||||
false,
|
||||
None,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert_eq!(stop_config_server_client(), -1);
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
{
|
||||
let mut session = 0;
|
||||
assert_eq!(
|
||||
unsafe { data_plane_session_open(std::ptr::null(), &mut session) },
|
||||
-(easytier_core::gateway::DataPlaneErrorKind::Io as c_int)
|
||||
);
|
||||
assert_eq!(session, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[test]
|
||||
fn active_config_server_rejects_data_plane() {
|
||||
set_active_for_test(true);
|
||||
|
||||
let name = CString::new("missing").unwrap();
|
||||
let mut session = 0;
|
||||
assert_eq!(
|
||||
unsafe { data_plane_session_open(name.as_ptr(), &mut session) },
|
||||
-(easytier_core::gateway::DataPlaneErrorKind::Io as c_int)
|
||||
);
|
||||
assert_eq!(session, 0);
|
||||
|
||||
set_active_for_test(false);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[test]
|
||||
fn data_plane_invalid_handle_errors_are_stable() {
|
||||
let closed = -(easytier_core::gateway::DataPlaneErrorKind::HandleClosed as c_int);
|
||||
assert_eq!(data_plane_completion_wait(u64::MAX, 0), closed);
|
||||
assert_eq!(data_plane_operation_cancel(u64::MAX, 1), closed);
|
||||
assert_eq!(data_plane_operation_free(u64::MAX, 1), closed);
|
||||
assert_eq!(data_plane_resource_close(u64::MAX, 1), closed);
|
||||
assert_eq!(
|
||||
data_plane_resource_deadline_set(u64::MAX, 1, DATA_PLANE_DEADLINE_READ, 0),
|
||||
closed
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use std::ffi::{c_char, c_void};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct KeyValuePair {
|
||||
pub key: *const c_char,
|
||||
pub value: *const c_char,
|
||||
}
|
||||
|
||||
pub type ConfigServerEventCallback = Option<unsafe extern "C" fn(*const c_char, *mut c_void)>;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct DataPlaneSocketAddr {
|
||||
/// `4` for IPv4. Other families are reserved for later ABI versions.
|
||||
pub family: u16,
|
||||
/// Native-endian port number.
|
||||
pub port: u16,
|
||||
/// Network-order address bytes. IPv4 uses the first four bytes.
|
||||
pub address: [u8; 16],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct DataPlaneCompletion {
|
||||
pub operation_id: u64,
|
||||
pub operation_kind: u16,
|
||||
/// `0` for success, otherwise a stable `DataPlaneErrorKind` value.
|
||||
pub status: u16,
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/sbin/sh
|
||||
|
||||
#################
|
||||
# Initialization
|
||||
#################
|
||||
|
||||
umask 022
|
||||
|
||||
# echo before loading util_functions
|
||||
ui_print() { echo "$1"; }
|
||||
|
||||
require_new_magisk() {
|
||||
ui_print "********************************"
|
||||
ui_print " Please install Magisk v20.4+! "
|
||||
ui_print "********************************"
|
||||
exit 1
|
||||
}
|
||||
|
||||
#########################
|
||||
# Load util_functions.sh
|
||||
#########################
|
||||
|
||||
OUTFD=$2
|
||||
ZIPFILE=$3
|
||||
|
||||
mount /data 2>/dev/null
|
||||
|
||||
[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk
|
||||
. /data/adb/magisk/util_functions.sh
|
||||
[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk
|
||||
|
||||
install_module
|
||||
exit 0
|
||||
@@ -0,0 +1 @@
|
||||
#MAGISK
|
||||
@@ -0,0 +1,6 @@
|
||||
# easytier_magisk版模块
|
||||
magisk安装后重启
|
||||
|
||||
目录位置:/data/adb/modules/easytier_magisk
|
||||
配置文件位置://data/adb/modules/easytier_magisk/config/config.toml
|
||||
修改config.conf即可,修改后配置文件后去magisk app重新开关模块即可生效
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/data/adb/magisk/busybox sh
|
||||
MODDIR=${0%/*}
|
||||
MODULE_PROP="${MODDIR}/module.prop"
|
||||
IP_RULE_SCRIPT="${MODDIR}/hotspot_iprule.sh"
|
||||
|
||||
ET_STATUS=""
|
||||
REDIR_STATUS=""
|
||||
IS_RUNNING=false
|
||||
|
||||
# 确保辅助脚本有执行权限
|
||||
chmod +x "${IP_RULE_SCRIPT}" 2>/dev/null
|
||||
|
||||
# 更新 module.prop 文件中的 description
|
||||
update_module_description() {
|
||||
local status_message=$1
|
||||
# 检查 module.prop 文件存在且 description 发生变化了再写入
|
||||
if [ -f "${MODULE_PROP}" ]; then
|
||||
local current_desc=$(grep "^description=" "${MODULE_PROP}")
|
||||
local new_desc="description=[状态] ${status_message}"
|
||||
if [ "${current_desc}" != "${new_desc}" ]; then
|
||||
sed -i "s#^description=.*#${new_desc}#" "${MODULE_PROP}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 判断程序启动状态
|
||||
if [ -f "${MODDIR}/disable" ]; then
|
||||
IS_RUNNING=false
|
||||
ET_STATUS="主程序已关闭"
|
||||
|
||||
elif pgrep -f "${MODDIR}/easytier-core" >/dev/null; then
|
||||
IS_RUNNING=true
|
||||
if [ -f "${MODDIR}/config/command_args" ]; then
|
||||
ET_STATUS="主程序正在运行(启动参数模式)"
|
||||
else
|
||||
ET_STATUS="主程序正在运行(配置文件模式)"
|
||||
fi
|
||||
|
||||
elif [ -z "$ET_STATUS" ]; then
|
||||
# 既没 disable 也没运行,说明是异常停止或未启动
|
||||
ET_STATUS="主程序启动失败或未运行"
|
||||
fi
|
||||
|
||||
# 无论主程序是否运行,都允许切换“开关文件”的状态,以便下次生效
|
||||
if [ -f "${MODDIR}/enable_IP_rule" ]; then
|
||||
rm -f "${MODDIR}/enable_IP_rule"
|
||||
|
||||
"${IP_RULE_SCRIPT}" del >/dev/null 2>&1
|
||||
|
||||
REDIR_STATUS="转发已禁用"
|
||||
echo "热点子网转发已禁用"
|
||||
echo "[ET-NAT] Action: IP rule disabled." >> "${MODDIR}/log.log"
|
||||
else
|
||||
touch "${MODDIR}/enable_IP_rule"
|
||||
|
||||
if [ "$IS_RUNNING" = true ]; then
|
||||
"${IP_RULE_SCRIPT}" del >/dev/null 2>&1
|
||||
"${IP_RULE_SCRIPT}" add_once
|
||||
echo "转发规则将立即生效,无需重启"
|
||||
else
|
||||
echo "主程序未运行,转发规则将在下次启动时生效"
|
||||
fi
|
||||
|
||||
REDIR_STATUS="转发已激活"
|
||||
echo "----------------------------------"
|
||||
echo "热点子网转发已激活"
|
||||
echo "热点开启后将自动将热点加入转发网络"
|
||||
echo "需要在配置中提前配置好 cidr 参数"
|
||||
echo "----------------------------------"
|
||||
echo "[ET-NAT] Action: IP rule enabled." >> "${MODDIR}/log.log"
|
||||
fi
|
||||
|
||||
sync
|
||||
update_module_description "${ET_STATUS}| ${REDIR_STATUS}"
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
|
||||
version=$(cat module.prop | grep 'version=' | awk -F '=' '{print $2}' | sed 's/ (.*//')
|
||||
|
||||
version='v'$(grep '^version =' ../../easytier/Cargo.toml | cut -d '"' -f 2)
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
echo "Error: 版本号不存在."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
filename="easytier_magisk_${version}.zip"
|
||||
echo $version
|
||||
|
||||
|
||||
if [ -f "./easytier-core" ] && [ -f "./easytier-cli" ] && [ -f "./easytier-web" ]; then
|
||||
zip -r -o -X "$filename" ./ -x '.git/*' -x '.github/*' -x 'folder/*' -x 'build.sh' -x 'magisk_update.json'
|
||||
else
|
||||
wget -O "easytier_last.zip" https://github.com/EasyTier/EasyTier/releases/download/"$version"/easytier-linux-aarch64-"$version".zip
|
||||
unzip -o easytier_last.zip -d ./
|
||||
mv ./easytier-linux-aarch64/* ./
|
||||
rm -rf ./easytier_last.zip
|
||||
rm -rf ./easytier-linux-aarch64
|
||||
zip -r -o -X "$filename" ./ -x '.git/*' -x '.github/*' -x 'folder/*' -x 'build.sh' -x 'magisk_update.json'
|
||||
fi
|
||||
@@ -0,0 +1 @@
|
||||
--config-server udp://127.0.0.1:22020/admin --machine-id easytier-magisk
|
||||
@@ -0,0 +1,38 @@
|
||||
instance_name = "default"
|
||||
dhcp = false
|
||||
#ipv4="本机ip"
|
||||
listeners = [
|
||||
"tcp://0.0.0.0:11010",
|
||||
"udp://0.0.0.0:11010",
|
||||
"wg://0.0.0.0:11011",
|
||||
"ws://0.0.0.0:11011/",
|
||||
"wss://0.0.0.0:11012/",
|
||||
]
|
||||
mapped_listeners = []
|
||||
exit_nodes = []
|
||||
rpc_portal = "0.0.0.0:15888"
|
||||
|
||||
[network_identity]
|
||||
network_name = "default"
|
||||
network_secret = ""
|
||||
|
||||
[[peer]]
|
||||
#uri = "协议://中转ip:端口"
|
||||
|
||||
[flags]
|
||||
default_protocol = "tcp"
|
||||
dev_name = ""
|
||||
enable_encryption = true
|
||||
enable_ipv6 = true
|
||||
mtu = 1380
|
||||
latency_first = false
|
||||
enable_exit_node = false
|
||||
no_tun = false
|
||||
use_smoltcp = false
|
||||
foreign_network_whitelist = "*"
|
||||
disable_p2p = false
|
||||
relay_all_peer_rpc = false
|
||||
disable_udp_hole_punching = false
|
||||
disable_tcp_hole_punching = false
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
SKIPMOUNT=false
|
||||
PROPFILE=true
|
||||
POSTFSDATA=true
|
||||
LATESTARTSERVICE=true
|
||||
|
||||
set_perm_recursive $MODPATH 0 0 0777 0777
|
||||
|
||||
ui_print "系统架构为:$ARCH"
|
||||
ui_print "系统 SDK 版本:$API"
|
||||
ui_print "EasyTier 安装位置:/data/adb/modules/easytier_magisk"
|
||||
ui_print "配置文件位置:/data/adb/modules/easytier_magisk/config/config.toml"
|
||||
ui_print "如需使用启动参数模式,请将 /data/adb/modules/easytier_magisk/config/command_args_sample 重命名为 command_args,并修改其中的内容"
|
||||
ui_print "config 目录中存在 command_args 文件时,模块会自动忽略 config.toml 文件"
|
||||
ui_print "----------------------------------"
|
||||
ui_print "注意!启动参数文件中不能存在 \" 和 ',配置文件则没有这个限制"
|
||||
ui_print "----------------------------------"
|
||||
ui_print "修改配置后无需重启设备,在 Magisk 中禁用 EasyTier 模块,等待 10 秒后重新启用即可让新配置生效"
|
||||
ui_print "点击 Magisk 中模块左下角的“操作”按钮可以禁用或激活热点子网转发,使用该功能前需要在配置中提前配置好 cidr 参数"
|
||||
ui_print "模块安装完成,重启设备生效"
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/system/bin/sh
|
||||
|
||||
MODDIR=${0%/*}
|
||||
CONFIG_FILE="${MODDIR}/config/config.toml"
|
||||
COMMAND_ARGS="${MODDIR}/config/command_args"
|
||||
LOG_FILE="${MODDIR}/log.log"
|
||||
MODULE_PROP="${MODDIR}/module.prop"
|
||||
EASYTIER="${MODDIR}/easytier-core"
|
||||
|
||||
# 处理获取到的设备型号中可能出现的空格
|
||||
BRAND=$(getprop ro.product.brand | tr ' ' '-')
|
||||
MODEL=$(getprop ro.product.model | tr ' ' '-')
|
||||
DEVICE_HOSTNAME="${BRAND}-${MODEL}"
|
||||
REDIR_STATUS=""
|
||||
|
||||
# 更新 module.prop 文件中的 description
|
||||
update_module_description() {
|
||||
local status_message=$1
|
||||
# 检查 module.prop 文件存在且 description 发生变化了再写入
|
||||
if [ -f "${MODULE_PROP}" ]; then
|
||||
local current_desc=$(grep "^description=" "${MODULE_PROP}")
|
||||
local new_desc="description=[状态] ${status_message}"
|
||||
if [ "${current_desc}" != "${new_desc}" ]; then
|
||||
sed -i "s#^description=.*#${new_desc}#" "${MODULE_PROP}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查并初始化 TUN 设备
|
||||
if [ ! -e /dev/net/tun ]; then
|
||||
if [ ! -d /dev/net ]; then
|
||||
mkdir -p /dev/net
|
||||
fi
|
||||
|
||||
ln -s /dev/tun /dev/net/tun
|
||||
fi
|
||||
|
||||
while true; do
|
||||
# 获取子网转发激活状态
|
||||
if [ -f "${MODDIR}/enable_IP_rule" ]; then
|
||||
REDIR_STATUS="转发已激活"
|
||||
else
|
||||
REDIR_STATUS="转发已禁用"
|
||||
fi
|
||||
|
||||
# 检查模块是否被禁用
|
||||
if [ -f "${MODDIR}/disable" ]; then
|
||||
update_module_description "主程序已关闭 | ${REDIR_STATUS}"
|
||||
if pgrep -f "${EASYTIER}" >/dev/null; then
|
||||
echo "开关控制 $(date "+%Y-%m-%d %H:%M:%S") 进程已存在,正在关闭"
|
||||
pkill -f "${EASYTIER}"
|
||||
fi
|
||||
sleep 10s
|
||||
continue
|
||||
fi
|
||||
|
||||
# 检查进程是否已经在运行
|
||||
if pgrep -f "${EASYTIER}" >/dev/null; then
|
||||
sleep 10s
|
||||
continue
|
||||
fi
|
||||
|
||||
# 检查配置文件是否存在
|
||||
if [ ! -f "${CONFIG_FILE}" ] && [ ! -f "${COMMAND_ARGS}" ]; then
|
||||
update_module_description "缺少配置文件或启动参数文件"
|
||||
sleep 10s
|
||||
continue
|
||||
fi
|
||||
|
||||
# 如果 config 目录下存在 command_args 文件,则读取其中的内容作为启动参数
|
||||
if [ -f "${COMMAND_ARGS}" ]; then
|
||||
# 启动参数模式
|
||||
CMD_CONTENT=$(tr '\r\n' ' ' < "${COMMAND_ARGS}")
|
||||
|
||||
if echo "${CMD_CONTENT}" | grep -q "\-\-hostname"; then
|
||||
FINAL_ARGS="${CMD_CONTENT}"
|
||||
else
|
||||
FINAL_ARGS="${CMD_CONTENT} --hostname ${DEVICE_HOSTNAME}"
|
||||
fi
|
||||
|
||||
TZ=Asia/Shanghai "${EASYTIER}" ${FINAL_ARGS} > "${LOG_FILE}" 2>&1 &
|
||||
STR_MODE="启动参数模式"
|
||||
|
||||
# 否则读取 config.toml 的内容作为启动参数
|
||||
else
|
||||
# 配置文件模式
|
||||
if grep -q "^[[:space:]]*hostname[[:space:]]*=" "${CONFIG_FILE}"; then
|
||||
TZ=Asia/Shanghai "${EASYTIER}" -c "${CONFIG_FILE}" > "${LOG_FILE}" 2>&1 &
|
||||
else
|
||||
TZ=Asia/Shanghai "${EASYTIER}" -c "${CONFIG_FILE}" --hostname "${DEVICE_HOSTNAME}" > "${LOG_FILE}" 2>&1 &
|
||||
fi
|
||||
|
||||
STR_MODE="配置文件模式"
|
||||
fi
|
||||
|
||||
# 等待进程启动
|
||||
sleep 5s
|
||||
|
||||
# 启动后的扫尾工作
|
||||
if pgrep -f "${EASYTIER}" >/dev/null; then
|
||||
|
||||
if ! ip rule show | grep -qE '^[0-9]+:[[:space:]]+from all lookup main$'; then
|
||||
ip rule add from all lookup main
|
||||
fi
|
||||
|
||||
update_module_description "主程序正在运行(${STR_MODE})| ${REDIR_STATUS}"
|
||||
else
|
||||
update_module_description "主程序启动失败,请检查配置文件或启动参数"
|
||||
fi
|
||||
|
||||
sleep 10s
|
||||
done
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/system/bin/sh
|
||||
MODDIR=${0%/*}
|
||||
CONFIG_FILE="${MODDIR}/config/config.toml"
|
||||
LOG_FILE="${MODDIR}/log.log"
|
||||
ACTION="$1" # 参数:add add_once del
|
||||
|
||||
|
||||
# 获取接口/IP
|
||||
get_et_iface() {
|
||||
awk '
|
||||
BEGIN { IGNORECASE = 1 }
|
||||
/^[[:space:]]*dev_name[[:space:]]*=/ {
|
||||
val = $0
|
||||
sub(/^[^=]*=[[:space:]]*/, "", val)
|
||||
gsub(/[" \t]/, "", val)
|
||||
print val
|
||||
exit
|
||||
}
|
||||
' "$CONFIG_FILE"
|
||||
}
|
||||
get_tun_iface() {
|
||||
ip link | awk -F': ' '/ tun[[:alnum:]]+/ {print $2; exit}'
|
||||
}
|
||||
get_hot_iface() {
|
||||
ip link | awk -F': ' '/(^| )(swlan[[:alnum:]_]*|softap[[:alnum:]_]*|p2p-wlan[[:alnum:]_]*|ap[[:alnum:]_]*)\:/ {print $2; exit}' | cut -d'@' -f1 | head -n1
|
||||
}
|
||||
get_usb_iface() {
|
||||
ip link | awk -F': ' '/(^| )(usb[[:alnum:]_]*|rndis[[:alnum:]_]*|eth[[:alnum:]_]*)\:/ {print $2; exit}' | cut -d'@' -f1 | head -n1
|
||||
}
|
||||
get_hot_cidr() {
|
||||
ip -4 addr show dev "$1" | awk '/inet /{print $2; exit}'
|
||||
}
|
||||
|
||||
|
||||
set_nat_rules() {
|
||||
ET_IFACE=$(get_et_iface)
|
||||
[ -z "$ET_IFACE" ] && ET_IFACE="$(get_tun_iface)"
|
||||
HOT_IFACE=$(get_hot_iface)
|
||||
USB_IFACE=$(get_usb_iface)
|
||||
HOT_CIDR=$(get_hot_cidr "$HOT_IFACE")
|
||||
USB_CIDR=$(get_hot_cidr "$USB_IFACE")
|
||||
|
||||
# 如果热点关闭就删除自定义链
|
||||
[ -n "$ET_IFACE" ] && { [ -n "$HOT_CIDR" ] || [ -n "$USB_CIDR" ]; } || return 1
|
||||
|
||||
# 创建自定义链(如不存在)
|
||||
iptables -t nat -N ET_NAT 2>/dev/null
|
||||
iptables -N ET_FWD 2>/dev/null
|
||||
|
||||
# 确保主链首条跳转到自定义链
|
||||
iptables -t nat -C POSTROUTING -j ET_NAT 2>/dev/null || \
|
||||
iptables -t nat -I POSTROUTING 1 -j ET_NAT
|
||||
iptables -C FORWARD -j ET_FWD 2>/dev/null || \
|
||||
iptables -I FORWARD 1 -j ET_FWD
|
||||
|
||||
# 添加规则
|
||||
if [ -n "$HOT_CIDR" ]; then
|
||||
iptables -t nat -A ET_NAT -s "$HOT_CIDR" -o "$ET_IFACE" -j MASQUERADE
|
||||
iptables -A ET_FWD -i "$HOT_IFACE" -o "$ET_IFACE" \
|
||||
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
|
||||
iptables -A ET_FWD -i "$ET_IFACE" -o "$HOT_IFACE" \
|
||||
-m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
echo "[ET-NAT] Rules applied: $HOT_IFACE $HOT_CIDR ↔ $ET_IFACE" >> "$LOG_FILE"
|
||||
fi
|
||||
if [ -n "$USB_CIDR" ]; then
|
||||
iptables -t nat -A ET_NAT -s "$USB_CIDR" -o "$ET_IFACE" -j MASQUERADE
|
||||
iptables -A ET_FWD -i "$USB_IFACE" -o "$ET_IFACE" \
|
||||
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
|
||||
iptables -A ET_FWD -i "$ET_IFACE" -o "$USB_IFACE" \
|
||||
-m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
echo "[ET-NAT] Rules applied: $USB_IFACE $USB_CIDR ↔ $ET_IFACE" >> "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
flush_rules() {
|
||||
iptables -t nat -F ET_NAT 2>/dev/null
|
||||
iptables -F ET_FWD 2>/dev/null
|
||||
echo "[ET-NAT] Custom chains flushed." >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
case "$ACTION" in
|
||||
add)
|
||||
set_nat_rules
|
||||
echo "[ET-NAT] Guard started." >> "$LOG_FILE"
|
||||
ip monitor link addr | while read -r _; do
|
||||
if [ -f "${MODDIR}/enable_IP_rule" ]; then
|
||||
flush_rules
|
||||
set_nat_rules
|
||||
fi
|
||||
done
|
||||
;;
|
||||
add_once)
|
||||
flush_rules
|
||||
set_nat_rules
|
||||
echo "[ET-NAT] One-time rules applied." >> "$LOG_FILE"
|
||||
;;
|
||||
del)
|
||||
flush_rules
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [add|del]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": "v1.0",
|
||||
"versionCode": 1,
|
||||
"zipUrl": "",
|
||||
"changelog": ""
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
id=easytier_magisk
|
||||
name=EasyTier_Magisk
|
||||
version=v2.6.4
|
||||
versionCode=1
|
||||
author=EasyTier
|
||||
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
|
||||
updateJson=https://raw.githubusercontent.com/EasyTier/EasyTier/refs/heads/main/easytier-contrib/easytier-magisk/magisk_update.json
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/data/adb/magisk/busybox sh
|
||||
MODDIR=${0%/*}
|
||||
# MODDIR="$(dirname $(readlink -f "$0"))"
|
||||
chmod 755 ${MODDIR}/*
|
||||
|
||||
# 等待系统启动成功
|
||||
while [ "$(getprop sys.boot_completed)" != "1" ]; do
|
||||
sleep 5s
|
||||
done
|
||||
|
||||
# 防止系统挂起
|
||||
echo "PowerManagerService.noSuspend" > /sys/power/wake_lock
|
||||
|
||||
# 修改模块描述
|
||||
sed -i 's/$(description=)$[^"]*/\1[状态]关闭中/' "$MODDIR/module.prop"
|
||||
|
||||
# 等待 3 秒
|
||||
sleep 3s
|
||||
|
||||
"${MODDIR}/easytier_core.sh" &
|
||||
"${MODDIR}/hotspot_iprule.sh" add &
|
||||
|
||||
# easytier_core.sh 和 hotspot_iprule.sh 都有内部循环做守护,
|
||||
# 所以这里不需要再做守护了
|
||||
@@ -0,0 +1,2 @@
|
||||
nameserver 114.114.114.114
|
||||
nameserver 223.5.5.5
|
||||
@@ -0,0 +1,5 @@
|
||||
MODDIR=${0%/*}
|
||||
pkill -f "${MODDIR}/easytier-core"
|
||||
|
||||
# 使用 ${MODDIR:?} 确保变量非空,避免执行 rm -rf /*
|
||||
rm -rf "${MODDIR:?}/"*
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "easytier-mini"
|
||||
description = "Minimal native EasyTier node with TCP/UDP tunnels, TUN and UDP hole punching."
|
||||
version = "2.6.4"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license-file = "../../LICENSE"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
easytier = { path = "../../easytier", version = "2.6.4", default-features = false, features = [
|
||||
"aes-gcm",
|
||||
"dhcp-ipv4",
|
||||
"logging",
|
||||
"proxy-cidr-monitor",
|
||||
"smoltcp",
|
||||
"tun",
|
||||
"web-client",
|
||||
] }
|
||||
tokio = { version = "1", default-features = false, features = ["macros", "rt", "signal"] }
|
||||
@@ -0,0 +1,113 @@
|
||||
# easytier-mini
|
||||
|
||||
`easytier-mini` is a native EasyTier POC binary. It shares EasyTier's TOML
|
||||
configuration model, peer protocol, TCP/UDP tunnel implementations, TUN,
|
||||
dynamic IPv4 allocation, the smoltcp userspace path and STUN/UDP hole-punching
|
||||
core with the full binary. It includes AES-GCM so its default encryption
|
||||
setting interoperates with the full binary's default configuration.
|
||||
|
||||
Build it with:
|
||||
|
||||
```sh
|
||||
cargo build --release -p easytier-mini
|
||||
```
|
||||
|
||||
For the static size target used by this POC:
|
||||
|
||||
```sh
|
||||
cargo build --profile mini --target x86_64-unknown-linux-musl -p easytier-mini
|
||||
```
|
||||
|
||||
MIPS targets use the repository's existing musl-cross toolchains. The helper
|
||||
builds the standard library for size, applies immediate-abort only to the mini
|
||||
MIPS target graph, and can build either or both byte orders:
|
||||
|
||||
```sh
|
||||
./easytier-contrib/easytier-mini/build-mips.sh all
|
||||
./easytier-contrib/easytier-mini/build-mips.sh mips
|
||||
./easytier-contrib/easytier-mini/build-mips.sh mipsel
|
||||
```
|
||||
|
||||
The `mini` profile derives from `release` and applies `opt-level=z` to the
|
||||
entire compact binary dependency graph. Full EasyTier release builds retain
|
||||
their normal `opt-level=3` profile. The musl builds use a mini-only static
|
||||
linker policy to stay below 5,000,000 bytes on x86-64 and 5,500,000 bytes on
|
||||
MIPS without UPX or another executable compressor. The compact x86-64 linker
|
||||
policy retains static PIE, packs relative relocations and folds identical code.
|
||||
MIPS builds omit standard-library backtrace support and use immediate abort;
|
||||
normal workspace MIPS builds are not affected. Compact linker policies omit
|
||||
unwind tables.
|
||||
|
||||
Start it with a normal EasyTier TOML file:
|
||||
|
||||
```sh
|
||||
easytier-mini --config mini.toml
|
||||
```
|
||||
|
||||
`-c` is accepted as the short form of `--config`.
|
||||
|
||||
Start it as an EasyTier Web managed node with a complete config-server URL:
|
||||
|
||||
```sh
|
||||
easytier-mini --config-server udp://config-server.easytier.cn:22020/TOKEN
|
||||
```
|
||||
|
||||
`--machine-id`, `--hostname`, and `--secure-mode` match the full client's Web
|
||||
identity and transport options. `--config` and `--config-server` may be used
|
||||
together: the local instance remains static while Web-owned instances are
|
||||
created, updated, retained, and deleted independently.
|
||||
|
||||
The node also exposes the native EasyTier management RPC protocol on
|
||||
`127.0.0.1:15888`, so the full `easytier-cli` can inspect it:
|
||||
|
||||
```sh
|
||||
easytier-cli node info
|
||||
easytier-cli peer
|
||||
easytier-cli route
|
||||
easytier-cli connector list
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```toml
|
||||
instance_name = "mini"
|
||||
ipv4 = "10.147.0.2"
|
||||
listeners = ["tcp://0.0.0.0:11010", "udp://0.0.0.0:11010"]
|
||||
|
||||
[network_identity]
|
||||
network_name = "mini-poc"
|
||||
network_secret = "change-me"
|
||||
|
||||
[[peer]]
|
||||
uri = "tcp://example.net:11010"
|
||||
```
|
||||
|
||||
Local TOML and Web configuration both retain the complete authoritative model.
|
||||
The compact runtime silently omits unsupported capabilities while normalizing
|
||||
that model into live runtime state. EasyTier Web therefore sees every accepted
|
||||
configuration value unchanged and its consistency checks converge. This also
|
||||
applies to hot patches: for example, a port-forward patch remains visible to
|
||||
the controller while no port-forward service starts in mini. ChaCha20 falls
|
||||
back to AES-GCM rather than plaintext.
|
||||
|
||||
The compact runtime supports `tcp://` and `udp://` listener, mapped-listener
|
||||
and peer URLs. `no_tun = true` runs through smoltcp without an OS TUN device,
|
||||
and `dhcp = true` allocates the virtual IPv4 address dynamically.
|
||||
|
||||
The mini feature set keeps STUN collection, UDP hole punching, Web heartbeats,
|
||||
Web instance lifecycle management and the config hot-patch RPC. It omits TCP
|
||||
hole punching, endpoint discovery (`http://`, `https://`, `txt://` and
|
||||
`srv://` peers), protobuf reflection, logger control and the rest of the full
|
||||
management surface. Unsupported connector URLs are accepted as no-ops. Its
|
||||
local RPC surface remains read-only for node, peer, route and connector
|
||||
queries. OSPF route messages keep their original protobuf wire data, so fields
|
||||
added by future EasyTier versions are forwarded without requiring
|
||||
`prost-reflect`.
|
||||
|
||||
For size, this POC reads one file directly and does not support configuration
|
||||
from stdin or `${VAR}` expansion. It omits the process-management event journal,
|
||||
while the console logger still reports runtime events such as peer, connection,
|
||||
listener, TUN and DHCP changes. The RPC address is currently fixed, so only one
|
||||
mini process can use the default portal on a host. The x86-64 musl POC cannot
|
||||
provide reliable stack backtraces because its release binary has no unwind
|
||||
tables.
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# Cargo invokes this same file as a rustc wrapper during compact MIPS builds.
|
||||
# Applying immediate-abort here keeps the size policy scoped to easytier-mini;
|
||||
# normal MIPS builds elsewhere in the workspace retain their panic behavior.
|
||||
if [ "${EASYTIER_MINI_MIPS_RUSTC_WRAPPER:-}" = "1" ]; then
|
||||
mini_rustc=$1
|
||||
shift
|
||||
for mini_rustc_arg in "$@"; do
|
||||
case "$mini_rustc_arg" in
|
||||
mips-unknown-linux-musl|mipsel-unknown-linux-musl)
|
||||
exec "$mini_rustc" "$@" \
|
||||
-Zunstable-options \
|
||||
-Cpanic=immediate-abort
|
||||
;;
|
||||
esac
|
||||
done
|
||||
exec "$mini_rustc" "$@"
|
||||
fi
|
||||
|
||||
mini_script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
mini_repo_dir=$(CDPATH= cd -- "$mini_script_dir/../.." && pwd)
|
||||
mini_requested_target=${1:-all}
|
||||
cd "$mini_repo_dir"
|
||||
|
||||
build_mips_target() {
|
||||
mini_target=$1
|
||||
mini_toolchain=$2
|
||||
PATH="$mini_repo_dir/musl_gcc/$mini_toolchain/bin:$PATH" \
|
||||
EASYTIER_MINI_MIPS_RUSTC_WRAPPER=1 \
|
||||
RUSTC_BOOTSTRAP=1 \
|
||||
RUSTC_WRAPPER="$mini_script_dir/build-mips.sh" \
|
||||
cargo build \
|
||||
--manifest-path "$mini_repo_dir/Cargo.toml" \
|
||||
--profile mini \
|
||||
--target "$mini_target" \
|
||||
-Z build-std=std \
|
||||
-Z build-std-features=optimize_for_size \
|
||||
-p easytier-mini
|
||||
}
|
||||
|
||||
case "$mini_requested_target" in
|
||||
all)
|
||||
build_mips_target mips-unknown-linux-musl mips-unknown-linux-muslsf
|
||||
build_mips_target mipsel-unknown-linux-musl mipsel-unknown-linux-muslsf
|
||||
;;
|
||||
mips|mips-unknown-linux-musl)
|
||||
build_mips_target mips-unknown-linux-musl mips-unknown-linux-muslsf
|
||||
;;
|
||||
mipsel|mipsel-unknown-linux-musl)
|
||||
build_mips_target mipsel-unknown-linux-musl mipsel-unknown-linux-muslsf
|
||||
;;
|
||||
-h|--help)
|
||||
echo "usage: $0 [all|mips|mipsel]"
|
||||
;;
|
||||
*)
|
||||
echo "unsupported MIPS target: $mini_requested_target" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,32 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
let target = env::var("TARGET").unwrap_or_default();
|
||||
let profile = env::var("PROFILE").unwrap_or_default();
|
||||
if !matches!(profile.as_str(), "release" | "mini")
|
||||
|| !matches!(
|
||||
target.as_str(),
|
||||
"x86_64-unknown-linux-musl" | "mips-unknown-linux-musl" | "mipsel-unknown-linux-musl"
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let script =
|
||||
PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("easytier-mini-musl.ld");
|
||||
println!("cargo:rerun-if-changed={}", script.display());
|
||||
// The release-derived mini profile already aborts panics. Keep the compact
|
||||
// binary's linker policy local so full EasyTier musl builds retain their
|
||||
// normal PIE/unwind settings.
|
||||
println!("cargo:rustc-link-arg-bin=easytier-mini=-Wl,--build-id=none");
|
||||
if target == "x86_64-unknown-linux-musl" {
|
||||
println!("cargo:rustc-link-arg-bin=easytier-mini=-Wl,--pack-dyn-relocs=relr");
|
||||
println!("cargo:rustc-link-arg-bin=easytier-mini=-Wl,--icf=all");
|
||||
}
|
||||
println!("cargo:rustc-link-arg-bin=easytier-mini=-Wl,--no-eh-frame-hdr");
|
||||
println!(
|
||||
"cargo:rustc-link-arg-bin=easytier-mini=-Wl,-T,{}",
|
||||
script.display()
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
SECTIONS
|
||||
{
|
||||
.eh_frame :
|
||||
{
|
||||
KEEP(*crtbegin.o(.eh_frame))
|
||||
KEEP(*crtend.o(.eh_frame))
|
||||
}
|
||||
/DISCARD/ :
|
||||
{
|
||||
*(EXCLUDE_FILE (*crtbegin.o *crtend.o) .eh_frame)
|
||||
*(.eh_frame_hdr)
|
||||
}
|
||||
}
|
||||
INSERT AFTER .data;
|
||||
@@ -0,0 +1,264 @@
|
||||
use std::{ffi::OsString, path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use easytier::common::MachineIdOptions;
|
||||
use easytier::{
|
||||
common::config::{ConfigFileControl, load_toml_config_from_path},
|
||||
instance::factory::native_compact_instance_manager_with_runtime,
|
||||
rpc_service::ReadOnlyApiRpcServer,
|
||||
web_client::{WebClientHooks, parse_config_server_endpoint, run_web_client},
|
||||
};
|
||||
|
||||
enum Command {
|
||||
Run(RunOptions),
|
||||
Exit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
struct RunOptions {
|
||||
config: Option<PathBuf>,
|
||||
config_server: Option<String>,
|
||||
machine_id: Option<String>,
|
||||
hostname: Option<String>,
|
||||
secure_mode: bool,
|
||||
}
|
||||
|
||||
const USAGE: &str = "usage: easytier-mini [--config <FILE>] [--config-server <URL>] \
|
||||
[--machine-id <ID>] [--hostname <NAME>] [--secure-mode]";
|
||||
|
||||
fn required_value(
|
||||
args: &mut impl Iterator<Item = OsString>,
|
||||
option: &str,
|
||||
) -> anyhow::Result<OsString> {
|
||||
args.next()
|
||||
.with_context(|| format!("{option} requires a value"))
|
||||
}
|
||||
|
||||
fn parse_args(mut args: impl Iterator<Item = OsString>) -> anyhow::Result<Command> {
|
||||
let mut options = RunOptions::default();
|
||||
while let Some(arg) = args.next() {
|
||||
if arg == "-h" || arg == "--help" {
|
||||
println!(
|
||||
"easytier-mini {}\n\nUsage: {USAGE}",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
return Ok(Command::Exit);
|
||||
}
|
||||
if arg == "-V" || arg == "--version" {
|
||||
println!("easytier-mini {}", env!("CARGO_PKG_VERSION"));
|
||||
return Ok(Command::Exit);
|
||||
}
|
||||
if arg == "-c" || arg == "--config" {
|
||||
if options.config.is_some() {
|
||||
anyhow::bail!("--config may only be specified once");
|
||||
}
|
||||
options.config = Some(PathBuf::from(required_value(&mut args, "--config")?));
|
||||
continue;
|
||||
}
|
||||
if arg == "-w" || arg == "--config-server" {
|
||||
if options.config_server.is_some() {
|
||||
anyhow::bail!("--config-server may only be specified once");
|
||||
}
|
||||
options.config_server = Some(
|
||||
required_value(&mut args, "--config-server")?
|
||||
.into_string()
|
||||
.map_err(|_| anyhow::anyhow!("--config-server must be valid UTF-8"))?,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if arg == "--machine-id" {
|
||||
options.machine_id = Some(
|
||||
required_value(&mut args, "--machine-id")?
|
||||
.into_string()
|
||||
.map_err(|_| anyhow::anyhow!("--machine-id must be valid UTF-8"))?,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if arg == "--hostname" {
|
||||
options.hostname = Some(
|
||||
required_value(&mut args, "--hostname")?
|
||||
.into_string()
|
||||
.map_err(|_| anyhow::anyhow!("--hostname must be valid UTF-8"))?,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if arg == "--secure-mode" {
|
||||
options.secure_mode = true;
|
||||
continue;
|
||||
}
|
||||
anyhow::bail!("unknown argument {arg:?}; {USAGE}");
|
||||
}
|
||||
if options.config.is_none() && options.config_server.is_none() {
|
||||
anyhow::bail!("either --config or --config-server is required; {USAGE}");
|
||||
}
|
||||
Ok(Command::Run(options))
|
||||
}
|
||||
|
||||
fn require_tcp_or_udp(scheme: &str, source: &str) -> anyhow::Result<()> {
|
||||
match scheme {
|
||||
"tcp" | "udp" => Ok(()),
|
||||
scheme => anyhow::bail!(
|
||||
"{source} uses unsupported tunnel scheme {scheme:?}; easytier-mini supports only tcp:// and udp://"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_config_server(config_server: &str) -> anyhow::Result<()> {
|
||||
let endpoint = parse_config_server_endpoint(config_server)?;
|
||||
require_tcp_or_udp(endpoint.connect_url().scheme(), "config server")
|
||||
}
|
||||
|
||||
struct MiniWebClientHooks;
|
||||
|
||||
impl WebClientHooks for MiniWebClientHooks {
|
||||
fn manages_remote_config_instances(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let Command::Run(options) = parse_args(std::env::args_os().skip(1))? else {
|
||||
return Ok(());
|
||||
};
|
||||
easytier::common::log::init_console()?;
|
||||
let local_config = options
|
||||
.config
|
||||
.as_ref()
|
||||
.map(|config_path| {
|
||||
load_toml_config_from_path(config_path)
|
||||
.with_context(|| format!("failed to load {}", config_path.display()))
|
||||
})
|
||||
.transpose()?;
|
||||
if let Some(config_server) = options.config_server.as_deref() {
|
||||
validate_config_server(config_server)?;
|
||||
}
|
||||
|
||||
let instances = Arc::new(native_compact_instance_manager_with_runtime(
|
||||
tokio::runtime::Handle::current(),
|
||||
));
|
||||
let local_instance_id = local_config
|
||||
.map(|config| instances.run_network_instance(config, ConfigFileControl::STATIC_CONFIG))
|
||||
.transpose()?;
|
||||
let _web_client = if let Some(config_server) = options.config_server.as_deref() {
|
||||
Some(
|
||||
run_web_client(
|
||||
config_server,
|
||||
MachineIdOptions {
|
||||
explicit_machine_id: options.machine_id,
|
||||
state_dir: None,
|
||||
},
|
||||
options.hostname,
|
||||
options.secure_mode,
|
||||
instances.clone(),
|
||||
Some(Arc::new(MiniWebClientHooks)),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let _rpc_server =
|
||||
ReadOnlyApiRpcServer::new(Some("127.0.0.1:15888".to_owned()), None, instances.clone())?
|
||||
.serve()
|
||||
.await?;
|
||||
eprintln!(
|
||||
"easytier-mini started: local={local_instance_id:?}, web={}; RPC: 127.0.0.1:15888",
|
||||
options.config_server.is_some()
|
||||
);
|
||||
|
||||
let stopped_unexpectedly = tokio::select! {
|
||||
signal = tokio::signal::ctrl_c() => {
|
||||
signal.context("failed to listen for Ctrl-C")?;
|
||||
false
|
||||
},
|
||||
_ = instances.wait() => true,
|
||||
};
|
||||
|
||||
for instance in instances.instances() {
|
||||
instance.stop().await;
|
||||
}
|
||||
if stopped_unexpectedly {
|
||||
anyhow::bail!("EasyTier instance stopped unexpectedly");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use easytier::common::config::{ConfigLoader as _, TomlConfigLoader};
|
||||
|
||||
#[test]
|
||||
fn parses_minimal_config_argument() {
|
||||
let Command::Run(options) =
|
||||
parse_args([OsString::from("--config"), OsString::from("mini.toml")].into_iter())
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("expected run command");
|
||||
};
|
||||
|
||||
assert_eq!(options.config, Some(PathBuf::from("mini.toml")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_web_client_arguments_without_a_local_config() {
|
||||
let Command::Run(options) = parse_args(
|
||||
[
|
||||
OsString::from("--config-server"),
|
||||
OsString::from("token"),
|
||||
OsString::from("--machine-id"),
|
||||
OsString::from("machine"),
|
||||
OsString::from("--hostname"),
|
||||
OsString::from("mini"),
|
||||
OsString::from("--secure-mode"),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
.unwrap() else {
|
||||
panic!("expected run command");
|
||||
};
|
||||
|
||||
assert_eq!(options.config_server.as_deref(), Some("token"));
|
||||
assert_eq!(options.machine_id.as_deref(), Some("machine"));
|
||||
assert_eq!(options.hostname.as_deref(), Some("mini"));
|
||||
assert!(options.secure_mode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_arguments() {
|
||||
let result = parse_args([OsString::from("extra")].into_iter());
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_tcp_udp_config_server() {
|
||||
assert!(validate_config_server("udp://127.0.0.1:22020/token").is_ok());
|
||||
assert!(validate_config_server("quic://127.0.0.1:22020/token").is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn compact_factory_accepts_unsupported_config_without_changing_it() {
|
||||
let config = TomlConfigLoader::new_from_str(
|
||||
r#"
|
||||
dhcp = true
|
||||
listeners = ["quic://127.0.0.1:11010"]
|
||||
proxy_network = [{ cidr = "10.20.0.0/16" }]
|
||||
|
||||
[flags]
|
||||
encryption_algorithm = "chacha20"
|
||||
data_compress_algo = "Zstd"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
config.get_id();
|
||||
let before = config.dump();
|
||||
let manager =
|
||||
native_compact_instance_manager_with_runtime(tokio::runtime::Handle::current());
|
||||
|
||||
let instance = manager.create(config, ()).unwrap();
|
||||
|
||||
assert_eq!(instance.toml_config().unwrap().dump(), before);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
dist/
|
||||
target/
|
||||
.DS_Store
|
||||
.idea/
|
||||
package/libs
|
||||
|
||||
*.har
|
||||
|
||||
Cargo.lock
|
||||
@@ -0,0 +1,69 @@
|
||||
[package]
|
||||
name = "easytier-ohrs"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type=["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1"
|
||||
base64 = "0.22"
|
||||
bytes = "1.5"
|
||||
easytier-core = { path = "../../easytier-core", default-features = false }
|
||||
easytier-proto = { path = "../../easytier-proto", default-features = false, features = [
|
||||
"api",
|
||||
"core",
|
||||
"json-rpc",
|
||||
] }
|
||||
flate2 = "1.1"
|
||||
futures = "0.3"
|
||||
gethostname = "1.1"
|
||||
easytier = { path = "../../easytier" }
|
||||
napi-derive-ohos = "1.1"
|
||||
napi-ohos = { version = "1.1", default-features = false, features = [
|
||||
"serde-json",
|
||||
"latin1",
|
||||
"chrono_date",
|
||||
"object_indexmap",
|
||||
"tokio",
|
||||
"async",
|
||||
"tokio_rt",
|
||||
"tokio_macros",
|
||||
"tokio_io_util",
|
||||
"deferred_trace",
|
||||
"napi8",
|
||||
"node_version_detect",
|
||||
"web_stream",
|
||||
] }
|
||||
once_cell = "1.21.3"
|
||||
ipnet = "2.10"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.125"
|
||||
prost-reflect = { version = "0.14.5", default-features = false, features = ["derive"] }
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
tracing-subscriber = "0.3.19"
|
||||
tracing-core = "0.1.33"
|
||||
tracing = "0.1.41"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
|
||||
url = "2.5"
|
||||
uuid = { version = "1.5.0", features = [
|
||||
"v4",
|
||||
"fast-rng",
|
||||
"macro-diagnostics",
|
||||
"serde",
|
||||
] }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build-ohos = "1.1"
|
||||
[profile.dev]
|
||||
panic = "unwind"
|
||||
debug = true
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
opt-level = 3
|
||||
strip = true
|
||||