mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-20 11:22:39 +00:00
Compare commits
284
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09e30aeff6 | ||
|
|
e21046a5fa | ||
|
|
b207b8a7bb | ||
|
|
811f151155 | ||
|
|
5e8a79a747 | ||
|
|
8485091614 | ||
|
|
e31fee1a6d | ||
|
|
743ec3c95e | ||
|
|
8428a89d2d | ||
|
|
513695297c | ||
|
|
bfbfa2ef8d | ||
|
|
8e1d079142 | ||
|
|
55f15bb6f0 | ||
|
|
96fd39649a | ||
|
|
74fc8b300d | ||
|
|
baeee40b79 | ||
|
|
4342c8d7a2 | ||
|
|
1178b312fa | ||
|
|
4c2859ae7c | ||
|
|
c9e610a175 | ||
|
|
22c458163a | ||
|
|
ae83de91ab | ||
|
|
348b83d768 | ||
|
|
4b95f44a91 | ||
|
|
96e2a6c775 | ||
|
|
480677f085 | ||
|
|
b7677031cb | ||
|
|
ee1f656d6b | ||
|
|
c866ed4da3 | ||
|
|
2fd8333ebb | ||
|
|
29ff4283d7 | ||
|
|
8c0652a2e2 | ||
|
|
92af568d8b | ||
|
|
0a23546e3f | ||
|
|
b85d5bc515 | ||
|
|
30b88bf626 | ||
|
|
486cc43059 | ||
|
|
46d1c2aef3 | ||
|
|
8cf9d493b8 | ||
|
|
e527717eef | ||
|
|
80041086de | ||
|
|
40d3e48cc4 | ||
|
|
88297868c3 | ||
|
|
d59257c8b7 | ||
|
|
fb4479a99f | ||
|
|
185777b741 | ||
|
|
7d536d3353 | ||
|
|
87f2905360 | ||
|
|
1338ab343c | ||
|
|
38e7366c15 | ||
|
|
0f4003245b | ||
|
|
524c8d9987 | ||
|
|
11626d8082 | ||
|
|
362547eaab | ||
|
|
5df7913043 | ||
|
|
9feb548bc2 | ||
|
|
cbb966d57c | ||
|
|
6db0be00ad | ||
|
|
185eb1e4c2 | ||
|
|
ae4e9b3513 | ||
|
|
7ae7725fbf | ||
|
|
5a516a195c | ||
|
|
c71ab7543f | ||
|
|
4f6f8f3d93 | ||
|
|
d5995ea1cb | ||
|
|
b0aae4f1fa | ||
|
|
4512e03d5f | ||
|
|
49e33f182b | ||
|
|
bc86917dea | ||
|
|
9332baf6f9 | ||
|
|
84984e1641 | ||
|
|
7345acfe7c | ||
|
|
6001eef736 | ||
|
|
b070cff8e2 | ||
|
|
cbdfdfacc1 | ||
|
|
b94ab414a0 | ||
|
|
3795800975 | ||
|
|
ba7fc1098b | ||
|
|
38cb4a22fd | ||
|
|
a60f101bda | ||
|
|
6b01554c17 | ||
|
|
5a8a1d3e6b | ||
|
|
6af62e939e | ||
|
|
012b35acd6 | ||
|
|
34dab9c739 | ||
|
|
09b26fd4fe | ||
|
|
50ecd3679b | ||
|
|
ddbfe76932 | ||
|
|
847e82ff8c | ||
|
|
501c2bdb73 | ||
|
|
8c5851e2ee | ||
|
|
7d47cc837f | ||
|
|
b422e603c8 | ||
|
|
ee217c51f6 | ||
|
|
4f180ab309 | ||
|
|
248117c119 | ||
|
|
4dad60a94d | ||
|
|
67108d14b0 | ||
|
|
9e8a4c94cb | ||
|
|
8610234ee2 | ||
|
|
b51b17e229 | ||
|
|
f17ee9244c | ||
|
|
0cd92ff215 | ||
|
|
4ed111dd04 | ||
|
|
2b3417d703 | ||
|
|
b7edbad4b9 | ||
|
|
a6988e55f6 | ||
|
|
1562092afe | ||
|
|
d5c68e356a | ||
|
|
a9facedf57 | ||
|
|
faa252fc39 | ||
|
|
a031b6f701 | ||
|
|
41d7335bd8 | ||
|
|
2a2f85397c | ||
|
|
18dc9e4ca3 | ||
|
|
39c595b5e9 | ||
|
|
604386f7e2 | ||
|
|
0d91aaf974 | ||
|
|
1b220dd825 | ||
|
|
e4f2078ff1 | ||
|
|
62b512fbae | ||
|
|
5f8548d272 | ||
|
|
9b18f820ad | ||
|
|
56bd8491d1 | ||
|
|
7cb4345e4d | ||
|
|
ea5e34b6af | ||
|
|
c8697e76b4 | ||
|
|
385ff54b3e | ||
|
|
d94cb3a96d | ||
|
|
b6afe6305a | ||
|
|
7c895c401b | ||
|
|
8da5dc5f6e | ||
|
|
561bbd2d62 | ||
|
|
23f63f1e4a | ||
|
|
cddf6abfa7 | ||
|
|
0733a1029c | ||
|
|
cb1e5ca273 | ||
|
|
7286a42d58 | ||
|
|
28f26a1750 | ||
|
|
96b9d89191 | ||
|
|
58c7828c85 | ||
|
|
c9ce17d25e | ||
|
|
231b7d8300 | ||
|
|
ffa7ca3850 | ||
|
|
5b4ec2f56b | ||
|
|
723fc90691 | ||
|
|
dcd0c7cc2f | ||
|
|
2b94df8320 | ||
|
|
447e15f478 | ||
|
|
d5d600a524 | ||
|
|
56f430bf10 | ||
|
|
09ba9a1203 | ||
|
|
4ff9e0f8d7 | ||
|
|
5bc0c1b2ff | ||
|
|
3fb1da6c2e | ||
|
|
725cdf7d06 | ||
|
|
484c264444 | ||
|
|
fe43b5706f | ||
|
|
0ca5e38dba | ||
|
|
b0e4808d00 | ||
|
|
2a34032221 | ||
|
|
15cc86c8bc | ||
|
|
0e5d2cbf08 | ||
|
|
8cb9856f09 | ||
|
|
97d534a8c0 | ||
|
|
093cd7c197 | ||
|
|
6c47b0d86c | ||
|
|
4f3244636d | ||
|
|
2f04de9420 | ||
|
|
e0173a52da | ||
|
|
08a1640e30 | ||
|
|
70fba64e17 | ||
|
|
95e460dbf5 | ||
|
|
a73a029c50 | ||
|
|
d9d211c5a4 | ||
|
|
8559de1857 | ||
|
|
18e28197b8 | ||
|
|
e01f2c22a8 | ||
|
|
918c9b9174 | ||
|
|
4ed9cbc4a1 | ||
|
|
202d8ec121 | ||
|
|
833ed8d8b8 | ||
|
|
a44a98d7c4 | ||
|
|
86871b14d5 | ||
|
|
3fc030de48 | ||
|
|
b5a33a192b | ||
|
|
9d1f134f9e | ||
|
|
57a2428f21 | ||
|
|
238858ebab | ||
|
|
0c8da66a74 | ||
|
|
14155d706c | ||
|
|
c968fb6d06 | ||
|
|
6618635622 | ||
|
|
30cfe7a74a | ||
|
|
13275aae99 | ||
|
|
2e1f9bc9cc | ||
|
|
fcfb0ad6bc | ||
|
|
e967434ef9 | ||
|
|
f75c35d4a0 | ||
|
|
088628c1b6 | ||
|
|
9b372d3087 | ||
|
|
5de87cd494 | ||
|
|
f5d3443928 | ||
|
|
5372596432 | ||
|
|
5457adb533 | ||
|
|
474f1a3636 | ||
|
|
efcdf4c456 | ||
|
|
e9a0e4f042 | ||
|
|
d54eb97201 | ||
|
|
dc1e1929c8 | ||
|
|
b7073504d8 | ||
|
|
f83816ac16 | ||
|
|
d3ca03e20e | ||
|
|
3d3dba28e8 | ||
|
|
e3ed91aced | ||
|
|
8f7fc6e07b | ||
|
|
5e69a26a53 | ||
|
|
488dfaf233 | ||
|
|
4a18a86ed6 | ||
|
|
a8fa7be4ce | ||
|
|
b469a28e19 | ||
|
|
a5fdebe778 | ||
|
|
87713e96b7 | ||
|
|
f9108672e6 | ||
|
|
dbb2f86a25 | ||
|
|
0fdfbc2636 | ||
|
|
2cd208fad4 | ||
|
|
e0897dcf99 | ||
|
|
13aaa437b2 | ||
|
|
6856649705 | ||
|
|
35b27b73a2 | ||
|
|
97edee20f9 | ||
|
|
743d8e4810 | ||
|
|
129f696a65 | ||
|
|
90409bfcaa | ||
|
|
f53ced6281 | ||
|
|
e4bf2ca959 | ||
|
|
fd39961567 | ||
|
|
9502a83143 | ||
|
|
822e742ee2 | ||
|
|
85c06b0758 | ||
|
|
7041214321 | ||
|
|
9caeeafbf9 | ||
|
|
9a2ba9cce8 | ||
|
|
2caf492551 | ||
|
|
4f3afc8ee8 | ||
|
|
863fc6f4cc | ||
|
|
de84adb3a6 | ||
|
|
a9dc19ce76 | ||
|
|
8076734cd4 | ||
|
|
888bec6eac | ||
|
|
7b1b651a46 | ||
|
|
070332b9a7 | ||
|
|
a6bec0fa6f | ||
|
|
77ccd7dbc9 | ||
|
|
a6d030494c | ||
|
|
2ca8b5f8a2 | ||
|
|
0e3474f049 | ||
|
|
28c4c7b4a0 | ||
|
|
2e68a8db91 | ||
|
|
7f8f036596 | ||
|
|
c4cf8cf1d8 | ||
|
|
cff0264caf | ||
|
|
2b5b80e179 | ||
|
|
9d48cefd05 | ||
|
|
6836d61127 | ||
|
|
75ad15c324 | ||
|
|
83bdcf0d2b | ||
|
|
ee3b7f2335 | ||
|
|
7e16b7f69f | ||
|
|
9376d3959b | ||
|
|
78b2afb785 | ||
|
|
8a93bb311b | ||
|
|
b24bb42faa | ||
|
|
2eaa5c215d | ||
|
|
3f3aec8edd | ||
|
|
7abdfd35e8 | ||
|
|
d3454febb7 | ||
|
|
63dd133f0b | ||
|
|
32f86e421a | ||
|
|
1c4303f5b7 | ||
|
|
07376eb690 | ||
|
|
0b6ff1ecd7 | ||
|
|
46b53c601e |
@@ -157,6 +157,9 @@ jobs:
|
||||
|
||||
- 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') }}
|
||||
@@ -227,7 +230,7 @@ jobs:
|
||||
*) UPX_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
UPX_VERSION=5.1.1
|
||||
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" .
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
image_tag:
|
||||
description: 'Tag for this image build'
|
||||
type: string
|
||||
default: 'v2.6.3'
|
||||
default: 'v2.6.4'
|
||||
required: true
|
||||
mark_latest:
|
||||
description: 'Mark this image as latest'
|
||||
|
||||
@@ -18,7 +18,7 @@ on:
|
||||
version:
|
||||
description: 'Version for this release'
|
||||
type: string
|
||||
default: 'v2.6.3'
|
||||
default: 'v2.6.4'
|
||||
required: true
|
||||
make_latest:
|
||||
description: 'Mark this release as latest'
|
||||
|
||||
Generated
+241
-165
@@ -915,7 +915,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@@ -1981,37 +1981,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||
dependencies = [
|
||||
"derive_builder_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||
dependencies = [
|
||||
"darling 0.20.10",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
@@ -2229,7 +2198,7 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "easytier"
|
||||
version = "2.6.3"
|
||||
version = "2.6.4"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -2262,35 +2231,36 @@ dependencies = [
|
||||
"defguard_wireguard_rs",
|
||||
"delegate",
|
||||
"derivative",
|
||||
"derive_builder",
|
||||
"derive_more 2.1.1",
|
||||
"easytier-rpc-build",
|
||||
"encoding",
|
||||
"flume 0.12.0",
|
||||
"forwarded-header-value",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"gethostname 0.5.0",
|
||||
"getset",
|
||||
"git-version",
|
||||
"globwalk",
|
||||
"guarden",
|
||||
"hickory-client",
|
||||
"hickory-net",
|
||||
"hickory-proto",
|
||||
"hickory-resolver",
|
||||
"hickory-server",
|
||||
"hmac",
|
||||
"hostname",
|
||||
"http",
|
||||
"http_req",
|
||||
"humansize",
|
||||
"humantime-serde",
|
||||
"idna 1.0.3",
|
||||
"igd-next",
|
||||
"indexmap 2.14.0",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"kcp-sys",
|
||||
"machine-uid",
|
||||
"maplit",
|
||||
"mimalloc",
|
||||
"moka",
|
||||
"multimap",
|
||||
"natpmp",
|
||||
"netlink-packet-core",
|
||||
@@ -2301,6 +2271,7 @@ dependencies = [
|
||||
"nix 0.29.0",
|
||||
"once_cell",
|
||||
"openssl",
|
||||
"optionize",
|
||||
"ordered_hash_map",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
@@ -2308,7 +2279,8 @@ dependencies = [
|
||||
"petgraph 0.8.1",
|
||||
"pin-project-lite",
|
||||
"pnet",
|
||||
"prefix-trie",
|
||||
"prefix-trie 0.7.0",
|
||||
"proc-macro2",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-reflect",
|
||||
@@ -2318,6 +2290,7 @@ dependencies = [
|
||||
"prost-wkt-types",
|
||||
"quinn",
|
||||
"quinn-plaintext",
|
||||
"quote",
|
||||
"rand 0.8.5",
|
||||
"rcgen",
|
||||
"regex",
|
||||
@@ -2330,6 +2303,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"serial_test",
|
||||
"service-manager",
|
||||
"sha2",
|
||||
@@ -2357,7 +2331,6 @@ dependencies = [
|
||||
"tokio-util",
|
||||
"tokio-websockets",
|
||||
"toml 0.8.19",
|
||||
"tonic-build",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tun-easytier",
|
||||
@@ -2384,7 +2357,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"easytier",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"log",
|
||||
"once_cell",
|
||||
"serde",
|
||||
@@ -2405,7 +2378,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "easytier-gui"
|
||||
version = "2.6.3"
|
||||
version = "2.6.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -2413,7 +2386,7 @@ dependencies = [
|
||||
"dashmap",
|
||||
"dunce",
|
||||
"easytier",
|
||||
"gethostname 1.1.0",
|
||||
"gethostname",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"security-framework-sys",
|
||||
@@ -2436,14 +2409,6 @@ dependencies = [
|
||||
"windows 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easytier-rpc-build"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"prost-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easytier-uptime"
|
||||
version = "0.1.0"
|
||||
@@ -2486,7 +2451,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "easytier-web"
|
||||
version = "2.6.3"
|
||||
version = "2.6.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -2708,24 +2673,6 @@ version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
||||
|
||||
[[package]]
|
||||
name = "endian-type"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
|
||||
|
||||
[[package]]
|
||||
name = "enum-as-inner"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.12"
|
||||
@@ -3307,16 +3254,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30"
|
||||
dependencies = [
|
||||
"rustix 0.38.34",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "1.1.0"
|
||||
@@ -3379,6 +3316,18 @@ dependencies = [
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getset"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912"
|
||||
dependencies = [
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.5.1"
|
||||
@@ -3742,88 +3691,103 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hickory-client"
|
||||
version = "0.25.2"
|
||||
name = "hickory-net"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c466cd63a4217d5b2b8e32f23f58312741ce96e3c84bf7438677d2baff0fc555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"data-encoding",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"hickory-proto",
|
||||
"once_cell",
|
||||
"radix_trie",
|
||||
"rand 0.9.1",
|
||||
"thiserror 2.0.11",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hickory-proto"
|
||||
version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502"
|
||||
checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"cfg-if",
|
||||
"data-encoding",
|
||||
"enum-as-inner",
|
||||
"futures-channel",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"hickory-proto",
|
||||
"http",
|
||||
"idna 1.0.3",
|
||||
"ipnet",
|
||||
"once_cell",
|
||||
"rand 0.9.1",
|
||||
"ring",
|
||||
"jni 0.22.4",
|
||||
"rand 0.10.1",
|
||||
"rustls",
|
||||
"serde",
|
||||
"thiserror 2.0.11",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tracing",
|
||||
"url",
|
||||
"webpki-roots 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hickory-proto"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"idna 1.0.3",
|
||||
"ipnet",
|
||||
"jni 0.22.4",
|
||||
"once_cell",
|
||||
"prefix-trie 0.8.4",
|
||||
"rand 0.10.1",
|
||||
"ring",
|
||||
"serde",
|
||||
"thiserror 2.0.11",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hickory-resolver"
|
||||
version = "0.25.2"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a"
|
||||
checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"hickory-net",
|
||||
"hickory-proto",
|
||||
"ipconfig",
|
||||
"ipnet",
|
||||
"jni 0.22.4",
|
||||
"moka",
|
||||
"ndk-context",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"rand 0.9.1",
|
||||
"rand 0.10.1",
|
||||
"resolv-conf",
|
||||
"rustls",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"system-configuration 0.7.0",
|
||||
"thiserror 2.0.11",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tracing",
|
||||
"webpki-roots 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hickory-server"
|
||||
version = "0.25.2"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d53e5fe811b941c74ee46b8818228bfd2bc2688ba276a0eaeb0f2c95ea3b2585"
|
||||
checksum = "130236ba6abba90da6a7acf7a87b27d862b592c3145dc74bc47bf86d8ff198ec"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"cfg-if",
|
||||
"data-encoding",
|
||||
"enum-as-inner",
|
||||
"futures-util",
|
||||
"hickory-net",
|
||||
"hickory-proto",
|
||||
"hickory-resolver",
|
||||
"ipnet",
|
||||
"prefix-trie",
|
||||
"prefix-trie 0.8.4",
|
||||
"serde",
|
||||
"thiserror 2.0.11",
|
||||
"time",
|
||||
@@ -3859,6 +3823,17 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.29.1"
|
||||
@@ -4044,7 +4019,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.3",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -4556,19 +4531,68 @@ dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"jni-sys 0.3.0",
|
||||
"log",
|
||||
"thiserror 1.0.63",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-macros",
|
||||
"jni-sys 0.4.1",
|
||||
"log",
|
||||
"simd_cesu8",
|
||||
"thiserror 2.0.11",
|
||||
"walkdir",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-macros"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"simd_cesu8",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
|
||||
dependencies = [
|
||||
"jni-sys-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys-macros"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.32"
|
||||
@@ -5103,9 +5127,12 @@ version = "0.12.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"crossbeam-channel",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
"event-listener",
|
||||
"futures-util",
|
||||
"loom",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
@@ -5207,7 +5234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"jni-sys",
|
||||
"jni-sys 0.3.0",
|
||||
"log",
|
||||
"ndk-sys",
|
||||
"num_enum",
|
||||
@@ -5227,7 +5254,7 @@ version = "0.6.0+11769913"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
|
||||
dependencies = [
|
||||
"jni-sys",
|
||||
"jni-sys 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5243,7 +5270,7 @@ dependencies = [
|
||||
"netlink-packet-route 0.17.1",
|
||||
"netlink-sys",
|
||||
"once_cell",
|
||||
"system-configuration",
|
||||
"system-configuration 0.6.1",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -5354,15 +5381,6 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "nibble_vec"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.25.1"
|
||||
@@ -5581,7 +5599,7 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro-crate 2.0.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@@ -5932,6 +5950,30 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "optionize"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f15b218321873a7919c8b2a188ad488bf18c067478274be4c69c3d402b2be3ea"
|
||||
dependencies = [
|
||||
"delegate",
|
||||
"derive_more 2.1.1",
|
||||
"optionize-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "optionize-macros"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1af5a2aba73a1808cbb39b451aabb5f8dc5c9c73d68ce5e61b79f8892f775a0f"
|
||||
dependencies = [
|
||||
"darling 0.23.0",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "2.10.1"
|
||||
@@ -6661,6 +6703,17 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prefix-trie"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7"
|
||||
dependencies = [
|
||||
"either",
|
||||
"ipnet",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.20"
|
||||
@@ -6701,11 +6754,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.2.0"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit 0.22.20",
|
||||
"toml_edit 0.25.12+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7058,16 +7111,6 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "radix_trie"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
|
||||
dependencies = [
|
||||
"endian-type",
|
||||
"nibble_vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
@@ -7394,7 +7437,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"system-configuration 0.6.1",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
@@ -7569,7 +7612,7 @@ checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"glob",
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
@@ -7732,6 +7775,7 @@ version = "0.23.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -7778,7 +7822,7 @@ checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.0",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
@@ -8577,6 +8621,16 @@ version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||
|
||||
[[package]]
|
||||
name = "simd_cesu8"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
"simdutf8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
@@ -9136,6 +9190,17 @@ dependencies = [
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
@@ -9205,7 +9270,7 @@ dependencies = [
|
||||
"gdkwayland-sys",
|
||||
"gdkx11-sys",
|
||||
"gtk",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"ndk",
|
||||
@@ -9267,7 +9332,7 @@ dependencies = [
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"image 0.25.10",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"mime",
|
||||
@@ -9402,7 +9467,7 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997"
|
||||
dependencies = [
|
||||
"gethostname 1.1.0",
|
||||
"gethostname",
|
||||
"log",
|
||||
"os_info",
|
||||
"serde",
|
||||
@@ -9495,7 +9560,7 @@ dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"objc2",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
@@ -9518,7 +9583,7 @@ checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
@@ -9969,6 +10034,15 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.15"
|
||||
@@ -10006,6 +10080,18 @@ dependencies = [
|
||||
"winnow 0.6.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.12+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
|
||||
dependencies = [
|
||||
"indexmap 2.14.0",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow 1.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
@@ -10051,19 +10137,6 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic-build"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "568392c5a2bd0020723e3f387891176aabafe36fd9fcd074ad309dfa0c8eb964"
|
||||
dependencies = [
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"prost-build",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
@@ -11866,6 +11939,9 @@ name = "winnow"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
@@ -12067,7 +12143,7 @@ dependencies = [
|
||||
"html5ever",
|
||||
"http",
|
||||
"javascriptcore-rs",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"kuchikiki",
|
||||
"libc",
|
||||
"ndk",
|
||||
@@ -12130,7 +12206,7 @@ version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
|
||||
dependencies = [
|
||||
"gethostname 1.1.0",
|
||||
"gethostname",
|
||||
"rustix 1.0.7",
|
||||
"x11rb-protocol",
|
||||
]
|
||||
@@ -12247,7 +12323,7 @@ version = "5.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@@ -12484,7 +12560,7 @@ version = "5.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
|
||||
@@ -3,7 +3,6 @@ resolver = "2"
|
||||
members = [
|
||||
"easytier",
|
||||
"easytier-gui/src-tauri",
|
||||
"easytier-rpc-build",
|
||||
"easytier-web",
|
||||
"easytier-contrib/easytier-ffi",
|
||||
"easytier-contrib/easytier-uptime",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
id=easytier_magisk
|
||||
name=EasyTier_Magisk
|
||||
version=v2.6.3
|
||||
version=v2.6.4
|
||||
versionCode=1
|
||||
author=EasyTier
|
||||
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
|
||||
|
||||
+544
-132
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,10 @@ edition = "2024"
|
||||
crate-type=["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
base64 = "0.22"
|
||||
flate2 = "1.1"
|
||||
gethostname = "1.1"
|
||||
ohos-hilog-binding = {version = "*", features = ["redirect"]}
|
||||
easytier = { path = "../../easytier" }
|
||||
napi-derive-ohos = "1.1"
|
||||
@@ -26,10 +30,16 @@ napi-ohos = { version = "1.1", default-features = false, features = [
|
||||
"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",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
pub(crate) mod repository;
|
||||
pub(crate) mod services;
|
||||
pub(crate) mod storage;
|
||||
pub(crate) mod types;
|
||||
@@ -0,0 +1,13 @@
|
||||
#[path = "../../config_repo/field_store.rs"]
|
||||
mod field_store;
|
||||
#[path = "../../config_repo/import_export.rs"]
|
||||
mod import_export;
|
||||
#[path = "../../config_repo/legacy_migration.rs"]
|
||||
mod legacy_migration;
|
||||
#[path = "../../config_repo/validation.rs"]
|
||||
mod validation;
|
||||
|
||||
#[path = "../../config_repo.rs"]
|
||||
mod repo;
|
||||
|
||||
pub use repo::*;
|
||||
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod schema_service;
|
||||
pub(crate) mod share_link_service;
|
||||
@@ -0,0 +1,414 @@
|
||||
use easytier::proto::ALL_DESCRIPTOR_BYTES;
|
||||
use napi_derive_ohos::napi;
|
||||
use once_cell::sync::Lazy;
|
||||
use prost_reflect::{Cardinality, DescriptorPool, FieldDescriptor, Kind, MessageDescriptor};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[napi(object)]
|
||||
pub struct FieldOption {
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[napi(object)]
|
||||
pub struct ValidationRule {
|
||||
pub rule_type: String,
|
||||
pub arg: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[napi(object)]
|
||||
pub struct NetworkConfigSchema {
|
||||
pub node_kind: String,
|
||||
pub name: String,
|
||||
pub field_number: i32,
|
||||
pub type_name: Option<String>,
|
||||
pub semantic_type: Option<String>,
|
||||
pub value_kind: String,
|
||||
pub is_list: bool,
|
||||
pub required: bool,
|
||||
pub default_value_text: Option<String>,
|
||||
pub enum_options: Vec<FieldOption>,
|
||||
pub validations: Vec<ValidationRule>,
|
||||
pub children: Vec<NetworkConfigSchema>,
|
||||
pub definitions: Vec<NetworkConfigSchema>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[napi(object)]
|
||||
pub struct ConfigFieldMapping {
|
||||
pub field_name: String,
|
||||
pub field_number: i32,
|
||||
}
|
||||
|
||||
static DESCRIPTOR_POOL: Lazy<DescriptorPool> = Lazy::new(|| {
|
||||
DescriptorPool::decode(ALL_DESCRIPTOR_BYTES)
|
||||
.expect("easytier descriptor pool should decode from embedded protobuf descriptors")
|
||||
});
|
||||
|
||||
const NETWORK_CONFIG_MESSAGE_NAME: &str = "api.manage.NetworkConfig";
|
||||
|
||||
fn descriptor_pool() -> &'static DescriptorPool {
|
||||
&DESCRIPTOR_POOL
|
||||
}
|
||||
|
||||
fn network_config_descriptor() -> MessageDescriptor {
|
||||
descriptor_pool()
|
||||
.get_message_by_name(NETWORK_CONFIG_MESSAGE_NAME)
|
||||
.expect("api.manage.NetworkConfig descriptor should exist")
|
||||
}
|
||||
|
||||
fn field_default_value_text(field: &FieldDescriptor) -> Option<String> {
|
||||
if field.is_list() || field.is_map() {
|
||||
return Some("[]".to_string());
|
||||
}
|
||||
|
||||
match field.kind() {
|
||||
Kind::Bool => Some("false".to_string()),
|
||||
Kind::String => Some("\"\"".to_string()),
|
||||
Kind::Bytes => Some("\"\"".to_string()),
|
||||
Kind::Int32
|
||||
| Kind::Sint32
|
||||
| Kind::Sfixed32
|
||||
| Kind::Int64
|
||||
| Kind::Sint64
|
||||
| Kind::Sfixed64
|
||||
| Kind::Uint32
|
||||
| Kind::Fixed32
|
||||
| Kind::Uint64
|
||||
| Kind::Fixed64
|
||||
| Kind::Float
|
||||
| Kind::Double => Some("0".to_string()),
|
||||
Kind::Enum(enum_desc) => enum_desc
|
||||
.get_value(0)
|
||||
.map(|value| value.number().to_string()),
|
||||
Kind::Message(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn field_type_name(field: &FieldDescriptor) -> Option<String> {
|
||||
match field.kind() {
|
||||
Kind::Enum(enum_desc) => Some(enum_desc.full_name().to_string()),
|
||||
Kind::Message(message_desc) => Some(message_desc.full_name().to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn field_semantic_type(field: &FieldDescriptor) -> Option<String> {
|
||||
match field.name() {
|
||||
"virtual_ipv4" => Some("cidr_ip".to_string()),
|
||||
"network_length" => Some("cidr_mask".to_string()),
|
||||
"peer_urls" => Some("peer[]".to_string()),
|
||||
"proxy_cidrs" => Some("cidr[]".to_string()),
|
||||
"listener_urls" => Some("listener[]".to_string()),
|
||||
"routes" => Some("route[]".to_string()),
|
||||
"exit_nodes" => Some("ip[]".to_string()),
|
||||
"relay_network_whitelist" => Some("network_name[]".to_string()),
|
||||
"mapped_listeners" => Some("mapped_listener[]".to_string()),
|
||||
"port_forwards" => Some("port_forward[]".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn enum_options(kind: Kind) -> Vec<FieldOption> {
|
||||
match kind {
|
||||
Kind::Enum(enum_desc) => enum_desc
|
||||
.values()
|
||||
.map(|value| FieldOption {
|
||||
label: value.name().to_string(),
|
||||
value: value.number().to_string(),
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn should_expose_field(field: &FieldDescriptor) -> bool {
|
||||
match field.containing_oneof() {
|
||||
Some(_) => field
|
||||
.field_descriptor_proto()
|
||||
.proto3_optional
|
||||
.unwrap_or(false),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_validations(field: &FieldDescriptor) -> Vec<ValidationRule> {
|
||||
if field.cardinality() == Cardinality::Required {
|
||||
return vec![ValidationRule {
|
||||
rule_type: "required".to_string(),
|
||||
arg: String::new(),
|
||||
message: format!("{} is required", field.name()),
|
||||
}];
|
||||
}
|
||||
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn kind_to_value_kind(field: &FieldDescriptor) -> String {
|
||||
if field.is_map() {
|
||||
return "object".to_string();
|
||||
}
|
||||
|
||||
match field.kind() {
|
||||
Kind::Bool => "boolean".to_string(),
|
||||
Kind::String | Kind::Bytes => "string".to_string(),
|
||||
Kind::Int32
|
||||
| Kind::Sint32
|
||||
| Kind::Sfixed32
|
||||
| Kind::Int64
|
||||
| Kind::Sint64
|
||||
| Kind::Sfixed64
|
||||
| Kind::Uint32
|
||||
| Kind::Fixed32
|
||||
| Kind::Uint64
|
||||
| Kind::Fixed64
|
||||
| Kind::Float
|
||||
| Kind::Double => "number".to_string(),
|
||||
Kind::Enum(_) => "enum".to_string(),
|
||||
Kind::Message(_) => "object".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_node(
|
||||
node_kind: &str,
|
||||
name: String,
|
||||
field_number: i32,
|
||||
type_name: Option<String>,
|
||||
semantic_type: Option<String>,
|
||||
value_kind: String,
|
||||
is_list: bool,
|
||||
required: bool,
|
||||
default_value_text: Option<String>,
|
||||
enum_options: Vec<FieldOption>,
|
||||
validations: Vec<ValidationRule>,
|
||||
children: Vec<NetworkConfigSchema>,
|
||||
definitions: Vec<NetworkConfigSchema>,
|
||||
) -> NetworkConfigSchema {
|
||||
NetworkConfigSchema {
|
||||
node_kind: node_kind.to_string(),
|
||||
name,
|
||||
field_number,
|
||||
type_name,
|
||||
semantic_type,
|
||||
value_kind,
|
||||
is_list,
|
||||
required,
|
||||
default_value_text,
|
||||
enum_options,
|
||||
validations,
|
||||
children,
|
||||
definitions,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_map_entry_node(message_desc: &MessageDescriptor) -> NetworkConfigSchema {
|
||||
let key_field = message_desc.map_entry_key_field();
|
||||
let value_field = message_desc.map_entry_value_field();
|
||||
|
||||
build_node(
|
||||
"object",
|
||||
message_desc.name().to_string(),
|
||||
0,
|
||||
Some(message_desc.full_name().to_string()),
|
||||
None,
|
||||
"object".to_string(),
|
||||
false,
|
||||
true,
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
vec![
|
||||
build_schema_field_node(&key_field),
|
||||
build_schema_field_node(&value_field),
|
||||
],
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
|
||||
fn field_children(field: &FieldDescriptor) -> Vec<NetworkConfigSchema> {
|
||||
if field.is_map() {
|
||||
if let Kind::Message(message_desc) = field.kind() {
|
||||
return vec![build_map_entry_node(&message_desc)];
|
||||
}
|
||||
}
|
||||
|
||||
match field.kind() {
|
||||
Kind::Message(message_desc) => build_message_children(&message_desc),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_message_children(message_desc: &MessageDescriptor) -> Vec<NetworkConfigSchema> {
|
||||
message_desc
|
||||
.fields()
|
||||
.filter(should_expose_field)
|
||||
.map(|field| build_schema_field_node(&field))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_schema_field_node(field: &FieldDescriptor) -> NetworkConfigSchema {
|
||||
build_node(
|
||||
"field",
|
||||
field.name().to_string(),
|
||||
field.number() as i32,
|
||||
field_type_name(field),
|
||||
field_semantic_type(field),
|
||||
kind_to_value_kind(field),
|
||||
field.is_list() || field.is_map(),
|
||||
field.cardinality() == Cardinality::Required,
|
||||
field_default_value_text(field),
|
||||
enum_options(field.kind()),
|
||||
build_validations(field),
|
||||
field_children(field),
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
|
||||
fn collect_definitions() -> Vec<NetworkConfigSchema> {
|
||||
let mut definitions = Vec::new();
|
||||
|
||||
for message_desc in descriptor_pool().all_messages() {
|
||||
let full_name = message_desc.full_name();
|
||||
if full_name == NETWORK_CONFIG_MESSAGE_NAME || message_desc.is_map_entry() {
|
||||
continue;
|
||||
}
|
||||
|
||||
definitions.push(build_node(
|
||||
"object",
|
||||
full_name.to_string(),
|
||||
0,
|
||||
Some(full_name.to_string()),
|
||||
None,
|
||||
"object".to_string(),
|
||||
false,
|
||||
true,
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
build_message_children(&message_desc),
|
||||
Vec::new(),
|
||||
));
|
||||
}
|
||||
|
||||
for enum_desc in descriptor_pool().all_enums() {
|
||||
definitions.push(build_node(
|
||||
"enum",
|
||||
enum_desc.full_name().to_string(),
|
||||
0,
|
||||
Some(enum_desc.full_name().to_string()),
|
||||
None,
|
||||
"enum".to_string(),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
enum_options(Kind::Enum(enum_desc.clone())),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
));
|
||||
}
|
||||
|
||||
definitions.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
definitions
|
||||
}
|
||||
|
||||
fn build_network_config_schema() -> NetworkConfigSchema {
|
||||
let network_config = network_config_descriptor();
|
||||
build_node(
|
||||
"schema",
|
||||
network_config.name().to_string(),
|
||||
0,
|
||||
Some(network_config.full_name().to_string()),
|
||||
None,
|
||||
"object".to_string(),
|
||||
false,
|
||||
true,
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
build_message_children(&network_config),
|
||||
collect_definitions(),
|
||||
)
|
||||
}
|
||||
|
||||
fn build_network_config_field_mappings() -> Vec<ConfigFieldMapping> {
|
||||
network_config_descriptor()
|
||||
.fields()
|
||||
.filter(should_expose_field)
|
||||
.map(|field| ConfigFieldMapping {
|
||||
field_name: field.name().to_string(),
|
||||
field_number: field.number() as i32,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_network_config_schema() -> NetworkConfigSchema {
|
||||
build_network_config_schema()
|
||||
}
|
||||
|
||||
pub fn get_network_config_field_mappings() -> Vec<ConfigFieldMapping> {
|
||||
build_network_config_field_mappings()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn schema_is_exposed_as_single_tree_type() {
|
||||
let schema = get_network_config_schema();
|
||||
assert_eq!(schema.node_kind, "schema");
|
||||
assert_eq!(schema.name, "NetworkConfig");
|
||||
assert_eq!(
|
||||
schema.type_name.as_deref(),
|
||||
Some("api.manage.NetworkConfig")
|
||||
);
|
||||
|
||||
let virtual_ipv4 = schema
|
||||
.children
|
||||
.iter()
|
||||
.find(|field| field.name == "virtual_ipv4")
|
||||
.expect("virtual_ipv4 field");
|
||||
assert_eq!(virtual_ipv4.semantic_type.as_deref(), Some("cidr_ip"));
|
||||
|
||||
let secure_mode = schema
|
||||
.children
|
||||
.iter()
|
||||
.find(|field| field.name == "secure_mode")
|
||||
.expect("secure_mode field");
|
||||
assert!(
|
||||
secure_mode
|
||||
.children
|
||||
.iter()
|
||||
.any(|field| field.name == "enabled")
|
||||
);
|
||||
|
||||
let secure_mode_definition = schema
|
||||
.definitions
|
||||
.iter()
|
||||
.find(|definition| definition.name == "common.SecureModeConfig")
|
||||
.expect("secure mode definition");
|
||||
assert!(
|
||||
secure_mode_definition
|
||||
.children
|
||||
.iter()
|
||||
.any(|field| field.name == "local_private_key")
|
||||
);
|
||||
|
||||
let networking_method_definition = schema
|
||||
.definitions
|
||||
.iter()
|
||||
.find(|definition| definition.name == "api.manage.NetworkingMethod")
|
||||
.expect("networking method enum definition");
|
||||
assert!(
|
||||
networking_method_definition
|
||||
.enum_options
|
||||
.iter()
|
||||
.any(|option| option.label == "PublicServer")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
use crate::config::repository::{get_config_record, save_config_record};
|
||||
use crate::config::services::schema_service::get_network_config_field_mappings;
|
||||
use crate::config::types::stored_config::SharedConfigLinkPayload;
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use flate2::{Compression, read::ZlibDecoder, write::ZlibEncoder};
|
||||
use gethostname::gethostname;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Write};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
const SHARE_LINK_HOST: &str = "easytier.cn";
|
||||
const SHARE_LINK_PATH: &str = "/comp_cfg";
|
||||
|
||||
fn field_name_to_id_map() -> HashMap<String, String> {
|
||||
get_network_config_field_mappings()
|
||||
.into_iter()
|
||||
.map(|mapping| (mapping.field_name, mapping.field_number.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn field_id_to_name_map() -> HashMap<String, String> {
|
||||
get_network_config_field_mappings()
|
||||
.into_iter()
|
||||
.map(|mapping| (mapping.field_number.to_string(), mapping.field_name))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn prune_empty(value: &serde_json::Value) -> Option<serde_json::Value> {
|
||||
match value {
|
||||
serde_json::Value::Null => None,
|
||||
serde_json::Value::Array(values) if values.is_empty() => None,
|
||||
_ => Some(value.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_config_json(config: &NetworkConfig) -> Result<String, String> {
|
||||
let field_name_to_id = field_name_to_id_map();
|
||||
let raw = serde_json::to_value(config).map_err(|err| err.to_string())?;
|
||||
let mut mapped = serde_json::Map::new();
|
||||
|
||||
for (key, value) in raw.as_object().cloned().unwrap_or_default() {
|
||||
let Some(value) = prune_empty(&value) else {
|
||||
continue;
|
||||
};
|
||||
let mapped_key = field_name_to_id.get(&key).cloned().unwrap_or(key);
|
||||
mapped.insert(mapped_key, value);
|
||||
}
|
||||
|
||||
serde_json::to_string(&mapped).map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
fn unmap_config_json(raw: &str) -> Result<NetworkConfig, String> {
|
||||
let field_id_to_name = field_id_to_name_map();
|
||||
let value = serde_json::from_str::<serde_json::Value>(raw).map_err(|err| err.to_string())?;
|
||||
let mut mapped = serde_json::Map::new();
|
||||
for (key, value) in value.as_object().cloned().unwrap_or_default() {
|
||||
let field_name = field_id_to_name.get(&key).cloned().unwrap_or(key);
|
||||
mapped.insert(field_name, value);
|
||||
}
|
||||
serde_json::from_value(serde_json::Value::Object(mapped)).map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
fn compress_to_base64url(raw: &str) -> Result<String, String> {
|
||||
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
|
||||
encoder
|
||||
.write_all(raw.as_bytes())
|
||||
.map_err(|err| err.to_string())?;
|
||||
let compressed = encoder.finish().map_err(|err| err.to_string())?;
|
||||
Ok(URL_SAFE_NO_PAD.encode(compressed))
|
||||
}
|
||||
|
||||
fn decompress_from_base64url(raw: &str) -> Result<String, String> {
|
||||
let compressed = URL_SAFE_NO_PAD.decode(raw).map_err(|err| err.to_string())?;
|
||||
let mut decoder = ZlibDecoder::new(compressed.as_slice());
|
||||
let mut out = String::new();
|
||||
decoder
|
||||
.read_to_string(&mut out)
|
||||
.map_err(|err| err.to_string())?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn build_config_share_link(
|
||||
config_id: &str,
|
||||
display_name: Option<String>,
|
||||
only_start: bool,
|
||||
) -> Option<String> {
|
||||
let record = get_config_record(config_id)?;
|
||||
let config = serde_json::from_str::<NetworkConfig>(&record.config_json).ok()?;
|
||||
let mapped_json = map_config_json(&config).ok()?;
|
||||
let compressed = compress_to_base64url(&mapped_json).ok()?;
|
||||
let final_name = display_name
|
||||
.or(Some(record.meta.display_name))
|
||||
.filter(|name| !name.is_empty());
|
||||
|
||||
let mut url = Url::parse(&format!("https://{SHARE_LINK_HOST}{SHARE_LINK_PATH}")).ok()?;
|
||||
url.query_pairs_mut().append_pair("cfg", &compressed);
|
||||
if let Some(name) = final_name {
|
||||
url.query_pairs_mut().append_pair("name", &name);
|
||||
}
|
||||
if only_start {
|
||||
url.query_pairs_mut().append_pair("only_start", "true");
|
||||
}
|
||||
Some(url.to_string())
|
||||
}
|
||||
|
||||
pub fn parse_config_share_link(share_link: &str) -> Option<SharedConfigLinkPayload> {
|
||||
let url = Url::parse(share_link).ok()?;
|
||||
if url.host_str()? != SHARE_LINK_HOST || url.path() != SHARE_LINK_PATH {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cfg = url
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "cfg")?
|
||||
.1
|
||||
.to_string();
|
||||
let mapped_json = decompress_from_base64url(&cfg).ok()?;
|
||||
let mut config = unmap_config_json(&mapped_json).ok()?;
|
||||
config.instance_id = Some(Uuid::new_v4().to_string());
|
||||
let hostname = gethostname().to_string_lossy().to_string();
|
||||
if !hostname.is_empty() {
|
||||
config.hostname = Some(hostname);
|
||||
}
|
||||
|
||||
let config_json = serde_json::to_string(&config).ok()?;
|
||||
let display_name = url
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "name")
|
||||
.map(|(_, value)| value.to_string())
|
||||
.filter(|name| !name.is_empty());
|
||||
let only_start = url
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "only_start")
|
||||
.map(|(_, value)| value == "true")
|
||||
.unwrap_or(false);
|
||||
|
||||
Some(SharedConfigLinkPayload {
|
||||
config_json,
|
||||
display_name,
|
||||
only_start,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn import_config_share_link(
|
||||
share_link: &str,
|
||||
display_name_override: Option<String>,
|
||||
) -> Option<String> {
|
||||
let payload = parse_config_share_link(share_link)?;
|
||||
let config = serde_json::from_str::<NetworkConfig>(&payload.config_json).ok()?;
|
||||
let config_id = config.instance_id.clone()?;
|
||||
let display_name = display_name_override
|
||||
.filter(|name| !name.is_empty())
|
||||
.or(payload.display_name)
|
||||
.unwrap_or_else(|| config_id.clone());
|
||||
|
||||
save_config_record(config_id.clone(), display_name, payload.config_json)?;
|
||||
Some(config_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config_repo::{create_config_record, init_config_store};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn test_root() -> String {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
std::env::temp_dir()
|
||||
.join(format!("easytier_ohrs_share_test_{unique}"))
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn share_link_roundtrip_works() {
|
||||
assert!(init_config_store(test_root()));
|
||||
create_config_record("cfg-share".to_string(), "share-demo".to_string())
|
||||
.expect("create config");
|
||||
|
||||
let link = build_config_share_link("cfg-share", None, true).expect("share link");
|
||||
let payload = parse_config_share_link(&link).expect("parse link");
|
||||
let config =
|
||||
serde_json::from_str::<NetworkConfig>(&payload.config_json).expect("config json");
|
||||
|
||||
assert!(payload.only_start);
|
||||
assert_eq!(payload.display_name.as_deref(), Some("share-demo"));
|
||||
assert_ne!(config.instance_id.as_deref(), Some("cfg-share"));
|
||||
|
||||
let imported_id = import_config_share_link(&link, None).expect("import link");
|
||||
assert_ne!(imported_id, "cfg-share");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
use crate::config::types::stored_config::{StoredConfigList, StoredConfigMeta};
|
||||
use ohos_hilog_binding::{hilog_debug, hilog_error};
|
||||
use rusqlite::{Connection, OptionalExtension, params};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
static CONFIG_DB_PATH: Mutex<Option<PathBuf>> = Mutex::new(None);
|
||||
const CONFIG_DB_FILE_NAME: &str = "easytier-config-store.db";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct StoredConfigMetaRecord {
|
||||
config_id: String,
|
||||
display_name: String,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
favorite: bool,
|
||||
temporary: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn now_ts_string() -> String {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs().to_string())
|
||||
.unwrap_or_else(|_| "0".to_string())
|
||||
}
|
||||
|
||||
fn db_file_path() -> Option<PathBuf> {
|
||||
CONFIG_DB_PATH
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.as_ref().cloned())
|
||||
}
|
||||
|
||||
fn init_schema(conn: &Connection) -> rusqlite::Result<()> {
|
||||
conn.execute_batch(
|
||||
"PRAGMA foreign_keys = ON;
|
||||
CREATE TABLE IF NOT EXISTS stored_configs (
|
||||
config_id TEXT PRIMARY KEY,
|
||||
display_name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
favorite INTEGER NOT NULL DEFAULT 0,
|
||||
temporary INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS stored_config_fields (
|
||||
config_id TEXT NOT NULL,
|
||||
field_name TEXT NOT NULL,
|
||||
field_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (config_id, field_name),
|
||||
FOREIGN KEY (config_id) REFERENCES stored_configs(config_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_stored_config_fields_config_id
|
||||
ON stored_config_fields(config_id);",
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn open_db() -> Option<Connection> {
|
||||
let path = db_file_path()?;
|
||||
let conn = match Connection::open(&path) {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] failed to open config db {}: {}", path.display(), e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = init_schema(&conn) {
|
||||
hilog_error!(
|
||||
"[Rust] failed to initialize config db {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(conn)
|
||||
}
|
||||
|
||||
fn row_to_meta(row: &rusqlite::Row<'_>) -> rusqlite::Result<StoredConfigMetaRecord> {
|
||||
Ok(StoredConfigMetaRecord {
|
||||
config_id: row.get(0)?,
|
||||
display_name: row.get(1)?,
|
||||
created_at: row.get(2)?,
|
||||
updated_at: row.get(3)?,
|
||||
favorite: row.get::<_, i64>(4)? != 0,
|
||||
temporary: row.get::<_, i64>(5)? != 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_meta_record(conn: &Connection, config_id: &str) -> Option<StoredConfigMetaRecord> {
|
||||
conn.query_row(
|
||||
"SELECT config_id, display_name, created_at, updated_at, favorite, temporary
|
||||
FROM stored_configs WHERE config_id = ?1",
|
||||
params![config_id],
|
||||
row_to_meta,
|
||||
)
|
||||
.optional()
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn to_meta(record: StoredConfigMetaRecord) -> StoredConfigMeta {
|
||||
StoredConfigMeta {
|
||||
config_id: record.config_id,
|
||||
display_name: record.display_name,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
favorite: record.favorite,
|
||||
temporary: record.temporary,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_config_meta_store(root_dir: String) -> bool {
|
||||
let root = PathBuf::from(root_dir);
|
||||
if let Err(e) = std::fs::create_dir_all(&root) {
|
||||
hilog_error!(
|
||||
"[Rust] failed to create config db dir {}: {}",
|
||||
root.display(),
|
||||
e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
let db_path = root.join(CONFIG_DB_FILE_NAME);
|
||||
match CONFIG_DB_PATH.lock() {
|
||||
Ok(mut guard) => {
|
||||
*guard = Some(db_path.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] failed to lock config db path: {}", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if open_db().is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
hilog_debug!("[Rust] initialized config db at {}", db_path.display());
|
||||
true
|
||||
}
|
||||
|
||||
pub fn list_config_meta_entries() -> StoredConfigList {
|
||||
let Some(conn) = open_db() else {
|
||||
return StoredConfigList { configs: vec![] };
|
||||
};
|
||||
|
||||
let mut stmt = match conn.prepare(
|
||||
"SELECT config_id, display_name, created_at, updated_at, favorite, temporary
|
||||
FROM stored_configs
|
||||
ORDER BY updated_at DESC, display_name ASC",
|
||||
) {
|
||||
Ok(stmt) => stmt,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] failed to prepare list meta query: {}", e);
|
||||
return StoredConfigList { configs: vec![] };
|
||||
}
|
||||
};
|
||||
|
||||
let rows = match stmt.query_map([], row_to_meta) {
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] failed to list config meta rows: {}", e);
|
||||
return StoredConfigList { configs: vec![] };
|
||||
}
|
||||
};
|
||||
|
||||
let configs = rows.filter_map(Result::ok).map(to_meta).collect();
|
||||
StoredConfigList { configs }
|
||||
}
|
||||
|
||||
pub fn get_config_display_name(config_id: &str) -> Option<String> {
|
||||
let conn = open_db()?;
|
||||
load_meta_record(&conn, config_id).map(|record| record.display_name)
|
||||
}
|
||||
|
||||
pub fn get_config_meta(config_id: &str) -> Option<StoredConfigMeta> {
|
||||
let conn = open_db()?;
|
||||
load_meta_record(&conn, config_id).map(to_meta)
|
||||
}
|
||||
|
||||
pub fn upsert_config_meta(
|
||||
config_id: String,
|
||||
display_name: String,
|
||||
favorite: bool,
|
||||
temporary: bool,
|
||||
) -> StoredConfigMeta {
|
||||
let now = now_ts_string();
|
||||
let Some(conn) = open_db() else {
|
||||
return StoredConfigMeta {
|
||||
config_id,
|
||||
display_name,
|
||||
created_at: now.clone(),
|
||||
updated_at: now,
|
||||
favorite,
|
||||
temporary,
|
||||
};
|
||||
};
|
||||
|
||||
let created_at = load_meta_record(&conn, &config_id)
|
||||
.map(|record| record.created_at)
|
||||
.unwrap_or_else(|| now.clone());
|
||||
|
||||
if let Err(e) = conn.execute(
|
||||
"INSERT INTO stored_configs (
|
||||
config_id, display_name, created_at, updated_at, favorite, temporary
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
ON CONFLICT(config_id) DO UPDATE SET
|
||||
display_name = excluded.display_name,
|
||||
updated_at = excluded.updated_at,
|
||||
favorite = excluded.favorite,
|
||||
temporary = excluded.temporary",
|
||||
params![
|
||||
config_id,
|
||||
display_name,
|
||||
created_at,
|
||||
now,
|
||||
if favorite { 1 } else { 0 },
|
||||
if temporary { 1 } else { 0 }
|
||||
],
|
||||
) {
|
||||
hilog_error!("[Rust] failed to upsert config meta: {}", e);
|
||||
}
|
||||
|
||||
get_config_meta(&config_id).unwrap_or(StoredConfigMeta {
|
||||
config_id,
|
||||
display_name,
|
||||
created_at,
|
||||
updated_at: now,
|
||||
favorite,
|
||||
temporary,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn upsert_config_meta_in_tx(
|
||||
tx: &rusqlite::Transaction<'_>,
|
||||
config_id: String,
|
||||
display_name: String,
|
||||
favorite: bool,
|
||||
temporary: bool,
|
||||
) -> Option<StoredConfigMeta> {
|
||||
let now = now_ts_string();
|
||||
let created_at = tx
|
||||
.query_row(
|
||||
"SELECT config_id, display_name, created_at, updated_at, favorite, temporary
|
||||
FROM stored_configs WHERE config_id = ?1",
|
||||
params![config_id],
|
||||
row_to_meta,
|
||||
)
|
||||
.optional()
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|record| record.created_at)
|
||||
.unwrap_or_else(|| now.clone());
|
||||
|
||||
tx.execute(
|
||||
"INSERT INTO stored_configs (
|
||||
config_id, display_name, created_at, updated_at, favorite, temporary
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
ON CONFLICT(config_id) DO UPDATE SET
|
||||
display_name = excluded.display_name,
|
||||
updated_at = excluded.updated_at,
|
||||
favorite = excluded.favorite,
|
||||
temporary = excluded.temporary",
|
||||
params![
|
||||
config_id,
|
||||
display_name,
|
||||
created_at,
|
||||
now,
|
||||
if favorite { 1 } else { 0 },
|
||||
if temporary { 1 } else { 0 }
|
||||
],
|
||||
)
|
||||
.ok()?;
|
||||
|
||||
tx.query_row(
|
||||
"SELECT config_id, display_name, created_at, updated_at, favorite, temporary
|
||||
FROM stored_configs WHERE config_id = ?1",
|
||||
params![config_id],
|
||||
row_to_meta,
|
||||
)
|
||||
.optional()
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(to_meta)
|
||||
.or(Some(StoredConfigMeta {
|
||||
config_id,
|
||||
display_name,
|
||||
created_at,
|
||||
updated_at: now,
|
||||
favorite,
|
||||
temporary,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn set_config_display_name(
|
||||
config_id: String,
|
||||
display_name: String,
|
||||
) -> Option<StoredConfigMeta> {
|
||||
let conn = open_db()?;
|
||||
let mut record = load_meta_record(&conn, &config_id)?;
|
||||
record.display_name = display_name;
|
||||
record.updated_at = now_ts_string();
|
||||
|
||||
conn.execute(
|
||||
"UPDATE stored_configs
|
||||
SET display_name = ?2, updated_at = ?3
|
||||
WHERE config_id = ?1",
|
||||
params![config_id, record.display_name, record.updated_at],
|
||||
)
|
||||
.ok()?;
|
||||
|
||||
Some(to_meta(record))
|
||||
}
|
||||
|
||||
pub fn delete_config_meta(config_id: &str) -> bool {
|
||||
let Some(conn) = open_db() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
match conn.execute(
|
||||
"DELETE FROM stored_configs WHERE config_id = ?1",
|
||||
params![config_id],
|
||||
) {
|
||||
Ok(rows) => rows > 0,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] failed to delete config meta {}: {}", config_id, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub(crate) mod config_meta;
|
||||
@@ -0,0 +1 @@
|
||||
pub(crate) mod stored_config;
|
||||
@@ -0,0 +1,68 @@
|
||||
use napi_derive_ohos::napi;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct StoredConfigMeta {
|
||||
pub config_id: String,
|
||||
pub display_name: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub favorite: bool,
|
||||
pub temporary: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct StoredConfigRecord {
|
||||
pub meta: StoredConfigMeta,
|
||||
pub config_json: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct StoredConfigList {
|
||||
pub configs: Vec<StoredConfigMeta>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct ExportTomlResult {
|
||||
pub toml_text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct StoredConfigSummary {
|
||||
pub config_id: String,
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct SharedConfigLinkPayload {
|
||||
pub config_json: String,
|
||||
pub display_name: Option<String>,
|
||||
pub only_start: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct LocalSocketSyncMessage {
|
||||
pub message_type: String,
|
||||
pub payload_json: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[napi(object)]
|
||||
pub struct KeyValuePair {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
use super::{field_store, import_export, legacy_migration, validation};
|
||||
use crate::config::storage::config_meta::{
|
||||
delete_config_meta, get_config_meta, init_config_meta_store, list_config_meta_entries, open_db,
|
||||
upsert_config_meta_in_tx,
|
||||
};
|
||||
use crate::config::types::stored_config::{ExportTomlResult, StoredConfigRecord};
|
||||
use easytier::common::config::ConfigLoader;
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use ohos_hilog_binding::{hilog_debug, hilog_error};
|
||||
use rusqlite::params;
|
||||
use serde_json::Value;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static CONFIG_ROOT_DIR: Mutex<Option<PathBuf>> = Mutex::new(None);
|
||||
pub(crate) const CONFIG_DIR_NAME: &str = "easytier-configs";
|
||||
pub(crate) const KERNEL_SOCKET_FILE_NAME: &str = "easytier-kernel.sock";
|
||||
|
||||
pub(crate) fn config_root_dir() -> Option<PathBuf> {
|
||||
CONFIG_ROOT_DIR
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.as_ref().cloned())
|
||||
}
|
||||
|
||||
pub(crate) fn kernel_socket_path() -> Option<PathBuf> {
|
||||
config_root_dir().map(|root| root.join(KERNEL_SOCKET_FILE_NAME))
|
||||
}
|
||||
|
||||
pub(crate) fn legacy_config_file_path(config_id: &str) -> Option<PathBuf> {
|
||||
legacy_migration::legacy_config_file_path(&config_root_dir(), CONFIG_DIR_NAME, config_id)
|
||||
}
|
||||
|
||||
pub fn init_config_store(root_dir: String) -> bool {
|
||||
let root = PathBuf::from(root_dir);
|
||||
let configs_dir = root.join(CONFIG_DIR_NAME);
|
||||
if let Err(e) = std::fs::create_dir_all(&configs_dir) {
|
||||
hilog_error!(
|
||||
"[Rust] failed to create config dir {}: {}",
|
||||
configs_dir.display(),
|
||||
e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
match CONFIG_ROOT_DIR.lock() {
|
||||
Ok(mut guard) => {
|
||||
*guard = Some(root.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] failed to lock config root dir: {}", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if !init_config_meta_store(root.to_string_lossy().into_owned()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hilog_debug!(
|
||||
"[Rust] initialized config repo at {}",
|
||||
configs_dir.display()
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
fn migrate_legacy_file_if_needed(config_id: &str) -> Option<()> {
|
||||
legacy_migration::migrate_legacy_file_if_needed(
|
||||
&config_root_dir(),
|
||||
CONFIG_DIR_NAME,
|
||||
config_id,
|
||||
save_config_record,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn save_config_record(
|
||||
config_id: String,
|
||||
display_name: String,
|
||||
config_json: String,
|
||||
) -> Option<StoredConfigRecord> {
|
||||
let config = match validation::validate_config_json(&config_json, config_id.clone()) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] save_config_record failed {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let normalized_json = match serde_json::to_string(&config) {
|
||||
Ok(raw) => raw,
|
||||
Err(e) => {
|
||||
hilog_error!(
|
||||
"[Rust] failed to serialize normalized config {}: {}",
|
||||
config_id,
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let fields = match validation::config_to_top_level_map(&config) {
|
||||
Some(fields) => fields,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let conn = open_db()?;
|
||||
let tx = conn.unchecked_transaction().ok()?;
|
||||
let existing_meta = get_config_meta(&config_id);
|
||||
let favorite = existing_meta
|
||||
.as_ref()
|
||||
.map(|meta| meta.favorite)
|
||||
.unwrap_or(false);
|
||||
let temporary = existing_meta
|
||||
.as_ref()
|
||||
.map(|meta| meta.temporary)
|
||||
.unwrap_or(false);
|
||||
let meta = upsert_config_meta_in_tx(&tx, config_id.clone(), display_name, favorite, temporary)?;
|
||||
|
||||
field_store::replace_config_fields(&tx, &config_id, fields)?;
|
||||
|
||||
tx.commit().ok()?;
|
||||
|
||||
if let Some(legacy_path) = legacy_config_file_path(&config_id) {
|
||||
if legacy_path.exists() {
|
||||
let _ = std::fs::remove_file(legacy_path);
|
||||
}
|
||||
}
|
||||
|
||||
Some(StoredConfigRecord {
|
||||
meta,
|
||||
config_json: normalized_json,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_config_json(config_id: &str) -> Option<String> {
|
||||
migrate_legacy_file_if_needed(config_id)?;
|
||||
let object = field_store::load_config_map_from_db(config_id)?;
|
||||
serde_json::to_string(&Value::Object(object)).ok()
|
||||
}
|
||||
|
||||
pub fn get_config_record(config_id: &str) -> Option<StoredConfigRecord> {
|
||||
let config_json = load_config_json(config_id)?;
|
||||
let meta = get_config_meta(config_id)?;
|
||||
Some(StoredConfigRecord { meta, config_json })
|
||||
}
|
||||
|
||||
pub fn get_config_field_value(config_id: &str, field: &str) -> Option<String> {
|
||||
migrate_legacy_file_if_needed(config_id)?;
|
||||
let conn = open_db()?;
|
||||
conn.query_row(
|
||||
"SELECT field_json FROM stored_config_fields
|
||||
WHERE config_id = ?1 AND field_name = ?2",
|
||||
params![config_id, field],
|
||||
|row| row.get::<_, String>(0),
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn set_config_field_value(config_id: &str, field: &str, json_value: &str) -> bool {
|
||||
if field.contains('.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let raw = match load_config_json(config_id) {
|
||||
Some(raw) => raw,
|
||||
None => return false,
|
||||
};
|
||||
let mut value = match serde_json::from_str::<Value>(&raw) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let new_field_value = match serde_json::from_str::<Value>(json_value) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let object = match value.as_object_mut() {
|
||||
Some(object) => object,
|
||||
None => return false,
|
||||
};
|
||||
object.insert(field.to_string(), new_field_value);
|
||||
|
||||
let normalized = match serde_json::to_string(&value) {
|
||||
Ok(raw) => raw,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let display_name = get_config_meta(config_id)
|
||||
.map(|meta| meta.display_name)
|
||||
.unwrap_or_else(|| config_id.to_string());
|
||||
|
||||
save_config_record(config_id.to_string(), display_name, normalized).is_some()
|
||||
}
|
||||
|
||||
pub fn get_display_name(config_id: &str) -> Option<String> {
|
||||
get_config_meta(config_id).map(|meta| meta.display_name)
|
||||
}
|
||||
|
||||
pub fn get_default_config_json() -> Option<String> {
|
||||
crate::build_default_network_config_json().ok()
|
||||
}
|
||||
|
||||
pub fn create_config_record(config_id: String, display_name: String) -> Option<StoredConfigRecord> {
|
||||
let raw = get_default_config_json()?;
|
||||
let mut config = serde_json::from_str::<NetworkConfig>(&raw).ok()?;
|
||||
config.instance_id = Some(config_id.clone());
|
||||
let normalized_json = serde_json::to_string(&config).ok()?;
|
||||
save_config_record(config_id, display_name, normalized_json)
|
||||
}
|
||||
|
||||
pub fn start_kernel_with_config_id(config_id: &str) -> bool {
|
||||
let raw = match load_config_json(config_id) {
|
||||
Some(raw) => raw,
|
||||
None => return false,
|
||||
};
|
||||
crate::run_network_instance_from_json(&raw)
|
||||
}
|
||||
|
||||
pub fn list_config_meta_json() -> String {
|
||||
serde_json::to_string(&list_config_meta_entries().configs).unwrap_or_else(|_| "[]".to_string())
|
||||
}
|
||||
|
||||
pub fn delete_config_record(config_id: &str) -> bool {
|
||||
if let Some(path) = legacy_config_file_path(config_id) {
|
||||
if path.exists() {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
|
||||
let conn = match open_db() {
|
||||
Some(conn) => conn,
|
||||
None => return false,
|
||||
};
|
||||
if let Err(e) = conn.execute(
|
||||
"DELETE FROM stored_config_fields WHERE config_id = ?1",
|
||||
params![config_id],
|
||||
) {
|
||||
hilog_error!("[Rust] failed to delete config fields {}: {}", config_id, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
delete_config_meta(config_id)
|
||||
}
|
||||
|
||||
pub fn export_config_toml(config_id: &str) -> Option<ExportTomlResult> {
|
||||
let record = get_config_record(config_id)?;
|
||||
import_export::export_config_toml_from_record(&record)
|
||||
}
|
||||
|
||||
pub fn import_toml_config(
|
||||
toml_text: String,
|
||||
display_name: Option<String>,
|
||||
) -> Option<StoredConfigRecord> {
|
||||
import_export::import_toml_to_record(toml_text, display_name, save_config_record)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rusqlite::params;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn test_root() -> String {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let dir = std::env::temp_dir().join(format!("easytier_ohrs_test_{}", unique));
|
||||
dir.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_get_export_delete_roundtrip() {
|
||||
let root = test_root();
|
||||
assert!(init_config_store(root.clone()));
|
||||
|
||||
let config_json = crate::build_default_network_config_json().expect("default config");
|
||||
let saved = save_config_record("cfg-1".to_string(), "test-config".to_string(), config_json)
|
||||
.expect("save config");
|
||||
|
||||
assert_eq!(saved.meta.config_id, "cfg-1");
|
||||
assert_eq!(saved.meta.display_name, "test-config");
|
||||
|
||||
let loaded = get_config_record("cfg-1").expect("load config");
|
||||
assert_eq!(loaded.meta.display_name, "test-config");
|
||||
assert!(loaded.config_json.contains("cfg-1"));
|
||||
|
||||
let legacy_json_path = PathBuf::from(&root)
|
||||
.join(CONFIG_DIR_NAME)
|
||||
.join("cfg-1.json");
|
||||
assert!(
|
||||
!legacy_json_path.exists(),
|
||||
"config should no longer be persisted as a per-config json file"
|
||||
);
|
||||
|
||||
let conn = open_db().expect("db should be open");
|
||||
let field_count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM stored_config_fields WHERE config_id = ?1",
|
||||
params!["cfg-1"],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.expect("count config fields");
|
||||
assert!(field_count > 0, "config fields should be stored in sqlite");
|
||||
|
||||
let exported = export_config_toml("cfg-1").expect("export toml");
|
||||
assert!(exported.toml_text.contains("instance_id"));
|
||||
|
||||
assert!(delete_config_record("cfg-1"));
|
||||
assert!(get_config_record("cfg-1").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_config_field_updates_only_requested_top_level_field() {
|
||||
let root = test_root();
|
||||
assert!(init_config_store(root));
|
||||
|
||||
let config_json = crate::build_default_network_config_json().expect("default config");
|
||||
save_config_record(
|
||||
"cfg-field".to_string(),
|
||||
"field-config".to_string(),
|
||||
config_json,
|
||||
)
|
||||
.expect("save config");
|
||||
|
||||
let before_network_name = get_config_field_value("cfg-field", "network_name");
|
||||
let before_instance_id = get_config_field_value("cfg-field", "instance_id")
|
||||
.expect("instance id field should exist");
|
||||
|
||||
assert!(set_config_field_value(
|
||||
"cfg-field",
|
||||
"network_name",
|
||||
"\"changed-network\""
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
get_config_field_value("cfg-field", "network_name"),
|
||||
Some("\"changed-network\"".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
get_config_field_value("cfg-field", "instance_id"),
|
||||
Some(before_instance_id)
|
||||
);
|
||||
assert_ne!(
|
||||
get_config_field_value("cfg-field", "network_name"),
|
||||
before_network_name
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use crate::config::storage::config_meta::{now_ts_string, open_db};
|
||||
use ohos_hilog_binding::hilog_error;
|
||||
use rusqlite::{Connection, params};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
pub(super) fn load_config_map_from_db(config_id: &str) -> Option<Map<String, Value>> {
|
||||
let conn = open_db()?;
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT field_name, field_json
|
||||
FROM stored_config_fields
|
||||
WHERE config_id = ?1",
|
||||
)
|
||||
.ok()?;
|
||||
let rows = stmt
|
||||
.query_map(params![config_id], |row| {
|
||||
let field_name: String = row.get(0)?;
|
||||
let field_json: String = row.get(1)?;
|
||||
Ok((field_name, field_json))
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let mut object = Map::new();
|
||||
for row in rows {
|
||||
let (field_name, field_json) = row.ok()?;
|
||||
let value = serde_json::from_str::<Value>(&field_json).ok()?;
|
||||
object.insert(field_name, value);
|
||||
}
|
||||
|
||||
if object.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(object)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn replace_config_fields(
|
||||
tx: &Connection,
|
||||
config_id: &str,
|
||||
fields: Map<String, Value>,
|
||||
) -> Option<()> {
|
||||
if let Err(e) = tx.execute(
|
||||
"DELETE FROM stored_config_fields WHERE config_id = ?1",
|
||||
params![config_id],
|
||||
) {
|
||||
hilog_error!(
|
||||
"[Rust] failed to clear existing config fields {}: {}",
|
||||
config_id,
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
for (field_name, value) in fields {
|
||||
let field_json = serde_json::to_string(&value).ok()?;
|
||||
if let Err(e) = tx.execute(
|
||||
"INSERT INTO stored_config_fields (config_id, field_name, field_json, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
params![config_id, field_name, field_json, now_ts_string()],
|
||||
) {
|
||||
hilog_error!("[Rust] failed to persist config field {}: {}", config_id, e);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use crate::config::types::stored_config::{ExportTomlResult, StoredConfigRecord};
|
||||
use easytier::common::config::{ConfigLoader, TomlConfigLoader};
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
|
||||
pub(super) fn export_config_toml_from_record(
|
||||
record: &StoredConfigRecord,
|
||||
) -> Option<ExportTomlResult> {
|
||||
let config = serde_json::from_str::<NetworkConfig>(&record.config_json).ok()?;
|
||||
let toml = config.gen_config().ok()?;
|
||||
Some(ExportTomlResult {
|
||||
toml_text: toml.dump(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn import_toml_to_record(
|
||||
toml_text: String,
|
||||
display_name: Option<String>,
|
||||
save_config_record: impl Fn(String, String, String) -> Option<StoredConfigRecord>,
|
||||
) -> Option<StoredConfigRecord> {
|
||||
let config =
|
||||
NetworkConfig::new_from_config(TomlConfigLoader::new_from_str(&toml_text).ok()?).ok()?;
|
||||
|
||||
let config_id = config.instance_id.clone()?;
|
||||
let name_from_toml = toml_text
|
||||
.lines()
|
||||
.find_map(|line| {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.starts_with("instance_name") {
|
||||
return None;
|
||||
}
|
||||
trimmed.split_once('=').map(|(_, value)| {
|
||||
value
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.trim_matches('\'')
|
||||
.to_string()
|
||||
})
|
||||
})
|
||||
.filter(|name| !name.is_empty());
|
||||
|
||||
let final_name = display_name
|
||||
.filter(|name| !name.is_empty())
|
||||
.or(name_from_toml)
|
||||
.unwrap_or_else(|| config_id.clone());
|
||||
|
||||
let config_json = serde_json::to_string(&config).ok()?;
|
||||
save_config_record(config_id, final_name, config_json)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use crate::config::storage::config_meta::get_config_meta;
|
||||
use ohos_hilog_binding::hilog_error;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub(super) fn legacy_config_file_path(
|
||||
root_dir: &Option<PathBuf>,
|
||||
config_dir_name: &str,
|
||||
config_id: &str,
|
||||
) -> Option<PathBuf> {
|
||||
root_dir.as_ref().map(|root| {
|
||||
root.join(config_dir_name)
|
||||
.join(format!("{}.json", config_id))
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn migrate_legacy_file_if_needed(
|
||||
root_dir: &Option<PathBuf>,
|
||||
config_dir_name: &str,
|
||||
config_id: &str,
|
||||
save_config_record: impl Fn(
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
) -> Option<crate::config::types::stored_config::StoredConfigRecord>,
|
||||
) -> Option<()> {
|
||||
let legacy_path = legacy_config_file_path(root_dir, config_dir_name, config_id)?;
|
||||
if !legacy_path.exists() {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
let raw = std::fs::read_to_string(&legacy_path).ok()?;
|
||||
let display_name = get_config_meta(config_id)
|
||||
.map(|meta| meta.display_name)
|
||||
.unwrap_or_else(|| config_id.to_string());
|
||||
save_config_record(config_id.to_string(), display_name, raw)?;
|
||||
|
||||
if let Err(e) = std::fs::remove_file(&legacy_path) {
|
||||
hilog_error!(
|
||||
"[Rust] failed to remove legacy config file {}: {}",
|
||||
legacy_path.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
pub(super) fn normalize_config_id(
|
||||
mut config: NetworkConfig,
|
||||
requested_id: String,
|
||||
) -> Result<NetworkConfig, String> {
|
||||
if requested_id.is_empty() {
|
||||
return Err("config_id is required".to_string());
|
||||
}
|
||||
config.instance_id = Some(requested_id);
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub(super) fn validate_config_json(
|
||||
config_json: &str,
|
||||
config_id: String,
|
||||
) -> Result<NetworkConfig, String> {
|
||||
let config = serde_json::from_str::<NetworkConfig>(config_json)
|
||||
.map_err(|e| format!("parse config json failed: {}", e))?;
|
||||
let config = normalize_config_id(config, config_id)?;
|
||||
config
|
||||
.gen_config()
|
||||
.map_err(|e| format!("generate toml failed: {}", e))?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub(super) fn config_to_top_level_map(config: &NetworkConfig) -> Option<Map<String, Value>> {
|
||||
serde_json::to_value(config).ok()?.as_object().cloned()
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod config_api;
|
||||
pub(crate) mod runtime_api;
|
||||
@@ -0,0 +1,46 @@
|
||||
use crate::config;
|
||||
|
||||
pub(crate) fn init_config_store(root_dir: String) -> bool {
|
||||
config::repository::init_config_store(root_dir)
|
||||
}
|
||||
|
||||
pub(crate) fn list_configs() -> String {
|
||||
config::repository::list_config_meta_json()
|
||||
}
|
||||
|
||||
pub(crate) fn save_config(config_id: String, display_name: String, config_json: String) -> bool {
|
||||
config::repository::save_config_record(config_id, display_name, config_json).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn create_config(config_id: String, display_name: String) -> bool {
|
||||
config::repository::create_config_record(config_id, display_name).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn delete_stored_config_meta(config_id: String) -> bool {
|
||||
config::repository::delete_config_record(&config_id)
|
||||
}
|
||||
|
||||
pub(crate) fn get_config(config_id: String) -> Option<String> {
|
||||
config::repository::load_config_json(&config_id)
|
||||
}
|
||||
|
||||
pub(crate) fn get_default_config() -> Option<String> {
|
||||
config::repository::get_default_config_json()
|
||||
}
|
||||
|
||||
pub(crate) fn get_config_field(config_id: String, field: String) -> Option<String> {
|
||||
config::repository::get_config_field_value(&config_id, &field)
|
||||
}
|
||||
|
||||
pub(crate) fn set_config_field(config_id: String, field: String, json_value: String) -> bool {
|
||||
config::repository::set_config_field_value(&config_id, &field, &json_value)
|
||||
}
|
||||
|
||||
pub(crate) fn import_toml(toml_text: String, display_name: Option<String>) -> Option<String> {
|
||||
config::repository::import_toml_config(toml_text, display_name)
|
||||
.map(|record| record.meta.config_id)
|
||||
}
|
||||
|
||||
pub(crate) fn export_toml(config_id: String) -> Option<String> {
|
||||
config::repository::export_config_toml(&config_id).map(|ret| ret.toml_text)
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
use crate::config::repository::load_config_json;
|
||||
use crate::config::storage::config_meta::get_config_display_name;
|
||||
use crate::config::types::stored_config::KeyValuePair;
|
||||
use crate::kernel_bridge::{
|
||||
aggregate_requested_tun_routes, start_local_socket_server as start_local_socket_server_inner,
|
||||
stop_local_socket_server as stop_local_socket_server_inner,
|
||||
};
|
||||
use crate::runtime::state::runtime_state::{
|
||||
RuntimeAggregateState, TunAggregateState, clear_tun_attached, mark_tun_attached,
|
||||
runtime_instance_from_running_info,
|
||||
};
|
||||
use crate::{ASYNC_RUNTIME, EASYTIER_VERSION, INSTANCE_MANAGER, WEB_CLIENTS};
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use ohos_hilog_binding::{hilog_error, hilog_info};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) fn start_kernel(
|
||||
config_id: String,
|
||||
start_kernel_with_config_id: impl Fn(&str) -> bool,
|
||||
) -> bool {
|
||||
start_kernel_with_config_id(&config_id)
|
||||
}
|
||||
|
||||
pub(crate) fn stop_kernel(
|
||||
config_id: String,
|
||||
stop_web_client: impl Fn(&str) -> bool,
|
||||
parse_instance_uuid: impl Fn(&str) -> Option<uuid::Uuid>,
|
||||
maybe_stop_local_socket_server: impl Fn(),
|
||||
) -> bool {
|
||||
clear_tun_attached(&config_id);
|
||||
if stop_web_client(&config_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some(instance_id) = parse_instance_uuid(&config_id) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let ret = INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
.map(|_| true)
|
||||
.unwrap_or_else(|err| {
|
||||
hilog_error!("[Rust] stop_kernel failed {}: {}", config_id, err);
|
||||
false
|
||||
});
|
||||
maybe_stop_local_socket_server();
|
||||
ret
|
||||
}
|
||||
|
||||
pub(crate) fn stop_network_instance(
|
||||
config_ids: Vec<String>,
|
||||
stop_kernel: impl Fn(String) -> bool,
|
||||
) -> bool {
|
||||
let mut ok = true;
|
||||
for config_id in config_ids {
|
||||
ok = stop_kernel(config_id) && ok;
|
||||
}
|
||||
ok
|
||||
}
|
||||
|
||||
pub(crate) fn collect_network_infos() -> Vec<KeyValuePair> {
|
||||
let infos = match INSTANCE_MANAGER.collect_network_infos_sync() {
|
||||
Ok(infos) => infos,
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] collect network infos failed {}", err);
|
||||
return vec![];
|
||||
}
|
||||
};
|
||||
|
||||
infos
|
||||
.into_iter()
|
||||
.filter_map(|(key, value)| {
|
||||
serde_json::to_string(&value)
|
||||
.ok()
|
||||
.map(|value_json| KeyValuePair {
|
||||
key: key.to_string(),
|
||||
value: value_json,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn set_tun_fd(
|
||||
config_id: String,
|
||||
fd: i32,
|
||||
parse_instance_uuid: impl Fn(&str) -> Option<uuid::Uuid>,
|
||||
) -> bool {
|
||||
let Some(instance_id) = parse_instance_uuid(&config_id) else {
|
||||
hilog_error!("[Rust] set_tun_fd invalid instance id: {}", config_id);
|
||||
return false;
|
||||
};
|
||||
|
||||
INSTANCE_MANAGER
|
||||
.set_tun_fd(&instance_id, fd)
|
||||
.map(|_| {
|
||||
mark_tun_attached(&config_id);
|
||||
hilog_info!(
|
||||
"[Rust] set_tun_fd success instance={} fd={} marked_attached=true",
|
||||
config_id,
|
||||
fd
|
||||
);
|
||||
true
|
||||
})
|
||||
.unwrap_or_else(|err| {
|
||||
hilog_error!("[Rust] set_tun_fd failed {}: {}", config_id, err);
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn get_runtime_snapshot() -> RuntimeAggregateState {
|
||||
get_runtime_snapshot_inner()
|
||||
}
|
||||
|
||||
pub(crate) fn get_runtime_snapshot_inner() -> RuntimeAggregateState {
|
||||
let infos = match INSTANCE_MANAGER.collect_network_infos_sync() {
|
||||
Ok(infos) => infos,
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] collect network infos failed {}", err);
|
||||
return RuntimeAggregateState {
|
||||
instances: vec![],
|
||||
tun: TunAggregateState {
|
||||
active: false,
|
||||
attached_instance_ids: vec![],
|
||||
aggregated_routes: vec![],
|
||||
dns_servers: vec![],
|
||||
need_rebuild: false,
|
||||
},
|
||||
running_instance_count: 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let mut instances = Vec::with_capacity(infos.len());
|
||||
for (instance_uuid, info) in infos {
|
||||
let config_id = instance_uuid.to_string();
|
||||
let display_name = get_config_display_name(&config_id).unwrap_or_else(|| config_id.clone());
|
||||
let config_json = load_config_json(&config_id);
|
||||
let stored_config = config_json
|
||||
.as_deref()
|
||||
.and_then(|raw| serde_json::from_str::<NetworkConfig>(raw).ok());
|
||||
let magic_dns_enabled = stored_config
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.enable_magic_dns)
|
||||
.unwrap_or(false);
|
||||
let need_exit_node = stored_config
|
||||
.as_ref()
|
||||
.map(|cfg| !cfg.exit_nodes.is_empty())
|
||||
.unwrap_or(false);
|
||||
instances.push(runtime_instance_from_running_info(
|
||||
config_id,
|
||||
display_name,
|
||||
magic_dns_enabled,
|
||||
need_exit_node,
|
||||
info,
|
||||
));
|
||||
}
|
||||
|
||||
instances.sort_by(|a, b| {
|
||||
a.display_name
|
||||
.cmp(&b.display_name)
|
||||
.then_with(|| a.instance_id.cmp(&b.instance_id))
|
||||
});
|
||||
let attached_instance_ids = instances
|
||||
.iter()
|
||||
.filter(|instance| instance.tun_required)
|
||||
.map(|instance| instance.instance_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let aggregated_routes = aggregate_requested_tun_routes(&instances);
|
||||
let running_instance_count =
|
||||
instances.iter().filter(|instance| instance.running).count() as i32;
|
||||
let tun_active = !attached_instance_ids.is_empty();
|
||||
|
||||
RuntimeAggregateState {
|
||||
instances,
|
||||
tun: TunAggregateState {
|
||||
active: tun_active,
|
||||
attached_instance_ids,
|
||||
aggregated_routes,
|
||||
dns_servers: vec![],
|
||||
need_rebuild: false,
|
||||
},
|
||||
running_instance_count,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
mod protocol;
|
||||
mod routing;
|
||||
mod socket_server;
|
||||
|
||||
pub(crate) use routing::aggregate_requested_tun_routes;
|
||||
pub use socket_server::{start_local_socket_server, stop_local_socket_server};
|
||||
@@ -0,0 +1,50 @@
|
||||
use crate::config::types::stored_config::LocalSocketSyncMessage;
|
||||
use serde::Serialize;
|
||||
use std::io::{Error, ErrorKind, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct TunRequestPayload {
|
||||
pub config_id: String,
|
||||
pub instance_id: String,
|
||||
pub display_name: String,
|
||||
pub virtual_ipv4: Option<String>,
|
||||
pub virtual_ipv4_cidr: Option<String>,
|
||||
pub aggregated_routes: Vec<String>,
|
||||
pub magic_dns_enabled: bool,
|
||||
pub need_exit_node: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn send_local_socket_message(
|
||||
stream: &mut UnixStream,
|
||||
message_type: &str,
|
||||
payload_json: String,
|
||||
) -> std::io::Result<()> {
|
||||
let message = LocalSocketSyncMessage {
|
||||
message_type: message_type.to_string(),
|
||||
payload_json,
|
||||
};
|
||||
let mut raw = serde_json::to_vec(&message)
|
||||
.map_err(|err| Error::new(ErrorKind::InvalidData, err.to_string()))?;
|
||||
raw.push(b'\n');
|
||||
stream.write_all(&raw)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn broadcast_local_socket_message(
|
||||
clients: &mut Vec<UnixStream>,
|
||||
message_type: &str,
|
||||
payload_json: &str,
|
||||
) -> bool {
|
||||
let mut active_clients = Vec::with_capacity(clients.len());
|
||||
let mut delivered = false;
|
||||
for mut client in clients.drain(..) {
|
||||
if send_local_socket_message(&mut client, message_type, payload_json.to_string()).is_ok() {
|
||||
delivered = true;
|
||||
active_clients.push(client);
|
||||
}
|
||||
}
|
||||
*clients = active_clients;
|
||||
delivered
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
use crate::config::repository::load_config_json;
|
||||
use crate::runtime::state::runtime_state::RuntimeInstanceState;
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use ipnet::IpNet;
|
||||
use ohos_hilog_binding::hilog_debug;
|
||||
use std::collections::HashSet;
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub(crate) fn load_manual_routes(config_id: &str) -> Vec<String> {
|
||||
load_config_json(config_id)
|
||||
.and_then(|raw| serde_json::from_str::<NetworkConfig>(&raw).ok())
|
||||
.map(|config| config.routes)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn normalize_route_cidr(route: &str) -> Option<String> {
|
||||
route
|
||||
.parse::<IpNet>()
|
||||
.ok()
|
||||
.map(|network| match network {
|
||||
IpNet::V4(net) => net.trunc().to_string(),
|
||||
IpNet::V6(net) => net.trunc().to_string(),
|
||||
})
|
||||
.or_else(|| {
|
||||
route.parse::<IpAddr>().ok().map(|addr| match addr {
|
||||
IpAddr::V4(ip) => format!("{}/32", ip),
|
||||
IpAddr::V6(ip) => format!("{}/128", ip),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn simplify_routes(routes: Vec<String>) -> Vec<String> {
|
||||
let mut parsed = routes
|
||||
.into_iter()
|
||||
.filter_map(|route| normalize_route_cidr(&route))
|
||||
.filter_map(|route| route.parse::<IpNet>().ok())
|
||||
.collect::<Vec<_>>();
|
||||
parsed.sort_by(|left, right| {
|
||||
left.prefix_len()
|
||||
.cmp(&right.prefix_len())
|
||||
.then_with(|| left.network().to_string().cmp(&right.network().to_string()))
|
||||
});
|
||||
|
||||
let mut simplified = Vec::<IpNet>::new();
|
||||
'outer: for route in parsed {
|
||||
for existing in &simplified {
|
||||
if existing.contains(&route.network()) && existing.prefix_len() <= route.prefix_len() {
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
simplified.retain(|existing| {
|
||||
!(route.contains(&existing.network()) && route.prefix_len() <= existing.prefix_len())
|
||||
});
|
||||
simplified.push(route);
|
||||
}
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
simplified
|
||||
.into_iter()
|
||||
.map(|route| route.to_string())
|
||||
.filter(|route| seen.insert(route.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn aggregate_tun_routes(instance: &RuntimeInstanceState) -> Vec<String> {
|
||||
let virtual_ipv4_cidr = instance
|
||||
.my_node_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.virtual_ipv4_cidr.clone());
|
||||
let manual_routes = load_manual_routes(&instance.config_id);
|
||||
let proxy_cidrs = instance
|
||||
.routes
|
||||
.iter()
|
||||
.flat_map(|route| route.proxy_cidrs.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
let mut raw_routes = Vec::new();
|
||||
|
||||
if let Some(cidr) = virtual_ipv4_cidr.clone() {
|
||||
raw_routes.push(cidr);
|
||||
}
|
||||
|
||||
raw_routes.extend(manual_routes.iter().cloned());
|
||||
raw_routes.extend(proxy_cidrs.iter().cloned());
|
||||
let aggregated_routes = simplify_routes(raw_routes);
|
||||
hilog_debug!(
|
||||
"[Rust] aggregate_tun_routes instance={} proxy_cidrs={:?} aggregated_routes={:?}",
|
||||
instance.instance_id,
|
||||
proxy_cidrs,
|
||||
aggregated_routes
|
||||
);
|
||||
aggregated_routes
|
||||
}
|
||||
|
||||
pub(crate) fn aggregate_requested_tun_routes(instances: &[RuntimeInstanceState]) -> Vec<String> {
|
||||
let mut aggregated_routes = Vec::new();
|
||||
let mut seen_routes = HashSet::new();
|
||||
for instance in instances.iter().filter(|instance| instance.tun_required) {
|
||||
for route in aggregate_tun_routes(instance) {
|
||||
if seen_routes.insert(route.clone()) {
|
||||
aggregated_routes.push(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
aggregated_routes
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
use super::protocol::{TunRequestPayload, broadcast_local_socket_message};
|
||||
use crate::config::repository::kernel_socket_path;
|
||||
use crate::get_runtime_snapshot_inner;
|
||||
use crate::kernel_bridge::routing::aggregate_tun_routes;
|
||||
use ohos_hilog_binding::{hilog_error, hilog_info};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::ErrorKind;
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::Duration;
|
||||
|
||||
struct LocalSocketState {
|
||||
stop_flag: std::sync::Arc<AtomicBool>,
|
||||
socket_path: PathBuf,
|
||||
worker: JoinHandle<()>,
|
||||
}
|
||||
|
||||
static LOCAL_SOCKET_STATE: Lazy<Mutex<Option<LocalSocketState>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
pub fn start_local_socket_server() -> bool {
|
||||
let socket_path = match kernel_socket_path() {
|
||||
Some(path) => path,
|
||||
None => {
|
||||
hilog_error!("[Rust] kernel socket path unavailable");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
match LOCAL_SOCKET_STATE.lock() {
|
||||
Ok(guard) if guard.is_some() => return true,
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] lock localsocket state failed: {}", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if socket_path.exists() {
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
}
|
||||
|
||||
let listener = match UnixListener::bind(&socket_path) {
|
||||
Ok(listener) => listener,
|
||||
Err(err) => {
|
||||
hilog_error!(
|
||||
"[Rust] bind localsocket failed {}: {}",
|
||||
socket_path.display(),
|
||||
err
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if let Err(err) = listener.set_nonblocking(true) {
|
||||
hilog_error!("[Rust] set localsocket nonblocking failed: {}", err);
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
let stop_flag = std::sync::Arc::new(AtomicBool::new(false));
|
||||
let worker_stop_flag = stop_flag.clone();
|
||||
let worker = thread::spawn(move || {
|
||||
let mut last_snapshot_json = String::new();
|
||||
let mut delivered_tun_requests = HashSet::new();
|
||||
let mut last_tun_route_signatures = HashMap::<String, String>::new();
|
||||
let mut clients = Vec::<UnixStream>::new();
|
||||
|
||||
while !worker_stop_flag.load(Ordering::Relaxed) {
|
||||
let mut accepted_client = false;
|
||||
loop {
|
||||
match listener.accept() {
|
||||
Ok((stream, _addr)) => {
|
||||
accepted_client = true;
|
||||
clients.push(stream);
|
||||
}
|
||||
Err(err) if err.kind() == ErrorKind::WouldBlock => break,
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] accept localsocket failed: {}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let snapshot = get_runtime_snapshot_inner();
|
||||
let snapshot_json = match serde_json::to_string(&snapshot) {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] serialize runtime snapshot failed: {}", err);
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if accepted_client || snapshot_json != last_snapshot_json {
|
||||
let _ = broadcast_local_socket_message(
|
||||
&mut clients,
|
||||
"runtime_snapshot",
|
||||
&snapshot_json,
|
||||
);
|
||||
last_snapshot_json = snapshot_json;
|
||||
}
|
||||
|
||||
for instance in snapshot.instances.iter() {
|
||||
if instance.running && instance.tun_required {
|
||||
let virtual_ipv4 = instance
|
||||
.my_node_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.virtual_ipv4.clone());
|
||||
let virtual_ipv4_cidr = instance
|
||||
.my_node_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.virtual_ipv4_cidr.clone());
|
||||
if clients.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if virtual_ipv4.is_none() || virtual_ipv4_cidr.is_none() {
|
||||
continue;
|
||||
}
|
||||
let aggregated_routes = aggregate_tun_routes(instance);
|
||||
let route_signature = serde_json::to_string(&aggregated_routes)
|
||||
.unwrap_or_else(|_| "[]".to_string());
|
||||
let should_send = !delivered_tun_requests.contains(&instance.instance_id)
|
||||
|| last_tun_route_signatures
|
||||
.get(&instance.instance_id)
|
||||
.map(|value| value != &route_signature)
|
||||
.unwrap_or(true);
|
||||
if !should_send {
|
||||
continue;
|
||||
}
|
||||
let payload = TunRequestPayload {
|
||||
config_id: instance.config_id.clone(),
|
||||
instance_id: instance.instance_id.clone(),
|
||||
display_name: instance.display_name.clone(),
|
||||
virtual_ipv4,
|
||||
virtual_ipv4_cidr,
|
||||
aggregated_routes,
|
||||
magic_dns_enabled: instance.magic_dns_enabled,
|
||||
need_exit_node: instance.need_exit_node,
|
||||
};
|
||||
let payload_json = match serde_json::to_string(&payload) {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] serialize tun request failed: {}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if broadcast_local_socket_message(&mut clients, "tun_request", &payload_json) {
|
||||
delivered_tun_requests.insert(instance.instance_id.clone());
|
||||
last_tun_route_signatures
|
||||
.insert(instance.instance_id.clone(), route_signature);
|
||||
}
|
||||
} else {
|
||||
delivered_tun_requests.remove(&instance.instance_id);
|
||||
last_tun_route_signatures.remove(&instance.instance_id);
|
||||
}
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
});
|
||||
|
||||
match LOCAL_SOCKET_STATE.lock() {
|
||||
Ok(mut guard) => {
|
||||
*guard = Some(LocalSocketState {
|
||||
stop_flag,
|
||||
socket_path,
|
||||
worker,
|
||||
});
|
||||
true
|
||||
}
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] lock localsocket state failed: {}", err);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_local_socket_server() -> bool {
|
||||
let state = match LOCAL_SOCKET_STATE.lock() {
|
||||
Ok(mut guard) => guard.take(),
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] lock localsocket state failed: {}", err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(state) = state {
|
||||
state.stop_flag.store(true, Ordering::Relaxed);
|
||||
let _ = state.worker.join();
|
||||
let _ = std::fs::remove_file(state.socket_path);
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -1,185 +1,485 @@
|
||||
mod native_log;
|
||||
mod config;
|
||||
mod exports;
|
||||
mod kernel_bridge;
|
||||
mod platform;
|
||||
mod runtime;
|
||||
|
||||
use easytier::common::config::{ConfigFileControl, ConfigLoader, TomlConfigLoader};
|
||||
use config::repository::{
|
||||
create_config_record, delete_config_record, export_config_toml, get_config_field_value,
|
||||
get_default_config_json, import_toml_config, init_config_store as init_repo_store,
|
||||
list_config_meta_json, save_config_record, set_config_field_value, start_kernel_with_config_id,
|
||||
};
|
||||
use config::services::schema_service::{
|
||||
ConfigFieldMapping, NetworkConfigSchema,
|
||||
get_network_config_field_mappings as build_network_config_field_mappings,
|
||||
get_network_config_schema as build_network_config_schema,
|
||||
};
|
||||
use config::services::share_link_service::{
|
||||
build_config_share_link as build_config_share_link_inner,
|
||||
import_config_share_link as import_config_share_link_inner,
|
||||
parse_config_share_link as parse_config_share_link_inner,
|
||||
};
|
||||
use config::storage::config_meta::get_config_display_name;
|
||||
use config::types::stored_config::{KeyValuePair, SharedConfigLinkPayload};
|
||||
use easytier::common::constants::EASYTIER_VERSION;
|
||||
use easytier::common::{
|
||||
MachineIdOptions,
|
||||
config::{ConfigFileControl, ConfigLoader, TomlConfigLoader},
|
||||
};
|
||||
use easytier::instance_manager::NetworkInstanceManager;
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use easytier::proto::api::manage::NetworkingMethod;
|
||||
use easytier::web_client::{WebClient, WebClientHooks, run_web_client};
|
||||
use kernel_bridge::{
|
||||
aggregate_requested_tun_routes, start_local_socket_server as start_local_socket_server_inner,
|
||||
stop_local_socket_server as stop_local_socket_server_inner,
|
||||
};
|
||||
use napi_derive_ohos::napi;
|
||||
use ohos_hilog_binding::{hilog_debug, hilog_error};
|
||||
use ohos_hilog_binding::{hilog_error, hilog_info};
|
||||
use runtime::state::runtime_state::{
|
||||
RuntimeAggregateState, TunAggregateState, clear_tun_attached, mark_tun_attached,
|
||||
runtime_instance_from_running_info,
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::format;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::runtime::{Builder, Runtime};
|
||||
use uuid::Uuid;
|
||||
|
||||
static INSTANCE_MANAGER: once_cell::sync::Lazy<NetworkInstanceManager> =
|
||||
once_cell::sync::Lazy::new(NetworkInstanceManager::new);
|
||||
pub(crate) static INSTANCE_MANAGER: once_cell::sync::Lazy<Arc<NetworkInstanceManager>> =
|
||||
once_cell::sync::Lazy::new(|| Arc::new(NetworkInstanceManager::new()));
|
||||
static ASYNC_RUNTIME: once_cell::sync::Lazy<Runtime> = once_cell::sync::Lazy::new(|| {
|
||||
Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("tokio runtime for easytier-ohrs")
|
||||
});
|
||||
static WEB_CLIENTS: once_cell::sync::Lazy<Mutex<HashMap<String, ManagedWebClient>>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
#[napi(object)]
|
||||
pub struct KeyValuePair {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
#[derive(Default)]
|
||||
struct TrackedWebClientHooks {
|
||||
instance_ids: Mutex<HashSet<Uuid>>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn easytier_version() -> String {
|
||||
EASYTIER_VERSION.to_string()
|
||||
struct ManagedWebClient {
|
||||
_client: WebClient,
|
||||
hooks: Arc<TrackedWebClientHooks>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn set_tun_fd(inst_id: String, fd: i32) -> bool {
|
||||
match Uuid::try_parse(&inst_id) {
|
||||
Ok(uuid) => match INSTANCE_MANAGER.set_tun_fd(&uuid, fd) {
|
||||
Ok(_) => {
|
||||
hilog_debug!("[Rust] set tun fd {} to {}.", fd, inst_id);
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] cant set tun fd {} to {}. {}", fd, inst_id, e);
|
||||
false
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] cant covert {} to uuid. {}", inst_id, e);
|
||||
#[async_trait::async_trait]
|
||||
impl WebClientHooks for TrackedWebClientHooks {
|
||||
async fn post_run_network_instance(&self, id: &Uuid) -> Result<(), String> {
|
||||
self.instance_ids
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?
|
||||
.insert(*id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn post_remove_network_instances(&self, ids: &[Uuid]) -> Result<(), String> {
|
||||
let mut guard = self.instance_ids.lock().map_err(|err| err.to_string())?;
|
||||
for id in ids {
|
||||
guard.remove(id);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn is_config_server_config(config: &NetworkConfig) -> bool {
|
||||
matches!(
|
||||
NetworkingMethod::try_from(config.networking_method.unwrap_or_default())
|
||||
.unwrap_or_default(),
|
||||
NetworkingMethod::PublicServer
|
||||
) && config
|
||||
.public_server_url
|
||||
.as_ref()
|
||||
.is_some_and(|url| !url.trim().is_empty())
|
||||
}
|
||||
|
||||
fn stop_web_client(config_id: &str) -> bool {
|
||||
let managed = match WEB_CLIENTS.lock() {
|
||||
Ok(mut guard) => guard.remove(config_id),
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] stop_web_client lock failed {}", err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(managed) = managed else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let tracked_ids = managed
|
||||
.hooks
|
||||
.instance_ids
|
||||
.lock()
|
||||
.map(|guard| guard.iter().copied().collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
drop(managed);
|
||||
|
||||
if tracked_ids.is_empty() {
|
||||
maybe_stop_local_socket_server();
|
||||
return true;
|
||||
}
|
||||
|
||||
let ret = INSTANCE_MANAGER
|
||||
.delete_network_instance(tracked_ids)
|
||||
.map(|_| true)
|
||||
.unwrap_or_else(|err| {
|
||||
hilog_error!(
|
||||
"[Rust] stop config server instances failed {}: {}",
|
||||
config_id,
|
||||
err
|
||||
);
|
||||
false
|
||||
});
|
||||
maybe_stop_local_socket_server();
|
||||
ret
|
||||
}
|
||||
|
||||
fn ensure_local_socket_server_started() -> bool {
|
||||
start_local_socket_server_inner()
|
||||
}
|
||||
|
||||
fn maybe_stop_local_socket_server() {
|
||||
let no_local_instances = INSTANCE_MANAGER.list_network_instance_ids().is_empty();
|
||||
let no_web_clients = WEB_CLIENTS
|
||||
.lock()
|
||||
.map(|guard| guard.is_empty())
|
||||
.unwrap_or(false);
|
||||
if no_local_instances && no_web_clients {
|
||||
let _ = stop_local_socket_server_inner();
|
||||
}
|
||||
}
|
||||
|
||||
fn run_config_server_instance(config_id: &str, config: &NetworkConfig) -> bool {
|
||||
if INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.iter()
|
||||
.next()
|
||||
.is_some()
|
||||
{
|
||||
hilog_error!("[Rust] there is a running instance!");
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(config_server_url) = config.public_server_url.clone() else {
|
||||
hilog_error!("[Rust] public_server_url missing for config server mode");
|
||||
return false;
|
||||
};
|
||||
let hooks = Arc::new(TrackedWebClientHooks::default());
|
||||
let secure_mode = config
|
||||
.secure_mode
|
||||
.as_ref()
|
||||
.map(|mode| mode.enabled)
|
||||
.unwrap_or(false);
|
||||
let hostname = config.hostname.clone();
|
||||
|
||||
if !ensure_local_socket_server_started() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let client = ASYNC_RUNTIME.block_on(run_web_client(
|
||||
&config_server_url,
|
||||
MachineIdOptions::default(),
|
||||
hostname,
|
||||
secure_mode,
|
||||
INSTANCE_MANAGER.clone(),
|
||||
Some(hooks.clone()),
|
||||
));
|
||||
|
||||
let client = match client {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] start config server failed {}", err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
match WEB_CLIENTS.lock() {
|
||||
Ok(mut guard) => {
|
||||
guard.insert(
|
||||
config_id.to_string(),
|
||||
ManagedWebClient {
|
||||
_client: client,
|
||||
hooks,
|
||||
},
|
||||
);
|
||||
true
|
||||
}
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] store config server client failed {}", err);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn default_network_config() -> String {
|
||||
match NetworkConfig::new_from_config(TomlConfigLoader::default()) {
|
||||
Ok(result) => serde_json::to_string(&result).unwrap_or_else(|e| format!("ERROR {}", e)),
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] default_network_config failed {}", e);
|
||||
format!("ERROR {}", e)
|
||||
}
|
||||
}
|
||||
pub(crate) fn build_default_network_config_json() -> Result<String, String> {
|
||||
let config = NetworkConfig::new_from_config(TomlConfigLoader::default())
|
||||
.map_err(|e| format!("default_network_config failed {}", e))?;
|
||||
serde_json::to_string(&config).map_err(|e| format!("default_network_config failed {}", e))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn convert_toml_to_network_config(cfg_str: String) -> String {
|
||||
match TomlConfigLoader::new_from_str(&cfg_str) {
|
||||
Ok(cfg) => match NetworkConfig::new_from_config(cfg) {
|
||||
Ok(result) => serde_json::to_string(&result).unwrap_or_else(|e| format!("ERROR {}", e)),
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] convert_toml_to_network_config failed {}", e);
|
||||
format!("ERROR {}", e)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] convert_toml_to_network_config failed {}", e);
|
||||
format!("ERROR {}", e)
|
||||
}
|
||||
}
|
||||
fn convert_toml_to_network_config_inner(toml_text: &str) -> Result<String, String> {
|
||||
let config = NetworkConfig::new_from_config(
|
||||
TomlConfigLoader::new_from_str(toml_text).map_err(|e| e.to_string())?,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_string(&config).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn parse_network_config(cfg_json: String) -> bool {
|
||||
match serde_json::from_str::<NetworkConfig>(&cfg_json) {
|
||||
Ok(cfg) => match cfg.gen_config() {
|
||||
Ok(toml) => {
|
||||
hilog_debug!("[Rust] Convert to Toml {}", toml.dump());
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] parse config failed {}", e);
|
||||
false
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] parse config failed {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
fn parse_network_config_inner(cfg_json: &str) -> bool {
|
||||
serde_json::from_str::<NetworkConfig>(cfg_json)
|
||||
.ok()
|
||||
.and_then(|cfg| cfg.gen_config().ok())
|
||||
.is_some()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn run_network_instance(cfg_json: String) -> bool {
|
||||
let cfg = match serde_json::from_str::<NetworkConfig>(&cfg_json) {
|
||||
Ok(cfg) => match cfg.gen_config() {
|
||||
Ok(toml) => toml,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] parse config failed {}", e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
pub(crate) fn run_network_instance_from_json(cfg_json: &str) -> bool {
|
||||
let config = match serde_json::from_str::<NetworkConfig>(cfg_json) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] parse config failed {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if INSTANCE_MANAGER.list_network_instance_ids().len() > 0 {
|
||||
if is_config_server_config(&config) {
|
||||
let Some(config_id) = config.instance_id.as_deref() else {
|
||||
hilog_error!("[Rust] config server config missing instance id");
|
||||
return false;
|
||||
};
|
||||
return run_config_server_instance(config_id, &config);
|
||||
}
|
||||
|
||||
let cfg = match config.gen_config() {
|
||||
Ok(toml) => toml,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] parse config failed {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if !INSTANCE_MANAGER.list_network_instance_ids().is_empty() {
|
||||
hilog_error!("[Rust] there is a running instance!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if !ensure_local_socket_server_started() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let inst_id = cfg.get_id();
|
||||
if INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.contains(&inst_id)
|
||||
{
|
||||
hilog_error!("[Rust] instance {} already exists", inst_id);
|
||||
return false;
|
||||
}
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
true
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn stop_network_instance(inst_names: Vec<String>) {
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(
|
||||
inst_names
|
||||
.into_iter()
|
||||
.filter_map(|s| Uuid::parse_str(&s).ok())
|
||||
.collect(),
|
||||
)
|
||||
.unwrap();
|
||||
hilog_debug!("[Rust] stop_network_instance");
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn collect_network_infos() -> Vec<KeyValuePair> {
|
||||
let mut result = Vec::new();
|
||||
match INSTANCE_MANAGER.collect_network_infos_sync() {
|
||||
Ok(map) => {
|
||||
for (uuid, info) in map.iter() {
|
||||
// convert value to json string
|
||||
let value = match serde_json::to_string(&info) {
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] failed to serialize instance {} info: {}", uuid, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
result.push(KeyValuePair {
|
||||
key: uuid.clone().to_string(),
|
||||
value: value.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn collect_running_network() -> Vec<String> {
|
||||
INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn is_running_network(inst_id: String) -> bool {
|
||||
match Uuid::try_parse(&inst_id) {
|
||||
Ok(uuid) => INSTANCE_MANAGER.list_network_instance_ids().contains(&uuid),
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] cant covert {} to uuid. {}", inst_id, e);
|
||||
match INSTANCE_MANAGER.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) {
|
||||
Ok(_) => true,
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] start_kernel failed for {}: {}", inst_id, err);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_instance_uuid(config_id: &str) -> Option<Uuid> {
|
||||
match Uuid::parse_str(config_id) {
|
||||
Ok(uuid) => Some(uuid),
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] invalid config_id {}: {}", config_id, err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn init_config_store(root_dir: String) -> bool {
|
||||
exports::config_api::init_config_store(root_dir)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn list_configs() -> String {
|
||||
exports::config_api::list_configs()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_config_display_name_by_id(config_id: String) -> Option<String> {
|
||||
get_config_display_name(&config_id)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn save_config(config_id: String, display_name: String, config_json: String) -> bool {
|
||||
exports::config_api::save_config(config_id, display_name, config_json)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn create_config(config_id: String, display_name: String) -> bool {
|
||||
exports::config_api::create_config(config_id, display_name)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn rename_stored_config(config_id: String, display_name: String) -> bool {
|
||||
config::storage::config_meta::set_config_display_name(config_id, display_name).is_some()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn delete_stored_config_meta(config_id: String) -> bool {
|
||||
exports::config_api::delete_stored_config_meta(config_id)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_config(config_id: String) -> Option<String> {
|
||||
exports::config_api::get_config(config_id)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_default_config() -> Option<String> {
|
||||
exports::config_api::get_default_config()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_config_field(config_id: String, field: String) -> Option<String> {
|
||||
exports::config_api::get_config_field(config_id, field)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn set_config_field(config_id: String, field: String, json_value: String) -> bool {
|
||||
exports::config_api::set_config_field(config_id, field, json_value)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn import_toml(toml_text: String, display_name: Option<String>) -> Option<String> {
|
||||
exports::config_api::import_toml(toml_text, display_name)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn export_toml(config_id: String) -> Option<String> {
|
||||
exports::config_api::export_toml(config_id)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn start_kernel(config_id: String) -> bool {
|
||||
exports::runtime_api::start_kernel(config_id, start_kernel_with_config_id)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn stop_kernel(config_id: String) -> bool {
|
||||
exports::runtime_api::stop_kernel(
|
||||
config_id,
|
||||
stop_web_client,
|
||||
parse_instance_uuid,
|
||||
maybe_stop_local_socket_server,
|
||||
)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn stop_network_instance(config_ids: Vec<String>) -> bool {
|
||||
exports::runtime_api::stop_network_instance(config_ids, stop_kernel)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn easytier_version() -> String {
|
||||
EASYTIER_VERSION.to_string()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn default_network_config() -> String {
|
||||
get_default_config().unwrap_or_else(|| "{}".to_string())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn convert_toml_to_network_config(toml_text: String) -> String {
|
||||
convert_toml_to_network_config_inner(&toml_text).unwrap_or_else(|err| format!("ERROR: {err}"))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn parse_network_config(cfg_json: String) -> bool {
|
||||
parse_network_config_inner(&cfg_json)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn run_network_instance(cfg_json: String) -> bool {
|
||||
run_network_instance_from_json(&cfg_json)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn collect_network_infos() -> Vec<KeyValuePair> {
|
||||
exports::runtime_api::collect_network_infos()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn set_tun_fd(config_id: String, fd: i32) -> bool {
|
||||
exports::runtime_api::set_tun_fd(config_id, fd, parse_instance_uuid)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_network_config_schema() -> NetworkConfigSchema {
|
||||
build_network_config_schema()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_network_config_field_mappings() -> Vec<ConfigFieldMapping> {
|
||||
build_network_config_field_mappings()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn exported_plain_object_schema_contains_core_networkconfig_metadata() {
|
||||
let schema = get_network_config_schema();
|
||||
assert_eq!(schema.name, "NetworkConfig");
|
||||
assert_eq!(schema.node_kind, "schema");
|
||||
assert!(
|
||||
schema
|
||||
.children
|
||||
.iter()
|
||||
.any(|field| field.name == "network_name")
|
||||
);
|
||||
let secure_mode = schema
|
||||
.children
|
||||
.iter()
|
||||
.find(|field| field.name == "secure_mode")
|
||||
.expect("secure_mode field");
|
||||
assert!(
|
||||
secure_mode
|
||||
.children
|
||||
.iter()
|
||||
.any(|field| field.name == "enabled")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_runtime_snapshot() -> RuntimeAggregateState {
|
||||
exports::runtime_api::get_runtime_snapshot()
|
||||
}
|
||||
|
||||
pub(crate) fn get_runtime_snapshot_inner() -> RuntimeAggregateState {
|
||||
exports::runtime_api::get_runtime_snapshot_inner()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn build_config_share_link(config_id: String, only_start: Option<bool>) -> Option<String> {
|
||||
build_config_share_link_inner(&config_id, None, only_start.unwrap_or(false))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn parse_config_share_link(share_link: String) -> Option<SharedConfigLinkPayload> {
|
||||
parse_config_share_link_inner(&share_link)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn import_config_share_link(
|
||||
share_link: String,
|
||||
display_name_override: Option<String>,
|
||||
) -> Option<String> {
|
||||
import_config_share_link_inner(&share_link, display_name_override)
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub(crate) mod logging;
|
||||
@@ -0,0 +1 @@
|
||||
pub(crate) mod native_log;
|
||||
@@ -0,0 +1 @@
|
||||
pub(crate) mod state;
|
||||
@@ -0,0 +1 @@
|
||||
pub(crate) mod runtime_state;
|
||||
@@ -0,0 +1,293 @@
|
||||
use easytier::proto::{api, common};
|
||||
use napi_derive_ohos::napi;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static ATTACHED_TUN_INSTANCE_IDS: once_cell::sync::Lazy<Mutex<HashSet<String>>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(HashSet::new()));
|
||||
|
||||
pub fn mark_tun_attached(instance_id: &str) {
|
||||
if let Ok(mut guard) = ATTACHED_TUN_INSTANCE_IDS.lock() {
|
||||
guard.insert(instance_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_tun_attached(instance_id: &str) {
|
||||
if let Ok(mut guard) = ATTACHED_TUN_INSTANCE_IDS.lock() {
|
||||
guard.remove(instance_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_tun_attached(instance_id: &str) -> bool {
|
||||
ATTACHED_TUN_INSTANCE_IDS
|
||||
.lock()
|
||||
.map(|guard| guard.contains(instance_id))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct PeerConnStats {
|
||||
pub rx_bytes: i64,
|
||||
pub tx_bytes: i64,
|
||||
pub rx_packets: i64,
|
||||
pub tx_packets: i64,
|
||||
pub latency_us: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct PeerConnInfo {
|
||||
pub conn_id: String,
|
||||
pub my_peer_id: i64,
|
||||
pub peer_id: i64,
|
||||
pub features: Vec<String>,
|
||||
pub tunnel_type: Option<String>,
|
||||
pub local_addr: Option<String>,
|
||||
pub remote_addr: Option<String>,
|
||||
pub resolved_remote_addr: Option<String>,
|
||||
pub stats: Option<PeerConnStats>,
|
||||
pub loss_rate: Option<f64>,
|
||||
pub is_client: bool,
|
||||
pub network_name: Option<String>,
|
||||
pub is_closed: bool,
|
||||
pub secure_auth_level: Option<i32>,
|
||||
pub peer_identity_type: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct PeerInfo {
|
||||
pub peer_id: i64,
|
||||
pub default_conn_id: Option<String>,
|
||||
pub directly_connected_conns: Vec<String>,
|
||||
pub conns: Vec<PeerConnInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct RouteView {
|
||||
pub peer_id: i64,
|
||||
pub hostname: Option<String>,
|
||||
pub ipv4: Option<String>,
|
||||
pub ipv4_cidr: Option<String>,
|
||||
pub ipv6_cidr: Option<String>,
|
||||
pub proxy_cidrs: Vec<String>,
|
||||
pub next_hop_peer_id: Option<i64>,
|
||||
pub cost: Option<i32>,
|
||||
pub path_latency: Option<i64>,
|
||||
pub udp_nat_type: Option<i32>,
|
||||
pub tcp_nat_type: Option<i32>,
|
||||
pub inst_id: Option<String>,
|
||||
pub version: Option<String>,
|
||||
pub is_public_server: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct MyNodeInfo {
|
||||
pub virtual_ipv4: Option<String>,
|
||||
pub virtual_ipv4_cidr: Option<String>,
|
||||
pub hostname: Option<String>,
|
||||
pub version: Option<String>,
|
||||
pub peer_id: Option<i64>,
|
||||
pub listeners: Vec<String>,
|
||||
pub vpn_portal_cfg: Option<String>,
|
||||
pub udp_nat_type: Option<i32>,
|
||||
pub tcp_nat_type: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct RuntimeInstanceState {
|
||||
pub config_id: String,
|
||||
pub instance_id: String,
|
||||
pub display_name: String,
|
||||
pub running: bool,
|
||||
pub tun_required: bool,
|
||||
pub tun_attached: bool,
|
||||
pub magic_dns_enabled: bool,
|
||||
pub need_exit_node: bool,
|
||||
pub error_message: Option<String>,
|
||||
pub my_node_info: Option<MyNodeInfo>,
|
||||
pub events: Vec<String>,
|
||||
pub routes: Vec<RouteView>,
|
||||
pub peers: Vec<PeerInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct TunAggregateState {
|
||||
pub active: bool,
|
||||
pub attached_instance_ids: Vec<String>,
|
||||
pub aggregated_routes: Vec<String>,
|
||||
pub dns_servers: Vec<String>,
|
||||
pub need_rebuild: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct RuntimeAggregateState {
|
||||
pub instances: Vec<RuntimeInstanceState>,
|
||||
pub tun: TunAggregateState,
|
||||
pub running_instance_count: i32,
|
||||
}
|
||||
|
||||
fn stringify_ipv4_inet(value: Option<common::Ipv4Inet>) -> Option<String> {
|
||||
value.map(|v| v.to_string())
|
||||
}
|
||||
|
||||
fn stringify_ipv6_inet(value: Option<common::Ipv6Inet>) -> Option<String> {
|
||||
value.map(|v| v.to_string())
|
||||
}
|
||||
|
||||
fn stringify_url(value: Option<common::Url>) -> Option<String> {
|
||||
value.map(|v| v.to_string())
|
||||
}
|
||||
|
||||
fn stringify_uuid(value: Option<common::Uuid>) -> Option<String> {
|
||||
value.map(|v| v.to_string())
|
||||
}
|
||||
|
||||
fn optional_u32_to_i64(value: Option<u32>) -> Option<i64> {
|
||||
value.map(|v| v as i64)
|
||||
}
|
||||
|
||||
fn optional_i32_to_i64(value: Option<i32>) -> Option<i64> {
|
||||
value.map(|v| v as i64)
|
||||
}
|
||||
|
||||
fn route_to_view(route: api::instance::Route) -> RouteView {
|
||||
let stun = route.stun_info;
|
||||
let feature_flag = route.feature_flag;
|
||||
RouteView {
|
||||
peer_id: route.peer_id as i64,
|
||||
hostname: (!route.hostname.is_empty()).then_some(route.hostname),
|
||||
ipv4: route
|
||||
.ipv4_addr
|
||||
.as_ref()
|
||||
.and_then(|inet| inet.address.as_ref())
|
||||
.map(|addr| addr.to_string()),
|
||||
ipv4_cidr: stringify_ipv4_inet(route.ipv4_addr),
|
||||
ipv6_cidr: stringify_ipv6_inet(route.ipv6_addr),
|
||||
proxy_cidrs: route.proxy_cidrs,
|
||||
next_hop_peer_id: optional_u32_to_i64(route.next_hop_peer_id_latency_first)
|
||||
.or_else(|| Some(route.next_hop_peer_id as i64)),
|
||||
cost: Some(route.cost),
|
||||
path_latency: optional_i32_to_i64(route.path_latency_latency_first)
|
||||
.or_else(|| Some(route.path_latency as i64)),
|
||||
udp_nat_type: stun.as_ref().map(|info| info.udp_nat_type),
|
||||
tcp_nat_type: stun.as_ref().map(|info| info.tcp_nat_type),
|
||||
inst_id: (!route.inst_id.is_empty()).then_some(route.inst_id),
|
||||
version: (!route.version.is_empty()).then_some(route.version),
|
||||
is_public_server: feature_flag.map(|flag| flag.is_public_server),
|
||||
}
|
||||
}
|
||||
|
||||
fn peer_conn_to_view(conn: api::instance::PeerConnInfo) -> PeerConnInfo {
|
||||
let stats = conn.stats.map(|stats| PeerConnStats {
|
||||
rx_bytes: stats.rx_bytes as i64,
|
||||
tx_bytes: stats.tx_bytes as i64,
|
||||
rx_packets: stats.rx_packets as i64,
|
||||
tx_packets: stats.tx_packets as i64,
|
||||
latency_us: stats.latency_us as i64,
|
||||
});
|
||||
|
||||
PeerConnInfo {
|
||||
conn_id: conn.conn_id,
|
||||
my_peer_id: conn.my_peer_id as i64,
|
||||
peer_id: conn.peer_id as i64,
|
||||
features: conn.features,
|
||||
tunnel_type: conn.tunnel.as_ref().map(|t| t.tunnel_type.clone()),
|
||||
local_addr: conn
|
||||
.tunnel
|
||||
.as_ref()
|
||||
.and_then(|t| stringify_url(t.local_addr.clone())),
|
||||
remote_addr: conn
|
||||
.tunnel
|
||||
.as_ref()
|
||||
.and_then(|t| stringify_url(t.remote_addr.clone())),
|
||||
resolved_remote_addr: conn
|
||||
.tunnel
|
||||
.as_ref()
|
||||
.and_then(|t| stringify_url(t.resolved_remote_addr.clone())),
|
||||
stats,
|
||||
loss_rate: Some(conn.loss_rate as f64),
|
||||
is_client: conn.is_client,
|
||||
network_name: (!conn.network_name.is_empty()).then_some(conn.network_name),
|
||||
is_closed: conn.is_closed,
|
||||
secure_auth_level: Some(conn.secure_auth_level),
|
||||
peer_identity_type: Some(conn.peer_identity_type),
|
||||
}
|
||||
}
|
||||
|
||||
fn peer_to_view(peer: api::instance::PeerInfo) -> PeerInfo {
|
||||
PeerInfo {
|
||||
peer_id: peer.peer_id as i64,
|
||||
default_conn_id: stringify_uuid(peer.default_conn_id),
|
||||
directly_connected_conns: peer
|
||||
.directly_connected_conns
|
||||
.into_iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect(),
|
||||
conns: peer.conns.into_iter().map(peer_conn_to_view).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn my_node_info_to_view(info: api::manage::MyNodeInfo) -> MyNodeInfo {
|
||||
MyNodeInfo {
|
||||
virtual_ipv4: info
|
||||
.virtual_ipv4
|
||||
.as_ref()
|
||||
.and_then(|inet| inet.address.as_ref())
|
||||
.map(|addr| addr.to_string()),
|
||||
virtual_ipv4_cidr: stringify_ipv4_inet(info.virtual_ipv4),
|
||||
hostname: (!info.hostname.is_empty()).then_some(info.hostname),
|
||||
version: (!info.version.is_empty()).then_some(info.version),
|
||||
peer_id: Some(info.peer_id as i64),
|
||||
listeners: info
|
||||
.listeners
|
||||
.into_iter()
|
||||
.map(|url| url.to_string())
|
||||
.collect(),
|
||||
vpn_portal_cfg: info.vpn_portal_cfg,
|
||||
udp_nat_type: info.stun_info.as_ref().map(|stun| stun.udp_nat_type),
|
||||
tcp_nat_type: info.stun_info.as_ref().map(|stun| stun.tcp_nat_type),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn runtime_instance_from_running_info(
|
||||
config_id: String,
|
||||
display_name: String,
|
||||
magic_dns_enabled: bool,
|
||||
need_exit_node: bool,
|
||||
info: api::manage::NetworkInstanceRunningInfo,
|
||||
) -> RuntimeInstanceState {
|
||||
let tun_attached = info.running && is_tun_attached(&config_id);
|
||||
let tun_required = info.running && (info.dev_name != "no_tun" || tun_attached);
|
||||
|
||||
RuntimeInstanceState {
|
||||
config_id: config_id.clone(),
|
||||
instance_id: config_id,
|
||||
display_name,
|
||||
running: info.running,
|
||||
tun_required,
|
||||
tun_attached,
|
||||
magic_dns_enabled,
|
||||
need_exit_node,
|
||||
error_message: info.error_msg,
|
||||
my_node_info: info.my_node_info.map(my_node_info_to_view),
|
||||
events: info.events,
|
||||
routes: info.routes.into_iter().map(route_to_view).collect(),
|
||||
peers: info.peers.into_iter().map(peer_to_view).collect(),
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "easytier-gui",
|
||||
"type": "module",
|
||||
"version": "2.6.3",
|
||||
"version": "2.6.4",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "easytier-gui"
|
||||
version = "2.6.3"
|
||||
version = "2.6.4"
|
||||
description = "EasyTier GUI"
|
||||
authors = ["you"]
|
||||
edition.workspace = true
|
||||
|
||||
@@ -15,9 +15,7 @@ use easytier::rpc_service::remote_client::{
|
||||
use easytier::web_client::{self, WebClient};
|
||||
use easytier::{
|
||||
common::{
|
||||
config::{
|
||||
ConfigLoader, ConfigSource, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader,
|
||||
},
|
||||
config::{ConfigLoader, ConfigSource, FileLoggerConfig, LoggingConfig, TomlConfigLoader},
|
||||
log,
|
||||
},
|
||||
instance_manager::NetworkInstanceManager,
|
||||
@@ -490,10 +488,18 @@ async fn init_web_client(app: AppHandle, url: Option<String>) -> Result<(), Stri
|
||||
.ok_or_else(|| "Instance manager is not available".to_string())?;
|
||||
|
||||
let hooks = Arc::new(manager::GuiHooks { app: app.clone() });
|
||||
let machine_id_state_dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.with_context(|| "Failed to resolve machine id state directory")
|
||||
.map_err(|e| format!("{:#}", e))?;
|
||||
|
||||
let web_client = web_client::run_web_client(
|
||||
url.as_str(),
|
||||
None,
|
||||
easytier::common::MachineIdOptions {
|
||||
explicit_machine_id: None,
|
||||
state_dir: Some(machine_id_state_dir),
|
||||
},
|
||||
None,
|
||||
false,
|
||||
instance_manager,
|
||||
@@ -1318,7 +1324,7 @@ pub fn run_gui() -> std::process::ExitCode {
|
||||
let Ok(log_dir) = get_log_dir(app.app_handle()) else {
|
||||
return Ok(());
|
||||
};
|
||||
let config = LoggingConfigBuilder::default()
|
||||
let config = LoggingConfig::builder()
|
||||
.file_logger(FileLoggerConfig {
|
||||
dir: Some(log_dir.to_string_lossy().to_string()),
|
||||
level: None,
|
||||
@@ -1326,8 +1332,7 @@ pub fn run_gui() -> std::process::ExitCode {
|
||||
size_mb: None,
|
||||
count: None,
|
||||
})
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
.build();
|
||||
let Ok(_) = log::init(&config, true) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"createUpdaterArtifacts": false
|
||||
},
|
||||
"productName": "easytier-gui",
|
||||
"version": "2.6.3",
|
||||
"version": "2.6.4",
|
||||
"identifier": "com.kkrainbow.easytier",
|
||||
"plugins": {
|
||||
"shell": {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "easytier-rpc-build"
|
||||
description = "Protobuf RPC Service Generator for EasyTier"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasyTier"
|
||||
authors = ["kkrainbow"]
|
||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||
categories = ["network-programming", "command-line-utilities"]
|
||||
license-file = "LICENSE"
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
heck = "0.5"
|
||||
prost-build = "0.13"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
internal-namespace = []
|
||||
@@ -1 +0,0 @@
|
||||
../LICENSE
|
||||
@@ -1,3 +0,0 @@
|
||||
# Introduction
|
||||
|
||||
This is a protobuf rpc service stub generator for [EasyTier](https://github.com/EasyTier/EasyTier) project.
|
||||
@@ -1,449 +0,0 @@
|
||||
extern crate heck;
|
||||
extern crate prost_build;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
#[cfg(feature = "internal-namespace")]
|
||||
const NAMESPACE: &str = "crate::proto::rpc_types";
|
||||
|
||||
#[cfg(not(feature = "internal-namespace"))]
|
||||
const NAMESPACE: &str = "easytier::proto::rpc_types";
|
||||
|
||||
/// The service generator to be used with `prost-build` to generate RPC implementations for
|
||||
/// `prost-simple-rpc`.
|
||||
///
|
||||
/// See the crate-level documentation for more info.
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ServiceGenerator {
|
||||
_private: (),
|
||||
}
|
||||
|
||||
impl prost_build::ServiceGenerator for ServiceGenerator {
|
||||
fn generate(&mut self, service: prost_build::Service, mut buf: &mut String) {
|
||||
use std::fmt::Write;
|
||||
|
||||
let descriptor_name = format!("{}Descriptor", service.name);
|
||||
let server_name = format!("{}Server", service.name);
|
||||
let client_name = format!("{}Client", service.name);
|
||||
let method_descriptor_name = format!("{}MethodDescriptor", service.name);
|
||||
|
||||
let mut trait_methods = String::new();
|
||||
let mut weak_impl_methods = String::new();
|
||||
let mut enum_methods = String::new();
|
||||
let mut list_enum_methods = String::new();
|
||||
let mut client_methods = String::new();
|
||||
let mut client_own_methods = String::new();
|
||||
let mut match_name_methods = String::new();
|
||||
let mut match_proto_name_methods = String::new();
|
||||
let mut match_input_type_methods = String::new();
|
||||
let mut match_input_proto_type_methods = String::new();
|
||||
let mut match_output_type_methods = String::new();
|
||||
let mut match_output_proto_type_methods = String::new();
|
||||
let mut match_handle_methods = String::new();
|
||||
// generate trait default method Xxx::json_call_method match branch
|
||||
let mut match_trait_json_methods = String::new();
|
||||
|
||||
let mut match_method_try_from = String::new();
|
||||
|
||||
for (idx, method) in service.methods.iter().enumerate() {
|
||||
assert!(
|
||||
!method.client_streaming,
|
||||
"Client streaming not yet supported for method {}",
|
||||
method.proto_name
|
||||
);
|
||||
assert!(
|
||||
!method.server_streaming,
|
||||
"Server streaming not yet supported for method {}",
|
||||
method.proto_name
|
||||
);
|
||||
|
||||
ServiceGenerator::write_comments(&mut trait_methods, 4, &method.comments).unwrap();
|
||||
writeln!(
|
||||
trait_methods,
|
||||
r#" async fn {name}(&self, ctrl: Self::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}>;"#,
|
||||
name = method.name,
|
||||
input_type = method.input_type,
|
||||
output_type = method.output_type,
|
||||
namespace = NAMESPACE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(
|
||||
weak_impl_methods,
|
||||
r#" async fn {method_name}(&self, ctrl: Self::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}> {{
|
||||
let Some(service) = self.upgrade() else {{
|
||||
return Err({namespace}::error::Error::Shutdown);
|
||||
}};
|
||||
service.{method_name}(ctrl, input).await
|
||||
}}"#,
|
||||
method_name = method.name,
|
||||
input_type = method.input_type,
|
||||
output_type = method.output_type,
|
||||
namespace = NAMESPACE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
ServiceGenerator::write_comments(&mut enum_methods, 4, &method.comments).unwrap();
|
||||
writeln!(
|
||||
enum_methods,
|
||||
" {name} = {index},",
|
||||
name = method.proto_name,
|
||||
index = idx + 1
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(
|
||||
match_method_try_from,
|
||||
" {index} => Ok({service_name}MethodDescriptor::{name}),",
|
||||
service_name = service.name,
|
||||
name = method.proto_name,
|
||||
index = idx + 1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(
|
||||
list_enum_methods,
|
||||
" {service_name}MethodDescriptor::{name},",
|
||||
service_name = service.name,
|
||||
name = method.proto_name
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(
|
||||
client_methods,
|
||||
r#" async fn {name}(&self, ctrl: H::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}> {{
|
||||
{client_name}Client::{name}_inner(self.0.clone(), ctrl, input).await
|
||||
}}"#,
|
||||
name = method.name,
|
||||
input_type = method.input_type,
|
||||
output_type = method.output_type,
|
||||
client_name = service.name,
|
||||
namespace = NAMESPACE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(
|
||||
client_own_methods,
|
||||
r#" async fn {name}_inner(handler: H, ctrl: H::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}> {{
|
||||
{namespace}::__rt::call_method(handler, ctrl, {method_descriptor_name}::{proto_name}, input).await
|
||||
}}"#,
|
||||
name = method.name,
|
||||
method_descriptor_name = method_descriptor_name,
|
||||
proto_name = method.proto_name,
|
||||
input_type = method.input_type,
|
||||
output_type = method.output_type,
|
||||
namespace = NAMESPACE,
|
||||
).unwrap();
|
||||
|
||||
let case = format!(
|
||||
" {service_name}MethodDescriptor::{proto_name} => ",
|
||||
service_name = service.name,
|
||||
proto_name = method.proto_name
|
||||
);
|
||||
|
||||
writeln!(match_name_methods, "{}{:?},", case, method.name).unwrap();
|
||||
writeln!(match_proto_name_methods, "{}{:?},", case, method.proto_name).unwrap();
|
||||
writeln!(
|
||||
match_input_type_methods,
|
||||
"{}::std::any::TypeId::of::<{}>(),",
|
||||
case, method.input_type
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
match_input_proto_type_methods,
|
||||
"{}{:?},",
|
||||
case, method.input_proto_type
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
match_output_type_methods,
|
||||
"{}::std::any::TypeId::of::<{}>(),",
|
||||
case, method.output_type
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
match_output_proto_type_methods,
|
||||
"{}{:?},",
|
||||
case, method.output_proto_type
|
||||
)
|
||||
.unwrap();
|
||||
write!(
|
||||
match_handle_methods,
|
||||
r#"{} {{
|
||||
let decoded: {input_type} = {namespace}::__rt::decode(input)?;
|
||||
let ret = service.{name}(ctrl, decoded).await?;
|
||||
{namespace}::__rt::encode(ret)
|
||||
}}
|
||||
"#,
|
||||
case,
|
||||
input_type = method.input_type,
|
||||
name = method.name,
|
||||
namespace = NAMESPACE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
write!(
|
||||
match_trait_json_methods,
|
||||
r#" "{name}" | "{proto_name}" => {{
|
||||
let req: {input_type} = ::serde_json::from_value(json).map_err(|e| {namespace}::error::Error::MalformatRpcPacket(format!("json error: {{}}", e)))?;
|
||||
let resp = self.{typed_method}(ctrl, req).await?;
|
||||
Ok(::serde_json::to_value(resp).map_err(|e| {namespace}::error::Error::MalformatRpcPacket(format!("json error: {{}}", e)))?)
|
||||
}}
|
||||
"#,
|
||||
name = method.name,
|
||||
proto_name = method.proto_name,
|
||||
input_type = method.input_type,
|
||||
typed_method = method.name,
|
||||
namespace = NAMESPACE,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
ServiceGenerator::write_comments(&mut buf, 0, &service.comments).unwrap();
|
||||
write!(
|
||||
buf,
|
||||
r#"
|
||||
#[async_trait::async_trait]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait {name} {{
|
||||
type Controller: {namespace}::controller::Controller;
|
||||
|
||||
{trait_methods}
|
||||
|
||||
async fn json_call_method(
|
||||
&self,
|
||||
ctrl: Self::Controller,
|
||||
method_name: &str,
|
||||
json: ::serde_json::Value,
|
||||
) -> {namespace}::error::Result<::serde_json::Value> {{
|
||||
match method_name {{
|
||||
{match_trait_json_methods}
|
||||
_ => Err({namespace}::error::Error::InvalidMethodIndex(0, method_name.to_string())),
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<T> {name} for ::std::sync::Weak<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
::std::sync::Arc<T>: {name},
|
||||
{{
|
||||
type Controller = <::std::sync::Arc<T> as {name}>::Controller;
|
||||
|
||||
{weak_impl_methods}
|
||||
}}
|
||||
|
||||
/// A service descriptor for a `{name}`.
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Default)]
|
||||
pub struct {descriptor_name};
|
||||
|
||||
/// Methods available on a `{name}`.
|
||||
///
|
||||
/// This can be used as a key when routing requests for servers/clients of a `{name}`.
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
#[repr(u8)]
|
||||
pub enum {method_descriptor_name} {{
|
||||
{enum_methods}
|
||||
}}
|
||||
|
||||
impl std::convert::TryFrom<u8> for {method_descriptor_name} {{
|
||||
type Error = {namespace}::error::Error;
|
||||
fn try_from(value: u8) -> {namespace}::error::Result<Self> {{
|
||||
match value {{
|
||||
{match_method_try_from}
|
||||
_ => Err({namespace}::error::Error::InvalidMethodIndex(value, "{name}".to_string())),
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
/// A client for a `{name}`.
|
||||
///
|
||||
/// This implements the `{name}` trait by dispatching all method calls to the supplied `Handler`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct {client_name}<H>(H) where H: {namespace}::handler::Handler;
|
||||
|
||||
impl<H> {client_name}<H> where H: {namespace}::handler::Handler<Descriptor = {descriptor_name}> {{
|
||||
/// Creates a new client instance that delegates all method calls to the supplied handler.
|
||||
pub fn new(handler: H) -> {client_name}<H> {{
|
||||
{client_name}(handler)
|
||||
}}
|
||||
}}
|
||||
|
||||
impl<H> {client_name}<H> where H: {namespace}::handler::Handler<Descriptor = {descriptor_name}> {{
|
||||
{client_own_methods}
|
||||
}}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<H> {name} for {client_name}<H> where H: {namespace}::handler::Handler<Descriptor = {descriptor_name}> {{
|
||||
type Controller = H::Controller;
|
||||
|
||||
{client_methods}
|
||||
}}
|
||||
|
||||
pub struct {client_name}Factory<C: {namespace}::controller::Controller>(std::marker::PhantomData<C>);
|
||||
|
||||
impl<C: {namespace}::controller::Controller> Clone for {client_name}Factory<C> {{
|
||||
fn clone(&self) -> Self {{
|
||||
Self(std::marker::PhantomData)
|
||||
}}
|
||||
}}
|
||||
|
||||
impl<C> {namespace}::__rt::RpcClientFactory for {client_name}Factory<C> where C: {namespace}::controller::Controller {{
|
||||
type Descriptor = {descriptor_name};
|
||||
type ClientImpl = Box<dyn {name}<Controller = C> + Send + Sync + 'static>;
|
||||
type Controller = C;
|
||||
|
||||
fn new(handler: impl {namespace}::handler::Handler<Descriptor = Self::Descriptor, Controller = Self::Controller>) -> Self::ClientImpl {{
|
||||
Box::new({client_name}::new(handler))
|
||||
}}
|
||||
}}
|
||||
|
||||
/// A server for a `{name}`.
|
||||
///
|
||||
/// This implements the `Server` trait by handling requests and dispatch them to methods on the
|
||||
/// supplied `{name}`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct {server_name}<A>(A) where A: {name} + Clone + Send + 'static;
|
||||
|
||||
impl<T> {server_name}<::std::sync::Weak<T>>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
::std::sync::Arc<T>: {name},
|
||||
{{
|
||||
pub fn new_arc(service: ::std::sync::Arc<T>) -> {server_name}<::std::sync::Weak<T>> {{
|
||||
{server_name}(::std::sync::Arc::downgrade(&service))
|
||||
}}
|
||||
}}
|
||||
|
||||
impl<A> {server_name}<A> where A: {name} + Clone + Send + 'static {{
|
||||
/// Creates a new server instance that dispatches all calls to the supplied service.
|
||||
pub fn new(service: A) -> {server_name}<A> {{
|
||||
{server_name}(service)
|
||||
}}
|
||||
|
||||
async fn call_inner(
|
||||
service: A,
|
||||
method: {method_descriptor_name},
|
||||
ctrl: A::Controller,
|
||||
input: ::bytes::Bytes)
|
||||
-> {namespace}::error::Result<::bytes::Bytes> {{
|
||||
match method {{
|
||||
{match_handle_methods}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
impl {namespace}::descriptor::ServiceDescriptor for {descriptor_name} {{
|
||||
type Method = {method_descriptor_name};
|
||||
fn name(&self) -> &'static str {{ {name:?} }}
|
||||
fn proto_name(&self) -> &'static str {{ {proto_name:?} }}
|
||||
fn package(&self) -> &'static str {{ {package:?} }}
|
||||
fn methods(&self) -> &'static [Self::Method] {{
|
||||
&[ {list_enum_methods} ]
|
||||
}}
|
||||
}}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<A> {namespace}::handler::Handler for {server_name}<A>
|
||||
where
|
||||
A: {name} + Clone + Send + Sync + 'static {{
|
||||
type Descriptor = {descriptor_name};
|
||||
type Controller = A::Controller;
|
||||
|
||||
async fn call(
|
||||
&self,
|
||||
ctrl: A::Controller,
|
||||
method: {method_descriptor_name},
|
||||
input: ::bytes::Bytes)
|
||||
-> {namespace}::error::Result<::bytes::Bytes> {{
|
||||
{server_name}::call_inner(self.0.clone(), method, ctrl, input).await
|
||||
}}
|
||||
}}
|
||||
|
||||
impl {namespace}::descriptor::MethodDescriptor for {method_descriptor_name} {{
|
||||
fn name(&self) -> &'static str {{
|
||||
match *self {{
|
||||
{match_name_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn proto_name(&self) -> &'static str {{
|
||||
match *self {{
|
||||
{match_proto_name_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn input_type(&self) -> ::std::any::TypeId {{
|
||||
match *self {{
|
||||
{match_input_type_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn input_proto_type(&self) -> &'static str {{
|
||||
match *self {{
|
||||
{match_input_proto_type_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn output_type(&self) -> ::std::any::TypeId {{
|
||||
match *self {{
|
||||
{match_output_type_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn output_proto_type(&self) -> &'static str {{
|
||||
match *self {{
|
||||
{match_output_proto_type_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn index(&self) -> u8 {{
|
||||
*self as u8
|
||||
}}
|
||||
}}
|
||||
"#,
|
||||
name = service.name,
|
||||
descriptor_name = descriptor_name,
|
||||
server_name = server_name,
|
||||
client_name = client_name,
|
||||
method_descriptor_name = method_descriptor_name,
|
||||
proto_name = service.proto_name,
|
||||
package = service.package,
|
||||
trait_methods = trait_methods,
|
||||
weak_impl_methods = weak_impl_methods,
|
||||
enum_methods = enum_methods,
|
||||
list_enum_methods = list_enum_methods,
|
||||
client_own_methods = client_own_methods,
|
||||
client_methods = client_methods,
|
||||
match_name_methods = match_name_methods,
|
||||
match_proto_name_methods = match_proto_name_methods,
|
||||
match_input_type_methods = match_input_type_methods,
|
||||
match_input_proto_type_methods = match_input_proto_type_methods,
|
||||
match_output_type_methods = match_output_type_methods,
|
||||
match_output_proto_type_methods = match_output_proto_type_methods,
|
||||
match_handle_methods = match_handle_methods,
|
||||
match_trait_json_methods = match_trait_json_methods,
|
||||
namespace = NAMESPACE,
|
||||
).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceGenerator {
|
||||
fn write_comments<W>(
|
||||
mut write: W,
|
||||
indent: usize,
|
||||
comments: &prost_build::Comments,
|
||||
) -> fmt::Result
|
||||
where
|
||||
W: fmt::Write,
|
||||
{
|
||||
for comment in &comments.leading {
|
||||
for line in comment.lines().filter(|s| !s.is_empty()) {
|
||||
writeln!(write, "{}///{}", " ".repeat(indent), line)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "easytier-web"
|
||||
version = "2.6.3"
|
||||
version = "2.6.4"
|
||||
edition.workspace = true
|
||||
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ const bool_flags: BoolFlag[] = [
|
||||
{ field: 'disable_encryption', help: 'disable_encryption_help' },
|
||||
{ field: 'disable_tcp_hole_punching', help: 'disable_tcp_hole_punching_help' },
|
||||
{ field: 'disable_udp_hole_punching', help: 'disable_udp_hole_punching_help' },
|
||||
{ field: 'enable_udp_broadcast_relay', help: 'enable_udp_broadcast_relay_help' },
|
||||
{ field: 'disable_upnp', help: 'disable_upnp_help' },
|
||||
{ field: 'disable_sym_hole_punching', help: 'disable_sym_hole_punching_help' },
|
||||
{ field: 'enable_magic_dns', help: 'enable_magic_dns_help' },
|
||||
|
||||
@@ -160,6 +160,9 @@ disable_tcp_hole_punching_help: 禁用TCP打洞功能
|
||||
disable_udp_hole_punching: 禁用UDP打洞
|
||||
disable_udp_hole_punching_help: 禁用UDP打洞功能
|
||||
|
||||
enable_udp_broadcast_relay: UDP 广播中继
|
||||
enable_udp_broadcast_relay_help: "仅 Windows:捕获物理网卡上的本机 UDP 广播包并转发给 EasyTier 对等节点,帮助局域网游戏发现房间。需要管理员权限。"
|
||||
|
||||
disable_upnp: 禁用 UPnP
|
||||
disable_upnp_help: 禁用符合条件监听器的运行时 UPnP/NAT-PMP 端口映射;自动端口映射默认开启。
|
||||
|
||||
@@ -260,6 +263,7 @@ event:
|
||||
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
||||
PortForwardAdded: 端口转发添加
|
||||
ProxyCidrsUpdated: 子网代理CIDR更新
|
||||
UdpBroadcastRelayStartResult: UDP广播中继启动结果
|
||||
|
||||
web:
|
||||
login:
|
||||
|
||||
@@ -159,6 +159,9 @@ disable_tcp_hole_punching_help: Disable tcp hole punching
|
||||
disable_udp_hole_punching: Disable UDP Hole Punching
|
||||
disable_udp_hole_punching_help: Disable udp hole punching
|
||||
|
||||
enable_udp_broadcast_relay: UDP Broadcast Relay
|
||||
enable_udp_broadcast_relay_help: "Windows only: capture local UDP broadcast packets from physical interfaces and forward them to EasyTier peers. Helps games to find rooms in local network. Requires administrator privileges."
|
||||
|
||||
disable_upnp: Disable UPnP
|
||||
disable_upnp_help: Disable runtime UPnP/NAT-PMP port mapping for eligible listeners; automatic port mapping is enabled by default.
|
||||
|
||||
@@ -260,6 +263,7 @@ event:
|
||||
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
||||
PortForwardAdded: PortForwardAdded
|
||||
ProxyCidrsUpdated: ProxyCidrsUpdated
|
||||
UdpBroadcastRelayStartResult: UDP Broadcast Relay Start Result
|
||||
|
||||
web:
|
||||
login:
|
||||
|
||||
@@ -134,6 +134,7 @@ export interface NetworkConfig {
|
||||
disable_tcp_hole_punching?: boolean
|
||||
disable_udp_hole_punching?: boolean
|
||||
disable_upnp?: boolean
|
||||
enable_udp_broadcast_relay?: boolean
|
||||
disable_sym_hole_punching?: boolean
|
||||
|
||||
enable_relay_network_whitelist?: boolean
|
||||
@@ -211,6 +212,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
disable_tcp_hole_punching: false,
|
||||
disable_udp_hole_punching: false,
|
||||
disable_upnp: false,
|
||||
enable_udp_broadcast_relay: false,
|
||||
disable_sym_hole_punching: false,
|
||||
enable_relay_network_whitelist: false,
|
||||
relay_network_whitelist: [],
|
||||
@@ -447,4 +449,6 @@ export enum EventType {
|
||||
PortForwardAdded = 'PortForwardAdded', // PortForwardConfigPb
|
||||
|
||||
ProxyCidrsUpdated = 'ProxyCidrsUpdated', // string[], string[]
|
||||
|
||||
UdpBroadcastRelayStartResult = 'UdpBroadcastRelayStartResult', // { capture_backend?: string, error?: string }
|
||||
}
|
||||
|
||||
@@ -365,6 +365,7 @@ mod tests {
|
||||
let _c = WebClient::new(
|
||||
connector,
|
||||
"test",
|
||||
uuid::Uuid::new_v4(),
|
||||
"test",
|
||||
false,
|
||||
Arc::new(NetworkInstanceManager::new()),
|
||||
|
||||
+19
-15
@@ -3,7 +3,7 @@ name = "easytier"
|
||||
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasyTier"
|
||||
version = "2.6.3"
|
||||
version = "2.6.4"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors = ["kkrainbow"]
|
||||
@@ -11,6 +11,7 @@ keywords = ["vpn", "p2p", "network", "easytier"]
|
||||
categories = ["network-programming", "command-line-utilities"]
|
||||
license-file = "LICENSE"
|
||||
readme = "README.md"
|
||||
build = "build/main.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -50,6 +51,9 @@ time = "0.3"
|
||||
toml = "0.8.12"
|
||||
chrono = { version = "0.4.37", features = ["serde"] }
|
||||
|
||||
getset = "0.1.6"
|
||||
optionize = "0.2"
|
||||
|
||||
guarden = "0.1"
|
||||
|
||||
delegate = "0.13.5"
|
||||
@@ -58,7 +62,7 @@ itertools = "0.14.0"
|
||||
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
|
||||
gethostname = "0.5.0"
|
||||
hostname = "0.4.2"
|
||||
|
||||
futures = { version = "0.3", features = ["bilock", "unstable"] }
|
||||
|
||||
@@ -69,9 +73,12 @@ tokio-util = { version = "0.7.9", features = ["codec", "net", "io", "rt"] }
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.74"
|
||||
|
||||
maplit = "1.0.2"
|
||||
dashmap = "6.0"
|
||||
timedmap = "=1.0.1"
|
||||
|
||||
moka = { version = "0.12", features = ["future"] }
|
||||
|
||||
# for full-path zero-copy
|
||||
zerocopy = { version = "0.7.32", features = ["derive", "simd"] }
|
||||
bytes = "1.5.0"
|
||||
@@ -150,6 +157,7 @@ rand = "0.8.5"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
pnet = { version = "0.35.0", features = ["serde"] }
|
||||
serde_json = "1"
|
||||
serde_with = "3"
|
||||
|
||||
clap = { version = "4.5.30", features = [
|
||||
"string",
|
||||
@@ -168,6 +176,7 @@ network-interface = "2.0"
|
||||
# for ospf route
|
||||
petgraph = "0.8.1"
|
||||
ordered_hash_map = "0.5.0"
|
||||
indexmap = "2.13.1"
|
||||
|
||||
# for wireguard
|
||||
boringtun = { package = "boringtun-easytier", version = "0.6.1", optional = true }
|
||||
@@ -234,17 +243,14 @@ http_req = { git = "https://github.com/EasyTier/http_req.git", default-features
|
||||
] }
|
||||
|
||||
# for dns connector
|
||||
hickory-resolver = "0.25.2"
|
||||
hickory-proto = "0.25.2"
|
||||
hickory-proto = "0.26.0"
|
||||
hickory-net = { version = "0.26.0", features = ["serde"] }
|
||||
hickory-resolver = { version = "0.26.0", features = ["https-ring", "webpki-roots"] }
|
||||
|
||||
# for magic dns
|
||||
hickory-client = { version = "0.25.2", optional = true }
|
||||
hickory-server = { version = "0.25.2", features = [
|
||||
"resolver",
|
||||
], optional = true }
|
||||
hickory-server = { version = "0.26.0", features = ["resolver"], optional = true }
|
||||
|
||||
bon = "3.9.1"
|
||||
derive_builder = "0.20.2"
|
||||
humantime-serde = "1.1.1"
|
||||
multimap = "0.10.1"
|
||||
version-compare = "0.2.0"
|
||||
@@ -315,15 +321,14 @@ jemalloc-sys = { package = "tikv-jemalloc-sys", version = "0.6.0", features = [
|
||||
|
||||
[build-dependencies]
|
||||
cfg_aliases = "0.2.1"
|
||||
tonic-build = "0.12"
|
||||
indoc = "2.0"
|
||||
globwalk = "0.8.1"
|
||||
regex = "1"
|
||||
prost-build = "0.13.5"
|
||||
prost-wkt-build = "0.6"
|
||||
easytier-rpc-build = { path = "../easytier-rpc-build", features = [
|
||||
"internal-namespace",
|
||||
] }
|
||||
prost-reflect-build = { version = "0.14.0" }
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
|
||||
"win7",
|
||||
] }
|
||||
@@ -337,7 +342,6 @@ zip = "4.0.0"
|
||||
serial_test = "3.0.0"
|
||||
rstest = "0.25.0"
|
||||
futures-util = "0.3.31"
|
||||
maplit = "1.0.2"
|
||||
tempfile = "3.22.0"
|
||||
ctor = "0.8.0"
|
||||
|
||||
@@ -399,7 +403,7 @@ jemalloc-prof = [
|
||||
"jemalloc-sys/stats",
|
||||
]
|
||||
tracing = ["tokio/tracing", "dep:console-subscriber"]
|
||||
magic-dns = ["dep:hickory-client", "dep:hickory-server"]
|
||||
magic-dns = ["dep:hickory-server"]
|
||||
faketcp = ["dep:flume"]
|
||||
zstd = ["dep:zstd"]
|
||||
# For Network Extension on macOS
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
mod rpc;
|
||||
|
||||
use crate::rpc::ServiceGenerator;
|
||||
use cfg_aliases::cfg_aliases;
|
||||
use prost_wkt_build::{FileDescriptorSet, Message as _};
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -163,7 +166,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
"src/proto/api_config.proto",
|
||||
"src/proto/api_manage.proto",
|
||||
"src/proto/web.proto",
|
||||
"src/proto/magic_dns.proto",
|
||||
"src/proto/dns.proto",
|
||||
"src/proto/acl.proto",
|
||||
];
|
||||
|
||||
@@ -171,17 +174,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("cargo:rerun-if-changed={proto_file}");
|
||||
}
|
||||
|
||||
let out = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let descriptor_file = out.join("descriptors.bin");
|
||||
|
||||
let mut config = prost_build::Config::new();
|
||||
config
|
||||
.type_attribute(".", "#[derive(serde::Serialize,serde::Deserialize)]")
|
||||
.extern_path(".google.protobuf.Any", "::prost_wkt_types::Any")
|
||||
.extern_path(".google.protobuf.Timestamp", "::prost_wkt_types::Timestamp")
|
||||
.extern_path(".google.protobuf.Value", "::prost_wkt_types::Value")
|
||||
.file_descriptor_set_path(&descriptor_file)
|
||||
.protoc_arg("--experimental_allow_proto3_optional")
|
||||
.extern_path(".google.protobuf.Value", "::prost_wkt_types::Value");
|
||||
|
||||
config
|
||||
.type_attribute(".", "#[derive(serde::Serialize,serde::Deserialize)]")
|
||||
.type_attribute("peer_rpc.DirectConnectedPeerInfo", "#[derive(Hash)]")
|
||||
.type_attribute("peer_rpc.PeerInfoForGlobalMap", "#[derive(Hash)]")
|
||||
.type_attribute("peer_rpc.ForeignNetworkRouteInfoKey", "#[derive(Hash, Eq)]")
|
||||
@@ -195,20 +195,28 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.type_attribute("acl.AclV1", "#[serde(default)]")
|
||||
.type_attribute("acl.Chain", "#[serde(default)]")
|
||||
.type_attribute("acl.Rule", "#[serde(default)]")
|
||||
.type_attribute("acl.GroupInfo", "#[serde(default)]")
|
||||
.field_attribute(".api.manage.NetworkConfig", "#[serde(default)]")
|
||||
.service_generator(Box::new(easytier_rpc_build::ServiceGenerator::default()))
|
||||
.btree_map(["."])
|
||||
.skip_debug([".common.Ipv4Addr", ".common.Ipv6Addr", ".common.UUID"]);
|
||||
.type_attribute("acl.GroupInfo", "#[serde(default)]");
|
||||
|
||||
config.compile_protos(&proto_files, &["src/proto/"])?;
|
||||
config.field_attribute("api.manage.NetworkConfig", "#[serde(default)]");
|
||||
|
||||
config.skip_debug([".common.Ipv4Addr", ".common.Ipv6Addr", ".common.UUID"]);
|
||||
|
||||
let out = PathBuf::from(env::var("OUT_DIR")?);
|
||||
let descriptor_file = out.join("descriptors.bin");
|
||||
|
||||
config
|
||||
.btree_map(["."])
|
||||
.service_generator(Box::new(ServiceGenerator::default()))
|
||||
.protoc_arg("--experimental_allow_proto3_optional")
|
||||
.file_descriptor_set_path(&descriptor_file)
|
||||
.compile_protos(&proto_files, &["src/proto/"])?;
|
||||
|
||||
prost_reflect_build::Builder::new()
|
||||
.file_descriptor_set_bytes("crate::proto::DESCRIPTOR_POOL_BYTES")
|
||||
.compile_protos_with_config(config, &proto_files_reflect, &["src/proto/"])?;
|
||||
|
||||
let descriptor_bytes = std::fs::read(descriptor_file).unwrap();
|
||||
let descriptor = FileDescriptorSet::decode(&descriptor_bytes[..]).unwrap();
|
||||
let descriptor_bytes = std::fs::read(descriptor_file)?;
|
||||
let descriptor = FileDescriptorSet::decode(&descriptor_bytes[..])?;
|
||||
prost_wkt_build::add_serde(out, descriptor);
|
||||
|
||||
check_locale();
|
||||
@@ -0,0 +1,720 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use indoc::formatdoc;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::{format_ident, quote};
|
||||
use std::str::FromStr;
|
||||
|
||||
fn parse(value: &str) -> TokenStream {
|
||||
TokenStream::from_str(value)
|
||||
.unwrap_or_else(|err| panic!("Failed to parse tokens: {} ({})", value, err))
|
||||
}
|
||||
|
||||
fn doc(comments: &prost_build::Comments) -> TokenStream {
|
||||
let doc = comments
|
||||
.leading
|
||||
.iter()
|
||||
.flat_map(|c| c.lines().filter(|s| !s.is_empty()));
|
||||
quote! { #( #[doc = #doc] )* }
|
||||
}
|
||||
|
||||
const NAMESPACE: &str = "crate::proto::rpc_types";
|
||||
|
||||
struct Method {
|
||||
index: u8,
|
||||
doc: TokenStream,
|
||||
method: Ident,
|
||||
method_inner: Ident,
|
||||
method_str: String,
|
||||
method_proto: Ident,
|
||||
method_proto_str: String,
|
||||
Input: TokenStream,
|
||||
Input_proto_str: String,
|
||||
Output: TokenStream,
|
||||
Output_proto_str: String,
|
||||
}
|
||||
|
||||
impl Method {
|
||||
fn new(index: u8, method: prost_build::Method) -> Self {
|
||||
assert!(
|
||||
!method.client_streaming,
|
||||
"Client streaming not yet supported for method {}",
|
||||
method.proto_name
|
||||
);
|
||||
assert!(
|
||||
!method.server_streaming,
|
||||
"Server streaming not yet supported for method {}",
|
||||
method.proto_name
|
||||
);
|
||||
Self {
|
||||
index,
|
||||
doc: doc(&method.comments),
|
||||
method: format_ident!("{}", method.name),
|
||||
method_inner: format_ident!("{}_inner", method.name),
|
||||
method_str: method.name,
|
||||
method_proto: format_ident!("{}", method.proto_name),
|
||||
method_proto_str: method.proto_name,
|
||||
Input: parse(&method.input_type),
|
||||
Input_proto_str: method.input_proto_type,
|
||||
Output: parse(&method.output_type),
|
||||
Output_proto_str: method.output_proto_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Service {
|
||||
namespace: TokenStream,
|
||||
doc: TokenStream,
|
||||
Service: Ident,
|
||||
ServiceDescriptor: Ident,
|
||||
ServiceServer: Ident,
|
||||
ServiceClient: Ident,
|
||||
ServiceClientFactory: Ident,
|
||||
ServiceMethodDescriptor: Ident,
|
||||
Service_str: String,
|
||||
Service_proto_str: String,
|
||||
Service_package_str: String,
|
||||
methods: Vec<Method>,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
fn new(service: prost_build::Service) -> Self {
|
||||
let methods = service
|
||||
.methods
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, method)| Method::new((i + 1) as u8, method))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
namespace: parse(NAMESPACE),
|
||||
doc: doc(&service.comments),
|
||||
Service: format_ident!("{}", service.name),
|
||||
ServiceDescriptor: format_ident!("{}Descriptor", service.name),
|
||||
ServiceServer: format_ident!("{}Server", service.name),
|
||||
ServiceClient: format_ident!("{}Client", service.name),
|
||||
ServiceClientFactory: format_ident!("{}ClientFactory", service.name),
|
||||
ServiceMethodDescriptor: format_ident!("{}MethodDescriptor", service.name),
|
||||
Service_str: service.name,
|
||||
Service_proto_str: service.proto_name,
|
||||
Service_package_str: service.package,
|
||||
methods,
|
||||
}
|
||||
}
|
||||
|
||||
fn trait_Service(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
doc,
|
||||
Service,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let match_json_call_method = methods.iter().map(
|
||||
|Method {
|
||||
method,
|
||||
method_str,
|
||||
method_proto_str,
|
||||
Input,
|
||||
..
|
||||
}| {
|
||||
quote! {
|
||||
#method_str | #method_proto_str => {
|
||||
let req: #Input = ::serde_json::from_value(json)
|
||||
.map_err(|e| #namespace::error::Error::MalformatRpcPacket(format!("json error: {}", e)))?;
|
||||
let resp = self.#method(ctrl, req).await?;
|
||||
Ok(::serde_json::to_value(resp)
|
||||
.map_err(|e| #namespace::error::Error::MalformatRpcPacket(format!("json error: {}", e)))?)
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let methods = methods.iter().map(
|
||||
|Method {
|
||||
doc,
|
||||
method,
|
||||
Input,
|
||||
Output,
|
||||
..
|
||||
}| {
|
||||
quote! {
|
||||
#doc
|
||||
async fn #method(&self, ctrl: Self::Controller, input: #Input) -> #namespace::error::Result<#Output>;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
#doc
|
||||
#[async_trait::async_trait]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait #Service {
|
||||
type Controller: #namespace::controller::Controller;
|
||||
|
||||
#(#methods)*
|
||||
|
||||
async fn json_call_method(
|
||||
&self,
|
||||
ctrl: Self::Controller,
|
||||
method: &str,
|
||||
json: ::serde_json::Value,
|
||||
) -> #namespace::error::Result<::serde_json::Value> {
|
||||
match method {
|
||||
#(#match_json_call_method)*
|
||||
_ => Err(#namespace::error::Error::InvalidMethodIndex(0, method.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_Service_for_Weak(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
Service,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
let methods = methods.iter().map(
|
||||
|Method {
|
||||
method,
|
||||
Input,
|
||||
Output,
|
||||
..
|
||||
}| {
|
||||
quote! {
|
||||
async fn #method(&self, ctrl: Self::Controller, input: #Input) -> #namespace::error::Result<#Output> {
|
||||
let Some(service) = self.upgrade() else {
|
||||
return Err(#namespace::error::Error::Shutdown);
|
||||
};
|
||||
service.#method(ctrl, input).await
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
#[async_trait::async_trait]
|
||||
impl<T> #Service for ::std::sync::Weak<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
::std::sync::Arc<T>: #Service,
|
||||
{
|
||||
type Controller = <::std::sync::Arc<T> as #Service>::Controller;
|
||||
|
||||
#(#methods)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn struct_ServiceDescriptor(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
ServiceDescriptor,
|
||||
ServiceMethodDescriptor,
|
||||
Service_str,
|
||||
Service_proto_str,
|
||||
Service_package_str,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let doc = format!("A service descriptor for a `{}`.", Service_str);
|
||||
|
||||
let methods = methods.iter().map(|Method { method_proto, .. }| {
|
||||
quote! { #ServiceMethodDescriptor::#method_proto, }
|
||||
});
|
||||
|
||||
quote! {
|
||||
#[doc = #doc]
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Default)]
|
||||
pub struct #ServiceDescriptor;
|
||||
|
||||
impl #namespace::descriptor::ServiceDescriptor for #ServiceDescriptor {
|
||||
type Method = #ServiceMethodDescriptor;
|
||||
fn name(&self) -> &'static str { #Service_str }
|
||||
fn proto_name(&self) -> &'static str { #Service_proto_str }
|
||||
fn package(&self) -> &'static str { #Service_package_str }
|
||||
fn methods(&self) -> &'static [Self::Method] {
|
||||
&[ #(#methods)* ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enum_ServiceMethodDescriptor(&self) -> TokenStream {
|
||||
let Self {
|
||||
ServiceMethodDescriptor,
|
||||
Service_str,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let doc = formatdoc! {"
|
||||
Methods available on a `{Service_str}`.
|
||||
|
||||
This can be used as a key when routing requests for servers/clients of a `{Service_str}`.
|
||||
"};
|
||||
|
||||
let variants = methods.iter().map(
|
||||
|Method {
|
||||
method_proto,
|
||||
index,
|
||||
..
|
||||
}| {
|
||||
quote! { #method_proto = #index, }
|
||||
},
|
||||
);
|
||||
|
||||
let impl_MethodDescriptor = self.impl_MethodDescriptor_for_ServiceMethodDescriptor();
|
||||
let impl_TryFrom = self.impl_TryFrom_for_ServiceMethodDescriptor();
|
||||
quote! {
|
||||
#[doc = #doc]
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
#[repr(u8)]
|
||||
pub enum #ServiceMethodDescriptor {
|
||||
#(#variants)*
|
||||
}
|
||||
|
||||
#impl_MethodDescriptor
|
||||
|
||||
#impl_TryFrom
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_MethodDescriptor_for_ServiceMethodDescriptor(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
ServiceMethodDescriptor,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let name = {
|
||||
let arms = methods.iter().map(
|
||||
|Method {
|
||||
method_proto,
|
||||
method_str,
|
||||
..
|
||||
}| {
|
||||
quote! { #ServiceMethodDescriptor::#method_proto => #method_str, }
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
fn name(&self) -> &'static str {
|
||||
match *self {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let proto_name = {
|
||||
let arms = methods.iter().map(
|
||||
|Method {
|
||||
method_proto,
|
||||
method_proto_str,
|
||||
..
|
||||
}| {
|
||||
quote! { #ServiceMethodDescriptor::#method_proto => #method_proto_str, }
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
fn proto_name(&self) -> &'static str {
|
||||
match *self {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let input_type = {
|
||||
let arms = methods.iter().map(|Method { method_proto, Input, .. }| {
|
||||
quote! { #ServiceMethodDescriptor::#method_proto => ::std::any::TypeId::of::<#Input>(), }
|
||||
});
|
||||
|
||||
quote! {
|
||||
fn input_type(&self) -> ::std::any::TypeId {
|
||||
match *self {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let input_proto_type = {
|
||||
let arms = methods.iter().map(
|
||||
|Method {
|
||||
method_proto,
|
||||
Input_proto_str,
|
||||
..
|
||||
}| {
|
||||
quote! { #ServiceMethodDescriptor::#method_proto => #Input_proto_str, }
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
fn input_proto_type(&self) -> &'static str {
|
||||
match *self {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let output_type = {
|
||||
let arms = methods.iter().map(|Method { method_proto, Output, .. }| {
|
||||
quote! { #ServiceMethodDescriptor::#method_proto => ::std::any::TypeId::of::<#Output>(), }
|
||||
});
|
||||
|
||||
quote! {
|
||||
fn output_type(&self) -> ::std::any::TypeId {
|
||||
match *self {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let output_proto_type = {
|
||||
let arms = methods.iter().map(
|
||||
|Method {
|
||||
method_proto,
|
||||
Output_proto_str,
|
||||
..
|
||||
}| {
|
||||
quote! { #ServiceMethodDescriptor::#method_proto => #Output_proto_str, }
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
fn output_proto_type(&self) -> &'static str {
|
||||
match *self {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
impl #namespace::descriptor::MethodDescriptor for #ServiceMethodDescriptor {
|
||||
#name
|
||||
|
||||
#proto_name
|
||||
|
||||
#input_type
|
||||
|
||||
#input_proto_type
|
||||
|
||||
#output_type
|
||||
|
||||
#output_proto_type
|
||||
|
||||
fn index(&self) -> u8 {
|
||||
*self as u8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_TryFrom_for_ServiceMethodDescriptor(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
ServiceMethodDescriptor,
|
||||
Service_str,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let arms = methods.iter().map(
|
||||
|Method {
|
||||
method_proto,
|
||||
index,
|
||||
..
|
||||
}| {
|
||||
quote! { #index => Ok(#ServiceMethodDescriptor::#method_proto), }
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
impl std::convert::TryFrom<u8> for #ServiceMethodDescriptor {
|
||||
type Error = #namespace::error::Error;
|
||||
fn try_from(value: u8) -> #namespace::error::Result<Self> {
|
||||
match value {
|
||||
#(#arms)*
|
||||
_ => Err(#namespace::error::Error::InvalidMethodIndex(value, #Service_str.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn struct_ServiceClient(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
ServiceDescriptor,
|
||||
ServiceClient,
|
||||
Service_str,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let doc = formatdoc! {"
|
||||
A client for a `{Service_str}`.
|
||||
|
||||
This implements the `{Service_str}` trait by dispatching all method calls to the supplied `Handler`.
|
||||
"};
|
||||
|
||||
let impl_service_client = self.impl_ServiceClient();
|
||||
let impl_service_for_client = self.impl_Service_for_ServiceClient();
|
||||
quote! {
|
||||
#[doc = #doc]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct #ServiceClient<H>(H) where H: #namespace::handler::Handler;
|
||||
|
||||
impl<H> #ServiceClient<H> where H: #namespace::handler::Handler<Descriptor = #ServiceDescriptor> {
|
||||
/// Creates a new client instance that delegates all method calls to the supplied handler.
|
||||
pub fn new(handler: H) -> Self {
|
||||
Self(handler)
|
||||
}
|
||||
}
|
||||
|
||||
#impl_service_client
|
||||
|
||||
#impl_service_for_client
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_ServiceClient(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
ServiceClient,
|
||||
ServiceDescriptor,
|
||||
ServiceMethodDescriptor,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let methods = methods.iter().map(
|
||||
|Method {
|
||||
method_inner,
|
||||
method_proto,
|
||||
Input,
|
||||
Output,
|
||||
..
|
||||
}| {
|
||||
quote! {
|
||||
async fn #method_inner(handler: H, ctrl: H::Controller, input: #Input) -> #namespace::error::Result<#Output> {
|
||||
#namespace::__rt::call_method(handler, ctrl, #ServiceMethodDescriptor::#method_proto, input).await
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
impl<H> #ServiceClient<H> where H: #namespace::handler::Handler<Descriptor = #ServiceDescriptor> {
|
||||
#(#methods)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_Service_for_ServiceClient(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
Service,
|
||||
ServiceClient,
|
||||
ServiceDescriptor,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let methods = methods.iter().map(
|
||||
|Method {
|
||||
method,
|
||||
method_inner,
|
||||
Input,
|
||||
Output,
|
||||
..
|
||||
}| {
|
||||
quote! {
|
||||
async fn #method(&self, ctrl: H::Controller, input: #Input) -> #namespace::error::Result<#Output> {
|
||||
#ServiceClient::#method_inner(self.0.clone(), ctrl, input).await
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
#[async_trait::async_trait]
|
||||
impl<H> #Service for #ServiceClient<H> where H: #namespace::handler::Handler<Descriptor = #ServiceDescriptor> {
|
||||
type Controller = H::Controller;
|
||||
|
||||
#(#methods)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn struct_ServiceClientFactory(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
Service,
|
||||
ServiceClient,
|
||||
ServiceClientFactory,
|
||||
ServiceDescriptor,
|
||||
..
|
||||
} = self;
|
||||
|
||||
quote! {
|
||||
pub struct #ServiceClientFactory<C: #namespace::controller::Controller>(std::marker::PhantomData<C>);
|
||||
|
||||
impl<C: #namespace::controller::Controller> Clone for #ServiceClientFactory<C> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(std::marker::PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> #namespace::__rt::RpcClientFactory for #ServiceClientFactory<C> where C: #namespace::controller::Controller {
|
||||
type Descriptor = #ServiceDescriptor;
|
||||
type ClientImpl = Box<dyn #Service<Controller = C> + Send + Sync + 'static>;
|
||||
type Controller = C;
|
||||
|
||||
fn new(handler: impl #namespace::handler::Handler<Descriptor = Self::Descriptor, Controller = Self::Controller>) -> Self::ClientImpl {
|
||||
Box::new(#ServiceClient::new(handler))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn struct_ServiceServer(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
Service,
|
||||
ServiceDescriptor,
|
||||
ServiceServer,
|
||||
ServiceMethodDescriptor,
|
||||
Service_str,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let doc = formatdoc! {"
|
||||
A server for a `{Service_str}`.
|
||||
|
||||
This implements the `Server` trait by handling requests and dispatch them to methods on the
|
||||
supplied `{Service_str}`.
|
||||
"};
|
||||
|
||||
let arms = methods.iter().map(
|
||||
|Method {
|
||||
method_proto,
|
||||
method,
|
||||
Input,
|
||||
..
|
||||
}| {
|
||||
quote! {
|
||||
#ServiceMethodDescriptor::#method_proto => {
|
||||
let decoded: #Input = #namespace::__rt::decode(input)?;
|
||||
let ret = service.#method(ctrl, decoded).await?;
|
||||
#namespace::__rt::encode(ret)
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
#[doc = #doc]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct #ServiceServer<A>(A) where A: #Service + Clone + Send + 'static;
|
||||
|
||||
impl<T> #ServiceServer<::std::sync::Weak<T>>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
::std::sync::Arc<T>: #Service,
|
||||
{
|
||||
pub fn new_arc(service: ::std::sync::Arc<T>) -> #ServiceServer<::std::sync::Weak<T>> {
|
||||
#ServiceServer(::std::sync::Arc::downgrade(&service))
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> #ServiceServer<A> where A: #Service + Clone + Send + 'static {
|
||||
/// Creates a new server instance that dispatches all calls to the supplied service.
|
||||
pub fn new(service: A) -> #ServiceServer<A> {
|
||||
#ServiceServer(service)
|
||||
}
|
||||
|
||||
async fn call_inner(
|
||||
service: A,
|
||||
method: #ServiceMethodDescriptor,
|
||||
ctrl: A::Controller,
|
||||
input: ::bytes::Bytes)
|
||||
-> #namespace::error::Result<::bytes::Bytes> {
|
||||
match method {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<A> #namespace::handler::Handler for #ServiceServer<A>
|
||||
where
|
||||
A: #Service + Clone + Send + Sync + 'static {
|
||||
type Descriptor = #ServiceDescriptor;
|
||||
type Controller = A::Controller;
|
||||
|
||||
async fn call(
|
||||
&self,
|
||||
ctrl: A::Controller,
|
||||
method: #ServiceMethodDescriptor,
|
||||
input: ::bytes::Bytes)
|
||||
-> #namespace::error::Result<::bytes::Bytes> {
|
||||
#ServiceServer::call_inner(self.0.clone(), method, ctrl, input).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The service generator to be used with `prost-build` to generate RPC implementations for
|
||||
/// `prost-simple-rpc`.
|
||||
///
|
||||
/// See the crate-level documentation for more info.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ServiceGenerator;
|
||||
|
||||
impl prost_build::ServiceGenerator for ServiceGenerator {
|
||||
fn generate(&mut self, service: prost_build::Service, buf: &mut String) {
|
||||
let info = Service::new(service);
|
||||
|
||||
let trait_Service = info.trait_Service();
|
||||
let impl_Service_for_Weak = info.impl_Service_for_Weak();
|
||||
let struct_ServiceDescriptor = info.struct_ServiceDescriptor();
|
||||
let enum_ServiceMethodDescriptor = info.enum_ServiceMethodDescriptor();
|
||||
let struct_ServiceClient = info.struct_ServiceClient();
|
||||
let struct_ServiceClientFactory = info.struct_ServiceClientFactory();
|
||||
let struct_ServiceServer = info.struct_ServiceServer();
|
||||
|
||||
let tokens = quote! {
|
||||
#trait_Service
|
||||
|
||||
#impl_Service_for_Weak
|
||||
|
||||
#struct_ServiceDescriptor
|
||||
|
||||
#enum_ServiceMethodDescriptor
|
||||
|
||||
#struct_ServiceClient
|
||||
|
||||
#struct_ServiceClientFactory
|
||||
|
||||
#struct_ServiceServer
|
||||
};
|
||||
|
||||
buf.push('\n');
|
||||
buf.push_str(&tokens.to_string());
|
||||
buf.push('\n');
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,9 @@ core_clap:
|
||||
仅用户名:--config-server admin,将使用官方的服务器
|
||||
machine_id:
|
||||
en: |+
|
||||
the machine id to identify this machine, used for config recovery after disconnection, must be unique and fixed. default is from system.
|
||||
the machine id to identify this machine, used for config recovery after disconnection, must be unique and fixed. by default it is loaded from persisted local state; on first start it may be migrated from system information or generated, then remains fixed.
|
||||
zh-CN: |+
|
||||
Web 配置服务器通过 machine id 来识别机器,用于断线重连后的配置恢复,需要保证唯一且固定不变。默认从系统获得。
|
||||
Web 配置服务器通过 machine id 来识别机器,用于断线重连后的配置恢复,需要保证唯一且固定不变。默认从本地持久化状态读取;首次启动时可能基于系统信息迁移或生成,之后保持固定不变。
|
||||
config_file:
|
||||
en: "path to the config file, NOTE: the options set by cmdline args will override options in config file"
|
||||
zh-CN: "配置文件路径,注意:命令行中的配置的选项会覆盖配置文件中的选项"
|
||||
@@ -184,6 +184,9 @@ core_clap:
|
||||
disable_upnp:
|
||||
en: "disable runtime UPnP/NAT-PMP port mapping for eligible listeners; automatic port mapping is enabled by default"
|
||||
zh-CN: "禁用符合条件监听器的运行时 UPnP/NAT-PMP 端口映射;自动端口映射默认开启"
|
||||
enable_udp_broadcast_relay:
|
||||
en: "Windows only: capture local UDP broadcast packets from physical interfaces and forward them to EasyTier peers. Helps games to find rooms in local network. Requires administrator privileges."
|
||||
zh-CN: "仅 Windows:捕获物理网卡上的本机 UDP 广播包并转发给 EasyTier 对等节点,帮助局域网游戏发现房间。需要管理员权限。"
|
||||
relay_all_peer_rpc:
|
||||
en: "relay all peer rpc packets, even if the peer is not in the relay network whitelist. this can help peers not in relay network whitelist to establish p2p connection."
|
||||
zh-CN: "转发所有对等节点的RPC数据包,即使对等节点不在转发网络白名单中。这可以帮助白名单外网络中的对等节点建立P2P连接。"
|
||||
@@ -217,12 +220,6 @@ core_clap:
|
||||
port_forward:
|
||||
en: "forward local port to remote port in virtual network. e.g.: udp://0.0.0.0:12345/10.126.126.1:23456, means forward local udp port 12345 to 10.126.126.1:23456 in the virtual network. can specify multiple."
|
||||
zh-CN: "将本地端口转发到虚拟网络中的远程端口。例如:udp://0.0.0.0:12345/10.126.126.1:23456,表示将本地UDP端口12345转发到虚拟网络中的10.126.126.1:23456。可以指定多个。"
|
||||
accept_dns:
|
||||
en: "if true, enable magic dns. with magic dns, you can access other nodes with a domain name, e.g.: <hostname>.et.net. magic dns will modify your system dns settings, enable it carefully."
|
||||
zh-CN: "如果为true,则启用魔法DNS。使用魔法DNS,您可以使用域名访问其他节点,例如:<hostname>.et.net。魔法DNS将修改您的系统DNS设置,请谨慎启用。"
|
||||
tld_dns_zone:
|
||||
en: "specify the top-level domain zone for magic DNS. if not provided, defaults to the value from dns_server module (et.net.). only used when accept_dns is true."
|
||||
zh-CN: "指定魔法DNS的顶级域名区域。如果未提供,默认使用dns_server模块中的值(et.net.)。仅在accept_dns为true时使用。"
|
||||
private_mode:
|
||||
en: "if true, foreign networks are only allowed when this node can verify they use the same network secret, or when a foreign credential node is already trusted via admin-issued credential propagation; different or missing secrets are otherwise rejected."
|
||||
zh-CN: "如果为true,则仅允许两类 foreign network 接入:本节点能验证其使用相同 network secret 的节点,或已通过 foreign network 管理节点传播而被信任的 credential 节点;否则 secret 不同或缺失时会被拒绝。"
|
||||
@@ -274,6 +271,9 @@ core_clap:
|
||||
check_config:
|
||||
en: Check config validity without starting the network
|
||||
zh-CN: 检查配置文件的有效性并退出
|
||||
daemon:
|
||||
en: Run in daemon mode
|
||||
zh-CN: 以守护进程模式运行
|
||||
file_log_size_mb:
|
||||
en: "per file log size in MB, default is 100MB"
|
||||
zh-CN: "单个文件日志大小,单位 MB,默认值为 100MB"
|
||||
|
||||
@@ -11,9 +11,8 @@ use windows::{
|
||||
NET_FW_RULE_DIR_OUT,
|
||||
},
|
||||
Networking::WinSock::{
|
||||
IP_UNICAST_IF, IPPROTO_IP, IPPROTO_IPV6, IPV6_UNICAST_IF, SIO_UDP_CONNRESET,
|
||||
SO_EXCLUSIVEADDRUSE, SOCKET, SOCKET_ERROR, SOL_SOCKET, WSAGetLastError, WSAIoctl,
|
||||
htonl, setsockopt,
|
||||
IP_UNICAST_IF, IPPROTO_IP, IPPROTO_IPV6, IPV6_UNICAST_IF, SIO_UDP_CONNRESET, SOCKET,
|
||||
SOCKET_ERROR, WSAGetLastError, WSAIoctl, htonl, setsockopt,
|
||||
},
|
||||
System::Com::{
|
||||
CLSCTX_ALL, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx, CoUninitialize,
|
||||
|
||||
+219
-45
@@ -1,36 +1,114 @@
|
||||
use std::{
|
||||
hash::Hasher,
|
||||
net::{IpAddr, SocketAddr},
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use base64::{Engine as _, prelude::BASE64_STANDARD};
|
||||
use clap::ValueEnum;
|
||||
use clap::builder::PossibleValue;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumString, VariantArray};
|
||||
use tokio::io::AsyncReadExt as _;
|
||||
|
||||
use super::env_parser;
|
||||
use crate::utils::dns;
|
||||
use crate::{
|
||||
common::stun::StunInfoCollector,
|
||||
instance::dns_server::DEFAULT_ET_DNS_ZONE,
|
||||
proto::{
|
||||
acl::Acl,
|
||||
api::manage::ConfigSource as RpcConfigSource,
|
||||
common::{CompressionAlgoPb, PortForwardConfigPb, SecureModeConfig, SocketType},
|
||||
},
|
||||
tunnel::{IpScheme, TunnelScheme, generate_digest_from_str},
|
||||
utils,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use base64::{Engine as _, prelude::BASE64_STANDARD};
|
||||
use bon::Builder;
|
||||
use clap::ValueEnum;
|
||||
use clap::builder::PossibleValue;
|
||||
use derivative::Derivative;
|
||||
use derive_more::{Constructor, Deref};
|
||||
use getset::Getters;
|
||||
use optionize::Optionized;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::{
|
||||
hash::Hasher,
|
||||
net::{IpAddr, SocketAddr},
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use strum::{Display, EnumString, VariantArray};
|
||||
use tokio::io::AsyncReadExt as _;
|
||||
|
||||
use super::env_parser;
|
||||
#[derive(Derivative, Debug, Clone, Constructor, Getters, Deref, Deserialize)]
|
||||
#[derivative(PartialEq(bound = "Parsed: PartialEq"))]
|
||||
#[serde(try_from = "Raw")]
|
||||
#[serde(
|
||||
bound = "Raw: Deserialize<'de>, <ConfigBase<Raw, Parsed, Data> as TryFrom<Raw>>::Error: Display"
|
||||
)]
|
||||
pub struct ConfigBase<Raw, Parsed, Data = ()>
|
||||
where
|
||||
Raw: Optionized<Subject = Parsed>,
|
||||
ConfigBase<Raw, Parsed, Data>: TryFrom<Raw>,
|
||||
{
|
||||
#[deref]
|
||||
parsed: Parsed,
|
||||
#[getset(get)]
|
||||
#[derivative(PartialEq = "ignore")]
|
||||
raw: Raw,
|
||||
#[getset(get)]
|
||||
#[derivative(PartialEq = "ignore")]
|
||||
data: Data,
|
||||
}
|
||||
|
||||
impl<Raw, Parsed, Data> Serialize for ConfigBase<Raw, Parsed, Data>
|
||||
where
|
||||
Raw: Optionized<Subject = Parsed> + Serialize,
|
||||
ConfigBase<Raw, Parsed, Data>: TryFrom<Raw, Error: Debug>,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.raw.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Raw, Parsed, Data> Default for ConfigBase<Raw, Parsed, Data>
|
||||
where
|
||||
Raw: Optionized<Subject = Parsed> + Default,
|
||||
ConfigBase<Raw, Parsed, Data>: TryFrom<Raw, Error: Debug>,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Raw::default().try_into().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Raw, Parsed, Data> ConfigBase<Raw, Parsed, Data>
|
||||
where
|
||||
Raw: Optionized<Subject = Parsed>,
|
||||
ConfigBase<Raw, Parsed, Data>: TryFrom<Raw, Error: Debug>,
|
||||
{
|
||||
pub fn into_parsed(self) -> Parsed {
|
||||
self.parsed
|
||||
}
|
||||
|
||||
pub fn into_raw(self) -> Raw {
|
||||
self.raw
|
||||
}
|
||||
|
||||
pub fn into_data(self) -> Data {
|
||||
self.data
|
||||
}
|
||||
|
||||
pub fn update(self, config: Raw) -> Result<Self, <Self as TryFrom<Raw>>::Error> {
|
||||
let mut raw = self.into_raw();
|
||||
raw.merge(config);
|
||||
raw.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
pub type Flags = crate::proto::common::FlagsInConfig;
|
||||
|
||||
pub fn gen_default_flags() -> Flags {
|
||||
#[allow(deprecated)]
|
||||
Flags {
|
||||
#[allow(deprecated)]
|
||||
quic_listen_port: u32::MAX,
|
||||
#[allow(deprecated)]
|
||||
accept_dns: false,
|
||||
#[allow(deprecated)]
|
||||
tld_dns_zone: "".to_string(),
|
||||
|
||||
default_protocol: "tcp".to_string(),
|
||||
dev_name: "".to_string(),
|
||||
enable_encryption: true,
|
||||
@@ -55,7 +133,6 @@ pub fn gen_default_flags() -> Flags {
|
||||
disable_kcp_input: false,
|
||||
disable_relay_kcp: false,
|
||||
enable_relay_foreign_network_kcp: false,
|
||||
accept_dns: false,
|
||||
private_mode: false,
|
||||
enable_quic_proxy: false,
|
||||
disable_quic_input: false,
|
||||
@@ -65,13 +142,11 @@ pub fn gen_default_flags() -> Flags {
|
||||
multi_thread_count: 2,
|
||||
encryption_algorithm: EncryptionAlgorithm::default().to_string(),
|
||||
disable_sym_hole_punching: false,
|
||||
tld_dns_zone: DEFAULT_ET_DNS_ZONE.to_string(),
|
||||
|
||||
quic_listen_port: u32::MAX,
|
||||
need_p2p: false,
|
||||
instance_recv_bps_limit: u64::MAX,
|
||||
disable_upnp: false,
|
||||
disable_relay_data: false,
|
||||
enable_udp_broadcast_relay: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,8 +226,19 @@ impl Default for EncryptionAlgorithm {
|
||||
}
|
||||
}
|
||||
|
||||
cfg_select! {
|
||||
feature = "magic-dns" => {
|
||||
use crate::dns::config::{DnsConfig, DnsConfigLoaderExt};
|
||||
}
|
||||
|
||||
_ => {
|
||||
#[auto_impl::auto_impl(Box, &)]
|
||||
pub trait DnsConfigLoaderExt {}
|
||||
}
|
||||
}
|
||||
|
||||
#[auto_impl::auto_impl(Box, &)]
|
||||
pub trait ConfigLoader: Send + Sync {
|
||||
pub trait ConfigLoader: Send + Sync + DnsConfigLoaderExt {
|
||||
fn get_id(&self) -> uuid::Uuid;
|
||||
fn set_id(&self, id: uuid::Uuid);
|
||||
|
||||
@@ -239,6 +325,14 @@ pub trait ConfigLoader: Send + Sync {
|
||||
fn get_stun_servers_v6(&self) -> Option<Vec<String>>;
|
||||
fn set_stun_servers_v6(&self, servers: Option<Vec<String>>);
|
||||
|
||||
fn get_dns_resolvers(&self) -> Vec<String> {
|
||||
dns::get_default_dns_resolvers()
|
||||
}
|
||||
fn get_dns_resolvers_config(&self) -> Option<Vec<String>> {
|
||||
None
|
||||
}
|
||||
fn set_dns_resolvers(&self, _resolvers: Option<Vec<String>>) {}
|
||||
|
||||
fn get_secure_mode(&self) -> Option<SecureModeConfig>;
|
||||
fn set_secure_mode(&self, secure_mode: Option<SecureModeConfig>);
|
||||
|
||||
@@ -426,11 +520,11 @@ pub struct ConsoleLoggerConfig {
|
||||
pub level: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, derive_builder::Builder)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Builder)]
|
||||
pub struct LoggingConfig {
|
||||
#[builder(setter(into, strip_option), default = None)]
|
||||
#[builder(into)]
|
||||
pub file_logger: Option<FileLoggerConfig>,
|
||||
#[builder(setter(into, strip_option), default = None)]
|
||||
#[builder(into)]
|
||||
pub console_logger: Option<ConsoleLoggerConfig>,
|
||||
}
|
||||
|
||||
@@ -541,6 +635,10 @@ struct Config {
|
||||
peer: Option<Vec<PeerConfig>>,
|
||||
proxy_network: Option<Vec<ProxyNetworkConfig>>,
|
||||
|
||||
#[cfg(feature = "magic-dns")]
|
||||
#[serde(default)]
|
||||
dns: DnsConfig,
|
||||
|
||||
vpn_portal_config: Option<VpnPortalConfig>,
|
||||
|
||||
routes: Option<Vec<cidr::Ipv4Cidr>>,
|
||||
@@ -562,6 +660,7 @@ struct Config {
|
||||
udp_whitelist: Option<Vec<String>>,
|
||||
stun_servers: Option<Vec<String>>,
|
||||
stun_servers_v6: Option<Vec<String>>,
|
||||
dns_resolvers: Option<Vec<String>>,
|
||||
|
||||
credential_file: Option<PathBuf>,
|
||||
source: Option<ConfigSourceConfig>,
|
||||
@@ -595,6 +694,10 @@ impl TomlConfigLoader {
|
||||
Self::normalize_config_source(&mut config);
|
||||
|
||||
config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default()));
|
||||
if let Some(dns_resolvers) = &config.dns_resolvers {
|
||||
dns::validate_dns_resolvers(dns_resolvers)
|
||||
.with_context(|| "invalid dns_resolvers config")?;
|
||||
}
|
||||
|
||||
let config = TomlConfigLoader {
|
||||
config: Arc::new(Mutex::new(config)),
|
||||
@@ -636,6 +739,21 @@ impl TomlConfigLoader {
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsConfigLoaderExt for TomlConfigLoader {
|
||||
cfg_select! {
|
||||
feature = "magic-dns" => {
|
||||
fn get_dns(&self) -> DnsConfig {
|
||||
self.config.lock().unwrap().dns.clone()
|
||||
}
|
||||
fn set_dns(&self, config: DnsConfig) {
|
||||
self.config.lock().unwrap().dns = config;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigLoader for TomlConfigLoader {
|
||||
fn get_inst_name(&self) -> String {
|
||||
self.config
|
||||
@@ -651,26 +769,17 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
}
|
||||
|
||||
fn get_hostname(&self) -> String {
|
||||
let hostname = self.config.lock().unwrap().hostname.clone();
|
||||
let hostname = self
|
||||
.config
|
||||
.lock()
|
||||
.unwrap()
|
||||
.hostname
|
||||
.as_ref()
|
||||
.map(|hostname| dns::sanitize(hostname))
|
||||
.filter(|h| !h.is_empty());
|
||||
|
||||
match hostname {
|
||||
Some(hostname) => {
|
||||
let hostname = hostname
|
||||
.chars()
|
||||
.filter(|c| !c.is_control())
|
||||
.take(32)
|
||||
.collect::<String>();
|
||||
|
||||
if !hostname.is_empty() {
|
||||
self.set_hostname(Some(hostname.clone()));
|
||||
hostname
|
||||
} else {
|
||||
self.set_hostname(None);
|
||||
gethostname::gethostname().to_string_lossy().to_string()
|
||||
}
|
||||
}
|
||||
None => gethostname::gethostname().to_string_lossy().to_string(),
|
||||
}
|
||||
self.set_hostname(hostname.clone());
|
||||
hostname.unwrap_or_else(|| utils::dns::sanitize(utils::hostname()))
|
||||
}
|
||||
|
||||
fn set_hostname(&self, name: Option<String>) {
|
||||
@@ -998,6 +1107,23 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
self.config.lock().unwrap().stun_servers_v6 = servers;
|
||||
}
|
||||
|
||||
fn get_dns_resolvers(&self) -> Vec<String> {
|
||||
self.config
|
||||
.lock()
|
||||
.unwrap()
|
||||
.dns_resolvers
|
||||
.clone()
|
||||
.unwrap_or_else(dns::get_default_dns_resolvers)
|
||||
}
|
||||
|
||||
fn get_dns_resolvers_config(&self) -> Option<Vec<String>> {
|
||||
self.config.lock().unwrap().dns_resolvers.clone()
|
||||
}
|
||||
|
||||
fn set_dns_resolvers(&self, resolvers: Option<Vec<String>>) {
|
||||
self.config.lock().unwrap().dns_resolvers = resolvers;
|
||||
}
|
||||
|
||||
fn get_secure_mode(&self) -> Option<SecureModeConfig> {
|
||||
self.config.lock().unwrap().secure_mode.clone()
|
||||
}
|
||||
@@ -1060,6 +1186,9 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
if config.stun_servers_v6 == Some(StunInfoCollector::get_default_servers_v6()) {
|
||||
config.stun_servers_v6 = None;
|
||||
}
|
||||
if config.dns_resolvers == Some(dns::get_default_dns_resolvers()) {
|
||||
config.dns_resolvers = None;
|
||||
}
|
||||
toml::to_string_pretty(&config).unwrap()
|
||||
}
|
||||
}
|
||||
@@ -1291,6 +1420,51 @@ stun_servers = [
|
||||
assert_eq!(stun_servers[2], "txt:stun.easytier.cn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dns_resolvers_default_and_roundtrip() {
|
||||
let config = TomlConfigLoader::default();
|
||||
assert_eq!(config.get_dns_resolvers_config(), None);
|
||||
assert_eq!(config.get_dns_resolvers(), vec!["system".to_string()]);
|
||||
assert!(!config.dump().contains("dns_resolvers"));
|
||||
|
||||
let config = TomlConfigLoader::new_from_str(
|
||||
r#"
|
||||
dns_resolvers = ["system", "https://dns.alidns.com/dns-query"]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
config.get_dns_resolvers_config().unwrap(),
|
||||
vec![
|
||||
"system".to_string(),
|
||||
"https://dns.alidns.com/dns-query".to_string()
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
config.get_dns_resolvers(),
|
||||
vec![
|
||||
"system".to_string(),
|
||||
"https://dns.alidns.com/dns-query".to_string()
|
||||
]
|
||||
);
|
||||
|
||||
let dumped = config.dump();
|
||||
assert!(dumped.contains("dns_resolvers"));
|
||||
let loaded = TomlConfigLoader::new_from_str(&dumped).unwrap();
|
||||
assert_eq!(loaded.get_dns_resolvers(), config.get_dns_resolvers());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dns_resolvers_reject_unknown_doh_without_bootstrap() {
|
||||
let err = TomlConfigLoader::new_from_str(
|
||||
r#"
|
||||
dns_resolvers = ["https://example.com/dns-query"]
|
||||
"#,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("invalid dns_resolvers"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_config_source_toml_roundtrip() {
|
||||
let config = TomlConfigLoader::default();
|
||||
|
||||
@@ -23,8 +23,6 @@ define_global_var!(MANUAL_CONNECTOR_RECONNECT_INTERVAL_MS, u64, 1000);
|
||||
|
||||
define_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, u64, 10);
|
||||
|
||||
define_global_var!(MACHINE_UID, Option<String>, None);
|
||||
|
||||
define_global_var!(MAX_DIRECT_CONNS_PER_PEER_IN_FOREIGN_NETWORK, u32, 3);
|
||||
|
||||
define_global_var!(DIRECT_CONNECT_TO_PUBLIC_SERVER, bool, true);
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::Context;
|
||||
use hickory_proto::runtime::TokioRuntimeProvider;
|
||||
use hickory_proto::xfer::Protocol;
|
||||
use hickory_resolver::config::{LookupIpStrategy, NameServerConfig, ResolverConfig, ResolverOpts};
|
||||
use hickory_resolver::name_server::{GenericConnector, TokioConnectionProvider};
|
||||
use hickory_resolver::system_conf::read_system_conf;
|
||||
use hickory_resolver::{Resolver, TokioResolver};
|
||||
use once_cell::sync::Lazy;
|
||||
use tokio::net::lookup_host;
|
||||
|
||||
use super::error::Error;
|
||||
|
||||
pub fn get_default_resolver_config() -> ResolverConfig {
|
||||
let mut default_resolve_config = ResolverConfig::new();
|
||||
default_resolve_config.add_name_server(NameServerConfig::new(
|
||||
"223.5.5.5:53".parse().unwrap(),
|
||||
Protocol::Udp,
|
||||
));
|
||||
default_resolve_config.add_name_server(NameServerConfig::new(
|
||||
"180.184.1.1:53".parse().unwrap(),
|
||||
Protocol::Udp,
|
||||
));
|
||||
default_resolve_config
|
||||
}
|
||||
|
||||
pub static ALLOW_USE_SYSTEM_DNS_RESOLVER: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(true));
|
||||
|
||||
pub static RESOLVER: Lazy<Arc<Resolver<GenericConnector<TokioRuntimeProvider>>>> =
|
||||
Lazy::new(|| {
|
||||
let system_cfg = read_system_conf();
|
||||
let mut cfg = get_default_resolver_config();
|
||||
let mut opt = ResolverOpts::default();
|
||||
if let Ok(s) = system_cfg {
|
||||
for ns in s.0.name_servers() {
|
||||
cfg.add_name_server(ns.clone());
|
||||
}
|
||||
opt = s.1;
|
||||
}
|
||||
opt.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
|
||||
let builder = TokioResolver::builder_with_config(cfg, TokioConnectionProvider::default())
|
||||
.with_options(opt);
|
||||
Arc::new(builder.build())
|
||||
});
|
||||
|
||||
pub async fn resolve_txt_record(domain_name: &str) -> Result<String, Error> {
|
||||
let r = RESOLVER.clone();
|
||||
let response = r
|
||||
.txt_lookup(domain_name)
|
||||
.await
|
||||
.with_context(|| format!("txt_lookup failed, domain_name: {}", domain_name))?;
|
||||
|
||||
let txt_record = response
|
||||
.iter()
|
||||
.next()
|
||||
.with_context(|| format!("no txt record found, domain_name: {}", domain_name))?;
|
||||
|
||||
let txt_data = String::from_utf8_lossy(&txt_record.txt_data()[0]);
|
||||
tracing::info!(?txt_data, ?domain_name, "get txt record");
|
||||
|
||||
Ok(txt_data.to_string())
|
||||
}
|
||||
|
||||
pub async fn socket_addrs(
|
||||
url: &url::Url,
|
||||
default_port_number: impl Fn() -> Option<u16>,
|
||||
) -> Result<Vec<SocketAddr>, Error> {
|
||||
let host = url.host().ok_or(Error::InvalidUrl(url.to_string()))?;
|
||||
let port = url
|
||||
.port()
|
||||
.or_else(default_port_number)
|
||||
.ok_or(Error::InvalidUrl(url.to_string()))?;
|
||||
|
||||
// if host is an ip address, return it directly
|
||||
match host {
|
||||
url::Host::Ipv4(ip) => return Ok(vec![SocketAddr::new(std::net::IpAddr::V4(ip), port)]),
|
||||
url::Host::Ipv6(ip) => return Ok(vec![SocketAddr::new(std::net::IpAddr::V6(ip), port)]),
|
||||
_ => {}
|
||||
}
|
||||
let host = host.to_string();
|
||||
|
||||
if ALLOW_USE_SYSTEM_DNS_RESOLVER.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
let socket_addr = format!("{}:{}", host, port);
|
||||
match lookup_host(socket_addr).await {
|
||||
Ok(a) => {
|
||||
let a = a.collect();
|
||||
tracing::debug!(?a, "system dns lookup done");
|
||||
return Ok(a);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(?e, "system dns lookup failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// use hickory_resolver
|
||||
let ret = RESOLVER.lookup_ip(&host).await.with_context(|| {
|
||||
format!(
|
||||
"hickory dns lookup_ip failed, host: {}, port: {}",
|
||||
host, port
|
||||
)
|
||||
})?;
|
||||
Ok(ret
|
||||
.iter()
|
||||
.map(|ip| SocketAddr::new(ip, port))
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use guarden::defer;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_socket_addrs() {
|
||||
let url = url::Url::parse("tcp://github-ci-test.easytier.cn:80").unwrap();
|
||||
let addrs = socket_addrs(&url, || Some(80)).await.unwrap();
|
||||
assert_eq!(2, addrs.len(), "addrs: {:?}", addrs);
|
||||
println!("addrs: {:?}", addrs);
|
||||
|
||||
ALLOW_USE_SYSTEM_DNS_RESOLVER.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
defer!(
|
||||
ALLOW_USE_SYSTEM_DNS_RESOLVER.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
);
|
||||
let addrs = socket_addrs(&url, || Some(80)).await.unwrap();
|
||||
assert_eq!(2, addrs.len(), "addrs: {:?}", addrs);
|
||||
println!("addrs2: {:?}", addrs);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn socket_addrs_preserves_explicit_zero_port() {
|
||||
let cases = [
|
||||
("ws://127.0.0.1:0", 80, 0),
|
||||
("wss://127.0.0.1:0", 443, 0),
|
||||
("ws://127.0.0.1", 80, 80),
|
||||
("wss://127.0.0.1", 443, 443),
|
||||
];
|
||||
|
||||
for (raw_url, default_port, expected_port) in cases {
|
||||
let url = url::Url::parse(raw_url).unwrap();
|
||||
let addrs = socket_addrs(&url, || Some(default_port)).await.unwrap();
|
||||
assert_eq!(
|
||||
addrs,
|
||||
vec![SocketAddr::from(([127, 0, 0, 1], expected_port))]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::{io, result};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::tunnel;
|
||||
@@ -55,4 +54,6 @@ pub enum Error {
|
||||
|
||||
pub type Result<T> = result::Result<T, Error>;
|
||||
|
||||
pub type ErrorCollection = crate::utils::error::ErrorCollection<Error>;
|
||||
|
||||
// impl From for std::
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
use arc_swap::ArcSwap;
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use dashmap::DashMap;
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
use socket2::Protocol;
|
||||
use std::{
|
||||
collections::{BTreeSet, HashMap, hash_map::DefaultHasher},
|
||||
hash::Hasher,
|
||||
iter,
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::{Arc, Mutex},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use dashmap::DashMap;
|
||||
|
||||
use super::{
|
||||
PeerId,
|
||||
config::{ConfigLoader, Flags},
|
||||
@@ -31,10 +35,11 @@ use crate::{
|
||||
rpc_service::protected_port,
|
||||
tunnel::matches_protocol,
|
||||
};
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
use socket2::Protocol;
|
||||
#[cfg(feature = "magic-dns")]
|
||||
use crate::{
|
||||
dns::config::{DnsConfigLoaderExt, DnsExportConfig, DnsGlobalCtxExt, zone::ZoneConfig},
|
||||
utils::dns,
|
||||
};
|
||||
|
||||
pub type NetworkIdentity = crate::common::config::NetworkIdentity;
|
||||
|
||||
@@ -48,6 +53,8 @@ pub enum GlobalCtxEvent {
|
||||
PeerConnAdded(PeerConnInfo),
|
||||
PeerConnRemoved(PeerConnInfo),
|
||||
|
||||
PeerInfoUpdated(Vec<PeerId>),
|
||||
|
||||
ListenerAdded(url::Url),
|
||||
ListenerAddFailed(url::Url, String), // (url, error message)
|
||||
ListenerAcceptFailed(url::Url, String), // (url, error message)
|
||||
@@ -77,6 +84,11 @@ pub enum GlobalCtxEvent {
|
||||
|
||||
ProxyCidrsUpdated(Vec<cidr::Ipv4Cidr>, Vec<cidr::Ipv4Cidr>), // (added, removed)
|
||||
|
||||
UdpBroadcastRelayStartResult {
|
||||
capture_backend: Option<String>,
|
||||
error: Option<String>,
|
||||
},
|
||||
|
||||
CredentialChanged,
|
||||
}
|
||||
|
||||
@@ -250,7 +262,7 @@ impl std::fmt::Debug for GlobalCtx {
|
||||
}
|
||||
}
|
||||
|
||||
pub type ArcGlobalCtx = std::sync::Arc<GlobalCtx>;
|
||||
pub type ArcGlobalCtx = Arc<GlobalCtx>;
|
||||
|
||||
impl GlobalCtx {
|
||||
fn apply_disable_relay_data_flag(
|
||||
@@ -282,6 +294,12 @@ impl GlobalCtx {
|
||||
|
||||
let (event_bus, _) = tokio::sync::broadcast::channel(16);
|
||||
|
||||
if let Some(dns_resolvers) = config_fs.get_dns_resolvers_config()
|
||||
&& let Err(e) = crate::utils::dns::set_dns_resolvers(dns_resolvers)
|
||||
{
|
||||
crate::common::log::warn!("failed to set dns resolvers: {:?}", e);
|
||||
}
|
||||
|
||||
let stun_info_collector = StunInfoCollector::new_with_default_servers();
|
||||
|
||||
if let Some(stun_servers) = config_fs.get_stun_servers() {
|
||||
@@ -486,7 +504,7 @@ impl GlobalCtx {
|
||||
}
|
||||
|
||||
pub fn get_hostname(&self) -> String {
|
||||
return self.hostname.lock().unwrap().clone();
|
||||
self.hostname.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn set_hostname(&self, hostname: String) {
|
||||
@@ -781,6 +799,39 @@ impl GlobalCtx {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "magic-dns")]
|
||||
impl DnsGlobalCtxExt for GlobalCtx {
|
||||
fn dns_self_zone(&self) -> ZoneConfig {
|
||||
use hickory_proto::rr::Name;
|
||||
let dns = self.config.get_dns();
|
||||
let name: Name = dns
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| dns::parse(self.get_hostname()))
|
||||
.into();
|
||||
let fqdn = name.append_domain(&dns.domain).unwrap_or_default().into();
|
||||
let ipv4 = self.get_ipv4().map(|ip| ip.address());
|
||||
let ipv6 = self.get_ipv6().map(|ip| ip.address());
|
||||
let ipv6 = ipv6.map(|a| vec![a]).unwrap_or_default();
|
||||
|
||||
ZoneConfig::dedicated(fqdn, ipv4, ipv6)
|
||||
}
|
||||
|
||||
fn dns_export_config(&self) -> DnsExportConfig {
|
||||
DnsExportConfig {
|
||||
zones: self
|
||||
.dns_iter_zones()
|
||||
.filter(|z| z.policy.export.as_ref().is_some_and(|f| !f.disabled)) // TODO: check policies of parent zones
|
||||
.map(ZoneConfig::into_data)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dns_iter_zones(&self) -> impl Iterator<Item = ZoneConfig> {
|
||||
iter::once(self.dns_self_zone()).chain(self.config.get_dns().into_parsed().zones)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
|
||||
@@ -0,0 +1,594 @@
|
||||
use std::{
|
||||
env,
|
||||
ffi::OsString,
|
||||
io::Write as _,
|
||||
path::{Path, PathBuf},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::utils::hostname;
|
||||
use anyhow::Context as _;
|
||||
#[cfg(unix)]
|
||||
use nix::{
|
||||
errno::Errno,
|
||||
fcntl::{Flock, FlockArg},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MachineIdOptions {
|
||||
pub explicit_machine_id: Option<String>,
|
||||
pub state_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn resolve_machine_id(opts: &MachineIdOptions) -> anyhow::Result<uuid::Uuid> {
|
||||
if let Some(explicit_machine_id) = opts.explicit_machine_id.as_deref() {
|
||||
return Ok(parse_or_hash_machine_id(explicit_machine_id));
|
||||
}
|
||||
|
||||
let state_file = resolve_machine_id_state_file(opts.state_dir.as_deref())?;
|
||||
let allow_legacy_machine_uid_migration =
|
||||
should_attempt_legacy_machine_uid_migration(&state_file);
|
||||
if let Some(machine_id) = read_state_machine_id(&state_file)? {
|
||||
return Ok(machine_id);
|
||||
}
|
||||
|
||||
if let Some(machine_id) = read_legacy_machine_id_file() {
|
||||
return persist_machine_id(&state_file, machine_id);
|
||||
}
|
||||
|
||||
if allow_legacy_machine_uid_migration
|
||||
&& let Some(machine_id) = resolve_legacy_machine_uid_hash()
|
||||
{
|
||||
return persist_machine_id(&state_file, machine_id);
|
||||
}
|
||||
|
||||
let machine_id = resolve_new_machine_id().unwrap_or_else(uuid::Uuid::new_v4);
|
||||
persist_machine_id(&state_file, machine_id)
|
||||
}
|
||||
|
||||
fn parse_or_hash_machine_id(raw: &str) -> uuid::Uuid {
|
||||
if let Ok(mid) = uuid::Uuid::parse_str(raw.trim()) {
|
||||
return mid;
|
||||
}
|
||||
digest_uuid_from_str(raw)
|
||||
}
|
||||
|
||||
fn digest_uuid_from_str(raw: &str) -> uuid::Uuid {
|
||||
let mut b = [0u8; 16];
|
||||
crate::tunnel::generate_digest_from_str("", raw, &mut b);
|
||||
uuid::Uuid::from_bytes(b)
|
||||
}
|
||||
|
||||
fn resolve_machine_id_state_file(state_dir: Option<&Path>) -> anyhow::Result<PathBuf> {
|
||||
let state_dir = match state_dir {
|
||||
Some(dir) => dir.to_path_buf(),
|
||||
None => default_machine_id_state_dir()?,
|
||||
};
|
||||
Ok(state_dir.join("machine_id"))
|
||||
}
|
||||
|
||||
fn non_empty_os_string(value: Option<OsString>) -> Option<OsString> {
|
||||
value.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn default_linux_machine_id_state_dir(
|
||||
xdg_data_home: Option<OsString>,
|
||||
home: Option<OsString>,
|
||||
) -> PathBuf {
|
||||
if let Some(path) = non_empty_os_string(xdg_data_home) {
|
||||
return PathBuf::from(path).join("easytier");
|
||||
}
|
||||
|
||||
if let Some(home) = non_empty_os_string(home) {
|
||||
return PathBuf::from(home)
|
||||
.join(".local")
|
||||
.join("share")
|
||||
.join("easytier");
|
||||
}
|
||||
|
||||
PathBuf::from("/var/lib/easytier")
|
||||
}
|
||||
|
||||
fn default_machine_id_state_dir() -> anyhow::Result<PathBuf> {
|
||||
cfg_select! {
|
||||
target_os = "linux" => Ok(default_linux_machine_id_state_dir(
|
||||
env::var_os("XDG_DATA_HOME"),
|
||||
env::var_os("HOME"),
|
||||
)),
|
||||
all(target_os = "macos", not(feature = "macos-ne")) => {
|
||||
let home = non_empty_os_string(env::var_os("HOME"))
|
||||
.ok_or_else(|| anyhow::anyhow!("HOME is not set, cannot resolve machine id state directory"))?;
|
||||
Ok(PathBuf::from(home)
|
||||
.join("Library")
|
||||
.join("Application Support")
|
||||
.join("com.easytier"))
|
||||
},
|
||||
target_os = "windows" => {
|
||||
let local_app_data = non_empty_os_string(env::var_os("LOCALAPPDATA")).ok_or_else(|| {
|
||||
anyhow::anyhow!("LOCALAPPDATA is not set, cannot resolve machine id state directory")
|
||||
})?;
|
||||
Ok(PathBuf::from(local_app_data).join("easytier"))
|
||||
},
|
||||
target_os = "freebsd" => {
|
||||
let home = non_empty_os_string(env::var_os("HOME"))
|
||||
.ok_or_else(|| anyhow::anyhow!("HOME is not set, cannot resolve machine id state directory"))?;
|
||||
Ok(PathBuf::from(home).join(".local").join("share").join("easytier"))
|
||||
},
|
||||
target_os = "android" => {
|
||||
anyhow::bail!("machine id state directory must be provided explicitly on Android");
|
||||
},
|
||||
_ => anyhow::bail!("machine id state directory is unsupported on this platform"),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_state_machine_id(path: &Path) -> anyhow::Result<Option<uuid::Uuid>> {
|
||||
let Some(contents) = read_optional_file(path)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let machine_id = uuid::Uuid::parse_str(contents.trim())
|
||||
.with_context(|| format!("invalid machine id in state file {}", path.display()))?;
|
||||
Ok(Some(machine_id))
|
||||
}
|
||||
|
||||
fn read_legacy_machine_id_file() -> Option<uuid::Uuid> {
|
||||
let path = legacy_machine_id_file_path()?;
|
||||
read_legacy_machine_id_file_at(&path)
|
||||
}
|
||||
|
||||
fn read_legacy_machine_id_file_at(path: &Path) -> Option<uuid::Uuid> {
|
||||
let contents = match std::fs::read_to_string(path) {
|
||||
Ok(contents) => contents,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return None,
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
path = %path.display(),
|
||||
%err,
|
||||
"ignoring unreadable legacy machine id file"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
match uuid::Uuid::parse_str(contents.trim()) {
|
||||
Ok(machine_id) => Some(machine_id),
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
path = %path.display(),
|
||||
%err,
|
||||
"ignoring invalid legacy machine id file"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn legacy_machine_id_file_path() -> Option<PathBuf> {
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.map(|path| path.with_file_name("et_machine_id"))
|
||||
}
|
||||
|
||||
fn read_optional_file(path: &Path) -> anyhow::Result<Option<String>> {
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(contents) => Ok(Some(contents)),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(err).with_context(|| format!("failed to read {}", path.display())),
|
||||
}
|
||||
}
|
||||
|
||||
fn should_attempt_legacy_machine_uid_migration(state_file: &Path) -> bool {
|
||||
let Some(state_dir) = state_file.parent() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Ok(mut entries) = std::fs::read_dir(state_dir) else {
|
||||
return false;
|
||||
};
|
||||
entries.any(|entry| entry.is_ok())
|
||||
}
|
||||
|
||||
fn resolve_legacy_machine_uid_hash() -> Option<uuid::Uuid> {
|
||||
machine_uid_seed().map(|seed| digest_uuid_from_str(seed.as_str()))
|
||||
}
|
||||
|
||||
fn resolve_new_machine_id() -> Option<uuid::Uuid> {
|
||||
let seed = machine_uid_seed()?;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let seed = linux_machine_id_seed(&seed);
|
||||
Some(digest_uuid_from_str(&seed))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Some(digest_uuid_from_str(&seed))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
all(target_os = "macos", not(feature = "macos-ne")),
|
||||
target_os = "windows",
|
||||
target_os = "freebsd"
|
||||
))]
|
||||
fn machine_uid_seed() -> Option<String> {
|
||||
machine_uid::get()
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
}
|
||||
|
||||
#[cfg(not(any(
|
||||
target_os = "linux",
|
||||
all(target_os = "macos", not(feature = "macos-ne")),
|
||||
target_os = "windows",
|
||||
target_os = "freebsd"
|
||||
)))]
|
||||
fn machine_uid_seed() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn linux_machine_id_seed(machine_uid: &str) -> String {
|
||||
let mut seed = format!("machine_uid={machine_uid}");
|
||||
|
||||
let hostname = hostname();
|
||||
if !hostname.is_empty() {
|
||||
seed.push_str("\nhostname=");
|
||||
seed.push_str(&hostname);
|
||||
}
|
||||
|
||||
let mac_addresses = collect_linux_mac_addresses();
|
||||
if !mac_addresses.is_empty() {
|
||||
seed.push_str("\nmacs=");
|
||||
seed.push_str(&mac_addresses.join(","));
|
||||
}
|
||||
|
||||
seed
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn collect_linux_mac_addresses() -> Vec<String> {
|
||||
let mut macs = Vec::new();
|
||||
let Ok(entries) = std::fs::read_dir("/sys/class/net") else {
|
||||
return macs;
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let Ok(name) = entry.file_name().into_string() else {
|
||||
continue;
|
||||
};
|
||||
if name == "lo" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let address_path = entry.path().join("address");
|
||||
let Ok(address) = std::fs::read_to_string(address_path) else {
|
||||
continue;
|
||||
};
|
||||
let address = address.trim().to_ascii_lowercase();
|
||||
if address.is_empty() || address == "00:00:00:00:00:00" {
|
||||
continue;
|
||||
}
|
||||
macs.push(address);
|
||||
}
|
||||
|
||||
macs.sort();
|
||||
macs.dedup();
|
||||
macs.truncate(3);
|
||||
macs
|
||||
}
|
||||
|
||||
fn persist_machine_id(path: &Path, machine_id: uuid::Uuid) -> anyhow::Result<uuid::Uuid> {
|
||||
if let Some(existing) = read_state_machine_id(path)? {
|
||||
return Ok(existing);
|
||||
}
|
||||
|
||||
let _lock = MachineIdWriteLock::acquire(path)?;
|
||||
|
||||
if let Some(existing) = read_state_machine_id(path)? {
|
||||
return Ok(existing);
|
||||
}
|
||||
|
||||
write_uuid_file_atomically(path, machine_id)?;
|
||||
Ok(machine_id)
|
||||
}
|
||||
|
||||
fn write_uuid_file_atomically(path: &Path, machine_id: uuid::Uuid) -> anyhow::Result<()> {
|
||||
let parent = path.parent().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"machine id state file {} has no parent directory",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
std::fs::create_dir_all(parent).with_context(|| {
|
||||
format!(
|
||||
"failed to create machine id state directory {}",
|
||||
parent.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
let tmp_path = parent.join(format!(
|
||||
".machine_id.tmp-{}-{}",
|
||||
std::process::id(),
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
{
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&tmp_path)
|
||||
.with_context(|| format!("failed to create {}", tmp_path.display()))?;
|
||||
file.write_all(machine_id.to_string().as_bytes())
|
||||
.with_context(|| format!("failed to write {}", tmp_path.display()))?;
|
||||
file.sync_all()
|
||||
.with_context(|| format!("failed to flush {}", tmp_path.display()))?;
|
||||
}
|
||||
|
||||
if let Err(err) = std::fs::rename(&tmp_path, path) {
|
||||
let _ = std::fs::remove_file(&tmp_path);
|
||||
return Err(err).with_context(|| {
|
||||
format!(
|
||||
"failed to move machine id state file into place at {}",
|
||||
path.display()
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct MachineIdWriteLock {
|
||||
#[cfg(unix)]
|
||||
_lock: Flock<std::fs::File>,
|
||||
#[cfg(not(unix))]
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl MachineIdWriteLock {
|
||||
fn acquire(path: &Path) -> anyhow::Result<Self> {
|
||||
let parent = path.parent().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"machine id state file {} has no parent directory",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
std::fs::create_dir_all(parent).with_context(|| {
|
||||
format!(
|
||||
"failed to create machine id state directory {}",
|
||||
parent.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
Self::acquire_unix(path)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
Self::acquire_fallback(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn acquire_unix(path: &Path) -> anyhow::Result<Self> {
|
||||
let lock_path = path.with_extension("lock");
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
let mut lock_file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(&lock_path)
|
||||
.with_context(|| format!("failed to open machine id lock {}", lock_path.display()))?;
|
||||
|
||||
loop {
|
||||
match Flock::lock(lock_file, FlockArg::LockExclusiveNonblock) {
|
||||
Ok(lock) => return Ok(Self { _lock: lock }),
|
||||
Err((file, Errno::EAGAIN)) => {
|
||||
if Instant::now() >= deadline {
|
||||
anyhow::bail!(
|
||||
"timed out waiting for machine id lock {}",
|
||||
lock_path.display()
|
||||
);
|
||||
}
|
||||
lock_file = file;
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
Err((_file, err)) => {
|
||||
anyhow::bail!(
|
||||
"failed to acquire machine id lock {}: {}",
|
||||
lock_path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn acquire_fallback(path: &Path) -> anyhow::Result<Self> {
|
||||
let lock_path = path.with_extension("lock");
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
|
||||
loop {
|
||||
match std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&lock_path)
|
||||
{
|
||||
Ok(mut file) => {
|
||||
writeln!(file, "pid={}", std::process::id()).ok();
|
||||
return Ok(Self { path: lock_path });
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||
if should_reap_stale_lock_file(&lock_path) {
|
||||
let _ = std::fs::remove_file(&lock_path);
|
||||
continue;
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
anyhow::bail!(
|
||||
"timed out waiting for machine id lock {}",
|
||||
lock_path.display()
|
||||
);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err).with_context(|| {
|
||||
format!("failed to acquire machine id lock {}", lock_path.display())
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn should_reap_stale_lock_file(lock_path: &Path) -> bool {
|
||||
const STALE_LOCK_AGE: Duration = Duration::from_secs(30);
|
||||
|
||||
let Ok(metadata) = std::fs::metadata(lock_path) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(modified) = metadata.modified() else {
|
||||
return false;
|
||||
};
|
||||
modified
|
||||
.elapsed()
|
||||
.is_ok_and(|elapsed| elapsed >= STALE_LOCK_AGE)
|
||||
}
|
||||
|
||||
impl Drop for MachineIdWriteLock {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(not(unix))]
|
||||
let _ = std::fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_resolve_machine_id_uses_uuid_seed_verbatim() {
|
||||
let raw = "33333333-3333-3333-3333-333333333333".to_string();
|
||||
let opts = MachineIdOptions {
|
||||
explicit_machine_id: Some(raw.clone()),
|
||||
state_dir: None,
|
||||
};
|
||||
assert_eq!(
|
||||
resolve_machine_id(&opts).unwrap(),
|
||||
uuid::Uuid::parse_str(&raw).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_machine_id_reads_state_file() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let expected = uuid::Uuid::new_v4();
|
||||
std::fs::write(temp_dir.path().join("machine_id"), expected.to_string()).unwrap();
|
||||
|
||||
let opts = MachineIdOptions {
|
||||
explicit_machine_id: None,
|
||||
state_dir: Some(temp_dir.path().to_path_buf()),
|
||||
};
|
||||
|
||||
assert_eq!(resolve_machine_id(&opts).unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_legacy_machine_id_file_ignores_read_errors() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
assert_eq!(read_legacy_machine_id_file_at(temp_dir.path()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_uuid_file_atomically_writes_expected_contents() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let machine_id = uuid::Uuid::new_v4();
|
||||
let state_file = temp_dir.path().join("machine_id");
|
||||
|
||||
write_uuid_file_atomically(&state_file, machine_id).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(state_file).unwrap(),
|
||||
machine_id.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_empty_os_string_filters_empty_values() {
|
||||
assert_eq!(non_empty_os_string(Some(OsString::new())), None);
|
||||
assert_eq!(
|
||||
non_empty_os_string(Some(OsString::from("foo"))),
|
||||
Some(OsString::from("foo"))
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn test_default_linux_machine_id_state_dir_falls_back_in_order() {
|
||||
assert_eq!(
|
||||
default_linux_machine_id_state_dir(
|
||||
Some(OsString::from("/tmp/xdg")),
|
||||
Some(OsString::from("/tmp/home"))
|
||||
),
|
||||
PathBuf::from("/tmp/xdg").join("easytier")
|
||||
);
|
||||
assert_eq!(
|
||||
default_linux_machine_id_state_dir(
|
||||
Some(OsString::new()),
|
||||
Some(OsString::from("/tmp/home"))
|
||||
),
|
||||
PathBuf::from("/tmp/home")
|
||||
.join(".local")
|
||||
.join("share")
|
||||
.join("easytier")
|
||||
);
|
||||
assert_eq!(
|
||||
default_linux_machine_id_state_dir(Some(OsString::new()), Some(OsString::new())),
|
||||
PathBuf::from("/var/lib/easytier")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_persist_machine_id_creates_missing_state_dir() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let state_file = temp_dir.path().join("nested").join("machine_id");
|
||||
let machine_id = uuid::Uuid::new_v4();
|
||||
|
||||
assert_eq!(
|
||||
persist_machine_id(&state_file, machine_id).unwrap(),
|
||||
machine_id
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(state_file).unwrap(),
|
||||
machine_id.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_legacy_machine_uid_migration_requires_existing_state_dir_content() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let missing_state_file = temp_dir.path().join("missing").join("machine_id");
|
||||
assert!(!should_attempt_legacy_machine_uid_migration(
|
||||
&missing_state_file
|
||||
));
|
||||
|
||||
let empty_dir = temp_dir.path().join("empty");
|
||||
std::fs::create_dir_all(&empty_dir).unwrap();
|
||||
assert!(!should_attempt_legacy_machine_uid_migration(
|
||||
&empty_dir.join("machine_id")
|
||||
));
|
||||
|
||||
std::fs::write(empty_dir.join("config.toml"), "x=1").unwrap();
|
||||
assert!(should_attempt_legacy_machine_uid_migration(
|
||||
&empty_dir.join("machine_id")
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,23 @@
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
future,
|
||||
io::Write as _,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use time::util::refresh_tz;
|
||||
use tokio::{task::JoinSet, time::timeout};
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::{set_global_var, use_global_var};
|
||||
|
||||
pub mod acl_processor;
|
||||
pub mod compressor;
|
||||
pub mod config;
|
||||
pub mod constants;
|
||||
pub mod dns;
|
||||
pub mod env_parser;
|
||||
pub mod error;
|
||||
pub mod global_ctx;
|
||||
pub mod idn;
|
||||
pub mod ifcfg;
|
||||
pub mod log;
|
||||
pub mod machine_id;
|
||||
pub mod netns;
|
||||
pub mod network;
|
||||
pub mod os_info;
|
||||
@@ -31,6 +28,8 @@ pub mod token_bucket;
|
||||
pub mod tracing_rolling_appender;
|
||||
pub mod upnp;
|
||||
|
||||
pub use machine_id::{MachineIdOptions, resolve_machine_id};
|
||||
|
||||
pub fn get_logger_timer<F: time::formatting::Formattable>(
|
||||
format: F,
|
||||
) -> tracing_subscriber::fmt::time::OffsetTime<F> {
|
||||
@@ -96,71 +95,6 @@ pub fn join_joinset_background<T: Debug + Send + Sync + 'static>(
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_default_machine_id(mid: Option<String>) {
|
||||
set_global_var!(MACHINE_UID, mid);
|
||||
}
|
||||
|
||||
pub fn get_machine_id() -> uuid::Uuid {
|
||||
if let Some(default_mid) = use_global_var!(MACHINE_UID) {
|
||||
if let Ok(mid) = uuid::Uuid::parse_str(default_mid.trim()) {
|
||||
return mid;
|
||||
}
|
||||
let mut b = [0u8; 16];
|
||||
crate::tunnel::generate_digest_from_str("", &default_mid, &mut b);
|
||||
return uuid::Uuid::from_bytes(b);
|
||||
}
|
||||
|
||||
// a path same as the binary
|
||||
let machine_id_file = std::env::current_exe()
|
||||
.map(|x| x.with_file_name("et_machine_id"))
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from("et_machine_id"));
|
||||
|
||||
// try load from local file
|
||||
if let Ok(mid) = std::fs::read_to_string(&machine_id_file)
|
||||
&& let Ok(mid) = uuid::Uuid::parse_str(mid.trim())
|
||||
{
|
||||
return mid;
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
all(target_os = "macos", not(feature = "macos-ne")),
|
||||
target_os = "windows",
|
||||
target_os = "freebsd"
|
||||
))]
|
||||
let gen_mid = machine_uid::get()
|
||||
.map(|x| {
|
||||
if x.is_empty() {
|
||||
return uuid::Uuid::new_v4();
|
||||
}
|
||||
let mut b = [0u8; 16];
|
||||
crate::tunnel::generate_digest_from_str("", x.as_str(), &mut b);
|
||||
uuid::Uuid::from_bytes(b)
|
||||
})
|
||||
.ok();
|
||||
|
||||
#[cfg(not(any(
|
||||
target_os = "linux",
|
||||
all(target_os = "macos", not(feature = "macos-ne")),
|
||||
target_os = "windows",
|
||||
target_os = "freebsd"
|
||||
)))]
|
||||
let gen_mid = None;
|
||||
|
||||
if let Some(mid) = gen_mid {
|
||||
return mid;
|
||||
}
|
||||
|
||||
let gen_mid = uuid::Uuid::new_v4();
|
||||
|
||||
// try save to local file
|
||||
if let Ok(mut file) = std::fs::File::create(machine_id_file) {
|
||||
let _ = file.write_all(gen_mid.to_string().as_bytes());
|
||||
}
|
||||
|
||||
gen_mid
|
||||
}
|
||||
|
||||
pub fn shrink_dashmap<K: Eq + std::hash::Hash, V>(
|
||||
map: &dashmap::DashMap<K, V>,
|
||||
threshold: Option<usize>,
|
||||
@@ -210,12 +144,4 @@ mod tests {
|
||||
assert_eq!(weak_js.weak_count(), 0);
|
||||
assert_eq!(weak_js.strong_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_machine_id_uses_uuid_seed_verbatim() {
|
||||
let raw = "33333333-3333-3333-3333-333333333333".to_string();
|
||||
set_default_machine_id(Some(raw.clone()));
|
||||
assert_eq!(get_machine_id(), uuid::Uuid::parse_str(&raw).unwrap());
|
||||
set_default_machine_id(None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,15 @@ pub enum MetricName {
|
||||
/// Traffic packets forwarded for foreign network, forward
|
||||
TrafficPacketsForeignForwardForwarded,
|
||||
|
||||
/// UDP broadcast relay packets captured from the raw socket
|
||||
UdpBroadcastRelayPacketsCaptured,
|
||||
/// UDP broadcast relay packets ignored before forwarding
|
||||
UdpBroadcastRelayPacketsIgnored,
|
||||
/// UDP broadcast relay packets forwarded
|
||||
UdpBroadcastRelayPacketsForwarded,
|
||||
/// UDP broadcast relay packets that failed to forward
|
||||
UdpBroadcastRelayPacketsForwardFailed,
|
||||
|
||||
/// Compression bytes before compression
|
||||
CompressionBytesRxBefore,
|
||||
/// Compression bytes after compression
|
||||
@@ -167,6 +176,19 @@ impl fmt::Display for MetricName {
|
||||
write!(f, "traffic_packets_foreign_forward_forwarded")
|
||||
}
|
||||
|
||||
MetricName::UdpBroadcastRelayPacketsCaptured => {
|
||||
write!(f, "udp_broadcast_relay_packets_captured")
|
||||
}
|
||||
MetricName::UdpBroadcastRelayPacketsIgnored => {
|
||||
write!(f, "udp_broadcast_relay_packets_ignored")
|
||||
}
|
||||
MetricName::UdpBroadcastRelayPacketsForwarded => {
|
||||
write!(f, "udp_broadcast_relay_packets_forwarded")
|
||||
}
|
||||
MetricName::UdpBroadcastRelayPacketsForwardFailed => {
|
||||
write!(f, "udp_broadcast_relay_packets_forward_failed")
|
||||
}
|
||||
|
||||
MetricName::CompressionBytesRxBefore => write!(f, "compression_bytes_rx_before"),
|
||||
MetricName::CompressionBytesRxAfter => write!(f, "compression_bytes_rx_after"),
|
||||
MetricName::CompressionBytesTxBefore => write!(f, "compression_bytes_tx_before"),
|
||||
|
||||
+79
-27
@@ -11,7 +11,7 @@ use crossbeam::atomic::AtomicCell;
|
||||
use rand::seq::IteratorRandom;
|
||||
use socket2::{SockAddr, SockRef};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{UdpSocket, lookup_host};
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::sync::{Mutex, broadcast};
|
||||
use tokio::task::JoinSet;
|
||||
use tracing::{Instrument, Level};
|
||||
@@ -20,10 +20,9 @@ use bytecodec::{DecodeExt, EncodeExt};
|
||||
use stun_codec::rfc5389::methods::BINDING;
|
||||
use stun_codec::{Message, MessageClass, MessageDecoder, MessageEncoder};
|
||||
|
||||
use crate::common::error::Error;
|
||||
|
||||
use super::dns::resolve_txt_record;
|
||||
use super::stun_codec_ext::*;
|
||||
use crate::common::error::Error;
|
||||
use crate::utils::dns::{resolve_host, txt_resolve};
|
||||
|
||||
const DEFAULT_UDP_STUN_SERVERS: &[&str] = &[
|
||||
"txt:stun.easytier.cn",
|
||||
@@ -61,9 +60,16 @@ impl HostResolverIter {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_txt_record(domain_name: &str) -> Result<Vec<String>, Error> {
|
||||
let txt_data = resolve_txt_record(domain_name).await?;
|
||||
Ok(txt_data.split(" ").map(|x| x.to_string()).collect())
|
||||
fn parse_ipv6_socket_addr_without_brackets(host: &str) -> Option<SocketAddr> {
|
||||
if host.parse::<IpAddr>().is_ok() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (ip, port) = host.rsplit_once(':')?;
|
||||
Some(SocketAddr::new(
|
||||
IpAddr::V6(ip.parse().ok()?),
|
||||
port.parse().ok()?,
|
||||
))
|
||||
}
|
||||
|
||||
#[async_recursion::async_recursion]
|
||||
@@ -74,15 +80,10 @@ impl HostResolverIter {
|
||||
}
|
||||
|
||||
let host = self.hostnames.remove(0);
|
||||
let host = if host.contains(':') {
|
||||
host
|
||||
} else {
|
||||
format!("{}:3478", host)
|
||||
};
|
||||
|
||||
if host.starts_with("txt:") {
|
||||
let domain_name = host.trim_start_matches("txt:");
|
||||
match Self::get_txt_record(domain_name).await {
|
||||
match txt_resolve(domain_name).await {
|
||||
Ok(hosts) => {
|
||||
tracing::info!(
|
||||
?domain_name,
|
||||
@@ -104,22 +105,53 @@ impl HostResolverIter {
|
||||
}
|
||||
|
||||
let use_ipv6 = self.use_ipv6;
|
||||
|
||||
match lookup_host(&host).await {
|
||||
Ok(ips) => {
|
||||
self.ips = ips
|
||||
.filter(|x| if use_ipv6 { x.is_ipv6() } else { x.is_ipv4() })
|
||||
.choose_multiple(&mut rand::thread_rng(), self.max_ip_per_domain as usize);
|
||||
|
||||
if self.ips.is_empty() {
|
||||
return self.next().await;
|
||||
}
|
||||
if let Ok(addr) = host.parse::<SocketAddr>() {
|
||||
if (use_ipv6 && addr.is_ipv6()) || (!use_ipv6 && addr.is_ipv4()) {
|
||||
self.ips = vec![addr];
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(?host, ?e, "lookup host for stun failed");
|
||||
if self.ips.is_empty() {
|
||||
return self.next().await;
|
||||
}
|
||||
};
|
||||
} else if let Some(addr) = Self::parse_ipv6_socket_addr_without_brackets(&host) {
|
||||
if use_ipv6 {
|
||||
self.ips = vec![addr];
|
||||
}
|
||||
if self.ips.is_empty() {
|
||||
return self.next().await;
|
||||
}
|
||||
} else {
|
||||
let (host, port) = if let Ok(ip) = host.parse::<IpAddr>() {
|
||||
(ip.to_string(), 3478)
|
||||
} else if let Ok(url) = url::Url::parse(&format!("stun://{}", host)) {
|
||||
let Some(parsed_host) = url.host_str() else {
|
||||
tracing::warn!(?host, "parse stun host failed");
|
||||
return self.next().await;
|
||||
};
|
||||
(parsed_host.to_string(), url.port().unwrap_or(3478))
|
||||
} else {
|
||||
(host, 3478)
|
||||
};
|
||||
|
||||
match resolve_host(&host, port).await {
|
||||
Ok(ips) => {
|
||||
self.ips = ips
|
||||
.into_iter()
|
||||
.filter(|x| if use_ipv6 { x.is_ipv6() } else { x.is_ipv4() })
|
||||
.choose_multiple(
|
||||
&mut rand::thread_rng(),
|
||||
self.max_ip_per_domain as usize,
|
||||
);
|
||||
|
||||
if self.ips.is_empty() {
|
||||
return self.next().await;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(?host, ?e, "resolve host for stun failed");
|
||||
return self.next().await;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Some(self.ips.remove(0))
|
||||
@@ -1349,6 +1381,26 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_ipv6_socket_addr_without_brackets_rejects_plain_ipv6_literals() {
|
||||
assert_eq!(
|
||||
HostResolverIter::parse_ipv6_socket_addr_without_brackets("2001:db8::1"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
HostResolverIter::parse_ipv6_socket_addr_without_brackets("2001:db8:0:0:0:0:0:1"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ipv6_socket_addr_without_brackets_accepts_unambiguous_port() {
|
||||
assert_eq!(
|
||||
HostResolverIter::parse_ipv6_socket_addr_without_brackets("::1:55355"),
|
||||
Some("[::1]:55355".parse().unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_udp_nat_type_detector() {
|
||||
let collector = StunInfoCollector::new(
|
||||
@@ -1563,6 +1615,6 @@ mod tests {
|
||||
});
|
||||
let stun_servers = vec!["::1:55355".to_string()];
|
||||
let ret = StunInfoCollector::get_public_ipv6(&stun_servers).await;
|
||||
println!("{:#?}", ret);
|
||||
assert_eq!(ret, Some(Ipv6Addr::LOCALHOST));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,7 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
common::{
|
||||
PeerId, dns::socket_addrs, error::Error, global_ctx::ArcGlobalCtx,
|
||||
stun::StunInfoCollectorTrait,
|
||||
},
|
||||
common::{PeerId, error::Error, global_ctx::ArcGlobalCtx, stun::StunInfoCollectorTrait},
|
||||
connector::udp_hole_punch::handle_rpc_result,
|
||||
peers::{
|
||||
peer_conn::PeerConnId,
|
||||
@@ -40,6 +37,7 @@ use super::{
|
||||
udp_hole_punch,
|
||||
};
|
||||
use crate::tunnel::{FromUrl, IpScheme, TunnelScheme, matches_scheme};
|
||||
use crate::utils::dns::socket_addrs;
|
||||
use anyhow::Context;
|
||||
use rand::Rng;
|
||||
use socket2::Protocol;
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use super::{create_connector_by_url, http_connector::TunnelWithInfo};
|
||||
use crate::utils::dns::{srv_lookup, txt_resolve};
|
||||
use crate::{
|
||||
common::{
|
||||
dns::{RESOLVER, resolve_txt_record},
|
||||
error::Error,
|
||||
global_ctx::ArcGlobalCtx,
|
||||
log,
|
||||
},
|
||||
common::{error::Error, global_ctx::ArcGlobalCtx, log},
|
||||
proto::common::TunnelInfo,
|
||||
tunnel::{IpScheme, IpVersion, Tunnel, TunnelConnector, TunnelError, TunnelScheme},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use dashmap::DashSet;
|
||||
use hickory_resolver::proto::rr::rdata::SRV;
|
||||
use hickory_proto::rr::rdata::SRV;
|
||||
use rand::{Rng as _, seq::SliceRandom};
|
||||
use strum::VariantArray;
|
||||
|
||||
@@ -58,14 +54,13 @@ impl DnsTunnelConnector {
|
||||
&self,
|
||||
domain_name: &str,
|
||||
) -> Result<Box<dyn TunnelConnector>, Error> {
|
||||
let txt_data = resolve_txt_record(domain_name)
|
||||
let txt_data = txt_resolve(domain_name)
|
||||
.await
|
||||
.with_context(|| format!("resolve txt record failed, domain_name: {}", domain_name))?;
|
||||
|
||||
let candidate_urls = txt_data
|
||||
.split(" ")
|
||||
.map(|s| s.to_string())
|
||||
.filter_map(|s| url::Url::parse(s.as_str()).ok())
|
||||
.iter()
|
||||
.filter_map(|s| url::Url::parse(s).ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// shuffle candidate_urls and get the first one
|
||||
@@ -73,7 +68,7 @@ impl DnsTunnelConnector {
|
||||
.choose(&mut rand::thread_rng())
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"no valid url found, txt_data: {}, expecting an url list splitted by space",
|
||||
"no valid url found, txt_data: {:?}, expecting an url list split by space",
|
||||
txt_data
|
||||
)
|
||||
})?;
|
||||
@@ -83,26 +78,23 @@ impl DnsTunnelConnector {
|
||||
Ok(connector)
|
||||
}
|
||||
|
||||
fn handle_one_srv_record(record: &SRV, protocol: IpScheme) -> Result<(url::Url, u64), Error> {
|
||||
fn handle_one_srv_record(record: SRV, protocol: IpScheme) -> Result<(url::Url, u64), Error> {
|
||||
// port must be non-zero
|
||||
if record.port() == 0 {
|
||||
if record.port == 0 {
|
||||
return Err(anyhow::anyhow!("port must be non-zero").into());
|
||||
}
|
||||
|
||||
let connector_dst = record.target().to_utf8();
|
||||
let dst_url = format!("{}://{}:{}", protocol, connector_dst, record.port());
|
||||
let connector_dst = record.target.to_utf8();
|
||||
let dst_url = format!("{}://{}:{}", protocol, connector_dst, record.port);
|
||||
|
||||
Ok((
|
||||
dst_url.parse().with_context(|| {
|
||||
format!(
|
||||
"parse dst_url failed, protocol: {}, connector_dst: {}, port: {}, dst_url: {}",
|
||||
protocol,
|
||||
connector_dst,
|
||||
record.port(),
|
||||
dst_url
|
||||
protocol, connector_dst, record.port, dst_url
|
||||
)
|
||||
})?,
|
||||
record.priority() as _,
|
||||
record.priority as _,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -122,14 +114,9 @@ impl DnsTunnelConnector {
|
||||
let srv_lookup_tasks = srv_domains
|
||||
.iter()
|
||||
.map(|(protocol, srv_domain)| {
|
||||
let resolver = RESOLVER.clone();
|
||||
let responses = responses.clone();
|
||||
async move {
|
||||
let response = resolver.srv_lookup(srv_domain).await.with_context(|| {
|
||||
format!("srv_lookup failed, srv_domain: {}", srv_domain)
|
||||
})?;
|
||||
tracing::info!(?response, ?srv_domain, "srv_lookup response");
|
||||
for record in response.iter() {
|
||||
for record in srv_lookup(srv_domain).await? {
|
||||
let parsed_record = Self::handle_one_srv_record(record, **protocol);
|
||||
tracing::info!(?parsed_record, ?srv_domain, "parsed_record");
|
||||
if let Err(e) = &parsed_record {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
future::Future,
|
||||
sync::{Arc, Weak},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use dashmap::DashSet;
|
||||
use tokio::{sync::mpsc, task::JoinSet, time::timeout};
|
||||
|
||||
use crate::{
|
||||
common::{PeerId, dns::socket_addrs, join_joinset_background},
|
||||
common::{PeerId, join_joinset_background},
|
||||
peers::peer_conn::PeerConnId,
|
||||
proto::{
|
||||
api::instance::{
|
||||
@@ -16,10 +18,12 @@ use crate::{
|
||||
},
|
||||
rpc_types::{self, controller::BaseController},
|
||||
},
|
||||
tunnel::{IpVersion, TunnelConnector},
|
||||
tunnel::{IpVersion, TunnelConnector, TunnelScheme, matches_scheme},
|
||||
utils::weak_upgrade,
|
||||
};
|
||||
|
||||
use super::create_connector_by_url;
|
||||
use crate::utils::dns::socket_addrs;
|
||||
use crate::{
|
||||
common::{
|
||||
error::Error,
|
||||
@@ -30,8 +34,6 @@ use crate::{
|
||||
use_global_var,
|
||||
};
|
||||
|
||||
use super::create_connector_by_url;
|
||||
|
||||
type ConnectorMap = Arc<DashSet<url::Url>>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -83,6 +85,55 @@ impl ManualConnectorManager {
|
||||
ret
|
||||
}
|
||||
|
||||
fn reconnect_timeout(dead_url: &url::Url) -> Duration {
|
||||
let use_long_timeout = matches_scheme!(
|
||||
dead_url,
|
||||
TunnelScheme::Http | TunnelScheme::Https | TunnelScheme::Txt | TunnelScheme::Srv
|
||||
) || matches!(dead_url.scheme(), "ws" | "wss");
|
||||
|
||||
Duration::from_secs(if use_long_timeout { 20 } else { 2 })
|
||||
}
|
||||
|
||||
fn remaining_budget(started_at: Instant, total_timeout: Duration) -> Option<Duration> {
|
||||
let remaining = total_timeout.checked_sub(started_at.elapsed())?;
|
||||
(!remaining.is_zero()).then_some(remaining)
|
||||
}
|
||||
|
||||
fn emit_connect_error(
|
||||
data: &ConnectorManagerData,
|
||||
dead_url: &url::Url,
|
||||
ip_version: IpVersion,
|
||||
error: &Error,
|
||||
) {
|
||||
data.global_ctx.issue_event(GlobalCtxEvent::ConnectError(
|
||||
dead_url.to_string(),
|
||||
format!("{:?}", ip_version),
|
||||
format!("{:#?}", error),
|
||||
));
|
||||
}
|
||||
|
||||
fn reconnect_timeout_error(stage: &str, duration: Duration) -> Error {
|
||||
Error::AnyhowError(anyhow::anyhow!("{} timeout after {:?}", stage, duration))
|
||||
}
|
||||
|
||||
async fn with_reconnect_timeout<T, F>(
|
||||
stage: &'static str,
|
||||
started_at: Instant,
|
||||
total_timeout: Duration,
|
||||
fut: F,
|
||||
) -> Result<T, Error>
|
||||
where
|
||||
F: Future<Output = Result<T, Error>>,
|
||||
{
|
||||
let remaining = Self::remaining_budget(started_at, total_timeout)
|
||||
.ok_or_else(|| Self::reconnect_timeout_error(stage, started_at.elapsed()))?;
|
||||
timeout(remaining, fut)
|
||||
.await
|
||||
.map_err(|_| Self::reconnect_timeout_error(stage, remaining))?
|
||||
}
|
||||
}
|
||||
|
||||
impl ManualConnectorManager {
|
||||
pub fn add_connector<T>(&self, connector: T)
|
||||
where
|
||||
T: TunnelConnector + 'static,
|
||||
@@ -242,11 +293,18 @@ impl ManualConnectorManager {
|
||||
|
||||
async fn conn_reconnect_with_ip_version(
|
||||
data: Arc<ConnectorManagerData>,
|
||||
dead_url: String,
|
||||
dead_url: url::Url,
|
||||
ip_version: IpVersion,
|
||||
started_at: Instant,
|
||||
total_timeout: Duration,
|
||||
) -> Result<ReconnResult, Error> {
|
||||
let connector =
|
||||
create_connector_by_url(&dead_url, &data.global_ctx.clone(), ip_version).await?;
|
||||
let connector = Self::with_reconnect_timeout(
|
||||
"resolve",
|
||||
started_at,
|
||||
total_timeout,
|
||||
create_connector_by_url(dead_url.as_str(), &data.global_ctx, ip_version),
|
||||
)
|
||||
.await?;
|
||||
|
||||
data.global_ctx
|
||||
.issue_event(GlobalCtxEvent::Connecting(connector.remote_url()));
|
||||
@@ -257,10 +315,25 @@ impl ManualConnectorManager {
|
||||
)));
|
||||
};
|
||||
|
||||
let (peer_id, conn_id) = pm.try_direct_connect(connector).await?;
|
||||
let tunnel = Self::with_reconnect_timeout(
|
||||
"connect",
|
||||
started_at,
|
||||
total_timeout,
|
||||
pm.connect_tunnel(connector),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (peer_id, conn_id) = Self::with_reconnect_timeout(
|
||||
"handshake",
|
||||
started_at,
|
||||
total_timeout,
|
||||
pm.add_client_tunnel_with_peer_id_hint(tunnel, true, None),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::info!("reconnect succ: {} {} {}", peer_id, conn_id, dead_url);
|
||||
Ok(ReconnResult {
|
||||
dead_url,
|
||||
dead_url: dead_url.to_string(),
|
||||
peer_id,
|
||||
conn_id,
|
||||
})
|
||||
@@ -273,22 +346,33 @@ impl ManualConnectorManager {
|
||||
tracing::info!("reconnect: {}", dead_url);
|
||||
|
||||
let mut ip_versions = vec![];
|
||||
if dead_url.scheme() == "ring" || dead_url.scheme() == "txt" || dead_url.scheme() == "srv" {
|
||||
if matches_scheme!(
|
||||
dead_url,
|
||||
TunnelScheme::Ring | TunnelScheme::Txt | TunnelScheme::Srv
|
||||
) {
|
||||
ip_versions.push(IpVersion::Both);
|
||||
} else {
|
||||
let converted_dead_url = crate::common::idn::convert_idn_to_ascii(dead_url.clone())?;
|
||||
let addrs = match socket_addrs(&converted_dead_url, || Some(1000)).await {
|
||||
let converted_dead_url =
|
||||
match crate::common::idn::convert_idn_to_ascii(dead_url.clone()) {
|
||||
Ok(url) => url,
|
||||
Err(error) => {
|
||||
let error: Error = error.into();
|
||||
Self::emit_connect_error(&data, &dead_url, IpVersion::Both, &error);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let addrs = match Self::with_reconnect_timeout(
|
||||
"resolve",
|
||||
Instant::now(),
|
||||
Self::reconnect_timeout(&dead_url),
|
||||
socket_addrs(&converted_dead_url, || Some(1000)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(addrs) => addrs,
|
||||
Err(e) => {
|
||||
data.global_ctx.issue_event(GlobalCtxEvent::ConnectError(
|
||||
dead_url.to_string(),
|
||||
format!("{:?}", IpVersion::Both),
|
||||
format!("{:?}", e),
|
||||
));
|
||||
return Err(Error::AnyhowError(anyhow::anyhow!(
|
||||
"get ip from url failed: {:?}",
|
||||
e
|
||||
)));
|
||||
Err(error) => {
|
||||
Self::emit_connect_error(&data, &dead_url, IpVersion::Both, &error);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
tracing::info!(?addrs, ?dead_url, "get ip from url done");
|
||||
@@ -313,46 +397,24 @@ impl ManualConnectorManager {
|
||||
"cannot get ip from url"
|
||||
)));
|
||||
for ip_version in ip_versions {
|
||||
let use_long_timeout = dead_url.scheme() == "http"
|
||||
|| dead_url.scheme() == "https"
|
||||
|| dead_url.scheme() == "ws"
|
||||
|| dead_url.scheme() == "wss"
|
||||
|| dead_url.scheme() == "txt"
|
||||
|| dead_url.scheme() == "srv";
|
||||
let ret = timeout(
|
||||
// allow http/websocket connector to wait longer
|
||||
std::time::Duration::from_secs(if use_long_timeout { 20 } else { 2 }),
|
||||
Self::conn_reconnect_with_ip_version(
|
||||
data.clone(),
|
||||
dead_url.to_string(),
|
||||
ip_version,
|
||||
),
|
||||
let started_at = Instant::now();
|
||||
let ret = Self::conn_reconnect_with_ip_version(
|
||||
data.clone(),
|
||||
dead_url.clone(),
|
||||
ip_version,
|
||||
started_at,
|
||||
Self::reconnect_timeout(&dead_url),
|
||||
)
|
||||
.await;
|
||||
tracing::info!("reconnect: {} done, ret: {:?}", dead_url, ret);
|
||||
|
||||
match ret {
|
||||
Ok(Ok(_)) => {
|
||||
// 外层和内层都成功:解包并跳出
|
||||
reconn_ret = ret.unwrap();
|
||||
break;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
// 外层成功,内层失败
|
||||
reconn_ret = Err(e);
|
||||
}
|
||||
Err(e) => {
|
||||
// 外层失败
|
||||
reconn_ret = Err(e.into());
|
||||
Ok(result) => return Ok(result),
|
||||
Err(error) => {
|
||||
Self::emit_connect_error(&data, &dead_url, ip_version, &error);
|
||||
reconn_ret = Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送事件(只有在未 break 时才执行)
|
||||
data.global_ctx.issue_event(GlobalCtxEvent::ConnectError(
|
||||
dead_url.to_string(),
|
||||
format!("{:?}", ip_version),
|
||||
format!("{:?}", reconn_ret),
|
||||
));
|
||||
}
|
||||
|
||||
reconn_ret
|
||||
@@ -388,6 +450,54 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconnect_timeout_reports_exhausted_budget_for_stage() {
|
||||
let started_at = Instant::now() - Duration::from_millis(50);
|
||||
let err = ManualConnectorManager::with_reconnect_timeout(
|
||||
"resolve",
|
||||
started_at,
|
||||
Duration::from_millis(1),
|
||||
async { Ok::<(), Error>(()) },
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
let message = err.to_string();
|
||||
assert!(message.contains("resolve timeout after"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconnect_timeout_reports_stage_timeout_with_remaining_budget() {
|
||||
let err = ManualConnectorManager::with_reconnect_timeout(
|
||||
"handshake",
|
||||
Instant::now(),
|
||||
Duration::from_millis(10),
|
||||
async {
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
Ok::<(), Error>(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
let message = err.to_string();
|
||||
assert!(message.contains("handshake timeout after"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconnect_timeout_preserves_success_within_budget() {
|
||||
let result = ManualConnectorManager::with_reconnect_timeout(
|
||||
"connect",
|
||||
Instant::now(),
|
||||
Duration::from_millis(50),
|
||||
async { Ok::<_, Error>(123_u32) },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result, 123);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reconnect_with_connecting_addr() {
|
||||
set_global_var!(MANUAL_CONNECTOR_RECONNECT_INTERVAL_MS, 1);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use std::net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
|
||||
|
||||
use crate::{
|
||||
common::{dns::socket_addrs, error::Error, global_ctx::ArcGlobalCtx, idn},
|
||||
common::{error::Error, global_ctx::ArcGlobalCtx, idn},
|
||||
connector::dns_connector::DnsTunnelConnector,
|
||||
proto::common::PeerFeatureFlag,
|
||||
tunnel::{
|
||||
self, IpScheme, IpVersion, TunnelConnector, TunnelError, TunnelScheme,
|
||||
ring::RingTunnelConnector, tcp::TcpTunnelConnector, udp::UdpTunnelConnector,
|
||||
},
|
||||
utils::BoxExt,
|
||||
utils::{BoxExt, dns::socket_addrs},
|
||||
};
|
||||
use http_connector::HttpTunnelConnector;
|
||||
use rand::seq::SliceRandom;
|
||||
|
||||
+16
-19
@@ -484,6 +484,15 @@ struct NetworkOptions {
|
||||
)]
|
||||
disable_upnp: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_ENABLE_UDP_BROADCAST_RELAY",
|
||||
help = t!("core_clap.enable_udp_broadcast_relay").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
enable_udp_broadcast_relay: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_RELAY_ALL_PEER_RPC",
|
||||
@@ -569,19 +578,6 @@ struct NetworkOptions {
|
||||
)]
|
||||
port_forward: Vec<url::Url>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_ACCEPT_DNS",
|
||||
help = t!("core_clap.accept_dns").to_string(),
|
||||
)]
|
||||
accept_dns: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long = "tld-dns-zone",
|
||||
env = "ET_TLD_DNS_ZONE",
|
||||
help = t!("core_clap.tld_dns_zone").to_string())]
|
||||
tld_dns_zone: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_PRIVATE_MODE",
|
||||
@@ -1121,7 +1117,6 @@ impl NetworkOptions {
|
||||
f.disable_kcp_input = self.disable_kcp_input.unwrap_or(f.disable_kcp_input);
|
||||
f.enable_quic_proxy = self.enable_quic_proxy.unwrap_or(f.enable_quic_proxy);
|
||||
f.disable_quic_input = self.disable_quic_input.unwrap_or(f.disable_quic_input);
|
||||
f.accept_dns = self.accept_dns.unwrap_or(f.accept_dns);
|
||||
f.private_mode = self.private_mode.unwrap_or(f.private_mode);
|
||||
f.foreign_relay_bps_limit = self
|
||||
.foreign_relay_bps_limit
|
||||
@@ -1142,10 +1137,9 @@ impl NetworkOptions {
|
||||
.disable_sym_hole_punching
|
||||
.unwrap_or(f.disable_sym_hole_punching);
|
||||
f.disable_upnp = self.disable_upnp.unwrap_or(f.disable_upnp);
|
||||
// Configure tld_dns_zone: use provided value if set
|
||||
if let Some(tld_dns_zone) = &self.tld_dns_zone {
|
||||
f.tld_dns_zone = tld_dns_zone.clone();
|
||||
}
|
||||
f.enable_udp_broadcast_relay = self
|
||||
.enable_udp_broadcast_relay
|
||||
.unwrap_or(f.enable_udp_broadcast_relay);
|
||||
cfg.set_flags(f);
|
||||
|
||||
if !self.exit_nodes.is_empty() {
|
||||
@@ -1336,7 +1330,10 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
||||
let _web_client = if let Some(config_server_url_s) = cli.config_server.as_ref() {
|
||||
let wc = web_client::run_web_client(
|
||||
config_server_url_s,
|
||||
cli.machine_id.clone(),
|
||||
crate::common::MachineIdOptions {
|
||||
explicit_machine_id: cli.machine_id.clone(),
|
||||
state_dir: None,
|
||||
},
|
||||
cli.network_options.hostname.clone(),
|
||||
cli.network_options.secure_mode.unwrap_or(false),
|
||||
manager.clone(),
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
use crate::common::config::ConfigBase;
|
||||
use crate::dns::config::policy::DnsPolicyConfig;
|
||||
use crate::dns::config::zone::ZoneConfig;
|
||||
use crate::dns::config::{DNS_DEFAULT_ADDRESSES, DNS_DEFAULT_DOMAIN};
|
||||
use crate::dns::utils::addr::NameServerAddrGroup;
|
||||
use crate::proto::dns::GetExportConfigResponse;
|
||||
use hickory_proto::rr::LowerName;
|
||||
use optionize::{Optionizable, optionized};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[optionized]
|
||||
#[optionize(name = "DnsConfigRaw")]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
|
||||
pub struct DnsConfigParsed {
|
||||
pub disabled: bool,
|
||||
#[serde(rename = "zone")]
|
||||
pub zones: Vec<ZoneConfig>,
|
||||
#[optionize(flatten)]
|
||||
#[serde(flatten)]
|
||||
pub policies: HashMap<LowerName, DnsPolicyConfig>,
|
||||
#[optionize(flatten)]
|
||||
pub name: Option<LowerName>,
|
||||
pub domain: LowerName,
|
||||
pub addresses: NameServerAddrGroup,
|
||||
pub listeners: NameServerAddrGroup,
|
||||
}
|
||||
|
||||
pub type DnsConfig = ConfigBase<DnsConfigRaw, DnsConfigParsed, ()>;
|
||||
|
||||
impl From<DnsConfigRaw> for DnsConfig {
|
||||
fn from(raw: DnsConfigRaw) -> Self {
|
||||
let mut parsed = DnsConfigParsed {
|
||||
domain: DNS_DEFAULT_DOMAIN.clone(),
|
||||
addresses: DNS_DEFAULT_ADDRESSES.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
parsed.load(raw.clone());
|
||||
Self::new(parsed, raw, ())
|
||||
}
|
||||
}
|
||||
|
||||
#[auto_impl::auto_impl(Box, &)]
|
||||
pub trait DnsConfigLoaderExt {
|
||||
fn get_dns(&self) -> DnsConfig;
|
||||
fn set_dns(&self, dns: DnsConfig);
|
||||
}
|
||||
|
||||
pub type DnsExportConfig = GetExportConfigResponse;
|
||||
|
||||
pub trait DnsGlobalCtxExt {
|
||||
fn dns_self_zone(&self) -> ZoneConfig;
|
||||
fn dns_export_config(&self) -> DnsExportConfig;
|
||||
fn dns_iter_zones(&self) -> impl Iterator<Item = ZoneConfig>;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use crate::dns::utils::addr::NameServerAddrGroup;
|
||||
use hickory_proto::rr::LowerName;
|
||||
use std::net::IpAddr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
mod dns;
|
||||
pub use dns::*;
|
||||
mod policy;
|
||||
pub mod zone;
|
||||
|
||||
pub static DNS_DEFAULT_DOMAIN: LazyLock<LowerName> =
|
||||
LazyLock::new(|| LowerName::from_str("et.net.").unwrap());
|
||||
pub static DNS_DEFAULT_ADDRESSES: LazyLock<NameServerAddrGroup> =
|
||||
LazyLock::new(|| IpAddr::from_str("100.100.100.101").unwrap().into());
|
||||
|
||||
pub static DNS_SERVER_RPC_ADDR: LazyLock<Url> =
|
||||
LazyLock::new(|| Url::parse("tcp://127.0.0.1:49813").unwrap());
|
||||
|
||||
pub const DNS_NODE_TTI: Duration = Duration::from_secs(5);
|
||||
|
||||
pub const DNS_NODE_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(2);
|
||||
pub const DNS_NODE_RECONCILE_INTERVAL: Duration = Duration::from_secs(10);
|
||||
pub const DNS_SERVER_ELECTION_INTERVAL: Duration = Duration::from_secs(5);
|
||||
pub const DNS_PEER_TTI: Duration = Duration::from_secs(3);
|
||||
pub const DNS_PEER_REFRESH_ATTEMPTS: usize = 3;
|
||||
pub const DNS_PEER_REFRESH_BACKOFF: Duration = Duration::from_secs(1);
|
||||
@@ -0,0 +1,46 @@
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct AclPolicy {
|
||||
pub whitelist: Option<Vec<String>>,
|
||||
pub blacklist: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize, Deref, DerefMut)]
|
||||
#[serde(default)]
|
||||
pub struct FunctionalityPolicy {
|
||||
#[serde(flatten)]
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
acl: AclPolicy, // TODO
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize, Deref, DerefMut)]
|
||||
#[serde(default)]
|
||||
pub struct DnsPolicy<P = FunctionalityPolicy> {
|
||||
#[serde(flatten)]
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
policy: P,
|
||||
pub recursive: bool, // TODO
|
||||
}
|
||||
|
||||
pub type ZoneExportPolicy = FunctionalityPolicy;
|
||||
pub type DnsExportPolicy = DnsPolicy<ZoneExportPolicy>;
|
||||
pub type DnsImportPolicy = DnsPolicy<FunctionalityPolicy>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct DnsPolicyConfig {
|
||||
pub import: DnsImportPolicy,
|
||||
pub export: Option<DnsExportPolicy>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct ZonePolicyConfig {
|
||||
pub export: Option<DnsExportPolicy>,
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
use crate::common::config::ConfigBase;
|
||||
use crate::dns::config::policy::{DnsExportPolicy, ZonePolicyConfig};
|
||||
use crate::dns::utils::addr::NameServerAddrGroup;
|
||||
use crate::dns::zone::Zone;
|
||||
use crate::proto::dns::ZoneData;
|
||||
use derive_more::From;
|
||||
use hickory_proto::op::ResponseCode;
|
||||
use hickory_proto::rr::LowerName;
|
||||
use maplit::hashset;
|
||||
use optionize::{Optionizable, optionized};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryFrom;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Hash, From, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Fallthrough {
|
||||
Any,
|
||||
ResponseCode(ResponseCode),
|
||||
}
|
||||
|
||||
impl From<Fallthrough> for i32 {
|
||||
fn from(value: Fallthrough) -> Self {
|
||||
match value {
|
||||
Fallthrough::ResponseCode(code) => u16::from(code).into(),
|
||||
Fallthrough::Any => -1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Fallthrough {
|
||||
fn from(value: i32) -> Self {
|
||||
match u16::try_from(value) {
|
||||
Ok(value) => Self::ResponseCode(value.into()),
|
||||
Err(_) => Self::Any,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[optionized]
|
||||
#[optionize(name = "ZoneConfigRaw")]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ZoneConfigParsed {
|
||||
#[optionize(flatten)]
|
||||
pub origin: LowerName,
|
||||
pub ttl: u32,
|
||||
pub records: Vec<String>,
|
||||
pub forwarders: NameServerAddrGroup,
|
||||
#[optionize(flatten)]
|
||||
#[serde(flatten)]
|
||||
pub policy: ZonePolicyConfig,
|
||||
pub fallthrough: HashSet<Fallthrough>,
|
||||
}
|
||||
|
||||
impl From<&ZoneConfigParsed> for ZoneData {
|
||||
fn from(value: &ZoneConfigParsed) -> Self {
|
||||
Self::new(
|
||||
&value.origin,
|
||||
value.ttl,
|
||||
&value.records,
|
||||
value.forwarders.iter().map(Into::into),
|
||||
value.fallthrough.iter().copied(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub type ZoneConfig = ConfigBase<ZoneConfigRaw, ZoneConfigParsed, ZoneData>;
|
||||
|
||||
impl TryFrom<ZoneConfigRaw> for ZoneConfig {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(raw: ZoneConfigRaw) -> Result<Self, Self::Error> {
|
||||
let mut parsed = ZoneConfigParsed {
|
||||
fallthrough: hashset! {Fallthrough::Any},
|
||||
..Default::default()
|
||||
};
|
||||
parsed.load(raw.clone());
|
||||
let data = (&parsed).into();
|
||||
let _ = Zone::try_from(&data)?; // validation
|
||||
Ok(Self::new(parsed, raw, data))
|
||||
}
|
||||
}
|
||||
|
||||
impl ZoneConfig {
|
||||
pub fn dedicated(origin: LowerName, ipv4: Option<Ipv4Addr>, ipv6: Vec<Ipv6Addr>) -> Self {
|
||||
let mut records = Vec::new();
|
||||
|
||||
if let Some(ipv4) = ipv4 {
|
||||
records.push(format!("@ IN A {}", ipv4));
|
||||
}
|
||||
for ipv6 in ipv6 {
|
||||
records.push(format!("@ IN AAAA {}", ipv6));
|
||||
}
|
||||
|
||||
let policy = ZonePolicyConfig {
|
||||
export: Some(DnsExportPolicy::default()),
|
||||
};
|
||||
|
||||
let parsed = ZoneConfigParsed {
|
||||
origin,
|
||||
records,
|
||||
policy,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let data = (&parsed).into();
|
||||
|
||||
Self::new(parsed, Default::default(), data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
pub mod config;
|
||||
pub mod node;
|
||||
mod node_mgr;
|
||||
mod peer_mgr;
|
||||
pub mod server;
|
||||
mod system;
|
||||
mod utils;
|
||||
mod zone;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,560 @@
|
||||
use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtxEvent};
|
||||
use crate::dns::config::{
|
||||
DNS_NODE_HEARTBEAT_INTERVAL, DNS_NODE_RECONCILE_INTERVAL, DNS_PEER_REFRESH_ATTEMPTS,
|
||||
DNS_PEER_REFRESH_BACKOFF, DNS_SERVER_ELECTION_INTERVAL, DNS_SERVER_RPC_ADDR,
|
||||
};
|
||||
use crate::dns::peer_mgr::DnsPeerMgr;
|
||||
use crate::dns::server::DnsServer;
|
||||
#[cfg(feature = "tun")]
|
||||
use crate::instance::instance::ArcNicCtx;
|
||||
use crate::peers::peer_manager::PeerManager;
|
||||
use crate::proto::dns::{DnsNodeMgrRpcClientFactory, HeartbeatRequest};
|
||||
use crate::proto::rpc_impl::standalone::{StandAloneClient, StandAloneServer};
|
||||
use crate::proto::rpc_types::controller::BaseController;
|
||||
use crate::tunnel::tcp::{TcpTunnelConnector, TcpTunnelListener};
|
||||
use crate::utils::task::CancellableTask;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Notify, broadcast};
|
||||
use tokio::task::JoinSet;
|
||||
use tokio::time::{MissedTickBehavior, interval};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct DnsNodeRuntime {
|
||||
mgr: DnsPeerMgr,
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
nic_ctx: ArcNicCtx, // TODO: REMOVE THIS
|
||||
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
|
||||
elect: Arc<Notify>,
|
||||
}
|
||||
|
||||
impl DnsNodeRuntime {
|
||||
fn id(&self) -> Uuid {
|
||||
self.global_ctx.get_id()
|
||||
}
|
||||
|
||||
#[instrument(skip_all, name = "DnsNode election loop")]
|
||||
async fn run_election(&self, token: CancellationToken) {
|
||||
let mut election_interval = interval(DNS_SERVER_ELECTION_INTERVAL);
|
||||
election_interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = token.cancelled() => {
|
||||
tracing::info!("DnsNode received shutdown signal, exiting election loop");
|
||||
break;
|
||||
}
|
||||
_ = self.elect.notified() => {}
|
||||
_ = election_interval.tick() => {}
|
||||
}
|
||||
|
||||
tracing::info!("trying to become DNS server");
|
||||
|
||||
let mut rpc =
|
||||
StandAloneServer::new(TcpTunnelListener::new(DNS_SERVER_RPC_ADDR.clone()));
|
||||
|
||||
if rpc.serve().await.is_err() {
|
||||
// Another node already owns the address — that's fine.
|
||||
tracing::info!(
|
||||
"failed to bind RPC server, another node might have won the election"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::info!("won DNS server election, starting DnsServer");
|
||||
|
||||
let server = Arc::new(DnsServer::new(
|
||||
self.peer_mgr.clone(),
|
||||
self.global_ctx.clone(),
|
||||
#[cfg(feature = "tun")]
|
||||
self.nic_ctx.clone(),
|
||||
));
|
||||
server.register(&rpc);
|
||||
server.run(token.child_token()).await;
|
||||
|
||||
tracing::warn!("DnsServer exited, will retry election");
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip_all, name = "DnsNode main loop")]
|
||||
async fn run(&self, token: CancellationToken) {
|
||||
let mut rpc = StandAloneClient::new(TcpTunnelConnector::new(DNS_SERVER_RPC_ADDR.clone()));
|
||||
|
||||
let mut heartbeat = HeartbeatRequest {
|
||||
id: Some(self.id().into()),
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut heartbeat_interval = interval(DNS_NODE_HEARTBEAT_INTERVAL);
|
||||
heartbeat_interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
|
||||
let mut reconcile_interval = interval(DNS_NODE_RECONCILE_INTERVAL);
|
||||
reconcile_interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
|
||||
let mut subscriber = self.global_ctx.subscribe();
|
||||
let mut tasks = JoinSet::new();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
|
||||
_ = token.cancelled() => {
|
||||
tracing::info!("DnsNode received shutdown signal, exiting main loop");
|
||||
break;
|
||||
}
|
||||
|
||||
_ = heartbeat_interval.tick() => {
|
||||
if let Err(error) = self.heartbeat(&mut rpc, &mut heartbeat).await {
|
||||
tracing::error!(?error, "heartbeat failed");
|
||||
self.elect.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
_ = reconcile_interval.tick() => {
|
||||
let mgr = self.mgr.clone();
|
||||
tasks.spawn(async move {
|
||||
mgr.reconcile().await;
|
||||
});
|
||||
}
|
||||
|
||||
_ = self.mgr.dirty.wait() => {}
|
||||
|
||||
event = subscriber.recv() => {
|
||||
match event {
|
||||
Ok(GlobalCtxEvent::PeerInfoUpdated(peer_ids)) => {
|
||||
for peer_id in peer_ids {
|
||||
let mgr = self.mgr.clone();
|
||||
tasks.spawn(async move {
|
||||
if let Err(error) = mgr.refresh(peer_id, DNS_PEER_REFRESH_ATTEMPTS, DNS_PEER_REFRESH_BACKOFF).await {
|
||||
tracing::error!(?error, ?peer_id, "failed to refresh peer");
|
||||
}
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Ok(
|
||||
GlobalCtxEvent::DhcpIpv4Changed(..)
|
||||
| GlobalCtxEvent::DhcpIpv4Conflicted(..),
|
||||
) => {
|
||||
tracing::info!(?event, "ip change detected, rebuilding snapshot");
|
||||
}
|
||||
Ok(GlobalCtxEvent::ConfigPatched(patch)) => {
|
||||
// TODO: inspect patch
|
||||
tracing::info!(?patch, "config change detected, rebuilding snapshot");
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||
tracing::warn!("event listener lagged, skipped {n} events, rebuilding snapshot");
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
tracing::info!("event bus closed");
|
||||
break;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
self.mgr.dirty.mark();
|
||||
}
|
||||
|
||||
result = tasks.join_next(), if !tasks.is_empty() => {
|
||||
if let Some(Err(error)) = result {
|
||||
tracing::error!(?error, "refresh task panicked");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn heartbeat(
|
||||
&self,
|
||||
rpc: &mut StandAloneClient<TcpTunnelConnector>,
|
||||
heartbeat: &mut HeartbeatRequest,
|
||||
) -> anyhow::Result<()> {
|
||||
let request = if heartbeat.snapshot.is_none() || self.mgr.dirty.reset() {
|
||||
heartbeat.update(self.mgr.snapshot());
|
||||
heartbeat.clone()
|
||||
} else {
|
||||
let snapshot = heartbeat.snapshot.take();
|
||||
let request = heartbeat.clone();
|
||||
heartbeat.snapshot = snapshot;
|
||||
request
|
||||
};
|
||||
|
||||
let client = rpc
|
||||
.scoped_client::<DnsNodeMgrRpcClientFactory<BaseController>>("".to_string())
|
||||
.await?;
|
||||
|
||||
let response = client.heartbeat(BaseController::default(), request).await?;
|
||||
if response.resync {
|
||||
tracing::trace!("resync requested by server, sending full snapshot");
|
||||
client
|
||||
.heartbeat(BaseController::default(), heartbeat.clone())
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DnsNode {
|
||||
runtime: DnsNodeRuntime,
|
||||
task: Option<CancellableTask<()>>,
|
||||
}
|
||||
|
||||
impl DnsNode {
|
||||
pub fn new(
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
#[cfg(feature = "tun")] nic_ctx: ArcNicCtx, // TODO: REMOVE THIS
|
||||
) -> Self {
|
||||
let runtime = DnsNodeRuntime {
|
||||
mgr: DnsPeerMgr::new(peer_mgr.clone(), global_ctx.clone()),
|
||||
#[cfg(feature = "tun")]
|
||||
nic_ctx,
|
||||
peer_mgr,
|
||||
global_ctx,
|
||||
elect: Default::default(),
|
||||
};
|
||||
|
||||
Self {
|
||||
runtime,
|
||||
task: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
let runtime = self.runtime.clone();
|
||||
self.task
|
||||
.replace(CancellableTask::spawn(|token| async move {
|
||||
runtime.elect.notify_one();
|
||||
tokio::join!(runtime.run_election(token.clone()), runtime.run(token));
|
||||
}));
|
||||
self.runtime.mgr.register();
|
||||
}
|
||||
|
||||
pub async fn stop(&mut self) -> io::Result<()> {
|
||||
self.runtime.mgr.unregister();
|
||||
let Some(task) = self.task.take() else {
|
||||
return Ok(());
|
||||
};
|
||||
task.stop(None).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DnsNode {
|
||||
fn drop(&mut self) {
|
||||
self.runtime.mgr.unregister();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "tun"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::common::global_ctx::GlobalCtxEvent;
|
||||
use crate::peers::tests::create_mock_peer_manager;
|
||||
use crate::proto::api::config::InstanceConfigPatch;
|
||||
use crate::proto::dns::{DnsNodeMgrRpc, DnsNodeMgrRpcServer, HeartbeatResponse};
|
||||
use crate::proto::rpc_impl::standalone::StandAloneServer;
|
||||
use crate::proto::rpc_types;
|
||||
use crate::tunnel::common::tests::wait_for_condition;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::sleep;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RecordingDnsNodeMgr {
|
||||
requests: Mutex<Vec<HeartbeatRequest>>,
|
||||
resync_on_first: AtomicBool,
|
||||
}
|
||||
|
||||
impl RecordingDnsNodeMgr {
|
||||
fn new(resync_on_first: bool) -> Self {
|
||||
Self {
|
||||
requests: Mutex::new(Vec::new()),
|
||||
resync_on_first: AtomicBool::new(resync_on_first),
|
||||
}
|
||||
}
|
||||
|
||||
async fn recorded_requests(&self) -> Vec<HeartbeatRequest> {
|
||||
self.requests.lock().await.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl DnsNodeMgrRpc for RecordingDnsNodeMgr {
|
||||
type Controller = BaseController;
|
||||
|
||||
async fn heartbeat(
|
||||
&self,
|
||||
_: Self::Controller,
|
||||
input: HeartbeatRequest,
|
||||
) -> rpc_types::error::Result<HeartbeatResponse> {
|
||||
let mut requests = self.requests.lock().await;
|
||||
requests.push(input);
|
||||
let is_first = requests.len() == 1;
|
||||
let resync = is_first && self.resync_on_first.load(Ordering::Relaxed);
|
||||
if is_first {
|
||||
self.resync_on_first.store(false, Ordering::Relaxed);
|
||||
}
|
||||
Ok(HeartbeatResponse { resync })
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_test_runtime() -> DnsNodeRuntime {
|
||||
let peer_mgr = create_mock_peer_manager().await;
|
||||
let global_ctx = peer_mgr.get_global_ctx();
|
||||
let nic_ctx: ArcNicCtx = Arc::new(Mutex::new(None));
|
||||
DnsNodeRuntime {
|
||||
mgr: DnsPeerMgr::new(peer_mgr.clone(), global_ctx.clone()),
|
||||
nic_ctx,
|
||||
peer_mgr,
|
||||
global_ctx,
|
||||
elect: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_recording_rpc_server(
|
||||
rpc_addr: Url,
|
||||
resync_on_first: bool,
|
||||
) -> anyhow::Result<(
|
||||
Arc<RecordingDnsNodeMgr>,
|
||||
StandAloneServer<TcpTunnelListener>,
|
||||
)> {
|
||||
let mgr = Arc::new(RecordingDnsNodeMgr::new(resync_on_first));
|
||||
let mut server = StandAloneServer::new(TcpTunnelListener::new(rpc_addr));
|
||||
server
|
||||
.registry()
|
||||
.register(DnsNodeMgrRpcServer::new_arc(mgr.clone()), "");
|
||||
server.serve().await?;
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
Ok((mgr, server))
|
||||
}
|
||||
|
||||
async fn occupy_dns_rpc_addr(rpc_addr: Url) -> StandAloneServer<TcpTunnelListener> {
|
||||
let mut server = StandAloneServer::new(TcpTunnelListener::new(rpc_addr));
|
||||
server.serve().await.unwrap();
|
||||
server
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn heartbeat_first_send_includes_snapshot() {
|
||||
let rpc_addr = Url::parse(&format!("tcp://127.0.0.1:{}", 49851)).unwrap();
|
||||
let (_mgr, server) = start_recording_rpc_server(rpc_addr.clone(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
let node = build_test_runtime().await;
|
||||
|
||||
let mut rpc = StandAloneClient::new(TcpTunnelConnector::new(rpc_addr));
|
||||
let mut heartbeat = HeartbeatRequest {
|
||||
id: Some(node.id().into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
node.heartbeat(&mut rpc, &mut heartbeat).await.unwrap();
|
||||
|
||||
drop(server);
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
assert!(heartbeat.snapshot.is_some());
|
||||
assert!(!heartbeat.digest.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn heartbeat_clean_send_digest_only() {
|
||||
let rpc_addr = Url::parse(&format!("tcp://127.0.0.1:{}", 49852)).unwrap();
|
||||
let (mgr, server) = start_recording_rpc_server(rpc_addr.clone(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
let node = build_test_runtime().await;
|
||||
|
||||
let mut rpc = StandAloneClient::new(TcpTunnelConnector::new(rpc_addr));
|
||||
let mut heartbeat = HeartbeatRequest {
|
||||
id: Some(node.id().into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
node.heartbeat(&mut rpc, &mut heartbeat).await.unwrap();
|
||||
let _ = node.mgr.dirty.reset();
|
||||
node.heartbeat(&mut rpc, &mut heartbeat).await.unwrap();
|
||||
|
||||
let requests = mgr.recorded_requests().await;
|
||||
drop(server);
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert!(requests[0].snapshot.is_some());
|
||||
assert!(requests[1].snapshot.is_none());
|
||||
assert_eq!(requests[0].digest, requests[1].digest);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn heartbeat_dirty_forces_full_snapshot() {
|
||||
let rpc_addr = Url::parse(&format!("tcp://127.0.0.1:{}", 49853)).unwrap();
|
||||
let (mgr, server) = start_recording_rpc_server(rpc_addr.clone(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
let node = build_test_runtime().await;
|
||||
|
||||
let mut rpc = StandAloneClient::new(TcpTunnelConnector::new(rpc_addr));
|
||||
let mut heartbeat = HeartbeatRequest {
|
||||
id: Some(node.id().into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
node.heartbeat(&mut rpc, &mut heartbeat).await.unwrap();
|
||||
node.mgr.dirty.mark();
|
||||
node.heartbeat(&mut rpc, &mut heartbeat).await.unwrap();
|
||||
|
||||
let requests = mgr.recorded_requests().await;
|
||||
drop(server);
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert!(requests[0].snapshot.is_some());
|
||||
assert!(requests[1].snapshot.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn heartbeat_resync_triggers_second_send() {
|
||||
let rpc_addr = Url::parse(&format!("tcp://127.0.0.1:{}", 49854)).unwrap();
|
||||
let (mgr, server) = start_recording_rpc_server(rpc_addr.clone(), true)
|
||||
.await
|
||||
.unwrap();
|
||||
let node = build_test_runtime().await;
|
||||
|
||||
let mut rpc = StandAloneClient::new(TcpTunnelConnector::new(rpc_addr));
|
||||
let mut heartbeat = HeartbeatRequest {
|
||||
id: Some(node.id().into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
node.heartbeat(&mut rpc, &mut heartbeat).await.unwrap();
|
||||
|
||||
let requests = mgr.recorded_requests().await;
|
||||
drop(server);
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert!(requests[0].snapshot.is_some());
|
||||
assert!(requests[1].snapshot.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial(dns_node_rpc_addr)]
|
||||
async fn run_marks_dirty_on_dhcp_event() {
|
||||
let node = build_test_runtime().await;
|
||||
|
||||
let _ = node.mgr.dirty.reset();
|
||||
assert!(!node.mgr.dirty.peek());
|
||||
|
||||
let token = CancellationToken::new();
|
||||
let handle = tokio::spawn({
|
||||
let node = node.clone();
|
||||
let token = token.clone();
|
||||
async move { node.run(token).await }
|
||||
});
|
||||
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
node.global_ctx
|
||||
.issue_event(GlobalCtxEvent::DhcpIpv4Changed(None, None));
|
||||
|
||||
wait_for_condition(async || node.mgr.dirty.peek(), Duration::from_secs(2)).await;
|
||||
|
||||
token.cancel();
|
||||
tokio::time::timeout(Duration::from_secs(2), handle)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_marks_dirty_on_config_patched_event() {
|
||||
let node = build_test_runtime().await;
|
||||
|
||||
let _ = node.mgr.dirty.reset();
|
||||
assert!(!node.mgr.dirty.peek());
|
||||
|
||||
let token = CancellationToken::new();
|
||||
let handle = tokio::spawn({
|
||||
let node = node.clone();
|
||||
let token = token.clone();
|
||||
async move { node.run(token).await }
|
||||
});
|
||||
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
node.global_ctx
|
||||
.issue_event(GlobalCtxEvent::ConfigPatched(InstanceConfigPatch::default()));
|
||||
|
||||
wait_for_condition(async || node.mgr.dirty.peek(), Duration::from_secs(2)).await;
|
||||
|
||||
token.cancel();
|
||||
tokio::time::timeout(Duration::from_secs(2), handle)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_peer_info_updated_non_self_does_not_mark_dirty() {
|
||||
let node = build_test_runtime().await;
|
||||
|
||||
let _ = node.mgr.dirty.reset();
|
||||
assert!(!node.mgr.dirty.peek());
|
||||
|
||||
let token = CancellationToken::new();
|
||||
let handle = tokio::spawn({
|
||||
let node = node.clone();
|
||||
let token = token.clone();
|
||||
async move { node.run(token).await }
|
||||
});
|
||||
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
node.global_ctx
|
||||
.issue_event(GlobalCtxEvent::PeerInfoUpdated(vec![u32::MAX]));
|
||||
sleep(Duration::from_millis(200)).await;
|
||||
|
||||
assert!(!node.mgr.dirty.peek());
|
||||
|
||||
token.cancel();
|
||||
tokio::time::timeout(Duration::from_secs(2), handle)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_heartbeat_error_notifies_election() {
|
||||
let node = build_test_runtime().await;
|
||||
|
||||
let _ = node.mgr.dirty.reset();
|
||||
|
||||
let token = CancellationToken::new();
|
||||
let notified = node.elect.notified();
|
||||
let handle = tokio::spawn({
|
||||
let node = node.clone();
|
||||
let token = token.clone();
|
||||
async move { node.run(token).await }
|
||||
});
|
||||
|
||||
tokio::time::timeout(2 * DNS_NODE_HEARTBEAT_INTERVAL, notified)
|
||||
.await
|
||||
.expect("heartbeat failure should notify election");
|
||||
|
||||
token.cancel();
|
||||
tokio::time::timeout(Duration::from_secs(5), handle)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
use crate::dns::config::DNS_NODE_TTI;
|
||||
use crate::dns::utils::addr::NameServerAddr;
|
||||
use crate::dns::zone::{Zone, ZoneGroup};
|
||||
use crate::proto::dns::DnsNodeMgrRpc;
|
||||
use crate::proto::dns::{DnsSnapshot, HeartbeatRequest, HeartbeatResponse};
|
||||
use crate::proto::rpc_types;
|
||||
use crate::proto::rpc_types::controller::BaseController;
|
||||
use crate::proto::utils::TransientDigest;
|
||||
use crate::utils::dirty::DirtyFlag;
|
||||
use anyhow::Error;
|
||||
use hickory_server::zone_handler::Catalog;
|
||||
use itertools::Itertools;
|
||||
use moka::future::Cache;
|
||||
use std::collections::HashSet;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct DnsNodeInfo {
|
||||
digest: [u8; 32],
|
||||
zones: ZoneGroup,
|
||||
addresses: HashSet<NameServerAddr>,
|
||||
listeners: HashSet<NameServerAddr>,
|
||||
}
|
||||
|
||||
impl TryFrom<&DnsSnapshot> for DnsNodeInfo {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: &DnsSnapshot) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
digest: value.digest(),
|
||||
zones: value.zones.as_slice().try_into()?,
|
||||
addresses: value
|
||||
.addresses
|
||||
.iter()
|
||||
.map(TryInto::try_into)
|
||||
.collect::<Result<_, _>>()?,
|
||||
listeners: value
|
||||
.listeners
|
||||
.iter()
|
||||
.map(TryInto::try_into)
|
||||
.collect::<Result<_, _>>()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DnsNodeMgrDirtyFlags {
|
||||
pub catalog: DirtyFlag,
|
||||
pub addresses: DirtyFlag,
|
||||
pub listeners: DirtyFlag,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DnsNodeMgr {
|
||||
nodes: Cache<Uuid, DnsNodeInfo>,
|
||||
pub dirty: DnsNodeMgrDirtyFlags,
|
||||
}
|
||||
|
||||
impl DnsNodeMgr {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
nodes: Cache::builder().time_to_idle(DNS_NODE_TTI).build(),
|
||||
dirty: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn catalog(&self) -> Catalog {
|
||||
let groups = self.collect_zones().into_groups();
|
||||
|
||||
tracing::trace!("building catalog with zones: {:?}", groups);
|
||||
|
||||
let system = Zone::system().create_forward_zone_handler();
|
||||
groups
|
||||
.into_iter()
|
||||
.fold(Catalog::new(), |mut catalog, (origin, zones)| {
|
||||
catalog.upsert(
|
||||
origin.clone(),
|
||||
zones
|
||||
.iter_zone_handlers()
|
||||
.chain(system.iter().cloned())
|
||||
.collect(),
|
||||
);
|
||||
catalog
|
||||
})
|
||||
}
|
||||
|
||||
pub fn collect_zones(&self) -> ZoneGroup {
|
||||
let mut zones = Vec::new();
|
||||
let mut local = HashSet::new();
|
||||
|
||||
for (_, info) in self.nodes.iter() {
|
||||
zones.extend(info.zones);
|
||||
local.extend(info.addresses);
|
||||
local.extend(info.listeners);
|
||||
}
|
||||
|
||||
zones.push(Zone::system());
|
||||
|
||||
for forward in zones.iter_mut().flat_map(|z| &mut z.forward) {
|
||||
forward.name_servers.retain_mut(|ns| {
|
||||
ns.connections
|
||||
.retain(|c| !local.contains(&(ns.ip, c).into()));
|
||||
!ns.connections.is_empty()
|
||||
});
|
||||
}
|
||||
|
||||
zones.into()
|
||||
}
|
||||
|
||||
pub fn iter_addresses(&self) -> impl Iterator<Item = NameServerAddr> + use<'_> {
|
||||
self.nodes
|
||||
.iter()
|
||||
.flat_map(|(_, info)| info.addresses)
|
||||
.unique()
|
||||
}
|
||||
|
||||
pub fn iter_listeners(&self) -> impl Iterator<Item = NameServerAddr> + use<'_> {
|
||||
self.nodes
|
||||
.iter()
|
||||
.flat_map(|(_, info)| info.listeners)
|
||||
.unique()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl DnsNodeMgrRpc for DnsNodeMgr {
|
||||
type Controller = BaseController;
|
||||
|
||||
async fn heartbeat(
|
||||
&self,
|
||||
_: BaseController,
|
||||
input: HeartbeatRequest,
|
||||
) -> rpc_types::error::Result<HeartbeatResponse> {
|
||||
let id = input
|
||||
.id
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"missing id in heartbeat request: {:?}",
|
||||
input
|
||||
))?
|
||||
.into();
|
||||
|
||||
let resync = if let Some(snapshot) = input.snapshot.as_ref() {
|
||||
let new = DnsNodeInfo::try_from(snapshot)?;
|
||||
let old = self.nodes.get(&id).await.unwrap_or_default();
|
||||
if new.digest != old.digest {
|
||||
self.dirty.catalog.mark();
|
||||
if new.addresses != old.addresses {
|
||||
self.dirty.addresses.mark();
|
||||
}
|
||||
if new.listeners != old.listeners {
|
||||
self.dirty.listeners.mark();
|
||||
}
|
||||
|
||||
self.nodes.insert(id, new).await;
|
||||
}
|
||||
false
|
||||
} else {
|
||||
self.nodes
|
||||
.get(&id)
|
||||
.await
|
||||
.is_none_or(|info| input.digest != info.digest)
|
||||
};
|
||||
|
||||
Ok(HeartbeatResponse { resync })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dns::tests::{
|
||||
dns_snapshot_with as snapshot_with, heartbeat_with_snapshot, new_request,
|
||||
zone_data_a_with_forwarders as valid_zone_data,
|
||||
};
|
||||
use crate::dns::utils::response::ResponseHandle;
|
||||
use hickory_proto::op::{Message, ResponseCode};
|
||||
use hickory_proto::rr::{RData, RecordType};
|
||||
use std::net::Ipv4Addr;
|
||||
use tokio::time::{Duration, sleep};
|
||||
|
||||
fn heartbeat_digest_only(id: Uuid, digest: Vec<u8>) -> HeartbeatRequest {
|
||||
HeartbeatRequest {
|
||||
id: Some(id.into()),
|
||||
digest,
|
||||
snapshot: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_all_dirty(mgr: &DnsNodeMgr) {
|
||||
let _ = mgr.dirty.catalog.reset();
|
||||
let _ = mgr.dirty.addresses.reset();
|
||||
let _ = mgr.dirty.listeners.reset();
|
||||
}
|
||||
|
||||
async fn send_heartbeat(mgr: &DnsNodeMgr, input: HeartbeatRequest) -> HeartbeatResponse {
|
||||
DnsNodeMgrRpc::heartbeat(mgr, BaseController::default(), input)
|
||||
.await
|
||||
.expect("heartbeat should succeed")
|
||||
}
|
||||
|
||||
fn ns(s: &str) -> NameServerAddr {
|
||||
s.parse().expect("invalid nameserver")
|
||||
}
|
||||
|
||||
async fn lookup_a_record(mgr: &DnsNodeMgr, name: &str) -> anyhow::Result<Message> {
|
||||
let request = new_request(name, RecordType::A)?;
|
||||
let response = ResponseHandle::new(512);
|
||||
let info = mgr
|
||||
.catalog()
|
||||
.lookup(&request, None, 0, response.clone())
|
||||
.await;
|
||||
|
||||
assert_eq!(info.response_code, ResponseCode::NoError);
|
||||
|
||||
let response = response.into_inner().expect("response should exist");
|
||||
Message::from_vec(&response).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn catalog_lookup_returns_record_after_snapshot_heartbeat() -> anyhow::Result<()> {
|
||||
let mgr = DnsNodeMgr::new();
|
||||
let id = Uuid::new_v4();
|
||||
let snapshot = snapshot_with(
|
||||
vec![valid_zone_data("catalog.test", "10.20.30.40", vec![])],
|
||||
vec![],
|
||||
vec![],
|
||||
);
|
||||
|
||||
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(id, snapshot)).await;
|
||||
|
||||
let message = lookup_a_record(&mgr, "catalog.test.").await?;
|
||||
assert!(message.answers.iter().any(|record| {
|
||||
matches!(
|
||||
record.data,
|
||||
RData::A(addr) if *addr == Ipv4Addr::new(10, 20, 30, 40)
|
||||
)
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn catalog_lookup_aggregates_records_from_multiple_nodes() -> anyhow::Result<()> {
|
||||
let mgr = DnsNodeMgr::new();
|
||||
|
||||
let snap_a = snapshot_with(
|
||||
vec![valid_zone_data("node-a.test", "10.11.12.13", vec![])],
|
||||
vec!["udp://10.0.1.1:53"],
|
||||
vec![],
|
||||
);
|
||||
let snap_b = snapshot_with(
|
||||
vec![valid_zone_data("node-b.test", "10.21.22.23", vec![])],
|
||||
vec!["udp://10.0.2.1:53"],
|
||||
vec![],
|
||||
);
|
||||
|
||||
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(Uuid::new_v4(), snap_a)).await;
|
||||
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(Uuid::new_v4(), snap_b)).await;
|
||||
|
||||
let message_a = lookup_a_record(&mgr, "node-a.test.").await?;
|
||||
let message_b = lookup_a_record(&mgr, "node-b.test.").await?;
|
||||
|
||||
assert!(message_a.answers.iter().any(|record| {
|
||||
matches!(
|
||||
record.data,
|
||||
RData::A(addr) if *addr == Ipv4Addr::new(10, 11, 12, 13)
|
||||
)
|
||||
}));
|
||||
assert!(message_b.answers.iter().any(|record| {
|
||||
matches!(
|
||||
record.data,
|
||||
RData::A(addr) if *addr == Ipv4Addr::new(10, 21, 22, 23)
|
||||
)
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn heartbeat_digest_only_resync_behavior() {
|
||||
let mgr = DnsNodeMgr::new();
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
let first = send_heartbeat(&mgr, heartbeat_digest_only(id, vec![1, 2, 3])).await;
|
||||
assert!(first.resync);
|
||||
|
||||
let snapshot = snapshot_with(
|
||||
vec![valid_zone_data("resync.test", "10.0.0.10", vec![])],
|
||||
vec!["udp://10.0.0.1:53"],
|
||||
vec!["udp://10.0.0.2:53"],
|
||||
);
|
||||
let digest = snapshot.digest();
|
||||
let full = send_heartbeat(&mgr, heartbeat_with_snapshot(id, snapshot)).await;
|
||||
assert!(!full.resync);
|
||||
|
||||
let same = send_heartbeat(&mgr, heartbeat_digest_only(id, digest.into())).await;
|
||||
assert!(!same.resync);
|
||||
|
||||
let different = send_heartbeat(&mgr, heartbeat_digest_only(id, vec![9, 9, 9])).await;
|
||||
assert!(different.resync);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn heartbeat_with_snapshot_marks_dirty_flags_by_field_changes() {
|
||||
let mgr = DnsNodeMgr::new();
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
reset_all_dirty(&mgr);
|
||||
|
||||
let first = snapshot_with(
|
||||
vec![valid_zone_data("dirty.test", "10.0.0.1", vec![])],
|
||||
vec!["udp://10.10.10.1:53"],
|
||||
vec!["udp://10.10.10.2:53"],
|
||||
);
|
||||
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(id, first)).await;
|
||||
assert!(mgr.dirty.catalog.peek());
|
||||
assert!(mgr.dirty.addresses.peek());
|
||||
assert!(mgr.dirty.listeners.peek());
|
||||
|
||||
reset_all_dirty(&mgr);
|
||||
|
||||
let record_changed = snapshot_with(
|
||||
vec![valid_zone_data("dirty.test", "10.0.0.2", vec![])],
|
||||
vec!["udp://10.10.10.1:53"],
|
||||
vec!["udp://10.10.10.2:53"],
|
||||
);
|
||||
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(id, record_changed)).await;
|
||||
assert!(mgr.dirty.catalog.peek());
|
||||
assert!(!mgr.dirty.addresses.peek());
|
||||
assert!(!mgr.dirty.listeners.peek());
|
||||
|
||||
reset_all_dirty(&mgr);
|
||||
|
||||
let addr_listener_changed = snapshot_with(
|
||||
vec![valid_zone_data("dirty.test", "10.0.0.2", vec![])],
|
||||
vec!["udp://10.10.10.10:53"],
|
||||
vec!["udp://10.10.10.20:53"],
|
||||
);
|
||||
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(id, addr_listener_changed)).await;
|
||||
assert!(mgr.dirty.catalog.peek());
|
||||
assert!(mgr.dirty.addresses.peek());
|
||||
assert!(mgr.dirty.listeners.peek());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn heartbeat_with_same_snapshot_digest_is_noop_for_dirty() {
|
||||
let mgr = DnsNodeMgr::new();
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
let snapshot = snapshot_with(
|
||||
vec![valid_zone_data("stable.test", "10.30.40.50", vec![])],
|
||||
vec!["udp://10.3.0.1:53"],
|
||||
vec!["udp://10.3.0.2:53"],
|
||||
);
|
||||
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(id, snapshot.clone())).await;
|
||||
|
||||
reset_all_dirty(&mgr);
|
||||
|
||||
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(id, snapshot)).await;
|
||||
assert!(!mgr.dirty.catalog.peek());
|
||||
assert!(!mgr.dirty.addresses.peek());
|
||||
assert!(!mgr.dirty.listeners.peek());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn heartbeat_missing_id_returns_error() {
|
||||
let mgr = DnsNodeMgr::new();
|
||||
let err =
|
||||
DnsNodeMgrRpc::heartbeat(&mgr, BaseController::default(), HeartbeatRequest::default())
|
||||
.await
|
||||
.expect_err("missing id should error");
|
||||
assert!(err.to_string().contains("missing id"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn iter_addresses_and_listeners_deduplicate_across_multiple_nodes() -> anyhow::Result<()>
|
||||
{
|
||||
let mgr = DnsNodeMgr::new();
|
||||
let zone_a = Zone::try_from(&valid_zone_data("iter-a.test", "10.1.1.1", vec![]))?;
|
||||
let zone_b = Zone::try_from(&valid_zone_data("iter-b.test", "10.1.1.2", vec![]))?;
|
||||
|
||||
mgr.nodes
|
||||
.insert(
|
||||
Uuid::new_v4(),
|
||||
DnsNodeInfo {
|
||||
digest: [1; 32],
|
||||
zones: vec![zone_a].into(),
|
||||
addresses: [ns("udp://10.100.0.1:53"), ns("udp://10.100.0.2:53")]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
listeners: [ns("udp://10.200.0.1:53")].into_iter().collect(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
mgr.nodes
|
||||
.insert(
|
||||
Uuid::new_v4(),
|
||||
DnsNodeInfo {
|
||||
digest: [2; 32],
|
||||
zones: vec![zone_b].into(),
|
||||
addresses: [ns("udp://10.100.0.2:53"), ns("udp://10.100.0.3:53")]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
listeners: [ns("udp://10.200.0.1:53"), ns("udp://10.200.0.2:53")]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let addresses: HashSet<_> = mgr.iter_addresses().collect();
|
||||
let listeners: HashSet<_> = mgr.iter_listeners().collect();
|
||||
|
||||
assert_eq!(addresses.len(), 3);
|
||||
assert!(addresses.contains(&ns("udp://10.100.0.1:53")));
|
||||
assert!(addresses.contains(&ns("udp://10.100.0.2:53")));
|
||||
assert!(addresses.contains(&ns("udp://10.100.0.3:53")));
|
||||
|
||||
assert_eq!(listeners.len(), 2);
|
||||
assert!(listeners.contains(&ns("udp://10.200.0.1:53")));
|
||||
assert!(listeners.contains(&ns("udp://10.200.0.2:53")));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn collect_zones_filters_out_local_forwarders() -> anyhow::Result<()> {
|
||||
let mgr = DnsNodeMgr::new();
|
||||
let zone = Zone::try_from(&valid_zone_data(
|
||||
"filter-loop.test",
|
||||
"10.2.3.4",
|
||||
vec![
|
||||
"udp://10.0.0.10:53",
|
||||
"tcp://10.0.0.11:53",
|
||||
"udp://1.1.1.1:53",
|
||||
],
|
||||
))?;
|
||||
|
||||
mgr.nodes
|
||||
.insert(
|
||||
Uuid::new_v4(),
|
||||
DnsNodeInfo {
|
||||
digest: [1; 32],
|
||||
zones: vec![zone].into(),
|
||||
addresses: [ns("udp://10.0.0.10:53")].into_iter().collect(),
|
||||
listeners: [ns("tcp://10.0.0.11:53")].into_iter().collect(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let zones: Vec<_> = mgr.collect_zones().into_iter().map(Into::into).collect();
|
||||
let loop_zone = zones
|
||||
.into_iter()
|
||||
.find(|z: &crate::proto::dns::ZoneData| z.content.contains("$ORIGIN filter-loop.test"))
|
||||
.expect("test zone should exist");
|
||||
|
||||
let forwarders: HashSet<NameServerAddr> = loop_zone
|
||||
.forwarders
|
||||
.iter()
|
||||
.map(|u| NameServerAddr::try_from(u).expect("forwarder should be valid"))
|
||||
.collect();
|
||||
|
||||
assert_eq!(forwarders.len(), 1);
|
||||
assert!(forwarders.contains(&ns("udp://1.1.1.1:53")));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn collect_zones_filters_cross_node_local_forwarders() -> anyhow::Result<()> {
|
||||
let mgr = DnsNodeMgr::new();
|
||||
|
||||
let node_a = snapshot_with(
|
||||
vec![valid_zone_data(
|
||||
"cross-node-filter.test",
|
||||
"10.8.8.8",
|
||||
vec![
|
||||
"udp://10.50.0.1:53",
|
||||
"udp://10.50.0.2:53",
|
||||
"udp://8.8.8.8:53",
|
||||
],
|
||||
)],
|
||||
vec!["udp://10.50.0.1:53"],
|
||||
vec![],
|
||||
);
|
||||
let node_b = snapshot_with(
|
||||
vec![valid_zone_data(
|
||||
"cross-node-helper.test",
|
||||
"10.9.9.9",
|
||||
vec![],
|
||||
)],
|
||||
vec![],
|
||||
vec!["udp://10.50.0.2:53"],
|
||||
);
|
||||
|
||||
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(Uuid::new_v4(), node_a)).await;
|
||||
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(Uuid::new_v4(), node_b)).await;
|
||||
|
||||
let zones: Vec<_> = mgr.collect_zones().into_iter().map(Into::into).collect();
|
||||
let zone = zones
|
||||
.into_iter()
|
||||
.find(|z: &crate::proto::dns::ZoneData| {
|
||||
z.content.contains("$ORIGIN cross-node-filter.test")
|
||||
})
|
||||
.expect("test zone should exist");
|
||||
|
||||
let forwarders: HashSet<NameServerAddr> = zone
|
||||
.forwarders
|
||||
.iter()
|
||||
.map(|u| NameServerAddr::try_from(u).expect("forwarder should be valid"))
|
||||
.collect();
|
||||
|
||||
assert_eq!(forwarders.len(), 1);
|
||||
assert!(forwarders.contains(&ns("udp://8.8.8.8:53")));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn heartbeat_digest_resync_is_node_scoped() {
|
||||
let mgr = DnsNodeMgr::new();
|
||||
let node_a = Uuid::new_v4();
|
||||
let node_b = Uuid::new_v4();
|
||||
|
||||
let snap_a = snapshot_with(
|
||||
vec![valid_zone_data("scope-a.test", "10.60.0.1", vec![])],
|
||||
vec!["udp://10.60.0.2:53"],
|
||||
vec![],
|
||||
);
|
||||
let digest_a = snap_a.digest();
|
||||
|
||||
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(node_a, snap_a)).await;
|
||||
|
||||
let a_same = send_heartbeat(&mgr, heartbeat_digest_only(node_a, digest_a.into())).await;
|
||||
assert!(!a_same.resync);
|
||||
|
||||
let b_unknown = send_heartbeat(&mgr, heartbeat_digest_only(node_b, vec![1, 2, 3])).await;
|
||||
assert!(b_unknown.resync);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn heartbeat_resync_after_node_idle_ttl_expiry() {
|
||||
let mgr = DnsNodeMgr::new();
|
||||
let id = Uuid::new_v4();
|
||||
let snapshot = snapshot_with(
|
||||
vec![valid_zone_data("ttl.test", "10.9.9.9", vec![])],
|
||||
vec!["udp://10.9.0.1:53"],
|
||||
vec![],
|
||||
);
|
||||
let digest = snapshot.digest();
|
||||
|
||||
let _ = send_heartbeat(&mgr, heartbeat_with_snapshot(id, snapshot)).await;
|
||||
let before_expiry = send_heartbeat(&mgr, heartbeat_digest_only(id, digest.to_vec())).await;
|
||||
assert!(!before_expiry.resync);
|
||||
|
||||
sleep(DNS_NODE_TTI + Duration::from_millis(300)).await;
|
||||
|
||||
let after_expiry = send_heartbeat(&mgr, heartbeat_digest_only(id, digest.into())).await;
|
||||
assert!(after_expiry.resync);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,873 @@
|
||||
use crate::common::PeerId;
|
||||
use crate::common::global_ctx::ArcGlobalCtx;
|
||||
use crate::dns::config::zone::ZoneConfig;
|
||||
use crate::dns::config::{
|
||||
DNS_PEER_REFRESH_ATTEMPTS, DNS_PEER_REFRESH_BACKOFF, DNS_PEER_TTI, DnsExportConfig,
|
||||
DnsGlobalCtxExt,
|
||||
};
|
||||
use crate::dns::zone::ZoneGroup;
|
||||
use crate::peer_center::instance::PeerCenterPeerManagerTrait;
|
||||
use crate::peers::peer_manager::PeerManager;
|
||||
use crate::peers::route_trait::Route;
|
||||
use crate::proto::dns::{
|
||||
DnsPeerMgrRpc, DnsPeerMgrRpcClientFactory, DnsPeerMgrRpcServer, DnsSnapshot,
|
||||
GetExportConfigRequest, GetExportConfigResponse, ZoneData,
|
||||
};
|
||||
use crate::proto::rpc_types;
|
||||
use crate::proto::rpc_types::controller::BaseController;
|
||||
use crate::proto::utils::TransientDigest;
|
||||
use crate::utils::dirty::DirtyFlag;
|
||||
use anyhow::Context;
|
||||
use futures::StreamExt;
|
||||
use futures::stream;
|
||||
use moka::future::Cache;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use tracing::instrument;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct DnsPeerInfo {
|
||||
digest: [u8; 32],
|
||||
zones: Vec<ZoneData>,
|
||||
}
|
||||
|
||||
impl TryFrom<DnsExportConfig> for DnsPeerInfo {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: DnsExportConfig) -> Result<Self, Self::Error> {
|
||||
let _ = ZoneGroup::try_from(value.zones.as_slice())?;
|
||||
Ok(Self {
|
||||
digest: value.digest(),
|
||||
zones: value.zones,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DnsPeerMgrInner {
|
||||
peers: Cache<PeerId, DnsPeerInfo>,
|
||||
pub dirty: DirtyFlag,
|
||||
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
}
|
||||
|
||||
impl DnsPeerMgrInner {
|
||||
pub fn snapshot(&self) -> DnsSnapshot {
|
||||
let global_ctx = &self.global_ctx;
|
||||
|
||||
let zones = global_ctx
|
||||
.dns_iter_zones()
|
||||
.map(ZoneConfig::into_data)
|
||||
.chain(
|
||||
self.peers
|
||||
.iter()
|
||||
.flat_map(|(_, info)| info.zones.into_iter()),
|
||||
)
|
||||
.collect();
|
||||
|
||||
let config = global_ctx.config.get_dns().into_parsed();
|
||||
DnsSnapshot {
|
||||
zones,
|
||||
addresses: config.addresses.into(),
|
||||
listeners: config.listeners.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self), level = "trace", ret)]
|
||||
pub async fn refresh(
|
||||
&self,
|
||||
peer_id: PeerId,
|
||||
mut attempts: usize,
|
||||
mut backoff: Duration,
|
||||
) -> anyhow::Result<bool> {
|
||||
loop {
|
||||
attempts = attempts.saturating_sub(1);
|
||||
let result = self.try_refresh(peer_id).await;
|
||||
match &result {
|
||||
Ok(_) => {
|
||||
tracing::trace!(?peer_id, "peer info refreshed");
|
||||
return result;
|
||||
}
|
||||
Err(_) if attempts == 0 => {
|
||||
self.peers.invalidate(&peer_id).await;
|
||||
self.dirty.mark();
|
||||
tracing::error!(
|
||||
?peer_id,
|
||||
"exhausted all attempts to refresh peer info, invalidating cache"
|
||||
);
|
||||
return result;
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
?error,
|
||||
?peer_id,
|
||||
"failed to refresh peer info, retrying in {:?}",
|
||||
backoff
|
||||
);
|
||||
sleep(backoff).await;
|
||||
backoff *= 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_refresh(&self, peer_id: PeerId) -> anyhow::Result<bool> {
|
||||
if peer_id == self.peer_mgr.my_peer_id() {
|
||||
self.dirty.mark();
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let Some(route) = self.peer_mgr.get_route().get_peer_info(peer_id).await else {
|
||||
if self.peers.remove(&peer_id).await.is_some() {
|
||||
tracing::debug!(?peer_id, "peer route disappeared, removing from cache");
|
||||
self.dirty.mark();
|
||||
}
|
||||
return Ok(true);
|
||||
};
|
||||
|
||||
if self
|
||||
.peers
|
||||
.get(&peer_id)
|
||||
.await
|
||||
.is_some_and(|info| route.dns == info.digest)
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if !route.dns.is_empty() {
|
||||
let info = self.fetch(peer_id).await.with_context(|| {
|
||||
format!("failed to fetch dns export config from peer {}", peer_id)
|
||||
})?;
|
||||
self.peers.insert(peer_id, info).await;
|
||||
} else {
|
||||
self.peers.invalidate(&peer_id).await;
|
||||
}
|
||||
|
||||
self.dirty.mark();
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[instrument(skip(self), level = "trace", ret)]
|
||||
async fn fetch(&self, peer_id: PeerId) -> anyhow::Result<DnsPeerInfo> {
|
||||
self.peer_mgr
|
||||
.get_peer_rpc_mgr()
|
||||
.rpc_client()
|
||||
.scoped_client::<DnsPeerMgrRpcClientFactory<BaseController>>(
|
||||
self.peer_mgr.my_peer_id(),
|
||||
peer_id,
|
||||
self.global_ctx.get_network_name(),
|
||||
)
|
||||
.get_export_config(BaseController::default(), GetExportConfigRequest {})
|
||||
.await
|
||||
.context("rpc call failed")?
|
||||
.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl DnsPeerMgrRpc for DnsPeerMgrInner {
|
||||
type Controller = BaseController;
|
||||
|
||||
async fn get_export_config(
|
||||
&self,
|
||||
_: Self::Controller,
|
||||
_: GetExportConfigRequest,
|
||||
) -> rpc_types::error::Result<GetExportConfigResponse> {
|
||||
Ok(self.global_ctx.dns_export_config())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DnsPeerMgr(Arc<DnsPeerMgrInner>);
|
||||
|
||||
impl DnsPeerMgr {
|
||||
pub fn new(peer_mgr: Arc<PeerManager>, global_ctx: ArcGlobalCtx) -> Self {
|
||||
Self(Arc::new(DnsPeerMgrInner {
|
||||
peers: Cache::builder().time_to_idle(DNS_PEER_TTI).build(),
|
||||
dirty: Default::default(),
|
||||
peer_mgr,
|
||||
global_ctx,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn register(&self) {
|
||||
self.peer_mgr
|
||||
.get_peer_rpc_mgr()
|
||||
.rpc_server()
|
||||
.registry()
|
||||
.register(
|
||||
DnsPeerMgrRpcServer::new_arc(self.0.clone()),
|
||||
&self.global_ctx.get_network_name(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn unregister(&self) -> Option<()> {
|
||||
self.peer_mgr
|
||||
.get_peer_rpc_mgr()
|
||||
.rpc_server()
|
||||
.registry()
|
||||
.unregister(
|
||||
DnsPeerMgrRpcServer::new_arc(self.0.clone()),
|
||||
&self.global_ctx.get_network_name(),
|
||||
)
|
||||
}
|
||||
|
||||
#[instrument(skip(self), level = "trace")]
|
||||
pub async fn reconcile(&self) {
|
||||
stream::iter(self.peer_mgr.list_routes().await.into_iter())
|
||||
.map(|route| {
|
||||
let peer_id = route.peer_id;
|
||||
let this = self.clone();
|
||||
async move {
|
||||
if let Err(error) = this
|
||||
.refresh(peer_id, DNS_PEER_REFRESH_ATTEMPTS, DNS_PEER_REFRESH_BACKOFF)
|
||||
.await
|
||||
{
|
||||
tracing::error!(?error, ?peer_id, "failed to refresh peer info");
|
||||
}
|
||||
}
|
||||
})
|
||||
.buffer_unordered(32)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for DnsPeerMgr {
|
||||
type Target = DnsPeerMgrInner;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::common::global_ctx::tests::get_mock_global_ctx;
|
||||
use crate::dns::config::zone::ZoneConfig;
|
||||
use crate::dns::tests::zone_data_a as valid_zone_data;
|
||||
use crate::peers::create_packet_recv_chan;
|
||||
use crate::peers::peer_manager::RouteAlgoType;
|
||||
use crate::peers::tests::{connect_peer_manager, wait_route_appear};
|
||||
use crate::proto::dns::GetExportConfigRequest;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use tokio::time::{Duration, sleep};
|
||||
|
||||
async fn create_peer_manager_with_zone(
|
||||
host: &str,
|
||||
origin: &str,
|
||||
record_ip: Ipv4Addr,
|
||||
) -> Arc<PeerManager> {
|
||||
let ctx = get_mock_global_ctx();
|
||||
let mut dns = ctx.config.get_dns().into_raw();
|
||||
dns.name = Some(host.parse().unwrap());
|
||||
dns.zones
|
||||
.get_or_insert_default()
|
||||
.push(ZoneConfig::dedicated(
|
||||
origin.parse().expect("invalid zone origin"),
|
||||
Some(record_ip),
|
||||
vec![],
|
||||
));
|
||||
ctx.config.set_dns(dns.into());
|
||||
|
||||
let (s, _r) = create_packet_recv_chan();
|
||||
let peer_mgr = Arc::new(PeerManager::new(RouteAlgoType::Ospf, ctx, s));
|
||||
peer_mgr.run().await.unwrap();
|
||||
peer_mgr
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dns_peer_info_try_from_valid_config() {
|
||||
let cfg = DnsExportConfig {
|
||||
zones: vec![valid_zone_data("valid.peer.test", "10.0.0.10")],
|
||||
};
|
||||
|
||||
let info = DnsPeerInfo::try_from(cfg).expect("valid export config should pass");
|
||||
assert_eq!(info.zones.len(), 1);
|
||||
assert!(!info.digest.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dns_peer_info_try_from_invalid_zone_rejected() {
|
||||
let cfg = DnsExportConfig {
|
||||
zones: vec![ZoneData::new(&".".parse().unwrap(), 60, ["?"], [], [])],
|
||||
};
|
||||
|
||||
assert!(DnsPeerInfo::try_from(cfg).is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn snapshot_merges_local_and_cached_peer_zones() {
|
||||
let peer_mgr = create_peer_manager_with_zone(
|
||||
"local-peer",
|
||||
"local-custom.test",
|
||||
Ipv4Addr::new(10, 10, 10, 10),
|
||||
)
|
||||
.await;
|
||||
let global_ctx = peer_mgr.get_global_ctx();
|
||||
let mgr = DnsPeerMgr::new(peer_mgr, global_ctx);
|
||||
|
||||
mgr.peers
|
||||
.insert(
|
||||
999_999,
|
||||
DnsPeerInfo {
|
||||
digest: [9; 32],
|
||||
zones: vec![valid_zone_data("peer-cache.test", "10.20.30.40")],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let snapshot = mgr.snapshot();
|
||||
assert!(
|
||||
snapshot
|
||||
.zones
|
||||
.iter()
|
||||
.any(|z| z.content.contains("$ORIGIN peer-cache.test"))
|
||||
);
|
||||
assert!(
|
||||
snapshot
|
||||
.zones
|
||||
.iter()
|
||||
.any(|z| z.content.contains("$ORIGIN local-custom.test"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn snapshot_includes_local_addresses_and_listeners() {
|
||||
let peer_mgr = create_peer_manager_with_zone(
|
||||
"local-addr-listener",
|
||||
"local-addr-zone.test",
|
||||
Ipv4Addr::new(10, 10, 11, 11),
|
||||
)
|
||||
.await;
|
||||
let global_ctx = peer_mgr.get_global_ctx();
|
||||
let expected = global_ctx.config.get_dns().into_parsed();
|
||||
let mgr = DnsPeerMgr::new(peer_mgr, global_ctx);
|
||||
|
||||
let snapshot = mgr.snapshot();
|
||||
let mut expected_addresses = expected
|
||||
.addresses
|
||||
.into_iter()
|
||||
.map(|a| a.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
let mut expected_listeners = expected
|
||||
.listeners
|
||||
.into_iter()
|
||||
.map(|a| a.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
let mut got_addresses = snapshot
|
||||
.addresses
|
||||
.into_iter()
|
||||
.map(|a| a.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
let mut got_listeners = snapshot
|
||||
.listeners
|
||||
.into_iter()
|
||||
.map(|a| a.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
expected_addresses.sort();
|
||||
expected_listeners.sort();
|
||||
got_addresses.sort();
|
||||
got_listeners.sort();
|
||||
|
||||
assert_eq!(got_addresses, expected_addresses);
|
||||
assert_eq!(got_listeners, expected_listeners);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn snapshot_aggregates_zones_from_multiple_cached_peers() {
|
||||
let peer_mgr = create_peer_manager_with_zone(
|
||||
"local-multi",
|
||||
"local-multi.test",
|
||||
Ipv4Addr::new(10, 10, 12, 1),
|
||||
)
|
||||
.await;
|
||||
let global_ctx = peer_mgr.get_global_ctx();
|
||||
let mgr = DnsPeerMgr::new(peer_mgr, global_ctx);
|
||||
|
||||
mgr.peers
|
||||
.insert(
|
||||
11,
|
||||
DnsPeerInfo {
|
||||
digest: [11; 32],
|
||||
zones: vec![valid_zone_data("peer-a.test", "10.20.30.41")],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
mgr.peers
|
||||
.insert(
|
||||
12,
|
||||
DnsPeerInfo {
|
||||
digest: [12; 32],
|
||||
zones: vec![valid_zone_data("peer-b.test", "10.20.30.42")],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let snapshot = mgr.snapshot();
|
||||
let contents: HashSet<_> = snapshot.zones.into_iter().map(|z| z.content).collect();
|
||||
|
||||
assert!(contents.iter().any(|z| z.contains("$ORIGIN peer-a.test")));
|
||||
assert!(contents.iter().any(|z| z.contains("$ORIGIN peer-b.test")));
|
||||
assert!(
|
||||
contents
|
||||
.iter()
|
||||
.any(|z| z.contains("$ORIGIN local-multi.test"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn snapshot_with_peer_without_zones_keeps_local_snapshot() {
|
||||
let peer_mgr = create_peer_manager_with_zone(
|
||||
"local-empty-peer-zone",
|
||||
"local-empty-zone.test",
|
||||
Ipv4Addr::new(10, 10, 13, 1),
|
||||
)
|
||||
.await;
|
||||
let mgr = DnsPeerMgr::new(peer_mgr, get_mock_global_ctx());
|
||||
|
||||
let before = mgr.snapshot();
|
||||
|
||||
mgr.peers
|
||||
.insert(
|
||||
13,
|
||||
DnsPeerInfo {
|
||||
digest: [13; 32],
|
||||
zones: vec![],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let after = mgr.snapshot();
|
||||
assert_eq!(before.zones.len(), after.zones.len());
|
||||
assert_eq!(before.addresses, after.addresses);
|
||||
assert_eq!(before.listeners, after.listeners);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_export_config_returns_global_ctx_export() {
|
||||
let peer_mgr = create_peer_manager_with_zone(
|
||||
"export-peer",
|
||||
"exported-zone.test",
|
||||
Ipv4Addr::new(10, 10, 20, 20),
|
||||
)
|
||||
.await;
|
||||
let global_ctx = peer_mgr.get_global_ctx();
|
||||
let mgr = DnsPeerMgr::new(peer_mgr, global_ctx.clone());
|
||||
|
||||
let got = DnsPeerMgrRpc::get_export_config(
|
||||
mgr.0.as_ref(),
|
||||
BaseController::default(),
|
||||
GetExportConfigRequest {},
|
||||
)
|
||||
.await
|
||||
.expect("get_export_config should succeed");
|
||||
|
||||
assert_eq!(got, global_ctx.dns_export_config());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_self_peer_marks_dirty_only() {
|
||||
let peer_mgr = create_peer_manager_with_zone(
|
||||
"self-peer",
|
||||
"self-zone.test",
|
||||
Ipv4Addr::new(10, 0, 0, 1),
|
||||
)
|
||||
.await;
|
||||
let mgr = DnsPeerMgr::new(peer_mgr.clone(), peer_mgr.get_global_ctx());
|
||||
|
||||
mgr.dirty.reset();
|
||||
mgr.try_refresh(peer_mgr.my_peer_id()).await.unwrap();
|
||||
|
||||
assert!(mgr.dirty.peek());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_missing_route_noop_and_not_dirty() {
|
||||
let peer_mgr = create_peer_manager_with_zone(
|
||||
"solo-peer",
|
||||
"solo-zone.test",
|
||||
Ipv4Addr::new(10, 0, 0, 2),
|
||||
)
|
||||
.await;
|
||||
let mgr = DnsPeerMgr::new(peer_mgr, get_mock_global_ctx());
|
||||
|
||||
mgr.dirty.reset();
|
||||
mgr.try_refresh(987_654).await.unwrap();
|
||||
|
||||
assert!(!mgr.dirty.peek());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_same_digest_skips_fetch_and_not_mark_dirty() {
|
||||
let local = create_peer_manager_with_zone(
|
||||
"local-same",
|
||||
"local-same.test",
|
||||
Ipv4Addr::new(10, 0, 1, 1),
|
||||
)
|
||||
.await;
|
||||
let remote = create_peer_manager_with_zone(
|
||||
"remote-same",
|
||||
"remote-same.test",
|
||||
Ipv4Addr::new(10, 0, 1, 2),
|
||||
)
|
||||
.await;
|
||||
|
||||
connect_peer_manager(local.clone(), remote.clone()).await;
|
||||
wait_route_appear(local.clone(), remote.clone())
|
||||
.await
|
||||
.expect("route should appear");
|
||||
|
||||
let remote_id = remote.my_peer_id();
|
||||
let remote_route_dns = local
|
||||
.get_route()
|
||||
.get_peer_info(remote_id)
|
||||
.await
|
||||
.expect("remote route should exist")
|
||||
.dns;
|
||||
|
||||
let mgr = DnsPeerMgr::new(local, get_mock_global_ctx());
|
||||
mgr.peers
|
||||
.insert(
|
||||
remote_id,
|
||||
DnsPeerInfo {
|
||||
digest: remote_route_dns
|
||||
.try_into()
|
||||
.expect("route dns digest should be 32 bytes"),
|
||||
zones: vec![valid_zone_data("cached-same.test", "10.0.1.9")],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
mgr.dirty.reset();
|
||||
mgr.try_refresh(remote_id).await.unwrap();
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
assert!(!mgr.dirty.peek());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_remote_peer_fetches_and_updates_snapshot() {
|
||||
let local = create_peer_manager_with_zone(
|
||||
"local-refresh",
|
||||
"local-refresh.test",
|
||||
Ipv4Addr::new(10, 0, 2, 1),
|
||||
)
|
||||
.await;
|
||||
let remote = create_peer_manager_with_zone(
|
||||
"remote-refresh",
|
||||
"remote-export.test",
|
||||
Ipv4Addr::new(10, 0, 2, 2),
|
||||
)
|
||||
.await;
|
||||
|
||||
let local_dns = DnsPeerMgr::new(local.clone(), local.get_global_ctx());
|
||||
let remote_dns = DnsPeerMgr::new(remote.clone(), remote.get_global_ctx());
|
||||
remote_dns.register();
|
||||
|
||||
connect_peer_manager(local.clone(), remote.clone()).await;
|
||||
wait_route_appear(local.clone(), remote.clone())
|
||||
.await
|
||||
.expect("route should appear");
|
||||
|
||||
local_dns.dirty.reset();
|
||||
local_dns.try_refresh(remote.my_peer_id()).await.unwrap();
|
||||
|
||||
assert!(local_dns.dirty.peek());
|
||||
let snapshot = local_dns.snapshot();
|
||||
assert!(
|
||||
snapshot
|
||||
.zones
|
||||
.iter()
|
||||
.any(|z| z.content.contains("$ORIGIN remote-export.test"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_peer_refresh_updates_only_target_peer_snapshot_data() {
|
||||
let local = create_peer_manager_with_zone(
|
||||
"local-multi-refresh",
|
||||
"local-multi-refresh.test",
|
||||
Ipv4Addr::new(10, 2, 0, 1),
|
||||
)
|
||||
.await;
|
||||
let peer_a =
|
||||
create_peer_manager_with_zone("peer-a", "remote-a.test", Ipv4Addr::new(10, 2, 0, 2))
|
||||
.await;
|
||||
let peer_b =
|
||||
create_peer_manager_with_zone("peer-b", "remote-b.test", Ipv4Addr::new(10, 2, 0, 3))
|
||||
.await;
|
||||
|
||||
let local_dns = DnsPeerMgr::new(local.clone(), local.get_global_ctx());
|
||||
let peer_a_dns = DnsPeerMgr::new(peer_a.clone(), peer_a.get_global_ctx());
|
||||
peer_a_dns.register();
|
||||
|
||||
connect_peer_manager(local.clone(), peer_a.clone()).await;
|
||||
connect_peer_manager(local.clone(), peer_b.clone()).await;
|
||||
wait_route_appear(local.clone(), peer_a.clone())
|
||||
.await
|
||||
.expect("route to peer_a should appear");
|
||||
wait_route_appear(local.clone(), peer_b.clone())
|
||||
.await
|
||||
.expect("route to peer_b should appear");
|
||||
|
||||
local_dns.try_refresh(peer_a.my_peer_id()).await.unwrap();
|
||||
|
||||
let snapshot = local_dns.snapshot();
|
||||
assert!(
|
||||
snapshot
|
||||
.zones
|
||||
.iter()
|
||||
.any(|z| z.content.contains("$ORIGIN remote-a.test"))
|
||||
);
|
||||
assert!(
|
||||
!snapshot
|
||||
.zones
|
||||
.iter()
|
||||
.any(|z| z.content.contains("$ORIGIN remote-b.test"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_peer_refresh_failure_invalidates_only_target_peer_cache() {
|
||||
let local = create_peer_manager_with_zone(
|
||||
"local-invalidate",
|
||||
"local-invalidate.test",
|
||||
Ipv4Addr::new(10, 2, 1, 1),
|
||||
)
|
||||
.await;
|
||||
let fail_peer = create_peer_manager_with_zone(
|
||||
"peer-fail",
|
||||
"peer-fail.test",
|
||||
Ipv4Addr::new(10, 2, 1, 2),
|
||||
)
|
||||
.await;
|
||||
let keep_peer = create_peer_manager_with_zone(
|
||||
"peer-keep",
|
||||
"peer-keep.test",
|
||||
Ipv4Addr::new(10, 2, 1, 3),
|
||||
)
|
||||
.await;
|
||||
|
||||
let local_dns = DnsPeerMgr::new(local.clone(), local.get_global_ctx());
|
||||
local_dns.register();
|
||||
let keep_dns = DnsPeerMgr::new(keep_peer.clone(), keep_peer.get_global_ctx());
|
||||
keep_dns.register();
|
||||
|
||||
let fail_id = fail_peer.my_peer_id();
|
||||
let keep_id = keep_peer.my_peer_id();
|
||||
|
||||
local_dns
|
||||
.peers
|
||||
.insert(
|
||||
fail_id,
|
||||
DnsPeerInfo {
|
||||
digest: [1; 32],
|
||||
zones: vec![valid_zone_data("cached-fail.test", "10.2.1.20")],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
local_dns
|
||||
.peers
|
||||
.insert(
|
||||
keep_id,
|
||||
DnsPeerInfo {
|
||||
digest: [2; 32],
|
||||
zones: vec![valid_zone_data("cached-keep.test", "10.2.1.21")],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
connect_peer_manager(local.clone(), fail_peer.clone()).await;
|
||||
connect_peer_manager(local.clone(), keep_peer.clone()).await;
|
||||
wait_route_appear(local.clone(), fail_peer.clone())
|
||||
.await
|
||||
.expect("route to fail_peer should appear");
|
||||
wait_route_appear(local.clone(), keep_peer.clone())
|
||||
.await
|
||||
.expect("route to keep_peer should appear");
|
||||
|
||||
local_dns.dirty.reset();
|
||||
local_dns
|
||||
.refresh(fail_id, Default::default(), Default::default())
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(local_dns.dirty.peek());
|
||||
assert!(local_dns.peers.get(&fail_id).await.is_none());
|
||||
assert!(local_dns.peers.get(&keep_id).await.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_peer_mixed_digest_changes_only_mark_for_changed_peer() {
|
||||
let local = create_peer_manager_with_zone(
|
||||
"local-mixed",
|
||||
"local-mixed.test",
|
||||
Ipv4Addr::new(10, 2, 2, 1),
|
||||
)
|
||||
.await;
|
||||
let changed_peer = create_peer_manager_with_zone(
|
||||
"peer-changed",
|
||||
"peer-changed.test",
|
||||
Ipv4Addr::new(10, 2, 2, 2),
|
||||
)
|
||||
.await;
|
||||
let unchanged_peer = create_peer_manager_with_zone(
|
||||
"peer-unchanged",
|
||||
"peer-unchanged.test",
|
||||
Ipv4Addr::new(10, 2, 2, 3),
|
||||
)
|
||||
.await;
|
||||
|
||||
let local_dns = DnsPeerMgr::new(local.clone(), local.get_global_ctx());
|
||||
let changed_dns = DnsPeerMgr::new(changed_peer.clone(), changed_peer.get_global_ctx());
|
||||
let unchanged_dns =
|
||||
DnsPeerMgr::new(unchanged_peer.clone(), unchanged_peer.get_global_ctx());
|
||||
changed_dns.register();
|
||||
unchanged_dns.register();
|
||||
|
||||
connect_peer_manager(local.clone(), changed_peer.clone()).await;
|
||||
connect_peer_manager(local.clone(), unchanged_peer.clone()).await;
|
||||
wait_route_appear(local.clone(), changed_peer.clone())
|
||||
.await
|
||||
.expect("route to changed_peer should appear");
|
||||
wait_route_appear(local.clone(), unchanged_peer.clone())
|
||||
.await
|
||||
.expect("route to unchanged_peer should appear");
|
||||
|
||||
let unchanged_id = unchanged_peer.my_peer_id();
|
||||
let unchanged_digest = local
|
||||
.get_route()
|
||||
.get_peer_info(unchanged_id)
|
||||
.await
|
||||
.expect("unchanged route should exist")
|
||||
.dns;
|
||||
|
||||
local_dns
|
||||
.peers
|
||||
.insert(
|
||||
changed_peer.my_peer_id(),
|
||||
DnsPeerInfo {
|
||||
digest: [0; 32],
|
||||
zones: vec![valid_zone_data("stale-changed.test", "10.2.2.20")],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
local_dns
|
||||
.peers
|
||||
.insert(
|
||||
unchanged_id,
|
||||
DnsPeerInfo {
|
||||
digest: unchanged_digest
|
||||
.try_into()
|
||||
.expect("route dns digest should be 32 bytes"),
|
||||
zones: vec![valid_zone_data("cached-unchanged.test", "10.2.2.21")],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
local_dns.dirty.reset();
|
||||
local_dns
|
||||
.try_refresh(changed_peer.my_peer_id())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(local_dns.dirty.peek());
|
||||
|
||||
local_dns.dirty.reset();
|
||||
local_dns.try_refresh(unchanged_id).await.unwrap();
|
||||
assert!(!local_dns.dirty.peek());
|
||||
|
||||
let unchanged_cache = local_dns
|
||||
.peers
|
||||
.get(&unchanged_id)
|
||||
.await
|
||||
.expect("unchanged peer cache should stay");
|
||||
assert!(
|
||||
unchanged_cache
|
||||
.zones
|
||||
.iter()
|
||||
.any(|z| z.content.contains("$ORIGIN cached-unchanged.test"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn snapshot_removes_cached_peer_zone_after_tti_expire() {
|
||||
let peer_mgr = create_peer_manager_with_zone(
|
||||
"local-tti",
|
||||
"local-tti.test",
|
||||
Ipv4Addr::new(10, 3, 0, 1),
|
||||
)
|
||||
.await;
|
||||
let global_ctx = peer_mgr.get_global_ctx();
|
||||
let mgr = DnsPeerMgr::new(peer_mgr, global_ctx);
|
||||
|
||||
let cached_peer_id = 66_666;
|
||||
mgr.peers
|
||||
.insert(
|
||||
cached_peer_id,
|
||||
DnsPeerInfo {
|
||||
digest: [6; 32],
|
||||
zones: vec![valid_zone_data("cached-expire.test", "10.3.0.2")],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let before = mgr.snapshot();
|
||||
assert!(
|
||||
before
|
||||
.zones
|
||||
.iter()
|
||||
.any(|z| z.content.contains("$ORIGIN cached-expire.test"))
|
||||
);
|
||||
assert!(
|
||||
before
|
||||
.zones
|
||||
.iter()
|
||||
.any(|z| z.content.contains("$ORIGIN local-tti.test"))
|
||||
);
|
||||
|
||||
let deadline = tokio::time::Instant::now() + DNS_PEER_TTI + Duration::from_secs(3);
|
||||
loop {
|
||||
let now_snapshot = mgr.snapshot();
|
||||
let expired = !now_snapshot
|
||||
.zones
|
||||
.iter()
|
||||
.any(|z| z.content.contains("$ORIGIN cached-expire.test"));
|
||||
if expired {
|
||||
assert!(
|
||||
now_snapshot
|
||||
.zones
|
||||
.iter()
|
||||
.any(|z| z.content.contains("local-tti.test"))
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
assert!(
|
||||
tokio::time::Instant::now() < deadline,
|
||||
"cached peer zone did not expire within expected TTI window"
|
||||
);
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_then_unregister_returns_some() {
|
||||
let peer_mgr = create_peer_manager_with_zone(
|
||||
"register-peer",
|
||||
"register-zone.test",
|
||||
Ipv4Addr::new(10, 1, 0, 1),
|
||||
)
|
||||
.await;
|
||||
let mgr = DnsPeerMgr::new(peer_mgr.clone(), peer_mgr.get_global_ctx());
|
||||
mgr.register();
|
||||
assert!(mgr.unregister().is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
## 目标
|
||||
|
||||
将 `instance/dns_server` 重写为单独的 `dns` 模块,为如下的配置项提供支持:
|
||||
|
||||
```toml
|
||||
[dns]
|
||||
name = "localhost" # optional, replaces hostname, default to system hostname
|
||||
domain = "localdomain" # optional, replaces tld_dns_zone, default to et.net
|
||||
|
||||
addresses = [
|
||||
"100.100.100.101:53",
|
||||
] # optional, default to [ "100.100.100.101:53" ]
|
||||
# any UDP packet or ICMP packet to these addresses will be hijacked by the dns server
|
||||
# the server does *not* bind to/listen on these addresses!
|
||||
|
||||
listeners = [
|
||||
] # optional, default to empty
|
||||
# let the dns server bind to these addresses
|
||||
# could be useful when no_tun = true
|
||||
|
||||
# these two options supersede accept_dns
|
||||
# setting both of them to empty is equivalent to set accept_dns = false, but zones are still broadcasted
|
||||
|
||||
# this policy applies to all zones with origin "example.com"
|
||||
[dns."example.com".import]
|
||||
whitelist = ["*"]
|
||||
blacklist = []
|
||||
disabled = true # optional, whether to reject zones with this origin from connected peers, default to false
|
||||
recursive = true # optional, apply this policy to all subzones, default to false
|
||||
|
||||
[[dns.zone]]
|
||||
origin = "example.com" # required, name of the zone
|
||||
ttl = 3600 # optional, default to 0
|
||||
records = [
|
||||
"www 60 IN A 123.123.123.123",
|
||||
"app IN CNAME www",
|
||||
] # optional, custom DNS records
|
||||
|
||||
forwarders = [
|
||||
"1.1.1.1",
|
||||
] # optional, forward DNS requests to these servers
|
||||
|
||||
fallthrough = false # optional, whether to fall back to next zone (with same origin) if request doesn't match any record in this zone, default to true
|
||||
|
||||
# this policy applies to the current zone block
|
||||
[dns.zone.export] # if present, export this zone to connected peers
|
||||
whitelist = ["*"] # optional
|
||||
blacklist = [] # optional
|
||||
|
||||
# same zone, but not exported
|
||||
[[dns.zone]]
|
||||
origin = "example.com"
|
||||
|
||||
forwarders = [
|
||||
"tcp://192.168.0.53:5353",
|
||||
]
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><h2>计划和进展</h2></summary>
|
||||
|
||||
每个 peer 会默认拥有一个专用 zone,它的 origin 是这个 peer 的 fqdn,唯一的记录是指向该 peer 的 ip 的 A、AAAA 记录
|
||||
|
||||
## protobuf
|
||||
|
||||
- `ZoneData`:包含 Zone 配置,以及一个 ID,该 ID 在读取 TOML 时生成
|
||||
- `GetExportConfigResponse` (`DnsExportConfig`):包含全部 export 的 `ZoneData`(特别地,包含专用 zone)、该 peer 的 fqdn
|
||||
- `HeartbeatRequest`: DnsNode 发送的心跳,包含:id、digest、`Option<Snapshot>`
|
||||
- `DnsSnapshot`: 所有 DnsServer 需要的配置
|
||||
|
||||
## RoutePeerInfo
|
||||
|
||||
为预防用户提交大量自定义 DNS 记录导致 RoutePeerInfo 泛洪造成带宽压力:
|
||||
|
||||
- 在 `RoutePeerInfo` 中只保存本地 DNS 配置的 hash
|
||||
- 收到 `RoutePeerInfo` 后读取其中 DNS 的 hash,若与本地不同,通过 RPC 拉取 Peer 的 DNS 配置
|
||||
|
||||
## DnsNode
|
||||
|
||||
1. - [x] 监听配置更新/IP 地址变化,重建快照
|
||||
2. - [x] (`GlobalCtx` 的扩展 trait) 使用自己的 name 和 domain 创建一个专用 zone,让 name 指向自身 IP(为 DNS 一致性避免使用 127.0.0.1 作为 IP,若没有 IP 则不创建这个 zone)
|
||||
3. - [x] 每次获得 RoutePeerInfo 时,读取其中的 dns 字段(和一些别的身份标记字段),这是远程 Peer 的 dns 配置(不含 addresses 和 listeners)的 digest
|
||||
- [x] 接收后检查 digest 和本地配置是否一致,如果一致,不做修改,否则标记 dirty,下一次心跳时将重建快照
|
||||
4. - [x] 每隔一小段时间向 DnsServer 发送心跳和当前 digest:
|
||||
1. 如果没有 dirty 标记,心跳不含 snapshot;
|
||||
2. 如果有 dirty 标记,重建 snapshot 并在心跳中包含;
|
||||
3. 如果 DnsServer 返回 resync,立刻重新发送带有 Snapshot 的心跳
|
||||
5. - [x] 一个 RPC 接口,供 Peer 拉取 DNS 配置
|
||||
6. - [x] 一个独立循环,用于选举 DnsServer,每次循环尝试绑定 DNS_SERVER_RPC_ADDR 监听 RPC 请求
|
||||
1. 一台机器上所有 EasyTier 实例一起尝试绑定 DNS_SERVER_RPC_ADDR,绑定成功的那个就启动 DnsServer(当然也启动 DnsNode),失败的那些就只有 DnsNode
|
||||
2. 每隔一小段时间或者 DnsNode 心跳失败(notify)后立刻尝试 bind,如果 bind 成功就说明 DnsServer 真挂了,那就自己在这个已有的 SocketAddr 上启动 DnsServer(忽略 bind 失败或启动失败,启动失败就直接释放 socket),这样才能保证服务不断
|
||||
|
||||
## DnsServer
|
||||
|
||||
1. - [x] 提供一个 RPC 接口接受 DnsNode 的心跳,如果心跳 digest 和本地不符则返回 resync
|
||||
2. - [x] 收到含有 snapshot 的心跳时替换本地配置;如果 snapshot 中的 listeners 或者 addresses 不同则 rebind
|
||||
3. - [x] (`moka::Cache`) 持续检查是否有过期(丢失心跳)的 DnsNode,需要把这些 DnsNode 提供的所有配置清除
|
||||
4. - [x] 每次更新 zone 时自动添加 root zone
|
||||
- [x] (`Zone::system`) 并把它的 forwarder 设置为系统 DNS
|
||||
5. - [x] 使用 snapshot 更新 zone。不用合并同名 zone,直接用 Zone 结构体提供的 ChainedZoneHandler 按顺序插入 Catalog 就行,不过注意要先插入 MemoryZoneHandler,这些都是 records,后插入 ForwardZoneHandler,这都是 forwarders
|
||||
6. - [x] 更新 zone 的时候自动去掉 forwarder 中导致回环的那些,就是把 addresses 和 listeners 去掉(root zone 也需要这个逻辑)
|
||||
7. - [x] 内部接口,控制 DnsServer 是否 bind 到某些 socket(也就是配置中的 listeners)
|
||||
8. - [x] Listeners 绑定失败打印日志(失败一个打印一次然后就跳过),即便这时 addresses 为空也不要停机。(否则释放 socket 绑定后会有 instance 抢占 socket 试图启动 server,然后就死循环)
|
||||
9. - [x] 内部接口,更新 addresses。目前这些用来 hijack 的 addresses 都是只支持 udp 简单查询,就是一个 UDP 包查询,tcp 完全不管。但是可以支持除了 53 之外的端口,这个不难。
|
||||
- [x] 并且给 tun 添加删除这些 addresses 的路由
|
||||
10. - [x] 启动时,往 packet pipeline 上挂一个 filter,和目前 magic dns 的操作一样,给 addresses 添加路由并劫持所有目的为配置中 addresses 的 UDP 包,直接作为 DNS request 读取并交给 DnsServer 解析
|
||||
- [x] 这个 addresses 可能还得 append 到 resolv.conf 之类的地方
|
||||
11. - [x] Addresses 和 Listeners 更新时~~需要检查所有 zone 的 forwarder~~直接更新所有 zone,之前为了避免回环可能去掉了一些 forwarder,或者有新的 forwarder 要去除
|
||||
|
||||
此外,还有以下几个设计要点:
|
||||
|
||||
- Zone 允许只有 forwarder,这时候就是纯转发器
|
||||
- Zone 允许没有 forwarder,这时候要检查是不是有 SOA 和 NS 记录,如果没有可能需要添加?
|
||||
- 另一种方案是 DnsNode 挂 filter,自己处理 UDP 劫持,用某种方式(如 RPC)把 DNS 请求代理给 DnsServer,该方案的优势在于完全解耦 DnsServer 的实现,特别是解决了 DnsServer 所在实例可能 no_tun 的问题,缺点是:
|
||||
- 性能更差
|
||||
- 操作路由表或 /etc/resolv.conf 时会有多个 instance 同时修改,修改结果没有确定性
|
||||
- DnsServer 仍然需要得知 addresses 以进行回环检测
|
||||
- debug 更麻烦
|
||||
- 难以实现策略 DNS,比如不同来源的 DNS 请求走不同的 zone
|
||||
|
||||
另外任何关于系统 DNS 的操作,清理都参考现有的 magic dns。
|
||||
|
||||
## 已知但无需/无计划解决的问题
|
||||
|
||||
- the ttl option isn't working because of https://github.com/hickory-dns/hickory-dns/pull/3450
|
||||
- [minor] address 路由绑定必须在有 tun 的实例上做;listener 绑定则与 tun 无关,现有竞选机制无法保证有 tun 的实例能优先启动 DnsServer
|
||||
- 不妨假设大多数情况下一台机器上所有实例的 no_tun 设置相同,这时候这个问题实际上不存在
|
||||
- [minor] DnsServer 更新 zone 的时候需要更精细的合并/去重控制,如延迟低者/本地优先
|
||||
- [minor] 更新 forwarder 时还需要检查间接回环,如 DNS 请求发送给某个 Peer,这个 Peer 又把请求转发回自己了
|
||||
- [minor] 防止死锁/挂起的 DnsServer 占用 socket
|
||||
- ~~[minor] RoutePeerInfo 可能不能过大~~
|
||||
- [minor] 增量 Zone 更新
|
||||
- DNS 策略
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h2>Related Issues</h2></summary>
|
||||
|
||||
- closes https://github.com/EasyTier/EasyTier/issues/742
|
||||
- closes https://github.com/EasyTier/EasyTier/issues/771
|
||||
- closes https://github.com/EasyTier/EasyTier/issues/927
|
||||
- closes https://github.com/EasyTier/EasyTier/issues/1071
|
||||
- closes https://github.com/EasyTier/EasyTier/issues/1142
|
||||
- closes https://github.com/EasyTier/EasyTier/issues/1322
|
||||
- closes https://github.com/EasyTier/EasyTier/issues/1381
|
||||
- closes https://github.com/EasyTier/EasyTier/issues/1488
|
||||
- closes https://github.com/EasyTier/EasyTier/issues/1597
|
||||
- closes https://github.com/EasyTier/EasyTier/issues/1645
|
||||
- closes https://github.com/EasyTier/EasyTier/issues/1764
|
||||
- closes https://github.com/EasyTier/EasyTier/issues/1814
|
||||
- closes https://github.com/EasyTier/EasyTier/issues/1826
|
||||
- closes https://github.com/EasyTier/EasyTier/issues/2004
|
||||
|
||||
---
|
||||
|
||||
- (maybe) related to https://github.com/EasyTier/EasyTier/issues/937
|
||||
- (maybe) related to https://github.com/EasyTier/EasyTier/issues/1016
|
||||
- (maybe) related to https://github.com/EasyTier/EasyTier/issues/1348
|
||||
- (maybe) related to https://github.com/EasyTier/EasyTier/issues/1699
|
||||
- (maybe) related to https://github.com/EasyTier/EasyTier/issues/1873
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,562 @@
|
||||
# EasyTier DNS 模块设计说明(重构版)
|
||||
|
||||
> 本文档基于 `easytier/src/dns` 当前代码实现与 `plan.md`。
|
||||
> 当前先给出第一部分:**整体架构与基础逻辑**。
|
||||
|
||||
## 1. 模块目标与定位
|
||||
|
||||
`dns` 模块是对旧 `instance/dns_server` 方案的重构,目标是把 DNS 能力从“单点功能”升级为“可同步、可扩展、可自治”的子系统。它同时承担三类职责:
|
||||
|
||||
1. **本机 DNS 服务能力**:
|
||||
- 能监听配置中的 `listeners`(UDP/TCP)作为标准 DNS server。
|
||||
- 能对配置中的 `addresses` 做流量劫持(UDP DNS + ICMP echo)。
|
||||
|
||||
2. **多 Peer DNS 配置同步能力**:
|
||||
- 每个实例作为 `DnsNode` 生成快照并定期心跳。
|
||||
- 机器上被选举出的 `DnsServer` 聚合所有 Node 快照并动态重建 Catalog。
|
||||
|
||||
3. **系统 DNS 接入能力**(tun 场景):
|
||||
- 把 DNS nameserver/search/match domain 写入系统配置(当前主要是 Windows/macOS,Linux 仍在演进中)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 顶层架构(角色分层)
|
||||
|
||||
从职责上看,模块分成 4 层:
|
||||
|
||||
- **配置层**(`config/*`)
|
||||
- 解析 TOML 的 `[dns]`、`[[dns.zone]]`、策略字段。
|
||||
- 产出 `DnsConfig`、`ZoneConfig`,并提供默认值(如默认域名 `et.net`、默认地址 `100.100.100.101:53`)。
|
||||
|
||||
- **节点层(控制面)**(`node.rs` + `peer_mgr.rs`)
|
||||
- `DnsNode`:本实例的 DNS 控制器,负责选举、心跳、事件监听、重建 snapshot。
|
||||
- `DnsPeerMgr`:维护远端 peer 的 DNS 摘要与配置拉取,拼装 `DnsSnapshot`。
|
||||
|
||||
- **服务层(数据面)**(`server.rs` + `node_mgr.rs` + `zone.rs`)
|
||||
- `DnsServer`:真正处理 DNS 请求、维护监听 socket、管理 hijack addresses。
|
||||
- `DnsNodeMgr`:服务端的快照管理器,接收 Node 心跳,维护节点 TTL 与 dirty 状态。
|
||||
- `Zone`/`ZoneGroup`:把 records + forwarders 变成 Hickory `ZoneHandler` 并装配 `Catalog`。
|
||||
|
||||
- **系统集成层**(`system/*`)
|
||||
- 将当前 DNS 配置下发到 OS(`SystemConfigurator` 抽象)。
|
||||
- 服务退出/变更时负责清理或覆盖。
|
||||
|
||||
---
|
||||
|
||||
## 3. 关键对象与数据模型
|
||||
|
||||
- **`DnsConfig`**(`config/dns.rs`)
|
||||
- 核心字段:`zones`、`policies`、`name`、`domain`、`addresses`、`listeners`。
|
||||
- `get_fqdn()` 用 `name + domain` 生成本机 FQDN。
|
||||
|
||||
- **`ZoneData` / `Zone`**(`proto/dns.proto` + `zone.rs`)
|
||||
- `ZoneData` 是网络传输模型(protobuf),含 `id/origin/ttl/records/forwarders`。
|
||||
- `Zone` 是运行期模型:
|
||||
- `records -> InMemoryZoneHandler`
|
||||
- `forwarders -> ForwardZoneHandler`
|
||||
- 同 origin 可链式共存(ChainedZoneHandler 语义)。
|
||||
|
||||
- **`DnsSnapshot`**(`proto/dns.proto`)
|
||||
- Node 发给 Server 的完整状态:`zones + addresses + listeners`。
|
||||
|
||||
- **`HeartbeatRequest`**
|
||||
- 发送 `id + digest + optional snapshot`。
|
||||
- digest 一致时可只发轻量心跳,不带 snapshot。
|
||||
|
||||
- **`DirtyFlag`**(`utils/dirty.rs`)
|
||||
- 全模块统一的“脏标记 + 通知器”,用于节流和增量触发(不是每次事件都全量重建)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 基础运行逻辑(主链路)
|
||||
|
||||
### 4.1 本地节点启动
|
||||
|
||||
`Instance` 在 `magic-dns` feature 下创建并启动 `DnsNode`。`DnsNode` 启动后并行跑两个循环:
|
||||
|
||||
1. **选举循环**(`run_election`)
|
||||
- 周期尝试绑定固定 RPC 地址 `tcp://127.0.0.1:49813`。
|
||||
- 绑定成功者成为本机 `DnsServer` 持有者;失败者继续只做 `DnsNode`。
|
||||
|
||||
2. **主循环**(`run`)
|
||||
- 监听配置变更/IP 变化/PeerInfo 更新。
|
||||
- 维护 dirty 状态并按节奏发送 heartbeat。
|
||||
|
||||
### 4.2 快照构建与同步
|
||||
|
||||
`DnsPeerMgr::snapshot()` 组装快照:
|
||||
|
||||
- 本机 zones:`dns_iter_zones()`(包含“自有专用 zone” + 用户配置 zone)。
|
||||
- 远端 zones:从 peer RPC 拉取并缓存的 export zones。
|
||||
- 本机 `addresses/listeners`:来自 `DnsConfig`。
|
||||
|
||||
Node 发送 heartbeat 时:
|
||||
|
||||
- dirty 或首包 -> 带 `snapshot` 全量发送。
|
||||
- 未 dirty -> 只发 `digest`(轻量心跳)。
|
||||
- Server 返回 `resync=true` -> 立刻补发全量 snapshot。
|
||||
|
||||
### 4.3 服务端聚合与生效
|
||||
|
||||
`DnsNodeMgr` 收到 heartbeat 后:
|
||||
|
||||
- 若 snapshot digest 改变:更新节点缓存并标记 dirty(catalog/addresses/listeners 分开标记)。
|
||||
- 若仅 digest 且本地无该节点或不一致:返回 `resync=true`。
|
||||
|
||||
`DnsServer::run()` 有三个独立 reload 循环:
|
||||
|
||||
- `reload_catalog`:替换 `DynamicCatalog`。
|
||||
- `reload_addresses`:更新 hijack 地址,并尝试下发系统 DNS。
|
||||
- `reload_listeners`:重绑 DNS listener socket。
|
||||
|
||||
这三个循环彼此解耦,避免单一失败阻塞全部 DNS 功能。
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据面请求路径(DNS/ICMP 劫持)
|
||||
|
||||
`DnsServer` 作为 `NicPacketFilter` 挂入 packet pipeline:
|
||||
|
||||
1. 检查目的 IP 是否命中 `addresses`。
|
||||
2. UDP:
|
||||
- 解析 DNS 请求 -> 投递给 `catalog.handle_request()`。
|
||||
- 用响应覆盖原 UDP payload,修正长度与校验和。
|
||||
3. ICMP:
|
||||
- 对 EchoRequest 直接改写为 EchoReply。
|
||||
4. 最后交换源/目的 IP,并把包回注到本机 peer pipeline。
|
||||
|
||||
这使得 `addresses` 不要求真实 bind/listen,也能作为“虚拟 DNS 入口地址”。
|
||||
|
||||
---
|
||||
|
||||
## 6. 可靠性与收敛机制
|
||||
|
||||
- **服务高可用(单机维度)**:
|
||||
- 任何实例都可竞选 Server;现任退出后其余实例会重试接管。
|
||||
|
||||
- **配置高效同步(全网维度)**:
|
||||
- `RoutePeerInfo` 只传播 DNS digest,不直接携带全量记录。
|
||||
- digest 变化后才通过 RPC 拉取详情,降低路由泛洪压力。
|
||||
|
||||
- **自动过期清理**:
|
||||
- `DnsNodeMgr` 通过 `moka::Cache` TTL 自动淘汰失联节点配置(心跳过期)。
|
||||
|
||||
- **回环防护**:
|
||||
- 重建 zones 时会从 forwarders 中剔除本地 `addresses/listeners`,避免显式自环。
|
||||
|
||||
---
|
||||
|
||||
## 7. 当前实现状态(对应 plan.md)
|
||||
|
||||
从代码可见,以下主干能力已经落地:
|
||||
|
||||
- Node/Server 双角色、选举、心跳与 resync。
|
||||
- 快照机制(zone/addresses/listeners)与 digest 驱动同步。
|
||||
- 自有专用 zone 自动生成与 export。
|
||||
- Catalog 动态替换、listener/address 分离热更新。
|
||||
- UDP DNS 劫持 + ICMP 响应。
|
||||
- forwarder 的本地回环剔除。
|
||||
|
||||
仍在计划中的重点:
|
||||
|
||||
- 系统 DNS 配置改造(尤其 Linux 路径统一与清理语义完善)。
|
||||
- 更完整的单元测试覆盖与 CLI 状态输出。
|
||||
|
||||
---
|
||||
|
||||
## 8. 配置层详解(`config/*`)
|
||||
|
||||
这一层负责把 TOML 配置映射成可校验、可传播、可计算 digest 的运行模型。
|
||||
|
||||
### 8.1 常量与默认值(`config/mod.rs`)
|
||||
|
||||
- `DNS_DEFAULT_TLD = et.net.`:`domain` 缺省值。
|
||||
- `DNS_DEFAULT_ADDRESS = udp://100.100.100.101:53`:`addresses` 缺省值。
|
||||
- `DNS_SERVER_RPC_ADDR = tcp://127.0.0.1:49813`:本机 DNS Server 选举地址。
|
||||
- `DNS_SERVER_ELECTION_INTERVAL = 5s`:选举重试周期。
|
||||
- `DNS_SUPPORTED_PROTOCOLS = [Udp, Tcp]`:地址/转发器协议白名单。
|
||||
|
||||
### 8.2 `DnsConfig`(`config/dns.rs`)
|
||||
|
||||
`DnsConfig` 是 `[dns]` 根配置,关键点如下:
|
||||
|
||||
- `zones: Vec<ZoneConfig>` 对应 `[[dns.zone]]`。
|
||||
- `policies: HashMap<LowerName, DnsPolicyConfig>` 用 `#[serde(flatten)]` 承接 `[dns."origin".import]` 形式策略。
|
||||
- `name/domain` 组合 FQDN。
|
||||
- `addresses/listeners` 使用 `NameServerAddrGroup`(支持 `ip`、`ip:port`、`udp://`、`tcp://` 解析)。
|
||||
|
||||
约束与语义:
|
||||
|
||||
- `deserialize_addresses()` 强制 `addresses` 只能是 UDP(与当前 hijack 数据面能力一致)。
|
||||
- `get_name()`:若 `name` 为空,回退系统 hostname。
|
||||
- `get_fqdn()`:将 `name` 拼接 `domain` 得到完整域名。
|
||||
- `set_fqdn()`:反向拆分 FQDN 到 `name` 和 `domain`。
|
||||
|
||||
### 8.3 `ZoneConfig` 与专用 Zone(`config/zone.rs`)
|
||||
|
||||
`ZoneConfig` 由两部分构成:
|
||||
|
||||
- `ZoneData`:用于 protobuf 传输(`id/origin/ttl/records/forwarders`)。
|
||||
- `ZoneConfigInner`:配置层字段(含 policy)。
|
||||
|
||||
关键设计:
|
||||
|
||||
- `TryFrom<ZoneConfigInner> for ZoneConfig` 会立即调用 `Zone::try_from(&ZoneData)` 做语法校验,确保“能进配置就能进运行时”。
|
||||
- `ZoneConfig::dedicated(...)` 用于自动生成“本节点专用 zone”:
|
||||
- `origin = 节点 fqdn`
|
||||
- records 自动填充 `@ IN A/AAAA ...`
|
||||
- `policy.export = Some(default)`,默认可导出给 peers。
|
||||
|
||||
### 8.4 策略结构体现状(`config/policy.rs`)
|
||||
|
||||
策略模型已就位,但功能并未完全落实到执行路径:
|
||||
|
||||
- `AclPolicy { whitelist, blacklist }`
|
||||
- `FunctionalityPolicy { disabled }`
|
||||
- `DnsPolicy { recursive }`
|
||||
|
||||
目前代码中的直接使用点主要是:
|
||||
|
||||
- `dns_export_config()` 只检查 `zone.policy.export.is_some()` 决定是否导出。
|
||||
- `import/recursive/acl` 仍处于待完整落地状态(与 `plan.md` 的 TODO 对齐)。
|
||||
|
||||
### 8.5 `DnsGlobalCtxExt`:配置到发布面的桥(`config/dns.rs`)
|
||||
|
||||
`GlobalCtx` 被扩展出 3 个关键方法:
|
||||
|
||||
- `dns_self_zone()`:基于当前 IP 与 FQDN 生成专用 zone。
|
||||
- `dns_iter_zones()`:`self_zone + 用户配置 zones`。
|
||||
- `dns_export_config()`:从 `dns_iter_zones()` 中筛选可导出的 zones,并附加本机 `fqdn`。
|
||||
|
||||
这三个方法是后续 `RoutePeerInfo.dns` digest 与 RPC 拉取的源头。
|
||||
|
||||
---
|
||||
|
||||
## 9. 节点控制面详解(`node.rs` + `peer_mgr.rs`)
|
||||
|
||||
### 9.1 `DnsNode` 初始化与 RPC 注册
|
||||
|
||||
`DnsNode::new(...)` 会创建 `DnsPeerMgr`,并把 `DnsPeerMgrRpcServer` 注册到 peer RPC registry。
|
||||
这使“我给别人提供 DNS 导出配置”与“我向别人拉取导出配置”在同一组件闭环。
|
||||
|
||||
### 9.2 选举循环(`DnsNode::run_election`)
|
||||
|
||||
选举逻辑是“抢占固定地址”的单机 leader 机制:
|
||||
|
||||
1. 周期或被 `elect.notify_one()` 触发。
|
||||
2. 尝试 `StandAloneServer(TcpTunnelListener(DNS_SERVER_RPC_ADDR)).serve()`。
|
||||
3. 绑定成功 -> 启动 `DnsServer`,注册 `DnsNodeMgrRpc`,并挂载 NIC packet pipeline。
|
||||
4. `DnsServer` 退出后清理 pipeline,回到选举循环。
|
||||
|
||||
要点:
|
||||
|
||||
- 不依赖外部分布式锁,仅利用本机 socket 独占。
|
||||
- 失败不是错误态,意味着“已有实例担任 Server”。
|
||||
|
||||
### 9.3 主循环(`DnsNode::run`)
|
||||
|
||||
主循环负责“何时重建、何时发全量、何时触发重选举”:
|
||||
|
||||
- 维护 `HeartbeatRequest { id, digest, snapshot? }`。
|
||||
- 基于 `DirtyFlag` 动态调整心跳节奏:
|
||||
- dirty 时更积极(`rr_interval`)
|
||||
- clean 时更快短轮询(`rr_interval / 8`)
|
||||
- 监听 `GlobalCtxEvent`:
|
||||
- `PeerInfoUpdated` -> 并发调用 `mgr.refresh(peer_id)`
|
||||
- IP 变化、配置变化、事件丢失(lagged)-> `dirty.mark()`
|
||||
- 心跳失败 -> 触发一次选举通知(可能是 Server 挂了)。
|
||||
|
||||
### 9.4 心跳协议(`DnsNode::heartbeat`)
|
||||
|
||||
发送策略:
|
||||
|
||||
- 首次或 dirty -> `heartbeat.update(self.mgr.snapshot())`,发送全量 snapshot。
|
||||
- 非 dirty -> 尽量只发 digest(轻量包)。
|
||||
|
||||
服务端响应:
|
||||
|
||||
- `resync = true` 时,客户端立刻再发一次带 snapshot 的心跳。
|
||||
|
||||
这实现了“正常轻量保活 + 状态漂移时快速自愈”。
|
||||
|
||||
### 9.5 `DnsPeerMgr`:远端配置拉取与去抖
|
||||
|
||||
`DnsPeerMgr` 核心职责:
|
||||
|
||||
- 本地缓存:`Cache<PeerId, DnsPeerInfo>`(TTL = 3s)。
|
||||
- `refresh(peer_id)`:
|
||||
- 先读路由里的 `route.dns` digest。
|
||||
- 若与本地缓存一致则跳过 RPC。
|
||||
- 不一致才调用 `fetch(peer_id)` 拉取 `GetExportConfigResponse`。
|
||||
- `snapshot()`:拼接
|
||||
- 本机 zones(`dns_iter_zones()`)
|
||||
- 所有远端缓存 zones
|
||||
- 本机 addresses/listeners
|
||||
|
||||
这正是 `plan.md` 中“RoutePeerInfo 仅携带 hash,详情按需拉取”的落地实现。
|
||||
|
||||
---
|
||||
|
||||
## 10. 服务聚合与数据面详解(`node_mgr.rs` + `server.rs`)
|
||||
|
||||
### 10.1 `DnsNodeMgr`:服务器侧状态机
|
||||
|
||||
`DnsNodeMgr` 保存每个 Node 的最新状态:
|
||||
|
||||
- `nodes: Cache<Uuid, DnsNodeInfo>`(TTL = 5s,心跳过期即自动淘汰)。
|
||||
- `DnsNodeInfo = digest + zones + addresses + listeners`。
|
||||
- `dirty` 分三类:`catalog`、`addresses`、`listeners`。
|
||||
|
||||
`heartbeat()` 判定逻辑:
|
||||
|
||||
- 请求带 snapshot:
|
||||
- 反序列化为 `DnsNodeInfo`。
|
||||
- digest 变化才更新缓存并打脏标记。
|
||||
- 此分支返回 `resync = false`。
|
||||
- 请求不带 snapshot:
|
||||
- 若本地没有该 node 或 digest 不一致 -> `resync = true`。
|
||||
|
||||
### 10.2 Catalog 构建(`DnsNodeMgr::catalog/collect_zones`)
|
||||
|
||||
构建步骤:
|
||||
|
||||
1. 聚合全部节点 zones。
|
||||
2. 追加 `Zone::system()` 作为 root zone。
|
||||
3. 收集本地所有 `addresses + listeners` 形成 `local` 集合。
|
||||
4. 遍历每个 zone 的 forwarders,剔除命中 `local` 的 nameserver(避免显式回环)。
|
||||
5. 以 `origin -> zone_handlers[]` 方式 `upsert` 到 Hickory `Catalog`。
|
||||
|
||||
### 10.3 `DnsServer::run`:三路热重载
|
||||
|
||||
`DnsServer` 使用 3 个异步循环处理不同脏标记:
|
||||
|
||||
- `reload_catalog`:`DynamicCatalog::replace(...)` 原子替换目录。
|
||||
- `reload_addresses`:更新劫持地址集合,并尝试下发系统 DNS。
|
||||
- `reload_listeners`:重建 `ServerFuture` 的 UDP/TCP 监听 socket。
|
||||
|
||||
每路失败都会重新 `mark()` 自己,避免瞬时错误导致永久失效。
|
||||
|
||||
### 10.4 listener/address 的行为边界
|
||||
|
||||
- `listeners`:真正 bind 的服务地址;单个地址 bind 失败会打印错误并跳过,不导致整体停机。
|
||||
- `addresses`:仅用于劫持匹配,不需要 bind;可用于 `no_tun=false` 下的虚拟 DNS 入口。
|
||||
- `addresses` 与 `listeners` 分离,符合 `plan.md` 中“hijack 地址不等于监听地址”的设计。
|
||||
|
||||
### 10.5 NIC 数据面处理(`NicPacketFilter`)
|
||||
|
||||
处理链:
|
||||
|
||||
1. `handle_ip_packet()` 解析 IPv4 头并检查目标 IP 是否属于 hijack 地址集合。
|
||||
2. UDP 分支:
|
||||
- `MessageRequest::from_bytes` 解包 DNS 查询。
|
||||
- 交给 `catalog.handle_request(...)` 获取响应。
|
||||
- 回填 payload,修正 UDP/IP 长度与 checksum。
|
||||
3. ICMP 分支:
|
||||
- EchoRequest 改写为 EchoReply。
|
||||
4. 统一收尾:交换 src/dst IP,并把包路由回本机 `peer_id`。
|
||||
|
||||
该路径让 DNS 响应无需经过用户态 socket recv/send,直接在 packet pipeline 内完成。
|
||||
|
||||
---
|
||||
|
||||
## 11. Zone 组装与权威链详解(`zone.rs`)
|
||||
|
||||
### 11.1 `Zone` 运行时模型
|
||||
|
||||
`Zone` 包含:
|
||||
|
||||
- `id: Uuid`(来源于配置/网络数据)
|
||||
- `origin: LowerName`
|
||||
- `records: BTreeMap<RrKey, RecordSet>`
|
||||
- `forward: Option<ForwardConfig>`
|
||||
|
||||
`PartialEq` 对 `forward` 使用自定义比较(只比较 nameserver 序列),避免与无关字段耦合。
|
||||
|
||||
### 11.2 反序列化与校验(`TryFrom<&ZoneData>`)
|
||||
|
||||
转换过程:
|
||||
|
||||
1. 必须有 `id`,否则报错。
|
||||
2. 用 Hickory `Parser` 解析 zone 文本(origin + RR)。
|
||||
3. 把 `forwarders` URL 转成 `NameServerAddr`,为空则 `forward=None`。
|
||||
|
||||
这确保网络收到的 `ZoneData` 能直接映射成可执行 zone_handler。
|
||||
|
||||
### 11.3 ZoneHandler 构建策略
|
||||
|
||||
- `create_memory_zone_handler()`:仅当 records 非空时创建 `InMemoryZoneHandler`。
|
||||
- `create_forward_zone_handler()`:仅当 forward 非空时创建 `ForwardZoneHandler`。
|
||||
|
||||
因此允许 3 种 zone 形态:
|
||||
|
||||
1. 纯记录(权威回答)
|
||||
2. 纯转发(forward-only)
|
||||
3. 记录 + 转发(链式)
|
||||
|
||||
### 11.4 `ZoneGroup` 与同源链式行为
|
||||
|
||||
- `ZoneGroup::into_groups()` 按 `origin` 分组。
|
||||
- `iter_zone_handlers()` 对每个 zone 按顺序产出:先 memory,再 forward。
|
||||
- `DnsNodeMgr::catalog()` 把同 origin 的多个 zone zone_handler 以数组形式 `upsert`。
|
||||
|
||||
结果是同 origin 下可自然形成 ChainedZoneHandler,不做“硬合并单 Zone”,与 `plan.md` 一致。
|
||||
|
||||
### 11.5 `Zone::system()` 的作用边界
|
||||
|
||||
`Zone::system()` 读取系统 resolver 作为 root zone forwarders。
|
||||
在当前文档范围内仅关注它在 catalog 聚合中的语义:**兜底递归出口**。
|
||||
|
||||
---
|
||||
|
||||
## 12. 文档后续范围
|
||||
|
||||
后续若继续扩写,将集中在以下主题(不再展开 `system/*`):
|
||||
|
||||
1. 策略执行链路补齐:`import/recursive/acl` 如何从配置走到查询路径。
|
||||
2. 测试矩阵梳理:单元测试、集成测试与故障注入测试的覆盖面。
|
||||
3. CLI 状态输出:如何观测 node/server 角色、snapshot digest、zone 来源与健康状态。
|
||||
|
||||
---
|
||||
|
||||
## 13. 策略执行链路现状与缺口
|
||||
|
||||
本节专门回答一个容易误解的问题:**配置里有策略字段,不等于运行时已经完全执行**。
|
||||
|
||||
### 13.1 已生效的策略相关行为
|
||||
|
||||
当前代码中,和策略直接相关且已生效的路径主要有一条:
|
||||
|
||||
- `GlobalCtx::dns_export_config()` 在导出 zones 时仅检查:
|
||||
- `zone.policy.export.is_some()`
|
||||
|
||||
也就是说,当前“导出/不导出”是可工作的,但粒度仍偏粗。
|
||||
|
||||
### 13.2 已建模但尚未完整落地的策略字段
|
||||
|
||||
以下字段在 `config/policy.rs` 已定义,但执行链路尚未完全打通:
|
||||
|
||||
- `import.whitelist / import.blacklist`
|
||||
- `import.disabled`
|
||||
- `import.recursive`
|
||||
- `export` 内更细粒度 ACL
|
||||
|
||||
从调用路径看:
|
||||
|
||||
- `DnsPeerMgr::snapshot()` 只做本地 + 远端 zones 拼接,不做 import/export ACL 过滤。
|
||||
- `DnsNodeMgr::collect_zones()` 只做聚合与回环剔除,不做来源级策略裁剪。
|
||||
- `DnsServer::handle_ip_packet()` 是纯查询执行,不做请求来源与策略绑定。
|
||||
|
||||
### 13.3 代码中的明确信号(TODO)
|
||||
|
||||
当前有两个关键 TODO 信号:
|
||||
|
||||
- `dns_export_config()` 里标注了 `TODO: check policies of parent zones`。
|
||||
- `policy.rs` 中 `AclPolicy`、`recursive` 旁边保留了 TODO 注释。
|
||||
|
||||
这说明作者已经把策略模型前置到配置层,但执行面仍属于“进行中”。
|
||||
|
||||
### 13.4 文档使用建议(给维护者)
|
||||
|
||||
在策略彻底落地前,建议把语义按两层理解:
|
||||
|
||||
1. **已可依赖**:`zone.policy.export.is_some()` 控制是否导出。
|
||||
2. **暂不可依赖**:import/export ACL、recursive、disabled 的全链路行为。
|
||||
|
||||
---
|
||||
|
||||
## 14. 测试体系与覆盖面
|
||||
|
||||
本模块测试不是集中在一个文件,而是“按组件就地内嵌”。
|
||||
|
||||
### 14.1 测试分布
|
||||
|
||||
- `dns/tests.rs`:测试基建与辅助函数(构造环境、启动 `DnsNode`、DNS 查询断言工具)。
|
||||
- `dns/server.rs`:数据面与 server 行为主测试集。
|
||||
- `dns/node_mgr.rs`:聚合 catalog 的基本可用性测试。
|
||||
- `dns/zone.rs`:配置解析、记录转换、zone_handler 装配测试。
|
||||
|
||||
> 说明:`system/*` 也有测试,但本轮文档按约定不展开。
|
||||
|
||||
### 14.2 `server.rs` 覆盖要点
|
||||
|
||||
`server.rs` 的测试集中验证了以下核心行为:
|
||||
|
||||
- `DynamicCatalog::replace()` 可安全替换。
|
||||
- hijack 判定:`is_hijacked_ip` / `is_hijacked_addr`。
|
||||
- ICMP 改写:EchoRequest -> EchoReply。
|
||||
- UDP DNS 包内联处理:解析请求、生成应答、回填 payload。
|
||||
- 一个基础端到端路径:真实 UDP listener + Hickory client 查询。
|
||||
|
||||
这些测试对应模块里最复杂、最容易回归的包处理逻辑。
|
||||
|
||||
### 14.3 `node_mgr.rs` 覆盖要点
|
||||
|
||||
`node_mgr.rs` 的测试重点是:
|
||||
|
||||
- 人工插入节点 zone 后,`catalog()` 能查到预期记录。
|
||||
|
||||
它验证了“快照聚合 -> Catalog 查询可用”的最小闭环,但尚未覆盖复杂心跳时序、TTL 过期后的清理行为。
|
||||
|
||||
### 14.4 `zone.rs` 覆盖要点
|
||||
|
||||
`zone.rs` 的测试覆盖了:
|
||||
|
||||
- TOML `DnsConfig` 解析。
|
||||
- `ZoneConfig -> ZoneData -> Zone` 转换链。
|
||||
- record 解析/TTL 基本行为。
|
||||
- memory/forward zone_handler 构建,以及通过 server 查询验证。
|
||||
|
||||
该测试更多是“模型与解析正确性”,不是策略执行链路完整验证。
|
||||
|
||||
### 14.5 当前测试缺口
|
||||
|
||||
结合 `plan.md` 与现有测试,仍建议补充:
|
||||
|
||||
- `DnsNode` 心跳 + resync + 重选举的并发时序测试。
|
||||
- `DnsNodeMgr` TTL 过期淘汰与脏标记联动测试。
|
||||
- 策略字段(import/export ACL、recursive)的行为测试。
|
||||
- 多 peer、同 origin 多 zone 的优先级/去重回归测试。
|
||||
|
||||
---
|
||||
|
||||
## 15. CLI 与可观测性现状
|
||||
|
||||
### 15.1 CLI 现状
|
||||
|
||||
从当前代码看,`easytier/src/easytier-cli.rs` 没有 DNS 专用子命令。
|
||||
因此“查看 DNS 子系统状态”主要依赖日志与通用状态接口,而非专门 CLI 面板。
|
||||
|
||||
### 15.2 日志观测点(已存在)
|
||||
|
||||
`dns` 子系统已经布置了较多 `tracing` 埋点,关键入口包括:
|
||||
|
||||
- `DnsNode election loop`
|
||||
- `DnsNode main loop`
|
||||
- `DnsServer main loop`
|
||||
- `DnsNodeMgr::heartbeat`(含来源 id 与 snapshot 信息)
|
||||
|
||||
可用于定位:
|
||||
|
||||
- 当前实例是否赢得选举。
|
||||
- 心跳是否失败、是否触发 `resync`。
|
||||
- catalog/addresses/listeners 是否持续重载失败。
|
||||
|
||||
### 15.3 当前可观测性短板
|
||||
|
||||
- 缺少 DNS 专项 CLI 展示:
|
||||
- 本机角色(Node/Server)
|
||||
- 当前 snapshot digest
|
||||
- zone 来源与数量
|
||||
- 监听地址与 hijack 地址状态
|
||||
- 缺少结构化指标(metrics),目前偏日志驱动排障。
|
||||
|
||||
### 15.4 建议的最小可观测面
|
||||
|
||||
后续若补 CLI,可先实现一个最小 DNS 状态视图:
|
||||
|
||||
1. 角色与选举状态(是否持有 `DNS_SERVER_RPC_ADDR`)。
|
||||
2. 最近心跳时间、`resync` 次数。
|
||||
3. 已装载 zone 数量(按本地/远端分组)。
|
||||
4. listeners 与 addresses 当前集合。
|
||||
|
||||
该视图不改变数据面行为,但能显著降低线上排障成本。
|
||||
@@ -0,0 +1,570 @@
|
||||
use crate::common::global_ctx::ArcGlobalCtx;
|
||||
use crate::dns::node_mgr::DnsNodeMgr;
|
||||
use crate::dns::system;
|
||||
use crate::dns::utils::addr::NameServerAddr;
|
||||
#[cfg(feature = "tun")]
|
||||
use crate::instance::instance::{ArcNicCtx, NicCtx};
|
||||
use crate::peers::peer_manager::PeerManager;
|
||||
use crate::proto::dns::DnsNodeMgrRpcServer;
|
||||
use crate::proto::rpc_impl::standalone::StandAloneServer;
|
||||
use crate::tunnel::common::bind;
|
||||
use crate::tunnel::tcp::TcpTunnelListener;
|
||||
use crate::utils::task::CancellableTask;
|
||||
use anyhow::Context;
|
||||
use derivative::Derivative;
|
||||
use guarden::guarded;
|
||||
use hickory_net::runtime::Time;
|
||||
use hickory_net::xfer::Protocol;
|
||||
use hickory_server::{
|
||||
Server,
|
||||
server::{Request, RequestHandler, ResponseHandler, ResponseInfo},
|
||||
zone_handler::Catalog,
|
||||
};
|
||||
use itertools::chain;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashSet;
|
||||
use std::net::IpAddr;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{Instrument, instrument};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DynamicCatalog {
|
||||
inner: Arc<tokio::sync::RwLock<Catalog>>,
|
||||
}
|
||||
|
||||
impl DynamicCatalog {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(tokio::sync::RwLock::new(Catalog::new())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn replace(&self, new: Catalog) {
|
||||
*self.inner.write().await = new;
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl RequestHandler for DynamicCatalog {
|
||||
async fn handle_request<R: ResponseHandler, T: Time>(
|
||||
&self,
|
||||
request: &Request,
|
||||
response_handle: R,
|
||||
) -> ResponseInfo {
|
||||
self.inner
|
||||
.read()
|
||||
.await
|
||||
.handle_request::<_, T>(request, response_handle)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug)]
|
||||
pub struct DnsServer {
|
||||
mgr: Arc<DnsNodeMgr>,
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
nic_ctx: ArcNicCtx, // TODO: REMOVE THIS
|
||||
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
|
||||
#[derivative(Debug = "ignore")]
|
||||
catalog: DynamicCatalog,
|
||||
runtime: Mutex<Option<CancellableTask<()>>>,
|
||||
bindings: RwLock<HashSet<NameServerAddr>>,
|
||||
|
||||
addresses: RwLock<HashSet<NameServerAddr>>,
|
||||
listeners: RwLock<HashSet<NameServerAddr>>,
|
||||
}
|
||||
|
||||
const DNS_SERVER_TCP_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const DNS_SERVER_TCP_BUFFER_SIZE: usize = 32;
|
||||
|
||||
impl DnsServer {
|
||||
pub fn new(
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
#[cfg(feature = "tun")] nic_ctx: ArcNicCtx, // TODO: REMOVE THIS
|
||||
) -> Self {
|
||||
Self {
|
||||
mgr: Arc::new(DnsNodeMgr::new()),
|
||||
#[cfg(feature = "tun")]
|
||||
nic_ctx,
|
||||
peer_mgr,
|
||||
global_ctx,
|
||||
catalog: DynamicCatalog::new(),
|
||||
runtime: Default::default(),
|
||||
bindings: Default::default(),
|
||||
listeners: Default::default(),
|
||||
addresses: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(&self, rpc: &StandAloneServer<TcpTunnelListener>) {
|
||||
rpc.registry()
|
||||
.register(DnsNodeMgrRpcServer::new_arc(self.mgr.clone()), "");
|
||||
}
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
async fn update_system(&self, nameservers: &HashSet<NameServerAddr>) -> anyhow::Result<()> {
|
||||
let nic_ctx = self.nic_ctx.lock().await;
|
||||
let nic_ctx = nic_ctx
|
||||
.as_ref()
|
||||
.and_then(|nic_ctx| nic_ctx.downcast_ref::<NicCtx>())
|
||||
.with_context(|| "failed to get NicCtx")?;
|
||||
let ifname = nic_ctx
|
||||
.ifname()
|
||||
.await
|
||||
.with_context(|| "failed to get interface name from NicCtx")?;
|
||||
let system = system::get(&ifname)?.with_context(|| "failed to get system configurator")?;
|
||||
let config = self.global_ctx.config.get_dns();
|
||||
let domain = vec![config.domain.to_string()];
|
||||
system.set_dns(&system::SystemConfig {
|
||||
nameservers: nameservers
|
||||
.iter()
|
||||
.filter_map(|a| {
|
||||
(a.protocol == Protocol::Udp && a.addr.port() == 53)
|
||||
.then_some(a.addr.ip().to_string())
|
||||
})
|
||||
.collect(),
|
||||
search_domains: domain.clone(),
|
||||
match_domains: domain
|
||||
.into_iter()
|
||||
.chain(config.zones.iter().map(|z| z.origin.to_string()))
|
||||
.collect(),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn rebind(&self) -> anyhow::Result<bool> {
|
||||
let Ok(mut runtime) = self.runtime.try_lock() else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let mut bindings = {
|
||||
let current = self.bindings.read();
|
||||
let bindings = chain(
|
||||
self.addresses.read().iter().cloned(),
|
||||
self.listeners.read().iter().cloned(),
|
||||
)
|
||||
.collect();
|
||||
if *current == bindings {
|
||||
tracing::info!("bindings unchanged, no need to rebind");
|
||||
return Ok(false);
|
||||
}
|
||||
bindings
|
||||
};
|
||||
|
||||
if let Some(runtime) = runtime.take() {
|
||||
runtime.stop(None).await?;
|
||||
}
|
||||
|
||||
let mut server = Server::new(self.catalog.clone());
|
||||
|
||||
bindings.retain(|binding| {
|
||||
let addr = binding.addr;
|
||||
tracing::info!(?addr, "binding");
|
||||
match binding.protocol {
|
||||
Protocol::Tcp => bind().addr(addr).call().map(|s| {
|
||||
server.register_listener(s, DNS_SERVER_TCP_TIMEOUT, DNS_SERVER_TCP_BUFFER_SIZE)
|
||||
}),
|
||||
Protocol::Udp => bind().addr(addr).call().map(|s| server.register_socket(s)),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
.inspect_err(|error| tracing::error!(?addr, ?error, "failed to bind"))
|
||||
.is_ok()
|
||||
});
|
||||
|
||||
let token = server.shutdown_token().clone();
|
||||
let handle = tokio::spawn(
|
||||
async move {
|
||||
server
|
||||
.block_until_done()
|
||||
.await
|
||||
.unwrap_or_else(|e| tracing::error!("DNS server exited with error: {:?}", e));
|
||||
}
|
||||
.instrument(tracing::info_span!("DNS server backend runtime")),
|
||||
);
|
||||
|
||||
*runtime = Some(CancellableTask::with_handle(token, handle));
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
if let Err(error) = self.update_system(&bindings).await {
|
||||
tracing::error!(?error, "failed to update system DNS settings");
|
||||
}
|
||||
|
||||
*self.bindings.write() = bindings;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn reload_addresses(&self) -> anyhow::Result<()> {
|
||||
let addresses = self.mgr.iter_addresses().collect();
|
||||
|
||||
let removed = {
|
||||
let current = self.addresses.read();
|
||||
if *current == addresses {
|
||||
tracing::info!("addresses unchanged, no need to reload");
|
||||
return Ok(());
|
||||
}
|
||||
current
|
||||
.difference(&addresses)
|
||||
.cloned()
|
||||
.collect::<HashSet<_>>()
|
||||
};
|
||||
tracing::info!(?addresses, "reloading");
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
{
|
||||
let nic_ctx = self.nic_ctx.lock().await;
|
||||
if let Some(nic_ctx) = nic_ctx
|
||||
.as_ref()
|
||||
.and_then(|nic_ctx| nic_ctx.downcast_ref::<NicCtx>())
|
||||
{
|
||||
for addr in &addresses {
|
||||
let ip = addr.addr.ip();
|
||||
if let Err(error) = match ip {
|
||||
IpAddr::V4(ipv4) => nic_ctx.add_ipv4_to_tun_device(ipv4.into()).await,
|
||||
IpAddr::V6(ipv6) => nic_ctx.add_ipv6_to_tun_device(ipv6.into()).await,
|
||||
} {
|
||||
tracing::error!(?addr, ?error, "failed to add address to tun device");
|
||||
}
|
||||
}
|
||||
|
||||
for addr in removed {
|
||||
let ip = addr.addr.ip();
|
||||
if let Err(error) = match ip {
|
||||
IpAddr::V4(ipv4) => nic_ctx.remove_ipv4_from_tun_device(ipv4.into()).await,
|
||||
IpAddr::V6(ipv6) => nic_ctx.remove_ipv6_from_tun_device(ipv6.into()).await,
|
||||
} {
|
||||
tracing::error!(?addr, ?error, "failed to remove address from tun device");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*self.addresses.write() = addresses;
|
||||
|
||||
self.rebind().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn reload_listeners(&self) -> anyhow::Result<()> {
|
||||
let listeners = self.mgr.iter_listeners().collect();
|
||||
|
||||
if *self.listeners.read() == listeners {
|
||||
tracing::info!("listeners unchanged, no need to reload");
|
||||
return Ok(());
|
||||
}
|
||||
tracing::info!(?listeners, "reloading");
|
||||
|
||||
*self.listeners.write() = listeners;
|
||||
|
||||
self.rebind().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all, name = "DnsServer main loop")]
|
||||
pub async fn run(&self, token: CancellationToken) {
|
||||
let dirty = &self.mgr.dirty;
|
||||
let runtime = None::<CancellableTask<()>>;
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
guarded! {
|
||||
system_guard => [
|
||||
nic_ctx = self.nic_ctx.clone(),
|
||||
]
|
||||
async move {
|
||||
if let Some(nic_ctx) = nic_ctx
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.and_then(|nic_ctx| nic_ctx.downcast_ref::<NicCtx>())
|
||||
&& let Some(system) = nic_ctx
|
||||
.ifname()
|
||||
.await
|
||||
.and_then(|ifname| system::get(&ifname).ok())
|
||||
.flatten()
|
||||
{
|
||||
let _ = system.clean();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guarded! {
|
||||
runtime_guard => [
|
||||
mut runtime,
|
||||
]
|
||||
async move {
|
||||
if let Some(runtime) = runtime.take() {
|
||||
let _ = runtime.stop(Some(Duration::from_secs(1))).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let reload_catalog = async {
|
||||
loop {
|
||||
dirty.catalog.wait().await;
|
||||
if dirty.catalog.reset() {
|
||||
self.catalog.replace(self.mgr.catalog()).await;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
};
|
||||
|
||||
let reload_addresses = async {
|
||||
loop {
|
||||
dirty.addresses.wait().await;
|
||||
if dirty.addresses.reset()
|
||||
&& let Err(error) = self.reload_addresses().await
|
||||
{
|
||||
tracing::error!(?error, "failed to reload addresses");
|
||||
dirty.addresses.mark();
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
};
|
||||
|
||||
let reload_listeners = async {
|
||||
loop {
|
||||
dirty.listeners.wait().await;
|
||||
if dirty.listeners.reset()
|
||||
&& let Err(error) = self.reload_listeners().await
|
||||
{
|
||||
tracing::error!(?error, "failed to reload listeners");
|
||||
dirty.listeners.mark();
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
};
|
||||
|
||||
tokio::select!(
|
||||
_ = token.cancelled() => {
|
||||
tracing::info!("DnsServer received shutdown signal, exiting server loop");
|
||||
}
|
||||
|
||||
_ = reload_catalog => {},
|
||||
_ = reload_addresses => {},
|
||||
_ = reload_listeners => {},
|
||||
);
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
system_guard.trigger().await;
|
||||
runtime_guard.trigger().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::peers::tests::create_mock_peer_manager;
|
||||
use hickory_net::client::{Client, ClientHandle};
|
||||
use hickory_net::runtime::TokioRuntimeProvider;
|
||||
use hickory_net::udp::UdpClientStream;
|
||||
use hickory_proto::op::{Message, MessageType, OpCode, Query};
|
||||
use hickory_proto::rr::{DNSClass, Name, RData, Record, RecordType, rdata};
|
||||
use hickory_proto::serialize::binary::BinEncodable;
|
||||
use hickory_server::store::in_memory::InMemoryZoneHandler;
|
||||
use hickory_server::zone_handler::ZoneType;
|
||||
use hickory_server::zone_handler::{AxfrPolicy, Catalog};
|
||||
use pnet::packet::icmp::{IcmpTypes, MutableIcmpPacket};
|
||||
use pnet::packet::ipv4::MutableIpv4Packet;
|
||||
use pnet::packet::udp::MutableUdpPacket;
|
||||
use pnet::packet::{MutablePacket, icmp, ipv4, udp};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
/// Build a `Catalog` containing a single A record: `test.example.com -> 1.2.3.4`.
|
||||
fn build_test_catalog() -> Catalog {
|
||||
let origin = Name::from_str("example.com.").unwrap();
|
||||
let mut zone_handler = InMemoryZoneHandler::<TokioRuntimeProvider>::empty(
|
||||
origin.clone(),
|
||||
ZoneType::Primary,
|
||||
AxfrPolicy::default(),
|
||||
);
|
||||
|
||||
let record = Record::from_rdata(
|
||||
Name::from_str("test.example.com.").unwrap(),
|
||||
60,
|
||||
RData::A(rdata::a::A(Ipv4Addr::new(1, 2, 3, 4))),
|
||||
);
|
||||
let rr_key =
|
||||
hickory_proto::rr::RrKey::new(record.name.clone().into(), record.record_type());
|
||||
let mut rr_set =
|
||||
hickory_proto::rr::RecordSet::new(record.name.clone(), record.record_type(), 0);
|
||||
rr_set.insert(record, 0);
|
||||
zone_handler
|
||||
.records_get_mut()
|
||||
.insert(rr_key, Arc::new(rr_set));
|
||||
|
||||
let mut catalog = Catalog::new();
|
||||
catalog.upsert(
|
||||
origin.into(),
|
||||
vec![Arc::new(zone_handler) as Arc<dyn hickory_server::zone_handler::ZoneHandler>],
|
||||
);
|
||||
catalog
|
||||
}
|
||||
|
||||
/// Create a test `DnsServer` with `create_mock_peer_manager()`.
|
||||
async fn create_test_server() -> Arc<DnsServer> {
|
||||
let peer_mgr = create_mock_peer_manager().await;
|
||||
let global_ctx = peer_mgr.get_global_ctx();
|
||||
Arc::new(DnsServer::new(
|
||||
peer_mgr,
|
||||
global_ctx,
|
||||
#[cfg(feature = "tun")]
|
||||
ArcNicCtx::default(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Build a raw IPv4 packet (as `Vec<u8>`) carrying the given L4 payload bytes.
|
||||
/// `protocol` selects ICMP / UDP etc.
|
||||
fn build_ipv4_packet(
|
||||
src: Ipv4Addr,
|
||||
dst: Ipv4Addr,
|
||||
protocol: pnet::packet::ip::IpNextHeaderProtocol,
|
||||
l4_payload: &[u8],
|
||||
) -> Vec<u8> {
|
||||
let ip_header_len = 20usize;
|
||||
let total_len = ip_header_len + l4_payload.len();
|
||||
let mut buf = vec![0u8; total_len];
|
||||
{
|
||||
let mut ip = MutableIpv4Packet::new(&mut buf).unwrap();
|
||||
ip.set_version(4);
|
||||
ip.set_header_length(5); // 20 bytes
|
||||
ip.set_total_length(total_len as u16);
|
||||
ip.set_ttl(64);
|
||||
ip.set_next_level_protocol(protocol);
|
||||
ip.set_source(src);
|
||||
ip.set_destination(dst);
|
||||
ip.payload_mut().copy_from_slice(l4_payload);
|
||||
ip.set_checksum(ipv4::checksum(&ip.to_immutable()));
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
/// Build ICMP Echo Request payload (8 bytes minimum).
|
||||
fn build_icmp_echo_request() -> Vec<u8> {
|
||||
let mut buf = vec![0u8; 8];
|
||||
{
|
||||
let mut icmp_pkt = MutableIcmpPacket::new(&mut buf).unwrap();
|
||||
icmp_pkt.set_icmp_type(IcmpTypes::EchoRequest);
|
||||
icmp_pkt.set_icmp_code(icmp::IcmpCode::new(0));
|
||||
icmp_pkt.set_checksum(icmp::checksum(&icmp_pkt.to_immutable()));
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
/// Build a minimal DNS query message for `name` and encode it to bytes.
|
||||
fn build_dns_query_bytes(name: &str) -> Vec<u8> {
|
||||
let mut msg = Message::new(0x1234, MessageType::Query, OpCode::Query);
|
||||
msg.metadata.recursion_desired = true;
|
||||
let mut query = Query::new();
|
||||
query.set_name(Name::from_str(name).unwrap());
|
||||
query.set_query_type(RecordType::A);
|
||||
query.set_query_class(DNSClass::IN);
|
||||
msg.add_query(query);
|
||||
msg.to_bytes().unwrap().to_vec()
|
||||
}
|
||||
|
||||
/// Build a UDP packet carrying `payload`, with given src/dst ports.
|
||||
fn build_udp_packet(
|
||||
src_port: u16,
|
||||
dst_port: u16,
|
||||
payload: &[u8],
|
||||
src_ip: Ipv4Addr,
|
||||
dst_ip: Ipv4Addr,
|
||||
) -> Vec<u8> {
|
||||
let udp_len = 8 + payload.len();
|
||||
let mut buf = vec![0u8; udp_len];
|
||||
{
|
||||
let mut udp_pkt = MutableUdpPacket::new(&mut buf).unwrap();
|
||||
udp_pkt.set_source(src_port);
|
||||
udp_pkt.set_destination(dst_port);
|
||||
udp_pkt.set_length(udp_len as u16);
|
||||
udp_pkt.payload_mut().copy_from_slice(payload);
|
||||
udp_pkt.set_checksum(udp::ipv4_checksum(
|
||||
&udp_pkt.to_immutable(),
|
||||
&src_ip,
|
||||
&dst_ip,
|
||||
));
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
async fn wait_until(mut f: impl FnMut() -> bool) {
|
||||
for _ in 0..80 {
|
||||
if f() {
|
||||
return;
|
||||
}
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
panic!("condition not met in time");
|
||||
}
|
||||
|
||||
// ─── Tests ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Full end-to-end test: start a real DNS UDP listener via `ServerFuture`,
|
||||
/// send a query with a `hickory_client`, and verify the response.
|
||||
#[tokio::test]
|
||||
async fn should_resolve_record_via_real_udp_listener() {
|
||||
use hickory_server::Server;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::time::timeout;
|
||||
|
||||
// Build a catalog with test.example.com -> 1.2.3.4.
|
||||
let catalog = build_test_catalog();
|
||||
|
||||
// Bind to a random port.
|
||||
let socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = socket.local_addr().unwrap();
|
||||
|
||||
let mut server = Server::new(catalog);
|
||||
server.register_socket(socket);
|
||||
|
||||
let shutdown_token = server.shutdown_token().clone();
|
||||
tokio::spawn(async move {
|
||||
server.block_until_done().await.ok();
|
||||
});
|
||||
|
||||
// Send a real DNS query using hickory_client.
|
||||
let stream = UdpClientStream::builder(addr, TokioRuntimeProvider::default()).build();
|
||||
let (mut client, bg) = Client::<TokioRuntimeProvider>::from_sender(stream);
|
||||
|
||||
tokio::spawn(bg);
|
||||
|
||||
let response = timeout(
|
||||
Duration::from_secs(2),
|
||||
client.query(
|
||||
Name::from_str("test.example.com.").unwrap(),
|
||||
DNSClass::IN,
|
||||
RecordType::A,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.expect("query timeout")
|
||||
.expect("query failed");
|
||||
|
||||
assert!(!response.answers.is_empty(), "should get answers");
|
||||
let a_record = &response.answers[0];
|
||||
if let RData::A(a) = a_record.data {
|
||||
assert_eq!(a.0, Ipv4Addr::new(1, 2, 3, 4));
|
||||
} else {
|
||||
panic!("expected A record, got {:?}", a_record.data);
|
||||
}
|
||||
|
||||
// Shutdown the server.
|
||||
shutdown_token.cancel();
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -6,13 +6,13 @@ use std::{
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use super::{OSConfig, SystemConfig};
|
||||
use super::{SystemConfig, SystemConfigurator};
|
||||
|
||||
const MAC_RESOLVER_FILE_HEADER: &str = "# Added by easytier\n";
|
||||
const ETC_RESOLVER: &str = "/etc/resolver";
|
||||
const ETC_RESOLV_CONF: &str = "/etc/resolv.conf";
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct DarwinConfigurator {}
|
||||
impl DarwinConfigurator {
|
||||
pub fn new() -> Self {
|
||||
@@ -27,7 +27,7 @@ impl DarwinConfigurator {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn do_set_dns(&self, cfg: &OSConfig) -> io::Result<()> {
|
||||
pub fn do_set_dns(&self, cfg: &SystemConfig) -> io::Result<()> {
|
||||
fs::create_dir_all(ETC_RESOLVER)?;
|
||||
let mut keep = HashSet::new();
|
||||
|
||||
@@ -104,12 +104,12 @@ impl DarwinConfigurator {
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemConfig for DarwinConfigurator {
|
||||
fn set_dns(&self, cfg: &OSConfig) -> io::Result<()> {
|
||||
impl SystemConfigurator for DarwinConfigurator {
|
||||
fn set_dns(&self, cfg: &SystemConfig) -> io::Result<()> {
|
||||
self.do_set_dns(cfg)
|
||||
}
|
||||
|
||||
fn close(&self) -> io::Result<()> {
|
||||
fn clean(&self) -> io::Result<()> {
|
||||
self.do_close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#[allow(unused_imports)]
|
||||
use crate::utils::BoxExt;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows;
|
||||
|
||||
#[cfg(all(target_os = "macos", not(feature = "macos-ne")))]
|
||||
pub mod macos;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct SystemConfig {
|
||||
pub nameservers: Vec<String>,
|
||||
pub search_domains: Vec<String>,
|
||||
pub match_domains: Vec<String>,
|
||||
}
|
||||
|
||||
pub trait SystemConfigurator: Send + Sync {
|
||||
fn set_dns(&self, cfg: &SystemConfig) -> std::io::Result<()>;
|
||||
fn clean(&self) -> std::io::Result<()>;
|
||||
}
|
||||
|
||||
// TODO: move this to nic mod
|
||||
pub fn get(
|
||||
#[allow(unused_variables)] interface: &str,
|
||||
) -> Result<Option<Box<dyn SystemConfigurator>>, anyhow::Error> {
|
||||
cfg_select! {
|
||||
target_os = "windows" => {
|
||||
use crate::dns::system::windows::WindowsDNSManager;
|
||||
Ok(Some(WindowsDNSManager::new(interface)?.boxed()))
|
||||
}
|
||||
|
||||
all(target_os = "macos", not(feature = "macos-ne")) => {
|
||||
use crate::dns::system::macos::DarwinConfigurator;
|
||||
Ok(Some(DarwinConfigurator::new().boxed()))
|
||||
}
|
||||
|
||||
_ => Ok(None)
|
||||
}
|
||||
}
|
||||
+101
-40
@@ -6,7 +6,7 @@ use winreg::RegKey;
|
||||
|
||||
use crate::common::ifcfg::RegistryManager;
|
||||
|
||||
use super::{OSConfig, SystemConfig};
|
||||
use super::{SystemConfig, SystemConfigurator};
|
||||
|
||||
pub fn is_windows_10_or_better() -> io::Result<bool> {
|
||||
let hklm = winreg::enums::HKEY_LOCAL_MACHINE;
|
||||
@@ -19,6 +19,7 @@ pub fn is_windows_10_or_better() -> io::Result<bool> {
|
||||
}
|
||||
|
||||
// 假设 interface_guid 是你的网络接口 GUID
|
||||
#[derive(Clone)]
|
||||
pub struct InterfaceControl {
|
||||
interface_guid: String,
|
||||
}
|
||||
@@ -125,6 +126,7 @@ impl InterfaceControl {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WindowsDNSManager {
|
||||
tun_dev_name: String,
|
||||
interface_control: InterfaceControl,
|
||||
@@ -146,8 +148,8 @@ impl WindowsDNSManager {
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemConfig for WindowsDNSManager {
|
||||
fn set_dns(&self, cfg: &OSConfig) -> io::Result<()> {
|
||||
impl SystemConfigurator for WindowsDNSManager {
|
||||
fn set_dns(&self, cfg: &SystemConfig) -> io::Result<()> {
|
||||
self.set_primary_dns(
|
||||
&cfg.nameservers
|
||||
.iter()
|
||||
@@ -158,44 +160,48 @@ impl SystemConfig for WindowsDNSManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close(&self) -> io::Result<()> {
|
||||
fn clean(&self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(test, target_os = "windows", feature = "magic-dns", feature = "tun"))]
|
||||
mod tests {
|
||||
use cidr::Ipv4Inet;
|
||||
use std::net::IpAddr;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[tokio::test]
|
||||
async fn test_windows_set_primary_server() {
|
||||
use std::{net::Ipv4Addr, str::FromStr as _, time::Duration};
|
||||
use std::{str::FromStr as _, sync::Arc, time::Duration};
|
||||
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::instance::dns_server::{
|
||||
runner::DnsRunner,
|
||||
tests::{check_dns_record, prepare_env},
|
||||
use crate::dns::{
|
||||
config::DNS_DEFAULT_ADDRESSES,
|
||||
tests::{prepare_env, start_dns_node},
|
||||
};
|
||||
use crate::instance::proxy_cidrs_monitor::ProxyCidrsMonitor;
|
||||
use crate::instance::virtual_nic::NicCtx;
|
||||
use crate::peers::peer_manager::PeerManager;
|
||||
|
||||
let tun_ip = Ipv4Inet::from_str("10.144.144.10/24").unwrap();
|
||||
let (peer_mgr, virtual_nic) = prepare_env("test1", tun_ip).await;
|
||||
let (peer_mgr, virtual_nic): (Arc<PeerManager>, NicCtx) =
|
||||
prepare_env("test1", tun_ip).await;
|
||||
let tun_name = virtual_nic.ifname().await.unwrap();
|
||||
|
||||
// prepare_env does not run full Instance::run, so start the monitor explicitly in test.
|
||||
let _monitor = ProxyCidrsMonitor::new(peer_mgr.clone(), peer_mgr.get_global_ctx()).start();
|
||||
|
||||
let mut dns_node = start_dns_node(peer_mgr, virtual_nic);
|
||||
|
||||
println!("dev_name: {}", tun_name);
|
||||
let fake_ip = Ipv4Addr::from_str("100.100.100.101").unwrap();
|
||||
let mut dns_runner = DnsRunner::new(peer_mgr, Some(tun_name.clone()), tun_ip, fake_ip);
|
||||
let fake_ip = match DNS_DEFAULT_ADDRESSES[0].addr.ip() {
|
||||
IpAddr::V4(ip) => ip,
|
||||
IpAddr::V6(ip) => panic!("unexpected ipv6 default dns address in test: {ip}"),
|
||||
};
|
||||
|
||||
let cancel_token = CancellationToken::new();
|
||||
let cancel_token_clone = cancel_token.clone();
|
||||
let t = tokio::spawn(async move {
|
||||
dns_runner.run(cancel_token_clone).await;
|
||||
});
|
||||
|
||||
// windows is slow to add a ip address, wait for a longer time for dns server ready ,with ping
|
||||
// Windows may take a while to attach the test IP; wait until ping succeeds.
|
||||
let now = std::time::Instant::now();
|
||||
while now.elapsed() < Duration::from_secs(15) {
|
||||
let mut ping_ready = false;
|
||||
while now.elapsed() < Duration::from_secs(5) {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
if let Ok(o) = tokio::process::Command::new("ping")
|
||||
.arg("-n")
|
||||
@@ -207,11 +213,49 @@ mod tests {
|
||||
.await
|
||||
&& o.status.success()
|
||||
{
|
||||
ping_ready = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !ping_ready {
|
||||
tracing::warn!(
|
||||
"dns test endpoint {} did not respond to ping in time; continue with dns checks",
|
||||
fake_ip
|
||||
);
|
||||
}
|
||||
|
||||
check_dns_record(&fake_ip, "test1.et.net", "10.144.144.10").await;
|
||||
// First verify the DNS node can answer queries when explicitly targeting it.
|
||||
let direct_lookup_deadline = std::time::Instant::now() + Duration::from_secs(3);
|
||||
let mut direct_lookup_ok = false;
|
||||
let mut last_direct_output = String::new();
|
||||
|
||||
while std::time::Instant::now() < direct_lookup_deadline {
|
||||
let result = tokio::time::timeout(
|
||||
Duration::from_secs(3),
|
||||
tokio::process::Command::new("nslookup")
|
||||
.arg("test1.et.net")
|
||||
.arg(fake_ip.to_string())
|
||||
.output(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok(Ok(ret)) = result {
|
||||
let output = String::from_utf8_lossy(&ret.stdout).to_string();
|
||||
println!("direct nslookup output: {}", output);
|
||||
if ret.status.success() && output.contains("10.144.144.10") {
|
||||
direct_lookup_ok = true;
|
||||
break;
|
||||
}
|
||||
last_direct_output = output;
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
assert!(
|
||||
direct_lookup_ok,
|
||||
"direct nslookup against dns server did not return expected IP in time; last output: {last_direct_output}"
|
||||
);
|
||||
|
||||
let dns_mgr = super::WindowsDNSManager::new(&tun_name).unwrap();
|
||||
println!("dev_name: {}", tun_name);
|
||||
@@ -219,27 +263,44 @@ mod tests {
|
||||
|
||||
dns_mgr
|
||||
.interface_control
|
||||
.set_primary_dns(
|
||||
&["100.100.100.101".parse().unwrap()],
|
||||
&[".et.net.".to_string()],
|
||||
)
|
||||
.set_primary_dns(&[fake_ip.into()], &[".et.net.".to_string()])
|
||||
.unwrap();
|
||||
dns_mgr.interface_control.flush_dns().unwrap();
|
||||
|
||||
tracing::info!("check dns record with nslookup");
|
||||
|
||||
// nslookup should return 10.144.144.10
|
||||
let ret = tokio::process::Command::new("nslookup")
|
||||
.arg("test1.et.net")
|
||||
.output()
|
||||
.await
|
||||
.expect("failed to execute process");
|
||||
assert!(ret.status.success());
|
||||
let output = String::from_utf8_lossy(&ret.stdout);
|
||||
println!("nslookup output: {}", output);
|
||||
assert!(output.contains("10.144.144.10"));
|
||||
// nslookup should eventually return 10.144.144.10 after system DNS setting propagation.
|
||||
let lookup_deadline = std::time::Instant::now() + Duration::from_secs(30);
|
||||
let mut lookup_ok = false;
|
||||
let mut last_output = String::new();
|
||||
|
||||
cancel_token.cancel();
|
||||
let _ = t.await;
|
||||
while std::time::Instant::now() < lookup_deadline {
|
||||
let result = tokio::time::timeout(
|
||||
Duration::from_secs(3),
|
||||
tokio::process::Command::new("nslookup")
|
||||
.arg("test1.et.net")
|
||||
.output(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok(Ok(ret)) = result {
|
||||
let output = String::from_utf8_lossy(&ret.stdout).to_string();
|
||||
println!("nslookup output: {}", output);
|
||||
if ret.status.success() && output.contains("10.144.144.10") {
|
||||
lookup_ok = true;
|
||||
break;
|
||||
}
|
||||
last_output = output;
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
assert!(
|
||||
lookup_ok,
|
||||
"nslookup did not return expected IP in time; last output: {last_output}"
|
||||
);
|
||||
|
||||
dns_node.stop().await.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,984 @@
|
||||
#![cfg(all(feature = "magic-dns", feature = "tun"))]
|
||||
|
||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::str::FromStr as _;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::common::config::TomlConfigLoader;
|
||||
use crate::common::global_ctx::GlobalCtx;
|
||||
use crate::common::global_ctx::tests::get_mock_global_ctx;
|
||||
use crate::connector::udp_hole_punch::tests::replace_stun_info_collector;
|
||||
use crate::dns::node::DnsNode;
|
||||
use crate::dns::peer_mgr::DnsPeerMgr;
|
||||
use crate::instance::instance::ArcNicCtx;
|
||||
use crate::instance::virtual_nic::NicCtx;
|
||||
use crate::peers::create_packet_recv_chan;
|
||||
use crate::peers::peer_manager::{PeerManager, RouteAlgoType};
|
||||
use crate::peers::tests::{connect_peer_manager, wait_route_appear};
|
||||
use crate::proto::common::{NatType, Url};
|
||||
use crate::proto::dns::{DnsSnapshot, HeartbeatRequest, ZoneData};
|
||||
use cidr::Ipv4Inet;
|
||||
use hickory_net::client::{Client, ClientHandle};
|
||||
use hickory_net::runtime::TokioRuntimeProvider;
|
||||
use hickory_net::udp::UdpClientStream;
|
||||
use hickory_net::xfer::Protocol;
|
||||
use hickory_proto::op::{Message, MessageType, OpCode, Query};
|
||||
use hickory_proto::rr;
|
||||
use hickory_proto::rr::{DNSClass, Name, RData, RecordType};
|
||||
use hickory_proto::serialize::binary::{BinEncodable, BinEncoder};
|
||||
use hickory_server::server::Request;
|
||||
use maplit::hashset;
|
||||
use tokio::sync::Notify;
|
||||
use uuid::Uuid;
|
||||
|
||||
// TODO: move to system::tests
|
||||
pub async fn prepare_env(dns_name: &str, tun_ip: Ipv4Inet) -> (Arc<PeerManager>, NicCtx) {
|
||||
prepare_env_with_tld_dns_zone(dns_name, tun_ip, None).await
|
||||
}
|
||||
|
||||
pub async fn prepare_env_with_tld_dns_zone(
|
||||
dns_name: &str,
|
||||
tun_ip: Ipv4Inet,
|
||||
tld_dns_zone: Option<&str>,
|
||||
) -> (Arc<PeerManager>, NicCtx) {
|
||||
let ctx = get_mock_global_ctx();
|
||||
ctx.set_hostname(dns_name.to_owned());
|
||||
ctx.set_ipv4(Some(tun_ip));
|
||||
|
||||
let mut dns_config = ctx.config.get_dns().into_raw();
|
||||
dns_config.name = Some(dns_name.parse().unwrap());
|
||||
if let Some(zone) = tld_dns_zone {
|
||||
dns_config.domain = Some(zone.parse().expect("invalid test dns zone"));
|
||||
}
|
||||
ctx.config.set_dns(dns_config.into());
|
||||
|
||||
let (s, r) = create_packet_recv_chan();
|
||||
let peer_mgr = Arc::new(PeerManager::new(RouteAlgoType::Ospf, ctx, s));
|
||||
peer_mgr.run().await.unwrap();
|
||||
replace_stun_info_collector(peer_mgr.clone(), NatType::PortRestricted);
|
||||
|
||||
let r = Arc::new(tokio::sync::Mutex::new(r));
|
||||
let mut virtual_nic = NicCtx::new(
|
||||
peer_mgr.get_global_ctx(),
|
||||
&peer_mgr,
|
||||
r,
|
||||
Arc::new(Notify::new()),
|
||||
);
|
||||
virtual_nic.run(Some(tun_ip), None).await.unwrap();
|
||||
|
||||
(peer_mgr, virtual_nic)
|
||||
}
|
||||
|
||||
pub fn start_dns_node(peer_mgr: Arc<PeerManager>, virtual_nic: NicCtx) -> DnsNode {
|
||||
let global_ctx = peer_mgr.get_global_ctx();
|
||||
let nic_ctx: ArcNicCtx = Arc::new(tokio::sync::Mutex::new(Some(Box::new(virtual_nic))));
|
||||
|
||||
let mut node = DnsNode::new(peer_mgr, global_ctx, nic_ctx);
|
||||
node.start();
|
||||
node
|
||||
}
|
||||
|
||||
pub fn start_dns_node_without_nic(peer_mgr: Arc<PeerManager>) -> DnsNode {
|
||||
let global_ctx = peer_mgr.get_global_ctx();
|
||||
let nic_ctx: ArcNicCtx = Arc::new(tokio::sync::Mutex::new(None));
|
||||
|
||||
let mut node = DnsNode::new(peer_mgr, global_ctx, nic_ctx);
|
||||
node.start();
|
||||
node
|
||||
}
|
||||
|
||||
pub async fn prepare_env_from_config_str(config_str: &str) -> Arc<PeerManager> {
|
||||
let config = TomlConfigLoader::new_from_str(config_str).expect("invalid test config");
|
||||
let ctx = Arc::new(GlobalCtx::new(config));
|
||||
|
||||
let (s, _r) = create_packet_recv_chan();
|
||||
let peer_mgr = Arc::new(PeerManager::new(RouteAlgoType::Ospf, ctx, s));
|
||||
peer_mgr.run().await.unwrap();
|
||||
replace_stun_info_collector(peer_mgr.clone(), NatType::PortRestricted);
|
||||
|
||||
peer_mgr
|
||||
}
|
||||
|
||||
pub fn zone_data_a(origin: &str, record: &str) -> ZoneData {
|
||||
zone_data_a_with_forwarders(origin, record, vec![])
|
||||
}
|
||||
|
||||
pub fn zone_data_a_with_forwarders(origin: &str, record: &str, forwarders: Vec<&str>) -> ZoneData {
|
||||
ZoneData::new(
|
||||
&origin.parse().unwrap(),
|
||||
60,
|
||||
[format!("@ IN A {record}")],
|
||||
forwarders.into_iter().map(|f| Url::from_str(f).unwrap()),
|
||||
hashset! {},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn dns_snapshot_with(
|
||||
zones: Vec<ZoneData>,
|
||||
addresses: Vec<&str>,
|
||||
listeners: Vec<&str>,
|
||||
) -> DnsSnapshot {
|
||||
DnsSnapshot {
|
||||
zones,
|
||||
addresses: addresses
|
||||
.into_iter()
|
||||
.map(|a| Url::from_str(a).expect("invalid address"))
|
||||
.collect(),
|
||||
listeners: listeners
|
||||
.into_iter()
|
||||
.map(|l| Url::from_str(l).expect("invalid listener"))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn heartbeat_with_snapshot(id: Uuid, snapshot: DnsSnapshot) -> HeartbeatRequest {
|
||||
let mut hb = HeartbeatRequest {
|
||||
id: Some(id.into()),
|
||||
..Default::default()
|
||||
};
|
||||
hb.update(snapshot);
|
||||
hb
|
||||
}
|
||||
|
||||
fn find_free_udp_port() -> u16 {
|
||||
std::net::UdpSocket::bind("127.0.0.1:0")
|
||||
.expect("failed to bind temp udp socket")
|
||||
.local_addr()
|
||||
.expect("failed to get local addr")
|
||||
.port()
|
||||
}
|
||||
|
||||
pub async fn check_dns_record(fake_ip: &Ipv4Addr, domain: &str, expected_ip: &str) {
|
||||
check_dns_record_at(SocketAddr::new((*fake_ip).into(), 53), domain, expected_ip).await;
|
||||
}
|
||||
|
||||
pub async fn check_dns_record_at(server_addr: SocketAddr, domain: &str, expected_ip: &str) {
|
||||
let expected = expected_ip.parse::<Ipv4Addr>().unwrap();
|
||||
let name = Name::from_str(domain).unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(30);
|
||||
|
||||
loop {
|
||||
let stream = UdpClientStream::builder(server_addr, TokioRuntimeProvider::default()).build();
|
||||
let (mut client, background) = Client::<TokioRuntimeProvider>::from_sender(stream);
|
||||
let background_task = tokio::spawn(background);
|
||||
|
||||
let query_result = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
client.query(name.clone(), DNSClass::IN, RecordType::A),
|
||||
)
|
||||
.await;
|
||||
|
||||
background_task.abort();
|
||||
let _ = background_task.await;
|
||||
|
||||
let attempt_err = match query_result {
|
||||
Ok(Ok(response)) => {
|
||||
if response.answers.len() == 1
|
||||
&& let Some(resp) = response.answers.first()
|
||||
&& matches!(resp.clone().data, RData::A(a) if a.0 == expected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
format!("unexpected response: {:?}", response.answers)
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
format!("DNS query failed for domain '{domain}': {e}")
|
||||
}
|
||||
Err(_) => {
|
||||
format!("DNS query timed out for domain '{domain}'")
|
||||
}
|
||||
};
|
||||
|
||||
if Instant::now() >= deadline {
|
||||
panic!(
|
||||
"DNS query failed unexpectedly for domain '{domain}' after retries: {attempt_err}"
|
||||
);
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn check_dns_record_missing(fake_ip: &Ipv4Addr, domain: &str) {
|
||||
check_dns_record_missing_at(SocketAddr::new((*fake_ip).into(), 53), domain).await;
|
||||
}
|
||||
|
||||
pub async fn check_dns_record_missing_at(server_addr: SocketAddr, domain: &str) {
|
||||
let deadline = Instant::now() + Duration::from_secs(30);
|
||||
|
||||
loop {
|
||||
let stream = UdpClientStream::builder(server_addr, TokioRuntimeProvider::default()).build();
|
||||
let (mut client, background) = Client::<TokioRuntimeProvider>::from_sender(stream);
|
||||
let background_task = tokio::spawn(background);
|
||||
let query_result = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
client.query(Name::from_str(domain).unwrap(), DNSClass::IN, RecordType::A),
|
||||
)
|
||||
.await;
|
||||
background_task.abort();
|
||||
let _ = background_task.await;
|
||||
|
||||
let attempt_err = match query_result {
|
||||
Ok(Ok(response)) => {
|
||||
if response.answers.is_empty() {
|
||||
return;
|
||||
}
|
||||
format!("unexpected non-empty response: {:?}", response.answers)
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
format!("DNS query for missing record failed for domain '{domain}': {e}")
|
||||
}
|
||||
Err(_) => {
|
||||
format!("DNS query for missing record timed out for domain '{domain}'")
|
||||
}
|
||||
};
|
||||
|
||||
if Instant::now() >= deadline {
|
||||
panic!(
|
||||
"missing-record query failed unexpectedly for domain '{domain}' after retries: {attempt_err}"
|
||||
);
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_request(name: &str, rtype: RecordType) -> anyhow::Result<Request> {
|
||||
let mut msg = Message::new(0, MessageType::Query, OpCode::Query);
|
||||
msg.metadata.recursion_desired = true;
|
||||
msg.add_query(Query::query(Name::from_ascii(name)?, rtype));
|
||||
|
||||
let mut request = Vec::new();
|
||||
let mut encoder = BinEncoder::new(&mut request);
|
||||
msg.emit(&mut encoder)?;
|
||||
|
||||
Ok(Request::from_bytes(
|
||||
request,
|
||||
SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0).into(),
|
||||
Protocol::Udp,
|
||||
)?)
|
||||
}
|
||||
|
||||
async fn wait_route_disappear(peer_mgr: Arc<PeerManager>, target_peer_id: u32) {
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
loop {
|
||||
let has_route = peer_mgr
|
||||
.list_routes()
|
||||
.await
|
||||
.iter()
|
||||
.any(|r| r.peer_id == target_peer_id);
|
||||
if !has_route {
|
||||
return;
|
||||
}
|
||||
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"route to peer {} did not disappear in time",
|
||||
target_peer_id
|
||||
);
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn disconnect_all_peer_conns(a: Arc<PeerManager>, b: Arc<PeerManager>) {
|
||||
if let Some(conns) = a.get_peer_map().list_peer_conns(b.my_peer_id()).await {
|
||||
for conn in conns {
|
||||
let conn_id = conn.conn_id.parse().expect("invalid conn id");
|
||||
let _ = a.close_peer_conn(b.my_peer_id(), &conn_id).await;
|
||||
}
|
||||
}
|
||||
if let Some(conns) = b.get_peer_map().list_peer_conns(a.my_peer_id()).await {
|
||||
for conn in conns {
|
||||
let conn_id = conn.conn_id.parse().expect("invalid conn id");
|
||||
let _ = b.close_peer_conn(a.my_peer_id(), &conn_id).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_dns_unavailable_at(server_addr: SocketAddr, domain: &str) {
|
||||
let deadline = Instant::now() + Duration::from_secs(15);
|
||||
let name = rr::Name::from_str(domain).unwrap();
|
||||
|
||||
loop {
|
||||
let stream = UdpClientStream::builder(server_addr, TokioRuntimeProvider::default()).build();
|
||||
let (mut client, background) = Client::<TokioRuntimeProvider>::from_sender(stream);
|
||||
|
||||
let background_task = tokio::spawn(background);
|
||||
let query_result = tokio::time::timeout(
|
||||
Duration::from_secs(1),
|
||||
client.query(name.clone(), DNSClass::IN, RecordType::A),
|
||||
)
|
||||
.await;
|
||||
|
||||
background_task.abort();
|
||||
let _ = background_task.await;
|
||||
|
||||
match query_result {
|
||||
Ok(Ok(response)) if !response.answers.is_empty() => {
|
||||
if Instant::now() >= deadline {
|
||||
panic!(
|
||||
"DNS endpoint {server_addr} still answered for '{domain}': {:?}",
|
||||
response.answers
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_peer_zone_visibility(
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
target_peer_id: u32,
|
||||
zone_origin_substr: &str,
|
||||
expected_visible: bool,
|
||||
) {
|
||||
let dns = DnsPeerMgr::new(peer_mgr.clone(), peer_mgr.get_global_ctx());
|
||||
let deadline = Instant::now() + Duration::from_secs(20);
|
||||
|
||||
loop {
|
||||
let _ = dns
|
||||
.refresh(target_peer_id, Default::default(), Default::default())
|
||||
.await;
|
||||
|
||||
let snapshot = dns.snapshot();
|
||||
|
||||
let visible = snapshot.zones.iter().any(|z| {
|
||||
z.content
|
||||
.contains(&format!("$ORIGIN {}", zone_origin_substr))
|
||||
});
|
||||
|
||||
if visible == expected_visible {
|
||||
return;
|
||||
}
|
||||
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"zone visibility mismatch for '{}': expected {}, got {}",
|
||||
zone_origin_substr,
|
||||
expected_visible,
|
||||
visible,
|
||||
);
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn cfg_with_listener(name: &str, ipv4: &str, domain: &str, listener_port: u16) -> String {
|
||||
format!(
|
||||
r#"
|
||||
instance_name = "dns-int-{name}"
|
||||
ipv4 = "{ipv4}"
|
||||
|
||||
[dns]
|
||||
name = "{name}"
|
||||
domain = "{domain}"
|
||||
listeners = ["udp://127.0.0.1:{listener_port}"]
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial(dns_integration_rpc)]
|
||||
async fn config_string_single_node_resolves_self_and_custom_zone() {
|
||||
let listener_port = find_free_udp_port();
|
||||
let config = format!(
|
||||
r#"
|
||||
{}
|
||||
|
||||
[[dns.zone]]
|
||||
origin = "services.alpha.et-test"
|
||||
records = ["api IN A 10.77.0.7"]
|
||||
|
||||
[dns.zone.export]
|
||||
whitelist = ["*"]
|
||||
"#,
|
||||
cfg_with_listener("alpha", "10.144.144.11/24", "et-test", listener_port)
|
||||
);
|
||||
|
||||
let peer_mgr = prepare_env_from_config_str(&config).await;
|
||||
let mut dns_node = start_dns_node_without_nic(peer_mgr);
|
||||
|
||||
let server_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_port);
|
||||
check_dns_record_at(server_addr, "alpha.et-test.", "10.144.144.11").await;
|
||||
check_dns_record_at(server_addr, "api.services.alpha.et-test.", "10.77.0.7").await;
|
||||
|
||||
dns_node.stop().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial(dns_integration_rpc)]
|
||||
async fn config_string_two_nodes_sync_self_zone_and_exported_zone() {
|
||||
let listener_a = find_free_udp_port();
|
||||
let listener_b = find_free_udp_port();
|
||||
|
||||
let config_a = format!(
|
||||
r#"
|
||||
{}
|
||||
|
||||
[[dns.zone]]
|
||||
origin = "shared.mesh-test"
|
||||
records = ["app IN A 10.66.0.8"]
|
||||
|
||||
[dns.zone.export]
|
||||
whitelist = ["*"]
|
||||
"#,
|
||||
cfg_with_listener("node-a", "10.144.144.21/24", "mesh-test", listener_a)
|
||||
);
|
||||
|
||||
let config_b = cfg_with_listener("node-b", "10.144.144.22/24", "mesh-test", listener_b);
|
||||
|
||||
let peer_a = prepare_env_from_config_str(&config_a).await;
|
||||
let peer_b = prepare_env_from_config_str(&config_b).await;
|
||||
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
|
||||
wait_route_appear(peer_a.clone(), peer_b.clone())
|
||||
.await
|
||||
.expect("route should appear");
|
||||
|
||||
let mut node_a = start_dns_node_without_nic(peer_a);
|
||||
let mut node_b = start_dns_node_without_nic(peer_b);
|
||||
|
||||
let addr_a = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_a);
|
||||
let addr_b = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_b);
|
||||
|
||||
check_dns_record_at(addr_a, "node-b.mesh-test.", "10.144.144.22").await;
|
||||
check_dns_record_at(addr_b, "node-b.mesh-test.", "10.144.144.22").await;
|
||||
check_dns_record_at(addr_a, "app.shared.mesh-test.", "10.66.0.8").await;
|
||||
check_dns_record_at(addr_b, "app.shared.mesh-test.", "10.66.0.8").await;
|
||||
|
||||
node_a.stop().await.unwrap();
|
||||
node_b.stop().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial(dns_integration_rpc)]
|
||||
async fn config_string_zone_without_export_section_is_not_synced() {
|
||||
let listener_a = find_free_udp_port();
|
||||
let listener_b = find_free_udp_port();
|
||||
|
||||
let config_a = format!(
|
||||
r#"
|
||||
{}
|
||||
|
||||
[[dns.zone]]
|
||||
origin = "private.mesh-test"
|
||||
records = ["secret IN A 10.99.0.9"]
|
||||
"#,
|
||||
cfg_with_listener("local-a", "10.144.144.31/24", "mesh-test", listener_a)
|
||||
);
|
||||
|
||||
let config_b = cfg_with_listener("local-b", "10.144.144.32/24", "mesh-test", listener_b);
|
||||
|
||||
let peer_a = prepare_env_from_config_str(&config_a).await;
|
||||
let peer_b = prepare_env_from_config_str(&config_b).await;
|
||||
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
|
||||
wait_route_appear(peer_a.clone(), peer_b.clone())
|
||||
.await
|
||||
.expect("route should appear");
|
||||
|
||||
// Export behavior is determined by whether `[dns.zone.export]` exists.
|
||||
// Verify from peer-sync view to avoid host-wide DNS-server election side effects.
|
||||
let dns_a = DnsPeerMgr::new(peer_a.clone(), peer_a.get_global_ctx());
|
||||
dns_a.register();
|
||||
let dns_b = DnsPeerMgr::new(peer_b.clone(), peer_b.get_global_ctx());
|
||||
dns_b.register();
|
||||
dns_b
|
||||
.refresh(peer_a.my_peer_id(), Default::default(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let snapshot = dns_b.snapshot();
|
||||
assert!(
|
||||
!snapshot
|
||||
.zones
|
||||
.iter()
|
||||
.any(|z| z.content.contains("$ORIGIN private.mesh-test")),
|
||||
"zone without [dns.zone.export] should not be exported to peer snapshot"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial(dns_integration_rpc)]
|
||||
async fn config_string_export_section_disabled_is_not_synced() {
|
||||
let listener_a = find_free_udp_port();
|
||||
let listener_b = find_free_udp_port();
|
||||
|
||||
let config_a = format!(
|
||||
r#"
|
||||
{}
|
||||
|
||||
[[dns.zone]]
|
||||
origin = "disabled.mesh-test"
|
||||
records = ["secret IN A 10.99.1.9"]
|
||||
|
||||
[dns.zone.export]
|
||||
disabled = true
|
||||
"#,
|
||||
cfg_with_listener("local-da", "10.144.144.41/24", "mesh-test", listener_a)
|
||||
);
|
||||
|
||||
let config_b = cfg_with_listener("local-db", "10.144.144.42/24", "mesh-test", listener_b);
|
||||
|
||||
let peer_a = prepare_env_from_config_str(&config_a).await;
|
||||
let peer_b = prepare_env_from_config_str(&config_b).await;
|
||||
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
|
||||
wait_route_appear(peer_a.clone(), peer_b.clone())
|
||||
.await
|
||||
.expect("route should appear");
|
||||
|
||||
let dns_a = DnsPeerMgr::new(peer_a.clone(), peer_a.get_global_ctx());
|
||||
dns_a.register();
|
||||
let dns_b = DnsPeerMgr::new(peer_b.clone(), peer_b.get_global_ctx());
|
||||
dns_b.register();
|
||||
dns_b
|
||||
.refresh(peer_a.my_peer_id(), Default::default(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let snapshot = dns_b.snapshot();
|
||||
assert!(
|
||||
!snapshot
|
||||
.zones
|
||||
.iter()
|
||||
.any(|z| z.content.contains("$ORIGIN disabled.mesh-test")),
|
||||
"zone with [dns.zone.export] disabled=true should not be exported"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial(dns_integration_rpc)]
|
||||
async fn config_patch_updates_zone_record_visible_on_query() {
|
||||
let listener = find_free_udp_port();
|
||||
let config = format!(
|
||||
r#"
|
||||
{}
|
||||
|
||||
[[dns.zone]]
|
||||
origin = "patch.mesh-test"
|
||||
records = ["api IN A 10.80.0.1"]
|
||||
|
||||
[dns.zone.export]
|
||||
"#,
|
||||
cfg_with_listener("patch-node", "10.144.149.11/24", "mesh-test", listener)
|
||||
);
|
||||
|
||||
let peer = prepare_env_from_config_str(&config).await;
|
||||
let mut dns_node = start_dns_node_without_nic(peer.clone());
|
||||
let server_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener);
|
||||
|
||||
check_dns_record_at(server_addr, "api.patch.mesh-test.", "10.80.0.1").await;
|
||||
|
||||
let mut dns = peer.get_global_ctx().config.get_dns().into_raw();
|
||||
let mut zones = dns.zones.unwrap();
|
||||
let zone_idx = zones
|
||||
.iter()
|
||||
.position(|z| z.origin.to_string().contains("patch.mesh-test"))
|
||||
.expect("patch zone should exist");
|
||||
let mut zone = zones[zone_idx].clone().into_raw();
|
||||
zone.records = Some(vec!["api IN A 10.80.0.2".to_string()]);
|
||||
zones[zone_idx] = zone.try_into().expect("patch zone update should be valid");
|
||||
dns.zones = Some(zones);
|
||||
peer.get_global_ctx().config.set_dns(dns.into());
|
||||
peer.get_global_ctx()
|
||||
.issue_event(crate::common::global_ctx::GlobalCtxEvent::ConfigPatched(
|
||||
crate::proto::api::config::InstanceConfigPatch::default(),
|
||||
));
|
||||
|
||||
check_dns_record_at(server_addr, "api.patch.mesh-test.", "10.80.0.2").await;
|
||||
|
||||
dns_node.stop().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial(dns_integration_rpc)]
|
||||
async fn config_patch_reloads_listener_binding() {
|
||||
let listener_old = find_free_udp_port();
|
||||
let listener_new = find_free_udp_port();
|
||||
let config = cfg_with_listener(
|
||||
"listener-patch",
|
||||
"10.144.150.11/24",
|
||||
"mesh-test",
|
||||
listener_old,
|
||||
);
|
||||
|
||||
let peer = prepare_env_from_config_str(&config).await;
|
||||
let mut dns_node = start_dns_node_without_nic(peer.clone());
|
||||
|
||||
let old_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_old);
|
||||
let new_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_new);
|
||||
check_dns_record_at(old_addr, "listener-patch.mesh-test.", "10.144.150.11").await;
|
||||
|
||||
let mut dns = peer.get_global_ctx().config.get_dns().into_raw();
|
||||
dns.listeners = Some(
|
||||
vec![
|
||||
format!("udp://127.0.0.1:{listener_new}")
|
||||
.parse()
|
||||
.expect("invalid listener"),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
peer.get_global_ctx().config.set_dns(dns.into());
|
||||
peer.get_global_ctx()
|
||||
.issue_event(crate::common::global_ctx::GlobalCtxEvent::ConfigPatched(
|
||||
crate::proto::api::config::InstanceConfigPatch::default(),
|
||||
));
|
||||
|
||||
check_dns_record_at(new_addr, "listener-patch.mesh-test.", "10.144.150.11").await;
|
||||
check_dns_unavailable_at(old_addr, "listener-patch.mesh-test.").await;
|
||||
|
||||
dns_node.stop().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial(dns_integration_rpc)]
|
||||
async fn config_string_three_nodes_partition_and_recover_dns_propagation() {
|
||||
let listener_a = find_free_udp_port();
|
||||
let listener_b = find_free_udp_port();
|
||||
let listener_c = find_free_udp_port();
|
||||
|
||||
let config_a = cfg_with_listener("node-a7", "10.144.151.11/24", "mesh7-test", listener_a);
|
||||
let config_b = cfg_with_listener("node-b7", "10.144.151.12/24", "mesh7-test", listener_b);
|
||||
let config_c = format!(
|
||||
r#"
|
||||
{}
|
||||
|
||||
[[dns.zone]]
|
||||
origin = "shared-c7.mesh7-test"
|
||||
records = ["svc IN A 10.77.7.7"]
|
||||
|
||||
[dns.zone.export]
|
||||
"#,
|
||||
cfg_with_listener("node-c7", "10.144.151.13/24", "mesh7-test", listener_c)
|
||||
);
|
||||
|
||||
let peer_a = prepare_env_from_config_str(&config_a).await;
|
||||
let peer_b = prepare_env_from_config_str(&config_b).await;
|
||||
let peer_c = prepare_env_from_config_str(&config_c).await;
|
||||
|
||||
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
|
||||
connect_peer_manager(peer_b.clone(), peer_c.clone()).await;
|
||||
wait_route_appear(peer_a.clone(), peer_c.clone())
|
||||
.await
|
||||
.expect("route a-c should appear via b");
|
||||
|
||||
let mut node_a = start_dns_node_without_nic(peer_a.clone());
|
||||
let mut node_b = start_dns_node_without_nic(peer_b.clone());
|
||||
let mut node_c = start_dns_node_without_nic(peer_c.clone());
|
||||
|
||||
let addr_a = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_a);
|
||||
check_dns_record_at(addr_a, "node-c7.mesh7-test.", "10.144.151.13").await;
|
||||
check_dns_record_at(addr_a, "svc.shared-c7.mesh7-test.", "10.77.7.7").await;
|
||||
wait_peer_zone_visibility(
|
||||
peer_a.clone(),
|
||||
peer_c.my_peer_id(),
|
||||
"node-c7.mesh7-test",
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
wait_peer_zone_visibility(
|
||||
peer_a.clone(),
|
||||
peer_c.my_peer_id(),
|
||||
"shared-c7.mesh7-test",
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
disconnect_all_peer_conns(peer_b.clone(), peer_c.clone()).await;
|
||||
wait_route_disappear(peer_a.clone(), peer_c.my_peer_id()).await;
|
||||
// Validate via peer-sync snapshot to avoid process-wide DNS-server election side effects.
|
||||
wait_peer_zone_visibility(
|
||||
peer_a.clone(),
|
||||
peer_c.my_peer_id(),
|
||||
"node-c7.mesh7-test",
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
wait_peer_zone_visibility(
|
||||
peer_a.clone(),
|
||||
peer_c.my_peer_id(),
|
||||
"shared-c7.mesh7-test",
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
connect_peer_manager(peer_b.clone(), peer_c.clone()).await;
|
||||
wait_route_appear(peer_a.clone(), peer_c.clone())
|
||||
.await
|
||||
.expect("route a-c should recover via b");
|
||||
|
||||
wait_peer_zone_visibility(
|
||||
peer_a.clone(),
|
||||
peer_c.my_peer_id(),
|
||||
"node-c7.mesh7-test",
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
wait_peer_zone_visibility(
|
||||
peer_a.clone(),
|
||||
peer_c.my_peer_id(),
|
||||
"shared-c7.mesh7-test",
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
check_dns_record_at(addr_a, "node-c7.mesh7-test.", "10.144.151.13").await;
|
||||
check_dns_record_at(addr_a, "svc.shared-c7.mesh7-test.", "10.77.7.7").await;
|
||||
|
||||
node_a.stop().await.unwrap();
|
||||
node_b.stop().await.unwrap();
|
||||
node_c.stop().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial(dns_integration_rpc)]
|
||||
async fn config_string_three_nodes_chain_sync_self_and_exported_zone() {
|
||||
let listener_a = find_free_udp_port();
|
||||
let listener_b = find_free_udp_port();
|
||||
let listener_c = find_free_udp_port();
|
||||
|
||||
let config_a = cfg_with_listener("node-a3", "10.144.145.11/24", "mesh3-test", listener_a);
|
||||
let config_b = cfg_with_listener("node-b3", "10.144.145.12/24", "mesh3-test", listener_b);
|
||||
let config_c = format!(
|
||||
r#"
|
||||
{}
|
||||
|
||||
[[dns.zone]]
|
||||
origin = "shared-c.mesh3-test"
|
||||
records = ["api IN A 10.66.1.8"]
|
||||
|
||||
[dns.zone.export]
|
||||
"#,
|
||||
cfg_with_listener("node-c3", "10.144.145.13/24", "mesh3-test", listener_c)
|
||||
);
|
||||
|
||||
let peer_a = prepare_env_from_config_str(&config_a).await;
|
||||
let peer_b = prepare_env_from_config_str(&config_b).await;
|
||||
let peer_c = prepare_env_from_config_str(&config_c).await;
|
||||
|
||||
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
|
||||
connect_peer_manager(peer_b.clone(), peer_c.clone()).await;
|
||||
wait_route_appear(peer_a.clone(), peer_b.clone())
|
||||
.await
|
||||
.expect("route a-b should appear");
|
||||
wait_route_appear(peer_b.clone(), peer_c.clone())
|
||||
.await
|
||||
.expect("route b-c should appear");
|
||||
wait_route_appear(peer_a.clone(), peer_c.clone())
|
||||
.await
|
||||
.expect("route a-c should appear via b");
|
||||
|
||||
let mut node_a = start_dns_node_without_nic(peer_a);
|
||||
let mut node_b = start_dns_node_without_nic(peer_b);
|
||||
let mut node_c = start_dns_node_without_nic(peer_c);
|
||||
|
||||
let addr_a = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_a);
|
||||
let addr_b = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_b);
|
||||
let addr_c = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_c);
|
||||
|
||||
check_dns_record_at(addr_a, "node-c3.mesh3-test.", "10.144.145.13").await;
|
||||
check_dns_record_at(addr_b, "node-c3.mesh3-test.", "10.144.145.13").await;
|
||||
check_dns_record_at(addr_c, "node-a3.mesh3-test.", "10.144.145.11").await;
|
||||
check_dns_record_at(addr_a, "api.shared-c.mesh3-test.", "10.66.1.8").await;
|
||||
check_dns_record_at(addr_c, "api.shared-c.mesh3-test.", "10.66.1.8").await;
|
||||
|
||||
node_a.stop().await.unwrap();
|
||||
node_b.stop().await.unwrap();
|
||||
node_c.stop().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial(dns_integration_rpc)]
|
||||
async fn config_string_three_nodes_late_join_propagates_dns() {
|
||||
let listener_a = find_free_udp_port();
|
||||
let listener_b = find_free_udp_port();
|
||||
let listener_c = find_free_udp_port();
|
||||
|
||||
let config_a = cfg_with_listener("node-a4", "10.144.146.11/24", "mesh4-test", listener_a);
|
||||
let config_b = cfg_with_listener("node-b4", "10.144.146.12/24", "mesh4-test", listener_b);
|
||||
let config_c = format!(
|
||||
r#"
|
||||
{}
|
||||
|
||||
[[dns.zone]]
|
||||
origin = "joined.mesh4-test"
|
||||
records = ["svc IN A 10.66.2.8"]
|
||||
|
||||
[dns.zone.export]
|
||||
"#,
|
||||
cfg_with_listener("node-c4", "10.144.146.13/24", "mesh4-test", listener_c)
|
||||
);
|
||||
|
||||
let peer_a = prepare_env_from_config_str(&config_a).await;
|
||||
let peer_b = prepare_env_from_config_str(&config_b).await;
|
||||
let peer_c = prepare_env_from_config_str(&config_c).await;
|
||||
|
||||
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
|
||||
wait_route_appear(peer_a.clone(), peer_b.clone())
|
||||
.await
|
||||
.expect("route a-b should appear");
|
||||
|
||||
let mut node_a = start_dns_node_without_nic(peer_a.clone());
|
||||
let mut node_b = start_dns_node_without_nic(peer_b.clone());
|
||||
|
||||
let addr_a = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), listener_a);
|
||||
check_dns_record_missing_at(addr_a, "node-c4.mesh4-test.").await;
|
||||
|
||||
connect_peer_manager(peer_b.clone(), peer_c.clone()).await;
|
||||
wait_route_appear(peer_b.clone(), peer_c.clone())
|
||||
.await
|
||||
.expect("route b-c should appear");
|
||||
wait_route_appear(peer_a.clone(), peer_c.clone())
|
||||
.await
|
||||
.expect("route a-c should appear after c joins");
|
||||
|
||||
let mut node_c = start_dns_node_without_nic(peer_c);
|
||||
|
||||
check_dns_record_at(addr_a, "node-c4.mesh4-test.", "10.144.146.13").await;
|
||||
check_dns_record_at(addr_a, "svc.joined.mesh4-test.", "10.66.2.8").await;
|
||||
|
||||
node_a.stop().await.unwrap();
|
||||
node_b.stop().await.unwrap();
|
||||
node_c.stop().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial(dns_integration_rpc)]
|
||||
async fn config_string_three_nodes_zone_without_export_not_synced_across_hop() {
|
||||
let listener_a = find_free_udp_port();
|
||||
let listener_b = find_free_udp_port();
|
||||
let listener_c = find_free_udp_port();
|
||||
|
||||
let config_a = cfg_with_listener("node-a5", "10.144.147.11/24", "mesh5-test", listener_a);
|
||||
let config_b = cfg_with_listener("node-b5", "10.144.147.12/24", "mesh5-test", listener_b);
|
||||
let config_c = format!(
|
||||
r#"
|
||||
{}
|
||||
|
||||
[[dns.zone]]
|
||||
origin = "private-c.mesh5-test"
|
||||
records = ["secret IN A 10.66.3.8"]
|
||||
"#,
|
||||
cfg_with_listener("node-c5", "10.144.147.13/24", "mesh5-test", listener_c)
|
||||
);
|
||||
|
||||
let peer_a = prepare_env_from_config_str(&config_a).await;
|
||||
let peer_b = prepare_env_from_config_str(&config_b).await;
|
||||
let peer_c = prepare_env_from_config_str(&config_c).await;
|
||||
|
||||
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
|
||||
connect_peer_manager(peer_b.clone(), peer_c.clone()).await;
|
||||
wait_route_appear(peer_a.clone(), peer_c.clone())
|
||||
.await
|
||||
.expect("route a-c should appear via b");
|
||||
|
||||
let dns_c = DnsPeerMgr::new(peer_c.clone(), peer_c.get_global_ctx());
|
||||
dns_c.register();
|
||||
let dns_a = DnsPeerMgr::new(peer_a.clone(), peer_a.get_global_ctx());
|
||||
dns_a.register();
|
||||
dns_a
|
||||
.refresh(peer_c.my_peer_id(), Default::default(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let snapshot = dns_a.snapshot();
|
||||
assert!(
|
||||
!snapshot
|
||||
.zones
|
||||
.iter()
|
||||
.any(|z| z.content.contains("$ORIGIN private-c.mesh5-test")),
|
||||
"zone without [dns.zone.export] should not sync over multi-hop"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial(dns_integration_rpc)]
|
||||
async fn config_string_two_nodes_peer_dns_offline_then_rejoin() {
|
||||
let listener_a = find_free_udp_port();
|
||||
let listener_b = find_free_udp_port();
|
||||
|
||||
let config_a = cfg_with_listener("node-a6", "10.144.148.11/24", "mesh6-test", listener_a);
|
||||
let config_b = cfg_with_listener("node-b6", "10.144.148.12/24", "mesh6-test", listener_b);
|
||||
|
||||
let peer_a = prepare_env_from_config_str(&config_a).await;
|
||||
let peer_b = prepare_env_from_config_str(&config_b).await;
|
||||
|
||||
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
|
||||
wait_route_appear(peer_a.clone(), peer_b.clone())
|
||||
.await
|
||||
.expect("route should appear");
|
||||
|
||||
let dns_b = DnsPeerMgr::new(peer_b.clone(), peer_b.get_global_ctx());
|
||||
dns_b.register();
|
||||
let dns_a_online = DnsPeerMgr::new(peer_a.clone(), peer_a.get_global_ctx());
|
||||
dns_a_online.register();
|
||||
dns_a_online
|
||||
.refresh(peer_b.my_peer_id(), Default::default(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
dns_a_online
|
||||
.snapshot()
|
||||
.zones
|
||||
.iter()
|
||||
.any(|z| z.content.contains("$ORIGIN node-b6.mesh6-test")),
|
||||
"peer B self zone should be visible after initial refresh"
|
||||
);
|
||||
|
||||
// Simulate peer offline by closing all direct connections and waiting route withdrawal.
|
||||
if let Some(conns) = peer_a
|
||||
.get_peer_map()
|
||||
.list_peer_conns(peer_b.my_peer_id())
|
||||
.await
|
||||
{
|
||||
for conn in conns {
|
||||
let conn_id = conn.conn_id.parse().expect("invalid conn id");
|
||||
let _ = peer_a.close_peer_conn(peer_b.my_peer_id(), &conn_id).await;
|
||||
}
|
||||
}
|
||||
if let Some(conns) = peer_b
|
||||
.get_peer_map()
|
||||
.list_peer_conns(peer_a.my_peer_id())
|
||||
.await
|
||||
{
|
||||
for conn in conns {
|
||||
let conn_id = conn.conn_id.parse().expect("invalid conn id");
|
||||
let _ = peer_b.close_peer_conn(peer_a.my_peer_id(), &conn_id).await;
|
||||
}
|
||||
}
|
||||
|
||||
wait_route_disappear(peer_a.clone(), peer_b.my_peer_id()).await;
|
||||
wait_route_disappear(peer_b.clone(), peer_a.my_peer_id()).await;
|
||||
|
||||
// Cached remote zones should be purged after peer cache idle timeout.
|
||||
tokio::time::sleep(Duration::from_secs(4)).await;
|
||||
let dns_a_offline = DnsPeerMgr::new(peer_a.clone(), peer_a.get_global_ctx());
|
||||
assert!(
|
||||
!dns_a_offline
|
||||
.snapshot()
|
||||
.zones
|
||||
.iter()
|
||||
.any(|z| z.content.contains("$ORIGIN node-b6.mesh6-test")),
|
||||
"peer B self zone should disappear after route withdrawal and cache expiry"
|
||||
);
|
||||
|
||||
connect_peer_manager(peer_a.clone(), peer_b.clone()).await;
|
||||
wait_route_appear(peer_a.clone(), peer_b.clone())
|
||||
.await
|
||||
.expect("route should re-appear");
|
||||
|
||||
let dns_a_rejoin = DnsPeerMgr::new(peer_a.clone(), peer_a.get_global_ctx());
|
||||
dns_a_rejoin
|
||||
.refresh(peer_b.my_peer_id(), Default::default(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
dns_a_rejoin
|
||||
.snapshot()
|
||||
.zones
|
||||
.iter()
|
||||
.any(|z| z.content.contains("$ORIGIN node-b6.mesh6-test")),
|
||||
"peer B self zone should be restored after DNS RPC rejoins"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
use crate::proto;
|
||||
use crate::proto::utils::{RepeatedDeserialize, RepeatedMessageModel, RepeatedSerialize};
|
||||
use anyhow::{Error, anyhow};
|
||||
use hickory_net::xfer::Protocol;
|
||||
use hickory_resolver::config::{ConnectionConfig, NameServerConfig, ProtocolConfig};
|
||||
use serde::de::IntoDeserializer;
|
||||
use serde::{Deserialize, Deserializer, de};
|
||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr)]
|
||||
pub struct NameServerAddr {
|
||||
pub protocol: Protocol,
|
||||
pub addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl From<NameServerAddr> for NameServerConfig {
|
||||
fn from(value: NameServerAddr) -> Self {
|
||||
let mut config = match value.protocol {
|
||||
Protocol::Udp => ConnectionConfig::udp(),
|
||||
Protocol::Tcp => ConnectionConfig::tcp(),
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
config.port = value.addr.port();
|
||||
Self::new(value.addr.ip(), true, vec![config])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(IpAddr, &ConnectionConfig)> for NameServerAddr {
|
||||
fn from(value: (IpAddr, &ConnectionConfig)) -> Self {
|
||||
let (ip, config) = value;
|
||||
Self {
|
||||
protocol: config.protocol.to_protocol(),
|
||||
addr: SocketAddr::new(ip, config.port),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Url> for NameServerAddr {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(url: &Url) -> Result<Self, Self::Error> {
|
||||
let protocol = match Protocol::deserialize(url.scheme().into_deserializer())
|
||||
.map_err(|e: de::value::Error| anyhow!("invalid protocol '{}': {}", url.scheme(), e))?
|
||||
{
|
||||
Protocol::Udp => ProtocolConfig::Udp,
|
||||
Protocol::Tcp => ProtocolConfig::Tcp,
|
||||
p => return Err(anyhow!("unsupported protocol: {}", p)),
|
||||
};
|
||||
let host = url.host_str().ok_or(anyhow!("host not found"))?;
|
||||
let port = url.port().unwrap_or(protocol.default_port());
|
||||
let addr = if let Ok(addr) = IpAddr::from_str(host) {
|
||||
SocketAddr::new(addr, port)
|
||||
} else {
|
||||
return Err(anyhow!("invalid address: {}", host));
|
||||
};
|
||||
Ok(Self {
|
||||
protocol: protocol.to_protocol(),
|
||||
addr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&proto::common::Url> for NameServerAddr {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: &proto::common::Url) -> Result<Self, Self::Error> {
|
||||
(&Url::try_from(value)?).try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&NameServerAddr> for Url {
|
||||
fn from(value: &NameServerAddr) -> Self {
|
||||
Url::parse(&format!("{}://{}", value.protocol, value.addr)).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&NameServerAddr> for proto::common::Url {
|
||||
fn from(value: &NameServerAddr) -> Self {
|
||||
Url::from(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NameServerAddr> for Url {
|
||||
fn from(value: NameServerAddr) -> Self {
|
||||
(&value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NameServerAddr> for proto::common::Url {
|
||||
fn from(value: NameServerAddr) -> Self {
|
||||
(&value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for NameServerAddr {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
(&Url::parse(s)?).try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for NameServerAddr {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(Url::from(*self).as_str())
|
||||
}
|
||||
}
|
||||
|
||||
pub type NameServerAddrGroup = RepeatedMessageModel<NameServerAddr>;
|
||||
|
||||
impl From<&NameServerConfig> for NameServerAddrGroup {
|
||||
fn from(value: &NameServerConfig) -> Self {
|
||||
value
|
||||
.connections
|
||||
.iter()
|
||||
.map(|c| (value.ip, c).into())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SocketAddr> for NameServerAddrGroup {
|
||||
fn from(value: SocketAddr) -> Self {
|
||||
vec![
|
||||
NameServerAddr {
|
||||
protocol: Protocol::Udp,
|
||||
addr: value,
|
||||
},
|
||||
NameServerAddr {
|
||||
protocol: Protocol::Tcp,
|
||||
addr: value,
|
||||
},
|
||||
]
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IpAddr> for NameServerAddrGroup {
|
||||
fn from(value: IpAddr) -> Self {
|
||||
SocketAddr::new(value, 53).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for NameServerAddrGroup {
|
||||
fn from(value: u16) -> Self {
|
||||
SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl RepeatedSerialize for NameServerAddr {}
|
||||
|
||||
impl<'de> RepeatedDeserialize<'de> for NameServerAddr {
|
||||
fn deserialize<D>(deserializer: D) -> Result<NameServerAddrGroup, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Candidate {
|
||||
NameServerAddr(NameServerAddr),
|
||||
U16(u16),
|
||||
IpAddr(IpAddr),
|
||||
SocketAddr(SocketAddr),
|
||||
}
|
||||
|
||||
let items = Vec::<Candidate>::deserialize(deserializer)?;
|
||||
let items = items
|
||||
.into_iter()
|
||||
.flat_map(|item| -> NameServerAddrGroup {
|
||||
match item {
|
||||
Candidate::NameServerAddr(addr) => vec![addr].into(),
|
||||
Candidate::U16(port) => port.into(),
|
||||
Candidate::IpAddr(ip) => ip.into(),
|
||||
Candidate::SocketAddr(addr) => addr.into(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod addr;
|
||||
pub mod response;
|
||||
pub mod zone_handler;
|
||||
@@ -0,0 +1,59 @@
|
||||
use hickory_net::NetError;
|
||||
use hickory_proto::rr::Record;
|
||||
use hickory_proto::serialize::binary::BinEncoder;
|
||||
use hickory_server::server::{ResponseHandler, ResponseInfo};
|
||||
use hickory_server::zone_handler::MessageResponse;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
// ResponseWrapper for serializing DNS responses into a byte buffer.
|
||||
// Used by the address hijacking NIC packet filter to produce DNS replies in-place.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResponseHandle {
|
||||
inner: Arc<Mutex<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl ResponseHandle {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(Vec::with_capacity(capacity))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> Option<Vec<u8>> {
|
||||
Arc::into_inner(self.inner).map(Mutex::into_inner)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RecordIter<'r>: Iterator<Item = &'r Record> + Send + 'r {}
|
||||
|
||||
impl<'r, T> RecordIter<'r> for T where T: Iterator<Item = &'r Record> + Send + 'r {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ResponseHandler for ResponseHandle {
|
||||
async fn send_response<'r>(
|
||||
&mut self,
|
||||
response: MessageResponse<
|
||||
'_,
|
||||
'r,
|
||||
impl RecordIter<'r>,
|
||||
impl RecordIter<'r>,
|
||||
impl RecordIter<'r>,
|
||||
impl RecordIter<'r>,
|
||||
>,
|
||||
) -> Result<ResponseInfo, NetError> {
|
||||
let max_size = if let Some(edns) = response.edns() {
|
||||
edns.max_payload()
|
||||
} else {
|
||||
hickory_net::udp::MAX_RECEIVE_BUFFER_SIZE as u16
|
||||
};
|
||||
|
||||
let mut inner = self.inner.lock();
|
||||
inner.clear();
|
||||
let mut encoder = BinEncoder::new(inner.as_mut());
|
||||
encoder.set_max_size(max_size);
|
||||
response
|
||||
.destructive_emit(&mut encoder)
|
||||
.map_err(NetError::Proto)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
use crate::dns::config::zone::Fallthrough;
|
||||
use delegate::delegate;
|
||||
use derive_more::{Constructor, Deref, DerefMut};
|
||||
use hickory_proto::op::ResponseCode;
|
||||
use hickory_proto::rr::{LowerName, RecordType, TSigResponseContext};
|
||||
use hickory_server::server::{Request, RequestInfo};
|
||||
use hickory_server::zone_handler::{
|
||||
AuthLookup, AxfrPolicy, LookupControlFlow, LookupError, LookupOptions, ZoneHandler, ZoneType,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub type ArcZoneHandler = Arc<dyn ZoneHandler>;
|
||||
|
||||
#[derive(Constructor, Deref, DerefMut)]
|
||||
pub struct ChainedZoneHandler<H>
|
||||
where
|
||||
H: ZoneHandler,
|
||||
{
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
handler: H,
|
||||
fallthrough: HashSet<Fallthrough>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<H> ZoneHandler for ChainedZoneHandler<H>
|
||||
where
|
||||
H: ZoneHandler,
|
||||
{
|
||||
delegate! {
|
||||
to self.handler {
|
||||
fn zone_type(&self) -> ZoneType;
|
||||
fn axfr_policy(&self) -> AxfrPolicy;
|
||||
fn origin(&self) -> &LowerName;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn update(
|
||||
&self,
|
||||
update: &Request,
|
||||
now: u64,
|
||||
) -> (Result<bool, ResponseCode>, Option<TSigResponseContext>) {
|
||||
self.handler.update(update, now).await
|
||||
}
|
||||
#[inline]
|
||||
async fn lookup(
|
||||
&self,
|
||||
name: &LowerName,
|
||||
rtype: RecordType,
|
||||
request_info: Option<&RequestInfo<'_>>,
|
||||
lookup_options: LookupOptions,
|
||||
) -> LookupControlFlow<AuthLookup> {
|
||||
self.handler
|
||||
.lookup(name, rtype, request_info, lookup_options)
|
||||
.await
|
||||
}
|
||||
#[inline]
|
||||
async fn search(
|
||||
&self,
|
||||
request: &Request,
|
||||
lookup_options: LookupOptions,
|
||||
) -> (LookupControlFlow<AuthLookup>, Option<TSigResponseContext>) {
|
||||
let (result, tsig) = self.handler.search(request, lookup_options).await;
|
||||
|
||||
match &result {
|
||||
LookupControlFlow::Continue(Err(e)) | LookupControlFlow::Break(Err(e))
|
||||
if self.fallthrough.contains(&Fallthrough::Any)
|
||||
|| matches!(e, LookupError::ResponseCode(c) if self.fallthrough.contains(&(*c).into())) =>
|
||||
{
|
||||
(LookupControlFlow::Skip, None)
|
||||
}
|
||||
_ => (result, tsig),
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
async fn nsec_records(
|
||||
&self,
|
||||
name: &LowerName,
|
||||
lookup_options: LookupOptions,
|
||||
) -> LookupControlFlow<AuthLookup> {
|
||||
self.handler.nsec_records(name, lookup_options).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,572 @@
|
||||
use crate::dns::config::zone::Fallthrough;
|
||||
use crate::dns::utils::addr::{NameServerAddr, NameServerAddrGroup};
|
||||
use crate::dns::utils::zone_handler::{ArcZoneHandler, ChainedZoneHandler};
|
||||
use crate::proto::dns::ZoneData;
|
||||
use crate::proto::utils::RepeatedMessageModel;
|
||||
use crate::utils::dns::resolver_conf;
|
||||
use hickory_net::runtime::TokioRuntimeProvider;
|
||||
use hickory_proto::rr::{LowerName, RecordSet, RrKey};
|
||||
use hickory_proto::serialize::txt::Parser;
|
||||
use hickory_server::store::forwarder::{ForwardConfig, ForwardZoneHandler};
|
||||
use hickory_server::store::in_memory::InMemoryZoneHandler;
|
||||
use hickory_server::zone_handler::{AxfrPolicy, ZoneType};
|
||||
use indexmap::IndexMap;
|
||||
use itertools::chain;
|
||||
use maplit::hashset;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Zone {
|
||||
origin: LowerName,
|
||||
records: BTreeMap<RrKey, RecordSet>,
|
||||
pub forward: Option<ForwardConfig>,
|
||||
fallthrough: HashSet<Fallthrough>,
|
||||
}
|
||||
|
||||
impl Zone {
|
||||
pub fn system() -> Self {
|
||||
let (config, opts) = resolver_conf();
|
||||
let forward = ForwardConfig {
|
||||
name_servers: config.name_servers().to_vec(),
|
||||
options: Some(opts),
|
||||
};
|
||||
Self {
|
||||
origin: ".".parse().unwrap(),
|
||||
forward: Some(forward),
|
||||
fallthrough: hashset! {},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Zone {
|
||||
pub fn create_memory_zone_handler(&self) -> Option<ArcZoneHandler> {
|
||||
(!self.records.is_empty()).then(|| {
|
||||
let mut memory = InMemoryZoneHandler::<TokioRuntimeProvider>::empty(
|
||||
self.origin.clone().into(),
|
||||
ZoneType::External,
|
||||
AxfrPolicy::default(),
|
||||
);
|
||||
|
||||
memory.records_get_mut().extend(
|
||||
self.records
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, Arc::new(v))),
|
||||
);
|
||||
|
||||
Arc::new(ChainedZoneHandler::new(memory, self.fallthrough.clone())) as _
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_forward_zone_handler(&self) -> Option<ArcZoneHandler> {
|
||||
self.forward.as_ref().and_then(|forward| {
|
||||
ForwardZoneHandler::builder_with_config(
|
||||
forward.clone(),
|
||||
TokioRuntimeProvider::default(),
|
||||
)
|
||||
.build()
|
||||
.inspect_err(|error| tracing::error!(?error, "failed to create forward zone_handler"))
|
||||
.ok()
|
||||
.map(|handler| {
|
||||
Arc::new(ChainedZoneHandler::new(handler, self.fallthrough.clone())) as _
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&ZoneData> for Zone {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &ZoneData) -> Result<Self, Self::Error> {
|
||||
let (origin, records) = Parser::new(&value.content, None, None)
|
||||
.parse()
|
||||
.map_err(|e| anyhow::anyhow!("failed to parse zone data: {e}"))?;
|
||||
|
||||
let name_servers = value
|
||||
.forwarders
|
||||
.iter()
|
||||
.map(NameServerAddr::try_from)
|
||||
.map(|a| a.map(Into::into))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let forward = (!name_servers.is_empty()).then_some(ForwardConfig {
|
||||
name_servers,
|
||||
options: None,
|
||||
});
|
||||
|
||||
let fallthrough = value.fallthrough.iter().copied().map(Into::into).collect();
|
||||
|
||||
Ok(Self {
|
||||
origin: origin.into(),
|
||||
records,
|
||||
forward,
|
||||
fallthrough,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Zone> for ZoneData {
|
||||
fn from(value: Zone) -> Self {
|
||||
let records = value
|
||||
.records
|
||||
.values()
|
||||
.flat_map(RecordSet::records_without_rrsigs)
|
||||
.map(ToString::to_string);
|
||||
|
||||
let forwarders = value
|
||||
.forward
|
||||
.into_iter()
|
||||
.flat_map(|f| f.name_servers.into_iter())
|
||||
.map(|ns| (&ns).into())
|
||||
.flat_map(NameServerAddrGroup::into_iter)
|
||||
.map(Into::into);
|
||||
|
||||
Self::new(&value.origin, 0, records, forwarders, value.fallthrough)
|
||||
}
|
||||
}
|
||||
|
||||
pub type ZoneGroup = RepeatedMessageModel<Zone>;
|
||||
|
||||
impl ZoneGroup {
|
||||
pub fn into_groups(self) -> IndexMap<LowerName, ZoneGroup> {
|
||||
self.into_iter().fold(IndexMap::new(), |mut map, zone| {
|
||||
map.entry(zone.origin.clone()).or_default().push(zone);
|
||||
map
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_zone_handlers(&self) -> impl Iterator<Item = ArcZoneHandler> + use<'_> {
|
||||
self.iter().flat_map(|zone| {
|
||||
chain(
|
||||
zone.create_memory_zone_handler(),
|
||||
zone.create_forward_zone_handler(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dns::tests::new_request;
|
||||
use crate::dns::utils::response::ResponseHandle;
|
||||
use crate::proto::common::Url;
|
||||
use crate::proto::dns::ZoneData;
|
||||
use hickory_proto::op::{Message, ResponseCode};
|
||||
use hickory_proto::rr::{RData, Record, RecordType, RrsetRecords};
|
||||
use hickory_server::Server;
|
||||
use hickory_server::zone_handler::Catalog;
|
||||
use maplit::hashset;
|
||||
use std::collections::HashSet;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
impl Zone {
|
||||
// Test-only record iterator for precise assertions.
|
||||
pub fn iter_records(&self) -> impl Iterator<Item = &Record> {
|
||||
self.records
|
||||
.values()
|
||||
.filter(|set| !set.is_empty())
|
||||
.flat_map(|set| {
|
||||
let RrsetRecords::RecordsOnly(records) = set.records_without_rrsigs() else {
|
||||
unreachable!()
|
||||
};
|
||||
records
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn zone_data_with_fallthrough(
|
||||
origin: &str,
|
||||
records: Vec<&str>,
|
||||
forwarders: Vec<&str>,
|
||||
fallthrough: HashSet<Fallthrough>,
|
||||
) -> ZoneData {
|
||||
ZoneData::new(
|
||||
&origin.parse().unwrap(),
|
||||
60,
|
||||
records,
|
||||
forwarders
|
||||
.into_iter()
|
||||
.map(|url| Url::from_str(url).unwrap()),
|
||||
fallthrough,
|
||||
)
|
||||
}
|
||||
|
||||
fn zone_data(origin: &str, records: Vec<&str>, forwarders: Vec<&str>) -> ZoneData {
|
||||
zone_data_with_fallthrough(origin, records, forwarders, hashset! {Fallthrough::Any})
|
||||
}
|
||||
|
||||
fn build_catalog(zones: ZoneGroup) -> Catalog {
|
||||
zones
|
||||
.into_groups()
|
||||
.into_iter()
|
||||
.fold(Catalog::new(), |mut catalog, (origin, group)| {
|
||||
catalog.upsert(origin, group.iter_zone_handlers().collect());
|
||||
catalog
|
||||
})
|
||||
}
|
||||
|
||||
async fn lookup_message(
|
||||
catalog: &Catalog,
|
||||
name: &str,
|
||||
record_type: RecordType,
|
||||
) -> anyhow::Result<(ResponseCode, Option<Message>)> {
|
||||
let request = new_request(name, record_type)?;
|
||||
let response = ResponseHandle::new(1024);
|
||||
let info = catalog.lookup(&request, None, 0, response.clone()).await;
|
||||
let message = response
|
||||
.into_inner()
|
||||
.map(|raw| Message::from_vec(&raw))
|
||||
.transpose()?;
|
||||
Ok((info.response_code, message))
|
||||
}
|
||||
|
||||
fn has_a_answer(message: &Message, expected: Ipv4Addr) -> bool {
|
||||
message
|
||||
.answers
|
||||
.iter()
|
||||
.any(|record| matches!(record.data, RData::A(addr) if *addr == expected))
|
||||
}
|
||||
|
||||
fn has_aaaa_answer(message: &Message, expected: Ipv6Addr) -> bool {
|
||||
message
|
||||
.answers
|
||||
.iter()
|
||||
.any(|record| matches!(record.data, RData::AAAA(addr) if *addr == expected))
|
||||
}
|
||||
|
||||
async fn start_upstream_server() -> anyhow::Result<(SocketAddr, JoinHandle<()>)> {
|
||||
let upstream = Zone::try_from(&zone_data(
|
||||
"upstream.test",
|
||||
vec!["from-forward 60 IN A 203.0.113.9"],
|
||||
vec![],
|
||||
))?;
|
||||
|
||||
let mut catalog = Catalog::new();
|
||||
catalog.upsert(
|
||||
upstream.origin.clone(),
|
||||
vec![upstream.create_memory_zone_handler().unwrap()],
|
||||
);
|
||||
|
||||
let socket = UdpSocket::bind("127.0.0.1:0").await?;
|
||||
let addr = socket.local_addr()?;
|
||||
|
||||
let mut server = Server::new(catalog);
|
||||
server.register_socket(socket);
|
||||
let handle = tokio::spawn(async move {
|
||||
let _ = server.block_until_done().await;
|
||||
});
|
||||
|
||||
Ok((addr, handle))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zone_try_from_rejects_invalid_record() {
|
||||
let data = zone_data("invalid-record.test", vec!["this is not a record"], vec![]);
|
||||
|
||||
let err = Zone::try_from(&data).expect_err("invalid record should fail");
|
||||
assert!(err.to_string().contains("failed to parse zone data"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zone_try_from_rejects_invalid_forwarder_protocol() {
|
||||
let data = zone_data("invalid-forwarder.test", vec![], vec!["http://1.1.1.1:53"]);
|
||||
|
||||
let err = Zone::try_from(&data).expect_err("unsupported forwarder should fail");
|
||||
assert!(err.to_string().contains("unsupported") || err.to_string().contains("protocol"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_zone_creates_no_zone_handler() -> anyhow::Result<()> {
|
||||
let zone = Zone::try_from(&zone_data("empty.test", vec![], vec![]))?;
|
||||
|
||||
assert!(zone.create_memory_zone_handler().is_none());
|
||||
assert!(zone.create_forward_zone_handler().is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zone_roundtrip_preserves_records_and_forwarders() -> anyhow::Result<()> {
|
||||
let zone = Zone::try_from(&zone_data(
|
||||
"roundtrip.test",
|
||||
vec!["www 0 IN A 123.123.123.123", "app IN CNAME www"],
|
||||
vec!["udp://1.1.1.1:53", "tcp://8.8.8.8:53"],
|
||||
))?;
|
||||
|
||||
assert_eq!(zone.iter_records().count(), 2);
|
||||
assert_eq!(zone.forward.as_ref().unwrap().name_servers.len(), 2);
|
||||
|
||||
let serialized = ZoneData::from(zone.clone());
|
||||
let reparsed = Zone::try_from(&serialized)?;
|
||||
assert_eq!(reparsed.origin.to_string(), "roundtrip.test.");
|
||||
assert_eq!(reparsed.iter_records().count(), 2);
|
||||
assert_eq!(reparsed.forward.as_ref().unwrap().name_servers.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zone_group_into_groups_merges_same_origin() -> anyhow::Result<()> {
|
||||
let zones: ZoneGroup = vec![
|
||||
Zone::try_from(&zone_data("same.test", vec!["@ IN A 10.0.0.1"], vec![]))?,
|
||||
Zone::try_from(&zone_data("other.test", vec!["@ IN A 10.0.0.2"], vec![]))?,
|
||||
Zone::try_from(&zone_data("same.test", vec![], vec!["udp://1.1.1.1:53"]))?,
|
||||
]
|
||||
.into();
|
||||
|
||||
let groups = zones.into_groups();
|
||||
assert_eq!(groups.len(), 2);
|
||||
assert_eq!(
|
||||
groups
|
||||
.get(&LowerName::from_str("same.test.")?)
|
||||
.unwrap()
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
groups
|
||||
.get(&LowerName::from_str("other.test.")?)
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zone_group_iter_zone_handlers_returns_memory_and_forward() -> anyhow::Result<()> {
|
||||
let zones: ZoneGroup = vec![Zone::try_from(&zone_data(
|
||||
"zone-handler.test",
|
||||
vec!["@ IN A 10.0.0.10"],
|
||||
vec!["udp://1.1.1.1:53"],
|
||||
))?]
|
||||
.into();
|
||||
|
||||
let zone_handlers = zones.iter_zone_handlers().collect::<Vec<_>>();
|
||||
assert_eq!(zone_handlers.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zone_system_builds_root_forwarder() {
|
||||
let zone = Zone::system();
|
||||
assert_eq!(zone.origin.to_string(), ".");
|
||||
assert!(zone.forward.is_some());
|
||||
assert!(zone.create_forward_zone_handler().is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn catalog_lookup_returns_a_record_from_memory_zone_handler() -> anyhow::Result<()> {
|
||||
let zones: ZoneGroup = vec![Zone::try_from(&zone_data(
|
||||
"memory.test",
|
||||
vec!["@ IN A 10.20.30.40"],
|
||||
vec![],
|
||||
))?]
|
||||
.into();
|
||||
let catalog = build_catalog(zones);
|
||||
|
||||
let (rcode, message) = lookup_message(&catalog, "memory.test.", RecordType::A).await?;
|
||||
assert_eq!(rcode, ResponseCode::NoError);
|
||||
let message = message.expect("response should exist");
|
||||
assert!(has_a_answer(&message, Ipv4Addr::new(10, 20, 30, 40)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn catalog_lookup_returns_refused_when_zone_is_missing() -> anyhow::Result<()> {
|
||||
let zones: ZoneGroup = vec![Zone::try_from(&zone_data(
|
||||
"present.test",
|
||||
vec!["@ IN A 10.20.30.41"],
|
||||
vec![],
|
||||
))?]
|
||||
.into();
|
||||
let catalog = build_catalog(zones);
|
||||
|
||||
let (rcode, _message) = lookup_message(&catalog, "absent.test.", RecordType::A).await?;
|
||||
assert_eq!(rcode, ResponseCode::Refused);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn catalog_lookup_forwards_on_nameexists() -> anyhow::Result<()> {
|
||||
let upstream = Zone::try_from(&zone_data(
|
||||
"forward-aaaa.test",
|
||||
vec!["host 60 IN AAAA 2001:db8::1"],
|
||||
vec![],
|
||||
))?;
|
||||
|
||||
let mut upstream_catalog = Catalog::new();
|
||||
upstream_catalog.upsert(
|
||||
upstream.origin.clone(),
|
||||
vec![upstream.create_memory_zone_handler().unwrap()],
|
||||
);
|
||||
|
||||
let socket = UdpSocket::bind("127.0.0.1:0").await?;
|
||||
let upstream_addr = socket.local_addr()?;
|
||||
|
||||
let mut server = Server::new(upstream_catalog);
|
||||
server.register_socket(socket);
|
||||
let upstream_handle = tokio::spawn(async move {
|
||||
let _ = server.block_until_done().await;
|
||||
});
|
||||
|
||||
let zones: ZoneGroup = vec![Zone::try_from(&zone_data(
|
||||
"forward-aaaa.test",
|
||||
vec!["host IN A 10.20.30.40"],
|
||||
vec![&format!("udp://{}", upstream_addr)],
|
||||
))?]
|
||||
.into();
|
||||
let catalog = build_catalog(zones);
|
||||
|
||||
let (rcode, message) =
|
||||
lookup_message(&catalog, "host.forward-aaaa.test.", RecordType::AAAA).await?;
|
||||
assert_eq!(rcode, ResponseCode::NoError);
|
||||
|
||||
let message = message.expect("response should exist");
|
||||
assert!(has_aaaa_answer(&message, "2001:db8::1".parse()?));
|
||||
|
||||
upstream_handle.abort();
|
||||
let _ = upstream_handle.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn catalog_lookup_forwards_on_nxdomain() -> anyhow::Result<()> {
|
||||
let upstream = Zone::try_from(&zone_data(
|
||||
"forward-nxdomain.test",
|
||||
vec!["missing 60 IN A 203.0.113.55"],
|
||||
vec![],
|
||||
))?;
|
||||
|
||||
let mut upstream_catalog = Catalog::new();
|
||||
upstream_catalog.upsert(
|
||||
upstream.origin.clone(),
|
||||
vec![upstream.create_memory_zone_handler().unwrap()],
|
||||
);
|
||||
|
||||
let socket = UdpSocket::bind("127.0.0.1:0").await?;
|
||||
let upstream_addr = socket.local_addr()?;
|
||||
|
||||
let mut server = Server::new(upstream_catalog);
|
||||
server.register_socket(socket);
|
||||
let upstream_handle = tokio::spawn(async move {
|
||||
let _ = server.block_until_done().await;
|
||||
});
|
||||
|
||||
let zones: ZoneGroup = vec![Zone::try_from(&zone_data(
|
||||
"forward-nxdomain.test",
|
||||
vec!["present IN A 10.20.30.41"],
|
||||
vec![&format!("udp://{}", upstream_addr)],
|
||||
))?]
|
||||
.into();
|
||||
let catalog = build_catalog(zones);
|
||||
|
||||
let (rcode, message) =
|
||||
lookup_message(&catalog, "missing.forward-nxdomain.test.", RecordType::A).await?;
|
||||
assert_eq!(rcode, ResponseCode::NoError);
|
||||
|
||||
let message = message.expect("response should exist");
|
||||
assert!(has_a_answer(&message, Ipv4Addr::new(203, 0, 113, 55)));
|
||||
|
||||
upstream_handle.abort();
|
||||
let _ = upstream_handle.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn catalog_lookup_falls_back_to_later_zone_handler_with_same_origin() -> anyhow::Result<()>
|
||||
{
|
||||
let zones: ZoneGroup = vec![
|
||||
// First matching zone exists but does not contain the queried name.
|
||||
Zone::try_from(&zone_data(
|
||||
"fallback.test",
|
||||
vec!["first IN A 10.20.30.1"],
|
||||
vec![],
|
||||
))?,
|
||||
// Second matching zone should be queried as fallback and answer.
|
||||
Zone::try_from(&zone_data(
|
||||
"fallback.test",
|
||||
vec!["target IN A 10.20.30.2"],
|
||||
vec![],
|
||||
))?,
|
||||
]
|
||||
.into();
|
||||
let catalog = build_catalog(zones);
|
||||
|
||||
let (rcode, message) =
|
||||
lookup_message(&catalog, "target.fallback.test.", RecordType::A).await?;
|
||||
assert_eq!(rcode, ResponseCode::NoError);
|
||||
assert!(has_a_answer(
|
||||
&message.expect("response should exist"),
|
||||
Ipv4Addr::new(10, 20, 30, 2)
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn catalog_lookup_does_not_fall_back_when_fallthrough_disabled() -> anyhow::Result<()> {
|
||||
let zones: ZoneGroup = vec![
|
||||
Zone::try_from(&zone_data_with_fallthrough(
|
||||
"fallback-disabled.test",
|
||||
vec!["first IN A 10.20.31.1"],
|
||||
vec![],
|
||||
hashset! {},
|
||||
))?,
|
||||
Zone::try_from(&zone_data_with_fallthrough(
|
||||
"fallback-disabled.test",
|
||||
vec!["target IN A 10.20.31.2"],
|
||||
vec![],
|
||||
hashset! {},
|
||||
))?,
|
||||
]
|
||||
.into();
|
||||
let catalog = build_catalog(zones);
|
||||
|
||||
let (rcode, message) =
|
||||
lookup_message(&catalog, "target.fallback-disabled.test.", RecordType::A).await?;
|
||||
|
||||
assert_ne!(rcode, ResponseCode::NoError);
|
||||
if let Some(message) = message.as_ref() {
|
||||
assert!(!has_a_answer(message, Ipv4Addr::new(10, 20, 31, 2)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn catalog_forward_only_zone_queries_upstream() -> anyhow::Result<()> {
|
||||
let (upstream_addr, upstream_handle) = start_upstream_server().await?;
|
||||
|
||||
let forward_zone = Zone::try_from(&zone_data(
|
||||
"upstream.test",
|
||||
vec![],
|
||||
vec![&format!("udp://{}", upstream_addr)],
|
||||
))?;
|
||||
let catalog = build_catalog(vec![forward_zone].into());
|
||||
|
||||
let (rcode, message) =
|
||||
lookup_message(&catalog, "from-forward.upstream.test.", RecordType::A).await?;
|
||||
assert_eq!(rcode, ResponseCode::NoError);
|
||||
assert!(has_a_answer(
|
||||
&message.expect("response should exist"),
|
||||
Ipv4Addr::new(203, 0, 113, 9)
|
||||
));
|
||||
|
||||
upstream_handle.abort();
|
||||
let _ = upstream_handle.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -193,8 +193,11 @@ struct PeerArgs {
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum PeerSubCommand {
|
||||
/// List connected peers
|
||||
List,
|
||||
/// Show public IPv6 address information
|
||||
Ipv6,
|
||||
/// List foreign networks discovered by this instance
|
||||
ListForeign {
|
||||
#[arg(
|
||||
long,
|
||||
@@ -203,6 +206,7 @@ enum PeerSubCommand {
|
||||
)]
|
||||
trusted_keys: bool,
|
||||
},
|
||||
/// List global foreign networks from the peer center
|
||||
ListGlobalForeign,
|
||||
}
|
||||
|
||||
@@ -214,16 +218,18 @@ struct RouteArgs {
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum RouteSubCommand {
|
||||
/// List routes propagated by peers
|
||||
List,
|
||||
/// Dump routes in CIDR format
|
||||
Dump,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
struct ConnectorArgs {
|
||||
#[arg(short, long)]
|
||||
#[arg(short, long, help = "filter connectors by virtual IPv4 address")]
|
||||
ipv4: Option<String>,
|
||||
|
||||
#[arg(short, long)]
|
||||
#[arg(short, long, help = "filter connectors by peer URL")]
|
||||
peers: Vec<String>,
|
||||
|
||||
#[command(subcommand)]
|
||||
@@ -242,6 +248,7 @@ enum ConnectorSubCommand {
|
||||
#[arg(help = "connector url, e.g., tcp://1.2.3.4:11010")]
|
||||
url: String,
|
||||
},
|
||||
/// List connectors
|
||||
List,
|
||||
}
|
||||
|
||||
@@ -283,6 +290,7 @@ struct AclArgs {
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum AclSubCommand {
|
||||
/// Show ACL rule hit statistics
|
||||
Stats,
|
||||
}
|
||||
|
||||
@@ -450,19 +458,25 @@ struct InstallArgs {
|
||||
#[arg(long, default_value = env!("CARGO_PKG_DESCRIPTION"), help = "service description")]
|
||||
description: String,
|
||||
|
||||
#[arg(long)]
|
||||
#[arg(long, help = "display name shown by the service manager")]
|
||||
display_name: Option<String>,
|
||||
|
||||
#[arg(long)]
|
||||
#[arg(
|
||||
long,
|
||||
help = "whether to disable starting the service automatically on boot (true/false)"
|
||||
)]
|
||||
disable_autostart: Option<bool>,
|
||||
|
||||
#[arg(long)]
|
||||
#[arg(
|
||||
long,
|
||||
help = "whether to disable automatic restart when the service fails (true/false)"
|
||||
)]
|
||||
disable_restart_on_failure: Option<bool>,
|
||||
|
||||
#[arg(long, help = "path to easytier-core binary")]
|
||||
core_path: Option<PathBuf>,
|
||||
|
||||
#[arg(long)]
|
||||
#[arg(long, help = "working directory for the easytier-core service")]
|
||||
service_work_dir: Option<PathBuf>,
|
||||
|
||||
#[arg(
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::{Context, anyhow, bail};
|
||||
use bytes::Bytes;
|
||||
use dashmap::DashMap;
|
||||
use guarden::defer;
|
||||
@@ -15,12 +15,13 @@ use kcp_sys::{
|
||||
stream::KcpStream,
|
||||
};
|
||||
use prost::Message;
|
||||
use tokio::{select, task::JoinSet};
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use super::{
|
||||
CidrSet,
|
||||
tcp_proxy::{NatDstConnector, NatDstTcpConnector, TcpProxy},
|
||||
};
|
||||
use crate::utils::task::HedgeExt;
|
||||
use crate::{
|
||||
common::{
|
||||
acl_processor::PacketInfo,
|
||||
@@ -114,72 +115,57 @@ pub struct NatDstKcpConnector {
|
||||
impl NatDstConnector for NatDstKcpConnector {
|
||||
type DstStream = KcpStream;
|
||||
|
||||
async fn connect(&self, src: SocketAddr, nat_dst: SocketAddr) -> Result<Self::DstStream> {
|
||||
async fn connect(
|
||||
&self,
|
||||
src: SocketAddr,
|
||||
nat_dst: SocketAddr,
|
||||
) -> anyhow::Result<Self::DstStream> {
|
||||
let peer_mgr = self
|
||||
.peer_mgr
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("peer manager is not available"))?;
|
||||
|
||||
let dst_peer = {
|
||||
let SocketAddr::V4(addr) = nat_dst else {
|
||||
bail!("ipv6 is not supported");
|
||||
};
|
||||
peer_mgr
|
||||
.get_peer_map()
|
||||
.get_peer_id_by_ipv4(addr.ip())
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("no peer found for nat dst: {}", nat_dst))?
|
||||
};
|
||||
|
||||
tracing::trace!(?nat_dst, ?dst_peer, "kcp nat");
|
||||
|
||||
let conn_data = KcpConnData {
|
||||
src: Some(src.into()),
|
||||
dst: Some(nat_dst.into()),
|
||||
};
|
||||
|
||||
let Some(peer_mgr) = self.peer_mgr.upgrade() else {
|
||||
return Err(anyhow::anyhow!("peer manager is not available").into());
|
||||
};
|
||||
let stream = (0..5)
|
||||
.map(|_| {
|
||||
let kcp_endpoint = self.kcp_endpoint.clone();
|
||||
let my_peer_id = peer_mgr.my_peer_id();
|
||||
|
||||
let dst_peer_id = match nat_dst {
|
||||
SocketAddr::V4(addr) => peer_mgr.get_peer_map().get_peer_id_by_ipv4(addr.ip()).await,
|
||||
SocketAddr::V6(_) => return Err(anyhow::anyhow!("ipv6 is not supported").into()),
|
||||
};
|
||||
async move {
|
||||
let conn_id = kcp_endpoint
|
||||
.connect(
|
||||
Duration::from_secs(10),
|
||||
my_peer_id,
|
||||
dst_peer,
|
||||
Bytes::from(conn_data.encode_to_vec()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Some(dst_peer) = dst_peer_id else {
|
||||
return Err(anyhow::anyhow!("no peer found for nat dst: {}", nat_dst).into());
|
||||
};
|
||||
|
||||
tracing::trace!("kcp nat dst: {:?}, dst peers: {:?}", nat_dst, dst_peer);
|
||||
|
||||
let mut connect_tasks: JoinSet<std::result::Result<ConnId, anyhow::Error>> = JoinSet::new();
|
||||
let mut retry_remain = 5;
|
||||
loop {
|
||||
select! {
|
||||
Some(Ok(Ok(ret))) = connect_tasks.join_next() => {
|
||||
// just wait for the previous connection to finish
|
||||
let stream = KcpStream::new(&self.kcp_endpoint, ret)
|
||||
.ok_or(anyhow::anyhow!("failed to create kcp stream"))?;
|
||||
return Ok(stream);
|
||||
KcpStream::new(&kcp_endpoint, conn_id).context("failed to create kcp stream")
|
||||
}
|
||||
_ = tokio::time::sleep(Duration::from_millis(200)), if !connect_tasks.is_empty() && retry_remain > 0 => {
|
||||
// no successful connection yet, trigger another connection attempt
|
||||
}
|
||||
else => {
|
||||
// got error in connect_tasks, continue to retry
|
||||
if retry_remain == 0 && connect_tasks.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.hedge(Duration::from_millis(200))
|
||||
.await
|
||||
.context("failed to connect to peer")?;
|
||||
|
||||
// create a new connection task
|
||||
if retry_remain == 0 {
|
||||
continue;
|
||||
}
|
||||
retry_remain -= 1;
|
||||
|
||||
let kcp_endpoint = self.kcp_endpoint.clone();
|
||||
let my_peer_id = peer_mgr.my_peer_id();
|
||||
let conn_data_clone = conn_data;
|
||||
|
||||
connect_tasks.spawn(async move {
|
||||
kcp_endpoint
|
||||
.connect(
|
||||
Duration::from_secs(10),
|
||||
my_peer_id,
|
||||
dst_peer,
|
||||
Bytes::from(conn_data_clone.encode_to_vec()),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to connect to nat dst: {}", nat_dst))
|
||||
});
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("failed to connect to nat dst: {}", nat_dst).into())
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
fn check_packet_from_peer_fast(&self, _cidr_set: &CidrSet, _global_ctx: &GlobalCtx) -> bool {
|
||||
|
||||
@@ -18,17 +18,20 @@ use crate::tunnel::packet_def::{
|
||||
PacketType, PeerManagerHeader, TAIL_RESERVED_SIZE, ZCPacket, ZCPacketType,
|
||||
};
|
||||
use crate::tunnel::quic::{client_config, endpoint_config, server_config};
|
||||
use anyhow::{Context, Error, anyhow};
|
||||
use crate::utils::task::HedgeExt;
|
||||
use anyhow::{Context, Error, anyhow, bail, ensure};
|
||||
use atomic_refcell::AtomicRefCell;
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use dashmap::DashMap;
|
||||
use derivative::Derivative;
|
||||
use derive_more::{Constructor, Deref, DerefMut, From, Into};
|
||||
use guarden::defer;
|
||||
use moka::future::Cache;
|
||||
use prost::Message;
|
||||
use quinn::udp::{EcnCodepoint, RecvMeta, Transmit};
|
||||
use quinn::{
|
||||
AsyncUdpSocket, Endpoint, RecvStream, SendStream, StreamId, UdpPoller, default_runtime,
|
||||
AsyncUdpSocket, Connection, ConnectionError, Endpoint, RecvStream, SendStream, StreamId,
|
||||
UdpPoller, WriteError, default_runtime,
|
||||
};
|
||||
use std::cmp::min;
|
||||
use std::future::Future;
|
||||
@@ -43,8 +46,8 @@ use tokio::io::{AsyncReadExt, Join, join};
|
||||
use tokio::sync::mpsc::error::TrySendError;
|
||||
use tokio::sync::mpsc::{Receiver, Sender, channel};
|
||||
use tokio::task::JoinSet;
|
||||
use tokio::time::{Instant, timeout};
|
||||
use tokio::{join, pin, select};
|
||||
use tokio::time::timeout;
|
||||
use tokio::{join, select};
|
||||
use tokio_util::sync::PollSender;
|
||||
use tracing::{debug, error, info, instrument, trace, warn};
|
||||
|
||||
@@ -279,6 +282,7 @@ impl From<(SendStream, RecvStream)> for QuicStream {
|
||||
pub struct NatDstQuicConnector {
|
||||
pub(crate) endpoint: Endpoint,
|
||||
pub(crate) peer_mgr: Weak<PeerManager>,
|
||||
pub(crate) conn_map: Cache<PeerId, Connection>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -289,21 +293,25 @@ impl NatDstConnector for NatDstQuicConnector {
|
||||
&self,
|
||||
src: SocketAddr,
|
||||
nat_dst: SocketAddr,
|
||||
) -> crate::common::error::Result<Self::DstStream> {
|
||||
let Some(peer_mgr) = self.peer_mgr.upgrade() else {
|
||||
return Err(anyhow::anyhow!("peer manager is not available").into());
|
||||
) -> anyhow::Result<Self::DstStream> {
|
||||
let peer_mgr = self
|
||||
.peer_mgr
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("peer manager is not available"))?;
|
||||
|
||||
let dst_peer = {
|
||||
let SocketAddr::V4(addr) = nat_dst else {
|
||||
bail!("ipv6 is not supported");
|
||||
};
|
||||
peer_mgr
|
||||
.get_peer_map()
|
||||
.get_peer_id_by_ipv4(addr.ip())
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("no peer found for nat dst: {}", nat_dst))?
|
||||
};
|
||||
|
||||
let Some(dst_peer_id) = (match nat_dst {
|
||||
SocketAddr::V4(addr) => peer_mgr.get_peer_map().get_peer_id_by_ipv4(addr.ip()).await,
|
||||
SocketAddr::V6(_) => return Err(anyhow::anyhow!("ipv6 is not supported").into()),
|
||||
}) else {
|
||||
return Err(anyhow::anyhow!("no peer found for nat dst: {}", nat_dst).into());
|
||||
};
|
||||
tracing::trace!(?nat_dst, ?dst_peer, "quic nat");
|
||||
|
||||
trace!("quic nat dst: {:?}, dst peers: {:?}", nat_dst, dst_peer_id);
|
||||
|
||||
let addr = QuicAddr::new(dst_peer_id, PacketType::QuicSrc).into();
|
||||
let header = {
|
||||
let conn_data = QuicConnData {
|
||||
src: Some(src.into()),
|
||||
@@ -311,61 +319,90 @@ impl NatDstConnector for NatDstQuicConnector {
|
||||
};
|
||||
|
||||
let len = conn_data.encoded_len();
|
||||
if len > (u16::MAX as usize) {
|
||||
return Err(anyhow!("conn data too large: {:?}", len).into());
|
||||
}
|
||||
ensure!(len <= u16::MAX as usize, "conn data too large: {len}");
|
||||
|
||||
let mut buf = BytesMut::with_capacity(2 + len);
|
||||
|
||||
buf.put_u16(len as u16);
|
||||
conn_data.encode(&mut buf).unwrap();
|
||||
conn_data.encode(&mut buf)?;
|
||||
|
||||
buf.freeze()
|
||||
};
|
||||
|
||||
let mut connect_tasks = JoinSet::<Result<QuicStream, Error>>::new();
|
||||
let connect = |tasks: &mut JoinSet<_>| {
|
||||
let endpoint = self.endpoint.clone();
|
||||
let header = header.clone();
|
||||
let reconnect = || async move {
|
||||
self.conn_map.invalidate(&dst_peer).await;
|
||||
|
||||
tasks.spawn(async move {
|
||||
let connection = endpoint.connect(addr, "")?.await?;
|
||||
let mut stream: QuicStream = connection.open_bi().await?.into();
|
||||
stream.writer_mut().write_chunk(header).await?;
|
||||
Ok(stream)
|
||||
});
|
||||
let connect = (0..5)
|
||||
.map(|_| {
|
||||
let endpoint = self.endpoint.clone();
|
||||
async move {
|
||||
endpoint
|
||||
.connect(QuicAddr::new(dst_peer, PacketType::QuicSrc).into(), "")
|
||||
.context("failed to create connection")?
|
||||
.await
|
||||
.context("connection failed")
|
||||
}
|
||||
})
|
||||
.hedge(Duration::from_millis(200));
|
||||
|
||||
self.conn_map
|
||||
.try_get_with(dst_peer, connect)
|
||||
.await
|
||||
.context("failed to connect to peer")
|
||||
};
|
||||
|
||||
connect(&mut connect_tasks);
|
||||
let mut reconnected = false;
|
||||
|
||||
let timer = tokio::time::sleep(Duration::from_millis(200));
|
||||
pin!(timer);
|
||||
let mut connection = if let Some(connection) = self.conn_map.get(&dst_peer).await
|
||||
&& connection.close_reason().is_none()
|
||||
{
|
||||
connection
|
||||
} else {
|
||||
reconnected = true;
|
||||
reconnect().await?
|
||||
};
|
||||
|
||||
let mut retry_remain = 5;
|
||||
loop {
|
||||
select! {
|
||||
Some(result) = connect_tasks.join_next() => {
|
||||
match result {
|
||||
Ok(Ok(stream)) => return Ok(stream.into()),
|
||||
_ => {
|
||||
if connect_tasks.is_empty() {
|
||||
if retry_remain == 0 {
|
||||
return Err(anyhow!("failed to connect to nat dst: {:?}", nat_dst).into())
|
||||
}
|
||||
let is_retryable = |error: &ConnectionError| {
|
||||
matches!(
|
||||
error,
|
||||
ConnectionError::ConnectionClosed(_)
|
||||
| ConnectionError::ApplicationClosed(_)
|
||||
| ConnectionError::Reset
|
||||
| ConnectionError::TimedOut
|
||||
)
|
||||
};
|
||||
let mut retry = !reconnected;
|
||||
let header = header.clone();
|
||||
let result = async {
|
||||
let mut stream: QuicStream = connection
|
||||
.open_bi()
|
||||
.await
|
||||
.inspect_err(|error| retry &= is_retryable(error))?
|
||||
.into();
|
||||
stream
|
||||
.writer_mut()
|
||||
.write_chunk(header)
|
||||
.await
|
||||
.inspect_err(|error| {
|
||||
retry &= matches!(error, WriteError::ConnectionLost(error) if is_retryable(error))
|
||||
})?;
|
||||
Ok(stream.into())
|
||||
}
|
||||
.await;
|
||||
|
||||
retry_remain -= 1;
|
||||
connect(&mut connect_tasks);
|
||||
timer.as_mut().reset(Instant::now() + Duration::from_millis(200))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = &mut timer, if retry_remain > 0 => {
|
||||
retry_remain -= 1;
|
||||
connect(&mut connect_tasks);
|
||||
timer.as_mut().reset(Instant::now() + Duration::from_millis(200));
|
||||
if let Err(error) = &result {
|
||||
if retry {
|
||||
debug!(?error, "failed to open quic stream, retrying...");
|
||||
reconnected = true;
|
||||
connection = reconnect().await?;
|
||||
continue;
|
||||
} else {
|
||||
self.conn_map.invalidate(&dst_peer).await;
|
||||
}
|
||||
}
|
||||
|
||||
break result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,10 +632,17 @@ impl QuicStreamReceiver {
|
||||
}
|
||||
};
|
||||
|
||||
match Self::establish_stream(stream, ctx.clone()).await {
|
||||
Ok(stream) => drop(tasks.spawn(stream)),
|
||||
Err(e) => warn!("failed to establish quic stream from {:?}: {:?}", connection.remote_address(), e),
|
||||
}
|
||||
let ctx = ctx.clone();
|
||||
tasks.spawn(async move {
|
||||
match Self::establish_stream(stream, ctx).await {
|
||||
Ok(transfer_fut) => {
|
||||
if let Err(e) = transfer_fut.await {
|
||||
warn!("quic stream transfer error: {:?}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("failed to establish quic stream: {:?}", e),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res = tasks.join_next(), if !tasks.is_empty() => {
|
||||
@@ -816,7 +860,7 @@ impl QuicProxy {
|
||||
Arc::new(socket),
|
||||
default_runtime().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
.unwrap(); // TODO: maybe a different transport config
|
||||
endpoint.set_default_client_config(client_config());
|
||||
self.endpoint = Some(endpoint.clone());
|
||||
|
||||
@@ -845,6 +889,10 @@ impl QuicProxy {
|
||||
NatDstQuicConnector {
|
||||
endpoint: endpoint.clone(),
|
||||
peer_mgr: Arc::downgrade(&peer_mgr),
|
||||
conn_map: Cache::builder()
|
||||
.max_capacity(u8::MAX.into()) // cf. quinn transport config (max_concurrent_bidi_streams)
|
||||
.time_to_idle(Duration::from_secs(600)) // cf. quinn transport config (max_idle_timeout)
|
||||
.build(),
|
||||
},
|
||||
));
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ impl AsyncTcpConnector for Socks5KcpConnector {
|
||||
let ret = c
|
||||
.connect(self.src_addr, addr)
|
||||
.await
|
||||
.map_err(|e| super::fast_socks5::SocksError::Other(e.into()))?;
|
||||
.map_err(super::fast_socks5::SocksError::Other)?;
|
||||
Ok(SocksTcpStream::Kcp(ret))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ use super::tokio_smoltcp::{self, Net, NetConfig, channel_device};
|
||||
pub(crate) trait NatDstConnector: Send + Sync + Clone + 'static {
|
||||
type DstStream: AsyncRead + AsyncWrite + Unpin + Send;
|
||||
|
||||
async fn connect(&self, src: SocketAddr, dst: SocketAddr) -> Result<Self::DstStream>;
|
||||
async fn connect(&self, src: SocketAddr, dst: SocketAddr) -> anyhow::Result<Self::DstStream>;
|
||||
fn check_packet_from_peer_fast(&self, cidr_set: &CidrSet, global_ctx: &GlobalCtx) -> bool;
|
||||
fn check_packet_from_peer(
|
||||
&self,
|
||||
@@ -63,14 +63,13 @@ pub struct NatDstTcpConnector;
|
||||
#[async_trait::async_trait]
|
||||
impl NatDstConnector for NatDstTcpConnector {
|
||||
type DstStream = TcpStream;
|
||||
async fn connect(&self, _src: SocketAddr, nat_dst: SocketAddr) -> Result<Self::DstStream> {
|
||||
let socket = match TcpSocket::new_v4() {
|
||||
Ok(s) => s,
|
||||
Err(error) => {
|
||||
log::error!(?error, "create v4 socket failed");
|
||||
return Err(error.into());
|
||||
}
|
||||
};
|
||||
async fn connect(
|
||||
&self,
|
||||
_src: SocketAddr,
|
||||
nat_dst: SocketAddr,
|
||||
) -> anyhow::Result<Self::DstStream> {
|
||||
let socket = TcpSocket::new_v4()
|
||||
.inspect_err(|error| log::error!(?error, "create v4 socket failed"))?;
|
||||
|
||||
let stream = timeout(Duration::from_secs(10), socket.connect(nat_dst))
|
||||
.await?
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use crate::{
|
||||
peers::peer_manager::PeerManager,
|
||||
proto::{
|
||||
api::instance::Route,
|
||||
common::Void,
|
||||
magic_dns::{
|
||||
HandshakeRequest, MagicDnsServerRpc, MagicDnsServerRpcClientFactory,
|
||||
UpdateDnsRecordRequest,
|
||||
},
|
||||
rpc_impl::standalone::StandAloneClient,
|
||||
rpc_types::controller::BaseController,
|
||||
},
|
||||
tunnel::tcp::TcpTunnelConnector,
|
||||
};
|
||||
|
||||
use super::MAGIC_DNS_INSTANCE_ADDR;
|
||||
|
||||
pub struct MagicDnsClientInstance {
|
||||
rpc_client: StandAloneClient<TcpTunnelConnector>,
|
||||
rpc_stub: Option<Box<dyn MagicDnsServerRpc<Controller = BaseController> + Send>>,
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
tasks: JoinSet<()>,
|
||||
}
|
||||
|
||||
impl MagicDnsClientInstance {
|
||||
pub async fn new(peer_mgr: Arc<PeerManager>) -> Result<Self, anyhow::Error> {
|
||||
let tcp_connector = TcpTunnelConnector::new(MAGIC_DNS_INSTANCE_ADDR.parse().unwrap());
|
||||
let mut rpc_client = StandAloneClient::new(tcp_connector);
|
||||
let rpc_stub = rpc_client
|
||||
.scoped_client::<MagicDnsServerRpcClientFactory<BaseController>>("".to_string())
|
||||
.await?;
|
||||
Ok(MagicDnsClientInstance {
|
||||
rpc_client,
|
||||
rpc_stub: Some(rpc_stub),
|
||||
peer_mgr,
|
||||
tasks: JoinSet::new(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_dns_task(
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
rpc_stub: Box<dyn MagicDnsServerRpc<Controller = BaseController> + Send>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut prev_last_update = None;
|
||||
rpc_stub
|
||||
.handshake(BaseController::default(), HandshakeRequest::default())
|
||||
.await?;
|
||||
loop {
|
||||
rpc_stub
|
||||
.heartbeat(BaseController::default(), Void::default())
|
||||
.await?;
|
||||
|
||||
let last_update = peer_mgr.get_route_peer_info_last_update_time().await;
|
||||
if Some(last_update) == prev_last_update {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut routes = peer_mgr.list_routes().await;
|
||||
// add self as a route
|
||||
let ctx = peer_mgr.get_global_ctx();
|
||||
routes.push(Route {
|
||||
hostname: ctx.get_hostname(),
|
||||
ipv4_addr: ctx.get_ipv4().map(Into::into),
|
||||
..Default::default()
|
||||
});
|
||||
// Use configured tld_dns_zone (always set by default)
|
||||
let flags = ctx.config.get_flags();
|
||||
let req = UpdateDnsRecordRequest {
|
||||
routes,
|
||||
zone: flags.tld_dns_zone.clone(),
|
||||
};
|
||||
tracing::debug!(
|
||||
"MagicDnsClientInstance::update_dns_task: update dns records: {:?}",
|
||||
req
|
||||
);
|
||||
rpc_stub
|
||||
.update_dns_record(BaseController::default(), req)
|
||||
.await?;
|
||||
|
||||
let last_update_after_rpc = peer_mgr.get_route_peer_info_last_update_time().await;
|
||||
if last_update_after_rpc == last_update {
|
||||
prev_last_update = Some(last_update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_and_wait(&mut self) {
|
||||
let rpc_stub = self.rpc_stub.take().unwrap();
|
||||
let peer_mgr = self.peer_mgr.clone();
|
||||
self.tasks.spawn(async move {
|
||||
let ret = Self::update_dns_task(peer_mgr, rpc_stub).await;
|
||||
if let Err(e) = ret {
|
||||
tracing::error!("MagicDnsServerInstanceData::run_and_wait: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
tokio::select! {
|
||||
_ = self.tasks.join_next() => {
|
||||
tracing::warn!("MagicDnsServerInstanceData::run_and_wait: dns record update task exited");
|
||||
}
|
||||
_ = self.rpc_client.wait() => {
|
||||
tracing::warn!("MagicDnsServerInstanceData::run_and_wait: rpc client exited");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
use hickory_proto::rr;
|
||||
use hickory_proto::rr::RData;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, derive_builder::Builder)]
|
||||
pub struct RunConfig {
|
||||
general: GeneralConfig,
|
||||
|
||||
#[builder(default = HashMap::new())]
|
||||
zones: Zone,
|
||||
|
||||
#[builder(default = Vec::new())]
|
||||
#[serde(default)]
|
||||
excluded_forward_nameservers: Vec<IpAddr>,
|
||||
}
|
||||
|
||||
impl RunConfig {
|
||||
pub fn general(&self) -> &GeneralConfig {
|
||||
&self.general
|
||||
}
|
||||
|
||||
pub fn zones(&self) -> &Zone {
|
||||
&self.zones
|
||||
}
|
||||
|
||||
pub fn excluded_forward_nameservers(&self) -> &Vec<IpAddr> {
|
||||
&self.excluded_forward_nameservers
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, derive_builder::Builder)]
|
||||
pub struct GeneralConfig {
|
||||
#[builder(setter(into, strip_option), default = None)]
|
||||
listen_tcp: Option<String>,
|
||||
|
||||
#[builder(setter(into, strip_option), default = None)]
|
||||
listen_udp: Option<String>,
|
||||
}
|
||||
|
||||
impl GeneralConfig {
|
||||
pub fn listen_tcp(&self) -> &Option<String> {
|
||||
&self.listen_tcp
|
||||
}
|
||||
|
||||
pub fn listen_udp(&self) -> &Option<String> {
|
||||
&self.listen_udp
|
||||
}
|
||||
}
|
||||
|
||||
pub type Zone = HashMap<String, Vec<Record>>; // domain -> records
|
||||
|
||||
pub type RecordType = rr::RecordType;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, derive_builder::Builder)]
|
||||
pub struct Record {
|
||||
#[serde(rename = "type")]
|
||||
rr_type: RecordType,
|
||||
|
||||
name: String,
|
||||
value: String,
|
||||
|
||||
#[serde(with = "humantime_serde")]
|
||||
ttl: Duration,
|
||||
}
|
||||
|
||||
impl Record {
|
||||
pub fn name(&self) -> anyhow::Result<rr::Name> {
|
||||
let name = rr::Name::from_str(self.name.as_str())?;
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
fn rr_type(&self) -> rr::RecordType {
|
||||
self.rr_type
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Record> for rr::Record {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: Record) -> Result<Self, Self::Error> {
|
||||
let r: rr::Record = (&value).try_into()?;
|
||||
Ok(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Record> for rr::Record {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &Record) -> Result<Self, Self::Error> {
|
||||
let name = value.name()?;
|
||||
let mut record = Self::update0(name, value.ttl.as_secs() as u32, value.rr_type());
|
||||
record.set_dns_class(rr::DNSClass::IN);
|
||||
match value.rr_type {
|
||||
RecordType::A => {
|
||||
let addr: Ipv4Addr = value.value.parse()?;
|
||||
record.set_data(RData::A(rr::rdata::a::A(addr)));
|
||||
}
|
||||
RecordType::SOA => {
|
||||
let soa = value.value.split_whitespace().collect::<Vec<_>>();
|
||||
if soa.len() != 7 {
|
||||
return Err(anyhow::anyhow!("invalid SOA record"));
|
||||
}
|
||||
let mname = rr::Name::from_str(soa[0])?;
|
||||
let rname = rr::Name::from_str(soa[1])?;
|
||||
let serial: u32 = soa[2].parse()?;
|
||||
let refresh: u32 = soa[3].parse()?;
|
||||
let retry: u32 = soa[4].parse()?;
|
||||
let expire: u32 = soa[5].parse()?;
|
||||
let minimum: u32 = soa[6].parse()?;
|
||||
record.set_data(RData::SOA(rr::rdata::soa::SOA::new(
|
||||
mname,
|
||||
rname,
|
||||
serial,
|
||||
refresh.try_into().unwrap(),
|
||||
retry.try_into().unwrap(),
|
||||
expire.try_into().unwrap(),
|
||||
minimum,
|
||||
)));
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
Ok(record)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::anyhow;
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_works() -> anyhow::Result<()> {
|
||||
let text = r#"
|
||||
[general]
|
||||
listen_tcp = "127.0.0.1:5300"
|
||||
listen_udp = "127.0.0.1:5353"
|
||||
|
||||
[[zones."et.internal"]]
|
||||
type = "A"
|
||||
name = "www"
|
||||
value = "123.123.123.123"
|
||||
ttl = "60s"
|
||||
|
||||
[[zones."et.top"]]
|
||||
type = "A"
|
||||
name = "@"
|
||||
value = "100.100.100.100"
|
||||
ttl = "61s"
|
||||
|
||||
"#;
|
||||
|
||||
let config = toml::from_str::<RunConfig>(text)?;
|
||||
assert_eq!(
|
||||
config.general.listen_tcp().clone().unwrap(),
|
||||
"127.0.0.1:5300"
|
||||
);
|
||||
assert_eq!(
|
||||
config.general.listen_udp().clone().unwrap(),
|
||||
"127.0.0.1:5353"
|
||||
);
|
||||
assert_eq!(config.zones.len(), 2);
|
||||
|
||||
let (domain, records) = config
|
||||
.zones
|
||||
.get_key_value("et.internal")
|
||||
.ok_or(anyhow!("et.internal not found"))?;
|
||||
assert_eq!(domain, "et.internal");
|
||||
assert_eq!(records.len(), 1);
|
||||
let record = &records[0];
|
||||
assert_eq!(record.rr_type, RecordType::A);
|
||||
assert_eq!(record.name, "www");
|
||||
assert_eq!(record.value, "123.123.123.123");
|
||||
assert_eq!(record.ttl.as_secs(), 60);
|
||||
|
||||
let (domain, records) = config
|
||||
.zones
|
||||
.get_key_value("et.top")
|
||||
.ok_or(anyhow!("et.top not found"))?;
|
||||
assert_eq!(domain, "et.top");
|
||||
assert_eq!(records.len(), 1);
|
||||
let record = &records[0];
|
||||
assert_eq!(record.rr_type, RecordType::A);
|
||||
assert_eq!(record.name, "@");
|
||||
assert_eq!(record.value, "100.100.100.100");
|
||||
assert_eq!(record.ttl.as_secs(), 61);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// This module is copy and modified from https://github.com/fanyang89/libdns
|
||||
#[cfg(feature = "magic-dns")]
|
||||
pub(crate) mod config;
|
||||
#[cfg(feature = "magic-dns")]
|
||||
pub(crate) mod server;
|
||||
|
||||
#[cfg(feature = "magic-dns")]
|
||||
pub mod client_instance;
|
||||
#[cfg(feature = "magic-dns")]
|
||||
pub mod runner;
|
||||
#[cfg(feature = "magic-dns")]
|
||||
pub mod server_instance;
|
||||
#[cfg(feature = "magic-dns")]
|
||||
pub mod system_config;
|
||||
|
||||
#[cfg(all(test, feature = "tun", feature = "magic-dns"))]
|
||||
mod tests;
|
||||
|
||||
pub static MAGIC_DNS_INSTANCE_ADDR: &str = "tcp://127.0.0.1:49813";
|
||||
pub static MAGIC_DNS_FAKE_IP: &str = "100.100.100.101";
|
||||
pub static DEFAULT_ET_DNS_ZONE: &str = "et.net.";
|
||||
@@ -1,93 +0,0 @@
|
||||
use cidr::Ipv4Inet;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::peers::peer_manager::PeerManager;
|
||||
use std::{net::Ipv4Addr, sync::Arc, time::Duration};
|
||||
|
||||
use super::{client_instance::MagicDnsClientInstance, server_instance::MagicDnsServerInstance};
|
||||
|
||||
static DEFAULT_ET_DNS_ZONE: &str = "et.net.";
|
||||
|
||||
pub struct DnsRunner {
|
||||
client: Option<MagicDnsClientInstance>,
|
||||
server: Option<MagicDnsServerInstance>,
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
tun_dev: Option<String>,
|
||||
tun_inet: Ipv4Inet,
|
||||
fake_ip: Ipv4Addr,
|
||||
}
|
||||
|
||||
impl DnsRunner {
|
||||
pub fn new(
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
tun_dev: Option<String>,
|
||||
tun_inet: Ipv4Inet,
|
||||
fake_ip: Ipv4Addr,
|
||||
) -> Self {
|
||||
Self {
|
||||
client: None,
|
||||
server: None,
|
||||
peer_mgr,
|
||||
tun_dev,
|
||||
tun_inet,
|
||||
fake_ip,
|
||||
}
|
||||
}
|
||||
|
||||
async fn clean_env(&mut self) {
|
||||
if let Some(server) = self.server.take() {
|
||||
server.clean_env().await;
|
||||
}
|
||||
self.client.take();
|
||||
}
|
||||
|
||||
async fn run_once(&mut self) -> anyhow::Result<()> {
|
||||
// try server first
|
||||
match MagicDnsServerInstance::new(
|
||||
self.peer_mgr.clone(),
|
||||
self.tun_dev.clone(),
|
||||
self.tun_inet,
|
||||
self.fake_ip,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(server) => {
|
||||
self.server = Some(server);
|
||||
tracing::info!("DnsRunner::run_once: server started");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("DnsRunner::run_once: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// every runner must run a client
|
||||
let client = MagicDnsClientInstance::new(self.peer_mgr.clone()).await?;
|
||||
self.client = Some(client);
|
||||
self.client.as_mut().unwrap().run_and_wait().await;
|
||||
|
||||
Err(anyhow::anyhow!("Client instance exit"))
|
||||
}
|
||||
|
||||
pub async fn run(&mut self, canel_token: CancellationToken) {
|
||||
loop {
|
||||
tracing::info!("DnsRunner::run: start");
|
||||
tokio::select! {
|
||||
_ = canel_token.cancelled() => {
|
||||
self.clean_env().await;
|
||||
tracing::info!("DnsRunner::run: cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
ret = self.run_once() => {
|
||||
self.clean_env().await;
|
||||
if let Err(e) = ret {
|
||||
tracing::error!("DnsRunner::run: {:?}", e);
|
||||
} else {
|
||||
tracing::info!("DnsRunner::run: unexpected exit, server may be down");
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use hickory_proto::op::Edns;
|
||||
use hickory_proto::rr;
|
||||
use hickory_proto::rr::LowerName;
|
||||
use hickory_resolver::config::ResolverOpts;
|
||||
use hickory_resolver::name_server::TokioConnectionProvider;
|
||||
use hickory_resolver::system_conf::read_system_conf;
|
||||
use hickory_server::ServerFuture;
|
||||
use hickory_server::authority::{AuthorityObject, Catalog, ZoneType};
|
||||
use hickory_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo};
|
||||
use hickory_server::store::forwarder::ForwardConfig;
|
||||
use hickory_server::store::{forwarder::ForwardAuthority, in_memory::InMemoryAuthority};
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::net::{TcpListener, UdpSocket};
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use crate::common::dns::get_default_resolver_config;
|
||||
|
||||
use super::config::{GeneralConfig, Record, RunConfig};
|
||||
|
||||
pub struct Server {
|
||||
server: ServerFuture<CatalogRequestHandler>,
|
||||
catalog: Arc<RwLock<Catalog>>,
|
||||
general_config: GeneralConfig,
|
||||
udp_local_addr: Option<SocketAddr>,
|
||||
tcp_local_addr: Option<SocketAddr>,
|
||||
tasks: JoinSet<()>,
|
||||
}
|
||||
|
||||
struct CatalogRequestHandler {
|
||||
catalog: Arc<RwLock<Catalog>>,
|
||||
}
|
||||
|
||||
impl CatalogRequestHandler {
|
||||
fn new(catalog: Arc<RwLock<Catalog>>) -> CatalogRequestHandler {
|
||||
// let system_conf = read_system_conf();
|
||||
// let recursor = match system_conf {
|
||||
// Ok((conf, _)) => RecursorBuilder::default().build(conf),
|
||||
// Err(_) => RecursorBuilder::default().build(get_default_resolver_config()),
|
||||
// }
|
||||
// // policy is security unware, this will never return an error
|
||||
// .unwrap();
|
||||
|
||||
Self { catalog }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl RequestHandler for CatalogRequestHandler {
|
||||
async fn handle_request<R: ResponseHandler>(
|
||||
&self,
|
||||
request: &Request,
|
||||
response_handle: R,
|
||||
) -> ResponseInfo {
|
||||
self.catalog
|
||||
.read()
|
||||
.await
|
||||
.handle_request(request, response_handle)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_authority(domain: &str, records: &[Record]) -> Result<InMemoryAuthority> {
|
||||
let zone = rr::Name::from_str(domain)?;
|
||||
let mut authority = InMemoryAuthority::empty(zone, ZoneType::Primary, false);
|
||||
for record in records.iter() {
|
||||
let r = record.try_into()?;
|
||||
authority.upsert_mut(r, 0);
|
||||
}
|
||||
Ok(authority)
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn new(config: RunConfig) -> Self {
|
||||
Self::try_new(config).unwrap()
|
||||
}
|
||||
|
||||
fn try_new(config: RunConfig) -> Result<Self> {
|
||||
let mut catalog = Catalog::new();
|
||||
for (domain, records) in config.zones().iter() {
|
||||
let zone = rr::Name::from_str(domain.as_str())?;
|
||||
let authroty = build_authority(domain, records)?;
|
||||
catalog.upsert(zone.clone().into(), vec![Arc::new(authroty)]);
|
||||
}
|
||||
|
||||
// use forwarder authority for the root zone
|
||||
let system_conf =
|
||||
read_system_conf().unwrap_or((get_default_resolver_config(), ResolverOpts::default()));
|
||||
let forward_config = ForwardConfig {
|
||||
name_servers: system_conf
|
||||
.0
|
||||
.name_servers()
|
||||
.iter()
|
||||
.filter(|&x| {
|
||||
!config
|
||||
.excluded_forward_nameservers()
|
||||
.contains(&x.socket_addr.ip())
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
options: Some(system_conf.1),
|
||||
};
|
||||
let auth = ForwardAuthority::builder_with_config(
|
||||
forward_config,
|
||||
TokioConnectionProvider::default(),
|
||||
)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
catalog.upsert(rr::Name::from_str(".")?.into(), vec![Arc::new(auth)]);
|
||||
|
||||
let catalog = Arc::new(RwLock::new(catalog));
|
||||
let handler = CatalogRequestHandler::new(catalog.clone());
|
||||
let server = ServerFuture::new(handler);
|
||||
|
||||
Ok(Self {
|
||||
server,
|
||||
catalog,
|
||||
general_config: config.general().clone(),
|
||||
udp_local_addr: None,
|
||||
tcp_local_addr: None,
|
||||
tasks: JoinSet::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn udp_local_addr(&self) -> Option<SocketAddr> {
|
||||
self.udp_local_addr
|
||||
}
|
||||
|
||||
pub fn tcp_local_addr(&self) -> Option<SocketAddr> {
|
||||
self.tcp_local_addr
|
||||
}
|
||||
|
||||
pub async fn register_udp_socket(&mut self, address: String) -> Result<SocketAddr> {
|
||||
let bind_addr = SocketAddr::from_str(&address)
|
||||
.with_context(|| format!("DNS Server failed to parse address {}", address))?;
|
||||
let socket = socket2::Socket::new(
|
||||
socket2::Domain::IPV4,
|
||||
socket2::Type::DGRAM,
|
||||
Some(socket2::Protocol::UDP),
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"DNS Server failed to create UDP socket for address {}",
|
||||
address
|
||||
)
|
||||
})?;
|
||||
socket2::SockRef::from(&socket)
|
||||
.set_reuse_address(true)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"DNS Server failed to set reuse address on socket {}",
|
||||
address
|
||||
)
|
||||
})?;
|
||||
socket.bind(&bind_addr.into()).with_context(|| {
|
||||
format!("DNS Server failed to bind socket to address {}", bind_addr)
|
||||
})?;
|
||||
socket
|
||||
.set_nonblocking(true)
|
||||
.with_context(|| "DNS Server failed to set socket to non-blocking".to_string())?;
|
||||
let socket = UdpSocket::from_std(socket.into()).with_context(|| {
|
||||
format!(
|
||||
"DNS Server failed to convert socket to UdpSocket for address {}",
|
||||
address
|
||||
)
|
||||
})?;
|
||||
|
||||
let local_addr = socket
|
||||
.local_addr()
|
||||
.with_context(|| "DNS Server failed to get local address".to_string())?;
|
||||
self.server.register_socket(socket);
|
||||
|
||||
Ok(local_addr)
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<()> {
|
||||
if let Some(address) = self.general_config.listen_tcp() {
|
||||
let tcp_listener = TcpListener::bind(address.clone())
|
||||
.await
|
||||
.with_context(|| format!("DNS Server failed to bind TCP address {}", address))?;
|
||||
self.tcp_local_addr = Some(tcp_listener.local_addr()?);
|
||||
self.server
|
||||
.register_listener(tcp_listener, Duration::from_secs(5));
|
||||
}
|
||||
|
||||
if let Some(address) = self.general_config.listen_udp() {
|
||||
let local_addr = self.register_udp_socket(address.clone()).await?;
|
||||
self.udp_local_addr = Some(local_addr);
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shutdown(&mut self) -> Result<()> {
|
||||
self.server.shutdown_gracefully().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn upsert(&self, name: LowerName, authority: Arc<dyn AuthorityObject>) {
|
||||
self.catalog.write().await.upsert(name, vec![authority]);
|
||||
}
|
||||
|
||||
pub async fn remove(&self, name: &LowerName) -> Option<Vec<Arc<dyn AuthorityObject>>> {
|
||||
self.catalog.write().await.remove(name)
|
||||
}
|
||||
|
||||
pub async fn update<R: ResponseHandler>(
|
||||
&self,
|
||||
update: &Request,
|
||||
response_edns: Option<Edns>,
|
||||
response_handle: R,
|
||||
) -> io::Result<ResponseInfo> {
|
||||
self.catalog
|
||||
.write()
|
||||
.await
|
||||
.update(update, response_edns, response_handle)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn contains(&self, name: &LowerName) -> bool {
|
||||
self.catalog.read().await.contains(name)
|
||||
}
|
||||
|
||||
pub async fn lookup<R: ResponseHandler>(
|
||||
&self,
|
||||
request: &Request,
|
||||
response_edns: Option<Edns>,
|
||||
response_handle: R,
|
||||
) -> ResponseInfo {
|
||||
self.catalog
|
||||
.read()
|
||||
.await
|
||||
.lookup(request, response_edns, response_handle)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn read_catalog(&self) -> RwLockReadGuard<'_, Catalog> {
|
||||
self.catalog.read().await
|
||||
}
|
||||
|
||||
pub async fn write_catalog(&self) -> RwLockWriteGuard<'_, Catalog> {
|
||||
self.catalog.write().await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::instance::dns_server::config::{
|
||||
GeneralConfigBuilder, RecordBuilder, RecordType, RunConfigBuilder,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use hickory_client::client::{Client, ClientHandle};
|
||||
use hickory_proto::rr;
|
||||
use hickory_proto::runtime::TokioRuntimeProvider;
|
||||
use hickory_proto::udp::UdpClientStream;
|
||||
use maplit::hashmap;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_works() -> Result<()> {
|
||||
let mut server = Server::new(
|
||||
RunConfigBuilder::default()
|
||||
.general(GeneralConfigBuilder::default().build()?)
|
||||
.build()?,
|
||||
);
|
||||
server.run().await?;
|
||||
server.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn can_resolve_records() -> Result<()> {
|
||||
let configured_record = RecordBuilder::default()
|
||||
.rr_type(RecordType::A)
|
||||
.name("www.et.internal.".to_string())
|
||||
.value("123.123.123.123".to_string())
|
||||
.ttl(Duration::from_secs(60))
|
||||
.build()?;
|
||||
let configured_record2 = RecordBuilder::default()
|
||||
.rr_type(RecordType::A)
|
||||
.name("中文.et.internal.".to_string())
|
||||
.value("123.123.123.123".to_string())
|
||||
.ttl(Duration::from_secs(60))
|
||||
.build()?;
|
||||
let soa_record = RecordBuilder::default()
|
||||
.rr_type(RecordType::SOA)
|
||||
.name("et.internal.".to_string())
|
||||
.value(
|
||||
"ns.et.internal. hostmaster.et.internal. 2023101001 7200 3600 1209600 86400"
|
||||
.to_string(),
|
||||
)
|
||||
.ttl(Duration::from_secs(60))
|
||||
.build()?;
|
||||
let config = RunConfigBuilder::default()
|
||||
.general(
|
||||
GeneralConfigBuilder::default()
|
||||
.listen_udp("127.0.0.1:0")
|
||||
.build()?,
|
||||
)
|
||||
.zones(hashmap! {
|
||||
"et.internal.".to_string() => vec![configured_record.clone(), soa_record.clone(), configured_record2.clone()],
|
||||
})
|
||||
.build()?;
|
||||
|
||||
let mut server = Server::new(config);
|
||||
server.run().await?;
|
||||
|
||||
let local_addr = server.udp_local_addr().unwrap();
|
||||
let stream = UdpClientStream::builder(local_addr, TokioRuntimeProvider::default()).build();
|
||||
let (mut client, background) = Client::connect(stream).await?;
|
||||
let background_task = tokio::spawn(background);
|
||||
let response = client
|
||||
.query(
|
||||
rr::Name::from_str("www.et.internal")?,
|
||||
rr::DNSClass::IN,
|
||||
rr::RecordType::A,
|
||||
)
|
||||
.await?;
|
||||
drop(background_task);
|
||||
|
||||
println!("Response: {:?}", response);
|
||||
|
||||
assert_eq!(response.answers().len(), 1);
|
||||
let expected_record: rr::Record = configured_record.try_into()?;
|
||||
assert_eq!(response.answers().first().unwrap(), &expected_record);
|
||||
|
||||
server.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user