mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-20 11:22:39 +00:00
Compare commits
273
Commits
lttng
...
resolve-doh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09e30aeff6 | ||
|
|
e21046a5fa | ||
|
|
b207b8a7bb | ||
|
|
5e8a79a747 | ||
|
|
8485091614 | ||
|
|
e31fee1a6d | ||
|
|
743ec3c95e | ||
|
|
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 |
Generated
+232
-139
@@ -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"
|
||||
@@ -2262,28 +2231,29 @@ dependencies = [
|
||||
"defguard_wireguard_rs",
|
||||
"delegate",
|
||||
"derivative",
|
||||
"derive_builder",
|
||||
"derive_more 2.1.1",
|
||||
"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",
|
||||
@@ -2301,6 +2271,7 @@ dependencies = [
|
||||
"nix 0.29.0",
|
||||
"once_cell",
|
||||
"openssl",
|
||||
"optionize",
|
||||
"ordered_hash_map",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
@@ -2308,7 +2279,7 @@ dependencies = [
|
||||
"petgraph 0.8.1",
|
||||
"pin-project-lite",
|
||||
"pnet",
|
||||
"prefix-trie",
|
||||
"prefix-trie 0.7.0",
|
||||
"proc-macro2",
|
||||
"prost",
|
||||
"prost-build",
|
||||
@@ -2332,6 +2303,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"serial_test",
|
||||
"service-manager",
|
||||
"sha2",
|
||||
@@ -2385,7 +2357,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"easytier",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"log",
|
||||
"once_cell",
|
||||
"serde",
|
||||
@@ -2414,7 +2386,7 @@ dependencies = [
|
||||
"dashmap",
|
||||
"dunce",
|
||||
"easytier",
|
||||
"gethostname 1.1.0",
|
||||
"gethostname",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"security-framework-sys",
|
||||
@@ -2701,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"
|
||||
@@ -3300,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"
|
||||
@@ -3372,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"
|
||||
@@ -3735,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",
|
||||
@@ -3852,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"
|
||||
@@ -4037,7 +4019,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.3",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -4549,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"
|
||||
@@ -5203,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",
|
||||
@@ -5223,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]]
|
||||
@@ -5239,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",
|
||||
]
|
||||
|
||||
@@ -5350,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"
|
||||
@@ -5577,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",
|
||||
@@ -5928,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"
|
||||
@@ -6657,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"
|
||||
@@ -6697,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]]
|
||||
@@ -7054,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"
|
||||
@@ -7390,7 +7437,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"system-configuration 0.6.1",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
@@ -7565,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",
|
||||
@@ -7728,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",
|
||||
@@ -7774,7 +7822,7 @@ checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.0",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
@@ -8573,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"
|
||||
@@ -9132,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"
|
||||
@@ -9201,7 +9270,7 @@ dependencies = [
|
||||
"gdkwayland-sys",
|
||||
"gdkx11-sys",
|
||||
"gtk",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"ndk",
|
||||
@@ -9263,7 +9332,7 @@ dependencies = [
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"image 0.25.10",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"mime",
|
||||
@@ -9398,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",
|
||||
@@ -9491,7 +9560,7 @@ dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"objc2",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
@@ -9514,7 +9583,7 @@ checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
@@ -9965,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"
|
||||
@@ -10002,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"
|
||||
@@ -11849,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"
|
||||
@@ -12050,7 +12143,7 @@ dependencies = [
|
||||
"html5ever",
|
||||
"http",
|
||||
"javascriptcore-rs",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"kuchikiki",
|
||||
"libc",
|
||||
"ndk",
|
||||
@@ -12113,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",
|
||||
]
|
||||
@@ -12230,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",
|
||||
@@ -12467,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",
|
||||
|
||||
@@ -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,
|
||||
@@ -1326,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,
|
||||
@@ -1334,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(());
|
||||
};
|
||||
|
||||
+14
-11
@@ -51,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"
|
||||
@@ -59,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"] }
|
||||
|
||||
@@ -70,10 +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"
|
||||
moka = { version = "0.12", features = ["future"] }
|
||||
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"
|
||||
@@ -152,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",
|
||||
@@ -170,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 }
|
||||
@@ -236,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"
|
||||
@@ -338,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"
|
||||
|
||||
@@ -400,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
|
||||
|
||||
+21
-16
@@ -166,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",
|
||||
];
|
||||
|
||||
@@ -174,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)]")
|
||||
@@ -198,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(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();
|
||||
|
||||
@@ -220,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 不同或缺失时会被拒绝。"
|
||||
|
||||
+218
-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,9 +142,6 @@ 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,
|
||||
@@ -152,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);
|
||||
|
||||
@@ -240,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>);
|
||||
|
||||
@@ -427,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>,
|
||||
}
|
||||
|
||||
@@ -542,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>>,
|
||||
@@ -563,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>,
|
||||
@@ -596,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)),
|
||||
@@ -637,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
|
||||
@@ -652,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>) {
|
||||
@@ -999,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()
|
||||
}
|
||||
@@ -1061,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()
|
||||
}
|
||||
}
|
||||
@@ -1292,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();
|
||||
|
||||
@@ -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,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)
|
||||
@@ -255,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(
|
||||
@@ -287,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() {
|
||||
@@ -491,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) {
|
||||
@@ -786,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::{
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::utils::hostname;
|
||||
use anyhow::Context as _;
|
||||
#[cfg(unix)]
|
||||
use nix::{
|
||||
@@ -233,10 +234,7 @@ fn machine_uid_seed() -> Option<String> {
|
||||
fn linux_machine_id_seed(machine_uid: &str) -> String {
|
||||
let mut seed = format!("machine_uid={machine_uid}");
|
||||
|
||||
let hostname = gethostname::gethostname()
|
||||
.to_string_lossy()
|
||||
.trim()
|
||||
.to_string();
|
||||
let hostname = hostname();
|
||||
if !hostname.is_empty() {
|
||||
seed.push_str("\nhostname=");
|
||||
seed.push_str(&hostname);
|
||||
|
||||
@@ -11,7 +11,6 @@ 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;
|
||||
|
||||
+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 {
|
||||
|
||||
@@ -9,7 +9,7 @@ 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::{
|
||||
@@ -22,6 +22,8 @@ use crate::{
|
||||
utils::weak_upgrade,
|
||||
};
|
||||
|
||||
use super::create_connector_by_url;
|
||||
use crate::utils::dns::socket_addrs;
|
||||
use crate::{
|
||||
common::{
|
||||
error::Error,
|
||||
@@ -32,8 +34,6 @@ use crate::{
|
||||
use_global_var,
|
||||
};
|
||||
|
||||
use super::create_connector_by_url;
|
||||
|
||||
type ConnectorMap = Arc<DashSet<url::Url>>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -578,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",
|
||||
@@ -1130,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
|
||||
@@ -1154,10 +1140,6 @@ impl NetworkOptions {
|
||||
f.enable_udp_broadcast_relay = self
|
||||
.enable_udp_broadcast_relay
|
||||
.unwrap_or(f.enable_udp_broadcast_relay);
|
||||
// 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();
|
||||
}
|
||||
cfg.set_flags(f);
|
||||
|
||||
if !self.exit_nodes.is_empty() {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -1,610 +0,0 @@
|
||||
// single-instance server in one machine, every easytier instance that has ip address and tun device will try to create a server instance.
|
||||
|
||||
// magic dns client will connect to this server to update the dns records.
|
||||
// magic dns server will add the dns server ip address to the tun device, and forward the dns request to the dns server
|
||||
|
||||
// magic dns client will establish a long live tcp connection to the magic dns server, and when the server stops or crashes,
|
||||
// all the clients will exit and let the easytier instance to launch a new server instance.
|
||||
|
||||
use super::{
|
||||
MAGIC_DNS_INSTANCE_ADDR,
|
||||
config::{GeneralConfigBuilder, RunConfigBuilder},
|
||||
server::Server,
|
||||
system_config::{OSConfig, SystemConfig},
|
||||
};
|
||||
use crate::{
|
||||
common::{
|
||||
PeerId,
|
||||
ifcfg::{IfConfiger, IfConfiguerTrait},
|
||||
},
|
||||
instance::dns_server::{
|
||||
config::{Record, RecordBuilder, RecordType},
|
||||
server::build_authority,
|
||||
},
|
||||
peers::{NicPacketFilter, peer_manager::PeerManager},
|
||||
proto::{
|
||||
api::instance::Route,
|
||||
common::{TunnelInfo, Void},
|
||||
magic_dns::{
|
||||
DnsRecord, DnsRecordA, DnsRecordList, GetDnsRecordResponse, HandshakeRequest,
|
||||
HandshakeResponse, MagicDnsServerRpc, MagicDnsServerRpcServer, UpdateDnsRecordRequest,
|
||||
dns_record::{self},
|
||||
},
|
||||
rpc_impl::standalone::{RpcServerHook, StandAloneServer},
|
||||
rpc_types::controller::{BaseController, Controller},
|
||||
},
|
||||
tunnel::{packet_def::ZCPacket, tcp::TcpTunnelListener},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use cidr::Ipv4Inet;
|
||||
use dashmap::DashMap;
|
||||
use hickory_proto::rr::LowerName;
|
||||
use hickory_proto::serialize::binary::{BinDecodable, BinEncoder};
|
||||
use hickory_server::authority::{MessageRequest, MessageResponse};
|
||||
use hickory_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo};
|
||||
use multimap::MultiMap;
|
||||
use pnet::packet::icmp::{IcmpTypes, MutableIcmpPacket};
|
||||
use pnet::packet::ipv4::Ipv4Packet;
|
||||
use pnet::packet::udp::UdpPacket;
|
||||
use pnet::packet::{
|
||||
MutablePacket, Packet, icmp,
|
||||
ip::IpNextHeaderProtocols,
|
||||
ipv4::{self, MutableIpv4Packet},
|
||||
udp::{self, MutableUdpPacket},
|
||||
};
|
||||
use std::net::{SocketAddr, SocketAddrV4};
|
||||
use std::sync::Mutex;
|
||||
use std::{collections::BTreeMap, io, net::Ipv4Addr, str::FromStr, sync::Arc, time::Duration};
|
||||
|
||||
static NIC_PIPELINE_NAME: &str = "magic_dns_server";
|
||||
|
||||
pub(super) struct MagicDnsServerInstanceData {
|
||||
dns_server: Server,
|
||||
tun_dev: Option<String>,
|
||||
tun_ip: Ipv4Addr,
|
||||
fake_ip: Ipv4Addr,
|
||||
my_peer_id: PeerId,
|
||||
|
||||
// zone -> (tunnel remote addr -> route)
|
||||
route_infos: DashMap<String, MultiMap<url::Url, Route>>,
|
||||
|
||||
system_config: Option<Box<dyn SystemConfig>>,
|
||||
}
|
||||
|
||||
impl MagicDnsServerInstanceData {
|
||||
pub async fn update_dns_records<'a, T: Iterator<Item = &'a Route>>(
|
||||
&self,
|
||||
routes: T,
|
||||
zone: &str,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut records: Vec<Record> = vec![];
|
||||
for route in routes {
|
||||
if route.hostname.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(ipv4_addr) = route.ipv4_addr.unwrap_or_default().address else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let record = RecordBuilder::default()
|
||||
.rr_type(RecordType::A)
|
||||
.name(format!("{}.{}", route.hostname, zone))
|
||||
.value(ipv4_addr.to_string())
|
||||
.ttl(Duration::from_secs(1))
|
||||
.build()?;
|
||||
|
||||
// check record name valid for dns
|
||||
if let Err(e) = record.name() {
|
||||
tracing::error!("Invalid subdomain label: {}", e);
|
||||
continue;
|
||||
}
|
||||
|
||||
records.push(record);
|
||||
}
|
||||
|
||||
let soa_record = RecordBuilder::default()
|
||||
.rr_type(RecordType::SOA)
|
||||
.name(zone.to_string())
|
||||
.value(format!(
|
||||
"ns.{} hostmaster.{} 2023101001 7200 3600 1209600 86400",
|
||||
zone, zone
|
||||
))
|
||||
.ttl(Duration::from_secs(60))
|
||||
.build()?;
|
||||
records.push(soa_record);
|
||||
|
||||
let authority = build_authority(zone, &records)?;
|
||||
|
||||
self.dns_server
|
||||
.upsert(
|
||||
LowerName::from_str(zone)
|
||||
.with_context(|| "Invalid zone name, expect format like \"et.net.\"")?,
|
||||
Arc::new(authority),
|
||||
)
|
||||
.await;
|
||||
|
||||
tracing::debug!("Updated DNS records for zone {}: {:?}", zone, records);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update(&self) {
|
||||
for item in self.route_infos.iter() {
|
||||
let zone = item.key();
|
||||
let route_iter = item.value().flat_iter().map(|x| x.1);
|
||||
if let Err(e) = self.update_dns_records(route_iter, zone).await {
|
||||
tracing::error!("Failed to update DNS records for zone {}: {:?}", zone, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn keep_zone_authoritative(&self, zone: &str) {
|
||||
if let Err(e) = self
|
||||
.update_dns_records(std::iter::empty::<&Route>(), zone)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to keep DNS zone {} authoritative after route prune: {:?}",
|
||||
zone,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn do_system_config(&self, zone: &str) -> Result<(), anyhow::Error> {
|
||||
if let Some(c) = &self.system_config {
|
||||
c.set_dns(&OSConfig {
|
||||
nameservers: vec![self.fake_ip.to_string()],
|
||||
search_domains: vec![zone.to_string()],
|
||||
match_domains: vec![zone.to_string()],
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MagicDnsServerRpc for MagicDnsServerInstanceData {
|
||||
type Controller = BaseController;
|
||||
async fn handshake(
|
||||
&self,
|
||||
_ctrl: Self::Controller,
|
||||
_input: HandshakeRequest,
|
||||
) -> crate::proto::rpc_types::error::Result<HandshakeResponse> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
async fn heartbeat(
|
||||
&self,
|
||||
_ctrl: Self::Controller,
|
||||
_input: Void,
|
||||
) -> crate::proto::rpc_types::error::Result<Void> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
async fn update_dns_record(
|
||||
&self,
|
||||
ctrl: Self::Controller,
|
||||
input: UpdateDnsRecordRequest,
|
||||
) -> crate::proto::rpc_types::error::Result<Void> {
|
||||
let Some(tunnel_info) = ctrl.get_tunnel_info() else {
|
||||
return Err(anyhow::anyhow!("No tunnel info").into());
|
||||
};
|
||||
let Some(remote_addr) = &tunnel_info.remote_addr else {
|
||||
return Err(anyhow::anyhow!("No remote addr").into());
|
||||
};
|
||||
let zone = input.zone.clone();
|
||||
let remote_addr: url::Url = remote_addr.clone().into();
|
||||
let mut zone_removed = false;
|
||||
|
||||
if let Some(mut routes_by_addr) = self.route_infos.get_mut(&zone) {
|
||||
routes_by_addr.remove(&remote_addr);
|
||||
if !input.routes.is_empty() {
|
||||
routes_by_addr.insert_many(remote_addr, input.routes);
|
||||
}
|
||||
zone_removed = routes_by_addr.is_empty();
|
||||
} else if !input.routes.is_empty() {
|
||||
let mut routes_by_addr = MultiMap::new();
|
||||
routes_by_addr.insert_many(remote_addr, input.routes);
|
||||
self.route_infos.insert(zone.clone(), routes_by_addr);
|
||||
}
|
||||
|
||||
if zone_removed {
|
||||
self.route_infos.remove(&zone);
|
||||
self.keep_zone_authoritative(&zone).await;
|
||||
}
|
||||
|
||||
self.update().await;
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
async fn get_dns_record(
|
||||
&self,
|
||||
_ctrl: Self::Controller,
|
||||
_input: Void,
|
||||
) -> crate::proto::rpc_types::error::Result<GetDnsRecordResponse> {
|
||||
let mut ret = BTreeMap::new();
|
||||
for item in self.route_infos.iter() {
|
||||
let zone = item.key();
|
||||
let routes = item.value();
|
||||
let mut dns_records = DnsRecordList::default();
|
||||
for route in routes.iter().map(|x| x.1) {
|
||||
dns_records.records.push(DnsRecord {
|
||||
record: Some(dns_record::Record::A(DnsRecordA {
|
||||
name: format!("{}.{}", route.hostname, zone),
|
||||
value: route.ipv4_addr.unwrap_or_default().address,
|
||||
ttl: 1,
|
||||
})),
|
||||
});
|
||||
}
|
||||
ret.insert(zone.clone(), dns_records);
|
||||
}
|
||||
Ok(GetDnsRecordResponse { records: ret })
|
||||
}
|
||||
}
|
||||
|
||||
// This should only be used for UDP response.
|
||||
// For other protocols, the variable `max_size` in `send_response` should be u16::MAX.
|
||||
#[derive(Clone)]
|
||||
struct ResponseWrapper {
|
||||
response: Arc<Mutex<Vec<u8>>>,
|
||||
}
|
||||
|
||||
trait RecordIter<'a>: Iterator<Item = &'a hickory_proto::rr::Record> + Send + 'a {}
|
||||
impl<'a, T> RecordIter<'a> for T where T: Iterator<Item = &'a hickory_proto::rr::Record> + Send + 'a {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ResponseHandler for ResponseWrapper {
|
||||
async fn send_response<'a>(
|
||||
&mut self,
|
||||
response: MessageResponse<
|
||||
'_,
|
||||
'a,
|
||||
impl RecordIter<'a>,
|
||||
impl RecordIter<'a>,
|
||||
impl RecordIter<'a>,
|
||||
impl RecordIter<'a>,
|
||||
>,
|
||||
) -> io::Result<ResponseInfo> {
|
||||
let mut buffer = self
|
||||
.response
|
||||
.lock()
|
||||
.map_err(|_| io::Error::other("lock poisoned"))?;
|
||||
|
||||
let mut encoder = BinEncoder::new(&mut buffer);
|
||||
|
||||
// `max_size` should be u16::MAX for protocol other than UDP.
|
||||
let max_size = if let Some(edns) = response.get_edns() {
|
||||
edns.max_payload()
|
||||
} else {
|
||||
hickory_proto::udp::MAX_RECEIVE_BUFFER_SIZE as u16
|
||||
};
|
||||
|
||||
encoder.set_max_size(max_size);
|
||||
response
|
||||
.destructive_emit(&mut encoder)
|
||||
.map_err(io::Error::other)
|
||||
}
|
||||
}
|
||||
|
||||
impl MagicDnsServerInstanceData {
|
||||
/// Replace content of incoming UDP DNS request and ICMP echo request packet with reply data,
|
||||
/// and swap source and destination IP addresses to send it back.
|
||||
async fn handle_ip_packet(&self, zc_packet: &mut ZCPacket) -> Option<()> {
|
||||
let (ip_header_length, ip_protocol, src_ip, dst_ip) = {
|
||||
let ip_packet = Ipv4Packet::new(zc_packet.payload())?;
|
||||
|
||||
if ip_packet.get_version() != 4 {
|
||||
return None;
|
||||
}
|
||||
|
||||
(
|
||||
ip_packet.get_header_length() as usize * 4,
|
||||
ip_packet.get_next_level_protocol(),
|
||||
ip_packet.get_source(),
|
||||
ip_packet.get_destination(),
|
||||
)
|
||||
};
|
||||
|
||||
if dst_ip != self.fake_ip {
|
||||
return None;
|
||||
}
|
||||
|
||||
match ip_protocol {
|
||||
IpNextHeaderProtocols::Udp => {
|
||||
self.handle_udp_packet(zc_packet, ip_header_length, src_ip, dst_ip)
|
||||
.await?;
|
||||
}
|
||||
IpNextHeaderProtocols::Icmp => {
|
||||
self.handle_icmp_packet(zc_packet, ip_header_length)?;
|
||||
}
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let mut ip_packet = MutableIpv4Packet::new(zc_packet.mut_payload())?;
|
||||
ip_packet.set_source(dst_ip);
|
||||
ip_packet.set_destination(src_ip);
|
||||
|
||||
ip_packet.set_checksum(ipv4::checksum(&ip_packet.to_immutable()));
|
||||
|
||||
zc_packet.mut_peer_manager_header().unwrap().to_peer_id = self.my_peer_id.into();
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Extract the DNS request message and send it to the hickory-dns server instance.
|
||||
/// Replace the content of the UDP packet with the response message.
|
||||
async fn handle_udp_packet(
|
||||
&self,
|
||||
zc_packet: &mut ZCPacket,
|
||||
ip_header_length: usize,
|
||||
src_ip: Ipv4Addr,
|
||||
dst_ip: Ipv4Addr,
|
||||
) -> Option<()> {
|
||||
let (src_port, dst_port, request, request_length) = {
|
||||
let udp_packet = UdpPacket::new(&zc_packet.payload()[ip_header_length..])?;
|
||||
|
||||
let src_port = udp_packet.get_source();
|
||||
let dst_port = udp_packet.get_destination();
|
||||
|
||||
// Remove this to support any UDP port
|
||||
if dst_port != 53 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let request_payload = udp_packet.payload();
|
||||
|
||||
(
|
||||
src_port,
|
||||
dst_port,
|
||||
Request::new(
|
||||
MessageRequest::from_bytes(request_payload).ok()?,
|
||||
SocketAddr::from(SocketAddrV4::new(src_ip, src_port)),
|
||||
hickory_proto::xfer::Protocol::Udp,
|
||||
),
|
||||
request_payload.len(),
|
||||
)
|
||||
};
|
||||
|
||||
let response_payload = {
|
||||
let response_payload_arc = Arc::new(Mutex::new(Vec::with_capacity(512)));
|
||||
|
||||
self.dns_server
|
||||
.read_catalog()
|
||||
.await
|
||||
.handle_request(
|
||||
&request,
|
||||
ResponseWrapper {
|
||||
response: response_payload_arc.clone(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
Arc::into_inner(response_payload_arc)?.into_inner().ok()?
|
||||
};
|
||||
|
||||
let response_length = response_payload.len();
|
||||
let delta_length = response_length as isize - request_length as isize;
|
||||
|
||||
let inner_length = (zc_packet.buf_len() as isize + delta_length) as usize;
|
||||
if zc_packet.mut_inner().capacity() < inner_length {
|
||||
let header_length = inner_length - response_length;
|
||||
zc_packet.mut_inner().truncate(header_length);
|
||||
}
|
||||
zc_packet.mut_inner().resize(inner_length, 0);
|
||||
|
||||
let mut ip_packet = MutableIpv4Packet::new(zc_packet.mut_payload())?;
|
||||
|
||||
let ip_length = (ip_packet.get_total_length() as isize + delta_length) as u16;
|
||||
ip_packet.set_total_length(ip_length);
|
||||
|
||||
let mut udp_packet = MutableUdpPacket::new(ip_packet.payload_mut())?;
|
||||
|
||||
let udp_length = (udp_packet.get_length() as isize + delta_length) as u16;
|
||||
udp_packet.set_length(udp_length);
|
||||
|
||||
udp_packet.set_source(dst_port);
|
||||
udp_packet.set_destination(src_port);
|
||||
|
||||
udp_packet.payload_mut().copy_from_slice(&response_payload);
|
||||
|
||||
udp_packet.set_checksum(udp::ipv4_checksum(
|
||||
&udp_packet.to_immutable(),
|
||||
&dst_ip,
|
||||
&src_ip,
|
||||
));
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn handle_icmp_packet(&self, zc_packet: &mut ZCPacket, ip_header_length: usize) -> Option<()> {
|
||||
let mut icmp_packet =
|
||||
MutableIcmpPacket::new(&mut zc_packet.mut_payload()[ip_header_length..])?;
|
||||
|
||||
if icmp_packet.get_icmp_type() != IcmpTypes::EchoRequest {
|
||||
return None;
|
||||
}
|
||||
|
||||
icmp_packet.set_icmp_type(IcmpTypes::EchoReply);
|
||||
icmp_packet.set_checksum(icmp::checksum(&icmp_packet.to_immutable()));
|
||||
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NicPacketFilter for MagicDnsServerInstanceData {
|
||||
async fn try_process_packet_from_nic(&self, zc_packet: &mut ZCPacket) -> bool {
|
||||
self.handle_ip_packet(zc_packet).await.is_some()
|
||||
}
|
||||
|
||||
fn id(&self) -> String {
|
||||
NIC_PIPELINE_NAME.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl RpcServerHook for MagicDnsServerInstanceData {
|
||||
async fn on_new_client(
|
||||
&self,
|
||||
tunnel_info: Option<TunnelInfo>,
|
||||
) -> Result<Option<TunnelInfo>, anyhow::Error> {
|
||||
tracing::info!(?tunnel_info, "New client connected");
|
||||
Ok(tunnel_info)
|
||||
}
|
||||
|
||||
async fn on_client_disconnected(&self, tunnel_info: Option<TunnelInfo>) {
|
||||
tracing::info!(?tunnel_info, "Client disconnected");
|
||||
let Some(tunnel_info) = tunnel_info else {
|
||||
return;
|
||||
};
|
||||
let Some(remote_addr) = tunnel_info.remote_addr else {
|
||||
return;
|
||||
};
|
||||
let remote_addr = remote_addr.into();
|
||||
let mut removed_zones = vec![];
|
||||
for mut item in self.route_infos.iter_mut() {
|
||||
item.value_mut().remove(&remote_addr);
|
||||
if item.value().is_empty() {
|
||||
removed_zones.push(item.key().clone());
|
||||
}
|
||||
}
|
||||
for zone in &removed_zones {
|
||||
self.route_infos.remove(zone);
|
||||
}
|
||||
for zone in removed_zones {
|
||||
self.keep_zone_authoritative(&zone).await;
|
||||
}
|
||||
self.update().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MagicDnsServerInstance {
|
||||
rpc_server: StandAloneServer<TcpTunnelListener>,
|
||||
pub(super) data: Arc<MagicDnsServerInstanceData>,
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
tun_inet: Ipv4Inet,
|
||||
}
|
||||
|
||||
fn get_system_config(
|
||||
_tun_name: Option<&str>,
|
||||
) -> Result<Option<Box<dyn SystemConfig>>, anyhow::Error> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use super::system_config::windows::WindowsDNSManager;
|
||||
let tun_name = _tun_name.ok_or_else(|| anyhow::anyhow!("No tun name"))?;
|
||||
return Ok(Some(Box::new(WindowsDNSManager::new(tun_name)?)));
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "macos", not(feature = "macos-ne")))]
|
||||
{
|
||||
use super::system_config::darwin::DarwinConfigurator;
|
||||
return Ok(Some(Box::new(DarwinConfigurator::new())));
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
impl MagicDnsServerInstance {
|
||||
pub async fn new(
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
tun_dev: Option<String>,
|
||||
tun_inet: Ipv4Inet,
|
||||
fake_ip: Ipv4Addr,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let tcp_listener = TcpTunnelListener::new(MAGIC_DNS_INSTANCE_ADDR.parse()?);
|
||||
let mut rpc_server = StandAloneServer::new(tcp_listener);
|
||||
rpc_server.serve().await?;
|
||||
|
||||
let dns_config = RunConfigBuilder::default()
|
||||
.general(GeneralConfigBuilder::default().build()?)
|
||||
.excluded_forward_nameservers(vec![fake_ip.into()])
|
||||
.build()?;
|
||||
let mut dns_server = Server::new(dns_config);
|
||||
dns_server.run().await?;
|
||||
|
||||
if !tun_inet.contains(&fake_ip)
|
||||
&& let Some(tun_dev_name) = &tun_dev
|
||||
{
|
||||
let cost = if cfg!(target_os = "windows") {
|
||||
Some(4)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let ifcfg = IfConfiger {};
|
||||
ifcfg
|
||||
.add_ipv4_route(tun_dev_name, fake_ip, 32, cost)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let data = Arc::new(MagicDnsServerInstanceData {
|
||||
dns_server,
|
||||
tun_dev: tun_dev.clone(),
|
||||
tun_ip: tun_inet.address(),
|
||||
fake_ip,
|
||||
my_peer_id: peer_mgr.my_peer_id(),
|
||||
route_infos: DashMap::new(),
|
||||
system_config: get_system_config(tun_dev.as_deref())?,
|
||||
});
|
||||
|
||||
rpc_server
|
||||
.registry()
|
||||
.register(MagicDnsServerRpcServer::new_arc(data.clone()), "");
|
||||
rpc_server.set_hook(data.clone());
|
||||
|
||||
peer_mgr
|
||||
.add_nic_packet_process_pipeline(Box::new(data.clone()))
|
||||
.await;
|
||||
// Use configured tld_dns_zone or fall back to DEFAULT_ET_DNS_ZONE if empty
|
||||
let flags = peer_mgr.get_global_ctx().config.get_flags();
|
||||
let tld_dns_zone_clone = flags.tld_dns_zone.clone();
|
||||
|
||||
data.update_dns_records(std::iter::empty(), &tld_dns_zone_clone)
|
||||
.await
|
||||
.context("Failed to initialize DNS zone")?;
|
||||
|
||||
let data_clone = data.clone();
|
||||
tokio::task::spawn_blocking(move || data_clone.do_system_config(&tld_dns_zone_clone))
|
||||
.await
|
||||
.context("Failed to configure system")??;
|
||||
|
||||
Ok(Self {
|
||||
rpc_server,
|
||||
data,
|
||||
peer_mgr,
|
||||
tun_inet,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn clean_env(&self) {
|
||||
if let Some(configer) = &self.data.system_config {
|
||||
let ret = configer.close();
|
||||
if let Err(e) = ret {
|
||||
tracing::error!("Failed to close system config: {:?}", e);
|
||||
}
|
||||
if !self.tun_inet.contains(&self.data.fake_ip)
|
||||
&& let Some(tun_dev_name) = &self.data.tun_dev
|
||||
{
|
||||
let ifcfg = IfConfiger {};
|
||||
let _ = ifcfg
|
||||
.remove_ipv4_route(tun_dev_name, self.data.fake_ip, 32)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self
|
||||
.peer_mgr
|
||||
.remove_nic_packet_process_pipeline(NIC_PIPELINE_NAME.to_string())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MagicDnsServerInstance {
|
||||
fn drop(&mut self) {
|
||||
println!("MagicDnsServerInstance dropped");
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
#[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 darwin;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct OSConfig {
|
||||
pub nameservers: Vec<String>,
|
||||
pub search_domains: Vec<String>,
|
||||
pub match_domains: Vec<String>,
|
||||
}
|
||||
|
||||
pub trait SystemConfig: Send + Sync {
|
||||
fn set_dns(&self, cfg: &OSConfig) -> std::io::Result<()>;
|
||||
fn close(&self) -> std::io::Result<()>;
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::str::FromStr as _;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use cidr::Ipv4Inet;
|
||||
use hickory_client::client::{Client, ClientHandle as _};
|
||||
use hickory_proto::rr;
|
||||
use hickory_proto::runtime::TokioRuntimeProvider;
|
||||
use hickory_proto::udp::UdpClientStream;
|
||||
use tokio::sync::Notify;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::common::global_ctx::tests::get_mock_global_ctx;
|
||||
use crate::connector::udp_hole_punch::tests::replace_stun_info_collector;
|
||||
|
||||
use crate::instance::dns_server::runner::DnsRunner;
|
||||
use crate::instance::dns_server::server_instance::MagicDnsServerInstance;
|
||||
use crate::instance::dns_server::{DEFAULT_ET_DNS_ZONE, MAGIC_DNS_FAKE_IP};
|
||||
use crate::instance::virtual_nic::NicCtx;
|
||||
use crate::peers::peer_manager::{PeerManager, RouteAlgoType};
|
||||
|
||||
use crate::peers::create_packet_recv_chan;
|
||||
use crate::proto::api::instance::Route;
|
||||
use crate::proto::common::NatType;
|
||||
use crate::proto::magic_dns::{MagicDnsServerRpc as _, UpdateDnsRecordRequest};
|
||||
use crate::proto::rpc_types::controller::{BaseController, Controller as _};
|
||||
|
||||
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));
|
||||
|
||||
if tld_dns_zone.is_some() {
|
||||
let mut flags = ctx.config.get_flags();
|
||||
flags.accept_dns = true; // Enable DNS
|
||||
if let Some(zone) = tld_dns_zone {
|
||||
flags.tld_dns_zone = zone.to_string();
|
||||
}
|
||||
ctx.set_flags(flags);
|
||||
}
|
||||
|
||||
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 async fn check_dns_record(fake_ip: &Ipv4Addr, domain: &str, expected_ip: &str) {
|
||||
let stream = UdpClientStream::builder(
|
||||
SocketAddr::new((*fake_ip).into(), 53),
|
||||
TokioRuntimeProvider::default(),
|
||||
)
|
||||
.build();
|
||||
let (mut client, background) = Client::connect(stream).await.unwrap();
|
||||
let background_task = tokio::spawn(background);
|
||||
let response = client
|
||||
.query(
|
||||
rr::Name::from_str(domain).unwrap(),
|
||||
rr::DNSClass::IN,
|
||||
rr::RecordType::A,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("DNS query failed unexpectedly for domain '{domain}': {e}"));
|
||||
background_task.abort();
|
||||
let _ = background_task.await;
|
||||
|
||||
println!("Response: {:?}", response);
|
||||
|
||||
assert_eq!(response.answers().len(), 1, "{:?}", response.answers());
|
||||
let resp = response.answers().first().unwrap();
|
||||
assert_eq!(
|
||||
resp.clone().into_parts().rdata.into_a().unwrap().0,
|
||||
expected_ip.parse::<Ipv4Addr>().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn check_dns_record_missing(fake_ip: &Ipv4Addr, domain: &str) {
|
||||
let stream = UdpClientStream::builder(
|
||||
SocketAddr::new((*fake_ip).into(), 53),
|
||||
TokioRuntimeProvider::default(),
|
||||
)
|
||||
.build();
|
||||
let (mut client, background) = Client::connect(stream).await.unwrap();
|
||||
let background_task = tokio::spawn(background);
|
||||
let response = client
|
||||
.query(
|
||||
rr::Name::from_str(domain).unwrap(),
|
||||
rr::DNSClass::IN,
|
||||
rr::RecordType::A,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
panic!("DNS query for missing record failed unexpectedly for domain '{domain}': {e}")
|
||||
});
|
||||
background_task.abort();
|
||||
let _ = background_task.await;
|
||||
assert!(response.answers().is_empty(), "{:?}", response.answers());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_magic_dns_server_instance() {
|
||||
let tun_ip = Ipv4Inet::from_str("10.144.144.10/24").unwrap();
|
||||
let (peer_mgr, virtual_nic) = prepare_env("test1", tun_ip).await;
|
||||
let tun_name = virtual_nic.ifname().await.unwrap();
|
||||
let fake_ip = Ipv4Addr::from_str("100.100.100.101").unwrap();
|
||||
let dns_server_inst =
|
||||
MagicDnsServerInstance::new(peer_mgr.clone(), Some(tun_name), tun_ip, fake_ip)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let routes = [
|
||||
Route {
|
||||
hostname: "test1".to_string(),
|
||||
ipv4_addr: Some(Ipv4Inet::from_str("8.8.8.8/24").unwrap().into()),
|
||||
..Default::default()
|
||||
},
|
||||
Route {
|
||||
hostname: "中文".to_string(),
|
||||
ipv4_addr: Some(Ipv4Inet::from_str("8.8.8.8/24").unwrap().into()),
|
||||
..Default::default()
|
||||
},
|
||||
Route {
|
||||
hostname: ".invalid".to_string(),
|
||||
ipv4_addr: Some(Ipv4Inet::from_str("8.8.8.8/24").unwrap().into()),
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
dns_server_inst
|
||||
.data
|
||||
.update_dns_records(routes.iter(), DEFAULT_ET_DNS_ZONE)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
check_dns_record(&fake_ip, "test1.et.net", "8.8.8.8").await;
|
||||
check_dns_record(&fake_ip, "中文.et.net", "8.8.8.8").await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_magic_dns_runner() {
|
||||
// Test first runner with default DNS settings
|
||||
{
|
||||
let tun_ip = Ipv4Inet::from_str("10.144.144.10/24").unwrap();
|
||||
let (peer_mgr, virtual_nic) = prepare_env("test1", tun_ip).await;
|
||||
let tun_name = virtual_nic.ifname().await.unwrap();
|
||||
let fake_ip = Ipv4Addr::from_str(MAGIC_DNS_FAKE_IP).unwrap();
|
||||
let mut dns_runner = DnsRunner::new(peer_mgr, Some(tun_name), tun_ip, fake_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;
|
||||
});
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
|
||||
// Test default settings: query should resolve test1.et.net to tunnel IP via default fake IP
|
||||
check_dns_record(&fake_ip, "test1.et.net", "10.144.144.10").await;
|
||||
|
||||
cancel_token.cancel();
|
||||
t.await.unwrap();
|
||||
|
||||
// Wait a bit for cleanup
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
// Test second runner with different TLD zone
|
||||
{
|
||||
let tun_ip = Ipv4Inet::from_str("10.144.144.20/24").unwrap();
|
||||
// NOTE: Using same fake IP to avoid system DNS configuration conflicts
|
||||
let custom_tld_zone = "custom.local."; // Different TLD zone is safer
|
||||
let (peer_mgr, virtual_nic) =
|
||||
prepare_env_with_tld_dns_zone("test2", tun_ip, Some(custom_tld_zone)).await;
|
||||
let tun_name = virtual_nic.ifname().await.unwrap();
|
||||
let fake_ip = Ipv4Addr::from_str(MAGIC_DNS_FAKE_IP).unwrap();
|
||||
let mut dns_runner = DnsRunner::new(peer_mgr, Some(tun_name), tun_ip, fake_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;
|
||||
});
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
|
||||
// Test with same fake IP but different TLD zone
|
||||
check_dns_record(&fake_ip, "test2.custom.local", "10.144.144.20").await;
|
||||
|
||||
cancel_token.cancel();
|
||||
t.await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_magic_dns_update_replaces_records_for_same_client() {
|
||||
let tun_ip = Ipv4Inet::from_str("100.100.100.0/24").unwrap();
|
||||
let ctx = get_mock_global_ctx();
|
||||
ctx.set_hostname("test1".to_string());
|
||||
ctx.set_ipv4(Some(tun_ip));
|
||||
|
||||
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 fake_ip = Ipv4Addr::from_str(MAGIC_DNS_FAKE_IP).unwrap();
|
||||
let dns_server_inst = MagicDnsServerInstance::new(peer_mgr.clone(), None, tun_ip, fake_ip)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut ctrl = BaseController::default();
|
||||
ctrl.set_tunnel_info(Some(crate::proto::common::TunnelInfo {
|
||||
tunnel_type: "tcp".to_string(),
|
||||
local_addr: None,
|
||||
remote_addr: Some(crate::proto::common::Url {
|
||||
url: "tcp://127.0.0.1:54321".to_string(),
|
||||
}),
|
||||
resolved_remote_addr: None,
|
||||
}));
|
||||
|
||||
dns_server_inst
|
||||
.data
|
||||
.update_dns_record(
|
||||
ctrl.clone(),
|
||||
UpdateDnsRecordRequest {
|
||||
zone: DEFAULT_ET_DNS_ZONE.to_string(),
|
||||
routes: vec![Route {
|
||||
hostname: "test1".to_string(),
|
||||
ipv4_addr: Some(Ipv4Inet::from_str("8.8.8.8/32").unwrap().into()),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
dns_server_inst
|
||||
.data
|
||||
.update_dns_record(
|
||||
ctrl,
|
||||
UpdateDnsRecordRequest {
|
||||
zone: DEFAULT_ET_DNS_ZONE.to_string(),
|
||||
routes: vec![Route {
|
||||
hostname: "test1".to_string(),
|
||||
ipv4_addr: Some(Ipv4Inet::from_str("1.1.1.1/32").unwrap().into()),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let dns_records = dns_server_inst
|
||||
.data
|
||||
.get_dns_record(
|
||||
BaseController::default(),
|
||||
crate::proto::common::Void::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let zone_records = dns_records.records.get(DEFAULT_ET_DNS_ZONE).unwrap();
|
||||
let a_records = zone_records
|
||||
.records
|
||||
.iter()
|
||||
.filter_map(|record| match record.record.as_ref() {
|
||||
Some(crate::proto::magic_dns::dns_record::Record::A(a))
|
||||
if a.name == "test1.et.net." =>
|
||||
{
|
||||
Some(a)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(a_records.len(), 1, "{a_records:?}");
|
||||
let resolved_ip = Ipv4Addr::from(a_records[0].value.unwrap_or_default());
|
||||
assert_eq!(resolved_ip, Ipv4Addr::new(1, 1, 1, 1));
|
||||
|
||||
let mut ctrl = BaseController::default();
|
||||
ctrl.set_tunnel_info(Some(crate::proto::common::TunnelInfo {
|
||||
tunnel_type: "tcp".to_string(),
|
||||
local_addr: None,
|
||||
remote_addr: Some(crate::proto::common::Url {
|
||||
url: "tcp://127.0.0.1:54321".to_string(),
|
||||
}),
|
||||
resolved_remote_addr: None,
|
||||
}));
|
||||
|
||||
dns_server_inst
|
||||
.data
|
||||
.update_dns_record(
|
||||
ctrl,
|
||||
UpdateDnsRecordRequest {
|
||||
zone: DEFAULT_ET_DNS_ZONE.to_string(),
|
||||
routes: vec![],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let dns_records = dns_server_inst
|
||||
.data
|
||||
.get_dns_record(
|
||||
BaseController::default(),
|
||||
crate::proto::common::Void::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!dns_records.records.contains_key(DEFAULT_ET_DNS_ZONE));
|
||||
}
|
||||
@@ -7,16 +7,12 @@ use std::sync::{Arc, Weak};
|
||||
#[cfg(feature = "tun")]
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use cidr::{IpCidr, Ipv4Inet};
|
||||
use futures::FutureExt;
|
||||
use tokio::sync::{Mutex, Notify};
|
||||
#[cfg(feature = "tun")]
|
||||
use tokio::{sync::oneshot, task::JoinSet};
|
||||
#[cfg(feature = "magic-dns")]
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
use super::listeners::ListenerManager;
|
||||
use super::public_ipv6_provider::{
|
||||
reconcile_public_ipv6_provider_runtime, run_public_ipv6_provider_reconcile_task,
|
||||
should_run_public_ipv6_provider_reconcile, validate_public_ipv6_config,
|
||||
validate_public_ipv6_config_values,
|
||||
};
|
||||
use crate::common::PeerId;
|
||||
use crate::common::acl_processor::AclRuleBuilder;
|
||||
use crate::common::config::ConfigLoader;
|
||||
@@ -26,6 +22,8 @@ use crate::connector::direct::DirectConnectorManager;
|
||||
use crate::connector::manual::{ConnectorManagerRpcService, ManualConnectorManager};
|
||||
use crate::connector::tcp_hole_punch::TcpHolePunchConnector;
|
||||
use crate::connector::udp_hole_punch::UdpHolePunchConnector;
|
||||
#[cfg(feature = "magic-dns")]
|
||||
use crate::dns::{config::DnsConfigLoaderExt, node::DnsNode};
|
||||
use crate::gateway::icmp_proxy::IcmpProxy;
|
||||
#[cfg(feature = "kcp")]
|
||||
use crate::gateway::kcp_proxy::{KcpProxyDst, KcpProxyDstRpcService, KcpProxySrc};
|
||||
@@ -60,15 +58,13 @@ use crate::proto::rpc_types::controller::BaseController;
|
||||
use crate::rpc_service::InstanceRpcService;
|
||||
use crate::utils::weak_upgrade;
|
||||
use crate::vpn_portal::{self, VpnPortal};
|
||||
|
||||
#[cfg(feature = "magic-dns")]
|
||||
use super::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner};
|
||||
use super::listeners::ListenerManager;
|
||||
use super::public_ipv6_provider::{
|
||||
reconcile_public_ipv6_provider_runtime, run_public_ipv6_provider_reconcile_task,
|
||||
should_run_public_ipv6_provider_reconcile, validate_public_ipv6_config,
|
||||
validate_public_ipv6_config_values,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use cidr::{IpCidr, Ipv4Inet};
|
||||
use futures::FutureExt;
|
||||
use tokio::sync::{Mutex, Notify};
|
||||
#[cfg(feature = "tun")]
|
||||
use tokio::{sync::oneshot, task::JoinSet};
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
use crate::gateway::socks5::Socks5Server;
|
||||
@@ -135,65 +131,10 @@ impl IpProxy {
|
||||
}
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
type NicCtx = super::virtual_nic::NicCtx;
|
||||
|
||||
#[cfg(feature = "magic-dns")]
|
||||
struct MagicDnsContainer {
|
||||
dns_runner_task: AbortOnDropHandle<()>,
|
||||
dns_runner_cancel_token: CancellationToken,
|
||||
}
|
||||
|
||||
// nic container will be cleared when dhcp ip changed
|
||||
#[cfg(feature = "tun")]
|
||||
pub struct NicCtxContainer {
|
||||
nic_ctx: Option<Box<dyn Any + 'static + Send>>,
|
||||
#[cfg(feature = "magic-dns")]
|
||||
magic_dns: Option<MagicDnsContainer>,
|
||||
}
|
||||
pub type NicCtx = super::virtual_nic::NicCtx;
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
impl NicCtxContainer {
|
||||
#[cfg(not(feature = "magic-dns"))]
|
||||
fn new(nic_ctx: NicCtx) -> Self {
|
||||
Self {
|
||||
nic_ctx: Some(Box::new(nic_ctx)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "magic-dns")]
|
||||
fn new(nic_ctx: NicCtx, dns_runner: Option<DnsRunner>) -> Self {
|
||||
if let Some(mut dns_runner) = dns_runner {
|
||||
let token = CancellationToken::new();
|
||||
let token_clone = token.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
let _ = dns_runner.run(token_clone).await;
|
||||
});
|
||||
Self {
|
||||
nic_ctx: Some(Box::new(nic_ctx)),
|
||||
magic_dns: Some(MagicDnsContainer {
|
||||
dns_runner_task: AbortOnDropHandle::new(task),
|
||||
dns_runner_cancel_token: token,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
nic_ctx: Some(Box::new(nic_ctx)),
|
||||
magic_dns: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_with_any<T: 'static + Send>(ctx: T) -> Self {
|
||||
Self {
|
||||
nic_ctx: Some(Box::new(ctx)),
|
||||
#[cfg(feature = "magic-dns")]
|
||||
magic_dns: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
type ArcNicCtx = Arc<Mutex<Option<NicCtxContainer>>>;
|
||||
pub type ArcNicCtx = Arc<Mutex<Option<Box<dyn Any + 'static + Send>>>>;
|
||||
|
||||
pub struct InstanceRpcServerHook {
|
||||
rpc_portal_whitelist: Vec<IpCidr>,
|
||||
@@ -616,6 +557,8 @@ pub struct Instance {
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
nic_ctx: ArcNicCtx,
|
||||
#[cfg(feature = "magic-dns")]
|
||||
dns: Option<DnsNode>,
|
||||
|
||||
peer_packet_receiver: Arc<Mutex<PacketRecvChanReceiver>>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
@@ -698,13 +641,19 @@ impl Instance {
|
||||
#[cfg(feature = "socks5")]
|
||||
let socks5_server = Socks5Server::new(global_ctx.clone(), peer_manager.clone(), None);
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
let nic_ctx = Arc::new(Mutex::new(None));
|
||||
|
||||
Instance {
|
||||
inst_name: global_ctx.inst_name.clone(),
|
||||
id,
|
||||
|
||||
peer_packet_receiver: Arc::new(Mutex::new(peer_packet_receiver)),
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
nic_ctx: Arc::new(Mutex::new(None)),
|
||||
nic_ctx,
|
||||
#[cfg(feature = "magic-dns")]
|
||||
dns: None,
|
||||
|
||||
peer_manager,
|
||||
listener_manager,
|
||||
@@ -760,16 +709,6 @@ impl Instance {
|
||||
arc_nic_ctx: ArcNicCtx,
|
||||
packet_recv: Arc<Mutex<PacketRecvChanReceiver>>,
|
||||
) {
|
||||
#[cfg(feature = "magic-dns")]
|
||||
if let Some(old_ctx) = arc_nic_ctx.lock().await.take()
|
||||
&& let Some(dns_runner) = old_ctx.magic_dns
|
||||
{
|
||||
dns_runner.dns_runner_cancel_token.cancel();
|
||||
tracing::debug!("cancelling dns runner task");
|
||||
let ret = dns_runner.dns_runner_task.await;
|
||||
tracing::debug!("dns runner task cancelled, ret: {:?}", ret);
|
||||
};
|
||||
|
||||
let mut tasks = JoinSet::new();
|
||||
tasks.spawn(async move {
|
||||
let mut packet_recv = packet_recv.lock().await;
|
||||
@@ -777,46 +716,15 @@ impl Instance {
|
||||
tracing::trace!("packet consumed by mock nic ctx: {:?}", packet);
|
||||
}
|
||||
});
|
||||
arc_nic_ctx
|
||||
.lock()
|
||||
.await
|
||||
.replace(NicCtxContainer::new_with_any(tasks));
|
||||
arc_nic_ctx.lock().await.replace(Box::new(tasks));
|
||||
|
||||
tracing::debug!("nic ctx cleared.");
|
||||
}
|
||||
|
||||
#[cfg(feature = "magic-dns")]
|
||||
fn create_magic_dns_runner(
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
tun_dev: Option<String>,
|
||||
tun_ip: Ipv4Inet,
|
||||
) -> Option<DnsRunner> {
|
||||
let ctx = peer_mgr.get_global_ctx();
|
||||
if !ctx.config.get_flags().accept_dns {
|
||||
return None;
|
||||
}
|
||||
|
||||
let runner = DnsRunner::new(
|
||||
peer_mgr,
|
||||
tun_dev,
|
||||
tun_ip,
|
||||
MAGIC_DNS_FAKE_IP.parse().unwrap(),
|
||||
);
|
||||
Some(runner)
|
||||
}
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
async fn use_new_nic_ctx(
|
||||
arc_nic_ctx: ArcNicCtx,
|
||||
nic_ctx: NicCtx,
|
||||
#[cfg(feature = "magic-dns")] magic_dns: Option<DnsRunner>,
|
||||
) {
|
||||
async fn use_new_nic_ctx(arc_nic_ctx: ArcNicCtx, nic_ctx: NicCtx) {
|
||||
let mut g = arc_nic_ctx.lock().await;
|
||||
*g = Some(NicCtxContainer::new(
|
||||
nic_ctx,
|
||||
#[cfg(feature = "magic-dns")]
|
||||
magic_dns,
|
||||
));
|
||||
*g = Some(Box::new(nic_ctx));
|
||||
tracing::debug!("nic ctx updated.");
|
||||
}
|
||||
|
||||
@@ -921,15 +829,7 @@ impl Instance {
|
||||
global_ctx_c.set_ipv4(None);
|
||||
continue;
|
||||
}
|
||||
#[cfg(feature = "magic-dns")]
|
||||
let ifname = new_nic_ctx.ifname().await;
|
||||
Self::use_new_nic_ctx(
|
||||
nic_ctx.clone(),
|
||||
new_nic_ctx,
|
||||
#[cfg(feature = "magic-dns")]
|
||||
Self::create_magic_dns_runner(peer_manager_c.clone(), ifname, ip),
|
||||
)
|
||||
.await;
|
||||
Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx).await;
|
||||
}
|
||||
|
||||
current_dhcp_ip = Some(ip);
|
||||
@@ -961,10 +861,11 @@ impl Instance {
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut output_tx = Some(first_round_output);
|
||||
|
||||
loop {
|
||||
let close_notifier = Arc::new(Notify::new());
|
||||
{
|
||||
let Some(peer_mgr) = peer_mgr.upgrade() else {
|
||||
let mut new_nic_ctx = {
|
||||
let Some(peer_manager) = peer_mgr.upgrade() else {
|
||||
tracing::warn!("peer manager is dropped, stop static ip check.");
|
||||
if let Some(output_tx) = output_tx.take() {
|
||||
let _ = output_tx.send(Err(Error::Unknown));
|
||||
@@ -973,38 +874,26 @@ impl Instance {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut new_nic_ctx = NicCtx::new(
|
||||
peer_mgr.get_global_ctx(),
|
||||
&peer_mgr,
|
||||
NicCtx::new(
|
||||
peer_manager.get_global_ctx(),
|
||||
&peer_manager,
|
||||
peer_packet_receiver.clone(),
|
||||
close_notifier.clone(),
|
||||
);
|
||||
)
|
||||
};
|
||||
|
||||
if let Err(e) = new_nic_ctx.run(ipv4_addr, ipv6_addr).await {
|
||||
if let Some(output_tx) = output_tx.take() {
|
||||
let _ = output_tx.send(Err(e));
|
||||
return;
|
||||
}
|
||||
tracing::error!("failed to create new nic ctx, err: {:?}", e);
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
continue;
|
||||
if let Err(e) = new_nic_ctx.run(ipv4_addr, ipv6_addr).await {
|
||||
if let Some(output_tx) = output_tx.take() {
|
||||
let _ = output_tx.send(Err(e));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create Magic DNS runner only if we have IPv4
|
||||
#[cfg(feature = "magic-dns")]
|
||||
{
|
||||
let ifname = new_nic_ctx.ifname().await;
|
||||
let dns_runner = if let Some(ipv4) = ipv4_addr {
|
||||
Self::create_magic_dns_runner(peer_mgr, ifname, ipv4)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx, dns_runner).await;
|
||||
}
|
||||
#[cfg(not(feature = "magic-dns"))]
|
||||
Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx).await;
|
||||
tracing::error!("failed to create new nic ctx, err: {:?}", e);
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx).await;
|
||||
|
||||
if let Some(output_tx) = output_tx.take() {
|
||||
let _ = output_tx.send(Ok(()));
|
||||
}
|
||||
@@ -1044,6 +933,18 @@ impl Instance {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "magic-dns")]
|
||||
if !self.global_ctx.config.get_dns().disabled {
|
||||
let mut node = DnsNode::new(
|
||||
self.get_peer_manager(),
|
||||
self.get_global_ctx(),
|
||||
#[cfg(feature = "tun")]
|
||||
self.get_nic_ctx(),
|
||||
);
|
||||
node.start();
|
||||
self.dns = Some(node);
|
||||
}
|
||||
|
||||
if self.global_ctx.config.get_dhcp() {
|
||||
self.check_dhcp_ip_conflict();
|
||||
}
|
||||
@@ -1583,26 +1484,28 @@ impl Instance {
|
||||
.await
|
||||
.with_context(|| "add ip failed")?;
|
||||
|
||||
let magic_dns_runner = if let Some(ipv4) = global_ctx.get_ipv4() {
|
||||
Self::create_magic_dns_runner(peer_manager.clone(), None, ipv4)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx, magic_dns_runner).await;
|
||||
Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn clear_resources(&mut self) {
|
||||
self.peer_manager.clear_resources().await;
|
||||
#[cfg(feature = "magic-dns")]
|
||||
if let Some(mut node) = self.dns.take() {
|
||||
let _ = node.stop().await;
|
||||
}
|
||||
#[cfg(feature = "tun")]
|
||||
let _ = self.nic_ctx.lock().await.take();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: duplicated with clear_resources?
|
||||
impl Drop for Instance {
|
||||
fn drop(&mut self) {
|
||||
let my_peer_id = self.peer_manager.my_peer_id();
|
||||
let pm = Arc::downgrade(&self.peer_manager);
|
||||
#[cfg(feature = "magic-dns")]
|
||||
let _ = self.dns.take(); // force abort
|
||||
#[cfg(feature = "tun")]
|
||||
let nic_ctx = self.nic_ctx.clone();
|
||||
tokio::spawn(async move {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod dns_server;
|
||||
#[allow(clippy::module_inception)]
|
||||
pub mod instance;
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtxEvent};
|
||||
use crate::peers::peer_manager::PeerManager;
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtxEvent};
|
||||
use crate::peers::peer_manager::PeerManager;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
/// ProxyCidrsMonitor monitors changes in proxy CIDRs from peer routes
|
||||
|
||||
@@ -835,6 +835,34 @@ impl NicCtx {
|
||||
nic.ifname.as_ref().map(|s| s.to_owned())
|
||||
}
|
||||
|
||||
pub async fn add_ipv4_to_tun_device(&self, ipv4_addr: cidr::Ipv4Inet) -> Result<(), Error> {
|
||||
let nic = self.nic.lock().await;
|
||||
nic.add_ip(ipv4_addr.address(), ipv4_addr.network_length() as i32)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn add_ipv6_to_tun_device(&self, ipv6_addr: cidr::Ipv6Inet) -> Result<(), Error> {
|
||||
let nic = self.nic.lock().await;
|
||||
nic.add_ipv6(ipv6_addr.address(), ipv6_addr.network_length() as i32)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn remove_ipv4_from_tun_device(
|
||||
&self,
|
||||
ipv4_addr: cidr::Ipv4Inet,
|
||||
) -> Result<(), Error> {
|
||||
let nic = self.nic.lock().await;
|
||||
nic.remove_ip(Some(ipv4_addr)).await
|
||||
}
|
||||
|
||||
pub async fn remove_ipv6_from_tun_device(
|
||||
&self,
|
||||
ipv6_addr: cidr::Ipv6Inet,
|
||||
) -> Result<(), Error> {
|
||||
let nic = self.nic.lock().await;
|
||||
nic.remove_ipv6(Some(ipv6_addr)).await
|
||||
}
|
||||
|
||||
pub async fn assign_ipv4_to_tun_device(&self, ipv4_addr: cidr::Ipv4Inet) -> Result<(), Error> {
|
||||
let nic = self.nic.lock().await;
|
||||
nic.link_up().await?;
|
||||
|
||||
@@ -316,6 +316,8 @@ fn handle_event(
|
||||
event!(info, peer_id, "[{}] peer removed", instance_id);
|
||||
}
|
||||
|
||||
GlobalCtxEvent::PeerInfoUpdated(_) => {}
|
||||
|
||||
GlobalCtxEvent::PeerConnAdded(conn_info) => {
|
||||
event!(
|
||||
info,
|
||||
|
||||
@@ -828,10 +828,6 @@ impl NetworkConfig {
|
||||
flags.disable_sym_hole_punching = disable_sym_hole_punching;
|
||||
}
|
||||
|
||||
if let Some(enable_magic_dns) = self.enable_magic_dns {
|
||||
flags.accept_dns = enable_magic_dns;
|
||||
}
|
||||
|
||||
if let Some(mtu) = self.mtu {
|
||||
flags.mtu = mtu as u32;
|
||||
}
|
||||
@@ -1001,7 +997,6 @@ impl NetworkConfig {
|
||||
result.disable_relay_data = Some(flags.disable_relay_data);
|
||||
result.enable_udp_broadcast_relay = Some(flags.enable_udp_broadcast_relay);
|
||||
result.disable_sym_hole_punching = Some(flags.disable_sym_hole_punching);
|
||||
result.enable_magic_dns = Some(flags.accept_dns);
|
||||
result.mtu = Some(flags.mtu as i32);
|
||||
result.instance_recv_bps_limit =
|
||||
(flags.instance_recv_bps_limit != u64::MAX).then_some(flags.instance_recv_bps_limit);
|
||||
@@ -1269,7 +1264,6 @@ mod tests {
|
||||
flags.disable_udp_hole_punching = rng.gen_bool(0.2);
|
||||
flags.disable_upnp = rng.gen_bool(0.2);
|
||||
flags.enable_udp_broadcast_relay = rng.gen_bool(0.2);
|
||||
flags.accept_dns = rng.gen_bool(0.6);
|
||||
flags.mtu = rng.gen_range(1200..1500);
|
||||
flags.private_mode = rng.gen_bool(0.3);
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ use clap::Command;
|
||||
use clap_complete::{Generator, Shell};
|
||||
|
||||
mod arch;
|
||||
#[cfg(feature = "magic-dns")]
|
||||
mod dns;
|
||||
mod gateway;
|
||||
pub mod instance;
|
||||
mod peer_center;
|
||||
|
||||
@@ -30,6 +30,8 @@ use tokio::{
|
||||
task::{JoinHandle, JoinSet},
|
||||
};
|
||||
|
||||
use crate::common::config::ConfigLoader;
|
||||
use crate::proto::utils::TransientDigest;
|
||||
use crate::{
|
||||
common::{
|
||||
PeerId,
|
||||
@@ -188,9 +190,11 @@ fn is_foreign_network_info_newer(
|
||||
}
|
||||
|
||||
impl RoutePeerInfo {
|
||||
#[allow(deprecated)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
#[allow(deprecated)]
|
||||
quic_port: None,
|
||||
|
||||
peer_id: 0,
|
||||
inst_id: Some(uuid::Uuid::nil().into()),
|
||||
cost: 0,
|
||||
@@ -209,8 +213,8 @@ impl RoutePeerInfo {
|
||||
network_length: 24,
|
||||
ipv6_addr: None,
|
||||
groups: Vec::new(),
|
||||
dns: Default::default(),
|
||||
|
||||
quic_port: None,
|
||||
noise_static_pubkey: Vec::new(),
|
||||
trusted_credential_pubkeys: Vec::new(),
|
||||
ipv6_public_addr_prefix: None,
|
||||
@@ -240,7 +244,19 @@ impl RoutePeerInfo {
|
||||
.and_then(|cfg| cfg.public_key().ok())
|
||||
.map(|pk| pk.as_bytes().to_vec())
|
||||
.unwrap_or_default();
|
||||
|
||||
let dns = cfg_select! {
|
||||
feature = "magic-dns" => {{
|
||||
use crate::dns::config::DnsGlobalCtxExt;
|
||||
global_ctx.dns_export_config().digest().into()
|
||||
}}
|
||||
_ => Default::default(),
|
||||
};
|
||||
|
||||
Self {
|
||||
#[allow(deprecated)]
|
||||
quic_port: None,
|
||||
|
||||
peer_id: my_peer_id,
|
||||
inst_id: Some(global_ctx.get_id().into()),
|
||||
cost: 0,
|
||||
@@ -280,6 +296,7 @@ impl RoutePeerInfo {
|
||||
ipv6_public_addr_lease: public_ipv6_addr_lease.map(Into::into),
|
||||
|
||||
groups: global_ctx.get_acl_groups(my_peer_id),
|
||||
dns,
|
||||
|
||||
noise_static_pubkey,
|
||||
|
||||
@@ -293,8 +310,6 @@ impl RoutePeerInfo {
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,7 +362,7 @@ impl From<RoutePeerInfo> for crate::proto::api::instance::Route {
|
||||
next_hop_peer_id: 0, // next_hop_peer_id is calculated in RouteTable.
|
||||
cost: 0, // cost is calculated in RouteTable.
|
||||
path_latency: 0, // path_latency is calculated in RouteTable.
|
||||
proxy_cidrs: val.proxy_cidrs.clone(),
|
||||
proxy_cidrs: val.proxy_cidrs,
|
||||
hostname: val.hostname.unwrap_or_default(),
|
||||
stun_info: {
|
||||
let mut stun_info = StunInfo::default();
|
||||
@@ -837,8 +852,8 @@ impl SyncedRouteInfo {
|
||||
dst_peer_id: PeerId,
|
||||
peer_infos: &[RoutePeerInfo],
|
||||
raw_peer_infos: &[DynamicMessage],
|
||||
) -> Result<(), Error> {
|
||||
let mut need_inc_version = false;
|
||||
) -> Result<Vec<PeerId>, Error> {
|
||||
let mut updated_peer_ids = Vec::new();
|
||||
for (idx, route_info) in peer_infos.iter().enumerate() {
|
||||
let mut route_info = route_info.clone();
|
||||
let raw_route_info = &raw_peer_infos[idx];
|
||||
@@ -874,14 +889,15 @@ impl SyncedRouteInfo {
|
||||
{
|
||||
self.raw_peer_infos
|
||||
.insert(route_info.peer_id, raw_route_info.clone());
|
||||
guard.insert(route_info.peer_id, route_info);
|
||||
need_inc_version = true;
|
||||
let peer_id = route_info.peer_id;
|
||||
guard.insert(peer_id, route_info);
|
||||
updated_peer_ids.push(peer_id);
|
||||
}
|
||||
}
|
||||
if need_inc_version {
|
||||
if !updated_peer_ids.is_empty() {
|
||||
self.version.inc();
|
||||
}
|
||||
Ok(())
|
||||
Ok(updated_peer_ids)
|
||||
}
|
||||
|
||||
fn update_conn_info_one_peer(
|
||||
@@ -3541,7 +3557,7 @@ impl RouteSessionManager {
|
||||
.get_network_identity()
|
||||
.network_secret
|
||||
.is_none();
|
||||
service_impl.synced_route_info.update_peer_infos(
|
||||
let updated_peer_ids = service_impl.synced_route_info.update_peer_infos(
|
||||
my_peer_id,
|
||||
service_impl.my_peer_route_id,
|
||||
from_peer_id,
|
||||
@@ -3557,6 +3573,12 @@ impl RouteSessionManager {
|
||||
);
|
||||
session.update_dst_saved_peer_info_version(pi, from_peer_id);
|
||||
need_update_route_table = true;
|
||||
|
||||
if !updated_peer_ids.is_empty() {
|
||||
service_impl
|
||||
.global_ctx
|
||||
.issue_event(GlobalCtxEvent::PeerInfoUpdated(updated_peer_ids));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ message NetworkConfig {
|
||||
optional int32 mtu = 40;
|
||||
repeated string mapped_listeners = 41;
|
||||
|
||||
optional bool enable_magic_dns = 42;
|
||||
optional bool enable_magic_dns = 42 [deprecated = true];
|
||||
optional bool enable_private_mode = 43;
|
||||
|
||||
// repeated string rpc_portal_whitelists = 44;
|
||||
|
||||
@@ -33,7 +33,7 @@ message FlagsInConfig {
|
||||
bool proxy_forward_by_system = 21;
|
||||
|
||||
// enable magic dns or not
|
||||
bool accept_dns = 22;
|
||||
bool accept_dns = 22 [deprecated = true];
|
||||
// enable private mode
|
||||
bool private_mode = 23;
|
||||
|
||||
@@ -65,7 +65,7 @@ message FlagsInConfig {
|
||||
bool disable_sym_hole_punching = 30;
|
||||
|
||||
// tld dns zone for magic dns
|
||||
string tld_dns_zone = 31;
|
||||
string tld_dns_zone = 31 [deprecated = true];
|
||||
|
||||
bool p2p_only = 32;
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "common.proto";
|
||||
|
||||
package dns;
|
||||
|
||||
message ZoneData {
|
||||
string content = 1;
|
||||
repeated common.Url forwarders = 2;
|
||||
repeated int32 fallthrough = 3;
|
||||
}
|
||||
|
||||
message GetExportConfigRequest {}
|
||||
|
||||
message GetExportConfigResponse {
|
||||
repeated ZoneData zones = 1;
|
||||
}
|
||||
|
||||
service DnsPeerMgrRpc {
|
||||
rpc GetExportConfig(GetExportConfigRequest) returns (GetExportConfigResponse) {}
|
||||
}
|
||||
|
||||
message DnsSnapshot {
|
||||
repeated ZoneData zones = 1;
|
||||
repeated common.Url addresses = 2;
|
||||
repeated common.Url listeners = 3;
|
||||
}
|
||||
|
||||
message HeartbeatRequest {
|
||||
common.UUID id = 1;
|
||||
bytes digest = 2;
|
||||
optional DnsSnapshot snapshot = 3;
|
||||
}
|
||||
|
||||
message HeartbeatResponse {
|
||||
bool resync = 1;
|
||||
}
|
||||
|
||||
service DnsNodeMgrRpc {
|
||||
rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse) {}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use crate::dns::config::zone::Fallthrough;
|
||||
use crate::proto::common::Url;
|
||||
use crate::proto::utils::TransientDigest;
|
||||
use hickory_proto::rr::LowerName;
|
||||
use std::fmt::Write;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/dns.rs"));
|
||||
|
||||
impl HeartbeatRequest {
|
||||
pub fn update(&mut self, snapshot: DnsSnapshot) {
|
||||
self.digest = snapshot.digest().into();
|
||||
self.snapshot = Some(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
impl ZoneData {
|
||||
pub fn new<Record: AsRef<str>>(
|
||||
origin: &LowerName,
|
||||
ttl: u32,
|
||||
records: impl IntoIterator<Item = Record>,
|
||||
forwarders: impl IntoIterator<Item = Url>,
|
||||
fallthrough: impl IntoIterator<Item = Fallthrough>,
|
||||
) -> Self {
|
||||
let mut content = String::new();
|
||||
|
||||
content.push_str("; EasyTier Magic DNS zone data\n");
|
||||
content.push_str("; https://github.com/easytier/easytier\n");
|
||||
|
||||
let mut origin = origin.to_string();
|
||||
if !origin.ends_with('.') {
|
||||
origin.push('.');
|
||||
}
|
||||
|
||||
writeln!(content, "$ORIGIN {}", origin).unwrap();
|
||||
writeln!(content, "$TTL {}", ttl).unwrap();
|
||||
|
||||
for record in records {
|
||||
content.push_str(record.as_ref());
|
||||
content.push('\n');
|
||||
}
|
||||
|
||||
let forwarders = forwarders.into_iter().collect();
|
||||
let fallthrough = fallthrough.into_iter().map(Into::into).collect();
|
||||
|
||||
Self {
|
||||
content,
|
||||
forwarders,
|
||||
fallthrough,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "common.proto";
|
||||
import "api_instance.proto";
|
||||
|
||||
package magic_dns;
|
||||
|
||||
message DnsRecordA {
|
||||
string name = 1;
|
||||
common.Ipv4Addr value = 2;
|
||||
int32 ttl = 3;
|
||||
}
|
||||
|
||||
message DnsRecordSOA {
|
||||
string name = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message DnsRecord {
|
||||
oneof record {
|
||||
DnsRecordA a = 1;
|
||||
DnsRecordSOA soa = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message DnsRecordList {
|
||||
repeated DnsRecord records = 1;
|
||||
}
|
||||
|
||||
message UpdateDnsRecordRequest {
|
||||
string zone = 1;
|
||||
repeated api.instance.Route routes = 2;
|
||||
}
|
||||
|
||||
message GetDnsRecordResponse {
|
||||
map<string, DnsRecordList> records = 1;
|
||||
}
|
||||
|
||||
message HandshakeRequest {}
|
||||
|
||||
message HandshakeResponse {}
|
||||
|
||||
service MagicDnsServerRpc {
|
||||
rpc Handshake(HandshakeRequest) returns (HandshakeResponse) {}
|
||||
rpc Heartbeat(common.Void) returns (common.Void) {}
|
||||
rpc UpdateDnsRecord(UpdateDnsRecordRequest) returns (common.Void) {}
|
||||
rpc GetDnsRecord(common.Void) returns (GetDnsRecordResponse) {}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
include!(concat!(env!("OUT_DIR"), "/magic_dns.rs"));
|
||||
@@ -4,9 +4,9 @@ pub mod rpc_types;
|
||||
pub mod acl;
|
||||
pub mod api;
|
||||
pub mod common;
|
||||
pub mod error;
|
||||
#[cfg(feature = "magic-dns")]
|
||||
pub mod magic_dns;
|
||||
pub mod dns;
|
||||
pub mod error;
|
||||
pub mod peer_rpc;
|
||||
pub mod web;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ syntax = "proto3";
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "common.proto";
|
||||
import "dns.proto";
|
||||
|
||||
package peer_rpc;
|
||||
|
||||
@@ -48,6 +49,8 @@ message RoutePeerInfo {
|
||||
// Trusted credential public keys published by admin nodes (holding network_secret)
|
||||
repeated TrustedCredentialPubkeyProof trusted_credential_pubkeys = 19;
|
||||
|
||||
bytes dns = 20;
|
||||
|
||||
optional common.Ipv6Inet ipv6_public_addr_prefix = 22;
|
||||
optional common.Ipv6Inet ipv6_public_addr_lease = 24;
|
||||
}
|
||||
|
||||
+63
-24
@@ -2,7 +2,7 @@ use delegate::delegate;
|
||||
use derivative::Derivative;
|
||||
use derive_more::{AsMut, AsRef, Deref, DerefMut, From, IntoIterator};
|
||||
use prost::Message;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// Generates a stable digest strictly within the lifecycle of the current process.
|
||||
@@ -36,43 +36,27 @@ where
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Derivative,
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
From,
|
||||
Deref,
|
||||
DerefMut,
|
||||
AsRef,
|
||||
AsMut,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
IntoIterator,
|
||||
Derivative, Debug, Clone, PartialEq, Eq, Hash, From, Deref, DerefMut, AsRef, AsMut, IntoIterator,
|
||||
)]
|
||||
#[derivative(Default(bound = ""))]
|
||||
#[as_ref(forward)]
|
||||
#[as_mut(forward)]
|
||||
#[serde(transparent)]
|
||||
#[into_iterator(owned, ref, ref_mut)]
|
||||
pub struct RepeatedMessageModel<Model>(Vec<Model>);
|
||||
|
||||
impl<Model> RepeatedMessageModel<Model> {
|
||||
pub fn into_inner(self) -> Vec<Model> {
|
||||
self.0
|
||||
}
|
||||
pub struct RepeatedMessageModel<Model> {
|
||||
pub models: Vec<Model>,
|
||||
}
|
||||
|
||||
impl<Model> FromIterator<Model> for RepeatedMessageModel<Model> {
|
||||
fn from_iter<I: IntoIterator<Item = Model>>(iter: I) -> Self {
|
||||
Self(iter.into_iter().collect())
|
||||
Self {
|
||||
models: iter.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Model> Extend<Model> for RepeatedMessageModel<Model> {
|
||||
delegate! {
|
||||
to self.0 {
|
||||
to self.models {
|
||||
fn extend<T: IntoIterator<Item = Model>>(&mut self, iter: T);
|
||||
}
|
||||
}
|
||||
@@ -99,3 +83,58 @@ where
|
||||
value.into_iter().map(Into::into).collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RepeatedSerialize: Serialize + Sized {
|
||||
fn serialize<S>(models: &RepeatedMessageModel<Self>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
Serialize::serialize(&**models, serializer)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RepeatedDeserialize<'de>: Deserialize<'de> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<RepeatedMessageModel<Self>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Vec::deserialize(deserializer).map(Into::into)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn deserialize_in_place<D>(
|
||||
deserializer: D,
|
||||
place: &mut RepeatedMessageModel<Self>,
|
||||
) -> Result<(), D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Deserialize::deserialize_in_place(deserializer, &mut place.models)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Model: RepeatedSerialize> Serialize for RepeatedMessageModel<Model> {
|
||||
delegate! {
|
||||
#[through(RepeatedSerialize)]
|
||||
to self {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, Model: RepeatedDeserialize<'de>> Deserialize<'de> for RepeatedMessageModel<Model> {
|
||||
delegate! {
|
||||
to RepeatedDeserialize {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>;
|
||||
|
||||
#[doc(hidden)]
|
||||
fn deserialize_in_place<D>(deserializer: D, place: &mut Self) -> Result<(), D::Error>
|
||||
where
|
||||
D: Deserializer<'de>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4213,3 +4213,263 @@ pub async fn relay_peer_session_cleanup() {
|
||||
|
||||
drop_insts(insts).await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "magic-dns")]
|
||||
async fn check_dns_record_at(server_addr: SocketAddr, domain: &str, expected_ip: &str) {
|
||||
use hickory_net::client::{Client, ClientHandle};
|
||||
use hickory_net::runtime::TokioRuntimeProvider;
|
||||
use hickory_net::udp::UdpClientStream;
|
||||
use hickory_proto::rr::{DNSClass, Name, RData, RecordType};
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
let expected = expected_ip.parse::<std::net::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()
|
||||
&& let RData::A(a) = &resp.data
|
||||
&& 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;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "magic-dns")]
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
pub async fn three_node_dns_export() {
|
||||
let insts = init_three_node_ex(
|
||||
"tcp",
|
||||
|cfg| {
|
||||
use crate::dns::config::zone::ZoneConfig;
|
||||
use crate::dns::config::{DnsConfigLoaderExt, DnsConfigRaw};
|
||||
use hickory_proto::rr::LowerName;
|
||||
use std::str::FromStr;
|
||||
|
||||
let inst_name = cfg.get_inst_name();
|
||||
let origin = LowerName::from_str(&format!("{}.com.", inst_name)).unwrap();
|
||||
let mut dns_config = DnsConfigRaw {
|
||||
name: Some(LowerName::from_str(&inst_name).unwrap()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let ipv4 = match inst_name.as_str() {
|
||||
"inst1" => "10.144.144.1".parse().ok(),
|
||||
"inst2" => "10.144.144.2".parse().ok(),
|
||||
"inst3" => "10.144.144.3".parse().ok(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
dns_config
|
||||
.zones
|
||||
.get_or_insert_default()
|
||||
.push(ZoneConfig::dedicated(origin, ipv4, vec![]));
|
||||
|
||||
let listener_port = match inst_name.as_str() {
|
||||
"inst1" => 5351,
|
||||
"inst2" => 5352,
|
||||
"inst3" => 5353,
|
||||
_ => 5350,
|
||||
};
|
||||
dns_config.listeners = Some(
|
||||
vec![
|
||||
format!("udp://127.0.0.1:{}", listener_port)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
|
||||
cfg.set_dns(dns_config.into());
|
||||
cfg
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
|
||||
let addr1 = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5351);
|
||||
let addr2 = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5352);
|
||||
let addr3 = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5353);
|
||||
|
||||
check_dns_record_at(addr1, "inst2.com.", "10.144.144.2").await;
|
||||
check_dns_record_at(addr1, "inst3.com.", "10.144.144.3").await;
|
||||
|
||||
check_dns_record_at(addr2, "inst1.com.", "10.144.144.1").await;
|
||||
check_dns_record_at(addr2, "inst3.com.", "10.144.144.3").await;
|
||||
|
||||
check_dns_record_at(addr3, "inst1.com.", "10.144.144.1").await;
|
||||
check_dns_record_at(addr3, "inst2.com.", "10.144.144.2").await;
|
||||
|
||||
drop_insts(insts).await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "magic-dns")]
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
pub async fn three_node_dns_export_chain() {
|
||||
prepare_linux_namespaces();
|
||||
|
||||
let cfg_cb = |cfg: TomlConfigLoader| {
|
||||
use crate::dns::config::zone::ZoneConfig;
|
||||
use crate::dns::config::{DnsConfigLoaderExt, DnsConfigRaw};
|
||||
use hickory_proto::rr::LowerName;
|
||||
use std::str::FromStr;
|
||||
|
||||
let inst_name = cfg.get_inst_name();
|
||||
let origin = LowerName::from_str(&format!("{}.com.", inst_name)).unwrap();
|
||||
let mut dns_config = DnsConfigRaw {
|
||||
name: Some(LowerName::from_str(&inst_name).unwrap()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let ipv4 = match inst_name.as_str() {
|
||||
"inst1" => "10.144.144.1".parse().ok(),
|
||||
"inst2" => "10.144.144.2".parse().ok(),
|
||||
"inst3" => "10.144.144.3".parse().ok(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
dns_config
|
||||
.zones
|
||||
.get_or_insert_default()
|
||||
.push(ZoneConfig::dedicated(origin, ipv4, vec![]));
|
||||
|
||||
let listener_port = match inst_name.as_str() {
|
||||
"inst1" => 5351,
|
||||
"inst2" => 5352,
|
||||
"inst3" => 5353,
|
||||
_ => 5350,
|
||||
};
|
||||
dns_config.listeners = Some(
|
||||
vec![
|
||||
format!("udp://127.0.0.1:{}", listener_port)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
|
||||
cfg.set_dns(dns_config.into());
|
||||
|
||||
let mut flags = cfg.get_flags();
|
||||
flags.disable_p2p = true;
|
||||
cfg.set_flags(flags);
|
||||
|
||||
cfg
|
||||
};
|
||||
|
||||
let mut inst1 = Instance::new(cfg_cb(get_inst_config(
|
||||
"inst1",
|
||||
Some("net_a"),
|
||||
"10.144.144.1",
|
||||
"fd00::1/64",
|
||||
)));
|
||||
let mut inst2 = Instance::new(cfg_cb(get_inst_config(
|
||||
"inst2",
|
||||
Some("net_b"),
|
||||
"10.144.144.2",
|
||||
"fd00::2/64",
|
||||
)));
|
||||
let mut inst3 = Instance::new(cfg_cb(get_inst_config(
|
||||
"inst3",
|
||||
Some("net_c"),
|
||||
"10.144.144.3",
|
||||
"fd00::3/64",
|
||||
)));
|
||||
|
||||
inst1.run().await.unwrap();
|
||||
inst2.run().await.unwrap();
|
||||
inst3.run().await.unwrap();
|
||||
|
||||
inst1
|
||||
.get_conn_manager()
|
||||
.add_connector(crate::tunnel::tcp::TcpTunnelConnector::new(
|
||||
"tcp://10.1.1.2:11010".parse().unwrap(),
|
||||
));
|
||||
|
||||
inst2
|
||||
.get_conn_manager()
|
||||
.add_connector(crate::tunnel::ring::RingTunnelConnector::new(
|
||||
format!("ring://{}", inst3.id()).parse().unwrap(),
|
||||
));
|
||||
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
let routes = inst2.get_peer_manager().list_routes().await;
|
||||
routes.len() == 2
|
||||
},
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await;
|
||||
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
let routes = inst1.get_peer_manager().list_routes().await;
|
||||
routes.len() == 2
|
||||
},
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await;
|
||||
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
let routes = inst3.get_peer_manager().list_routes().await;
|
||||
routes.len() == 2
|
||||
},
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await;
|
||||
|
||||
let insts = vec![inst1, inst2, inst3];
|
||||
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
|
||||
let addr1 = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5351);
|
||||
let addr2 = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5352);
|
||||
let addr3 = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5353);
|
||||
|
||||
check_dns_record_at(addr1, "inst2.com.", "10.144.144.2").await;
|
||||
check_dns_record_at(addr1, "inst3.com.", "10.144.144.3").await;
|
||||
|
||||
check_dns_record_at(addr2, "inst1.com.", "10.144.144.1").await;
|
||||
check_dns_record_at(addr2, "inst3.com.", "10.144.144.3").await;
|
||||
|
||||
check_dns_record_at(addr3, "inst1.com.", "10.144.144.1").await;
|
||||
check_dns_record_at(addr3, "inst2.com.", "10.144.144.2").await;
|
||||
|
||||
drop_insts(insts).await;
|
||||
}
|
||||
|
||||
@@ -541,14 +541,14 @@ pub fn bind<B: Bindable>(
|
||||
B::finalize(socket)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
pub fn reserve_buf(buf: &mut BytesMut, min_size: usize, max_size: usize) {
|
||||
if buf.capacity() < min_size {
|
||||
buf.reserve(max_size);
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
pub mod tests {
|
||||
use atomic_shim::AtomicU64;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
@@ -2,10 +2,7 @@ use std::{
|
||||
collections::hash_map::DefaultHasher, hash::Hasher, net::SocketAddr, pin::Pin, sync::Arc,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
common::{dns::socket_addrs, error::Error},
|
||||
proto::common::TunnelInfo,
|
||||
};
|
||||
use crate::{common::error::Error, proto::common::TunnelInfo};
|
||||
use async_trait::async_trait;
|
||||
use derive_more::{From, TryInto};
|
||||
use futures::{Sink, Stream};
|
||||
@@ -397,6 +394,7 @@ macro_rules! __matches_protocol__ {
|
||||
};
|
||||
}
|
||||
|
||||
use crate::utils::dns::socket_addrs;
|
||||
pub(crate) use __matches_protocol__ as matches_protocol;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
use tokio::sync::watch;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DirtyFlag {
|
||||
tx: watch::Sender<bool>,
|
||||
rx: watch::Receiver<bool>,
|
||||
}
|
||||
|
||||
impl DirtyFlag {
|
||||
pub fn new(value: bool) -> Self {
|
||||
let (tx, rx) = watch::channel(value);
|
||||
Self { tx, rx }
|
||||
}
|
||||
|
||||
pub fn mark(&self) {
|
||||
self.tx.send(true).ok();
|
||||
}
|
||||
|
||||
pub fn peek(&self) -> bool {
|
||||
*self.tx.borrow()
|
||||
}
|
||||
|
||||
pub fn reset(&self) -> bool {
|
||||
self.tx.send_replace(false)
|
||||
}
|
||||
|
||||
pub async fn wait(&self) {
|
||||
let mut rx = self.rx.clone();
|
||||
let _ = rx.wait_for(|v| *v).await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DirtyFlag {
|
||||
fn default() -> Self {
|
||||
Self::new(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
use crate::common::error::Error;
|
||||
use anyhow::Context;
|
||||
use hickory_net::runtime::TokioRuntimeProvider;
|
||||
use hickory_proto::rr::rdata::SRV;
|
||||
use hickory_proto::rr::{IntoName, LowerName, RData};
|
||||
use hickory_resolver::config::{
|
||||
ConnectionConfig, LookupIpStrategy, NameServerConfig, ResolverConfig, ResolverOpts,
|
||||
};
|
||||
use hickory_resolver::system_conf::read_system_conf;
|
||||
use hickory_resolver::{Resolver, TokioResolver};
|
||||
use idna::AsciiDenyList;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use tokio::net::lookup_host;
|
||||
|
||||
const SYSTEM_DNS_RESOLVER: &str = "system";
|
||||
|
||||
#[derive(Clone)]
|
||||
enum DnsResolver {
|
||||
System,
|
||||
Hickory {
|
||||
uri: String,
|
||||
resolver: Arc<Resolver<TokioRuntimeProvider>>,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn get_default_dns_resolvers() -> Vec<String> {
|
||||
vec![SYSTEM_DNS_RESOLVER.to_string()]
|
||||
}
|
||||
|
||||
fn bootstrap_ips_for_doh_host(host: &str) -> Option<Vec<IpAddr>> {
|
||||
if let Ok(ip) = host.parse::<IpAddr>() {
|
||||
return Some(vec![ip]);
|
||||
}
|
||||
|
||||
match host {
|
||||
"dns.alidns.com" => Some(vec![
|
||||
IpAddr::V4(Ipv4Addr::new(223, 5, 5, 5)),
|
||||
IpAddr::V4(Ipv4Addr::new(223, 6, 6, 6)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2400, 0x3200, 0, 0, 0, 0, 0, 1)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2400, 0x3200, 0xbaba, 0, 0, 0, 0, 1)),
|
||||
]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_hickory_resolver(
|
||||
config: ResolverConfig,
|
||||
) -> Result<Arc<Resolver<TokioRuntimeProvider>>, Error> {
|
||||
let mut opts = ResolverOpts::default();
|
||||
opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
|
||||
Ok(Arc::new(
|
||||
TokioResolver::builder_with_config(config, TokioRuntimeProvider::default())
|
||||
.with_options(opts)
|
||||
.build()
|
||||
.context("failed to build DNS resolver")?,
|
||||
))
|
||||
}
|
||||
|
||||
fn build_doh_resolver(raw: &str) -> Result<DnsResolver, Error> {
|
||||
let url = url::Url::parse(raw).map_err(|e| Error::InvalidUrl(e.to_string()))?;
|
||||
if url.scheme() != "https" {
|
||||
return Err(anyhow::anyhow!("unsupported dns resolver scheme: {}", url.scheme()).into());
|
||||
}
|
||||
|
||||
let host = url
|
||||
.host_str()
|
||||
.with_context(|| format!("DoH resolver host is empty: {}", raw))?;
|
||||
let ips = bootstrap_ips_for_doh_host(host).with_context(|| {
|
||||
format!(
|
||||
"DoH resolver {} requires a known bootstrap IP; currently only dns.alidns.com or IP literals are supported",
|
||||
raw
|
||||
)
|
||||
})?;
|
||||
let port = url.port().unwrap_or(443);
|
||||
let server_name: Arc<str> = Arc::from(host.to_string());
|
||||
let http_endpoint: Option<Arc<str>> = match url.path() {
|
||||
"" | "/" | "/dns-query" => None,
|
||||
path => Some(Arc::from(path.to_string())),
|
||||
};
|
||||
|
||||
let name_servers = ips
|
||||
.into_iter()
|
||||
.map(|ip| {
|
||||
let mut connection =
|
||||
ConnectionConfig::https(server_name.clone(), http_endpoint.clone());
|
||||
connection.port = port;
|
||||
|
||||
NameServerConfig::new(ip, true, vec![connection])
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(DnsResolver::Hickory {
|
||||
uri: raw.to_string(),
|
||||
resolver: build_hickory_resolver(ResolverConfig::from_parts(
|
||||
None,
|
||||
Vec::new(),
|
||||
name_servers,
|
||||
))?,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_dns_resolver(raw: &str) -> Result<DnsResolver, Error> {
|
||||
if raw.eq_ignore_ascii_case(SYSTEM_DNS_RESOLVER) {
|
||||
return Ok(DnsResolver::System);
|
||||
}
|
||||
|
||||
build_doh_resolver(raw)
|
||||
}
|
||||
|
||||
fn build_dns_resolvers(raw_resolvers: &[String]) -> Result<Vec<DnsResolver>, Error> {
|
||||
let raw_resolvers = if raw_resolvers.is_empty() {
|
||||
get_default_dns_resolvers()
|
||||
} else {
|
||||
raw_resolvers.to_vec()
|
||||
};
|
||||
|
||||
raw_resolvers
|
||||
.iter()
|
||||
.map(|raw| build_dns_resolver(raw))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn validate_dns_resolvers(raw_resolvers: &[String]) -> Result<(), Error> {
|
||||
build_dns_resolvers(raw_resolvers).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn set_dns_resolvers(raw_resolvers: Vec<String>) -> Result<(), Error> {
|
||||
let resolvers = build_dns_resolvers(&raw_resolvers)?;
|
||||
*DNS_RESOLVERS.write().unwrap() = resolvers;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
static DNS_RESOLVERS: Lazy<RwLock<Vec<DnsResolver>>> =
|
||||
Lazy::new(|| RwLock::new(build_dns_resolvers(&get_default_dns_resolvers()).unwrap()));
|
||||
|
||||
fn configured_dns_resolvers() -> Vec<DnsResolver> {
|
||||
DNS_RESOLVERS.read().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn sanitize(name: impl AsRef<str>) -> String {
|
||||
let name = name.as_ref();
|
||||
let dot = name.ends_with('.');
|
||||
let mut name = idna::domain_to_ascii_cow(name.as_ref(), AsciiDenyList::EMPTY)
|
||||
.unwrap_or_default()
|
||||
.into_owned()
|
||||
.to_lowercase()
|
||||
.split('.')
|
||||
.map(|label| {
|
||||
label
|
||||
.chars()
|
||||
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
|
||||
.take(63)
|
||||
.collect::<String>()
|
||||
.trim_matches('-')
|
||||
.to_string()
|
||||
})
|
||||
.filter(|label| !label.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(".");
|
||||
name.truncate(253);
|
||||
if dot {
|
||||
name.push('.');
|
||||
}
|
||||
name
|
||||
}
|
||||
|
||||
pub fn parse(name: impl AsRef<str>) -> LowerName {
|
||||
let name = name.as_ref();
|
||||
if let Ok(name) = name.parse() {
|
||||
name
|
||||
} else {
|
||||
let sanitized = sanitize(name);
|
||||
tracing::debug!("invalid name: {}, sanitized to: {}", name, sanitized);
|
||||
sanitized.parse().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolver_conf() -> (ResolverConfig, ResolverOpts) {
|
||||
let mut config = ResolverConfig::default();
|
||||
for server in ["223.5.5.5", "180.184.1.1"] {
|
||||
config.add_name_server(NameServerConfig::new(
|
||||
server.parse().unwrap(),
|
||||
true,
|
||||
vec![ConnectionConfig::udp()],
|
||||
));
|
||||
}
|
||||
|
||||
let mut opts = ResolverOpts::default();
|
||||
|
||||
if let Ok((system_config, system_opts)) = read_system_conf() {
|
||||
for ns in system_config.name_servers() {
|
||||
config.add_name_server(ns.clone());
|
||||
}
|
||||
opts = system_opts;
|
||||
}
|
||||
|
||||
opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
|
||||
|
||||
(config, opts)
|
||||
}
|
||||
|
||||
static ALLOW_USE_SYSTEM_DNS_RESOLVER: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
static RESOLVER: Lazy<Arc<Resolver<TokioRuntimeProvider>>> = Lazy::new(|| {
|
||||
let (config, opts) = resolver_conf();
|
||||
let builder = TokioResolver::builder_with_config(config, TokioRuntimeProvider::default())
|
||||
.with_options(opts);
|
||||
Arc::new(
|
||||
builder
|
||||
.build()
|
||||
.expect("failed to initialize global DNS resolver"),
|
||||
)
|
||||
});
|
||||
|
||||
pub async fn txt_lookup(name: impl IntoName) -> Result<Vec<String>, Error> {
|
||||
let name = name.into_name().context("invalid txt record name")?;
|
||||
let mut last_err = None;
|
||||
|
||||
for resolver in configured_dns_resolvers() {
|
||||
let response = match resolver {
|
||||
DnsResolver::System => RESOLVER.txt_lookup(name.clone()).await,
|
||||
DnsResolver::Hickory { uri, resolver } => {
|
||||
let response = resolver.txt_lookup(name.clone()).await;
|
||||
if response.is_err() {
|
||||
tracing::debug!(?uri, ?name, "txt lookup failed with resolver");
|
||||
}
|
||||
response
|
||||
}
|
||||
};
|
||||
|
||||
let Ok(response) = response else {
|
||||
last_err = Some(anyhow::anyhow!("failed to lookup txt record").into());
|
||||
continue;
|
||||
};
|
||||
|
||||
let data = response
|
||||
.answers()
|
||||
.iter()
|
||||
.filter_map(|record| match record.data {
|
||||
RData::TXT(ref txt) => Some(txt.to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if data.is_empty() {
|
||||
last_err = Some(Error::NotFound);
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::info!(?data, "got txt record(s)");
|
||||
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
Err(last_err.unwrap_or(Error::NotFound))
|
||||
}
|
||||
|
||||
pub async fn txt_resolve(name: impl IntoName) -> Result<Vec<String>, Error> {
|
||||
Ok(txt_lookup(name)
|
||||
.await?
|
||||
.iter()
|
||||
.flat_map(|s| s.split_whitespace())
|
||||
.map(String::from)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn srv_lookup(name: impl IntoName) -> Result<Vec<SRV>, Error> {
|
||||
let name = name.into_name().context("invalid srv record name")?;
|
||||
let mut last_err = None;
|
||||
|
||||
for resolver in configured_dns_resolvers() {
|
||||
let response = match resolver {
|
||||
DnsResolver::System => RESOLVER.srv_lookup(name.clone()).await,
|
||||
DnsResolver::Hickory { uri, resolver } => {
|
||||
let response = resolver.srv_lookup(name.clone()).await;
|
||||
if response.is_err() {
|
||||
tracing::debug!(?uri, ?name, "srv lookup failed with resolver");
|
||||
}
|
||||
response
|
||||
}
|
||||
};
|
||||
|
||||
let Ok(response) = response else {
|
||||
last_err = Some(anyhow::anyhow!("failed to lookup srv record").into());
|
||||
continue;
|
||||
};
|
||||
|
||||
let data = response
|
||||
.answers()
|
||||
.iter()
|
||||
.filter_map(|record| match record.data {
|
||||
RData::SRV(ref srv) => Some(srv.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if data.is_empty() {
|
||||
last_err = Some(Error::NotFound);
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::info!(?data, "got srv record(s)");
|
||||
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
Err(last_err.unwrap_or(Error::NotFound))
|
||||
}
|
||||
|
||||
pub async fn resolve_host(host: &str, port: u16) -> Result<Vec<SocketAddr>, Error> {
|
||||
if let Ok(ip) = host.parse::<IpAddr>() {
|
||||
return Ok(vec![SocketAddr::new(ip, port)]);
|
||||
}
|
||||
|
||||
let mut last_err = None;
|
||||
for resolver in configured_dns_resolvers() {
|
||||
match resolver {
|
||||
DnsResolver::System => {
|
||||
if !ALLOW_USE_SYSTEM_DNS_RESOLVER.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match lookup_host(format!("{}:{}", host, port)).await {
|
||||
Ok(addrs) => {
|
||||
let addrs = addrs.collect::<Vec<_>>();
|
||||
if !addrs.is_empty() {
|
||||
tracing::debug!(?addrs, "system dns lookup done");
|
||||
return Ok(addrs);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::debug!(?error, "system dns lookup failed");
|
||||
last_err = Some(Error::from(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
DnsResolver::Hickory { uri, resolver } => match resolver.lookup_ip(host).await {
|
||||
Ok(lookup) => {
|
||||
let addrs = lookup
|
||||
.iter()
|
||||
.map(|ip| SocketAddr::new(ip, port))
|
||||
.collect::<Vec<_>>();
|
||||
if !addrs.is_empty() {
|
||||
return Ok(addrs);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::debug!(?uri, ?host, ?error, "hickory dns lookup failed");
|
||||
last_err = Some(
|
||||
anyhow::anyhow!(
|
||||
"hickory dns lookup_ip failed, host: {}, port: {}",
|
||||
host,
|
||||
port
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_err.unwrap_or(Error::NotFound))
|
||||
}
|
||||
|
||||
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()))?;
|
||||
|
||||
// see https://github.com/EasyTier/EasyTier/pull/947, https://github.com/EasyTier/EasyTier/pull/1700
|
||||
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
|
||||
if let Some(ip) = match host {
|
||||
url::Host::Ipv4(ip) => Some(ip.into()),
|
||||
url::Host::Ipv6(ip) => Some(ip.into()),
|
||||
_ => None,
|
||||
} {
|
||||
return Ok(vec![SocketAddr::new(ip, port)]);
|
||||
}
|
||||
|
||||
resolve_host(&host.to_string(), port).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use guarden::defer;
|
||||
|
||||
#[test]
|
||||
fn parse_matrix_cases() {
|
||||
let cases = [
|
||||
["Example.COM.", "example.com."],
|
||||
["a_b!.et.net.", "a-b.et.net."],
|
||||
["foo..bar.com.", "foo.bar.com."],
|
||||
[
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com.",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com.",
|
||||
],
|
||||
["___", "___"],
|
||||
["!", ""],
|
||||
["", ""],
|
||||
];
|
||||
|
||||
for [input, expected] in cases {
|
||||
let parsed = parse(input);
|
||||
let expected: LowerName = expected.parse().unwrap();
|
||||
assert_eq!(
|
||||
parsed, expected,
|
||||
"parse({input:?}) should equal {expected:?}, got {parsed:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_dns_resolver_is_system() {
|
||||
assert_eq!(get_default_dns_resolvers(), vec!["system".to_string()]);
|
||||
assert!(matches!(
|
||||
build_dns_resolvers(&get_default_dns_resolvers()).unwrap()[0],
|
||||
DnsResolver::System
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alidns_doh_resolver_is_supported() {
|
||||
validate_dns_resolvers(&["https://dns.alidns.com/dns-query".to_string()]).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_doh_resolver_requires_bootstrap_ip() {
|
||||
assert!(validate_dns_resolvers(&["https://example.com/dns-query".to_string()]).is_err());
|
||||
}
|
||||
|
||||
#[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,3 +1,5 @@
|
||||
pub mod dirty;
|
||||
pub mod dns;
|
||||
pub mod error;
|
||||
pub mod panic;
|
||||
pub mod string;
|
||||
@@ -22,6 +24,13 @@ pub fn weak_upgrade<T>(weak: &Weak<T>) -> anyhow::Result<Arc<T>> {
|
||||
.ok_or_else(|| anyhow::anyhow!("{} not available", std::any::type_name::<T>()))
|
||||
}
|
||||
|
||||
pub fn hostname() -> String {
|
||||
hostname::get()
|
||||
.unwrap_or_else(|_| "localhost".into())
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
pub trait BoxExt: Sized {
|
||||
fn boxed(self) -> Box<Self> {
|
||||
Box::new(self)
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::{
|
||||
instance_manager::{DaemonGuard, NetworkInstanceManager},
|
||||
proto::common::NatType,
|
||||
tunnel::{IpVersion, Tunnel, TunnelConnector, TunnelError, TunnelScheme},
|
||||
utils,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
@@ -282,11 +283,7 @@ pub async fn run_web_client(
|
||||
let mut flags = global_ctx.get_flags();
|
||||
flags.bind_device = false;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
let hostname = match hostname {
|
||||
None => gethostname::gethostname().to_string_lossy().to_string(),
|
||||
Some(hostname) => hostname,
|
||||
};
|
||||
let hostname = hostname.unwrap_or_else(utils::hostname);
|
||||
Ok(WebClient::new(
|
||||
ConfigServerConnector {
|
||||
url: c_url,
|
||||
|
||||
Reference in New Issue
Block a user