mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-20 03:22:05 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf0b2bcce8 | ||
|
|
3832020d50 | ||
|
|
ee9b51ff8a |
@@ -157,9 +157,6 @@ jobs:
|
||||
|
||||
- uses: mlugg/setup-zig@v2
|
||||
if: ${{ contains(matrix.OS, 'ubuntu') }}
|
||||
with:
|
||||
version: 0.16.0
|
||||
use-cache: true
|
||||
|
||||
- uses: taiki-e/install-action@v2
|
||||
if: ${{ contains(matrix.OS, 'ubuntu') }}
|
||||
@@ -230,7 +227,7 @@ jobs:
|
||||
*) UPX_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
UPX_VERSION=4.2.4
|
||||
UPX_VERSION=5.1.1
|
||||
UPX_PKG="upx-${UPX_VERSION}-${UPX_ARCH}_linux"
|
||||
curl -L "https://github.com/upx/upx/releases/download/v${UPX_VERSION}/${UPX_PKG}.tar.xz" -s | tar xJvf -
|
||||
cp "${UPX_PKG}/upx" .
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
image_tag:
|
||||
description: 'Tag for this image build'
|
||||
type: string
|
||||
default: 'v2.6.4'
|
||||
default: 'v2.6.2'
|
||||
required: true
|
||||
mark_latest:
|
||||
description: 'Mark this image as latest'
|
||||
|
||||
@@ -18,7 +18,7 @@ on:
|
||||
version:
|
||||
description: 'Version for this release'
|
||||
type: string
|
||||
default: 'v2.6.4'
|
||||
default: 'v2.6.2'
|
||||
required: true
|
||||
make_latest:
|
||||
description: 'Mark this release as latest'
|
||||
|
||||
Generated
+155
-231
@@ -915,7 +915,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@@ -1981,6 +1981,37 @@ 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"
|
||||
@@ -2198,7 +2229,7 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "easytier"
|
||||
version = "2.6.4"
|
||||
version = "2.6.2"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -2231,36 +2262,35 @@ dependencies = [
|
||||
"defguard_wireguard_rs",
|
||||
"delegate",
|
||||
"derivative",
|
||||
"derive_builder",
|
||||
"derive_more 2.1.1",
|
||||
"easytier-rpc-build",
|
||||
"encoding",
|
||||
"flume 0.12.0",
|
||||
"forwarded-header-value",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"getset",
|
||||
"gethostname 0.5.0",
|
||||
"git-version",
|
||||
"globwalk",
|
||||
"guarden",
|
||||
"hickory-net",
|
||||
"hickory-client",
|
||||
"hickory-proto",
|
||||
"hickory-resolver",
|
||||
"hickory-server",
|
||||
"hmac",
|
||||
"hostname",
|
||||
"http",
|
||||
"http_req",
|
||||
"humansize",
|
||||
"humantime-serde",
|
||||
"idna 1.0.3",
|
||||
"igd-next",
|
||||
"indexmap 2.14.0",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"kcp-sys",
|
||||
"machine-uid",
|
||||
"maplit",
|
||||
"mimalloc",
|
||||
"moka",
|
||||
"multimap",
|
||||
"natpmp",
|
||||
"netlink-packet-core",
|
||||
@@ -2271,7 +2301,6 @@ dependencies = [
|
||||
"nix 0.29.0",
|
||||
"once_cell",
|
||||
"openssl",
|
||||
"optionize",
|
||||
"ordered_hash_map",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
@@ -2279,8 +2308,7 @@ dependencies = [
|
||||
"petgraph 0.8.1",
|
||||
"pin-project-lite",
|
||||
"pnet",
|
||||
"prefix-trie 0.7.0",
|
||||
"proc-macro2",
|
||||
"prefix-trie",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-reflect",
|
||||
@@ -2290,7 +2318,6 @@ dependencies = [
|
||||
"prost-wkt-types",
|
||||
"quinn",
|
||||
"quinn-plaintext",
|
||||
"quote",
|
||||
"rand 0.8.5",
|
||||
"rcgen",
|
||||
"regex",
|
||||
@@ -2303,7 +2330,6 @@ dependencies = [
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"serial_test",
|
||||
"service-manager",
|
||||
"sha2",
|
||||
@@ -2331,6 +2357,7 @@ dependencies = [
|
||||
"tokio-util",
|
||||
"tokio-websockets",
|
||||
"toml 0.8.19",
|
||||
"tonic-build",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tun-easytier",
|
||||
@@ -2357,7 +2384,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"easytier",
|
||||
"jni 0.21.1",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"serde",
|
||||
@@ -2378,7 +2405,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "easytier-gui"
|
||||
version = "2.6.4"
|
||||
version = "2.6.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -2386,7 +2413,7 @@ dependencies = [
|
||||
"dashmap",
|
||||
"dunce",
|
||||
"easytier",
|
||||
"gethostname",
|
||||
"gethostname 1.1.0",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"security-framework-sys",
|
||||
@@ -2409,6 +2436,14 @@ dependencies = [
|
||||
"windows 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easytier-rpc-build"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"prost-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easytier-uptime"
|
||||
version = "0.1.0"
|
||||
@@ -2451,7 +2486,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "easytier-web"
|
||||
version = "2.6.4"
|
||||
version = "2.6.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -2673,6 +2708,24 @@ 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"
|
||||
@@ -3254,6 +3307,16 @@ 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"
|
||||
@@ -3316,18 +3379,6 @@ 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"
|
||||
@@ -3691,103 +3742,88 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hickory-net"
|
||||
version = "0.26.1"
|
||||
name = "hickory-client"
|
||||
version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183"
|
||||
checksum = "c466cd63a4217d5b2b8e32f23f58312741ce96e3c84bf7438677d2baff0fc555"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"cfg-if",
|
||||
"data-encoding",
|
||||
"futures-channel",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"hickory-proto",
|
||||
"http",
|
||||
"idna 1.0.3",
|
||||
"ipnet",
|
||||
"jni 0.22.4",
|
||||
"rand 0.10.1",
|
||||
"rustls",
|
||||
"serde",
|
||||
"once_cell",
|
||||
"radix_trie",
|
||||
"rand 0.9.1",
|
||||
"thiserror 2.0.11",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tracing",
|
||||
"url",
|
||||
"webpki-roots 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hickory-proto"
|
||||
version = "0.26.1"
|
||||
version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643"
|
||||
checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"cfg-if",
|
||||
"data-encoding",
|
||||
"enum-as-inner",
|
||||
"futures-channel",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"idna 1.0.3",
|
||||
"ipnet",
|
||||
"jni 0.22.4",
|
||||
"once_cell",
|
||||
"prefix-trie 0.8.4",
|
||||
"rand 0.10.1",
|
||||
"rand 0.9.1",
|
||||
"ring",
|
||||
"serde",
|
||||
"thiserror 2.0.11",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hickory-resolver"
|
||||
version = "0.26.1"
|
||||
version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c"
|
||||
checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"hickory-net",
|
||||
"hickory-proto",
|
||||
"ipconfig",
|
||||
"ipnet",
|
||||
"jni 0.22.4",
|
||||
"moka",
|
||||
"ndk-context",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"rand 0.10.1",
|
||||
"rand 0.9.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.26.1"
|
||||
version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "130236ba6abba90da6a7acf7a87b27d862b592c3145dc74bc47bf86d8ff198ec"
|
||||
checksum = "d53e5fe811b941c74ee46b8818228bfd2bc2688ba276a0eaeb0f2c95ea3b2585"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"cfg-if",
|
||||
"data-encoding",
|
||||
"enum-as-inner",
|
||||
"futures-util",
|
||||
"hickory-net",
|
||||
"hickory-proto",
|
||||
"hickory-resolver",
|
||||
"ipnet",
|
||||
"prefix-trie 0.8.4",
|
||||
"prefix-trie",
|
||||
"serde",
|
||||
"thiserror 2.0.11",
|
||||
"time",
|
||||
@@ -3823,17 +3859,6 @@ 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"
|
||||
@@ -4019,7 +4044,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.3",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -4531,68 +4556,19 @@ dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys 0.3.0",
|
||||
"jni-sys",
|
||||
"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"
|
||||
@@ -5127,12 +5103,9 @@ version = "0.12.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"crossbeam-channel",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
"event-listener",
|
||||
"futures-util",
|
||||
"loom",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
@@ -5234,7 +5207,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"jni-sys 0.3.0",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"ndk-sys",
|
||||
"num_enum",
|
||||
@@ -5254,7 +5227,7 @@ version = "0.6.0+11769913"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
|
||||
dependencies = [
|
||||
"jni-sys 0.3.0",
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5270,7 +5243,7 @@ dependencies = [
|
||||
"netlink-packet-route 0.17.1",
|
||||
"netlink-sys",
|
||||
"once_cell",
|
||||
"system-configuration 0.6.1",
|
||||
"system-configuration",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -5381,6 +5354,15 @@ 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"
|
||||
@@ -5599,7 +5581,7 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||
dependencies = [
|
||||
"proc-macro-crate 2.0.0",
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@@ -5950,30 +5932,6 @@ 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"
|
||||
@@ -6703,17 +6661,6 @@ 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"
|
||||
@@ -6754,11 +6701,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
|
||||
dependencies = [
|
||||
"toml_edit 0.25.12+spec-1.1.0",
|
||||
"toml_edit 0.22.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7111,6 +7058,16 @@ 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"
|
||||
@@ -7437,7 +7394,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration 0.6.1",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
@@ -7612,7 +7569,7 @@ checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"glob",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
@@ -7775,7 +7732,6 @@ version = "0.23.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -7822,7 +7778,7 @@ checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.0",
|
||||
"core-foundation-sys",
|
||||
"jni 0.21.1",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
@@ -8621,16 +8577,6 @@ 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"
|
||||
@@ -9190,17 +9136,6 @@ 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"
|
||||
@@ -9270,7 +9205,7 @@ dependencies = [
|
||||
"gdkwayland-sys",
|
||||
"gdkx11-sys",
|
||||
"gtk",
|
||||
"jni 0.21.1",
|
||||
"jni",
|
||||
"libc",
|
||||
"log",
|
||||
"ndk",
|
||||
@@ -9332,7 +9267,7 @@ dependencies = [
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"image 0.25.10",
|
||||
"jni 0.21.1",
|
||||
"jni",
|
||||
"libc",
|
||||
"log",
|
||||
"mime",
|
||||
@@ -9467,7 +9402,7 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997"
|
||||
dependencies = [
|
||||
"gethostname",
|
||||
"gethostname 1.1.0",
|
||||
"log",
|
||||
"os_info",
|
||||
"serde",
|
||||
@@ -9560,7 +9495,7 @@ dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http",
|
||||
"jni 0.21.1",
|
||||
"jni",
|
||||
"objc2",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
@@ -9583,7 +9518,7 @@ checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"jni 0.21.1",
|
||||
"jni",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
@@ -10034,15 +9969,6 @@ 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"
|
||||
@@ -10080,18 +10006,6 @@ 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"
|
||||
@@ -10137,6 +10051,19 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic-build"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "568392c5a2bd0020723e3f387891176aabafe36fd9fcd074ad309dfa0c8eb964"
|
||||
dependencies = [
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"prost-build",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
@@ -11939,9 +11866,6 @@ name = "winnow"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
@@ -12143,7 +12067,7 @@ dependencies = [
|
||||
"html5ever",
|
||||
"http",
|
||||
"javascriptcore-rs",
|
||||
"jni 0.21.1",
|
||||
"jni",
|
||||
"kuchikiki",
|
||||
"libc",
|
||||
"ndk",
|
||||
@@ -12206,7 +12130,7 @@ version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
|
||||
dependencies = [
|
||||
"gethostname",
|
||||
"gethostname 1.1.0",
|
||||
"rustix 1.0.7",
|
||||
"x11rb-protocol",
|
||||
]
|
||||
@@ -12323,7 +12247,7 @@ version = "5.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@@ -12560,7 +12484,7 @@ version = "5.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
|
||||
@@ -3,6 +3,7 @@ resolver = "2"
|
||||
members = [
|
||||
"easytier",
|
||||
"easytier-gui/src-tauri",
|
||||
"easytier-rpc-build",
|
||||
"easytier-web",
|
||||
"easytier-contrib/easytier-ffi",
|
||||
"easytier-contrib/easytier-uptime",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
id=easytier_magisk
|
||||
name=EasyTier_Magisk
|
||||
version=v2.6.4
|
||||
version=v2.6.2
|
||||
versionCode=1
|
||||
author=EasyTier
|
||||
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
|
||||
|
||||
+130
-542
File diff suppressed because it is too large
Load Diff
@@ -7,10 +7,6 @@ edition = "2024"
|
||||
crate-type=["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
base64 = "0.22"
|
||||
flate2 = "1.1"
|
||||
gethostname = "1.1"
|
||||
ohos-hilog-binding = {version = "*", features = ["redirect"]}
|
||||
easytier = { path = "../../easytier" }
|
||||
napi-derive-ohos = "1.1"
|
||||
@@ -30,16 +26,10 @@ napi-ohos = { version = "1.1", default-features = false, features = [
|
||||
"web_stream",
|
||||
] }
|
||||
once_cell = "1.21.3"
|
||||
ipnet = "2.10"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.125"
|
||||
prost-reflect = { version = "0.14.5", default-features = false, features = ["derive"] }
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
tracing-subscriber = "0.3.19"
|
||||
tracing-core = "0.1.33"
|
||||
tracing = "0.1.41"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
|
||||
url = "2.5"
|
||||
uuid = { version = "1.5.0", features = [
|
||||
"v4",
|
||||
"fast-rng",
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
pub(crate) mod repository;
|
||||
pub(crate) mod services;
|
||||
pub(crate) mod storage;
|
||||
pub(crate) mod types;
|
||||
@@ -1,13 +0,0 @@
|
||||
#[path = "../../config_repo/field_store.rs"]
|
||||
mod field_store;
|
||||
#[path = "../../config_repo/import_export.rs"]
|
||||
mod import_export;
|
||||
#[path = "../../config_repo/legacy_migration.rs"]
|
||||
mod legacy_migration;
|
||||
#[path = "../../config_repo/validation.rs"]
|
||||
mod validation;
|
||||
|
||||
#[path = "../../config_repo.rs"]
|
||||
mod repo;
|
||||
|
||||
pub use repo::*;
|
||||
@@ -1,2 +0,0 @@
|
||||
pub(crate) mod schema_service;
|
||||
pub(crate) mod share_link_service;
|
||||
@@ -1,414 +0,0 @@
|
||||
use easytier::proto::ALL_DESCRIPTOR_BYTES;
|
||||
use napi_derive_ohos::napi;
|
||||
use once_cell::sync::Lazy;
|
||||
use prost_reflect::{Cardinality, DescriptorPool, FieldDescriptor, Kind, MessageDescriptor};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[napi(object)]
|
||||
pub struct FieldOption {
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[napi(object)]
|
||||
pub struct ValidationRule {
|
||||
pub rule_type: String,
|
||||
pub arg: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[napi(object)]
|
||||
pub struct NetworkConfigSchema {
|
||||
pub node_kind: String,
|
||||
pub name: String,
|
||||
pub field_number: i32,
|
||||
pub type_name: Option<String>,
|
||||
pub semantic_type: Option<String>,
|
||||
pub value_kind: String,
|
||||
pub is_list: bool,
|
||||
pub required: bool,
|
||||
pub default_value_text: Option<String>,
|
||||
pub enum_options: Vec<FieldOption>,
|
||||
pub validations: Vec<ValidationRule>,
|
||||
pub children: Vec<NetworkConfigSchema>,
|
||||
pub definitions: Vec<NetworkConfigSchema>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[napi(object)]
|
||||
pub struct ConfigFieldMapping {
|
||||
pub field_name: String,
|
||||
pub field_number: i32,
|
||||
}
|
||||
|
||||
static DESCRIPTOR_POOL: Lazy<DescriptorPool> = Lazy::new(|| {
|
||||
DescriptorPool::decode(ALL_DESCRIPTOR_BYTES)
|
||||
.expect("easytier descriptor pool should decode from embedded protobuf descriptors")
|
||||
});
|
||||
|
||||
const NETWORK_CONFIG_MESSAGE_NAME: &str = "api.manage.NetworkConfig";
|
||||
|
||||
fn descriptor_pool() -> &'static DescriptorPool {
|
||||
&DESCRIPTOR_POOL
|
||||
}
|
||||
|
||||
fn network_config_descriptor() -> MessageDescriptor {
|
||||
descriptor_pool()
|
||||
.get_message_by_name(NETWORK_CONFIG_MESSAGE_NAME)
|
||||
.expect("api.manage.NetworkConfig descriptor should exist")
|
||||
}
|
||||
|
||||
fn field_default_value_text(field: &FieldDescriptor) -> Option<String> {
|
||||
if field.is_list() || field.is_map() {
|
||||
return Some("[]".to_string());
|
||||
}
|
||||
|
||||
match field.kind() {
|
||||
Kind::Bool => Some("false".to_string()),
|
||||
Kind::String => Some("\"\"".to_string()),
|
||||
Kind::Bytes => Some("\"\"".to_string()),
|
||||
Kind::Int32
|
||||
| Kind::Sint32
|
||||
| Kind::Sfixed32
|
||||
| Kind::Int64
|
||||
| Kind::Sint64
|
||||
| Kind::Sfixed64
|
||||
| Kind::Uint32
|
||||
| Kind::Fixed32
|
||||
| Kind::Uint64
|
||||
| Kind::Fixed64
|
||||
| Kind::Float
|
||||
| Kind::Double => Some("0".to_string()),
|
||||
Kind::Enum(enum_desc) => enum_desc
|
||||
.get_value(0)
|
||||
.map(|value| value.number().to_string()),
|
||||
Kind::Message(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn field_type_name(field: &FieldDescriptor) -> Option<String> {
|
||||
match field.kind() {
|
||||
Kind::Enum(enum_desc) => Some(enum_desc.full_name().to_string()),
|
||||
Kind::Message(message_desc) => Some(message_desc.full_name().to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn field_semantic_type(field: &FieldDescriptor) -> Option<String> {
|
||||
match field.name() {
|
||||
"virtual_ipv4" => Some("cidr_ip".to_string()),
|
||||
"network_length" => Some("cidr_mask".to_string()),
|
||||
"peer_urls" => Some("peer[]".to_string()),
|
||||
"proxy_cidrs" => Some("cidr[]".to_string()),
|
||||
"listener_urls" => Some("listener[]".to_string()),
|
||||
"routes" => Some("route[]".to_string()),
|
||||
"exit_nodes" => Some("ip[]".to_string()),
|
||||
"relay_network_whitelist" => Some("network_name[]".to_string()),
|
||||
"mapped_listeners" => Some("mapped_listener[]".to_string()),
|
||||
"port_forwards" => Some("port_forward[]".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn enum_options(kind: Kind) -> Vec<FieldOption> {
|
||||
match kind {
|
||||
Kind::Enum(enum_desc) => enum_desc
|
||||
.values()
|
||||
.map(|value| FieldOption {
|
||||
label: value.name().to_string(),
|
||||
value: value.number().to_string(),
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn should_expose_field(field: &FieldDescriptor) -> bool {
|
||||
match field.containing_oneof() {
|
||||
Some(_) => field
|
||||
.field_descriptor_proto()
|
||||
.proto3_optional
|
||||
.unwrap_or(false),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_validations(field: &FieldDescriptor) -> Vec<ValidationRule> {
|
||||
if field.cardinality() == Cardinality::Required {
|
||||
return vec![ValidationRule {
|
||||
rule_type: "required".to_string(),
|
||||
arg: String::new(),
|
||||
message: format!("{} is required", field.name()),
|
||||
}];
|
||||
}
|
||||
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn kind_to_value_kind(field: &FieldDescriptor) -> String {
|
||||
if field.is_map() {
|
||||
return "object".to_string();
|
||||
}
|
||||
|
||||
match field.kind() {
|
||||
Kind::Bool => "boolean".to_string(),
|
||||
Kind::String | Kind::Bytes => "string".to_string(),
|
||||
Kind::Int32
|
||||
| Kind::Sint32
|
||||
| Kind::Sfixed32
|
||||
| Kind::Int64
|
||||
| Kind::Sint64
|
||||
| Kind::Sfixed64
|
||||
| Kind::Uint32
|
||||
| Kind::Fixed32
|
||||
| Kind::Uint64
|
||||
| Kind::Fixed64
|
||||
| Kind::Float
|
||||
| Kind::Double => "number".to_string(),
|
||||
Kind::Enum(_) => "enum".to_string(),
|
||||
Kind::Message(_) => "object".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_node(
|
||||
node_kind: &str,
|
||||
name: String,
|
||||
field_number: i32,
|
||||
type_name: Option<String>,
|
||||
semantic_type: Option<String>,
|
||||
value_kind: String,
|
||||
is_list: bool,
|
||||
required: bool,
|
||||
default_value_text: Option<String>,
|
||||
enum_options: Vec<FieldOption>,
|
||||
validations: Vec<ValidationRule>,
|
||||
children: Vec<NetworkConfigSchema>,
|
||||
definitions: Vec<NetworkConfigSchema>,
|
||||
) -> NetworkConfigSchema {
|
||||
NetworkConfigSchema {
|
||||
node_kind: node_kind.to_string(),
|
||||
name,
|
||||
field_number,
|
||||
type_name,
|
||||
semantic_type,
|
||||
value_kind,
|
||||
is_list,
|
||||
required,
|
||||
default_value_text,
|
||||
enum_options,
|
||||
validations,
|
||||
children,
|
||||
definitions,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_map_entry_node(message_desc: &MessageDescriptor) -> NetworkConfigSchema {
|
||||
let key_field = message_desc.map_entry_key_field();
|
||||
let value_field = message_desc.map_entry_value_field();
|
||||
|
||||
build_node(
|
||||
"object",
|
||||
message_desc.name().to_string(),
|
||||
0,
|
||||
Some(message_desc.full_name().to_string()),
|
||||
None,
|
||||
"object".to_string(),
|
||||
false,
|
||||
true,
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
vec![
|
||||
build_schema_field_node(&key_field),
|
||||
build_schema_field_node(&value_field),
|
||||
],
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
|
||||
fn field_children(field: &FieldDescriptor) -> Vec<NetworkConfigSchema> {
|
||||
if field.is_map() {
|
||||
if let Kind::Message(message_desc) = field.kind() {
|
||||
return vec![build_map_entry_node(&message_desc)];
|
||||
}
|
||||
}
|
||||
|
||||
match field.kind() {
|
||||
Kind::Message(message_desc) => build_message_children(&message_desc),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_message_children(message_desc: &MessageDescriptor) -> Vec<NetworkConfigSchema> {
|
||||
message_desc
|
||||
.fields()
|
||||
.filter(should_expose_field)
|
||||
.map(|field| build_schema_field_node(&field))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_schema_field_node(field: &FieldDescriptor) -> NetworkConfigSchema {
|
||||
build_node(
|
||||
"field",
|
||||
field.name().to_string(),
|
||||
field.number() as i32,
|
||||
field_type_name(field),
|
||||
field_semantic_type(field),
|
||||
kind_to_value_kind(field),
|
||||
field.is_list() || field.is_map(),
|
||||
field.cardinality() == Cardinality::Required,
|
||||
field_default_value_text(field),
|
||||
enum_options(field.kind()),
|
||||
build_validations(field),
|
||||
field_children(field),
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
|
||||
fn collect_definitions() -> Vec<NetworkConfigSchema> {
|
||||
let mut definitions = Vec::new();
|
||||
|
||||
for message_desc in descriptor_pool().all_messages() {
|
||||
let full_name = message_desc.full_name();
|
||||
if full_name == NETWORK_CONFIG_MESSAGE_NAME || message_desc.is_map_entry() {
|
||||
continue;
|
||||
}
|
||||
|
||||
definitions.push(build_node(
|
||||
"object",
|
||||
full_name.to_string(),
|
||||
0,
|
||||
Some(full_name.to_string()),
|
||||
None,
|
||||
"object".to_string(),
|
||||
false,
|
||||
true,
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
build_message_children(&message_desc),
|
||||
Vec::new(),
|
||||
));
|
||||
}
|
||||
|
||||
for enum_desc in descriptor_pool().all_enums() {
|
||||
definitions.push(build_node(
|
||||
"enum",
|
||||
enum_desc.full_name().to_string(),
|
||||
0,
|
||||
Some(enum_desc.full_name().to_string()),
|
||||
None,
|
||||
"enum".to_string(),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
enum_options(Kind::Enum(enum_desc.clone())),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
));
|
||||
}
|
||||
|
||||
definitions.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
definitions
|
||||
}
|
||||
|
||||
fn build_network_config_schema() -> NetworkConfigSchema {
|
||||
let network_config = network_config_descriptor();
|
||||
build_node(
|
||||
"schema",
|
||||
network_config.name().to_string(),
|
||||
0,
|
||||
Some(network_config.full_name().to_string()),
|
||||
None,
|
||||
"object".to_string(),
|
||||
false,
|
||||
true,
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
build_message_children(&network_config),
|
||||
collect_definitions(),
|
||||
)
|
||||
}
|
||||
|
||||
fn build_network_config_field_mappings() -> Vec<ConfigFieldMapping> {
|
||||
network_config_descriptor()
|
||||
.fields()
|
||||
.filter(should_expose_field)
|
||||
.map(|field| ConfigFieldMapping {
|
||||
field_name: field.name().to_string(),
|
||||
field_number: field.number() as i32,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_network_config_schema() -> NetworkConfigSchema {
|
||||
build_network_config_schema()
|
||||
}
|
||||
|
||||
pub fn get_network_config_field_mappings() -> Vec<ConfigFieldMapping> {
|
||||
build_network_config_field_mappings()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn schema_is_exposed_as_single_tree_type() {
|
||||
let schema = get_network_config_schema();
|
||||
assert_eq!(schema.node_kind, "schema");
|
||||
assert_eq!(schema.name, "NetworkConfig");
|
||||
assert_eq!(
|
||||
schema.type_name.as_deref(),
|
||||
Some("api.manage.NetworkConfig")
|
||||
);
|
||||
|
||||
let virtual_ipv4 = schema
|
||||
.children
|
||||
.iter()
|
||||
.find(|field| field.name == "virtual_ipv4")
|
||||
.expect("virtual_ipv4 field");
|
||||
assert_eq!(virtual_ipv4.semantic_type.as_deref(), Some("cidr_ip"));
|
||||
|
||||
let secure_mode = schema
|
||||
.children
|
||||
.iter()
|
||||
.find(|field| field.name == "secure_mode")
|
||||
.expect("secure_mode field");
|
||||
assert!(
|
||||
secure_mode
|
||||
.children
|
||||
.iter()
|
||||
.any(|field| field.name == "enabled")
|
||||
);
|
||||
|
||||
let secure_mode_definition = schema
|
||||
.definitions
|
||||
.iter()
|
||||
.find(|definition| definition.name == "common.SecureModeConfig")
|
||||
.expect("secure mode definition");
|
||||
assert!(
|
||||
secure_mode_definition
|
||||
.children
|
||||
.iter()
|
||||
.any(|field| field.name == "local_private_key")
|
||||
);
|
||||
|
||||
let networking_method_definition = schema
|
||||
.definitions
|
||||
.iter()
|
||||
.find(|definition| definition.name == "api.manage.NetworkingMethod")
|
||||
.expect("networking method enum definition");
|
||||
assert!(
|
||||
networking_method_definition
|
||||
.enum_options
|
||||
.iter()
|
||||
.any(|option| option.label == "PublicServer")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
use crate::config::repository::{get_config_record, save_config_record};
|
||||
use crate::config::services::schema_service::get_network_config_field_mappings;
|
||||
use crate::config::types::stored_config::SharedConfigLinkPayload;
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use flate2::{Compression, read::ZlibDecoder, write::ZlibEncoder};
|
||||
use gethostname::gethostname;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Write};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
const SHARE_LINK_HOST: &str = "easytier.cn";
|
||||
const SHARE_LINK_PATH: &str = "/comp_cfg";
|
||||
|
||||
fn field_name_to_id_map() -> HashMap<String, String> {
|
||||
get_network_config_field_mappings()
|
||||
.into_iter()
|
||||
.map(|mapping| (mapping.field_name, mapping.field_number.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn field_id_to_name_map() -> HashMap<String, String> {
|
||||
get_network_config_field_mappings()
|
||||
.into_iter()
|
||||
.map(|mapping| (mapping.field_number.to_string(), mapping.field_name))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn prune_empty(value: &serde_json::Value) -> Option<serde_json::Value> {
|
||||
match value {
|
||||
serde_json::Value::Null => None,
|
||||
serde_json::Value::Array(values) if values.is_empty() => None,
|
||||
_ => Some(value.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_config_json(config: &NetworkConfig) -> Result<String, String> {
|
||||
let field_name_to_id = field_name_to_id_map();
|
||||
let raw = serde_json::to_value(config).map_err(|err| err.to_string())?;
|
||||
let mut mapped = serde_json::Map::new();
|
||||
|
||||
for (key, value) in raw.as_object().cloned().unwrap_or_default() {
|
||||
let Some(value) = prune_empty(&value) else {
|
||||
continue;
|
||||
};
|
||||
let mapped_key = field_name_to_id.get(&key).cloned().unwrap_or(key);
|
||||
mapped.insert(mapped_key, value);
|
||||
}
|
||||
|
||||
serde_json::to_string(&mapped).map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
fn unmap_config_json(raw: &str) -> Result<NetworkConfig, String> {
|
||||
let field_id_to_name = field_id_to_name_map();
|
||||
let value = serde_json::from_str::<serde_json::Value>(raw).map_err(|err| err.to_string())?;
|
||||
let mut mapped = serde_json::Map::new();
|
||||
for (key, value) in value.as_object().cloned().unwrap_or_default() {
|
||||
let field_name = field_id_to_name.get(&key).cloned().unwrap_or(key);
|
||||
mapped.insert(field_name, value);
|
||||
}
|
||||
serde_json::from_value(serde_json::Value::Object(mapped)).map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
fn compress_to_base64url(raw: &str) -> Result<String, String> {
|
||||
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
|
||||
encoder
|
||||
.write_all(raw.as_bytes())
|
||||
.map_err(|err| err.to_string())?;
|
||||
let compressed = encoder.finish().map_err(|err| err.to_string())?;
|
||||
Ok(URL_SAFE_NO_PAD.encode(compressed))
|
||||
}
|
||||
|
||||
fn decompress_from_base64url(raw: &str) -> Result<String, String> {
|
||||
let compressed = URL_SAFE_NO_PAD.decode(raw).map_err(|err| err.to_string())?;
|
||||
let mut decoder = ZlibDecoder::new(compressed.as_slice());
|
||||
let mut out = String::new();
|
||||
decoder
|
||||
.read_to_string(&mut out)
|
||||
.map_err(|err| err.to_string())?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn build_config_share_link(
|
||||
config_id: &str,
|
||||
display_name: Option<String>,
|
||||
only_start: bool,
|
||||
) -> Option<String> {
|
||||
let record = get_config_record(config_id)?;
|
||||
let config = serde_json::from_str::<NetworkConfig>(&record.config_json).ok()?;
|
||||
let mapped_json = map_config_json(&config).ok()?;
|
||||
let compressed = compress_to_base64url(&mapped_json).ok()?;
|
||||
let final_name = display_name
|
||||
.or(Some(record.meta.display_name))
|
||||
.filter(|name| !name.is_empty());
|
||||
|
||||
let mut url = Url::parse(&format!("https://{SHARE_LINK_HOST}{SHARE_LINK_PATH}")).ok()?;
|
||||
url.query_pairs_mut().append_pair("cfg", &compressed);
|
||||
if let Some(name) = final_name {
|
||||
url.query_pairs_mut().append_pair("name", &name);
|
||||
}
|
||||
if only_start {
|
||||
url.query_pairs_mut().append_pair("only_start", "true");
|
||||
}
|
||||
Some(url.to_string())
|
||||
}
|
||||
|
||||
pub fn parse_config_share_link(share_link: &str) -> Option<SharedConfigLinkPayload> {
|
||||
let url = Url::parse(share_link).ok()?;
|
||||
if url.host_str()? != SHARE_LINK_HOST || url.path() != SHARE_LINK_PATH {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cfg = url
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "cfg")?
|
||||
.1
|
||||
.to_string();
|
||||
let mapped_json = decompress_from_base64url(&cfg).ok()?;
|
||||
let mut config = unmap_config_json(&mapped_json).ok()?;
|
||||
config.instance_id = Some(Uuid::new_v4().to_string());
|
||||
let hostname = gethostname().to_string_lossy().to_string();
|
||||
if !hostname.is_empty() {
|
||||
config.hostname = Some(hostname);
|
||||
}
|
||||
|
||||
let config_json = serde_json::to_string(&config).ok()?;
|
||||
let display_name = url
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "name")
|
||||
.map(|(_, value)| value.to_string())
|
||||
.filter(|name| !name.is_empty());
|
||||
let only_start = url
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "only_start")
|
||||
.map(|(_, value)| value == "true")
|
||||
.unwrap_or(false);
|
||||
|
||||
Some(SharedConfigLinkPayload {
|
||||
config_json,
|
||||
display_name,
|
||||
only_start,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn import_config_share_link(
|
||||
share_link: &str,
|
||||
display_name_override: Option<String>,
|
||||
) -> Option<String> {
|
||||
let payload = parse_config_share_link(share_link)?;
|
||||
let config = serde_json::from_str::<NetworkConfig>(&payload.config_json).ok()?;
|
||||
let config_id = config.instance_id.clone()?;
|
||||
let display_name = display_name_override
|
||||
.filter(|name| !name.is_empty())
|
||||
.or(payload.display_name)
|
||||
.unwrap_or_else(|| config_id.clone());
|
||||
|
||||
save_config_record(config_id.clone(), display_name, payload.config_json)?;
|
||||
Some(config_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config_repo::{create_config_record, init_config_store};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn test_root() -> String {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
std::env::temp_dir()
|
||||
.join(format!("easytier_ohrs_share_test_{unique}"))
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn share_link_roundtrip_works() {
|
||||
assert!(init_config_store(test_root()));
|
||||
create_config_record("cfg-share".to_string(), "share-demo".to_string())
|
||||
.expect("create config");
|
||||
|
||||
let link = build_config_share_link("cfg-share", None, true).expect("share link");
|
||||
let payload = parse_config_share_link(&link).expect("parse link");
|
||||
let config =
|
||||
serde_json::from_str::<NetworkConfig>(&payload.config_json).expect("config json");
|
||||
|
||||
assert!(payload.only_start);
|
||||
assert_eq!(payload.display_name.as_deref(), Some("share-demo"));
|
||||
assert_ne!(config.instance_id.as_deref(), Some("cfg-share"));
|
||||
|
||||
let imported_id = import_config_share_link(&link, None).expect("import link");
|
||||
assert_ne!(imported_id, "cfg-share");
|
||||
}
|
||||
}
|
||||
@@ -1,333 +0,0 @@
|
||||
use crate::config::types::stored_config::{StoredConfigList, StoredConfigMeta};
|
||||
use ohos_hilog_binding::{hilog_debug, hilog_error};
|
||||
use rusqlite::{Connection, OptionalExtension, params};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
static CONFIG_DB_PATH: Mutex<Option<PathBuf>> = Mutex::new(None);
|
||||
const CONFIG_DB_FILE_NAME: &str = "easytier-config-store.db";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct StoredConfigMetaRecord {
|
||||
config_id: String,
|
||||
display_name: String,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
favorite: bool,
|
||||
temporary: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn now_ts_string() -> String {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs().to_string())
|
||||
.unwrap_or_else(|_| "0".to_string())
|
||||
}
|
||||
|
||||
fn db_file_path() -> Option<PathBuf> {
|
||||
CONFIG_DB_PATH
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.as_ref().cloned())
|
||||
}
|
||||
|
||||
fn init_schema(conn: &Connection) -> rusqlite::Result<()> {
|
||||
conn.execute_batch(
|
||||
"PRAGMA foreign_keys = ON;
|
||||
CREATE TABLE IF NOT EXISTS stored_configs (
|
||||
config_id TEXT PRIMARY KEY,
|
||||
display_name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
favorite INTEGER NOT NULL DEFAULT 0,
|
||||
temporary INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS stored_config_fields (
|
||||
config_id TEXT NOT NULL,
|
||||
field_name TEXT NOT NULL,
|
||||
field_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (config_id, field_name),
|
||||
FOREIGN KEY (config_id) REFERENCES stored_configs(config_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_stored_config_fields_config_id
|
||||
ON stored_config_fields(config_id);",
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn open_db() -> Option<Connection> {
|
||||
let path = db_file_path()?;
|
||||
let conn = match Connection::open(&path) {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] failed to open config db {}: {}", path.display(), e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = init_schema(&conn) {
|
||||
hilog_error!(
|
||||
"[Rust] failed to initialize config db {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(conn)
|
||||
}
|
||||
|
||||
fn row_to_meta(row: &rusqlite::Row<'_>) -> rusqlite::Result<StoredConfigMetaRecord> {
|
||||
Ok(StoredConfigMetaRecord {
|
||||
config_id: row.get(0)?,
|
||||
display_name: row.get(1)?,
|
||||
created_at: row.get(2)?,
|
||||
updated_at: row.get(3)?,
|
||||
favorite: row.get::<_, i64>(4)? != 0,
|
||||
temporary: row.get::<_, i64>(5)? != 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_meta_record(conn: &Connection, config_id: &str) -> Option<StoredConfigMetaRecord> {
|
||||
conn.query_row(
|
||||
"SELECT config_id, display_name, created_at, updated_at, favorite, temporary
|
||||
FROM stored_configs WHERE config_id = ?1",
|
||||
params![config_id],
|
||||
row_to_meta,
|
||||
)
|
||||
.optional()
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn to_meta(record: StoredConfigMetaRecord) -> StoredConfigMeta {
|
||||
StoredConfigMeta {
|
||||
config_id: record.config_id,
|
||||
display_name: record.display_name,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
favorite: record.favorite,
|
||||
temporary: record.temporary,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_config_meta_store(root_dir: String) -> bool {
|
||||
let root = PathBuf::from(root_dir);
|
||||
if let Err(e) = std::fs::create_dir_all(&root) {
|
||||
hilog_error!(
|
||||
"[Rust] failed to create config db dir {}: {}",
|
||||
root.display(),
|
||||
e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
let db_path = root.join(CONFIG_DB_FILE_NAME);
|
||||
match CONFIG_DB_PATH.lock() {
|
||||
Ok(mut guard) => {
|
||||
*guard = Some(db_path.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] failed to lock config db path: {}", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if open_db().is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
hilog_debug!("[Rust] initialized config db at {}", db_path.display());
|
||||
true
|
||||
}
|
||||
|
||||
pub fn list_config_meta_entries() -> StoredConfigList {
|
||||
let Some(conn) = open_db() else {
|
||||
return StoredConfigList { configs: vec![] };
|
||||
};
|
||||
|
||||
let mut stmt = match conn.prepare(
|
||||
"SELECT config_id, display_name, created_at, updated_at, favorite, temporary
|
||||
FROM stored_configs
|
||||
ORDER BY updated_at DESC, display_name ASC",
|
||||
) {
|
||||
Ok(stmt) => stmt,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] failed to prepare list meta query: {}", e);
|
||||
return StoredConfigList { configs: vec![] };
|
||||
}
|
||||
};
|
||||
|
||||
let rows = match stmt.query_map([], row_to_meta) {
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] failed to list config meta rows: {}", e);
|
||||
return StoredConfigList { configs: vec![] };
|
||||
}
|
||||
};
|
||||
|
||||
let configs = rows.filter_map(Result::ok).map(to_meta).collect();
|
||||
StoredConfigList { configs }
|
||||
}
|
||||
|
||||
pub fn get_config_display_name(config_id: &str) -> Option<String> {
|
||||
let conn = open_db()?;
|
||||
load_meta_record(&conn, config_id).map(|record| record.display_name)
|
||||
}
|
||||
|
||||
pub fn get_config_meta(config_id: &str) -> Option<StoredConfigMeta> {
|
||||
let conn = open_db()?;
|
||||
load_meta_record(&conn, config_id).map(to_meta)
|
||||
}
|
||||
|
||||
pub fn upsert_config_meta(
|
||||
config_id: String,
|
||||
display_name: String,
|
||||
favorite: bool,
|
||||
temporary: bool,
|
||||
) -> StoredConfigMeta {
|
||||
let now = now_ts_string();
|
||||
let Some(conn) = open_db() else {
|
||||
return StoredConfigMeta {
|
||||
config_id,
|
||||
display_name,
|
||||
created_at: now.clone(),
|
||||
updated_at: now,
|
||||
favorite,
|
||||
temporary,
|
||||
};
|
||||
};
|
||||
|
||||
let created_at = load_meta_record(&conn, &config_id)
|
||||
.map(|record| record.created_at)
|
||||
.unwrap_or_else(|| now.clone());
|
||||
|
||||
if let Err(e) = conn.execute(
|
||||
"INSERT INTO stored_configs (
|
||||
config_id, display_name, created_at, updated_at, favorite, temporary
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
ON CONFLICT(config_id) DO UPDATE SET
|
||||
display_name = excluded.display_name,
|
||||
updated_at = excluded.updated_at,
|
||||
favorite = excluded.favorite,
|
||||
temporary = excluded.temporary",
|
||||
params![
|
||||
config_id,
|
||||
display_name,
|
||||
created_at,
|
||||
now,
|
||||
if favorite { 1 } else { 0 },
|
||||
if temporary { 1 } else { 0 }
|
||||
],
|
||||
) {
|
||||
hilog_error!("[Rust] failed to upsert config meta: {}", e);
|
||||
}
|
||||
|
||||
get_config_meta(&config_id).unwrap_or(StoredConfigMeta {
|
||||
config_id,
|
||||
display_name,
|
||||
created_at,
|
||||
updated_at: now,
|
||||
favorite,
|
||||
temporary,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn upsert_config_meta_in_tx(
|
||||
tx: &rusqlite::Transaction<'_>,
|
||||
config_id: String,
|
||||
display_name: String,
|
||||
favorite: bool,
|
||||
temporary: bool,
|
||||
) -> Option<StoredConfigMeta> {
|
||||
let now = now_ts_string();
|
||||
let created_at = tx
|
||||
.query_row(
|
||||
"SELECT config_id, display_name, created_at, updated_at, favorite, temporary
|
||||
FROM stored_configs WHERE config_id = ?1",
|
||||
params![config_id],
|
||||
row_to_meta,
|
||||
)
|
||||
.optional()
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|record| record.created_at)
|
||||
.unwrap_or_else(|| now.clone());
|
||||
|
||||
tx.execute(
|
||||
"INSERT INTO stored_configs (
|
||||
config_id, display_name, created_at, updated_at, favorite, temporary
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
ON CONFLICT(config_id) DO UPDATE SET
|
||||
display_name = excluded.display_name,
|
||||
updated_at = excluded.updated_at,
|
||||
favorite = excluded.favorite,
|
||||
temporary = excluded.temporary",
|
||||
params![
|
||||
config_id,
|
||||
display_name,
|
||||
created_at,
|
||||
now,
|
||||
if favorite { 1 } else { 0 },
|
||||
if temporary { 1 } else { 0 }
|
||||
],
|
||||
)
|
||||
.ok()?;
|
||||
|
||||
tx.query_row(
|
||||
"SELECT config_id, display_name, created_at, updated_at, favorite, temporary
|
||||
FROM stored_configs WHERE config_id = ?1",
|
||||
params![config_id],
|
||||
row_to_meta,
|
||||
)
|
||||
.optional()
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(to_meta)
|
||||
.or(Some(StoredConfigMeta {
|
||||
config_id,
|
||||
display_name,
|
||||
created_at,
|
||||
updated_at: now,
|
||||
favorite,
|
||||
temporary,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn set_config_display_name(
|
||||
config_id: String,
|
||||
display_name: String,
|
||||
) -> Option<StoredConfigMeta> {
|
||||
let conn = open_db()?;
|
||||
let mut record = load_meta_record(&conn, &config_id)?;
|
||||
record.display_name = display_name;
|
||||
record.updated_at = now_ts_string();
|
||||
|
||||
conn.execute(
|
||||
"UPDATE stored_configs
|
||||
SET display_name = ?2, updated_at = ?3
|
||||
WHERE config_id = ?1",
|
||||
params![config_id, record.display_name, record.updated_at],
|
||||
)
|
||||
.ok()?;
|
||||
|
||||
Some(to_meta(record))
|
||||
}
|
||||
|
||||
pub fn delete_config_meta(config_id: &str) -> bool {
|
||||
let Some(conn) = open_db() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
match conn.execute(
|
||||
"DELETE FROM stored_configs WHERE config_id = ?1",
|
||||
params![config_id],
|
||||
) {
|
||||
Ok(rows) => rows > 0,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] failed to delete config meta {}: {}", config_id, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub(crate) mod config_meta;
|
||||
@@ -1 +0,0 @@
|
||||
pub(crate) mod stored_config;
|
||||
@@ -1,68 +0,0 @@
|
||||
use napi_derive_ohos::napi;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct StoredConfigMeta {
|
||||
pub config_id: String,
|
||||
pub display_name: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub favorite: bool,
|
||||
pub temporary: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct StoredConfigRecord {
|
||||
pub meta: StoredConfigMeta,
|
||||
pub config_json: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct StoredConfigList {
|
||||
pub configs: Vec<StoredConfigMeta>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct ExportTomlResult {
|
||||
pub toml_text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct StoredConfigSummary {
|
||||
pub config_id: String,
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct SharedConfigLinkPayload {
|
||||
pub config_json: String,
|
||||
pub display_name: Option<String>,
|
||||
pub only_start: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct LocalSocketSyncMessage {
|
||||
pub message_type: String,
|
||||
pub payload_json: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[napi(object)]
|
||||
pub struct KeyValuePair {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
use super::{field_store, import_export, legacy_migration, validation};
|
||||
use crate::config::storage::config_meta::{
|
||||
delete_config_meta, get_config_meta, init_config_meta_store, list_config_meta_entries, open_db,
|
||||
upsert_config_meta_in_tx,
|
||||
};
|
||||
use crate::config::types::stored_config::{ExportTomlResult, StoredConfigRecord};
|
||||
use easytier::common::config::ConfigLoader;
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use ohos_hilog_binding::{hilog_debug, hilog_error};
|
||||
use rusqlite::params;
|
||||
use serde_json::Value;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static CONFIG_ROOT_DIR: Mutex<Option<PathBuf>> = Mutex::new(None);
|
||||
pub(crate) const CONFIG_DIR_NAME: &str = "easytier-configs";
|
||||
pub(crate) const KERNEL_SOCKET_FILE_NAME: &str = "easytier-kernel.sock";
|
||||
|
||||
pub(crate) fn config_root_dir() -> Option<PathBuf> {
|
||||
CONFIG_ROOT_DIR
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.as_ref().cloned())
|
||||
}
|
||||
|
||||
pub(crate) fn kernel_socket_path() -> Option<PathBuf> {
|
||||
config_root_dir().map(|root| root.join(KERNEL_SOCKET_FILE_NAME))
|
||||
}
|
||||
|
||||
pub(crate) fn legacy_config_file_path(config_id: &str) -> Option<PathBuf> {
|
||||
legacy_migration::legacy_config_file_path(&config_root_dir(), CONFIG_DIR_NAME, config_id)
|
||||
}
|
||||
|
||||
pub fn init_config_store(root_dir: String) -> bool {
|
||||
let root = PathBuf::from(root_dir);
|
||||
let configs_dir = root.join(CONFIG_DIR_NAME);
|
||||
if let Err(e) = std::fs::create_dir_all(&configs_dir) {
|
||||
hilog_error!(
|
||||
"[Rust] failed to create config dir {}: {}",
|
||||
configs_dir.display(),
|
||||
e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
match CONFIG_ROOT_DIR.lock() {
|
||||
Ok(mut guard) => {
|
||||
*guard = Some(root.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] failed to lock config root dir: {}", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if !init_config_meta_store(root.to_string_lossy().into_owned()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hilog_debug!(
|
||||
"[Rust] initialized config repo at {}",
|
||||
configs_dir.display()
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
fn migrate_legacy_file_if_needed(config_id: &str) -> Option<()> {
|
||||
legacy_migration::migrate_legacy_file_if_needed(
|
||||
&config_root_dir(),
|
||||
CONFIG_DIR_NAME,
|
||||
config_id,
|
||||
save_config_record,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn save_config_record(
|
||||
config_id: String,
|
||||
display_name: String,
|
||||
config_json: String,
|
||||
) -> Option<StoredConfigRecord> {
|
||||
let config = match validation::validate_config_json(&config_json, config_id.clone()) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] save_config_record failed {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let normalized_json = match serde_json::to_string(&config) {
|
||||
Ok(raw) => raw,
|
||||
Err(e) => {
|
||||
hilog_error!(
|
||||
"[Rust] failed to serialize normalized config {}: {}",
|
||||
config_id,
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let fields = match validation::config_to_top_level_map(&config) {
|
||||
Some(fields) => fields,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let conn = open_db()?;
|
||||
let tx = conn.unchecked_transaction().ok()?;
|
||||
let existing_meta = get_config_meta(&config_id);
|
||||
let favorite = existing_meta
|
||||
.as_ref()
|
||||
.map(|meta| meta.favorite)
|
||||
.unwrap_or(false);
|
||||
let temporary = existing_meta
|
||||
.as_ref()
|
||||
.map(|meta| meta.temporary)
|
||||
.unwrap_or(false);
|
||||
let meta = upsert_config_meta_in_tx(&tx, config_id.clone(), display_name, favorite, temporary)?;
|
||||
|
||||
field_store::replace_config_fields(&tx, &config_id, fields)?;
|
||||
|
||||
tx.commit().ok()?;
|
||||
|
||||
if let Some(legacy_path) = legacy_config_file_path(&config_id) {
|
||||
if legacy_path.exists() {
|
||||
let _ = std::fs::remove_file(legacy_path);
|
||||
}
|
||||
}
|
||||
|
||||
Some(StoredConfigRecord {
|
||||
meta,
|
||||
config_json: normalized_json,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_config_json(config_id: &str) -> Option<String> {
|
||||
migrate_legacy_file_if_needed(config_id)?;
|
||||
let object = field_store::load_config_map_from_db(config_id)?;
|
||||
serde_json::to_string(&Value::Object(object)).ok()
|
||||
}
|
||||
|
||||
pub fn get_config_record(config_id: &str) -> Option<StoredConfigRecord> {
|
||||
let config_json = load_config_json(config_id)?;
|
||||
let meta = get_config_meta(config_id)?;
|
||||
Some(StoredConfigRecord { meta, config_json })
|
||||
}
|
||||
|
||||
pub fn get_config_field_value(config_id: &str, field: &str) -> Option<String> {
|
||||
migrate_legacy_file_if_needed(config_id)?;
|
||||
let conn = open_db()?;
|
||||
conn.query_row(
|
||||
"SELECT field_json FROM stored_config_fields
|
||||
WHERE config_id = ?1 AND field_name = ?2",
|
||||
params![config_id, field],
|
||||
|row| row.get::<_, String>(0),
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn set_config_field_value(config_id: &str, field: &str, json_value: &str) -> bool {
|
||||
if field.contains('.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let raw = match load_config_json(config_id) {
|
||||
Some(raw) => raw,
|
||||
None => return false,
|
||||
};
|
||||
let mut value = match serde_json::from_str::<Value>(&raw) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let new_field_value = match serde_json::from_str::<Value>(json_value) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let object = match value.as_object_mut() {
|
||||
Some(object) => object,
|
||||
None => return false,
|
||||
};
|
||||
object.insert(field.to_string(), new_field_value);
|
||||
|
||||
let normalized = match serde_json::to_string(&value) {
|
||||
Ok(raw) => raw,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let display_name = get_config_meta(config_id)
|
||||
.map(|meta| meta.display_name)
|
||||
.unwrap_or_else(|| config_id.to_string());
|
||||
|
||||
save_config_record(config_id.to_string(), display_name, normalized).is_some()
|
||||
}
|
||||
|
||||
pub fn get_display_name(config_id: &str) -> Option<String> {
|
||||
get_config_meta(config_id).map(|meta| meta.display_name)
|
||||
}
|
||||
|
||||
pub fn get_default_config_json() -> Option<String> {
|
||||
crate::build_default_network_config_json().ok()
|
||||
}
|
||||
|
||||
pub fn create_config_record(config_id: String, display_name: String) -> Option<StoredConfigRecord> {
|
||||
let raw = get_default_config_json()?;
|
||||
let mut config = serde_json::from_str::<NetworkConfig>(&raw).ok()?;
|
||||
config.instance_id = Some(config_id.clone());
|
||||
let normalized_json = serde_json::to_string(&config).ok()?;
|
||||
save_config_record(config_id, display_name, normalized_json)
|
||||
}
|
||||
|
||||
pub fn start_kernel_with_config_id(config_id: &str) -> bool {
|
||||
let raw = match load_config_json(config_id) {
|
||||
Some(raw) => raw,
|
||||
None => return false,
|
||||
};
|
||||
crate::run_network_instance_from_json(&raw)
|
||||
}
|
||||
|
||||
pub fn list_config_meta_json() -> String {
|
||||
serde_json::to_string(&list_config_meta_entries().configs).unwrap_or_else(|_| "[]".to_string())
|
||||
}
|
||||
|
||||
pub fn delete_config_record(config_id: &str) -> bool {
|
||||
if let Some(path) = legacy_config_file_path(config_id) {
|
||||
if path.exists() {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
|
||||
let conn = match open_db() {
|
||||
Some(conn) => conn,
|
||||
None => return false,
|
||||
};
|
||||
if let Err(e) = conn.execute(
|
||||
"DELETE FROM stored_config_fields WHERE config_id = ?1",
|
||||
params![config_id],
|
||||
) {
|
||||
hilog_error!("[Rust] failed to delete config fields {}: {}", config_id, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
delete_config_meta(config_id)
|
||||
}
|
||||
|
||||
pub fn export_config_toml(config_id: &str) -> Option<ExportTomlResult> {
|
||||
let record = get_config_record(config_id)?;
|
||||
import_export::export_config_toml_from_record(&record)
|
||||
}
|
||||
|
||||
pub fn import_toml_config(
|
||||
toml_text: String,
|
||||
display_name: Option<String>,
|
||||
) -> Option<StoredConfigRecord> {
|
||||
import_export::import_toml_to_record(toml_text, display_name, save_config_record)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rusqlite::params;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn test_root() -> String {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let dir = std::env::temp_dir().join(format!("easytier_ohrs_test_{}", unique));
|
||||
dir.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_get_export_delete_roundtrip() {
|
||||
let root = test_root();
|
||||
assert!(init_config_store(root.clone()));
|
||||
|
||||
let config_json = crate::build_default_network_config_json().expect("default config");
|
||||
let saved = save_config_record("cfg-1".to_string(), "test-config".to_string(), config_json)
|
||||
.expect("save config");
|
||||
|
||||
assert_eq!(saved.meta.config_id, "cfg-1");
|
||||
assert_eq!(saved.meta.display_name, "test-config");
|
||||
|
||||
let loaded = get_config_record("cfg-1").expect("load config");
|
||||
assert_eq!(loaded.meta.display_name, "test-config");
|
||||
assert!(loaded.config_json.contains("cfg-1"));
|
||||
|
||||
let legacy_json_path = PathBuf::from(&root)
|
||||
.join(CONFIG_DIR_NAME)
|
||||
.join("cfg-1.json");
|
||||
assert!(
|
||||
!legacy_json_path.exists(),
|
||||
"config should no longer be persisted as a per-config json file"
|
||||
);
|
||||
|
||||
let conn = open_db().expect("db should be open");
|
||||
let field_count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM stored_config_fields WHERE config_id = ?1",
|
||||
params!["cfg-1"],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.expect("count config fields");
|
||||
assert!(field_count > 0, "config fields should be stored in sqlite");
|
||||
|
||||
let exported = export_config_toml("cfg-1").expect("export toml");
|
||||
assert!(exported.toml_text.contains("instance_id"));
|
||||
|
||||
assert!(delete_config_record("cfg-1"));
|
||||
assert!(get_config_record("cfg-1").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_config_field_updates_only_requested_top_level_field() {
|
||||
let root = test_root();
|
||||
assert!(init_config_store(root));
|
||||
|
||||
let config_json = crate::build_default_network_config_json().expect("default config");
|
||||
save_config_record(
|
||||
"cfg-field".to_string(),
|
||||
"field-config".to_string(),
|
||||
config_json,
|
||||
)
|
||||
.expect("save config");
|
||||
|
||||
let before_network_name = get_config_field_value("cfg-field", "network_name");
|
||||
let before_instance_id = get_config_field_value("cfg-field", "instance_id")
|
||||
.expect("instance id field should exist");
|
||||
|
||||
assert!(set_config_field_value(
|
||||
"cfg-field",
|
||||
"network_name",
|
||||
"\"changed-network\""
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
get_config_field_value("cfg-field", "network_name"),
|
||||
Some("\"changed-network\"".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
get_config_field_value("cfg-field", "instance_id"),
|
||||
Some(before_instance_id)
|
||||
);
|
||||
assert_ne!(
|
||||
get_config_field_value("cfg-field", "network_name"),
|
||||
before_network_name
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
use crate::config::storage::config_meta::{now_ts_string, open_db};
|
||||
use ohos_hilog_binding::hilog_error;
|
||||
use rusqlite::{Connection, params};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
pub(super) fn load_config_map_from_db(config_id: &str) -> Option<Map<String, Value>> {
|
||||
let conn = open_db()?;
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT field_name, field_json
|
||||
FROM stored_config_fields
|
||||
WHERE config_id = ?1",
|
||||
)
|
||||
.ok()?;
|
||||
let rows = stmt
|
||||
.query_map(params![config_id], |row| {
|
||||
let field_name: String = row.get(0)?;
|
||||
let field_json: String = row.get(1)?;
|
||||
Ok((field_name, field_json))
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let mut object = Map::new();
|
||||
for row in rows {
|
||||
let (field_name, field_json) = row.ok()?;
|
||||
let value = serde_json::from_str::<Value>(&field_json).ok()?;
|
||||
object.insert(field_name, value);
|
||||
}
|
||||
|
||||
if object.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(object)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn replace_config_fields(
|
||||
tx: &Connection,
|
||||
config_id: &str,
|
||||
fields: Map<String, Value>,
|
||||
) -> Option<()> {
|
||||
if let Err(e) = tx.execute(
|
||||
"DELETE FROM stored_config_fields WHERE config_id = ?1",
|
||||
params![config_id],
|
||||
) {
|
||||
hilog_error!(
|
||||
"[Rust] failed to clear existing config fields {}: {}",
|
||||
config_id,
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
for (field_name, value) in fields {
|
||||
let field_json = serde_json::to_string(&value).ok()?;
|
||||
if let Err(e) = tx.execute(
|
||||
"INSERT INTO stored_config_fields (config_id, field_name, field_json, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
params![config_id, field_name, field_json, now_ts_string()],
|
||||
) {
|
||||
hilog_error!("[Rust] failed to persist config field {}: {}", config_id, e);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
use crate::config::types::stored_config::{ExportTomlResult, StoredConfigRecord};
|
||||
use easytier::common::config::{ConfigLoader, TomlConfigLoader};
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
|
||||
pub(super) fn export_config_toml_from_record(
|
||||
record: &StoredConfigRecord,
|
||||
) -> Option<ExportTomlResult> {
|
||||
let config = serde_json::from_str::<NetworkConfig>(&record.config_json).ok()?;
|
||||
let toml = config.gen_config().ok()?;
|
||||
Some(ExportTomlResult {
|
||||
toml_text: toml.dump(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn import_toml_to_record(
|
||||
toml_text: String,
|
||||
display_name: Option<String>,
|
||||
save_config_record: impl Fn(String, String, String) -> Option<StoredConfigRecord>,
|
||||
) -> Option<StoredConfigRecord> {
|
||||
let config =
|
||||
NetworkConfig::new_from_config(TomlConfigLoader::new_from_str(&toml_text).ok()?).ok()?;
|
||||
|
||||
let config_id = config.instance_id.clone()?;
|
||||
let name_from_toml = toml_text
|
||||
.lines()
|
||||
.find_map(|line| {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.starts_with("instance_name") {
|
||||
return None;
|
||||
}
|
||||
trimmed.split_once('=').map(|(_, value)| {
|
||||
value
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.trim_matches('\'')
|
||||
.to_string()
|
||||
})
|
||||
})
|
||||
.filter(|name| !name.is_empty());
|
||||
|
||||
let final_name = display_name
|
||||
.filter(|name| !name.is_empty())
|
||||
.or(name_from_toml)
|
||||
.unwrap_or_else(|| config_id.clone());
|
||||
|
||||
let config_json = serde_json::to_string(&config).ok()?;
|
||||
save_config_record(config_id, final_name, config_json)
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
use crate::config::storage::config_meta::get_config_meta;
|
||||
use ohos_hilog_binding::hilog_error;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub(super) fn legacy_config_file_path(
|
||||
root_dir: &Option<PathBuf>,
|
||||
config_dir_name: &str,
|
||||
config_id: &str,
|
||||
) -> Option<PathBuf> {
|
||||
root_dir.as_ref().map(|root| {
|
||||
root.join(config_dir_name)
|
||||
.join(format!("{}.json", config_id))
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn migrate_legacy_file_if_needed(
|
||||
root_dir: &Option<PathBuf>,
|
||||
config_dir_name: &str,
|
||||
config_id: &str,
|
||||
save_config_record: impl Fn(
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
) -> Option<crate::config::types::stored_config::StoredConfigRecord>,
|
||||
) -> Option<()> {
|
||||
let legacy_path = legacy_config_file_path(root_dir, config_dir_name, config_id)?;
|
||||
if !legacy_path.exists() {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
let raw = std::fs::read_to_string(&legacy_path).ok()?;
|
||||
let display_name = get_config_meta(config_id)
|
||||
.map(|meta| meta.display_name)
|
||||
.unwrap_or_else(|| config_id.to_string());
|
||||
save_config_record(config_id.to_string(), display_name, raw)?;
|
||||
|
||||
if let Err(e) = std::fs::remove_file(&legacy_path) {
|
||||
hilog_error!(
|
||||
"[Rust] failed to remove legacy config file {}: {}",
|
||||
legacy_path.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
pub(super) fn normalize_config_id(
|
||||
mut config: NetworkConfig,
|
||||
requested_id: String,
|
||||
) -> Result<NetworkConfig, String> {
|
||||
if requested_id.is_empty() {
|
||||
return Err("config_id is required".to_string());
|
||||
}
|
||||
config.instance_id = Some(requested_id);
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub(super) fn validate_config_json(
|
||||
config_json: &str,
|
||||
config_id: String,
|
||||
) -> Result<NetworkConfig, String> {
|
||||
let config = serde_json::from_str::<NetworkConfig>(config_json)
|
||||
.map_err(|e| format!("parse config json failed: {}", e))?;
|
||||
let config = normalize_config_id(config, config_id)?;
|
||||
config
|
||||
.gen_config()
|
||||
.map_err(|e| format!("generate toml failed: {}", e))?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub(super) fn config_to_top_level_map(config: &NetworkConfig) -> Option<Map<String, Value>> {
|
||||
serde_json::to_value(config).ok()?.as_object().cloned()
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub(crate) mod config_api;
|
||||
pub(crate) mod runtime_api;
|
||||
@@ -1,46 +0,0 @@
|
||||
use crate::config;
|
||||
|
||||
pub(crate) fn init_config_store(root_dir: String) -> bool {
|
||||
config::repository::init_config_store(root_dir)
|
||||
}
|
||||
|
||||
pub(crate) fn list_configs() -> String {
|
||||
config::repository::list_config_meta_json()
|
||||
}
|
||||
|
||||
pub(crate) fn save_config(config_id: String, display_name: String, config_json: String) -> bool {
|
||||
config::repository::save_config_record(config_id, display_name, config_json).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn create_config(config_id: String, display_name: String) -> bool {
|
||||
config::repository::create_config_record(config_id, display_name).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn delete_stored_config_meta(config_id: String) -> bool {
|
||||
config::repository::delete_config_record(&config_id)
|
||||
}
|
||||
|
||||
pub(crate) fn get_config(config_id: String) -> Option<String> {
|
||||
config::repository::load_config_json(&config_id)
|
||||
}
|
||||
|
||||
pub(crate) fn get_default_config() -> Option<String> {
|
||||
config::repository::get_default_config_json()
|
||||
}
|
||||
|
||||
pub(crate) fn get_config_field(config_id: String, field: String) -> Option<String> {
|
||||
config::repository::get_config_field_value(&config_id, &field)
|
||||
}
|
||||
|
||||
pub(crate) fn set_config_field(config_id: String, field: String, json_value: String) -> bool {
|
||||
config::repository::set_config_field_value(&config_id, &field, &json_value)
|
||||
}
|
||||
|
||||
pub(crate) fn import_toml(toml_text: String, display_name: Option<String>) -> Option<String> {
|
||||
config::repository::import_toml_config(toml_text, display_name)
|
||||
.map(|record| record.meta.config_id)
|
||||
}
|
||||
|
||||
pub(crate) fn export_toml(config_id: String) -> Option<String> {
|
||||
config::repository::export_config_toml(&config_id).map(|ret| ret.toml_text)
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
use crate::config::repository::load_config_json;
|
||||
use crate::config::storage::config_meta::get_config_display_name;
|
||||
use crate::config::types::stored_config::KeyValuePair;
|
||||
use crate::kernel_bridge::{
|
||||
aggregate_requested_tun_routes, start_local_socket_server as start_local_socket_server_inner,
|
||||
stop_local_socket_server as stop_local_socket_server_inner,
|
||||
};
|
||||
use crate::runtime::state::runtime_state::{
|
||||
RuntimeAggregateState, TunAggregateState, clear_tun_attached, mark_tun_attached,
|
||||
runtime_instance_from_running_info,
|
||||
};
|
||||
use crate::{ASYNC_RUNTIME, EASYTIER_VERSION, INSTANCE_MANAGER, WEB_CLIENTS};
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use ohos_hilog_binding::{hilog_error, hilog_info};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) fn start_kernel(
|
||||
config_id: String,
|
||||
start_kernel_with_config_id: impl Fn(&str) -> bool,
|
||||
) -> bool {
|
||||
start_kernel_with_config_id(&config_id)
|
||||
}
|
||||
|
||||
pub(crate) fn stop_kernel(
|
||||
config_id: String,
|
||||
stop_web_client: impl Fn(&str) -> bool,
|
||||
parse_instance_uuid: impl Fn(&str) -> Option<uuid::Uuid>,
|
||||
maybe_stop_local_socket_server: impl Fn(),
|
||||
) -> bool {
|
||||
clear_tun_attached(&config_id);
|
||||
if stop_web_client(&config_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some(instance_id) = parse_instance_uuid(&config_id) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let ret = INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
.map(|_| true)
|
||||
.unwrap_or_else(|err| {
|
||||
hilog_error!("[Rust] stop_kernel failed {}: {}", config_id, err);
|
||||
false
|
||||
});
|
||||
maybe_stop_local_socket_server();
|
||||
ret
|
||||
}
|
||||
|
||||
pub(crate) fn stop_network_instance(
|
||||
config_ids: Vec<String>,
|
||||
stop_kernel: impl Fn(String) -> bool,
|
||||
) -> bool {
|
||||
let mut ok = true;
|
||||
for config_id in config_ids {
|
||||
ok = stop_kernel(config_id) && ok;
|
||||
}
|
||||
ok
|
||||
}
|
||||
|
||||
pub(crate) fn collect_network_infos() -> Vec<KeyValuePair> {
|
||||
let infos = match INSTANCE_MANAGER.collect_network_infos_sync() {
|
||||
Ok(infos) => infos,
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] collect network infos failed {}", err);
|
||||
return vec![];
|
||||
}
|
||||
};
|
||||
|
||||
infos
|
||||
.into_iter()
|
||||
.filter_map(|(key, value)| {
|
||||
serde_json::to_string(&value)
|
||||
.ok()
|
||||
.map(|value_json| KeyValuePair {
|
||||
key: key.to_string(),
|
||||
value: value_json,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn set_tun_fd(
|
||||
config_id: String,
|
||||
fd: i32,
|
||||
parse_instance_uuid: impl Fn(&str) -> Option<uuid::Uuid>,
|
||||
) -> bool {
|
||||
let Some(instance_id) = parse_instance_uuid(&config_id) else {
|
||||
hilog_error!("[Rust] set_tun_fd invalid instance id: {}", config_id);
|
||||
return false;
|
||||
};
|
||||
|
||||
INSTANCE_MANAGER
|
||||
.set_tun_fd(&instance_id, fd)
|
||||
.map(|_| {
|
||||
mark_tun_attached(&config_id);
|
||||
hilog_info!(
|
||||
"[Rust] set_tun_fd success instance={} fd={} marked_attached=true",
|
||||
config_id,
|
||||
fd
|
||||
);
|
||||
true
|
||||
})
|
||||
.unwrap_or_else(|err| {
|
||||
hilog_error!("[Rust] set_tun_fd failed {}: {}", config_id, err);
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn get_runtime_snapshot() -> RuntimeAggregateState {
|
||||
get_runtime_snapshot_inner()
|
||||
}
|
||||
|
||||
pub(crate) fn get_runtime_snapshot_inner() -> RuntimeAggregateState {
|
||||
let infos = match INSTANCE_MANAGER.collect_network_infos_sync() {
|
||||
Ok(infos) => infos,
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] collect network infos failed {}", err);
|
||||
return RuntimeAggregateState {
|
||||
instances: vec![],
|
||||
tun: TunAggregateState {
|
||||
active: false,
|
||||
attached_instance_ids: vec![],
|
||||
aggregated_routes: vec![],
|
||||
dns_servers: vec![],
|
||||
need_rebuild: false,
|
||||
},
|
||||
running_instance_count: 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let mut instances = Vec::with_capacity(infos.len());
|
||||
for (instance_uuid, info) in infos {
|
||||
let config_id = instance_uuid.to_string();
|
||||
let display_name = get_config_display_name(&config_id).unwrap_or_else(|| config_id.clone());
|
||||
let config_json = load_config_json(&config_id);
|
||||
let stored_config = config_json
|
||||
.as_deref()
|
||||
.and_then(|raw| serde_json::from_str::<NetworkConfig>(raw).ok());
|
||||
let magic_dns_enabled = stored_config
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.enable_magic_dns)
|
||||
.unwrap_or(false);
|
||||
let need_exit_node = stored_config
|
||||
.as_ref()
|
||||
.map(|cfg| !cfg.exit_nodes.is_empty())
|
||||
.unwrap_or(false);
|
||||
instances.push(runtime_instance_from_running_info(
|
||||
config_id,
|
||||
display_name,
|
||||
magic_dns_enabled,
|
||||
need_exit_node,
|
||||
info,
|
||||
));
|
||||
}
|
||||
|
||||
instances.sort_by(|a, b| {
|
||||
a.display_name
|
||||
.cmp(&b.display_name)
|
||||
.then_with(|| a.instance_id.cmp(&b.instance_id))
|
||||
});
|
||||
let attached_instance_ids = instances
|
||||
.iter()
|
||||
.filter(|instance| instance.tun_required)
|
||||
.map(|instance| instance.instance_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let aggregated_routes = aggregate_requested_tun_routes(&instances);
|
||||
let running_instance_count =
|
||||
instances.iter().filter(|instance| instance.running).count() as i32;
|
||||
let tun_active = !attached_instance_ids.is_empty();
|
||||
|
||||
RuntimeAggregateState {
|
||||
instances,
|
||||
tun: TunAggregateState {
|
||||
active: tun_active,
|
||||
attached_instance_ids,
|
||||
aggregated_routes,
|
||||
dns_servers: vec![],
|
||||
need_rebuild: false,
|
||||
},
|
||||
running_instance_count,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
mod protocol;
|
||||
mod routing;
|
||||
mod socket_server;
|
||||
|
||||
pub(crate) use routing::aggregate_requested_tun_routes;
|
||||
pub use socket_server::{start_local_socket_server, stop_local_socket_server};
|
||||
@@ -1,50 +0,0 @@
|
||||
use crate::config::types::stored_config::LocalSocketSyncMessage;
|
||||
use serde::Serialize;
|
||||
use std::io::{Error, ErrorKind, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct TunRequestPayload {
|
||||
pub config_id: String,
|
||||
pub instance_id: String,
|
||||
pub display_name: String,
|
||||
pub virtual_ipv4: Option<String>,
|
||||
pub virtual_ipv4_cidr: Option<String>,
|
||||
pub aggregated_routes: Vec<String>,
|
||||
pub magic_dns_enabled: bool,
|
||||
pub need_exit_node: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn send_local_socket_message(
|
||||
stream: &mut UnixStream,
|
||||
message_type: &str,
|
||||
payload_json: String,
|
||||
) -> std::io::Result<()> {
|
||||
let message = LocalSocketSyncMessage {
|
||||
message_type: message_type.to_string(),
|
||||
payload_json,
|
||||
};
|
||||
let mut raw = serde_json::to_vec(&message)
|
||||
.map_err(|err| Error::new(ErrorKind::InvalidData, err.to_string()))?;
|
||||
raw.push(b'\n');
|
||||
stream.write_all(&raw)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn broadcast_local_socket_message(
|
||||
clients: &mut Vec<UnixStream>,
|
||||
message_type: &str,
|
||||
payload_json: &str,
|
||||
) -> bool {
|
||||
let mut active_clients = Vec::with_capacity(clients.len());
|
||||
let mut delivered = false;
|
||||
for mut client in clients.drain(..) {
|
||||
if send_local_socket_message(&mut client, message_type, payload_json.to_string()).is_ok() {
|
||||
delivered = true;
|
||||
active_clients.push(client);
|
||||
}
|
||||
}
|
||||
*clients = active_clients;
|
||||
delivered
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
use crate::config::repository::load_config_json;
|
||||
use crate::runtime::state::runtime_state::RuntimeInstanceState;
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use ipnet::IpNet;
|
||||
use ohos_hilog_binding::hilog_debug;
|
||||
use std::collections::HashSet;
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub(crate) fn load_manual_routes(config_id: &str) -> Vec<String> {
|
||||
load_config_json(config_id)
|
||||
.and_then(|raw| serde_json::from_str::<NetworkConfig>(&raw).ok())
|
||||
.map(|config| config.routes)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn normalize_route_cidr(route: &str) -> Option<String> {
|
||||
route
|
||||
.parse::<IpNet>()
|
||||
.ok()
|
||||
.map(|network| match network {
|
||||
IpNet::V4(net) => net.trunc().to_string(),
|
||||
IpNet::V6(net) => net.trunc().to_string(),
|
||||
})
|
||||
.or_else(|| {
|
||||
route.parse::<IpAddr>().ok().map(|addr| match addr {
|
||||
IpAddr::V4(ip) => format!("{}/32", ip),
|
||||
IpAddr::V6(ip) => format!("{}/128", ip),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn simplify_routes(routes: Vec<String>) -> Vec<String> {
|
||||
let mut parsed = routes
|
||||
.into_iter()
|
||||
.filter_map(|route| normalize_route_cidr(&route))
|
||||
.filter_map(|route| route.parse::<IpNet>().ok())
|
||||
.collect::<Vec<_>>();
|
||||
parsed.sort_by(|left, right| {
|
||||
left.prefix_len()
|
||||
.cmp(&right.prefix_len())
|
||||
.then_with(|| left.network().to_string().cmp(&right.network().to_string()))
|
||||
});
|
||||
|
||||
let mut simplified = Vec::<IpNet>::new();
|
||||
'outer: for route in parsed {
|
||||
for existing in &simplified {
|
||||
if existing.contains(&route.network()) && existing.prefix_len() <= route.prefix_len() {
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
simplified.retain(|existing| {
|
||||
!(route.contains(&existing.network()) && route.prefix_len() <= existing.prefix_len())
|
||||
});
|
||||
simplified.push(route);
|
||||
}
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
simplified
|
||||
.into_iter()
|
||||
.map(|route| route.to_string())
|
||||
.filter(|route| seen.insert(route.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn aggregate_tun_routes(instance: &RuntimeInstanceState) -> Vec<String> {
|
||||
let virtual_ipv4_cidr = instance
|
||||
.my_node_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.virtual_ipv4_cidr.clone());
|
||||
let manual_routes = load_manual_routes(&instance.config_id);
|
||||
let proxy_cidrs = instance
|
||||
.routes
|
||||
.iter()
|
||||
.flat_map(|route| route.proxy_cidrs.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
let mut raw_routes = Vec::new();
|
||||
|
||||
if let Some(cidr) = virtual_ipv4_cidr.clone() {
|
||||
raw_routes.push(cidr);
|
||||
}
|
||||
|
||||
raw_routes.extend(manual_routes.iter().cloned());
|
||||
raw_routes.extend(proxy_cidrs.iter().cloned());
|
||||
let aggregated_routes = simplify_routes(raw_routes);
|
||||
hilog_debug!(
|
||||
"[Rust] aggregate_tun_routes instance={} proxy_cidrs={:?} aggregated_routes={:?}",
|
||||
instance.instance_id,
|
||||
proxy_cidrs,
|
||||
aggregated_routes
|
||||
);
|
||||
aggregated_routes
|
||||
}
|
||||
|
||||
pub(crate) fn aggregate_requested_tun_routes(instances: &[RuntimeInstanceState]) -> Vec<String> {
|
||||
let mut aggregated_routes = Vec::new();
|
||||
let mut seen_routes = HashSet::new();
|
||||
for instance in instances.iter().filter(|instance| instance.tun_required) {
|
||||
for route in aggregate_tun_routes(instance) {
|
||||
if seen_routes.insert(route.clone()) {
|
||||
aggregated_routes.push(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
aggregated_routes
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
use super::protocol::{TunRequestPayload, broadcast_local_socket_message};
|
||||
use crate::config::repository::kernel_socket_path;
|
||||
use crate::get_runtime_snapshot_inner;
|
||||
use crate::kernel_bridge::routing::aggregate_tun_routes;
|
||||
use ohos_hilog_binding::{hilog_error, hilog_info};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::ErrorKind;
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::Duration;
|
||||
|
||||
struct LocalSocketState {
|
||||
stop_flag: std::sync::Arc<AtomicBool>,
|
||||
socket_path: PathBuf,
|
||||
worker: JoinHandle<()>,
|
||||
}
|
||||
|
||||
static LOCAL_SOCKET_STATE: Lazy<Mutex<Option<LocalSocketState>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
pub fn start_local_socket_server() -> bool {
|
||||
let socket_path = match kernel_socket_path() {
|
||||
Some(path) => path,
|
||||
None => {
|
||||
hilog_error!("[Rust] kernel socket path unavailable");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
match LOCAL_SOCKET_STATE.lock() {
|
||||
Ok(guard) if guard.is_some() => return true,
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] lock localsocket state failed: {}", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if socket_path.exists() {
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
}
|
||||
|
||||
let listener = match UnixListener::bind(&socket_path) {
|
||||
Ok(listener) => listener,
|
||||
Err(err) => {
|
||||
hilog_error!(
|
||||
"[Rust] bind localsocket failed {}: {}",
|
||||
socket_path.display(),
|
||||
err
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if let Err(err) = listener.set_nonblocking(true) {
|
||||
hilog_error!("[Rust] set localsocket nonblocking failed: {}", err);
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
let stop_flag = std::sync::Arc::new(AtomicBool::new(false));
|
||||
let worker_stop_flag = stop_flag.clone();
|
||||
let worker = thread::spawn(move || {
|
||||
let mut last_snapshot_json = String::new();
|
||||
let mut delivered_tun_requests = HashSet::new();
|
||||
let mut last_tun_route_signatures = HashMap::<String, String>::new();
|
||||
let mut clients = Vec::<UnixStream>::new();
|
||||
|
||||
while !worker_stop_flag.load(Ordering::Relaxed) {
|
||||
let mut accepted_client = false;
|
||||
loop {
|
||||
match listener.accept() {
|
||||
Ok((stream, _addr)) => {
|
||||
accepted_client = true;
|
||||
clients.push(stream);
|
||||
}
|
||||
Err(err) if err.kind() == ErrorKind::WouldBlock => break,
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] accept localsocket failed: {}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let snapshot = get_runtime_snapshot_inner();
|
||||
let snapshot_json = match serde_json::to_string(&snapshot) {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] serialize runtime snapshot failed: {}", err);
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if accepted_client || snapshot_json != last_snapshot_json {
|
||||
let _ = broadcast_local_socket_message(
|
||||
&mut clients,
|
||||
"runtime_snapshot",
|
||||
&snapshot_json,
|
||||
);
|
||||
last_snapshot_json = snapshot_json;
|
||||
}
|
||||
|
||||
for instance in snapshot.instances.iter() {
|
||||
if instance.running && instance.tun_required {
|
||||
let virtual_ipv4 = instance
|
||||
.my_node_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.virtual_ipv4.clone());
|
||||
let virtual_ipv4_cidr = instance
|
||||
.my_node_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.virtual_ipv4_cidr.clone());
|
||||
if clients.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if virtual_ipv4.is_none() || virtual_ipv4_cidr.is_none() {
|
||||
continue;
|
||||
}
|
||||
let aggregated_routes = aggregate_tun_routes(instance);
|
||||
let route_signature = serde_json::to_string(&aggregated_routes)
|
||||
.unwrap_or_else(|_| "[]".to_string());
|
||||
let should_send = !delivered_tun_requests.contains(&instance.instance_id)
|
||||
|| last_tun_route_signatures
|
||||
.get(&instance.instance_id)
|
||||
.map(|value| value != &route_signature)
|
||||
.unwrap_or(true);
|
||||
if !should_send {
|
||||
continue;
|
||||
}
|
||||
let payload = TunRequestPayload {
|
||||
config_id: instance.config_id.clone(),
|
||||
instance_id: instance.instance_id.clone(),
|
||||
display_name: instance.display_name.clone(),
|
||||
virtual_ipv4,
|
||||
virtual_ipv4_cidr,
|
||||
aggregated_routes,
|
||||
magic_dns_enabled: instance.magic_dns_enabled,
|
||||
need_exit_node: instance.need_exit_node,
|
||||
};
|
||||
let payload_json = match serde_json::to_string(&payload) {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] serialize tun request failed: {}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if broadcast_local_socket_message(&mut clients, "tun_request", &payload_json) {
|
||||
delivered_tun_requests.insert(instance.instance_id.clone());
|
||||
last_tun_route_signatures
|
||||
.insert(instance.instance_id.clone(), route_signature);
|
||||
}
|
||||
} else {
|
||||
delivered_tun_requests.remove(&instance.instance_id);
|
||||
last_tun_route_signatures.remove(&instance.instance_id);
|
||||
}
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
});
|
||||
|
||||
match LOCAL_SOCKET_STATE.lock() {
|
||||
Ok(mut guard) => {
|
||||
*guard = Some(LocalSocketState {
|
||||
stop_flag,
|
||||
socket_path,
|
||||
worker,
|
||||
});
|
||||
true
|
||||
}
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] lock localsocket state failed: {}", err);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_local_socket_server() -> bool {
|
||||
let state = match LOCAL_SOCKET_STATE.lock() {
|
||||
Ok(mut guard) => guard.take(),
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] lock localsocket state failed: {}", err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(state) = state {
|
||||
state.stop_flag.store(true, Ordering::Relaxed);
|
||||
let _ = state.worker.join();
|
||||
let _ = std::fs::remove_file(state.socket_path);
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -1,386 +1,21 @@
|
||||
mod config;
|
||||
mod exports;
|
||||
mod kernel_bridge;
|
||||
mod platform;
|
||||
mod runtime;
|
||||
mod native_log;
|
||||
|
||||
use config::repository::{
|
||||
create_config_record, delete_config_record, export_config_toml, get_config_field_value,
|
||||
get_default_config_json, import_toml_config, init_config_store as init_repo_store,
|
||||
list_config_meta_json, save_config_record, set_config_field_value, start_kernel_with_config_id,
|
||||
};
|
||||
use config::services::schema_service::{
|
||||
ConfigFieldMapping, NetworkConfigSchema,
|
||||
get_network_config_field_mappings as build_network_config_field_mappings,
|
||||
get_network_config_schema as build_network_config_schema,
|
||||
};
|
||||
use config::services::share_link_service::{
|
||||
build_config_share_link as build_config_share_link_inner,
|
||||
import_config_share_link as import_config_share_link_inner,
|
||||
parse_config_share_link as parse_config_share_link_inner,
|
||||
};
|
||||
use config::storage::config_meta::get_config_display_name;
|
||||
use config::types::stored_config::{KeyValuePair, SharedConfigLinkPayload};
|
||||
use easytier::common::config::{ConfigFileControl, ConfigLoader, TomlConfigLoader};
|
||||
use easytier::common::constants::EASYTIER_VERSION;
|
||||
use easytier::common::{
|
||||
MachineIdOptions,
|
||||
config::{ConfigFileControl, ConfigLoader, TomlConfigLoader},
|
||||
};
|
||||
use easytier::instance_manager::NetworkInstanceManager;
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use easytier::proto::api::manage::NetworkingMethod;
|
||||
use easytier::web_client::{WebClient, WebClientHooks, run_web_client};
|
||||
use kernel_bridge::{
|
||||
aggregate_requested_tun_routes, start_local_socket_server as start_local_socket_server_inner,
|
||||
stop_local_socket_server as stop_local_socket_server_inner,
|
||||
};
|
||||
use napi_derive_ohos::napi;
|
||||
use ohos_hilog_binding::{hilog_error, hilog_info};
|
||||
use runtime::state::runtime_state::{
|
||||
RuntimeAggregateState, TunAggregateState, clear_tun_attached, mark_tun_attached,
|
||||
runtime_instance_from_running_info,
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use ohos_hilog_binding::{hilog_debug, hilog_error};
|
||||
use std::format;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::runtime::{Builder, Runtime};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) static INSTANCE_MANAGER: once_cell::sync::Lazy<Arc<NetworkInstanceManager>> =
|
||||
once_cell::sync::Lazy::new(|| Arc::new(NetworkInstanceManager::new()));
|
||||
static ASYNC_RUNTIME: once_cell::sync::Lazy<Runtime> = once_cell::sync::Lazy::new(|| {
|
||||
Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("tokio runtime for easytier-ohrs")
|
||||
});
|
||||
static WEB_CLIENTS: once_cell::sync::Lazy<Mutex<HashMap<String, ManagedWebClient>>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
static INSTANCE_MANAGER: once_cell::sync::Lazy<NetworkInstanceManager> =
|
||||
once_cell::sync::Lazy::new(NetworkInstanceManager::new);
|
||||
|
||||
#[derive(Default)]
|
||||
struct TrackedWebClientHooks {
|
||||
instance_ids: Mutex<HashSet<Uuid>>,
|
||||
}
|
||||
|
||||
struct ManagedWebClient {
|
||||
_client: WebClient,
|
||||
hooks: Arc<TrackedWebClientHooks>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl WebClientHooks for TrackedWebClientHooks {
|
||||
async fn post_run_network_instance(&self, id: &Uuid) -> Result<(), String> {
|
||||
self.instance_ids
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?
|
||||
.insert(*id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn post_remove_network_instances(&self, ids: &[Uuid]) -> Result<(), String> {
|
||||
let mut guard = self.instance_ids.lock().map_err(|err| err.to_string())?;
|
||||
for id in ids {
|
||||
guard.remove(id);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn is_config_server_config(config: &NetworkConfig) -> bool {
|
||||
matches!(
|
||||
NetworkingMethod::try_from(config.networking_method.unwrap_or_default())
|
||||
.unwrap_or_default(),
|
||||
NetworkingMethod::PublicServer
|
||||
) && config
|
||||
.public_server_url
|
||||
.as_ref()
|
||||
.is_some_and(|url| !url.trim().is_empty())
|
||||
}
|
||||
|
||||
fn stop_web_client(config_id: &str) -> bool {
|
||||
let managed = match WEB_CLIENTS.lock() {
|
||||
Ok(mut guard) => guard.remove(config_id),
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] stop_web_client lock failed {}", err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(managed) = managed else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let tracked_ids = managed
|
||||
.hooks
|
||||
.instance_ids
|
||||
.lock()
|
||||
.map(|guard| guard.iter().copied().collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
drop(managed);
|
||||
|
||||
if tracked_ids.is_empty() {
|
||||
maybe_stop_local_socket_server();
|
||||
return true;
|
||||
}
|
||||
|
||||
let ret = INSTANCE_MANAGER
|
||||
.delete_network_instance(tracked_ids)
|
||||
.map(|_| true)
|
||||
.unwrap_or_else(|err| {
|
||||
hilog_error!(
|
||||
"[Rust] stop config server instances failed {}: {}",
|
||||
config_id,
|
||||
err
|
||||
);
|
||||
false
|
||||
});
|
||||
maybe_stop_local_socket_server();
|
||||
ret
|
||||
}
|
||||
|
||||
fn ensure_local_socket_server_started() -> bool {
|
||||
start_local_socket_server_inner()
|
||||
}
|
||||
|
||||
fn maybe_stop_local_socket_server() {
|
||||
let no_local_instances = INSTANCE_MANAGER.list_network_instance_ids().is_empty();
|
||||
let no_web_clients = WEB_CLIENTS
|
||||
.lock()
|
||||
.map(|guard| guard.is_empty())
|
||||
.unwrap_or(false);
|
||||
if no_local_instances && no_web_clients {
|
||||
let _ = stop_local_socket_server_inner();
|
||||
}
|
||||
}
|
||||
|
||||
fn run_config_server_instance(config_id: &str, config: &NetworkConfig) -> bool {
|
||||
if INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.iter()
|
||||
.next()
|
||||
.is_some()
|
||||
{
|
||||
hilog_error!("[Rust] there is a running instance!");
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(config_server_url) = config.public_server_url.clone() else {
|
||||
hilog_error!("[Rust] public_server_url missing for config server mode");
|
||||
return false;
|
||||
};
|
||||
let hooks = Arc::new(TrackedWebClientHooks::default());
|
||||
let secure_mode = config
|
||||
.secure_mode
|
||||
.as_ref()
|
||||
.map(|mode| mode.enabled)
|
||||
.unwrap_or(false);
|
||||
let hostname = config.hostname.clone();
|
||||
|
||||
if !ensure_local_socket_server_started() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let client = ASYNC_RUNTIME.block_on(run_web_client(
|
||||
&config_server_url,
|
||||
MachineIdOptions::default(),
|
||||
hostname,
|
||||
secure_mode,
|
||||
INSTANCE_MANAGER.clone(),
|
||||
Some(hooks.clone()),
|
||||
));
|
||||
|
||||
let client = match client {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] start config server failed {}", err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
match WEB_CLIENTS.lock() {
|
||||
Ok(mut guard) => {
|
||||
guard.insert(
|
||||
config_id.to_string(),
|
||||
ManagedWebClient {
|
||||
_client: client,
|
||||
hooks,
|
||||
},
|
||||
);
|
||||
true
|
||||
}
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] store config server client failed {}", err);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_default_network_config_json() -> Result<String, String> {
|
||||
let config = NetworkConfig::new_from_config(TomlConfigLoader::default())
|
||||
.map_err(|e| format!("default_network_config failed {}", e))?;
|
||||
serde_json::to_string(&config).map_err(|e| format!("default_network_config failed {}", e))
|
||||
}
|
||||
|
||||
fn convert_toml_to_network_config_inner(toml_text: &str) -> Result<String, String> {
|
||||
let config = NetworkConfig::new_from_config(
|
||||
TomlConfigLoader::new_from_str(toml_text).map_err(|e| e.to_string())?,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_string(&config).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn parse_network_config_inner(cfg_json: &str) -> bool {
|
||||
serde_json::from_str::<NetworkConfig>(cfg_json)
|
||||
.ok()
|
||||
.and_then(|cfg| cfg.gen_config().ok())
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn run_network_instance_from_json(cfg_json: &str) -> bool {
|
||||
let config = match serde_json::from_str::<NetworkConfig>(cfg_json) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] parse config failed {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if is_config_server_config(&config) {
|
||||
let Some(config_id) = config.instance_id.as_deref() else {
|
||||
hilog_error!("[Rust] config server config missing instance id");
|
||||
return false;
|
||||
};
|
||||
return run_config_server_instance(config_id, &config);
|
||||
}
|
||||
|
||||
let cfg = match config.gen_config() {
|
||||
Ok(toml) => toml,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] parse config failed {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if !INSTANCE_MANAGER.list_network_instance_ids().is_empty() {
|
||||
hilog_error!("[Rust] there is a running instance!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if !ensure_local_socket_server_started() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let inst_id = cfg.get_id();
|
||||
if INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.contains(&inst_id)
|
||||
{
|
||||
hilog_error!("[Rust] instance {} already exists", inst_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
match INSTANCE_MANAGER.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) {
|
||||
Ok(_) => true,
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] start_kernel failed for {}: {}", inst_id, err);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_instance_uuid(config_id: &str) -> Option<Uuid> {
|
||||
match Uuid::parse_str(config_id) {
|
||||
Ok(uuid) => Some(uuid),
|
||||
Err(err) => {
|
||||
hilog_error!("[Rust] invalid config_id {}: {}", config_id, err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn init_config_store(root_dir: String) -> bool {
|
||||
exports::config_api::init_config_store(root_dir)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn list_configs() -> String {
|
||||
exports::config_api::list_configs()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_config_display_name_by_id(config_id: String) -> Option<String> {
|
||||
get_config_display_name(&config_id)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn save_config(config_id: String, display_name: String, config_json: String) -> bool {
|
||||
exports::config_api::save_config(config_id, display_name, config_json)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn create_config(config_id: String, display_name: String) -> bool {
|
||||
exports::config_api::create_config(config_id, display_name)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn rename_stored_config(config_id: String, display_name: String) -> bool {
|
||||
config::storage::config_meta::set_config_display_name(config_id, display_name).is_some()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn delete_stored_config_meta(config_id: String) -> bool {
|
||||
exports::config_api::delete_stored_config_meta(config_id)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_config(config_id: String) -> Option<String> {
|
||||
exports::config_api::get_config(config_id)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_default_config() -> Option<String> {
|
||||
exports::config_api::get_default_config()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_config_field(config_id: String, field: String) -> Option<String> {
|
||||
exports::config_api::get_config_field(config_id, field)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn set_config_field(config_id: String, field: String, json_value: String) -> bool {
|
||||
exports::config_api::set_config_field(config_id, field, json_value)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn import_toml(toml_text: String, display_name: Option<String>) -> Option<String> {
|
||||
exports::config_api::import_toml(toml_text, display_name)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn export_toml(config_id: String) -> Option<String> {
|
||||
exports::config_api::export_toml(config_id)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn start_kernel(config_id: String) -> bool {
|
||||
exports::runtime_api::start_kernel(config_id, start_kernel_with_config_id)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn stop_kernel(config_id: String) -> bool {
|
||||
exports::runtime_api::stop_kernel(
|
||||
config_id,
|
||||
stop_web_client,
|
||||
parse_instance_uuid,
|
||||
maybe_stop_local_socket_server,
|
||||
)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn stop_network_instance(config_ids: Vec<String>) -> bool {
|
||||
exports::runtime_api::stop_network_instance(config_ids, stop_kernel)
|
||||
#[napi(object)]
|
||||
pub struct KeyValuePair {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
@@ -389,97 +24,162 @@ pub fn easytier_version() -> String {
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn default_network_config() -> String {
|
||||
get_default_config().unwrap_or_else(|| "{}".to_string())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn convert_toml_to_network_config(toml_text: String) -> String {
|
||||
convert_toml_to_network_config_inner(&toml_text).unwrap_or_else(|err| format!("ERROR: {err}"))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn parse_network_config(cfg_json: String) -> bool {
|
||||
parse_network_config_inner(&cfg_json)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn run_network_instance(cfg_json: String) -> bool {
|
||||
run_network_instance_from_json(&cfg_json)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn collect_network_infos() -> Vec<KeyValuePair> {
|
||||
exports::runtime_api::collect_network_infos()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn set_tun_fd(config_id: String, fd: i32) -> bool {
|
||||
exports::runtime_api::set_tun_fd(config_id, fd, parse_instance_uuid)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_network_config_schema() -> NetworkConfigSchema {
|
||||
build_network_config_schema()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_network_config_field_mappings() -> Vec<ConfigFieldMapping> {
|
||||
build_network_config_field_mappings()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn exported_plain_object_schema_contains_core_networkconfig_metadata() {
|
||||
let schema = get_network_config_schema();
|
||||
assert_eq!(schema.name, "NetworkConfig");
|
||||
assert_eq!(schema.node_kind, "schema");
|
||||
assert!(
|
||||
schema
|
||||
.children
|
||||
.iter()
|
||||
.any(|field| field.name == "network_name")
|
||||
);
|
||||
let secure_mode = schema
|
||||
.children
|
||||
.iter()
|
||||
.find(|field| field.name == "secure_mode")
|
||||
.expect("secure_mode field");
|
||||
assert!(
|
||||
secure_mode
|
||||
.children
|
||||
.iter()
|
||||
.any(|field| field.name == "enabled")
|
||||
);
|
||||
pub fn set_tun_fd(inst_id: String, fd: i32) -> bool {
|
||||
match Uuid::try_parse(&inst_id) {
|
||||
Ok(uuid) => match INSTANCE_MANAGER.set_tun_fd(&uuid, fd) {
|
||||
Ok(_) => {
|
||||
hilog_debug!("[Rust] set tun fd {} to {}.", fd, inst_id);
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] cant set tun fd {} to {}. {}", fd, inst_id, e);
|
||||
false
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] cant covert {} to uuid. {}", inst_id, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_runtime_snapshot() -> RuntimeAggregateState {
|
||||
exports::runtime_api::get_runtime_snapshot()
|
||||
}
|
||||
|
||||
pub(crate) fn get_runtime_snapshot_inner() -> RuntimeAggregateState {
|
||||
exports::runtime_api::get_runtime_snapshot_inner()
|
||||
pub fn default_network_config() -> String {
|
||||
match NetworkConfig::new_from_config(TomlConfigLoader::default()) {
|
||||
Ok(result) => serde_json::to_string(&result).unwrap_or_else(|e| format!("ERROR {}", e)),
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] default_network_config failed {}", e);
|
||||
format!("ERROR {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn build_config_share_link(config_id: String, only_start: Option<bool>) -> Option<String> {
|
||||
build_config_share_link_inner(&config_id, None, only_start.unwrap_or(false))
|
||||
pub fn convert_toml_to_network_config(cfg_str: String) -> String {
|
||||
match TomlConfigLoader::new_from_str(&cfg_str) {
|
||||
Ok(cfg) => match NetworkConfig::new_from_config(cfg) {
|
||||
Ok(result) => serde_json::to_string(&result).unwrap_or_else(|e| format!("ERROR {}", e)),
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] convert_toml_to_network_config failed {}", e);
|
||||
format!("ERROR {}", e)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] convert_toml_to_network_config failed {}", e);
|
||||
format!("ERROR {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn parse_config_share_link(share_link: String) -> Option<SharedConfigLinkPayload> {
|
||||
parse_config_share_link_inner(&share_link)
|
||||
pub fn parse_network_config(cfg_json: String) -> bool {
|
||||
match serde_json::from_str::<NetworkConfig>(&cfg_json) {
|
||||
Ok(cfg) => match cfg.gen_config() {
|
||||
Ok(toml) => {
|
||||
hilog_debug!("[Rust] Convert to Toml {}", toml.dump());
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] parse config failed {}", e);
|
||||
false
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] parse config failed {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn import_config_share_link(
|
||||
share_link: String,
|
||||
display_name_override: Option<String>,
|
||||
) -> Option<String> {
|
||||
import_config_share_link_inner(&share_link, display_name_override)
|
||||
pub fn run_network_instance(cfg_json: String) -> bool {
|
||||
let cfg = match serde_json::from_str::<NetworkConfig>(&cfg_json) {
|
||||
Ok(cfg) => match cfg.gen_config() {
|
||||
Ok(toml) => toml,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] parse config failed {}", e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] parse config failed {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if INSTANCE_MANAGER.list_network_instance_ids().len() > 0 {
|
||||
hilog_error!("[Rust] there is a running instance!");
|
||||
return false;
|
||||
}
|
||||
|
||||
let inst_id = cfg.get_id();
|
||||
if INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.contains(&inst_id)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
true
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn stop_network_instance(inst_names: Vec<String>) {
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(
|
||||
inst_names
|
||||
.into_iter()
|
||||
.filter_map(|s| Uuid::parse_str(&s).ok())
|
||||
.collect(),
|
||||
)
|
||||
.unwrap();
|
||||
hilog_debug!("[Rust] stop_network_instance");
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn collect_network_infos() -> Vec<KeyValuePair> {
|
||||
let mut result = Vec::new();
|
||||
match INSTANCE_MANAGER.collect_network_infos_sync() {
|
||||
Ok(map) => {
|
||||
for (uuid, info) in map.iter() {
|
||||
// convert value to json string
|
||||
let value = match serde_json::to_string(&info) {
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] failed to serialize instance {} info: {}", uuid, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
result.push(KeyValuePair {
|
||||
key: uuid.clone().to_string(),
|
||||
value: value.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn collect_running_network() -> Vec<String> {
|
||||
INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn is_running_network(inst_id: String) -> bool {
|
||||
match Uuid::try_parse(&inst_id) {
|
||||
Ok(uuid) => INSTANCE_MANAGER.list_network_instance_ids().contains(&uuid),
|
||||
Err(e) => {
|
||||
hilog_error!("[Rust] cant covert {} to uuid. {}", inst_id, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub(crate) mod logging;
|
||||
@@ -1 +0,0 @@
|
||||
pub(crate) mod native_log;
|
||||
@@ -1 +0,0 @@
|
||||
pub(crate) mod state;
|
||||
@@ -1 +0,0 @@
|
||||
pub(crate) mod runtime_state;
|
||||
@@ -1,293 +0,0 @@
|
||||
use easytier::proto::{api, common};
|
||||
use napi_derive_ohos::napi;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static ATTACHED_TUN_INSTANCE_IDS: once_cell::sync::Lazy<Mutex<HashSet<String>>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(HashSet::new()));
|
||||
|
||||
pub fn mark_tun_attached(instance_id: &str) {
|
||||
if let Ok(mut guard) = ATTACHED_TUN_INSTANCE_IDS.lock() {
|
||||
guard.insert(instance_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_tun_attached(instance_id: &str) {
|
||||
if let Ok(mut guard) = ATTACHED_TUN_INSTANCE_IDS.lock() {
|
||||
guard.remove(instance_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_tun_attached(instance_id: &str) -> bool {
|
||||
ATTACHED_TUN_INSTANCE_IDS
|
||||
.lock()
|
||||
.map(|guard| guard.contains(instance_id))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct PeerConnStats {
|
||||
pub rx_bytes: i64,
|
||||
pub tx_bytes: i64,
|
||||
pub rx_packets: i64,
|
||||
pub tx_packets: i64,
|
||||
pub latency_us: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct PeerConnInfo {
|
||||
pub conn_id: String,
|
||||
pub my_peer_id: i64,
|
||||
pub peer_id: i64,
|
||||
pub features: Vec<String>,
|
||||
pub tunnel_type: Option<String>,
|
||||
pub local_addr: Option<String>,
|
||||
pub remote_addr: Option<String>,
|
||||
pub resolved_remote_addr: Option<String>,
|
||||
pub stats: Option<PeerConnStats>,
|
||||
pub loss_rate: Option<f64>,
|
||||
pub is_client: bool,
|
||||
pub network_name: Option<String>,
|
||||
pub is_closed: bool,
|
||||
pub secure_auth_level: Option<i32>,
|
||||
pub peer_identity_type: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct PeerInfo {
|
||||
pub peer_id: i64,
|
||||
pub default_conn_id: Option<String>,
|
||||
pub directly_connected_conns: Vec<String>,
|
||||
pub conns: Vec<PeerConnInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct RouteView {
|
||||
pub peer_id: i64,
|
||||
pub hostname: Option<String>,
|
||||
pub ipv4: Option<String>,
|
||||
pub ipv4_cidr: Option<String>,
|
||||
pub ipv6_cidr: Option<String>,
|
||||
pub proxy_cidrs: Vec<String>,
|
||||
pub next_hop_peer_id: Option<i64>,
|
||||
pub cost: Option<i32>,
|
||||
pub path_latency: Option<i64>,
|
||||
pub udp_nat_type: Option<i32>,
|
||||
pub tcp_nat_type: Option<i32>,
|
||||
pub inst_id: Option<String>,
|
||||
pub version: Option<String>,
|
||||
pub is_public_server: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct MyNodeInfo {
|
||||
pub virtual_ipv4: Option<String>,
|
||||
pub virtual_ipv4_cidr: Option<String>,
|
||||
pub hostname: Option<String>,
|
||||
pub version: Option<String>,
|
||||
pub peer_id: Option<i64>,
|
||||
pub listeners: Vec<String>,
|
||||
pub vpn_portal_cfg: Option<String>,
|
||||
pub udp_nat_type: Option<i32>,
|
||||
pub tcp_nat_type: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct RuntimeInstanceState {
|
||||
pub config_id: String,
|
||||
pub instance_id: String,
|
||||
pub display_name: String,
|
||||
pub running: bool,
|
||||
pub tun_required: bool,
|
||||
pub tun_attached: bool,
|
||||
pub magic_dns_enabled: bool,
|
||||
pub need_exit_node: bool,
|
||||
pub error_message: Option<String>,
|
||||
pub my_node_info: Option<MyNodeInfo>,
|
||||
pub events: Vec<String>,
|
||||
pub routes: Vec<RouteView>,
|
||||
pub peers: Vec<PeerInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct TunAggregateState {
|
||||
pub active: bool,
|
||||
pub attached_instance_ids: Vec<String>,
|
||||
pub aggregated_routes: Vec<String>,
|
||||
pub dns_servers: Vec<String>,
|
||||
pub need_rebuild: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct RuntimeAggregateState {
|
||||
pub instances: Vec<RuntimeInstanceState>,
|
||||
pub tun: TunAggregateState,
|
||||
pub running_instance_count: i32,
|
||||
}
|
||||
|
||||
fn stringify_ipv4_inet(value: Option<common::Ipv4Inet>) -> Option<String> {
|
||||
value.map(|v| v.to_string())
|
||||
}
|
||||
|
||||
fn stringify_ipv6_inet(value: Option<common::Ipv6Inet>) -> Option<String> {
|
||||
value.map(|v| v.to_string())
|
||||
}
|
||||
|
||||
fn stringify_url(value: Option<common::Url>) -> Option<String> {
|
||||
value.map(|v| v.to_string())
|
||||
}
|
||||
|
||||
fn stringify_uuid(value: Option<common::Uuid>) -> Option<String> {
|
||||
value.map(|v| v.to_string())
|
||||
}
|
||||
|
||||
fn optional_u32_to_i64(value: Option<u32>) -> Option<i64> {
|
||||
value.map(|v| v as i64)
|
||||
}
|
||||
|
||||
fn optional_i32_to_i64(value: Option<i32>) -> Option<i64> {
|
||||
value.map(|v| v as i64)
|
||||
}
|
||||
|
||||
fn route_to_view(route: api::instance::Route) -> RouteView {
|
||||
let stun = route.stun_info;
|
||||
let feature_flag = route.feature_flag;
|
||||
RouteView {
|
||||
peer_id: route.peer_id as i64,
|
||||
hostname: (!route.hostname.is_empty()).then_some(route.hostname),
|
||||
ipv4: route
|
||||
.ipv4_addr
|
||||
.as_ref()
|
||||
.and_then(|inet| inet.address.as_ref())
|
||||
.map(|addr| addr.to_string()),
|
||||
ipv4_cidr: stringify_ipv4_inet(route.ipv4_addr),
|
||||
ipv6_cidr: stringify_ipv6_inet(route.ipv6_addr),
|
||||
proxy_cidrs: route.proxy_cidrs,
|
||||
next_hop_peer_id: optional_u32_to_i64(route.next_hop_peer_id_latency_first)
|
||||
.or_else(|| Some(route.next_hop_peer_id as i64)),
|
||||
cost: Some(route.cost),
|
||||
path_latency: optional_i32_to_i64(route.path_latency_latency_first)
|
||||
.or_else(|| Some(route.path_latency as i64)),
|
||||
udp_nat_type: stun.as_ref().map(|info| info.udp_nat_type),
|
||||
tcp_nat_type: stun.as_ref().map(|info| info.tcp_nat_type),
|
||||
inst_id: (!route.inst_id.is_empty()).then_some(route.inst_id),
|
||||
version: (!route.version.is_empty()).then_some(route.version),
|
||||
is_public_server: feature_flag.map(|flag| flag.is_public_server),
|
||||
}
|
||||
}
|
||||
|
||||
fn peer_conn_to_view(conn: api::instance::PeerConnInfo) -> PeerConnInfo {
|
||||
let stats = conn.stats.map(|stats| PeerConnStats {
|
||||
rx_bytes: stats.rx_bytes as i64,
|
||||
tx_bytes: stats.tx_bytes as i64,
|
||||
rx_packets: stats.rx_packets as i64,
|
||||
tx_packets: stats.tx_packets as i64,
|
||||
latency_us: stats.latency_us as i64,
|
||||
});
|
||||
|
||||
PeerConnInfo {
|
||||
conn_id: conn.conn_id,
|
||||
my_peer_id: conn.my_peer_id as i64,
|
||||
peer_id: conn.peer_id as i64,
|
||||
features: conn.features,
|
||||
tunnel_type: conn.tunnel.as_ref().map(|t| t.tunnel_type.clone()),
|
||||
local_addr: conn
|
||||
.tunnel
|
||||
.as_ref()
|
||||
.and_then(|t| stringify_url(t.local_addr.clone())),
|
||||
remote_addr: conn
|
||||
.tunnel
|
||||
.as_ref()
|
||||
.and_then(|t| stringify_url(t.remote_addr.clone())),
|
||||
resolved_remote_addr: conn
|
||||
.tunnel
|
||||
.as_ref()
|
||||
.and_then(|t| stringify_url(t.resolved_remote_addr.clone())),
|
||||
stats,
|
||||
loss_rate: Some(conn.loss_rate as f64),
|
||||
is_client: conn.is_client,
|
||||
network_name: (!conn.network_name.is_empty()).then_some(conn.network_name),
|
||||
is_closed: conn.is_closed,
|
||||
secure_auth_level: Some(conn.secure_auth_level),
|
||||
peer_identity_type: Some(conn.peer_identity_type),
|
||||
}
|
||||
}
|
||||
|
||||
fn peer_to_view(peer: api::instance::PeerInfo) -> PeerInfo {
|
||||
PeerInfo {
|
||||
peer_id: peer.peer_id as i64,
|
||||
default_conn_id: stringify_uuid(peer.default_conn_id),
|
||||
directly_connected_conns: peer
|
||||
.directly_connected_conns
|
||||
.into_iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect(),
|
||||
conns: peer.conns.into_iter().map(peer_conn_to_view).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn my_node_info_to_view(info: api::manage::MyNodeInfo) -> MyNodeInfo {
|
||||
MyNodeInfo {
|
||||
virtual_ipv4: info
|
||||
.virtual_ipv4
|
||||
.as_ref()
|
||||
.and_then(|inet| inet.address.as_ref())
|
||||
.map(|addr| addr.to_string()),
|
||||
virtual_ipv4_cidr: stringify_ipv4_inet(info.virtual_ipv4),
|
||||
hostname: (!info.hostname.is_empty()).then_some(info.hostname),
|
||||
version: (!info.version.is_empty()).then_some(info.version),
|
||||
peer_id: Some(info.peer_id as i64),
|
||||
listeners: info
|
||||
.listeners
|
||||
.into_iter()
|
||||
.map(|url| url.to_string())
|
||||
.collect(),
|
||||
vpn_portal_cfg: info.vpn_portal_cfg,
|
||||
udp_nat_type: info.stun_info.as_ref().map(|stun| stun.udp_nat_type),
|
||||
tcp_nat_type: info.stun_info.as_ref().map(|stun| stun.tcp_nat_type),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn runtime_instance_from_running_info(
|
||||
config_id: String,
|
||||
display_name: String,
|
||||
magic_dns_enabled: bool,
|
||||
need_exit_node: bool,
|
||||
info: api::manage::NetworkInstanceRunningInfo,
|
||||
) -> RuntimeInstanceState {
|
||||
let tun_attached = info.running && is_tun_attached(&config_id);
|
||||
let tun_required = info.running && (info.dev_name != "no_tun" || tun_attached);
|
||||
|
||||
RuntimeInstanceState {
|
||||
config_id: config_id.clone(),
|
||||
instance_id: config_id,
|
||||
display_name,
|
||||
running: info.running,
|
||||
tun_required,
|
||||
tun_attached,
|
||||
magic_dns_enabled,
|
||||
need_exit_node,
|
||||
error_message: info.error_msg,
|
||||
my_node_info: info.my_node_info.map(my_node_info_to_view),
|
||||
events: info.events,
|
||||
routes: info.routes.into_iter().map(route_to_view).collect(),
|
||||
peers: info.peers.into_iter().map(peer_to_view).collect(),
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tauri + Vue + TS</title>
|
||||
</head>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "easytier-gui",
|
||||
"type": "module",
|
||||
"version": "2.6.4",
|
||||
"version": "2.6.2",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build:deps": "pnpm --filter tauri-plugin-vpnservice-api build && pnpm --filter easytier-frontend-lib build",
|
||||
"dev": "pnpm run build:deps && vite",
|
||||
"build": "pnpm run build:deps && vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"lint": "eslint . --ignore-pattern src-tauri",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "easytier-gui"
|
||||
version = "2.6.4"
|
||||
version = "2.6.2"
|
||||
description = "EasyTier GUI"
|
||||
authors = ["you"]
|
||||
edition.workspace = true
|
||||
|
||||
@@ -15,7 +15,9 @@ use easytier::rpc_service::remote_client::{
|
||||
use easytier::web_client::{self, WebClient};
|
||||
use easytier::{
|
||||
common::{
|
||||
config::{ConfigLoader, ConfigSource, FileLoggerConfig, LoggingConfig, TomlConfigLoader},
|
||||
config::{
|
||||
ConfigLoader, ConfigSource, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader,
|
||||
},
|
||||
log,
|
||||
},
|
||||
instance_manager::NetworkInstanceManager,
|
||||
@@ -488,18 +490,10 @@ async fn init_web_client(app: AppHandle, url: Option<String>) -> Result<(), Stri
|
||||
.ok_or_else(|| "Instance manager is not available".to_string())?;
|
||||
|
||||
let hooks = Arc::new(manager::GuiHooks { app: app.clone() });
|
||||
let machine_id_state_dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.with_context(|| "Failed to resolve machine id state directory")
|
||||
.map_err(|e| format!("{:#}", e))?;
|
||||
|
||||
let web_client = web_client::run_web_client(
|
||||
url.as_str(),
|
||||
easytier::common::MachineIdOptions {
|
||||
explicit_machine_id: None,
|
||||
state_dir: Some(machine_id_state_dir),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
instance_manager,
|
||||
@@ -1324,7 +1318,7 @@ pub fn run_gui() -> std::process::ExitCode {
|
||||
let Ok(log_dir) = get_log_dir(app.app_handle()) else {
|
||||
return Ok(());
|
||||
};
|
||||
let config = LoggingConfig::builder()
|
||||
let config = LoggingConfigBuilder::default()
|
||||
.file_logger(FileLoggerConfig {
|
||||
dir: Some(log_dir.to_string_lossy().to_string()),
|
||||
level: None,
|
||||
@@ -1332,7 +1326,8 @@ pub fn run_gui() -> std::process::ExitCode {
|
||||
size_mb: None,
|
||||
count: None,
|
||||
})
|
||||
.build();
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let Ok(_) = log::init(&config, true) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"createUpdaterArtifacts": false
|
||||
},
|
||||
"productName": "easytier-gui",
|
||||
"version": "2.6.4",
|
||||
"version": "2.6.2",
|
||||
"identifier": "com.kkrainbow.easytier",
|
||||
"plugins": {
|
||||
"shell": {
|
||||
|
||||
@@ -33,6 +33,7 @@ const host = process.env.TAURI_DEV_HOST
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
base: './',
|
||||
resolve: {
|
||||
alias: {
|
||||
'~/': `${path.resolve(__dirname, 'src')}/`,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "easytier-rpc-build"
|
||||
description = "Protobuf RPC Service Generator for EasyTier"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasyTier"
|
||||
authors = ["kkrainbow"]
|
||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||
categories = ["network-programming", "command-line-utilities"]
|
||||
license-file = "LICENSE"
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
heck = "0.5"
|
||||
prost-build = "0.13"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
internal-namespace = []
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../LICENSE
|
||||
@@ -0,0 +1,3 @@
|
||||
# Introduction
|
||||
|
||||
This is a protobuf rpc service stub generator for [EasyTier](https://github.com/EasyTier/EasyTier) project.
|
||||
@@ -0,0 +1,449 @@
|
||||
extern crate heck;
|
||||
extern crate prost_build;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
#[cfg(feature = "internal-namespace")]
|
||||
const NAMESPACE: &str = "crate::proto::rpc_types";
|
||||
|
||||
#[cfg(not(feature = "internal-namespace"))]
|
||||
const NAMESPACE: &str = "easytier::proto::rpc_types";
|
||||
|
||||
/// The service generator to be used with `prost-build` to generate RPC implementations for
|
||||
/// `prost-simple-rpc`.
|
||||
///
|
||||
/// See the crate-level documentation for more info.
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ServiceGenerator {
|
||||
_private: (),
|
||||
}
|
||||
|
||||
impl prost_build::ServiceGenerator for ServiceGenerator {
|
||||
fn generate(&mut self, service: prost_build::Service, mut buf: &mut String) {
|
||||
use std::fmt::Write;
|
||||
|
||||
let descriptor_name = format!("{}Descriptor", service.name);
|
||||
let server_name = format!("{}Server", service.name);
|
||||
let client_name = format!("{}Client", service.name);
|
||||
let method_descriptor_name = format!("{}MethodDescriptor", service.name);
|
||||
|
||||
let mut trait_methods = String::new();
|
||||
let mut weak_impl_methods = String::new();
|
||||
let mut enum_methods = String::new();
|
||||
let mut list_enum_methods = String::new();
|
||||
let mut client_methods = String::new();
|
||||
let mut client_own_methods = String::new();
|
||||
let mut match_name_methods = String::new();
|
||||
let mut match_proto_name_methods = String::new();
|
||||
let mut match_input_type_methods = String::new();
|
||||
let mut match_input_proto_type_methods = String::new();
|
||||
let mut match_output_type_methods = String::new();
|
||||
let mut match_output_proto_type_methods = String::new();
|
||||
let mut match_handle_methods = String::new();
|
||||
// generate trait default method Xxx::json_call_method match branch
|
||||
let mut match_trait_json_methods = String::new();
|
||||
|
||||
let mut match_method_try_from = String::new();
|
||||
|
||||
for (idx, method) in service.methods.iter().enumerate() {
|
||||
assert!(
|
||||
!method.client_streaming,
|
||||
"Client streaming not yet supported for method {}",
|
||||
method.proto_name
|
||||
);
|
||||
assert!(
|
||||
!method.server_streaming,
|
||||
"Server streaming not yet supported for method {}",
|
||||
method.proto_name
|
||||
);
|
||||
|
||||
ServiceGenerator::write_comments(&mut trait_methods, 4, &method.comments).unwrap();
|
||||
writeln!(
|
||||
trait_methods,
|
||||
r#" async fn {name}(&self, ctrl: Self::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}>;"#,
|
||||
name = method.name,
|
||||
input_type = method.input_type,
|
||||
output_type = method.output_type,
|
||||
namespace = NAMESPACE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(
|
||||
weak_impl_methods,
|
||||
r#" async fn {method_name}(&self, ctrl: Self::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}> {{
|
||||
let Some(service) = self.upgrade() else {{
|
||||
return Err({namespace}::error::Error::Shutdown);
|
||||
}};
|
||||
service.{method_name}(ctrl, input).await
|
||||
}}"#,
|
||||
method_name = method.name,
|
||||
input_type = method.input_type,
|
||||
output_type = method.output_type,
|
||||
namespace = NAMESPACE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
ServiceGenerator::write_comments(&mut enum_methods, 4, &method.comments).unwrap();
|
||||
writeln!(
|
||||
enum_methods,
|
||||
" {name} = {index},",
|
||||
name = method.proto_name,
|
||||
index = idx + 1
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(
|
||||
match_method_try_from,
|
||||
" {index} => Ok({service_name}MethodDescriptor::{name}),",
|
||||
service_name = service.name,
|
||||
name = method.proto_name,
|
||||
index = idx + 1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(
|
||||
list_enum_methods,
|
||||
" {service_name}MethodDescriptor::{name},",
|
||||
service_name = service.name,
|
||||
name = method.proto_name
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(
|
||||
client_methods,
|
||||
r#" async fn {name}(&self, ctrl: H::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}> {{
|
||||
{client_name}Client::{name}_inner(self.0.clone(), ctrl, input).await
|
||||
}}"#,
|
||||
name = method.name,
|
||||
input_type = method.input_type,
|
||||
output_type = method.output_type,
|
||||
client_name = service.name,
|
||||
namespace = NAMESPACE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(
|
||||
client_own_methods,
|
||||
r#" async fn {name}_inner(handler: H, ctrl: H::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}> {{
|
||||
{namespace}::__rt::call_method(handler, ctrl, {method_descriptor_name}::{proto_name}, input).await
|
||||
}}"#,
|
||||
name = method.name,
|
||||
method_descriptor_name = method_descriptor_name,
|
||||
proto_name = method.proto_name,
|
||||
input_type = method.input_type,
|
||||
output_type = method.output_type,
|
||||
namespace = NAMESPACE,
|
||||
).unwrap();
|
||||
|
||||
let case = format!(
|
||||
" {service_name}MethodDescriptor::{proto_name} => ",
|
||||
service_name = service.name,
|
||||
proto_name = method.proto_name
|
||||
);
|
||||
|
||||
writeln!(match_name_methods, "{}{:?},", case, method.name).unwrap();
|
||||
writeln!(match_proto_name_methods, "{}{:?},", case, method.proto_name).unwrap();
|
||||
writeln!(
|
||||
match_input_type_methods,
|
||||
"{}::std::any::TypeId::of::<{}>(),",
|
||||
case, method.input_type
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
match_input_proto_type_methods,
|
||||
"{}{:?},",
|
||||
case, method.input_proto_type
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
match_output_type_methods,
|
||||
"{}::std::any::TypeId::of::<{}>(),",
|
||||
case, method.output_type
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
match_output_proto_type_methods,
|
||||
"{}{:?},",
|
||||
case, method.output_proto_type
|
||||
)
|
||||
.unwrap();
|
||||
write!(
|
||||
match_handle_methods,
|
||||
r#"{} {{
|
||||
let decoded: {input_type} = {namespace}::__rt::decode(input)?;
|
||||
let ret = service.{name}(ctrl, decoded).await?;
|
||||
{namespace}::__rt::encode(ret)
|
||||
}}
|
||||
"#,
|
||||
case,
|
||||
input_type = method.input_type,
|
||||
name = method.name,
|
||||
namespace = NAMESPACE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
write!(
|
||||
match_trait_json_methods,
|
||||
r#" "{name}" | "{proto_name}" => {{
|
||||
let req: {input_type} = ::serde_json::from_value(json).map_err(|e| {namespace}::error::Error::MalformatRpcPacket(format!("json error: {{}}", e)))?;
|
||||
let resp = self.{typed_method}(ctrl, req).await?;
|
||||
Ok(::serde_json::to_value(resp).map_err(|e| {namespace}::error::Error::MalformatRpcPacket(format!("json error: {{}}", e)))?)
|
||||
}}
|
||||
"#,
|
||||
name = method.name,
|
||||
proto_name = method.proto_name,
|
||||
input_type = method.input_type,
|
||||
typed_method = method.name,
|
||||
namespace = NAMESPACE,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
ServiceGenerator::write_comments(&mut buf, 0, &service.comments).unwrap();
|
||||
write!(
|
||||
buf,
|
||||
r#"
|
||||
#[async_trait::async_trait]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait {name} {{
|
||||
type Controller: {namespace}::controller::Controller;
|
||||
|
||||
{trait_methods}
|
||||
|
||||
async fn json_call_method(
|
||||
&self,
|
||||
ctrl: Self::Controller,
|
||||
method_name: &str,
|
||||
json: ::serde_json::Value,
|
||||
) -> {namespace}::error::Result<::serde_json::Value> {{
|
||||
match method_name {{
|
||||
{match_trait_json_methods}
|
||||
_ => Err({namespace}::error::Error::InvalidMethodIndex(0, method_name.to_string())),
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<T> {name} for ::std::sync::Weak<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
::std::sync::Arc<T>: {name},
|
||||
{{
|
||||
type Controller = <::std::sync::Arc<T> as {name}>::Controller;
|
||||
|
||||
{weak_impl_methods}
|
||||
}}
|
||||
|
||||
/// A service descriptor for a `{name}`.
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Default)]
|
||||
pub struct {descriptor_name};
|
||||
|
||||
/// Methods available on a `{name}`.
|
||||
///
|
||||
/// This can be used as a key when routing requests for servers/clients of a `{name}`.
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
#[repr(u8)]
|
||||
pub enum {method_descriptor_name} {{
|
||||
{enum_methods}
|
||||
}}
|
||||
|
||||
impl std::convert::TryFrom<u8> for {method_descriptor_name} {{
|
||||
type Error = {namespace}::error::Error;
|
||||
fn try_from(value: u8) -> {namespace}::error::Result<Self> {{
|
||||
match value {{
|
||||
{match_method_try_from}
|
||||
_ => Err({namespace}::error::Error::InvalidMethodIndex(value, "{name}".to_string())),
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
/// A client for a `{name}`.
|
||||
///
|
||||
/// This implements the `{name}` trait by dispatching all method calls to the supplied `Handler`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct {client_name}<H>(H) where H: {namespace}::handler::Handler;
|
||||
|
||||
impl<H> {client_name}<H> where H: {namespace}::handler::Handler<Descriptor = {descriptor_name}> {{
|
||||
/// Creates a new client instance that delegates all method calls to the supplied handler.
|
||||
pub fn new(handler: H) -> {client_name}<H> {{
|
||||
{client_name}(handler)
|
||||
}}
|
||||
}}
|
||||
|
||||
impl<H> {client_name}<H> where H: {namespace}::handler::Handler<Descriptor = {descriptor_name}> {{
|
||||
{client_own_methods}
|
||||
}}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<H> {name} for {client_name}<H> where H: {namespace}::handler::Handler<Descriptor = {descriptor_name}> {{
|
||||
type Controller = H::Controller;
|
||||
|
||||
{client_methods}
|
||||
}}
|
||||
|
||||
pub struct {client_name}Factory<C: {namespace}::controller::Controller>(std::marker::PhantomData<C>);
|
||||
|
||||
impl<C: {namespace}::controller::Controller> Clone for {client_name}Factory<C> {{
|
||||
fn clone(&self) -> Self {{
|
||||
Self(std::marker::PhantomData)
|
||||
}}
|
||||
}}
|
||||
|
||||
impl<C> {namespace}::__rt::RpcClientFactory for {client_name}Factory<C> where C: {namespace}::controller::Controller {{
|
||||
type Descriptor = {descriptor_name};
|
||||
type ClientImpl = Box<dyn {name}<Controller = C> + Send + Sync + 'static>;
|
||||
type Controller = C;
|
||||
|
||||
fn new(handler: impl {namespace}::handler::Handler<Descriptor = Self::Descriptor, Controller = Self::Controller>) -> Self::ClientImpl {{
|
||||
Box::new({client_name}::new(handler))
|
||||
}}
|
||||
}}
|
||||
|
||||
/// A server for a `{name}`.
|
||||
///
|
||||
/// This implements the `Server` trait by handling requests and dispatch them to methods on the
|
||||
/// supplied `{name}`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct {server_name}<A>(A) where A: {name} + Clone + Send + 'static;
|
||||
|
||||
impl<T> {server_name}<::std::sync::Weak<T>>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
::std::sync::Arc<T>: {name},
|
||||
{{
|
||||
pub fn new_arc(service: ::std::sync::Arc<T>) -> {server_name}<::std::sync::Weak<T>> {{
|
||||
{server_name}(::std::sync::Arc::downgrade(&service))
|
||||
}}
|
||||
}}
|
||||
|
||||
impl<A> {server_name}<A> where A: {name} + Clone + Send + 'static {{
|
||||
/// Creates a new server instance that dispatches all calls to the supplied service.
|
||||
pub fn new(service: A) -> {server_name}<A> {{
|
||||
{server_name}(service)
|
||||
}}
|
||||
|
||||
async fn call_inner(
|
||||
service: A,
|
||||
method: {method_descriptor_name},
|
||||
ctrl: A::Controller,
|
||||
input: ::bytes::Bytes)
|
||||
-> {namespace}::error::Result<::bytes::Bytes> {{
|
||||
match method {{
|
||||
{match_handle_methods}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
impl {namespace}::descriptor::ServiceDescriptor for {descriptor_name} {{
|
||||
type Method = {method_descriptor_name};
|
||||
fn name(&self) -> &'static str {{ {name:?} }}
|
||||
fn proto_name(&self) -> &'static str {{ {proto_name:?} }}
|
||||
fn package(&self) -> &'static str {{ {package:?} }}
|
||||
fn methods(&self) -> &'static [Self::Method] {{
|
||||
&[ {list_enum_methods} ]
|
||||
}}
|
||||
}}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<A> {namespace}::handler::Handler for {server_name}<A>
|
||||
where
|
||||
A: {name} + Clone + Send + Sync + 'static {{
|
||||
type Descriptor = {descriptor_name};
|
||||
type Controller = A::Controller;
|
||||
|
||||
async fn call(
|
||||
&self,
|
||||
ctrl: A::Controller,
|
||||
method: {method_descriptor_name},
|
||||
input: ::bytes::Bytes)
|
||||
-> {namespace}::error::Result<::bytes::Bytes> {{
|
||||
{server_name}::call_inner(self.0.clone(), method, ctrl, input).await
|
||||
}}
|
||||
}}
|
||||
|
||||
impl {namespace}::descriptor::MethodDescriptor for {method_descriptor_name} {{
|
||||
fn name(&self) -> &'static str {{
|
||||
match *self {{
|
||||
{match_name_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn proto_name(&self) -> &'static str {{
|
||||
match *self {{
|
||||
{match_proto_name_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn input_type(&self) -> ::std::any::TypeId {{
|
||||
match *self {{
|
||||
{match_input_type_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn input_proto_type(&self) -> &'static str {{
|
||||
match *self {{
|
||||
{match_input_proto_type_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn output_type(&self) -> ::std::any::TypeId {{
|
||||
match *self {{
|
||||
{match_output_type_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn output_proto_type(&self) -> &'static str {{
|
||||
match *self {{
|
||||
{match_output_proto_type_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn index(&self) -> u8 {{
|
||||
*self as u8
|
||||
}}
|
||||
}}
|
||||
"#,
|
||||
name = service.name,
|
||||
descriptor_name = descriptor_name,
|
||||
server_name = server_name,
|
||||
client_name = client_name,
|
||||
method_descriptor_name = method_descriptor_name,
|
||||
proto_name = service.proto_name,
|
||||
package = service.package,
|
||||
trait_methods = trait_methods,
|
||||
weak_impl_methods = weak_impl_methods,
|
||||
enum_methods = enum_methods,
|
||||
list_enum_methods = list_enum_methods,
|
||||
client_own_methods = client_own_methods,
|
||||
client_methods = client_methods,
|
||||
match_name_methods = match_name_methods,
|
||||
match_proto_name_methods = match_proto_name_methods,
|
||||
match_input_type_methods = match_input_type_methods,
|
||||
match_input_proto_type_methods = match_input_proto_type_methods,
|
||||
match_output_type_methods = match_output_type_methods,
|
||||
match_output_proto_type_methods = match_output_proto_type_methods,
|
||||
match_handle_methods = match_handle_methods,
|
||||
match_trait_json_methods = match_trait_json_methods,
|
||||
namespace = NAMESPACE,
|
||||
).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceGenerator {
|
||||
fn write_comments<W>(
|
||||
mut write: W,
|
||||
indent: usize,
|
||||
comments: &prost_build::Comments,
|
||||
) -> fmt::Result
|
||||
where
|
||||
W: fmt::Write,
|
||||
{
|
||||
for comment in &comments.leading {
|
||||
for line in comment.lines().filter(|s| !s.is_empty()) {
|
||||
writeln!(write, "{}///{}", " ".repeat(indent), line)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "easytier-web"
|
||||
version = "2.6.4"
|
||||
version = "2.6.2"
|
||||
edition.workspace = true
|
||||
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
||||
|
||||
[dependencies]
|
||||
easytier = { path = "../easytier" }
|
||||
easytier = { path = "../easytier", default-features = false, features = ["websocket"] }
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
anyhow = { version = "1.0" }
|
||||
thiserror = "1.0"
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"test": "vitest run",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -43,10 +44,11 @@
|
||||
"typescript": "~5.6.3",
|
||||
"vite": "^5.4.21",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vitest": "^2.1.9",
|
||||
"vue-tsc": "^2.1.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.12",
|
||||
"primevue": "^4.3.9"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,6 @@ const bool_flags: BoolFlag[] = [
|
||||
{ field: 'latency_first', help: 'latency_first_help' },
|
||||
{ field: 'use_smoltcp', help: 'use_smoltcp_help' },
|
||||
{ field: 'disable_ipv6', help: 'disable_ipv6_help' },
|
||||
{ field: 'ipv6_public_addr_auto', help: 'ipv6_public_addr_auto_help' },
|
||||
{ field: 'enable_kcp_proxy', help: 'enable_kcp_proxy_help' },
|
||||
{ field: 'disable_kcp_input', help: 'disable_kcp_input_help' },
|
||||
{ field: 'enable_quic_proxy', help: 'enable_quic_proxy_help' },
|
||||
@@ -99,8 +98,6 @@ const bool_flags: BoolFlag[] = [
|
||||
{ field: 'disable_encryption', help: 'disable_encryption_help' },
|
||||
{ field: 'disable_tcp_hole_punching', help: 'disable_tcp_hole_punching_help' },
|
||||
{ field: 'disable_udp_hole_punching', help: 'disable_udp_hole_punching_help' },
|
||||
{ field: 'enable_udp_broadcast_relay', help: 'enable_udp_broadcast_relay_help' },
|
||||
{ field: 'disable_upnp', help: 'disable_upnp_help' },
|
||||
{ field: 'disable_sym_hole_punching', help: 'disable_sym_hole_punching_help' },
|
||||
{ field: 'enable_magic_dns', help: 'enable_magic_dns_help' },
|
||||
{ field: 'enable_private_mode', help: 'enable_private_mode_help' },
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import { AutoComplete, Button, Dialog, InputNumber, InputText } from 'primevue'
|
||||
import InputGroup from 'primevue/inputgroup'
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { buildUrlInputValue, getHostInputValue, parseHostInputOnBlur, parseUrlInput } from '../modules/url-input'
|
||||
|
||||
const props = defineProps<{
|
||||
placeholder?: string
|
||||
@@ -13,75 +14,30 @@ const props = defineProps<{
|
||||
const { t } = useI18n()
|
||||
const url = defineModel<string>({ required: true })
|
||||
const editing = ref(false)
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const internalCompact = ref(false)
|
||||
const hostFocused = ref(false)
|
||||
|
||||
const parseUrl = (val: string | null | undefined): { proto: string; host: string; port: number | null } => {
|
||||
const getValidPort = (portStr: string, proto: string) => {
|
||||
const p = parseInt(portStr)
|
||||
return isNaN(p) ? (props.protos[proto] ?? 11010) : p
|
||||
}
|
||||
const parseByPattern = (input: string) => {
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) {
|
||||
return null
|
||||
}
|
||||
const match = trimmed.match(/^(\w+):\/\/(.*)$/)
|
||||
const proto = match ? match[1] : 'tcp'
|
||||
const rest = match ? match[2] : trimmed
|
||||
const authority = rest.split(/[/?#]/)[0]
|
||||
if (!authority) {
|
||||
return null
|
||||
}
|
||||
const hostAndMaybePort = authority.includes('@') ? authority.slice(authority.lastIndexOf('@') + 1) : authority
|
||||
if (hostAndMaybePort.startsWith('[')) {
|
||||
const ipv6End = hostAndMaybePort.indexOf(']')
|
||||
if (ipv6End > 0) {
|
||||
const host = hostAndMaybePort.slice(0, ipv6End + 1)
|
||||
const remain = hostAndMaybePort.slice(ipv6End + 1)
|
||||
// null = no explicit port in URL; do not fabricate a default
|
||||
const port: number | null = remain.startsWith(':') ? getValidPort(remain.slice(1), proto) : null
|
||||
return { proto, host, port }
|
||||
onMounted(() => {
|
||||
if (container.value) {
|
||||
const observer = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
internalCompact.value = entry.contentRect.width < 400
|
||||
}
|
||||
}
|
||||
const portMatch = hostAndMaybePort.match(/^(.*):(\d+)$/)
|
||||
const host = portMatch ? portMatch[1] : hostAndMaybePort
|
||||
// null = no explicit port in URL; buildUrlValue will omit the port entirely,
|
||||
// preserving the protocol's implied standard port (e.g. 443 for wss://).
|
||||
const port: number | null = portMatch ? parseInt(portMatch[2]) : null
|
||||
return { proto, host, port }
|
||||
}
|
||||
})
|
||||
observer.observe(container.value)
|
||||
|
||||
if (!val) {
|
||||
return { proto: 'tcp', host: '', port: props.protos['tcp'] ?? 11010 }
|
||||
onUnmounted(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
}
|
||||
const parsedByPattern = parseByPattern(val)
|
||||
if (parsedByPattern) {
|
||||
return parsedByPattern
|
||||
}
|
||||
return { proto: 'tcp', host: '', port: null }
|
||||
}
|
||||
})
|
||||
|
||||
const internalValue = ref(parseUrl(url.value))
|
||||
const internalValue = ref(parseUrlInput(url.value, props.protos))
|
||||
const defaultHost = '0.0.0.0'
|
||||
|
||||
const buildUrlValue = (value: { proto: string, host: string, port: number | null }, forceDefaultHost = false) => {
|
||||
const proto = value.proto || 'tcp'
|
||||
const rawHost = (value.host ?? '').trim()
|
||||
const host = rawHost || (forceDefaultHost ? defaultHost : '')
|
||||
if (!host) {
|
||||
return null
|
||||
}
|
||||
// Omit port when the protocol uses no port (protos value = 0), or when the
|
||||
// original URL had no explicit port (port === null) – avoids overwriting an
|
||||
// implicit standard port (e.g. 443 for wss) with an EasyTier default (11012).
|
||||
if (props.protos[proto] === 0 || value.port === null) {
|
||||
return `${proto}://${host}`
|
||||
}
|
||||
return `${proto}://${host}:${value.port}`
|
||||
}
|
||||
|
||||
const syncUrlFromInternal = (forceDefaultHost = false) => {
|
||||
const nextUrl = buildUrlValue(internalValue.value, forceDefaultHost)
|
||||
const nextUrl = buildUrlInputValue(internalValue.value, props.protos, forceDefaultHost)
|
||||
if (!nextUrl || nextUrl === url.value) {
|
||||
return
|
||||
}
|
||||
@@ -90,6 +46,10 @@ const syncUrlFromInternal = (forceDefaultHost = false) => {
|
||||
|
||||
const onHostBlur = () => {
|
||||
hostFocused.value = false
|
||||
const parsedHost = parseHostInputOnBlur(internalValue.value.host ?? '', internalValue.value.proto, props.protos)
|
||||
if (parsedHost) {
|
||||
internalValue.value = parsedHost
|
||||
}
|
||||
syncUrlFromInternal(true)
|
||||
}
|
||||
|
||||
@@ -106,12 +66,20 @@ const isNoPortProto = computed(() => {
|
||||
return props.protos[internalValue.value.proto] === 0
|
||||
})
|
||||
|
||||
const hostInputValue = computed({
|
||||
get: () => getHostInputValue(internalValue.value),
|
||||
set: (value: string) => {
|
||||
internalValue.value.host = value
|
||||
internalValue.value.suffix = undefined
|
||||
},
|
||||
})
|
||||
|
||||
// Sync from external
|
||||
watch(() => url.value, (newVal) => {
|
||||
if (hostFocused.value) {
|
||||
return
|
||||
}
|
||||
const parsed = parseUrl(newVal)
|
||||
const parsed = parseUrlInput(newVal, props.protos)
|
||||
const internalHost = internalValue.value.host ?? ''
|
||||
const sameHost = parsed.host === internalHost || (!internalHost.trim() && parsed.host === defaultHost)
|
||||
if (parsed.proto !== internalValue.value.proto ||
|
||||
@@ -123,6 +91,9 @@ watch(() => url.value, (newVal) => {
|
||||
|
||||
// Sync to external
|
||||
watch(internalValue, () => {
|
||||
if (hostFocused.value) {
|
||||
return
|
||||
}
|
||||
syncUrlFromInternal(false)
|
||||
}, { deep: true })
|
||||
|
||||
@@ -148,34 +119,34 @@ const onProtoChange = (newProto: string) => {
|
||||
internalValue.value.port = newDefault
|
||||
}
|
||||
internalValue.value.proto = newProto
|
||||
internalValue.value.suffix = undefined
|
||||
internalValue.value.hasExplicitPort = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="url-input-container w-full min-w-0 overflow-hidden">
|
||||
<InputGroup class="url-input-full w-full min-w-0">
|
||||
<div ref="container" class="w-full">
|
||||
<InputGroup v-if="!internalCompact" class="w-full">
|
||||
<AutoComplete :model-value="internalValue.proto" :suggestions="filteredProtos" dropdown
|
||||
class="max-w-32 proto-autocomplete-in-group" @complete="searchProtos"
|
||||
@update:model-value="onProtoChange" />
|
||||
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="grow min-w-0"
|
||||
<InputText v-model="hostInputValue" :placeholder="placeholder || '0.0.0.0'" class="grow"
|
||||
@focus="onHostFocus" @blur="onHostBlur" />
|
||||
<template v-if="!isNoPortProto">
|
||||
<InputGroupAddon>
|
||||
<span style="font-weight: bold">:</span>
|
||||
</InputGroupAddon>
|
||||
<InputNumber v-model="internalValue.port" :format="false" :min="1" :max="65535" class="max-w-24"
|
||||
:placeholder="String(protos[internalValue.proto] ?? 11010)" fluid />
|
||||
:placeholder="String(protos[internalValue.proto] ?? 11010)"
|
||||
fluid />
|
||||
</template>
|
||||
<!-- Rendered in both responsive branches; keep action slot content free of side effects and duplicate IDs. -->
|
||||
<slot name="actions"></slot>
|
||||
</InputGroup>
|
||||
|
||||
<div
|
||||
class="url-input-compact flex justify-between items-center p-2 border rounded w-full min-w-0 overflow-hidden">
|
||||
<span class="truncate mr-2 min-w-0 flex-1 overflow-hidden">{{ url }}</span>
|
||||
<div class="flex items-center shrink-0">
|
||||
<Button icon="pi pi-pencil" class="p-button-sm p-button-text" :aria-label="t('web.common.edit')"
|
||||
@click="editing = true" />
|
||||
<div v-else class="flex justify-between items-center p-2 border rounded w-full">
|
||||
<span class="truncate mr-2">{{ url }}</span>
|
||||
<div class="flex items-center">
|
||||
<Button icon="pi pi-pencil" class="p-button-sm p-button-text" @click="editing = true" />
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,7 +160,7 @@ const onProtoChange = (newProto: string) => {
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label>{{ t('web.common.address') || 'Address' }}</label>
|
||||
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="w-full"
|
||||
<InputText v-model="hostInputValue" :placeholder="placeholder || '0.0.0.0'" class="w-full"
|
||||
@focus="onHostFocus" @blur="onHostBlur" />
|
||||
</div>
|
||||
<div v-if="!isNoPortProto" class="flex flex-col gap-2">
|
||||
@@ -207,28 +178,6 @@ const onProtoChange = (newProto: string) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.url-input-container {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.url-input-full {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.url-input-compact {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@container (min-width: 400px) {
|
||||
.url-input-full {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.url-input-compact {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.proto-autocomplete-in-group,
|
||||
.proto-autocomplete-in-group :deep(.p-autocomplete-input),
|
||||
.proto-autocomplete-in-group :deep(.p-autocomplete-dropdown) {
|
||||
|
||||
@@ -104,9 +104,6 @@ use_smoltcp_help: 使用用户态 TCP/IP 协议栈,避免操作系统防火墙
|
||||
disable_ipv6: 禁用IPv6
|
||||
disable_ipv6_help: 禁用此节点的IPv6功能,仅使用IPv4进行网络通信。
|
||||
|
||||
ipv6_public_addr_auto: 自动获取公网 IPv6
|
||||
ipv6_public_addr_auto_help: 自动从共享了 IPv6 子网的对等节点获取一个公网 IPv6 地址。
|
||||
|
||||
enable_kcp_proxy: 启用 KCP 代理
|
||||
enable_kcp_proxy_help: 将 TCP 流量转为 KCP 流量,降低传输延迟,提升传输速度。
|
||||
|
||||
@@ -160,12 +157,6 @@ disable_tcp_hole_punching_help: 禁用TCP打洞功能
|
||||
disable_udp_hole_punching: 禁用UDP打洞
|
||||
disable_udp_hole_punching_help: 禁用UDP打洞功能
|
||||
|
||||
enable_udp_broadcast_relay: UDP 广播中继
|
||||
enable_udp_broadcast_relay_help: "仅 Windows:捕获物理网卡上的本机 UDP 广播包并转发给 EasyTier 对等节点,帮助局域网游戏发现房间。需要管理员权限。"
|
||||
|
||||
disable_upnp: 禁用 UPnP
|
||||
disable_upnp_help: 禁用符合条件监听器的运行时 UPnP/NAT-PMP 端口映射;自动端口映射默认开启。
|
||||
|
||||
disable_sym_hole_punching: 禁用对称NAT打洞
|
||||
disable_sym_hole_punching_help: 禁用对称NAT的打洞(生日攻击),将对称NAT视为锥形NAT处理
|
||||
|
||||
@@ -263,7 +254,6 @@ event:
|
||||
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
||||
PortForwardAdded: 端口转发添加
|
||||
ProxyCidrsUpdated: 子网代理CIDR更新
|
||||
UdpBroadcastRelayStartResult: UDP广播中继启动结果
|
||||
|
||||
web:
|
||||
login:
|
||||
|
||||
@@ -103,9 +103,6 @@ use_smoltcp_help: Use a user-space TCP/IP stack to avoid issues with operating s
|
||||
disable_ipv6: Disable IPv6
|
||||
disable_ipv6_help: Disable IPv6 functionality for this node, only use IPv4 for network communication.
|
||||
|
||||
ipv6_public_addr_auto: Auto Public IPv6
|
||||
ipv6_public_addr_auto_help: Auto-obtain a public IPv6 address from a peer that shares its IPv6 subnet.
|
||||
|
||||
enable_kcp_proxy: Enable KCP Proxy
|
||||
enable_kcp_proxy_help: Convert TCP traffic to KCP traffic to reduce latency and boost transmission speed.
|
||||
|
||||
@@ -159,12 +156,6 @@ disable_tcp_hole_punching_help: Disable tcp hole punching
|
||||
disable_udp_hole_punching: Disable UDP Hole Punching
|
||||
disable_udp_hole_punching_help: Disable udp hole punching
|
||||
|
||||
enable_udp_broadcast_relay: UDP Broadcast Relay
|
||||
enable_udp_broadcast_relay_help: "Windows only: capture local UDP broadcast packets from physical interfaces and forward them to EasyTier peers. Helps games to find rooms in local network. Requires administrator privileges."
|
||||
|
||||
disable_upnp: Disable UPnP
|
||||
disable_upnp_help: Disable runtime UPnP/NAT-PMP port mapping for eligible listeners; automatic port mapping is enabled by default.
|
||||
|
||||
disable_sym_hole_punching: Disable Symmetric NAT Hole Punching
|
||||
disable_sym_hole_punching_help: Disable special hole punching handling for symmetric NAT (based on birthday attack), treat symmetric NAT as cone NAT
|
||||
|
||||
@@ -263,7 +254,6 @@ event:
|
||||
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
||||
PortForwardAdded: PortForwardAdded
|
||||
ProxyCidrsUpdated: ProxyCidrsUpdated
|
||||
UdpBroadcastRelayStartResult: UDP Broadcast Relay Start Result
|
||||
|
||||
web:
|
||||
login:
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildUrlInputValue, getHostInputValue, parseHostInputOnBlur, parseUrlInput, type ProtoPorts } from './url-input'
|
||||
|
||||
const protos: ProtoPorts = {
|
||||
tcp: 11010,
|
||||
udp: 11010,
|
||||
wg: 11011,
|
||||
ws: 11011,
|
||||
wss: 11012,
|
||||
quic: 11012,
|
||||
faketcp: 11013,
|
||||
http: 80,
|
||||
https: 443,
|
||||
txt: 0,
|
||||
srv: 0,
|
||||
}
|
||||
|
||||
function normalizeUrl(input: string, defaultProto = 'tcp') {
|
||||
return buildUrlInputValue(parseUrlInput(input, protos, defaultProto), protos, true)
|
||||
}
|
||||
|
||||
describe('parseUrlInput', () => {
|
||||
it.each([
|
||||
['https://raw.githubusercontent.com/aaa/bb/cc.txt', {
|
||||
proto: 'https',
|
||||
host: 'raw.githubusercontent.com',
|
||||
port: null,
|
||||
suffix: '/aaa/bb/cc.txt',
|
||||
hasExplicitPort: false,
|
||||
}],
|
||||
['https://host:4443/path?x=1#hash', {
|
||||
proto: 'https',
|
||||
host: 'host',
|
||||
port: 4443,
|
||||
suffix: '/path?x=1#hash',
|
||||
hasExplicitPort: true,
|
||||
}],
|
||||
['[::1]:11010/path', {
|
||||
proto: 'tcp',
|
||||
host: '[::1]',
|
||||
port: 11010,
|
||||
suffix: '/path',
|
||||
hasExplicitPort: true,
|
||||
}],
|
||||
[' http://host/path ', {
|
||||
proto: 'http',
|
||||
host: 'host',
|
||||
port: null,
|
||||
suffix: '/path',
|
||||
hasExplicitPort: false,
|
||||
}],
|
||||
])('parses %s', (input, expected) => {
|
||||
expect(parseUrlInput(input, protos)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('parses IPv6 host without an explicit port', () => {
|
||||
expect(parseUrlInput('[::1]', protos)).toEqual({
|
||||
proto: 'tcp',
|
||||
host: '[::1]',
|
||||
port: null,
|
||||
suffix: '',
|
||||
hasExplicitPort: false,
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
['host:', 'host'],
|
||||
['host:notaport', 'host'],
|
||||
])('falls back to the default port for invalid port input %s', (input, host) => {
|
||||
expect(parseUrlInput(input, protos)).toEqual({
|
||||
proto: 'tcp',
|
||||
host,
|
||||
port: 11010,
|
||||
suffix: '',
|
||||
hasExplicitPort: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the explicit proto for an input without authority', () => {
|
||||
expect(parseUrlInput('https://', protos)).toEqual({
|
||||
proto: 'https',
|
||||
host: '',
|
||||
port: null,
|
||||
suffix: '',
|
||||
hasExplicitPort: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildUrlInputValue', () => {
|
||||
it.each([
|
||||
['https://host', 'https://host'],
|
||||
['http://host', 'http://host'],
|
||||
['https://host:4443/path', 'https://host:4443/path'],
|
||||
['https://host:443/path', 'https://host:443/path'],
|
||||
['tcp://host', 'tcp://host'],
|
||||
['wss://host', 'wss://host'],
|
||||
['http://host/path?x=1#hash', 'http://host/path?x=1#hash'],
|
||||
['https://host?x=1', 'https://host?x=1'],
|
||||
['https://host#hash', 'https://host#hash'],
|
||||
['txt://example.com/path.txt', 'txt://example.com/path.txt'],
|
||||
['srv://_easytier._tcp.example.com', 'srv://_easytier._tcp.example.com'],
|
||||
['custom://host/path', 'custom://host/path'],
|
||||
])('normalizes %s to %s', (input, expected) => {
|
||||
expect(normalizeUrl(input)).toBe(expected)
|
||||
})
|
||||
|
||||
it('returns null for empty host unless default host is forced', () => {
|
||||
const parsed = parseUrlInput('', protos)
|
||||
|
||||
expect(buildUrlInputValue(parsed, protos, false)).toBeNull()
|
||||
expect(buildUrlInputValue(parsed, protos, true)).toBe('tcp://0.0.0.0:11010')
|
||||
})
|
||||
|
||||
it('does not build a broken URL for a protocol without authority', () => {
|
||||
const parsed = parseUrlInput('https://', protos)
|
||||
|
||||
expect(buildUrlInputValue(parsed, protos, false)).toBeNull()
|
||||
expect(buildUrlInputValue(parsed, protos, true)).toBe('https://0.0.0.0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseHostInputOnBlur', () => {
|
||||
it('infers https for a pasted host:port/path when the current proto is tcp', () => {
|
||||
const parsed = parseHostInputOnBlur('raw.githubusercontent.com:4443/aaa/bb/cc.txt', 'tcp', protos)
|
||||
|
||||
expect(parsed).toEqual({
|
||||
proto: 'https',
|
||||
host: 'raw.githubusercontent.com',
|
||||
port: 4443,
|
||||
suffix: '/aaa/bb/cc.txt',
|
||||
hasExplicitPort: true,
|
||||
})
|
||||
expect(buildUrlInputValue(parsed!, protos, true)).toBe('https://raw.githubusercontent.com:4443/aaa/bb/cc.txt')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['raw.githubusercontent.com/aaa/bb/cc.txt', 'tcp', 'https://raw.githubusercontent.com/aaa/bb/cc.txt'],
|
||||
['raw.githubusercontent.com:4443/aaa/bb/cc.txt', 'https', 'https://raw.githubusercontent.com:4443/aaa/bb/cc.txt'],
|
||||
['https://raw.githubusercontent.com:4443/aaa/bb/cc.txt', 'tcp', 'https://raw.githubusercontent.com:4443/aaa/bb/cc.txt'],
|
||||
[' https://raw.githubusercontent.com/aaa/bb/cc.txt ', 'tcp', 'https://raw.githubusercontent.com/aaa/bb/cc.txt'],
|
||||
])('normalizes pasted host input %s with current proto %s', (input, currentProto, expected) => {
|
||||
const parsed = parseHostInputOnBlur(input, currentProto, protos)
|
||||
|
||||
expect(buildUrlInputValue(parsed!, protos, true)).toBe(expected)
|
||||
})
|
||||
|
||||
it('keeps ordinary host:port input on the current tcp protocol', () => {
|
||||
const parsed = parseHostInputOnBlur('example.com:11010', 'tcp', protos)
|
||||
|
||||
expect(buildUrlInputValue(parsed!, protos, true)).toBe('tcp://example.com:11010')
|
||||
})
|
||||
|
||||
it('returns null for a simple host without port or suffix', () => {
|
||||
expect(parseHostInputOnBlur('example.com', 'tcp', protos)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getHostInputValue', () => {
|
||||
it('shows host and suffix while keeping the port in the port field', () => {
|
||||
const parsed = parseUrlInput('https://raw.githubusercontent.com:4443/aaa/bb/cc.txt', protos)
|
||||
|
||||
expect(getHostInputValue(parsed)).toBe('raw.githubusercontent.com/aaa/bb/cc.txt')
|
||||
})
|
||||
|
||||
it('shows query and hash in the host input suffix', () => {
|
||||
const parsed = parseUrlInput('https://host/path?x=1#hash', protos)
|
||||
|
||||
expect(getHostInputValue(parsed)).toBe('host/path?x=1#hash')
|
||||
})
|
||||
})
|
||||
|
||||
describe('round trip scenarios', () => {
|
||||
it.each([
|
||||
['https://raw.githubusercontent.com/aaa/bb/cc.txt'],
|
||||
['https://raw.githubusercontent.com:4443/aaa/bb/cc.txt'],
|
||||
['http://host/path?x=1#hash'],
|
||||
['tcp://example.com:11010'],
|
||||
['txt://example.com/path.txt'],
|
||||
['srv://_easytier._tcp.example.com'],
|
||||
])('keeps %s stable after parse and build', (input) => {
|
||||
expect(normalizeUrl(input)).toBe(input)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,105 @@
|
||||
export interface UrlInputParts {
|
||||
proto: string
|
||||
host: string
|
||||
port: number | null
|
||||
suffix?: string
|
||||
hasExplicitPort?: boolean
|
||||
}
|
||||
|
||||
export type ProtoPorts = Record<string, number>
|
||||
|
||||
const fallbackProto = 'tcp'
|
||||
const fallbackPort = 11010
|
||||
const defaultHost = '0.0.0.0'
|
||||
|
||||
function defaultPortFor(protos: ProtoPorts, proto: string) {
|
||||
return protos[proto] ?? fallbackPort
|
||||
}
|
||||
|
||||
function getValidPort(portStr: string, protos: ProtoPorts, proto: string) {
|
||||
const p = parseInt(portStr)
|
||||
return isNaN(p) ? defaultPortFor(protos, proto) : p
|
||||
}
|
||||
|
||||
export function parseUrlInput(val: string | null | undefined, protos: ProtoPorts, defaultProto = fallbackProto): UrlInputParts {
|
||||
const parseByPattern = (input: string) => {
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) {
|
||||
return null
|
||||
}
|
||||
|
||||
const match = trimmed.match(/^(\w+):\/\/(.*)$/)
|
||||
const proto = match ? match[1] : defaultProto
|
||||
const rest = match ? match[2] : trimmed
|
||||
const suffixStart = rest.search(/[/?#]/)
|
||||
const authority = suffixStart >= 0 ? rest.slice(0, suffixStart) : rest
|
||||
const suffix = suffixStart >= 0 ? rest.slice(suffixStart) : ''
|
||||
if (!authority) {
|
||||
return { proto, host: '', port: null, suffix, hasExplicitPort: false }
|
||||
}
|
||||
|
||||
const hostAndMaybePort = authority.includes('@') ? authority.slice(authority.lastIndexOf('@') + 1) : authority
|
||||
if (hostAndMaybePort.startsWith('[')) {
|
||||
const ipv6End = hostAndMaybePort.indexOf(']')
|
||||
if (ipv6End > 0) {
|
||||
const host = hostAndMaybePort.slice(0, ipv6End + 1)
|
||||
const remain = hostAndMaybePort.slice(ipv6End + 1)
|
||||
const hasExplicitPort = remain.startsWith(':')
|
||||
const port = hasExplicitPort ? getValidPort(remain.slice(1), protos, proto) : null
|
||||
return { proto, host, port, suffix, hasExplicitPort }
|
||||
}
|
||||
}
|
||||
|
||||
const portMatch = hostAndMaybePort.match(/^(.*):(\d+)$/)
|
||||
if (portMatch) {
|
||||
return { proto, host: portMatch[1], port: parseInt(portMatch[2]), suffix, hasExplicitPort: true }
|
||||
}
|
||||
|
||||
const invalidPortMatch = hostAndMaybePort.match(/^([^:]+):[^:]*$/)
|
||||
const host = invalidPortMatch ? invalidPortMatch[1] : hostAndMaybePort
|
||||
const port = invalidPortMatch ? defaultPortFor(protos, proto) : null
|
||||
return { proto, host, port, suffix, hasExplicitPort: false }
|
||||
}
|
||||
|
||||
if (!val) {
|
||||
return { proto: defaultProto, host: '', port: defaultPortFor(protos, defaultProto) }
|
||||
}
|
||||
const parsedByPattern = parseByPattern(val)
|
||||
if (parsedByPattern) {
|
||||
return parsedByPattern
|
||||
}
|
||||
return { proto: defaultProto, host: '', port: defaultPortFor(protos, defaultProto) }
|
||||
}
|
||||
|
||||
export function buildUrlInputValue(value: UrlInputParts, protos: ProtoPorts, forceDefaultHost = false) {
|
||||
const proto = value.proto || fallbackProto
|
||||
const rawHost = (value.host ?? '').trim()
|
||||
const host = rawHost || (forceDefaultHost ? defaultHost : '')
|
||||
if (!host) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (protos[proto] === 0 || value.port === null) {
|
||||
return `${proto}://${host}${value.suffix ?? ''}`
|
||||
}
|
||||
|
||||
let port = value.port
|
||||
if (isNaN(parseInt(port as any))) {
|
||||
port = defaultPortFor(protos, proto)
|
||||
}
|
||||
|
||||
return `${proto}://${host}:${port}${value.suffix ?? ''}`
|
||||
}
|
||||
|
||||
export function parseHostInputOnBlur(rawHost: string, currentProto: string, protos: ProtoPorts) {
|
||||
const inferredProto = rawHost.includes('/') && currentProto === fallbackProto ? 'https' : currentProto
|
||||
const parsedHost = parseUrlInput(rawHost, protos, inferredProto)
|
||||
if (parsedHost.host && (parsedHost.proto !== currentProto || parsedHost.hasExplicitPort || parsedHost.suffix)) {
|
||||
return parsedHost
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function getHostInputValue(value: UrlInputParts) {
|
||||
return `${value.host ?? ''}${value.suffix ?? ''}`
|
||||
}
|
||||
@@ -115,7 +115,6 @@ export interface NetworkConfig {
|
||||
|
||||
use_smoltcp?: boolean
|
||||
disable_ipv6?: boolean
|
||||
ipv6_public_addr_auto?: boolean
|
||||
enable_kcp_proxy?: boolean
|
||||
disable_kcp_input?: boolean
|
||||
enable_quic_proxy?: boolean
|
||||
@@ -133,8 +132,6 @@ export interface NetworkConfig {
|
||||
disable_encryption?: boolean
|
||||
disable_tcp_hole_punching?: boolean
|
||||
disable_udp_hole_punching?: boolean
|
||||
disable_upnp?: boolean
|
||||
enable_udp_broadcast_relay?: boolean
|
||||
disable_sym_hole_punching?: boolean
|
||||
|
||||
enable_relay_network_whitelist?: boolean
|
||||
@@ -193,7 +190,6 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
|
||||
use_smoltcp: false,
|
||||
disable_ipv6: false,
|
||||
ipv6_public_addr_auto: false,
|
||||
enable_kcp_proxy: false,
|
||||
disable_kcp_input: false,
|
||||
enable_quic_proxy: false,
|
||||
@@ -211,8 +207,6 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
disable_encryption: false,
|
||||
disable_tcp_hole_punching: false,
|
||||
disable_udp_hole_punching: false,
|
||||
disable_upnp: false,
|
||||
enable_udp_broadcast_relay: false,
|
||||
disable_sym_hole_punching: false,
|
||||
enable_relay_network_whitelist: false,
|
||||
relay_network_whitelist: [],
|
||||
@@ -449,6 +443,4 @@ export enum EventType {
|
||||
PortForwardAdded = 'PortForwardAdded', // PortForwardConfigPb
|
||||
|
||||
ProxyCidrsUpdated = 'ProxyCidrsUpdated', // string[], string[]
|
||||
|
||||
UdpBroadcastRelayStartResult = 'UdpBroadcastRelayStartResult', // { capture_backend?: string, error?: string }
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"build:deps": "pnpm --filter easytier-frontend-lib build",
|
||||
"dev": "pnpm run build:deps && vite",
|
||||
"build": "pnpm run build:deps && vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -32,4 +33,4 @@
|
||||
"vite-plugin-singlefile": "^2.0.3",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,7 +365,6 @@ mod tests {
|
||||
let _c = WebClient::new(
|
||||
connector,
|
||||
"test",
|
||||
uuid::Uuid::new_v4(),
|
||||
"test",
|
||||
false,
|
||||
Arc::new(NetworkInstanceManager::new()),
|
||||
|
||||
+15
-19
@@ -3,7 +3,7 @@ name = "easytier"
|
||||
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasyTier"
|
||||
version = "2.6.4"
|
||||
version = "2.6.2"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors = ["kkrainbow"]
|
||||
@@ -11,7 +11,6 @@ keywords = ["vpn", "p2p", "network", "easytier"]
|
||||
categories = ["network-programming", "command-line-utilities"]
|
||||
license-file = "LICENSE"
|
||||
readme = "README.md"
|
||||
build = "build/main.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -51,9 +50,6 @@ 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"
|
||||
@@ -62,7 +58,7 @@ itertools = "0.14.0"
|
||||
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
|
||||
hostname = "0.4.2"
|
||||
gethostname = "0.5.0"
|
||||
|
||||
futures = { version = "0.3", features = ["bilock", "unstable"] }
|
||||
|
||||
@@ -73,12 +69,9 @@ tokio-util = { version = "0.7.9", features = ["codec", "net", "io", "rt"] }
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.74"
|
||||
|
||||
maplit = "1.0.2"
|
||||
dashmap = "6.0"
|
||||
timedmap = "=1.0.1"
|
||||
|
||||
moka = { version = "0.12", features = ["future"] }
|
||||
|
||||
# for full-path zero-copy
|
||||
zerocopy = { version = "0.7.32", features = ["derive", "simd"] }
|
||||
bytes = "1.5.0"
|
||||
@@ -157,7 +150,6 @@ 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",
|
||||
@@ -176,7 +168,6 @@ 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 }
|
||||
@@ -243,14 +234,17 @@ http_req = { git = "https://github.com/EasyTier/http_req.git", default-features
|
||||
] }
|
||||
|
||||
# for dns connector
|
||||
hickory-proto = "0.26.0"
|
||||
hickory-net = { version = "0.26.0", features = ["serde"] }
|
||||
hickory-resolver = { version = "0.26.0", features = ["https-ring", "webpki-roots"] }
|
||||
hickory-resolver = "0.25.2"
|
||||
hickory-proto = "0.25.2"
|
||||
|
||||
# for magic dns
|
||||
hickory-server = { version = "0.26.0", features = ["resolver"], optional = true }
|
||||
hickory-client = { version = "0.25.2", optional = true }
|
||||
hickory-server = { version = "0.25.2", 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"
|
||||
@@ -321,14 +315,15 @@ jemalloc-sys = { package = "tikv-jemalloc-sys", version = "0.6.0", features = [
|
||||
|
||||
[build-dependencies]
|
||||
cfg_aliases = "0.2.1"
|
||||
indoc = "2.0"
|
||||
tonic-build = "0.12"
|
||||
globwalk = "0.8.1"
|
||||
regex = "1"
|
||||
prost-build = "0.13.5"
|
||||
prost-wkt-build = "0.6"
|
||||
easytier-rpc-build = { path = "../easytier-rpc-build", features = [
|
||||
"internal-namespace",
|
||||
] }
|
||||
prost-reflect-build = { version = "0.14.0" }
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
|
||||
"win7",
|
||||
] }
|
||||
@@ -342,6 +337,7 @@ 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"
|
||||
|
||||
@@ -403,7 +399,7 @@ jemalloc-prof = [
|
||||
"jemalloc-sys/stats",
|
||||
]
|
||||
tracing = ["tokio/tracing", "dep:console-subscriber"]
|
||||
magic-dns = ["dep:hickory-server"]
|
||||
magic-dns = ["dep:hickory-client", "dep:hickory-server"]
|
||||
faketcp = ["dep:flume"]
|
||||
zstd = ["dep:zstd"]
|
||||
# For Network Extension on macOS
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
mod rpc;
|
||||
|
||||
use crate::rpc::ServiceGenerator;
|
||||
use cfg_aliases::cfg_aliases;
|
||||
use prost_wkt_build::{FileDescriptorSet, Message as _};
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -166,7 +163,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/dns.proto",
|
||||
"src/proto/magic_dns.proto",
|
||||
"src/proto/acl.proto",
|
||||
];
|
||||
|
||||
@@ -174,14 +171,17 @@ 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");
|
||||
|
||||
config
|
||||
.type_attribute(".", "#[derive(serde::Serialize,serde::Deserialize)]")
|
||||
.extern_path(".google.protobuf.Value", "::prost_wkt_types::Value")
|
||||
.file_descriptor_set_path(&descriptor_file)
|
||||
.protoc_arg("--experimental_allow_proto3_optional")
|
||||
.type_attribute("peer_rpc.DirectConnectedPeerInfo", "#[derive(Hash)]")
|
||||
.type_attribute("peer_rpc.PeerInfoForGlobalMap", "#[derive(Hash)]")
|
||||
.type_attribute("peer_rpc.ForeignNetworkRouteInfoKey", "#[derive(Hash, Eq)]")
|
||||
@@ -191,32 +191,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
)
|
||||
.type_attribute("peer_rpc.RouteForeignNetworkSummary", "#[derive(Hash, Eq)]")
|
||||
.type_attribute("common.RpcDescriptor", "#[derive(Hash, Eq)]")
|
||||
.type_attribute("acl.Acl", "#[serde(default)]")
|
||||
.type_attribute("acl.AclV1", "#[serde(default)]")
|
||||
.type_attribute("acl.Chain", "#[serde(default)]")
|
||||
.type_attribute("acl.Rule", "#[serde(default)]")
|
||||
.type_attribute("acl.GroupInfo", "#[serde(default)]");
|
||||
|
||||
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
|
||||
.field_attribute(".api.manage.NetworkConfig", "#[serde(default)]")
|
||||
.service_generator(Box::new(easytier_rpc_build::ServiceGenerator::default()))
|
||||
.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/"])?;
|
||||
.skip_debug([".common.Ipv4Addr", ".common.Ipv6Addr", ".common.UUID"]);
|
||||
|
||||
config.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)?;
|
||||
let descriptor = FileDescriptorSet::decode(&descriptor_bytes[..])?;
|
||||
let descriptor_bytes = std::fs::read(descriptor_file).unwrap();
|
||||
let descriptor = FileDescriptorSet::decode(&descriptor_bytes[..]).unwrap();
|
||||
prost_wkt_build::add_serde(out, descriptor);
|
||||
|
||||
check_locale();
|
||||
@@ -1,720 +0,0 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use indoc::formatdoc;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::{format_ident, quote};
|
||||
use std::str::FromStr;
|
||||
|
||||
fn parse(value: &str) -> TokenStream {
|
||||
TokenStream::from_str(value)
|
||||
.unwrap_or_else(|err| panic!("Failed to parse tokens: {} ({})", value, err))
|
||||
}
|
||||
|
||||
fn doc(comments: &prost_build::Comments) -> TokenStream {
|
||||
let doc = comments
|
||||
.leading
|
||||
.iter()
|
||||
.flat_map(|c| c.lines().filter(|s| !s.is_empty()));
|
||||
quote! { #( #[doc = #doc] )* }
|
||||
}
|
||||
|
||||
const NAMESPACE: &str = "crate::proto::rpc_types";
|
||||
|
||||
struct Method {
|
||||
index: u8,
|
||||
doc: TokenStream,
|
||||
method: Ident,
|
||||
method_inner: Ident,
|
||||
method_str: String,
|
||||
method_proto: Ident,
|
||||
method_proto_str: String,
|
||||
Input: TokenStream,
|
||||
Input_proto_str: String,
|
||||
Output: TokenStream,
|
||||
Output_proto_str: String,
|
||||
}
|
||||
|
||||
impl Method {
|
||||
fn new(index: u8, method: prost_build::Method) -> Self {
|
||||
assert!(
|
||||
!method.client_streaming,
|
||||
"Client streaming not yet supported for method {}",
|
||||
method.proto_name
|
||||
);
|
||||
assert!(
|
||||
!method.server_streaming,
|
||||
"Server streaming not yet supported for method {}",
|
||||
method.proto_name
|
||||
);
|
||||
Self {
|
||||
index,
|
||||
doc: doc(&method.comments),
|
||||
method: format_ident!("{}", method.name),
|
||||
method_inner: format_ident!("{}_inner", method.name),
|
||||
method_str: method.name,
|
||||
method_proto: format_ident!("{}", method.proto_name),
|
||||
method_proto_str: method.proto_name,
|
||||
Input: parse(&method.input_type),
|
||||
Input_proto_str: method.input_proto_type,
|
||||
Output: parse(&method.output_type),
|
||||
Output_proto_str: method.output_proto_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Service {
|
||||
namespace: TokenStream,
|
||||
doc: TokenStream,
|
||||
Service: Ident,
|
||||
ServiceDescriptor: Ident,
|
||||
ServiceServer: Ident,
|
||||
ServiceClient: Ident,
|
||||
ServiceClientFactory: Ident,
|
||||
ServiceMethodDescriptor: Ident,
|
||||
Service_str: String,
|
||||
Service_proto_str: String,
|
||||
Service_package_str: String,
|
||||
methods: Vec<Method>,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
fn new(service: prost_build::Service) -> Self {
|
||||
let methods = service
|
||||
.methods
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, method)| Method::new((i + 1) as u8, method))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
namespace: parse(NAMESPACE),
|
||||
doc: doc(&service.comments),
|
||||
Service: format_ident!("{}", service.name),
|
||||
ServiceDescriptor: format_ident!("{}Descriptor", service.name),
|
||||
ServiceServer: format_ident!("{}Server", service.name),
|
||||
ServiceClient: format_ident!("{}Client", service.name),
|
||||
ServiceClientFactory: format_ident!("{}ClientFactory", service.name),
|
||||
ServiceMethodDescriptor: format_ident!("{}MethodDescriptor", service.name),
|
||||
Service_str: service.name,
|
||||
Service_proto_str: service.proto_name,
|
||||
Service_package_str: service.package,
|
||||
methods,
|
||||
}
|
||||
}
|
||||
|
||||
fn trait_Service(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
doc,
|
||||
Service,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let match_json_call_method = methods.iter().map(
|
||||
|Method {
|
||||
method,
|
||||
method_str,
|
||||
method_proto_str,
|
||||
Input,
|
||||
..
|
||||
}| {
|
||||
quote! {
|
||||
#method_str | #method_proto_str => {
|
||||
let req: #Input = ::serde_json::from_value(json)
|
||||
.map_err(|e| #namespace::error::Error::MalformatRpcPacket(format!("json error: {}", e)))?;
|
||||
let resp = self.#method(ctrl, req).await?;
|
||||
Ok(::serde_json::to_value(resp)
|
||||
.map_err(|e| #namespace::error::Error::MalformatRpcPacket(format!("json error: {}", e)))?)
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let methods = methods.iter().map(
|
||||
|Method {
|
||||
doc,
|
||||
method,
|
||||
Input,
|
||||
Output,
|
||||
..
|
||||
}| {
|
||||
quote! {
|
||||
#doc
|
||||
async fn #method(&self, ctrl: Self::Controller, input: #Input) -> #namespace::error::Result<#Output>;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
#doc
|
||||
#[async_trait::async_trait]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait #Service {
|
||||
type Controller: #namespace::controller::Controller;
|
||||
|
||||
#(#methods)*
|
||||
|
||||
async fn json_call_method(
|
||||
&self,
|
||||
ctrl: Self::Controller,
|
||||
method: &str,
|
||||
json: ::serde_json::Value,
|
||||
) -> #namespace::error::Result<::serde_json::Value> {
|
||||
match method {
|
||||
#(#match_json_call_method)*
|
||||
_ => Err(#namespace::error::Error::InvalidMethodIndex(0, method.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_Service_for_Weak(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
Service,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
let methods = methods.iter().map(
|
||||
|Method {
|
||||
method,
|
||||
Input,
|
||||
Output,
|
||||
..
|
||||
}| {
|
||||
quote! {
|
||||
async fn #method(&self, ctrl: Self::Controller, input: #Input) -> #namespace::error::Result<#Output> {
|
||||
let Some(service) = self.upgrade() else {
|
||||
return Err(#namespace::error::Error::Shutdown);
|
||||
};
|
||||
service.#method(ctrl, input).await
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
#[async_trait::async_trait]
|
||||
impl<T> #Service for ::std::sync::Weak<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
::std::sync::Arc<T>: #Service,
|
||||
{
|
||||
type Controller = <::std::sync::Arc<T> as #Service>::Controller;
|
||||
|
||||
#(#methods)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn struct_ServiceDescriptor(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
ServiceDescriptor,
|
||||
ServiceMethodDescriptor,
|
||||
Service_str,
|
||||
Service_proto_str,
|
||||
Service_package_str,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let doc = format!("A service descriptor for a `{}`.", Service_str);
|
||||
|
||||
let methods = methods.iter().map(|Method { method_proto, .. }| {
|
||||
quote! { #ServiceMethodDescriptor::#method_proto, }
|
||||
});
|
||||
|
||||
quote! {
|
||||
#[doc = #doc]
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Default)]
|
||||
pub struct #ServiceDescriptor;
|
||||
|
||||
impl #namespace::descriptor::ServiceDescriptor for #ServiceDescriptor {
|
||||
type Method = #ServiceMethodDescriptor;
|
||||
fn name(&self) -> &'static str { #Service_str }
|
||||
fn proto_name(&self) -> &'static str { #Service_proto_str }
|
||||
fn package(&self) -> &'static str { #Service_package_str }
|
||||
fn methods(&self) -> &'static [Self::Method] {
|
||||
&[ #(#methods)* ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enum_ServiceMethodDescriptor(&self) -> TokenStream {
|
||||
let Self {
|
||||
ServiceMethodDescriptor,
|
||||
Service_str,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let doc = formatdoc! {"
|
||||
Methods available on a `{Service_str}`.
|
||||
|
||||
This can be used as a key when routing requests for servers/clients of a `{Service_str}`.
|
||||
"};
|
||||
|
||||
let variants = methods.iter().map(
|
||||
|Method {
|
||||
method_proto,
|
||||
index,
|
||||
..
|
||||
}| {
|
||||
quote! { #method_proto = #index, }
|
||||
},
|
||||
);
|
||||
|
||||
let impl_MethodDescriptor = self.impl_MethodDescriptor_for_ServiceMethodDescriptor();
|
||||
let impl_TryFrom = self.impl_TryFrom_for_ServiceMethodDescriptor();
|
||||
quote! {
|
||||
#[doc = #doc]
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
#[repr(u8)]
|
||||
pub enum #ServiceMethodDescriptor {
|
||||
#(#variants)*
|
||||
}
|
||||
|
||||
#impl_MethodDescriptor
|
||||
|
||||
#impl_TryFrom
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_MethodDescriptor_for_ServiceMethodDescriptor(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
ServiceMethodDescriptor,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let name = {
|
||||
let arms = methods.iter().map(
|
||||
|Method {
|
||||
method_proto,
|
||||
method_str,
|
||||
..
|
||||
}| {
|
||||
quote! { #ServiceMethodDescriptor::#method_proto => #method_str, }
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
fn name(&self) -> &'static str {
|
||||
match *self {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let proto_name = {
|
||||
let arms = methods.iter().map(
|
||||
|Method {
|
||||
method_proto,
|
||||
method_proto_str,
|
||||
..
|
||||
}| {
|
||||
quote! { #ServiceMethodDescriptor::#method_proto => #method_proto_str, }
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
fn proto_name(&self) -> &'static str {
|
||||
match *self {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let input_type = {
|
||||
let arms = methods.iter().map(|Method { method_proto, Input, .. }| {
|
||||
quote! { #ServiceMethodDescriptor::#method_proto => ::std::any::TypeId::of::<#Input>(), }
|
||||
});
|
||||
|
||||
quote! {
|
||||
fn input_type(&self) -> ::std::any::TypeId {
|
||||
match *self {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let input_proto_type = {
|
||||
let arms = methods.iter().map(
|
||||
|Method {
|
||||
method_proto,
|
||||
Input_proto_str,
|
||||
..
|
||||
}| {
|
||||
quote! { #ServiceMethodDescriptor::#method_proto => #Input_proto_str, }
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
fn input_proto_type(&self) -> &'static str {
|
||||
match *self {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let output_type = {
|
||||
let arms = methods.iter().map(|Method { method_proto, Output, .. }| {
|
||||
quote! { #ServiceMethodDescriptor::#method_proto => ::std::any::TypeId::of::<#Output>(), }
|
||||
});
|
||||
|
||||
quote! {
|
||||
fn output_type(&self) -> ::std::any::TypeId {
|
||||
match *self {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let output_proto_type = {
|
||||
let arms = methods.iter().map(
|
||||
|Method {
|
||||
method_proto,
|
||||
Output_proto_str,
|
||||
..
|
||||
}| {
|
||||
quote! { #ServiceMethodDescriptor::#method_proto => #Output_proto_str, }
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
fn output_proto_type(&self) -> &'static str {
|
||||
match *self {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
impl #namespace::descriptor::MethodDescriptor for #ServiceMethodDescriptor {
|
||||
#name
|
||||
|
||||
#proto_name
|
||||
|
||||
#input_type
|
||||
|
||||
#input_proto_type
|
||||
|
||||
#output_type
|
||||
|
||||
#output_proto_type
|
||||
|
||||
fn index(&self) -> u8 {
|
||||
*self as u8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_TryFrom_for_ServiceMethodDescriptor(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
ServiceMethodDescriptor,
|
||||
Service_str,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let arms = methods.iter().map(
|
||||
|Method {
|
||||
method_proto,
|
||||
index,
|
||||
..
|
||||
}| {
|
||||
quote! { #index => Ok(#ServiceMethodDescriptor::#method_proto), }
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
impl std::convert::TryFrom<u8> for #ServiceMethodDescriptor {
|
||||
type Error = #namespace::error::Error;
|
||||
fn try_from(value: u8) -> #namespace::error::Result<Self> {
|
||||
match value {
|
||||
#(#arms)*
|
||||
_ => Err(#namespace::error::Error::InvalidMethodIndex(value, #Service_str.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn struct_ServiceClient(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
ServiceDescriptor,
|
||||
ServiceClient,
|
||||
Service_str,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let doc = formatdoc! {"
|
||||
A client for a `{Service_str}`.
|
||||
|
||||
This implements the `{Service_str}` trait by dispatching all method calls to the supplied `Handler`.
|
||||
"};
|
||||
|
||||
let impl_service_client = self.impl_ServiceClient();
|
||||
let impl_service_for_client = self.impl_Service_for_ServiceClient();
|
||||
quote! {
|
||||
#[doc = #doc]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct #ServiceClient<H>(H) where H: #namespace::handler::Handler;
|
||||
|
||||
impl<H> #ServiceClient<H> where H: #namespace::handler::Handler<Descriptor = #ServiceDescriptor> {
|
||||
/// Creates a new client instance that delegates all method calls to the supplied handler.
|
||||
pub fn new(handler: H) -> Self {
|
||||
Self(handler)
|
||||
}
|
||||
}
|
||||
|
||||
#impl_service_client
|
||||
|
||||
#impl_service_for_client
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_ServiceClient(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
ServiceClient,
|
||||
ServiceDescriptor,
|
||||
ServiceMethodDescriptor,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let methods = methods.iter().map(
|
||||
|Method {
|
||||
method_inner,
|
||||
method_proto,
|
||||
Input,
|
||||
Output,
|
||||
..
|
||||
}| {
|
||||
quote! {
|
||||
async fn #method_inner(handler: H, ctrl: H::Controller, input: #Input) -> #namespace::error::Result<#Output> {
|
||||
#namespace::__rt::call_method(handler, ctrl, #ServiceMethodDescriptor::#method_proto, input).await
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
impl<H> #ServiceClient<H> where H: #namespace::handler::Handler<Descriptor = #ServiceDescriptor> {
|
||||
#(#methods)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_Service_for_ServiceClient(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
Service,
|
||||
ServiceClient,
|
||||
ServiceDescriptor,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let methods = methods.iter().map(
|
||||
|Method {
|
||||
method,
|
||||
method_inner,
|
||||
Input,
|
||||
Output,
|
||||
..
|
||||
}| {
|
||||
quote! {
|
||||
async fn #method(&self, ctrl: H::Controller, input: #Input) -> #namespace::error::Result<#Output> {
|
||||
#ServiceClient::#method_inner(self.0.clone(), ctrl, input).await
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
#[async_trait::async_trait]
|
||||
impl<H> #Service for #ServiceClient<H> where H: #namespace::handler::Handler<Descriptor = #ServiceDescriptor> {
|
||||
type Controller = H::Controller;
|
||||
|
||||
#(#methods)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn struct_ServiceClientFactory(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
Service,
|
||||
ServiceClient,
|
||||
ServiceClientFactory,
|
||||
ServiceDescriptor,
|
||||
..
|
||||
} = self;
|
||||
|
||||
quote! {
|
||||
pub struct #ServiceClientFactory<C: #namespace::controller::Controller>(std::marker::PhantomData<C>);
|
||||
|
||||
impl<C: #namespace::controller::Controller> Clone for #ServiceClientFactory<C> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(std::marker::PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> #namespace::__rt::RpcClientFactory for #ServiceClientFactory<C> where C: #namespace::controller::Controller {
|
||||
type Descriptor = #ServiceDescriptor;
|
||||
type ClientImpl = Box<dyn #Service<Controller = C> + Send + Sync + 'static>;
|
||||
type Controller = C;
|
||||
|
||||
fn new(handler: impl #namespace::handler::Handler<Descriptor = Self::Descriptor, Controller = Self::Controller>) -> Self::ClientImpl {
|
||||
Box::new(#ServiceClient::new(handler))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn struct_ServiceServer(&self) -> TokenStream {
|
||||
let Self {
|
||||
namespace,
|
||||
Service,
|
||||
ServiceDescriptor,
|
||||
ServiceServer,
|
||||
ServiceMethodDescriptor,
|
||||
Service_str,
|
||||
methods,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let doc = formatdoc! {"
|
||||
A server for a `{Service_str}`.
|
||||
|
||||
This implements the `Server` trait by handling requests and dispatch them to methods on the
|
||||
supplied `{Service_str}`.
|
||||
"};
|
||||
|
||||
let arms = methods.iter().map(
|
||||
|Method {
|
||||
method_proto,
|
||||
method,
|
||||
Input,
|
||||
..
|
||||
}| {
|
||||
quote! {
|
||||
#ServiceMethodDescriptor::#method_proto => {
|
||||
let decoded: #Input = #namespace::__rt::decode(input)?;
|
||||
let ret = service.#method(ctrl, decoded).await?;
|
||||
#namespace::__rt::encode(ret)
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
#[doc = #doc]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct #ServiceServer<A>(A) where A: #Service + Clone + Send + 'static;
|
||||
|
||||
impl<T> #ServiceServer<::std::sync::Weak<T>>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
::std::sync::Arc<T>: #Service,
|
||||
{
|
||||
pub fn new_arc(service: ::std::sync::Arc<T>) -> #ServiceServer<::std::sync::Weak<T>> {
|
||||
#ServiceServer(::std::sync::Arc::downgrade(&service))
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> #ServiceServer<A> where A: #Service + Clone + Send + 'static {
|
||||
/// Creates a new server instance that dispatches all calls to the supplied service.
|
||||
pub fn new(service: A) -> #ServiceServer<A> {
|
||||
#ServiceServer(service)
|
||||
}
|
||||
|
||||
async fn call_inner(
|
||||
service: A,
|
||||
method: #ServiceMethodDescriptor,
|
||||
ctrl: A::Controller,
|
||||
input: ::bytes::Bytes)
|
||||
-> #namespace::error::Result<::bytes::Bytes> {
|
||||
match method {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<A> #namespace::handler::Handler for #ServiceServer<A>
|
||||
where
|
||||
A: #Service + Clone + Send + Sync + 'static {
|
||||
type Descriptor = #ServiceDescriptor;
|
||||
type Controller = A::Controller;
|
||||
|
||||
async fn call(
|
||||
&self,
|
||||
ctrl: A::Controller,
|
||||
method: #ServiceMethodDescriptor,
|
||||
input: ::bytes::Bytes)
|
||||
-> #namespace::error::Result<::bytes::Bytes> {
|
||||
#ServiceServer::call_inner(self.0.clone(), method, ctrl, input).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The service generator to be used with `prost-build` to generate RPC implementations for
|
||||
/// `prost-simple-rpc`.
|
||||
///
|
||||
/// See the crate-level documentation for more info.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ServiceGenerator;
|
||||
|
||||
impl prost_build::ServiceGenerator for ServiceGenerator {
|
||||
fn generate(&mut self, service: prost_build::Service, buf: &mut String) {
|
||||
let info = Service::new(service);
|
||||
|
||||
let trait_Service = info.trait_Service();
|
||||
let impl_Service_for_Weak = info.impl_Service_for_Weak();
|
||||
let struct_ServiceDescriptor = info.struct_ServiceDescriptor();
|
||||
let enum_ServiceMethodDescriptor = info.enum_ServiceMethodDescriptor();
|
||||
let struct_ServiceClient = info.struct_ServiceClient();
|
||||
let struct_ServiceClientFactory = info.struct_ServiceClientFactory();
|
||||
let struct_ServiceServer = info.struct_ServiceServer();
|
||||
|
||||
let tokens = quote! {
|
||||
#trait_Service
|
||||
|
||||
#impl_Service_for_Weak
|
||||
|
||||
#struct_ServiceDescriptor
|
||||
|
||||
#enum_ServiceMethodDescriptor
|
||||
|
||||
#struct_ServiceClient
|
||||
|
||||
#struct_ServiceClientFactory
|
||||
|
||||
#struct_ServiceServer
|
||||
};
|
||||
|
||||
buf.push('\n');
|
||||
buf.push_str(&tokens.to_string());
|
||||
buf.push('\n');
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,9 @@ core_clap:
|
||||
仅用户名:--config-server admin,将使用官方的服务器
|
||||
machine_id:
|
||||
en: |+
|
||||
the machine id to identify this machine, used for config recovery after disconnection, must be unique and fixed. by default it is loaded from persisted local state; on first start it may be migrated from system information or generated, then remains fixed.
|
||||
the machine id to identify this machine, used for config recovery after disconnection, must be unique and fixed. default is from system.
|
||||
zh-CN: |+
|
||||
Web 配置服务器通过 machine id 来识别机器,用于断线重连后的配置恢复,需要保证唯一且固定不变。默认从本地持久化状态读取;首次启动时可能基于系统信息迁移或生成,之后保持固定不变。
|
||||
Web 配置服务器通过 machine id 来识别机器,用于断线重连后的配置恢复,需要保证唯一且固定不变。默认从系统获得。
|
||||
config_file:
|
||||
en: "path to the config file, NOTE: the options set by cmdline args will override options in config file"
|
||||
zh-CN: "配置文件路径,注意:命令行中的配置的选项会覆盖配置文件中的选项"
|
||||
@@ -184,9 +184,6 @@ core_clap:
|
||||
disable_upnp:
|
||||
en: "disable runtime UPnP/NAT-PMP port mapping for eligible listeners; automatic port mapping is enabled by default"
|
||||
zh-CN: "禁用符合条件监听器的运行时 UPnP/NAT-PMP 端口映射;自动端口映射默认开启"
|
||||
enable_udp_broadcast_relay:
|
||||
en: "Windows only: capture local UDP broadcast packets from physical interfaces and forward them to EasyTier peers. Helps games to find rooms in local network. Requires administrator privileges."
|
||||
zh-CN: "仅 Windows:捕获物理网卡上的本机 UDP 广播包并转发给 EasyTier 对等节点,帮助局域网游戏发现房间。需要管理员权限。"
|
||||
relay_all_peer_rpc:
|
||||
en: "relay all peer rpc packets, even if the peer is not in the relay network whitelist. this can help peers not in relay network whitelist to establish p2p connection."
|
||||
zh-CN: "转发所有对等节点的RPC数据包,即使对等节点不在转发网络白名单中。这可以帮助白名单外网络中的对等节点建立P2P连接。"
|
||||
@@ -220,6 +217,12 @@ 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 不同或缺失时会被拒绝。"
|
||||
@@ -271,9 +274,6 @@ core_clap:
|
||||
check_config:
|
||||
en: Check config validity without starting the network
|
||||
zh-CN: 检查配置文件的有效性并退出
|
||||
daemon:
|
||||
en: Run in daemon mode
|
||||
zh-CN: 以守护进程模式运行
|
||||
file_log_size_mb:
|
||||
en: "per file log size in MB, default is 100MB"
|
||||
zh-CN: "单个文件日志大小,单位 MB,默认值为 100MB"
|
||||
|
||||
@@ -11,8 +11,9 @@ use windows::{
|
||||
NET_FW_RULE_DIR_OUT,
|
||||
},
|
||||
Networking::WinSock::{
|
||||
IP_UNICAST_IF, IPPROTO_IP, IPPROTO_IPV6, IPV6_UNICAST_IF, SIO_UDP_CONNRESET, SOCKET,
|
||||
SOCKET_ERROR, WSAGetLastError, WSAIoctl, htonl, setsockopt,
|
||||
IP_UNICAST_IF, IPPROTO_IP, IPPROTO_IPV6, IPV6_UNICAST_IF, SIO_UDP_CONNRESET,
|
||||
SO_EXCLUSIVEADDRUSE, SOCKET, SOCKET_ERROR, SOL_SOCKET, WSAGetLastError, WSAIoctl,
|
||||
htonl, setsockopt,
|
||||
},
|
||||
System::Com::{
|
||||
CLSCTX_ALL, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx, CoUninitialize,
|
||||
@@ -136,13 +137,12 @@ pub fn setup_socket_for_win<S: AsRawSocket>(
|
||||
}
|
||||
|
||||
let socket = SOCKET(socket.as_raw_socket() as usize);
|
||||
|
||||
// let optval = 1_i32.to_ne_bytes();
|
||||
// unsafe {
|
||||
// if setsockopt(socket, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, Some(&optval)) == SOCKET_ERROR {
|
||||
// return Err(io::Error::last_os_error());
|
||||
// }
|
||||
// }
|
||||
let optval = 1_i32.to_ne_bytes();
|
||||
unsafe {
|
||||
if setsockopt(socket, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, Some(&optval)) == SOCKET_ERROR {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(iface) = bind_dev {
|
||||
set_ip_unicast_if(socket, bind_addr, &iface)?;
|
||||
|
||||
@@ -1339,45 +1339,6 @@ mod tests {
|
||||
assert_eq!(result.matched_rule, Some(RuleId::Priority(70)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_forward_acl_source_ip_whitelist() {
|
||||
let mut acl_config = Acl::default();
|
||||
let mut acl_v1 = AclV1::default();
|
||||
let mut chain = Chain {
|
||||
name: "subnet_proxy_protect".to_string(),
|
||||
chain_type: ChainType::Forward as i32,
|
||||
enabled: true,
|
||||
default_action: Action::Drop as i32,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
chain.rules.push(Rule {
|
||||
name: "allow_my_devices".to_string(),
|
||||
priority: 1000,
|
||||
enabled: true,
|
||||
action: Action::Allow as i32,
|
||||
protocol: Protocol::Any as i32,
|
||||
source_ips: vec!["10.172.192.2/32".to_string()],
|
||||
..Default::default()
|
||||
});
|
||||
acl_v1.chains.push(chain);
|
||||
acl_config.acl_v1 = Some(acl_v1);
|
||||
|
||||
let processor = AclProcessor::new(acl_config);
|
||||
let mut packet_info = create_test_packet_info();
|
||||
packet_info.dst_ip = "192.168.1.10".parse().unwrap();
|
||||
|
||||
packet_info.src_ip = "10.172.192.2".parse().unwrap();
|
||||
let result = processor.process_packet(&packet_info, ChainType::Forward);
|
||||
assert_eq!(result.action, Action::Allow);
|
||||
assert_eq!(result.matched_rule, Some(RuleId::Priority(1000)));
|
||||
|
||||
packet_info.src_ip = "10.172.192.3".parse().unwrap();
|
||||
let result = processor.process_packet(&packet_info, ChainType::Forward);
|
||||
assert_eq!(result.action, Action::Drop);
|
||||
assert_eq!(result.matched_rule, Some(RuleId::Default));
|
||||
}
|
||||
|
||||
fn create_test_acl_config() -> Acl {
|
||||
let mut acl_config = Acl::default();
|
||||
|
||||
|
||||
+45
-285
@@ -1,114 +1,36 @@
|
||||
use super::env_parser;
|
||||
use crate::utils::dns;
|
||||
use crate::{
|
||||
common::stun::StunInfoCollector,
|
||||
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 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 _;
|
||||
|
||||
#[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,
|
||||
}
|
||||
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},
|
||||
};
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
use super::env_parser;
|
||||
|
||||
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,
|
||||
@@ -133,6 +55,7 @@ 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,
|
||||
@@ -142,11 +65,12 @@ pub fn gen_default_flags() -> Flags {
|
||||
multi_thread_count: 2,
|
||||
encryption_algorithm: EncryptionAlgorithm::default().to_string(),
|
||||
disable_sym_hole_punching: false,
|
||||
tld_dns_zone: DEFAULT_ET_DNS_ZONE.to_string(),
|
||||
|
||||
quic_listen_port: u32::MAX,
|
||||
need_p2p: false,
|
||||
instance_recv_bps_limit: u64::MAX,
|
||||
disable_upnp: false,
|
||||
disable_relay_data: false,
|
||||
enable_udp_broadcast_relay: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,19 +150,8 @@ 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 + DnsConfigLoaderExt {
|
||||
pub trait ConfigLoader: Send + Sync {
|
||||
fn get_id(&self) -> uuid::Uuid;
|
||||
fn set_id(&self, id: uuid::Uuid);
|
||||
|
||||
@@ -325,14 +238,6 @@ pub trait ConfigLoader: Send + Sync + DnsConfigLoaderExt {
|
||||
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>);
|
||||
|
||||
@@ -520,11 +425,11 @@ pub struct ConsoleLoggerConfig {
|
||||
pub level: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Builder)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, derive_builder::Builder)]
|
||||
pub struct LoggingConfig {
|
||||
#[builder(into)]
|
||||
#[builder(setter(into, strip_option), default = None)]
|
||||
pub file_logger: Option<FileLoggerConfig>,
|
||||
#[builder(into)]
|
||||
#[builder(setter(into, strip_option), default = None)]
|
||||
pub console_logger: Option<ConsoleLoggerConfig>,
|
||||
}
|
||||
|
||||
@@ -635,10 +540,6 @@ 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>>,
|
||||
@@ -660,7 +561,6 @@ 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>,
|
||||
@@ -694,10 +594,6 @@ 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)),
|
||||
@@ -739,21 +635,6 @@ 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
|
||||
@@ -769,17 +650,26 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
}
|
||||
|
||||
fn get_hostname(&self) -> String {
|
||||
let hostname = self
|
||||
.config
|
||||
.lock()
|
||||
.unwrap()
|
||||
.hostname
|
||||
.as_ref()
|
||||
.map(|hostname| dns::sanitize(hostname))
|
||||
.filter(|h| !h.is_empty());
|
||||
let hostname = self.config.lock().unwrap().hostname.clone();
|
||||
|
||||
self.set_hostname(hostname.clone());
|
||||
hostname.unwrap_or_else(|| utils::dns::sanitize(utils::hostname()))
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_hostname(&self, name: Option<String>) {
|
||||
@@ -1107,23 +997,6 @@ 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()
|
||||
}
|
||||
@@ -1186,9 +1059,6 @@ 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()
|
||||
}
|
||||
}
|
||||
@@ -1420,51 +1290,6 @@ 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();
|
||||
@@ -1511,71 +1336,6 @@ dns_resolvers = ["https://example.com/dns-query"]
|
||||
assert!(err.to_string().contains("mapped listener port is missing"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acl_toml_rule_uses_defaults_for_omitted_fields() {
|
||||
use crate::proto::acl::{Action, ChainType, Protocol};
|
||||
|
||||
let config_str = r#"
|
||||
[[acl.acl_v1.chains]]
|
||||
name = "subnet_proxy_protect"
|
||||
chain_type = 3
|
||||
enabled = true
|
||||
default_action = 2
|
||||
|
||||
[[acl.acl_v1.chains.rules]]
|
||||
name = "allow_my_devices"
|
||||
priority = 1000
|
||||
action = 1
|
||||
source_ips = ["10.172.192.2/32"]
|
||||
protocol = 5
|
||||
enabled = true
|
||||
"#;
|
||||
|
||||
let config = TomlConfigLoader::new_from_str(config_str).unwrap();
|
||||
let acl = config.get_acl().unwrap();
|
||||
let acl_v1 = acl.acl_v1.unwrap();
|
||||
let chain = &acl_v1.chains[0];
|
||||
let rule = &chain.rules[0];
|
||||
|
||||
assert_eq!(chain.chain_type, ChainType::Forward as i32);
|
||||
assert_eq!(chain.default_action, Action::Drop as i32);
|
||||
assert_eq!(rule.action, Action::Allow as i32);
|
||||
assert_eq!(rule.protocol, Protocol::Any as i32);
|
||||
assert_eq!(rule.source_ips, vec!["10.172.192.2/32"]);
|
||||
assert!(rule.ports.is_empty());
|
||||
assert!(rule.source_ports.is_empty());
|
||||
assert!(rule.destination_ips.is_empty());
|
||||
assert!(rule.source_groups.is_empty());
|
||||
assert!(rule.destination_groups.is_empty());
|
||||
assert_eq!(rule.rate_limit, 0);
|
||||
assert_eq!(rule.burst_limit, 0);
|
||||
assert!(!rule.stateful);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acl_toml_group_can_omit_declares_or_members() {
|
||||
let declares_only = r#"
|
||||
[acl.acl_v1.group]
|
||||
|
||||
[[acl.acl_v1.group.declares]]
|
||||
group_name = "admin"
|
||||
group_secret = "admin-pw"
|
||||
"#;
|
||||
let config = TomlConfigLoader::new_from_str(declares_only).unwrap();
|
||||
let group = config.get_acl().unwrap().acl_v1.unwrap().group.unwrap();
|
||||
assert_eq!(group.declares.len(), 1);
|
||||
assert!(group.members.is_empty());
|
||||
|
||||
let members_only = r#"
|
||||
[acl.acl_v1.group]
|
||||
members = ["admin"]
|
||||
"#;
|
||||
let config = TomlConfigLoader::new_from_str(members_only).unwrap();
|
||||
let group = config.get_acl().unwrap().acl_v1.unwrap().group.unwrap();
|
||||
assert!(group.declares.is_empty());
|
||||
assert_eq!(group.members, vec!["admin"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_config_source_user_is_implicit() {
|
||||
let config = TomlConfigLoader::default();
|
||||
|
||||
@@ -23,6 +23,8 @@ define_global_var!(MANUAL_CONNECTOR_RECONNECT_INTERVAL_MS, u64, 1000);
|
||||
|
||||
define_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, u64, 10);
|
||||
|
||||
define_global_var!(MACHINE_UID, Option<String>, None);
|
||||
|
||||
define_global_var!(MAX_DIRECT_CONNS_PER_PEER_IN_FOREIGN_NETWORK, u32, 3);
|
||||
|
||||
define_global_var!(DIRECT_CONNECT_TO_PUBLIC_SERVER, bool, true);
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
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()))?;
|
||||
// See https://github.com/EasyTier/EasyTier/pull/947
|
||||
// here is for compatibility with old version
|
||||
let port = match port {
|
||||
0 => match url.scheme() {
|
||||
"ws" => 80,
|
||||
"wss" => 443,
|
||||
_ => port,
|
||||
},
|
||||
_ => port,
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{io, result};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::tunnel;
|
||||
@@ -54,6 +55,4 @@ pub enum Error {
|
||||
|
||||
pub type Result<T> = result::Result<T, Error>;
|
||||
|
||||
pub type ErrorCollection = crate::utils::error::ErrorCollection<Error>;
|
||||
|
||||
// impl From for std::
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
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},
|
||||
@@ -35,11 +31,10 @@ use crate::{
|
||||
rpc_service::protected_port,
|
||||
tunnel::matches_protocol,
|
||||
};
|
||||
#[cfg(feature = "magic-dns")]
|
||||
use crate::{
|
||||
dns::config::{DnsConfigLoaderExt, DnsExportConfig, DnsGlobalCtxExt, zone::ZoneConfig},
|
||||
utils::dns,
|
||||
};
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
use socket2::Protocol;
|
||||
|
||||
pub type NetworkIdentity = crate::common::config::NetworkIdentity;
|
||||
|
||||
@@ -53,8 +48,6 @@ 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)
|
||||
@@ -84,11 +77,6 @@ pub enum GlobalCtxEvent {
|
||||
|
||||
ProxyCidrsUpdated(Vec<cidr::Ipv4Cidr>, Vec<cidr::Ipv4Cidr>), // (added, removed)
|
||||
|
||||
UdpBroadcastRelayStartResult {
|
||||
capture_backend: Option<String>,
|
||||
error: Option<String>,
|
||||
},
|
||||
|
||||
CredentialChanged,
|
||||
}
|
||||
|
||||
@@ -229,12 +217,6 @@ pub struct GlobalCtx {
|
||||
|
||||
flags: ArcSwap<Flags>,
|
||||
|
||||
// Runtime/base advertised feature flags before config-owned fields are
|
||||
// overlaid by set_flags. Keep this separate so config patches do not erase
|
||||
// runtime state such as public-server role, IPv6 provider status, or the
|
||||
// non-whitelist avoid-relay preference.
|
||||
base_feature_flags: AtomicCell<PeerFeatureFlag>,
|
||||
|
||||
feature_flags: AtomicCell<PeerFeatureFlag>,
|
||||
|
||||
token_bucket_manager: TokenBucketManager,
|
||||
@@ -262,20 +244,11 @@ impl std::fmt::Debug for GlobalCtx {
|
||||
}
|
||||
}
|
||||
|
||||
pub type ArcGlobalCtx = Arc<GlobalCtx>;
|
||||
pub type ArcGlobalCtx = std::sync::Arc<GlobalCtx>;
|
||||
|
||||
impl GlobalCtx {
|
||||
fn apply_disable_relay_data_flag(
|
||||
flags: &Flags,
|
||||
mut feature_flags: PeerFeatureFlag,
|
||||
) -> PeerFeatureFlag {
|
||||
if flags.disable_relay_data {
|
||||
feature_flags.avoid_relay_data = true;
|
||||
}
|
||||
feature_flags
|
||||
}
|
||||
|
||||
fn derive_feature_flags(flags: &Flags, mut feature_flags: PeerFeatureFlag) -> PeerFeatureFlag {
|
||||
fn derive_feature_flags(flags: &Flags, current: Option<PeerFeatureFlag>) -> PeerFeatureFlag {
|
||||
let mut feature_flags = current.unwrap_or_default();
|
||||
feature_flags.kcp_input = !flags.disable_kcp_input;
|
||||
feature_flags.no_relay_kcp = flags.disable_relay_kcp;
|
||||
feature_flags.support_conn_list_sync = true;
|
||||
@@ -283,7 +256,7 @@ impl GlobalCtx {
|
||||
feature_flags.no_relay_quic = flags.disable_relay_quic;
|
||||
feature_flags.need_p2p = flags.need_p2p;
|
||||
feature_flags.disable_p2p = flags.disable_p2p;
|
||||
Self::apply_disable_relay_data_flag(flags, feature_flags)
|
||||
feature_flags
|
||||
}
|
||||
|
||||
pub fn new(config_fs: impl ConfigLoader + 'static) -> Self {
|
||||
@@ -294,12 +267,6 @@ 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() {
|
||||
@@ -318,8 +285,7 @@ impl GlobalCtx {
|
||||
|
||||
let flags = config_fs.get_flags();
|
||||
|
||||
let base_feature_flags = PeerFeatureFlag::default();
|
||||
let feature_flags = Self::derive_feature_flags(&flags, base_feature_flags);
|
||||
let feature_flags = Self::derive_feature_flags(&flags, None);
|
||||
|
||||
let credential_storage_path = config_fs.get_credential_file();
|
||||
let credential_manager = Arc::new(CredentialManager::new(credential_storage_path));
|
||||
@@ -352,8 +318,6 @@ impl GlobalCtx {
|
||||
|
||||
flags: ArcSwap::new(Arc::new(flags)),
|
||||
|
||||
base_feature_flags: AtomicCell::new(base_feature_flags),
|
||||
|
||||
feature_flags: AtomicCell::new(feature_flags),
|
||||
|
||||
token_bucket_manager: TokenBucketManager::new(),
|
||||
@@ -504,7 +468,7 @@ impl GlobalCtx {
|
||||
}
|
||||
|
||||
pub fn get_hostname(&self) -> String {
|
||||
self.hostname.lock().unwrap().clone()
|
||||
return self.hostname.lock().unwrap().clone();
|
||||
}
|
||||
|
||||
pub fn set_hostname(&self, hostname: String) {
|
||||
@@ -549,7 +513,7 @@ impl GlobalCtx {
|
||||
self.config.set_flags(flags.clone());
|
||||
self.feature_flags.store(Self::derive_feature_flags(
|
||||
&flags,
|
||||
self.base_feature_flags.load(),
|
||||
Some(self.feature_flags.load()),
|
||||
));
|
||||
self.flags.store(Arc::new(flags));
|
||||
}
|
||||
@@ -614,53 +578,8 @@ impl GlobalCtx {
|
||||
self.feature_flags.load()
|
||||
}
|
||||
|
||||
/// Replace the runtime/base advertised flags as a complete snapshot.
|
||||
///
|
||||
/// This is intended for foreign scoped contexts that inherit an already
|
||||
/// computed feature-flag snapshot from their parent. Most callers should use
|
||||
/// a narrower setter so they do not accidentally overwrite unrelated runtime
|
||||
/// state.
|
||||
pub fn set_base_advertised_feature_flags(&self, feature_flags: PeerFeatureFlag) {
|
||||
self.base_feature_flags.store(feature_flags);
|
||||
let flags = self.flags.load();
|
||||
self.feature_flags
|
||||
.store(Self::apply_disable_relay_data_flag(
|
||||
flags.as_ref(),
|
||||
feature_flags,
|
||||
));
|
||||
}
|
||||
|
||||
/// Set the avoid-relay preference that is independent of disable_relay_data.
|
||||
///
|
||||
/// disable_relay_data still forces the effective advertised flag to true,
|
||||
/// but this base preference is preserved when that config flag is toggled.
|
||||
pub fn set_avoid_relay_data_preference(&self, avoid_relay_data: bool) -> bool {
|
||||
let mut base_feature_flags = self.base_feature_flags.load();
|
||||
base_feature_flags.avoid_relay_data = avoid_relay_data;
|
||||
self.base_feature_flags.store(base_feature_flags);
|
||||
|
||||
let mut feature_flags = self.feature_flags.load();
|
||||
let previous = feature_flags.avoid_relay_data;
|
||||
feature_flags.avoid_relay_data = avoid_relay_data || self.flags.load().disable_relay_data;
|
||||
self.feature_flags.store(feature_flags);
|
||||
previous != feature_flags.avoid_relay_data
|
||||
}
|
||||
|
||||
/// Set the runtime IPv6-provider advertised bit without touching
|
||||
/// config-derived feature flags.
|
||||
pub fn set_ipv6_public_addr_provider_feature_flag(&self, enabled: bool) -> bool {
|
||||
let mut base_feature_flags = self.base_feature_flags.load();
|
||||
base_feature_flags.ipv6_public_addr_provider = enabled;
|
||||
self.base_feature_flags.store(base_feature_flags);
|
||||
|
||||
let mut feature_flags = self.feature_flags.load();
|
||||
if feature_flags.ipv6_public_addr_provider == enabled {
|
||||
return false;
|
||||
}
|
||||
|
||||
feature_flags.ipv6_public_addr_provider = enabled;
|
||||
self.feature_flags.store(feature_flags);
|
||||
true
|
||||
pub fn set_feature_flags(&self, flags: PeerFeatureFlag) {
|
||||
self.feature_flags.store(flags);
|
||||
}
|
||||
|
||||
pub fn token_bucket_manager(&self) -> &TokenBucketManager {
|
||||
@@ -799,39 +718,6 @@ 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::{
|
||||
@@ -910,7 +796,7 @@ pub mod tests {
|
||||
let mut feature_flags = global_ctx.get_feature_flags();
|
||||
feature_flags.avoid_relay_data = true;
|
||||
feature_flags.is_public_server = true;
|
||||
global_ctx.set_base_advertised_feature_flags(feature_flags);
|
||||
global_ctx.set_feature_flags(feature_flags);
|
||||
|
||||
let mut flags = global_ctx.get_flags().clone();
|
||||
flags.disable_kcp_input = true;
|
||||
@@ -934,83 +820,6 @@ pub mod tests {
|
||||
assert!(!feature_flags.ipv6_public_addr_provider);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_base_advertised_feature_flags_applies_current_values() {
|
||||
let config = TomlConfigLoader::default();
|
||||
let global_ctx = GlobalCtx::new(config);
|
||||
|
||||
let feature_flags = PeerFeatureFlag {
|
||||
kcp_input: false,
|
||||
no_relay_kcp: true,
|
||||
quic_input: false,
|
||||
no_relay_quic: true,
|
||||
is_public_server: true,
|
||||
..Default::default()
|
||||
};
|
||||
global_ctx.set_base_advertised_feature_flags(feature_flags);
|
||||
|
||||
assert_eq!(global_ctx.get_feature_flags(), feature_flags);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_base_advertised_feature_flags_keeps_disable_relay_data_effective() {
|
||||
let config = TomlConfigLoader::default();
|
||||
let global_ctx = GlobalCtx::new(config);
|
||||
|
||||
let mut flags = global_ctx.get_flags().clone();
|
||||
flags.disable_relay_data = true;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
let mut feature_flags = global_ctx.get_feature_flags();
|
||||
feature_flags.avoid_relay_data = false;
|
||||
feature_flags.is_public_server = true;
|
||||
global_ctx.set_base_advertised_feature_flags(feature_flags);
|
||||
|
||||
let advertised_feature_flags = global_ctx.get_feature_flags();
|
||||
assert!(advertised_feature_flags.avoid_relay_data);
|
||||
assert!(advertised_feature_flags.is_public_server);
|
||||
|
||||
let mut flags = global_ctx.get_flags().clone();
|
||||
flags.disable_relay_data = false;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
let advertised_feature_flags = global_ctx.get_feature_flags();
|
||||
assert!(!advertised_feature_flags.avoid_relay_data);
|
||||
assert!(advertised_feature_flags.is_public_server);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disable_relay_data_sets_avoid_relay_feature_flag() {
|
||||
let config = TomlConfigLoader::default();
|
||||
let global_ctx = GlobalCtx::new(config);
|
||||
|
||||
let mut flags = global_ctx.get_flags().clone();
|
||||
flags.disable_relay_data = true;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
assert!(global_ctx.get_feature_flags().avoid_relay_data);
|
||||
|
||||
let mut flags = global_ctx.get_flags().clone();
|
||||
flags.disable_relay_data = false;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
assert!(!global_ctx.get_feature_flags().avoid_relay_data);
|
||||
|
||||
global_ctx.set_avoid_relay_data_preference(true);
|
||||
|
||||
let mut flags = global_ctx.get_flags().clone();
|
||||
flags.disable_relay_data = true;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
assert!(global_ctx.get_feature_flags().avoid_relay_data);
|
||||
|
||||
let mut flags = global_ctx.get_flags().clone();
|
||||
flags.disable_relay_data = false;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
assert!(global_ctx.get_feature_flags().avoid_relay_data);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn should_deny_proxy_for_process_wide_rpc_port() {
|
||||
protected_port::clear_protected_tcp_ports_for_test();
|
||||
|
||||
+11
-193
@@ -58,21 +58,6 @@ fn parse_env_filter(default_level: Option<LevelFilter>) -> Result<EnvFilter, any
|
||||
.with_context(|| "failed to create env filter")
|
||||
}
|
||||
|
||||
fn parse_static_filter(level: LevelFilter) -> Result<EnvFilter, anyhow::Error> {
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(level.into())
|
||||
.parse("")
|
||||
.with_context(|| "failed to create static filter")
|
||||
}
|
||||
|
||||
fn parse_file_filter(level: LevelFilter) -> Result<EnvFilter, anyhow::Error> {
|
||||
if matches!(level, LevelFilter::OFF) {
|
||||
parse_static_filter(level)
|
||||
} else {
|
||||
parse_env_filter(Some(level))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_log(meta: &Metadata) -> bool {
|
||||
meta.target() == LOG_TARGET || meta.target().starts_with(&format!("{LOG_TARGET}::"))
|
||||
}
|
||||
@@ -180,17 +165,14 @@ fn file_layers(
|
||||
) -> anyhow::Result<(Vec<BoxLayer>, Option<NewFilterSender>)> {
|
||||
let mut layers = Vec::new();
|
||||
|
||||
let level = config
|
||||
.level
|
||||
.map(|s| s.parse().unwrap())
|
||||
.unwrap_or(LevelFilter::OFF);
|
||||
let level = config.level.map(|s| s.parse().unwrap());
|
||||
|
||||
if matches!(level, LevelFilter::OFF) && !reload {
|
||||
if matches!(level, Some(LevelFilter::OFF)) && !reload {
|
||||
return Ok((layers, None));
|
||||
}
|
||||
|
||||
let (file_filter, file_filter_reloader) =
|
||||
tracing_subscriber::reload::Layer::<_, Registry>::new(parse_file_filter(level)?);
|
||||
tracing_subscriber::reload::Layer::<_, Registry>::new(parse_env_filter(level)?);
|
||||
|
||||
let layer = |wrapper| {
|
||||
layer()
|
||||
@@ -236,7 +218,9 @@ fn file_layers(
|
||||
|
||||
// 初始化全局状态
|
||||
let _ = LOGGER_LEVEL_SENDER.set(std::sync::Mutex::new(tx.clone()));
|
||||
let _ = CURRENT_LOG_LEVEL.set(std::sync::Mutex::new(level.to_string()));
|
||||
if let Some(level) = level {
|
||||
let _ = CURRENT_LOG_LEVEL.set(std::sync::Mutex::new(level.to_string()));
|
||||
}
|
||||
|
||||
std::thread::spawn(move || {
|
||||
while let Ok(lf) = rx.recv() {
|
||||
@@ -248,7 +232,11 @@ fn file_layers(
|
||||
}
|
||||
};
|
||||
|
||||
let mut new_filter = match parse_file_filter(parsed_level) {
|
||||
let mut new_filter = match EnvFilter::builder()
|
||||
.with_default_directive(parsed_level.into())
|
||||
.from_env()
|
||||
.with_context(|| "failed to create file filter")
|
||||
{
|
||||
Ok(filter) => Some(filter),
|
||||
Err(e) => {
|
||||
error!("Failed to build new log filter for {:?}: {:?}", lf, e);
|
||||
@@ -280,36 +268,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::common::config::FileLoggerConfig;
|
||||
|
||||
const RUST_LOG: &str = "RUST_LOG";
|
||||
|
||||
struct EnvVarGuard {
|
||||
key: &'static str,
|
||||
previous: Option<std::ffi::OsString>,
|
||||
}
|
||||
|
||||
impl EnvVarGuard {
|
||||
fn set(key: &'static str, value: &str) -> Self {
|
||||
let previous = std::env::var_os(key);
|
||||
unsafe { std::env::set_var(key, value) };
|
||||
Self { key, previous }
|
||||
}
|
||||
|
||||
fn unset(key: &'static str) -> Self {
|
||||
let previous = std::env::var_os(key);
|
||||
unsafe { std::env::remove_var(key) };
|
||||
Self { key, previous }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvVarGuard {
|
||||
fn drop(&mut self) {
|
||||
match &self.previous {
|
||||
Some(value) => unsafe { std::env::set_var(self.key, value) },
|
||||
None => unsafe { std::env::remove_var(self.key) },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init() {
|
||||
let _ = Registry::default()
|
||||
@@ -318,147 +276,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_file_logger_level_is_off_without_reload() {
|
||||
let (layers, sender) = file_layers(FileLoggerConfig::default(), false).unwrap();
|
||||
|
||||
assert!(layers.is_empty());
|
||||
assert!(sender.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn default_file_logger_level_filters_info_with_reload() {
|
||||
let _guard = EnvVarGuard::set(RUST_LOG, "info");
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let log_file_name = "default-off-test.log".to_string();
|
||||
let log_path = temp_dir.path().join(&log_file_name);
|
||||
|
||||
let cfg = FileLoggerConfig {
|
||||
file: Some(log_file_name),
|
||||
dir: Some(temp_dir.path().to_string_lossy().to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (layers, _sender) = file_layers(cfg, true).unwrap();
|
||||
let marker = "default-file-logger-off-marker";
|
||||
let subscriber = Registry::default().with(layers);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
tracing::info!(target: LOG_TARGET, "{}", marker);
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
});
|
||||
|
||||
let content = std::fs::read_to_string(&log_path).unwrap_or_default();
|
||||
assert!(
|
||||
!content.contains(marker),
|
||||
"default file logger level should filter info logs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn file_logger_level_uses_env_filter_when_enabled() {
|
||||
let _guard = EnvVarGuard::set(RUST_LOG, "debug");
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let log_file_name = "env-filter-test.log".to_string();
|
||||
let log_path = temp_dir.path().join(&log_file_name);
|
||||
|
||||
let cfg = FileLoggerConfig {
|
||||
level: Some(LevelFilter::INFO.to_string()),
|
||||
file: Some(log_file_name),
|
||||
dir: Some(temp_dir.path().to_string_lossy().to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (layers, _sender) = file_layers(cfg, true).unwrap();
|
||||
let marker = "file-logger-env-filter-marker";
|
||||
let subscriber = Registry::default().with(layers);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
tracing::debug!(target: LOG_TARGET, "{}", marker);
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
});
|
||||
|
||||
let content = std::fs::read_to_string(&log_path).unwrap_or_default();
|
||||
assert!(
|
||||
content.contains(marker),
|
||||
"enabled file logger should use RUST_LOG directives"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn file_logger_reload_uses_env_filter_when_enabled() {
|
||||
let _guard = EnvVarGuard::set(RUST_LOG, "debug");
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let log_file_name = "reload-env-filter-test.log".to_string();
|
||||
let log_path = temp_dir.path().join(&log_file_name);
|
||||
|
||||
let cfg = FileLoggerConfig {
|
||||
file: Some(log_file_name),
|
||||
dir: Some(temp_dir.path().to_string_lossy().to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (layers, sender) = file_layers(cfg, true).unwrap();
|
||||
let sender = sender.expect("reload=true should return a sender");
|
||||
let marker = "file-logger-reload-env-filter-marker";
|
||||
let subscriber = Registry::default().with(layers);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
sender.send(LevelFilter::INFO.to_string()).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
|
||||
tracing::debug!(target: LOG_TARGET, "{}", marker);
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
});
|
||||
|
||||
let content = std::fs::read_to_string(&log_path).unwrap_or_default();
|
||||
assert!(
|
||||
content.contains(marker),
|
||||
"file logger enabled by reload should use RUST_LOG directives"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn file_logger_reload_off_ignores_env_filter() {
|
||||
let _guard = EnvVarGuard::set(RUST_LOG, "info");
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let log_file_name = "reload-off-test.log".to_string();
|
||||
let log_path = temp_dir.path().join(&log_file_name);
|
||||
|
||||
let cfg = FileLoggerConfig {
|
||||
level: Some(LevelFilter::INFO.to_string()),
|
||||
file: Some(log_file_name),
|
||||
dir: Some(temp_dir.path().to_string_lossy().to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (layers, sender) = file_layers(cfg, true).unwrap();
|
||||
let sender = sender.expect("reload=true should return a sender");
|
||||
let marker = "file-logger-reload-off-marker";
|
||||
let subscriber = Registry::default().with(layers);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
sender.send(LevelFilter::OFF.to_string()).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
|
||||
tracing::info!(target: LOG_TARGET, "{}", marker);
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
});
|
||||
|
||||
let content = std::fs::read_to_string(&log_path).unwrap_or_default();
|
||||
assert!(
|
||||
!content.contains(marker),
|
||||
"disabled file logger should ignore RUST_LOG directives"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn test_logger_reload() {
|
||||
let _guard = EnvVarGuard::unset(RUST_LOG);
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let log_file_name = "reload-test.log".to_string();
|
||||
let log_path = temp_dir.path().join(&log_file_name);
|
||||
|
||||
@@ -1,594 +0,0 @@
|
||||
use std::{
|
||||
env,
|
||||
ffi::OsString,
|
||||
io::Write as _,
|
||||
path::{Path, PathBuf},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::utils::hostname;
|
||||
use anyhow::Context as _;
|
||||
#[cfg(unix)]
|
||||
use nix::{
|
||||
errno::Errno,
|
||||
fcntl::{Flock, FlockArg},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MachineIdOptions {
|
||||
pub explicit_machine_id: Option<String>,
|
||||
pub state_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn resolve_machine_id(opts: &MachineIdOptions) -> anyhow::Result<uuid::Uuid> {
|
||||
if let Some(explicit_machine_id) = opts.explicit_machine_id.as_deref() {
|
||||
return Ok(parse_or_hash_machine_id(explicit_machine_id));
|
||||
}
|
||||
|
||||
let state_file = resolve_machine_id_state_file(opts.state_dir.as_deref())?;
|
||||
let allow_legacy_machine_uid_migration =
|
||||
should_attempt_legacy_machine_uid_migration(&state_file);
|
||||
if let Some(machine_id) = read_state_machine_id(&state_file)? {
|
||||
return Ok(machine_id);
|
||||
}
|
||||
|
||||
if let Some(machine_id) = read_legacy_machine_id_file() {
|
||||
return persist_machine_id(&state_file, machine_id);
|
||||
}
|
||||
|
||||
if allow_legacy_machine_uid_migration
|
||||
&& let Some(machine_id) = resolve_legacy_machine_uid_hash()
|
||||
{
|
||||
return persist_machine_id(&state_file, machine_id);
|
||||
}
|
||||
|
||||
let machine_id = resolve_new_machine_id().unwrap_or_else(uuid::Uuid::new_v4);
|
||||
persist_machine_id(&state_file, machine_id)
|
||||
}
|
||||
|
||||
fn parse_or_hash_machine_id(raw: &str) -> uuid::Uuid {
|
||||
if let Ok(mid) = uuid::Uuid::parse_str(raw.trim()) {
|
||||
return mid;
|
||||
}
|
||||
digest_uuid_from_str(raw)
|
||||
}
|
||||
|
||||
fn digest_uuid_from_str(raw: &str) -> uuid::Uuid {
|
||||
let mut b = [0u8; 16];
|
||||
crate::tunnel::generate_digest_from_str("", raw, &mut b);
|
||||
uuid::Uuid::from_bytes(b)
|
||||
}
|
||||
|
||||
fn resolve_machine_id_state_file(state_dir: Option<&Path>) -> anyhow::Result<PathBuf> {
|
||||
let state_dir = match state_dir {
|
||||
Some(dir) => dir.to_path_buf(),
|
||||
None => default_machine_id_state_dir()?,
|
||||
};
|
||||
Ok(state_dir.join("machine_id"))
|
||||
}
|
||||
|
||||
fn non_empty_os_string(value: Option<OsString>) -> Option<OsString> {
|
||||
value.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn default_linux_machine_id_state_dir(
|
||||
xdg_data_home: Option<OsString>,
|
||||
home: Option<OsString>,
|
||||
) -> PathBuf {
|
||||
if let Some(path) = non_empty_os_string(xdg_data_home) {
|
||||
return PathBuf::from(path).join("easytier");
|
||||
}
|
||||
|
||||
if let Some(home) = non_empty_os_string(home) {
|
||||
return PathBuf::from(home)
|
||||
.join(".local")
|
||||
.join("share")
|
||||
.join("easytier");
|
||||
}
|
||||
|
||||
PathBuf::from("/var/lib/easytier")
|
||||
}
|
||||
|
||||
fn default_machine_id_state_dir() -> anyhow::Result<PathBuf> {
|
||||
cfg_select! {
|
||||
target_os = "linux" => Ok(default_linux_machine_id_state_dir(
|
||||
env::var_os("XDG_DATA_HOME"),
|
||||
env::var_os("HOME"),
|
||||
)),
|
||||
all(target_os = "macos", not(feature = "macos-ne")) => {
|
||||
let home = non_empty_os_string(env::var_os("HOME"))
|
||||
.ok_or_else(|| anyhow::anyhow!("HOME is not set, cannot resolve machine id state directory"))?;
|
||||
Ok(PathBuf::from(home)
|
||||
.join("Library")
|
||||
.join("Application Support")
|
||||
.join("com.easytier"))
|
||||
},
|
||||
target_os = "windows" => {
|
||||
let local_app_data = non_empty_os_string(env::var_os("LOCALAPPDATA")).ok_or_else(|| {
|
||||
anyhow::anyhow!("LOCALAPPDATA is not set, cannot resolve machine id state directory")
|
||||
})?;
|
||||
Ok(PathBuf::from(local_app_data).join("easytier"))
|
||||
},
|
||||
target_os = "freebsd" => {
|
||||
let home = non_empty_os_string(env::var_os("HOME"))
|
||||
.ok_or_else(|| anyhow::anyhow!("HOME is not set, cannot resolve machine id state directory"))?;
|
||||
Ok(PathBuf::from(home).join(".local").join("share").join("easytier"))
|
||||
},
|
||||
target_os = "android" => {
|
||||
anyhow::bail!("machine id state directory must be provided explicitly on Android");
|
||||
},
|
||||
_ => anyhow::bail!("machine id state directory is unsupported on this platform"),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_state_machine_id(path: &Path) -> anyhow::Result<Option<uuid::Uuid>> {
|
||||
let Some(contents) = read_optional_file(path)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let machine_id = uuid::Uuid::parse_str(contents.trim())
|
||||
.with_context(|| format!("invalid machine id in state file {}", path.display()))?;
|
||||
Ok(Some(machine_id))
|
||||
}
|
||||
|
||||
fn read_legacy_machine_id_file() -> Option<uuid::Uuid> {
|
||||
let path = legacy_machine_id_file_path()?;
|
||||
read_legacy_machine_id_file_at(&path)
|
||||
}
|
||||
|
||||
fn read_legacy_machine_id_file_at(path: &Path) -> Option<uuid::Uuid> {
|
||||
let contents = match std::fs::read_to_string(path) {
|
||||
Ok(contents) => contents,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return None,
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
path = %path.display(),
|
||||
%err,
|
||||
"ignoring unreadable legacy machine id file"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
match uuid::Uuid::parse_str(contents.trim()) {
|
||||
Ok(machine_id) => Some(machine_id),
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
path = %path.display(),
|
||||
%err,
|
||||
"ignoring invalid legacy machine id file"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn legacy_machine_id_file_path() -> Option<PathBuf> {
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.map(|path| path.with_file_name("et_machine_id"))
|
||||
}
|
||||
|
||||
fn read_optional_file(path: &Path) -> anyhow::Result<Option<String>> {
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(contents) => Ok(Some(contents)),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(err).with_context(|| format!("failed to read {}", path.display())),
|
||||
}
|
||||
}
|
||||
|
||||
fn should_attempt_legacy_machine_uid_migration(state_file: &Path) -> bool {
|
||||
let Some(state_dir) = state_file.parent() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Ok(mut entries) = std::fs::read_dir(state_dir) else {
|
||||
return false;
|
||||
};
|
||||
entries.any(|entry| entry.is_ok())
|
||||
}
|
||||
|
||||
fn resolve_legacy_machine_uid_hash() -> Option<uuid::Uuid> {
|
||||
machine_uid_seed().map(|seed| digest_uuid_from_str(seed.as_str()))
|
||||
}
|
||||
|
||||
fn resolve_new_machine_id() -> Option<uuid::Uuid> {
|
||||
let seed = machine_uid_seed()?;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let seed = linux_machine_id_seed(&seed);
|
||||
Some(digest_uuid_from_str(&seed))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Some(digest_uuid_from_str(&seed))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
all(target_os = "macos", not(feature = "macos-ne")),
|
||||
target_os = "windows",
|
||||
target_os = "freebsd"
|
||||
))]
|
||||
fn machine_uid_seed() -> Option<String> {
|
||||
machine_uid::get()
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
}
|
||||
|
||||
#[cfg(not(any(
|
||||
target_os = "linux",
|
||||
all(target_os = "macos", not(feature = "macos-ne")),
|
||||
target_os = "windows",
|
||||
target_os = "freebsd"
|
||||
)))]
|
||||
fn machine_uid_seed() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn linux_machine_id_seed(machine_uid: &str) -> String {
|
||||
let mut seed = format!("machine_uid={machine_uid}");
|
||||
|
||||
let hostname = hostname();
|
||||
if !hostname.is_empty() {
|
||||
seed.push_str("\nhostname=");
|
||||
seed.push_str(&hostname);
|
||||
}
|
||||
|
||||
let mac_addresses = collect_linux_mac_addresses();
|
||||
if !mac_addresses.is_empty() {
|
||||
seed.push_str("\nmacs=");
|
||||
seed.push_str(&mac_addresses.join(","));
|
||||
}
|
||||
|
||||
seed
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn collect_linux_mac_addresses() -> Vec<String> {
|
||||
let mut macs = Vec::new();
|
||||
let Ok(entries) = std::fs::read_dir("/sys/class/net") else {
|
||||
return macs;
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let Ok(name) = entry.file_name().into_string() else {
|
||||
continue;
|
||||
};
|
||||
if name == "lo" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let address_path = entry.path().join("address");
|
||||
let Ok(address) = std::fs::read_to_string(address_path) else {
|
||||
continue;
|
||||
};
|
||||
let address = address.trim().to_ascii_lowercase();
|
||||
if address.is_empty() || address == "00:00:00:00:00:00" {
|
||||
continue;
|
||||
}
|
||||
macs.push(address);
|
||||
}
|
||||
|
||||
macs.sort();
|
||||
macs.dedup();
|
||||
macs.truncate(3);
|
||||
macs
|
||||
}
|
||||
|
||||
fn persist_machine_id(path: &Path, machine_id: uuid::Uuid) -> anyhow::Result<uuid::Uuid> {
|
||||
if let Some(existing) = read_state_machine_id(path)? {
|
||||
return Ok(existing);
|
||||
}
|
||||
|
||||
let _lock = MachineIdWriteLock::acquire(path)?;
|
||||
|
||||
if let Some(existing) = read_state_machine_id(path)? {
|
||||
return Ok(existing);
|
||||
}
|
||||
|
||||
write_uuid_file_atomically(path, machine_id)?;
|
||||
Ok(machine_id)
|
||||
}
|
||||
|
||||
fn write_uuid_file_atomically(path: &Path, machine_id: uuid::Uuid) -> anyhow::Result<()> {
|
||||
let parent = path.parent().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"machine id state file {} has no parent directory",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
std::fs::create_dir_all(parent).with_context(|| {
|
||||
format!(
|
||||
"failed to create machine id state directory {}",
|
||||
parent.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
let tmp_path = parent.join(format!(
|
||||
".machine_id.tmp-{}-{}",
|
||||
std::process::id(),
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
{
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&tmp_path)
|
||||
.with_context(|| format!("failed to create {}", tmp_path.display()))?;
|
||||
file.write_all(machine_id.to_string().as_bytes())
|
||||
.with_context(|| format!("failed to write {}", tmp_path.display()))?;
|
||||
file.sync_all()
|
||||
.with_context(|| format!("failed to flush {}", tmp_path.display()))?;
|
||||
}
|
||||
|
||||
if let Err(err) = std::fs::rename(&tmp_path, path) {
|
||||
let _ = std::fs::remove_file(&tmp_path);
|
||||
return Err(err).with_context(|| {
|
||||
format!(
|
||||
"failed to move machine id state file into place at {}",
|
||||
path.display()
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct MachineIdWriteLock {
|
||||
#[cfg(unix)]
|
||||
_lock: Flock<std::fs::File>,
|
||||
#[cfg(not(unix))]
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl MachineIdWriteLock {
|
||||
fn acquire(path: &Path) -> anyhow::Result<Self> {
|
||||
let parent = path.parent().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"machine id state file {} has no parent directory",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
std::fs::create_dir_all(parent).with_context(|| {
|
||||
format!(
|
||||
"failed to create machine id state directory {}",
|
||||
parent.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
Self::acquire_unix(path)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
Self::acquire_fallback(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn acquire_unix(path: &Path) -> anyhow::Result<Self> {
|
||||
let lock_path = path.with_extension("lock");
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
let mut lock_file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(&lock_path)
|
||||
.with_context(|| format!("failed to open machine id lock {}", lock_path.display()))?;
|
||||
|
||||
loop {
|
||||
match Flock::lock(lock_file, FlockArg::LockExclusiveNonblock) {
|
||||
Ok(lock) => return Ok(Self { _lock: lock }),
|
||||
Err((file, Errno::EAGAIN)) => {
|
||||
if Instant::now() >= deadline {
|
||||
anyhow::bail!(
|
||||
"timed out waiting for machine id lock {}",
|
||||
lock_path.display()
|
||||
);
|
||||
}
|
||||
lock_file = file;
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
Err((_file, err)) => {
|
||||
anyhow::bail!(
|
||||
"failed to acquire machine id lock {}: {}",
|
||||
lock_path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn acquire_fallback(path: &Path) -> anyhow::Result<Self> {
|
||||
let lock_path = path.with_extension("lock");
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
|
||||
loop {
|
||||
match std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&lock_path)
|
||||
{
|
||||
Ok(mut file) => {
|
||||
writeln!(file, "pid={}", std::process::id()).ok();
|
||||
return Ok(Self { path: lock_path });
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||
if should_reap_stale_lock_file(&lock_path) {
|
||||
let _ = std::fs::remove_file(&lock_path);
|
||||
continue;
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
anyhow::bail!(
|
||||
"timed out waiting for machine id lock {}",
|
||||
lock_path.display()
|
||||
);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err).with_context(|| {
|
||||
format!("failed to acquire machine id lock {}", lock_path.display())
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn should_reap_stale_lock_file(lock_path: &Path) -> bool {
|
||||
const STALE_LOCK_AGE: Duration = Duration::from_secs(30);
|
||||
|
||||
let Ok(metadata) = std::fs::metadata(lock_path) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(modified) = metadata.modified() else {
|
||||
return false;
|
||||
};
|
||||
modified
|
||||
.elapsed()
|
||||
.is_ok_and(|elapsed| elapsed >= STALE_LOCK_AGE)
|
||||
}
|
||||
|
||||
impl Drop for MachineIdWriteLock {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(not(unix))]
|
||||
let _ = std::fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_resolve_machine_id_uses_uuid_seed_verbatim() {
|
||||
let raw = "33333333-3333-3333-3333-333333333333".to_string();
|
||||
let opts = MachineIdOptions {
|
||||
explicit_machine_id: Some(raw.clone()),
|
||||
state_dir: None,
|
||||
};
|
||||
assert_eq!(
|
||||
resolve_machine_id(&opts).unwrap(),
|
||||
uuid::Uuid::parse_str(&raw).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_machine_id_reads_state_file() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let expected = uuid::Uuid::new_v4();
|
||||
std::fs::write(temp_dir.path().join("machine_id"), expected.to_string()).unwrap();
|
||||
|
||||
let opts = MachineIdOptions {
|
||||
explicit_machine_id: None,
|
||||
state_dir: Some(temp_dir.path().to_path_buf()),
|
||||
};
|
||||
|
||||
assert_eq!(resolve_machine_id(&opts).unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_legacy_machine_id_file_ignores_read_errors() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
assert_eq!(read_legacy_machine_id_file_at(temp_dir.path()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_uuid_file_atomically_writes_expected_contents() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let machine_id = uuid::Uuid::new_v4();
|
||||
let state_file = temp_dir.path().join("machine_id");
|
||||
|
||||
write_uuid_file_atomically(&state_file, machine_id).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(state_file).unwrap(),
|
||||
machine_id.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_empty_os_string_filters_empty_values() {
|
||||
assert_eq!(non_empty_os_string(Some(OsString::new())), None);
|
||||
assert_eq!(
|
||||
non_empty_os_string(Some(OsString::from("foo"))),
|
||||
Some(OsString::from("foo"))
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn test_default_linux_machine_id_state_dir_falls_back_in_order() {
|
||||
assert_eq!(
|
||||
default_linux_machine_id_state_dir(
|
||||
Some(OsString::from("/tmp/xdg")),
|
||||
Some(OsString::from("/tmp/home"))
|
||||
),
|
||||
PathBuf::from("/tmp/xdg").join("easytier")
|
||||
);
|
||||
assert_eq!(
|
||||
default_linux_machine_id_state_dir(
|
||||
Some(OsString::new()),
|
||||
Some(OsString::from("/tmp/home"))
|
||||
),
|
||||
PathBuf::from("/tmp/home")
|
||||
.join(".local")
|
||||
.join("share")
|
||||
.join("easytier")
|
||||
);
|
||||
assert_eq!(
|
||||
default_linux_machine_id_state_dir(Some(OsString::new()), Some(OsString::new())),
|
||||
PathBuf::from("/var/lib/easytier")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_persist_machine_id_creates_missing_state_dir() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let state_file = temp_dir.path().join("nested").join("machine_id");
|
||||
let machine_id = uuid::Uuid::new_v4();
|
||||
|
||||
assert_eq!(
|
||||
persist_machine_id(&state_file, machine_id).unwrap(),
|
||||
machine_id
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(state_file).unwrap(),
|
||||
machine_id.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_legacy_machine_uid_migration_requires_existing_state_dir_content() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let missing_state_file = temp_dir.path().join("missing").join("machine_id");
|
||||
assert!(!should_attempt_legacy_machine_uid_migration(
|
||||
&missing_state_file
|
||||
));
|
||||
|
||||
let empty_dir = temp_dir.path().join("empty");
|
||||
std::fs::create_dir_all(&empty_dir).unwrap();
|
||||
assert!(!should_attempt_legacy_machine_uid_migration(
|
||||
&empty_dir.join("machine_id")
|
||||
));
|
||||
|
||||
std::fs::write(empty_dir.join("config.toml"), "x=1").unwrap();
|
||||
assert!(should_attempt_legacy_machine_uid_migration(
|
||||
&empty_dir.join("machine_id")
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,26 @@
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
future,
|
||||
io::Write as _,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use time::util::refresh_tz;
|
||||
use tokio::{task::JoinSet, time::timeout};
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::{set_global_var, use_global_var};
|
||||
|
||||
pub mod acl_processor;
|
||||
pub mod compressor;
|
||||
pub mod config;
|
||||
pub mod constants;
|
||||
pub mod dns;
|
||||
pub mod env_parser;
|
||||
pub mod error;
|
||||
pub mod global_ctx;
|
||||
pub mod idn;
|
||||
pub mod ifcfg;
|
||||
pub mod log;
|
||||
pub mod machine_id;
|
||||
pub mod netns;
|
||||
pub mod network;
|
||||
pub mod os_info;
|
||||
@@ -28,8 +31,6 @@ pub mod token_bucket;
|
||||
pub mod tracing_rolling_appender;
|
||||
pub mod upnp;
|
||||
|
||||
pub use machine_id::{MachineIdOptions, resolve_machine_id};
|
||||
|
||||
pub fn get_logger_timer<F: time::formatting::Formattable>(
|
||||
format: F,
|
||||
) -> tracing_subscriber::fmt::time::OffsetTime<F> {
|
||||
@@ -95,6 +96,71 @@ pub fn join_joinset_background<T: Debug + Send + Sync + 'static>(
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_default_machine_id(mid: Option<String>) {
|
||||
set_global_var!(MACHINE_UID, mid);
|
||||
}
|
||||
|
||||
pub fn get_machine_id() -> uuid::Uuid {
|
||||
if let Some(default_mid) = use_global_var!(MACHINE_UID) {
|
||||
if let Ok(mid) = uuid::Uuid::parse_str(default_mid.trim()) {
|
||||
return mid;
|
||||
}
|
||||
let mut b = [0u8; 16];
|
||||
crate::tunnel::generate_digest_from_str("", &default_mid, &mut b);
|
||||
return uuid::Uuid::from_bytes(b);
|
||||
}
|
||||
|
||||
// a path same as the binary
|
||||
let machine_id_file = std::env::current_exe()
|
||||
.map(|x| x.with_file_name("et_machine_id"))
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from("et_machine_id"));
|
||||
|
||||
// try load from local file
|
||||
if let Ok(mid) = std::fs::read_to_string(&machine_id_file)
|
||||
&& let Ok(mid) = uuid::Uuid::parse_str(mid.trim())
|
||||
{
|
||||
return mid;
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
all(target_os = "macos", not(feature = "macos-ne")),
|
||||
target_os = "windows",
|
||||
target_os = "freebsd"
|
||||
))]
|
||||
let gen_mid = machine_uid::get()
|
||||
.map(|x| {
|
||||
if x.is_empty() {
|
||||
return uuid::Uuid::new_v4();
|
||||
}
|
||||
let mut b = [0u8; 16];
|
||||
crate::tunnel::generate_digest_from_str("", x.as_str(), &mut b);
|
||||
uuid::Uuid::from_bytes(b)
|
||||
})
|
||||
.ok();
|
||||
|
||||
#[cfg(not(any(
|
||||
target_os = "linux",
|
||||
all(target_os = "macos", not(feature = "macos-ne")),
|
||||
target_os = "windows",
|
||||
target_os = "freebsd"
|
||||
)))]
|
||||
let gen_mid = None;
|
||||
|
||||
if let Some(mid) = gen_mid {
|
||||
return mid;
|
||||
}
|
||||
|
||||
let gen_mid = uuid::Uuid::new_v4();
|
||||
|
||||
// try save to local file
|
||||
if let Ok(mut file) = std::fs::File::create(machine_id_file) {
|
||||
let _ = file.write_all(gen_mid.to_string().as_bytes());
|
||||
}
|
||||
|
||||
gen_mid
|
||||
}
|
||||
|
||||
pub fn shrink_dashmap<K: Eq + std::hash::Hash, V>(
|
||||
map: &dashmap::DashMap<K, V>,
|
||||
threshold: Option<usize>,
|
||||
@@ -144,4 +210,12 @@ mod tests {
|
||||
assert_eq!(weak_js.weak_count(), 0);
|
||||
assert_eq!(weak_js.strong_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_machine_id_uses_uuid_seed_verbatim() {
|
||||
let raw = "33333333-3333-3333-3333-333333333333".to_string();
|
||||
set_default_machine_id(Some(raw.clone()));
|
||||
assert_eq!(get_machine_id(), uuid::Uuid::parse_str(&raw).unwrap());
|
||||
set_default_machine_id(None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,15 +85,6 @@ pub enum MetricName {
|
||||
/// Traffic packets forwarded for foreign network, forward
|
||||
TrafficPacketsForeignForwardForwarded,
|
||||
|
||||
/// UDP broadcast relay packets captured from the raw socket
|
||||
UdpBroadcastRelayPacketsCaptured,
|
||||
/// UDP broadcast relay packets ignored before forwarding
|
||||
UdpBroadcastRelayPacketsIgnored,
|
||||
/// UDP broadcast relay packets forwarded
|
||||
UdpBroadcastRelayPacketsForwarded,
|
||||
/// UDP broadcast relay packets that failed to forward
|
||||
UdpBroadcastRelayPacketsForwardFailed,
|
||||
|
||||
/// Compression bytes before compression
|
||||
CompressionBytesRxBefore,
|
||||
/// Compression bytes after compression
|
||||
@@ -176,19 +167,6 @@ impl fmt::Display for MetricName {
|
||||
write!(f, "traffic_packets_foreign_forward_forwarded")
|
||||
}
|
||||
|
||||
MetricName::UdpBroadcastRelayPacketsCaptured => {
|
||||
write!(f, "udp_broadcast_relay_packets_captured")
|
||||
}
|
||||
MetricName::UdpBroadcastRelayPacketsIgnored => {
|
||||
write!(f, "udp_broadcast_relay_packets_ignored")
|
||||
}
|
||||
MetricName::UdpBroadcastRelayPacketsForwarded => {
|
||||
write!(f, "udp_broadcast_relay_packets_forwarded")
|
||||
}
|
||||
MetricName::UdpBroadcastRelayPacketsForwardFailed => {
|
||||
write!(f, "udp_broadcast_relay_packets_forward_failed")
|
||||
}
|
||||
|
||||
MetricName::CompressionBytesRxBefore => write!(f, "compression_bytes_rx_before"),
|
||||
MetricName::CompressionBytesRxAfter => write!(f, "compression_bytes_rx_after"),
|
||||
MetricName::CompressionBytesTxBefore => write!(f, "compression_bytes_tx_before"),
|
||||
|
||||
+26
-78
@@ -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;
|
||||
use tokio::net::{UdpSocket, lookup_host};
|
||||
use tokio::sync::{Mutex, broadcast};
|
||||
use tokio::task::JoinSet;
|
||||
use tracing::{Instrument, Level};
|
||||
@@ -20,9 +20,10 @@ use bytecodec::{DecodeExt, EncodeExt};
|
||||
use stun_codec::rfc5389::methods::BINDING;
|
||||
use stun_codec::{Message, MessageClass, MessageDecoder, MessageEncoder};
|
||||
|
||||
use super::stun_codec_ext::*;
|
||||
use crate::common::error::Error;
|
||||
use crate::utils::dns::{resolve_host, txt_resolve};
|
||||
|
||||
use super::dns::resolve_txt_record;
|
||||
use super::stun_codec_ext::*;
|
||||
|
||||
const DEFAULT_UDP_STUN_SERVERS: &[&str] = &[
|
||||
"txt:stun.easytier.cn",
|
||||
@@ -60,16 +61,9 @@ impl HostResolverIter {
|
||||
}
|
||||
}
|
||||
|
||||
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 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())
|
||||
}
|
||||
|
||||
#[async_recursion::async_recursion]
|
||||
@@ -80,10 +74,15 @@ 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 txt_resolve(domain_name).await {
|
||||
match Self::get_txt_record(domain_name).await {
|
||||
Ok(hosts) => {
|
||||
tracing::info!(
|
||||
?domain_name,
|
||||
@@ -105,53 +104,22 @@ impl HostResolverIter {
|
||||
}
|
||||
|
||||
let use_ipv6 = self.use_ipv6;
|
||||
if let Ok(addr) = host.parse::<SocketAddr>() {
|
||||
if (use_ipv6 && addr.is_ipv6()) || (!use_ipv6 && addr.is_ipv4()) {
|
||||
self.ips = vec![addr];
|
||||
}
|
||||
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,
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(?host, ?e, "resolve host for stun failed");
|
||||
if self.ips.is_empty() {
|
||||
return self.next().await;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(?host, ?e, "lookup host for stun failed");
|
||||
return self.next().await;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Some(self.ips.remove(0))
|
||||
@@ -1381,26 +1349,6 @@ 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(
|
||||
@@ -1615,6 +1563,6 @@ mod tests {
|
||||
});
|
||||
let stun_servers = vec!["::1:55355".to_string()];
|
||||
let ret = StunInfoCollector::get_public_ipv6(&stun_servers).await;
|
||||
assert_eq!(ret, Some(Ipv6Addr::LOCALHOST));
|
||||
println!("{:#?}", ret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
common::{PeerId, error::Error, global_ctx::ArcGlobalCtx, stun::StunInfoCollectorTrait},
|
||||
common::{
|
||||
PeerId, dns::socket_addrs, error::Error, global_ctx::ArcGlobalCtx,
|
||||
stun::StunInfoCollectorTrait,
|
||||
},
|
||||
connector::udp_hole_punch::handle_rpc_result,
|
||||
peers::{
|
||||
peer_conn::PeerConnId,
|
||||
@@ -37,7 +40,6 @@ 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,15 +1,19 @@
|
||||
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::{error::Error, global_ctx::ArcGlobalCtx, log},
|
||||
common::{
|
||||
dns::{RESOLVER, resolve_txt_record},
|
||||
error::Error,
|
||||
global_ctx::ArcGlobalCtx,
|
||||
log,
|
||||
},
|
||||
proto::common::TunnelInfo,
|
||||
tunnel::{IpScheme, IpVersion, Tunnel, TunnelConnector, TunnelError, TunnelScheme},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use dashmap::DashSet;
|
||||
use hickory_proto::rr::rdata::SRV;
|
||||
use hickory_resolver::proto::rr::rdata::SRV;
|
||||
use rand::{Rng as _, seq::SliceRandom};
|
||||
use strum::VariantArray;
|
||||
|
||||
@@ -54,13 +58,14 @@ impl DnsTunnelConnector {
|
||||
&self,
|
||||
domain_name: &str,
|
||||
) -> Result<Box<dyn TunnelConnector>, Error> {
|
||||
let txt_data = txt_resolve(domain_name)
|
||||
let txt_data = resolve_txt_record(domain_name)
|
||||
.await
|
||||
.with_context(|| format!("resolve txt record failed, domain_name: {}", domain_name))?;
|
||||
|
||||
let candidate_urls = txt_data
|
||||
.iter()
|
||||
.filter_map(|s| url::Url::parse(s).ok())
|
||||
.split(" ")
|
||||
.map(|s| s.to_string())
|
||||
.filter_map(|s| url::Url::parse(s.as_str()).ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// shuffle candidate_urls and get the first one
|
||||
@@ -68,7 +73,7 @@ impl DnsTunnelConnector {
|
||||
.choose(&mut rand::thread_rng())
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"no valid url found, txt_data: {:?}, expecting an url list split by space",
|
||||
"no valid url found, txt_data: {}, expecting an url list splitted by space",
|
||||
txt_data
|
||||
)
|
||||
})?;
|
||||
@@ -78,23 +83,26 @@ 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 _,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -114,9 +122,14 @@ impl DnsTunnelConnector {
|
||||
let srv_lookup_tasks = srv_domains
|
||||
.iter()
|
||||
.map(|(protocol, srv_domain)| {
|
||||
let resolver = RESOLVER.clone();
|
||||
let responses = responses.clone();
|
||||
async move {
|
||||
for record in srv_lookup(srv_domain).await? {
|
||||
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() {
|
||||
let parsed_record = Self::handle_one_srv_record(record, **protocol);
|
||||
tracing::info!(?parsed_record, ?srv_domain, "parsed_record");
|
||||
if let Err(e) = &parsed_record {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
future::Future,
|
||||
sync::{Arc, Weak},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use dashmap::DashSet;
|
||||
use tokio::{sync::mpsc, task::JoinSet, time::timeout};
|
||||
|
||||
use crate::{
|
||||
common::{PeerId, join_joinset_background},
|
||||
common::{PeerId, dns::socket_addrs, join_joinset_background},
|
||||
peers::peer_conn::PeerConnId,
|
||||
proto::{
|
||||
api::instance::{
|
||||
@@ -18,12 +16,10 @@ use crate::{
|
||||
},
|
||||
rpc_types::{self, controller::BaseController},
|
||||
},
|
||||
tunnel::{IpVersion, TunnelConnector, TunnelScheme, matches_scheme},
|
||||
tunnel::{IpVersion, TunnelConnector},
|
||||
utils::weak_upgrade,
|
||||
};
|
||||
|
||||
use super::create_connector_by_url;
|
||||
use crate::utils::dns::socket_addrs;
|
||||
use crate::{
|
||||
common::{
|
||||
error::Error,
|
||||
@@ -34,6 +30,8 @@ use crate::{
|
||||
use_global_var,
|
||||
};
|
||||
|
||||
use super::create_connector_by_url;
|
||||
|
||||
type ConnectorMap = Arc<DashSet<url::Url>>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -85,55 +83,6 @@ impl ManualConnectorManager {
|
||||
ret
|
||||
}
|
||||
|
||||
fn reconnect_timeout(dead_url: &url::Url) -> Duration {
|
||||
let use_long_timeout = matches_scheme!(
|
||||
dead_url,
|
||||
TunnelScheme::Http | TunnelScheme::Https | TunnelScheme::Txt | TunnelScheme::Srv
|
||||
) || matches!(dead_url.scheme(), "ws" | "wss");
|
||||
|
||||
Duration::from_secs(if use_long_timeout { 20 } else { 2 })
|
||||
}
|
||||
|
||||
fn remaining_budget(started_at: Instant, total_timeout: Duration) -> Option<Duration> {
|
||||
let remaining = total_timeout.checked_sub(started_at.elapsed())?;
|
||||
(!remaining.is_zero()).then_some(remaining)
|
||||
}
|
||||
|
||||
fn emit_connect_error(
|
||||
data: &ConnectorManagerData,
|
||||
dead_url: &url::Url,
|
||||
ip_version: IpVersion,
|
||||
error: &Error,
|
||||
) {
|
||||
data.global_ctx.issue_event(GlobalCtxEvent::ConnectError(
|
||||
dead_url.to_string(),
|
||||
format!("{:?}", ip_version),
|
||||
format!("{:#?}", error),
|
||||
));
|
||||
}
|
||||
|
||||
fn reconnect_timeout_error(stage: &str, duration: Duration) -> Error {
|
||||
Error::AnyhowError(anyhow::anyhow!("{} timeout after {:?}", stage, duration))
|
||||
}
|
||||
|
||||
async fn with_reconnect_timeout<T, F>(
|
||||
stage: &'static str,
|
||||
started_at: Instant,
|
||||
total_timeout: Duration,
|
||||
fut: F,
|
||||
) -> Result<T, Error>
|
||||
where
|
||||
F: Future<Output = Result<T, Error>>,
|
||||
{
|
||||
let remaining = Self::remaining_budget(started_at, total_timeout)
|
||||
.ok_or_else(|| Self::reconnect_timeout_error(stage, started_at.elapsed()))?;
|
||||
timeout(remaining, fut)
|
||||
.await
|
||||
.map_err(|_| Self::reconnect_timeout_error(stage, remaining))?
|
||||
}
|
||||
}
|
||||
|
||||
impl ManualConnectorManager {
|
||||
pub fn add_connector<T>(&self, connector: T)
|
||||
where
|
||||
T: TunnelConnector + 'static,
|
||||
@@ -293,18 +242,11 @@ impl ManualConnectorManager {
|
||||
|
||||
async fn conn_reconnect_with_ip_version(
|
||||
data: Arc<ConnectorManagerData>,
|
||||
dead_url: url::Url,
|
||||
dead_url: String,
|
||||
ip_version: IpVersion,
|
||||
started_at: Instant,
|
||||
total_timeout: Duration,
|
||||
) -> Result<ReconnResult, Error> {
|
||||
let connector = Self::with_reconnect_timeout(
|
||||
"resolve",
|
||||
started_at,
|
||||
total_timeout,
|
||||
create_connector_by_url(dead_url.as_str(), &data.global_ctx, ip_version),
|
||||
)
|
||||
.await?;
|
||||
let connector =
|
||||
create_connector_by_url(&dead_url, &data.global_ctx.clone(), ip_version).await?;
|
||||
|
||||
data.global_ctx
|
||||
.issue_event(GlobalCtxEvent::Connecting(connector.remote_url()));
|
||||
@@ -315,25 +257,10 @@ impl ManualConnectorManager {
|
||||
)));
|
||||
};
|
||||
|
||||
let tunnel = Self::with_reconnect_timeout(
|
||||
"connect",
|
||||
started_at,
|
||||
total_timeout,
|
||||
pm.connect_tunnel(connector),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (peer_id, conn_id) = Self::with_reconnect_timeout(
|
||||
"handshake",
|
||||
started_at,
|
||||
total_timeout,
|
||||
pm.add_client_tunnel_with_peer_id_hint(tunnel, true, None),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (peer_id, conn_id) = pm.try_direct_connect(connector).await?;
|
||||
tracing::info!("reconnect succ: {} {} {}", peer_id, conn_id, dead_url);
|
||||
Ok(ReconnResult {
|
||||
dead_url: dead_url.to_string(),
|
||||
dead_url,
|
||||
peer_id,
|
||||
conn_id,
|
||||
})
|
||||
@@ -346,33 +273,22 @@ impl ManualConnectorManager {
|
||||
tracing::info!("reconnect: {}", dead_url);
|
||||
|
||||
let mut ip_versions = vec![];
|
||||
if matches_scheme!(
|
||||
dead_url,
|
||||
TunnelScheme::Ring | TunnelScheme::Txt | TunnelScheme::Srv
|
||||
) {
|
||||
if dead_url.scheme() == "ring" || dead_url.scheme() == "txt" || dead_url.scheme() == "srv" {
|
||||
ip_versions.push(IpVersion::Both);
|
||||
} else {
|
||||
let converted_dead_url =
|
||||
match crate::common::idn::convert_idn_to_ascii(dead_url.clone()) {
|
||||
Ok(url) => url,
|
||||
Err(error) => {
|
||||
let error: Error = error.into();
|
||||
Self::emit_connect_error(&data, &dead_url, IpVersion::Both, &error);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let addrs = match Self::with_reconnect_timeout(
|
||||
"resolve",
|
||||
Instant::now(),
|
||||
Self::reconnect_timeout(&dead_url),
|
||||
socket_addrs(&converted_dead_url, || Some(1000)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
let converted_dead_url = crate::common::idn::convert_idn_to_ascii(dead_url.clone())?;
|
||||
let addrs = match socket_addrs(&converted_dead_url, || Some(1000)).await {
|
||||
Ok(addrs) => addrs,
|
||||
Err(error) => {
|
||||
Self::emit_connect_error(&data, &dead_url, IpVersion::Both, &error);
|
||||
return Err(error);
|
||||
Err(e) => {
|
||||
data.global_ctx.issue_event(GlobalCtxEvent::ConnectError(
|
||||
dead_url.to_string(),
|
||||
format!("{:?}", IpVersion::Both),
|
||||
format!("{:?}", e),
|
||||
));
|
||||
return Err(Error::AnyhowError(anyhow::anyhow!(
|
||||
"get ip from url failed: {:?}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
};
|
||||
tracing::info!(?addrs, ?dead_url, "get ip from url done");
|
||||
@@ -397,24 +313,46 @@ impl ManualConnectorManager {
|
||||
"cannot get ip from url"
|
||||
)));
|
||||
for ip_version in ip_versions {
|
||||
let started_at = Instant::now();
|
||||
let ret = Self::conn_reconnect_with_ip_version(
|
||||
data.clone(),
|
||||
dead_url.clone(),
|
||||
ip_version,
|
||||
started_at,
|
||||
Self::reconnect_timeout(&dead_url),
|
||||
let use_long_timeout = dead_url.scheme() == "http"
|
||||
|| dead_url.scheme() == "https"
|
||||
|| dead_url.scheme() == "ws"
|
||||
|| dead_url.scheme() == "wss"
|
||||
|| dead_url.scheme() == "txt"
|
||||
|| dead_url.scheme() == "srv";
|
||||
let ret = timeout(
|
||||
// allow http/websocket connector to wait longer
|
||||
std::time::Duration::from_secs(if use_long_timeout { 20 } else { 2 }),
|
||||
Self::conn_reconnect_with_ip_version(
|
||||
data.clone(),
|
||||
dead_url.to_string(),
|
||||
ip_version,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
tracing::info!("reconnect: {} done, ret: {:?}", dead_url, ret);
|
||||
|
||||
match ret {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(error) => {
|
||||
Self::emit_connect_error(&data, &dead_url, ip_version, &error);
|
||||
reconn_ret = Err(error);
|
||||
Ok(Ok(_)) => {
|
||||
// 外层和内层都成功:解包并跳出
|
||||
reconn_ret = ret.unwrap();
|
||||
break;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
// 外层成功,内层失败
|
||||
reconn_ret = Err(e);
|
||||
}
|
||||
Err(e) => {
|
||||
// 外层失败
|
||||
reconn_ret = Err(e.into());
|
||||
}
|
||||
}
|
||||
|
||||
// 发送事件(只有在未 break 时才执行)
|
||||
data.global_ctx.issue_event(GlobalCtxEvent::ConnectError(
|
||||
dead_url.to_string(),
|
||||
format!("{:?}", ip_version),
|
||||
format!("{:?}", reconn_ret),
|
||||
));
|
||||
}
|
||||
|
||||
reconn_ret
|
||||
@@ -450,54 +388,6 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconnect_timeout_reports_exhausted_budget_for_stage() {
|
||||
let started_at = Instant::now() - Duration::from_millis(50);
|
||||
let err = ManualConnectorManager::with_reconnect_timeout(
|
||||
"resolve",
|
||||
started_at,
|
||||
Duration::from_millis(1),
|
||||
async { Ok::<(), Error>(()) },
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
let message = err.to_string();
|
||||
assert!(message.contains("resolve timeout after"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconnect_timeout_reports_stage_timeout_with_remaining_budget() {
|
||||
let err = ManualConnectorManager::with_reconnect_timeout(
|
||||
"handshake",
|
||||
Instant::now(),
|
||||
Duration::from_millis(10),
|
||||
async {
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
Ok::<(), Error>(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
let message = err.to_string();
|
||||
assert!(message.contains("handshake timeout after"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconnect_timeout_preserves_success_within_budget() {
|
||||
let result = ManualConnectorManager::with_reconnect_timeout(
|
||||
"connect",
|
||||
Instant::now(),
|
||||
Duration::from_millis(50),
|
||||
async { Ok::<_, Error>(123_u32) },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result, 123);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reconnect_with_connecting_addr() {
|
||||
set_global_var!(MANUAL_CONNECTOR_RECONNECT_INTERVAL_MS, 1);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use std::net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
|
||||
|
||||
use crate::{
|
||||
common::{error::Error, global_ctx::ArcGlobalCtx, idn},
|
||||
common::{dns::socket_addrs, 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, dns::socket_addrs},
|
||||
utils::BoxExt,
|
||||
};
|
||||
use http_connector::HttpTunnelConnector;
|
||||
use rand::seq::SliceRandom;
|
||||
|
||||
+19
-16
@@ -484,15 +484,6 @@ struct NetworkOptions {
|
||||
)]
|
||||
disable_upnp: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_ENABLE_UDP_BROADCAST_RELAY",
|
||||
help = t!("core_clap.enable_udp_broadcast_relay").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
enable_udp_broadcast_relay: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_RELAY_ALL_PEER_RPC",
|
||||
@@ -578,6 +569,19 @@ 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",
|
||||
@@ -1117,6 +1121,7 @@ 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
|
||||
@@ -1137,9 +1142,10 @@ impl NetworkOptions {
|
||||
.disable_sym_hole_punching
|
||||
.unwrap_or(f.disable_sym_hole_punching);
|
||||
f.disable_upnp = self.disable_upnp.unwrap_or(f.disable_upnp);
|
||||
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() {
|
||||
@@ -1330,10 +1336,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
||||
let _web_client = if let Some(config_server_url_s) = cli.config_server.as_ref() {
|
||||
let wc = web_client::run_web_client(
|
||||
config_server_url_s,
|
||||
crate::common::MachineIdOptions {
|
||||
explicit_machine_id: cli.machine_id.clone(),
|
||||
state_dir: None,
|
||||
},
|
||||
cli.machine_id.clone(),
|
||||
cli.network_options.hostname.clone(),
|
||||
cli.network_options.secure_mode.unwrap_or(false),
|
||||
manager.clone(),
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
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>;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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);
|
||||
@@ -1,46 +0,0 @@
|
||||
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>,
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
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;
|
||||
@@ -1,560 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,561 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,873 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
## 目标
|
||||
|
||||
将 `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>
|
||||
@@ -1,562 +0,0 @@
|
||||
# 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 当前集合。
|
||||
|
||||
该视图不改变数据面行为,但能显著降低线上排障成本。
|
||||
@@ -1,570 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
@@ -1,984 +0,0 @@
|
||||
#![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"
|
||||
);
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod addr;
|
||||
pub mod response;
|
||||
pub mod zone_handler;
|
||||
@@ -1,59 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,572 +0,0 @@
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ use easytier::{
|
||||
common::{NatType, PortForwardConfigPb, SocketType},
|
||||
peer_rpc::{GetGlobalPeerMapRequest, PeerCenterRpc, PeerCenterRpcClientFactory},
|
||||
rpc_impl::standalone::StandAloneClient,
|
||||
rpc_types::{controller::BaseController, error::Error as RpcError},
|
||||
rpc_types::controller::BaseController,
|
||||
},
|
||||
tunnel::{TunnelScheme, tcp::TcpTunnelConnector},
|
||||
utils::{PeerRoutePair, string::cost_to_str},
|
||||
@@ -193,11 +193,8 @@ struct PeerArgs {
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum PeerSubCommand {
|
||||
/// List connected peers
|
||||
List,
|
||||
/// Show public IPv6 address information
|
||||
Ipv6,
|
||||
/// List foreign networks discovered by this instance
|
||||
ListForeign {
|
||||
#[arg(
|
||||
long,
|
||||
@@ -206,7 +203,6 @@ enum PeerSubCommand {
|
||||
)]
|
||||
trusted_keys: bool,
|
||||
},
|
||||
/// List global foreign networks from the peer center
|
||||
ListGlobalForeign,
|
||||
}
|
||||
|
||||
@@ -218,18 +214,16 @@ struct RouteArgs {
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum RouteSubCommand {
|
||||
/// List routes propagated by peers
|
||||
List,
|
||||
/// Dump routes in CIDR format
|
||||
Dump,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
struct ConnectorArgs {
|
||||
#[arg(short, long, help = "filter connectors by virtual IPv4 address")]
|
||||
#[arg(short, long)]
|
||||
ipv4: Option<String>,
|
||||
|
||||
#[arg(short, long, help = "filter connectors by peer URL")]
|
||||
#[arg(short, long)]
|
||||
peers: Vec<String>,
|
||||
|
||||
#[command(subcommand)]
|
||||
@@ -248,7 +242,6 @@ enum ConnectorSubCommand {
|
||||
#[arg(help = "connector url, e.g., tcp://1.2.3.4:11010")]
|
||||
url: String,
|
||||
},
|
||||
/// List connectors
|
||||
List,
|
||||
}
|
||||
|
||||
@@ -290,7 +283,6 @@ struct AclArgs {
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum AclSubCommand {
|
||||
/// Show ACL rule hit statistics
|
||||
Stats,
|
||||
}
|
||||
|
||||
@@ -458,25 +450,19 @@ struct InstallArgs {
|
||||
#[arg(long, default_value = env!("CARGO_PKG_DESCRIPTION"), help = "service description")]
|
||||
description: String,
|
||||
|
||||
#[arg(long, help = "display name shown by the service manager")]
|
||||
#[arg(long)]
|
||||
display_name: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "whether to disable starting the service automatically on boot (true/false)"
|
||||
)]
|
||||
#[arg(long)]
|
||||
disable_autostart: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "whether to disable automatic restart when the service fails (true/false)"
|
||||
)]
|
||||
#[arg(long)]
|
||||
disable_restart_on_failure: Option<bool>,
|
||||
|
||||
#[arg(long, help = "path to easytier-core binary")]
|
||||
core_path: Option<PathBuf>,
|
||||
|
||||
#[arg(long, help = "working directory for the easytier-core service")]
|
||||
#[arg(long)]
|
||||
service_work_dir: Option<PathBuf>,
|
||||
|
||||
#[arg(
|
||||
@@ -540,40 +526,6 @@ type LocalBoxFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, Error>> + 'a>
|
||||
type ForeignNetworkMap = BTreeMap<String, ForeignNetworkEntryPb>;
|
||||
type GlobalForeignNetworkMap = BTreeMap<u32, list_global_foreign_network_response::ForeignNetworks>;
|
||||
|
||||
fn is_missing_web_client_service(error: &RpcError) -> bool {
|
||||
matches!(
|
||||
error,
|
||||
RpcError::InvalidServiceKey(service_name, _)
|
||||
if service_name.trim_matches('"') == "WebClientService"
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn missing_web_client_service_matches_raw_service_name() {
|
||||
let error = RpcError::InvalidServiceKey("WebClientService".to_string(), "".to_string());
|
||||
|
||||
assert!(is_missing_web_client_service(&error));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_web_client_service_matches_serialized_service_name() {
|
||||
let error = RpcError::InvalidServiceKey("\"WebClientService\"".to_string(), "".to_string());
|
||||
|
||||
assert!(is_missing_web_client_service(&error));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_web_client_service_rejects_other_services() {
|
||||
let error = RpcError::InvalidServiceKey("PeerManageRpc".to_string(), "".to_string());
|
||||
|
||||
assert!(!is_missing_web_client_service(&error));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct PeerListData {
|
||||
node_info: NodeInfo,
|
||||
@@ -647,15 +599,9 @@ impl<'a> CommandHandler<'a> {
|
||||
}
|
||||
|
||||
let client = self.get_manage_client().await?;
|
||||
let list_response = match client
|
||||
let inst_ids = client
|
||||
.list_network_instance(BaseController::default(), ListNetworkInstanceRequest {})
|
||||
.await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(error) if is_missing_web_client_service(&error) => return Ok(None),
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
let inst_ids = list_response
|
||||
.await?
|
||||
.inst_ids
|
||||
.into_iter()
|
||||
.map(uuid::Uuid::from)
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context, anyhow, bail};
|
||||
use anyhow::Context;
|
||||
use bytes::Bytes;
|
||||
use dashmap::DashMap;
|
||||
use guarden::defer;
|
||||
@@ -15,13 +15,12 @@ use kcp_sys::{
|
||||
stream::KcpStream,
|
||||
};
|
||||
use prost::Message;
|
||||
use tokio::task::JoinSet;
|
||||
use tokio::{select, task::JoinSet};
|
||||
|
||||
use super::{
|
||||
CidrSet,
|
||||
tcp_proxy::{NatDstConnector, NatDstTcpConnector, TcpProxy},
|
||||
};
|
||||
use crate::utils::task::HedgeExt;
|
||||
use crate::{
|
||||
common::{
|
||||
acl_processor::PacketInfo,
|
||||
@@ -115,57 +114,72 @@ pub struct NatDstKcpConnector {
|
||||
impl NatDstConnector for NatDstKcpConnector {
|
||||
type DstStream = KcpStream;
|
||||
|
||||
async fn connect(
|
||||
&self,
|
||||
src: SocketAddr,
|
||||
nat_dst: SocketAddr,
|
||||
) -> anyhow::Result<Self::DstStream> {
|
||||
let peer_mgr = self
|
||||
.peer_mgr
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("peer manager is not available"))?;
|
||||
|
||||
let dst_peer = {
|
||||
let SocketAddr::V4(addr) = nat_dst else {
|
||||
bail!("ipv6 is not supported");
|
||||
};
|
||||
peer_mgr
|
||||
.get_peer_map()
|
||||
.get_peer_id_by_ipv4(addr.ip())
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("no peer found for nat dst: {}", nat_dst))?
|
||||
};
|
||||
|
||||
tracing::trace!(?nat_dst, ?dst_peer, "kcp nat");
|
||||
|
||||
async fn connect(&self, src: SocketAddr, nat_dst: SocketAddr) -> Result<Self::DstStream> {
|
||||
let conn_data = KcpConnData {
|
||||
src: Some(src.into()),
|
||||
dst: Some(nat_dst.into()),
|
||||
};
|
||||
|
||||
let stream = (0..5)
|
||||
.map(|_| {
|
||||
let kcp_endpoint = self.kcp_endpoint.clone();
|
||||
let my_peer_id = peer_mgr.my_peer_id();
|
||||
let Some(peer_mgr) = self.peer_mgr.upgrade() else {
|
||||
return Err(anyhow::anyhow!("peer manager is not available").into());
|
||||
};
|
||||
|
||||
async move {
|
||||
let conn_id = kcp_endpoint
|
||||
.connect(
|
||||
Duration::from_secs(10),
|
||||
my_peer_id,
|
||||
dst_peer,
|
||||
Bytes::from(conn_data.encode_to_vec()),
|
||||
)
|
||||
.await?;
|
||||
let dst_peer_id = match nat_dst {
|
||||
SocketAddr::V4(addr) => peer_mgr.get_peer_map().get_peer_id_by_ipv4(addr.ip()).await,
|
||||
SocketAddr::V6(_) => return Err(anyhow::anyhow!("ipv6 is not supported").into()),
|
||||
};
|
||||
|
||||
KcpStream::new(&kcp_endpoint, conn_id).context("failed to create kcp stream")
|
||||
let Some(dst_peer) = dst_peer_id else {
|
||||
return Err(anyhow::anyhow!("no peer found for nat dst: {}", nat_dst).into());
|
||||
};
|
||||
|
||||
tracing::trace!("kcp nat dst: {:?}, dst peers: {:?}", nat_dst, dst_peer);
|
||||
|
||||
let mut connect_tasks: JoinSet<std::result::Result<ConnId, anyhow::Error>> = JoinSet::new();
|
||||
let mut retry_remain = 5;
|
||||
loop {
|
||||
select! {
|
||||
Some(Ok(Ok(ret))) = connect_tasks.join_next() => {
|
||||
// just wait for the previous connection to finish
|
||||
let stream = KcpStream::new(&self.kcp_endpoint, ret)
|
||||
.ok_or(anyhow::anyhow!("failed to create kcp stream"))?;
|
||||
return Ok(stream);
|
||||
}
|
||||
})
|
||||
.hedge(Duration::from_millis(200))
|
||||
.await
|
||||
.context("failed to connect to peer")?;
|
||||
_ = tokio::time::sleep(Duration::from_millis(200)), if !connect_tasks.is_empty() && retry_remain > 0 => {
|
||||
// no successful connection yet, trigger another connection attempt
|
||||
}
|
||||
else => {
|
||||
// got error in connect_tasks, continue to retry
|
||||
if retry_remain == 0 && connect_tasks.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(stream)
|
||||
// create a new connection task
|
||||
if retry_remain == 0 {
|
||||
continue;
|
||||
}
|
||||
retry_remain -= 1;
|
||||
|
||||
let kcp_endpoint = self.kcp_endpoint.clone();
|
||||
let my_peer_id = peer_mgr.my_peer_id();
|
||||
let conn_data_clone = conn_data;
|
||||
|
||||
connect_tasks.spawn(async move {
|
||||
kcp_endpoint
|
||||
.connect(
|
||||
Duration::from_secs(10),
|
||||
my_peer_id,
|
||||
dst_peer,
|
||||
Bytes::from(conn_data_clone.encode_to_vec()),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to connect to nat dst: {}", nat_dst))
|
||||
});
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("failed to connect to nat dst: {}", nat_dst).into())
|
||||
}
|
||||
|
||||
fn check_packet_from_peer_fast(&self, _cidr_set: &CidrSet, _global_ctx: &GlobalCtx) -> bool {
|
||||
|
||||
@@ -18,20 +18,17 @@ use crate::tunnel::packet_def::{
|
||||
PacketType, PeerManagerHeader, TAIL_RESERVED_SIZE, ZCPacket, ZCPacketType,
|
||||
};
|
||||
use crate::tunnel::quic::{client_config, endpoint_config, server_config};
|
||||
use crate::utils::task::HedgeExt;
|
||||
use anyhow::{Context, Error, anyhow, bail, ensure};
|
||||
use anyhow::{Context, Error, anyhow};
|
||||
use atomic_refcell::AtomicRefCell;
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use dashmap::DashMap;
|
||||
use derivative::Derivative;
|
||||
use derive_more::{Constructor, Deref, DerefMut, From, Into};
|
||||
use guarden::defer;
|
||||
use moka::future::Cache;
|
||||
use prost::Message;
|
||||
use quinn::udp::{EcnCodepoint, RecvMeta, Transmit};
|
||||
use quinn::{
|
||||
AsyncUdpSocket, Connection, ConnectionError, Endpoint, RecvStream, SendStream, StreamId,
|
||||
UdpPoller, WriteError, default_runtime,
|
||||
AsyncUdpSocket, Endpoint, RecvStream, SendStream, StreamId, UdpPoller, default_runtime,
|
||||
};
|
||||
use std::cmp::min;
|
||||
use std::future::Future;
|
||||
@@ -46,8 +43,8 @@ use tokio::io::{AsyncReadExt, Join, join};
|
||||
use tokio::sync::mpsc::error::TrySendError;
|
||||
use tokio::sync::mpsc::{Receiver, Sender, channel};
|
||||
use tokio::task::JoinSet;
|
||||
use tokio::time::timeout;
|
||||
use tokio::{join, select};
|
||||
use tokio::time::{Instant, timeout};
|
||||
use tokio::{join, pin, select};
|
||||
use tokio_util::sync::PollSender;
|
||||
use tracing::{debug, error, info, instrument, trace, warn};
|
||||
|
||||
@@ -282,7 +279,6 @@ impl From<(SendStream, RecvStream)> for QuicStream {
|
||||
pub struct NatDstQuicConnector {
|
||||
pub(crate) endpoint: Endpoint,
|
||||
pub(crate) peer_mgr: Weak<PeerManager>,
|
||||
pub(crate) conn_map: Cache<PeerId, Connection>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -293,25 +289,21 @@ impl NatDstConnector for NatDstQuicConnector {
|
||||
&self,
|
||||
src: SocketAddr,
|
||||
nat_dst: SocketAddr,
|
||||
) -> anyhow::Result<Self::DstStream> {
|
||||
let peer_mgr = self
|
||||
.peer_mgr
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("peer manager is not available"))?;
|
||||
|
||||
let dst_peer = {
|
||||
let SocketAddr::V4(addr) = nat_dst else {
|
||||
bail!("ipv6 is not supported");
|
||||
};
|
||||
peer_mgr
|
||||
.get_peer_map()
|
||||
.get_peer_id_by_ipv4(addr.ip())
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("no peer found for nat dst: {}", nat_dst))?
|
||||
) -> crate::common::error::Result<Self::DstStream> {
|
||||
let Some(peer_mgr) = self.peer_mgr.upgrade() else {
|
||||
return Err(anyhow::anyhow!("peer manager is not available").into());
|
||||
};
|
||||
|
||||
tracing::trace!(?nat_dst, ?dst_peer, "quic nat");
|
||||
let Some(dst_peer_id) = (match nat_dst {
|
||||
SocketAddr::V4(addr) => peer_mgr.get_peer_map().get_peer_id_by_ipv4(addr.ip()).await,
|
||||
SocketAddr::V6(_) => return Err(anyhow::anyhow!("ipv6 is not supported").into()),
|
||||
}) else {
|
||||
return Err(anyhow::anyhow!("no peer found for nat dst: {}", nat_dst).into());
|
||||
};
|
||||
|
||||
trace!("quic nat dst: {:?}, dst peers: {:?}", nat_dst, dst_peer_id);
|
||||
|
||||
let addr = QuicAddr::new(dst_peer_id, PacketType::QuicSrc).into();
|
||||
let header = {
|
||||
let conn_data = QuicConnData {
|
||||
src: Some(src.into()),
|
||||
@@ -319,90 +311,61 @@ impl NatDstConnector for NatDstQuicConnector {
|
||||
};
|
||||
|
||||
let len = conn_data.encoded_len();
|
||||
ensure!(len <= u16::MAX as usize, "conn data too large: {len}");
|
||||
if len > (u16::MAX as usize) {
|
||||
return Err(anyhow!("conn data too large: {:?}", len).into());
|
||||
}
|
||||
|
||||
let mut buf = BytesMut::with_capacity(2 + len);
|
||||
|
||||
buf.put_u16(len as u16);
|
||||
conn_data.encode(&mut buf)?;
|
||||
conn_data.encode(&mut buf).unwrap();
|
||||
|
||||
buf.freeze()
|
||||
};
|
||||
|
||||
let reconnect = || async move {
|
||||
self.conn_map.invalidate(&dst_peer).await;
|
||||
|
||||
let connect = (0..5)
|
||||
.map(|_| {
|
||||
let endpoint = self.endpoint.clone();
|
||||
async move {
|
||||
endpoint
|
||||
.connect(QuicAddr::new(dst_peer, PacketType::QuicSrc).into(), "")
|
||||
.context("failed to create connection")?
|
||||
.await
|
||||
.context("connection failed")
|
||||
}
|
||||
})
|
||||
.hedge(Duration::from_millis(200));
|
||||
|
||||
self.conn_map
|
||||
.try_get_with(dst_peer, connect)
|
||||
.await
|
||||
.context("failed to connect to peer")
|
||||
};
|
||||
|
||||
let mut reconnected = false;
|
||||
|
||||
let mut connection = if let Some(connection) = self.conn_map.get(&dst_peer).await
|
||||
&& connection.close_reason().is_none()
|
||||
{
|
||||
connection
|
||||
} else {
|
||||
reconnected = true;
|
||||
reconnect().await?
|
||||
};
|
||||
|
||||
loop {
|
||||
let is_retryable = |error: &ConnectionError| {
|
||||
matches!(
|
||||
error,
|
||||
ConnectionError::ConnectionClosed(_)
|
||||
| ConnectionError::ApplicationClosed(_)
|
||||
| ConnectionError::Reset
|
||||
| ConnectionError::TimedOut
|
||||
)
|
||||
};
|
||||
let mut retry = !reconnected;
|
||||
let mut connect_tasks = JoinSet::<Result<QuicStream, Error>>::new();
|
||||
let connect = |tasks: &mut JoinSet<_>| {
|
||||
let endpoint = self.endpoint.clone();
|
||||
let header = header.clone();
|
||||
let result = async {
|
||||
let mut stream: QuicStream = connection
|
||||
.open_bi()
|
||||
.await
|
||||
.inspect_err(|error| retry &= is_retryable(error))?
|
||||
.into();
|
||||
stream
|
||||
.writer_mut()
|
||||
.write_chunk(header)
|
||||
.await
|
||||
.inspect_err(|error| {
|
||||
retry &= matches!(error, WriteError::ConnectionLost(error) if is_retryable(error))
|
||||
})?;
|
||||
Ok(stream.into())
|
||||
}
|
||||
.await;
|
||||
|
||||
if let Err(error) = &result {
|
||||
if retry {
|
||||
debug!(?error, "failed to open quic stream, retrying...");
|
||||
reconnected = true;
|
||||
connection = reconnect().await?;
|
||||
continue;
|
||||
} else {
|
||||
self.conn_map.invalidate(&dst_peer).await;
|
||||
tasks.spawn(async move {
|
||||
let connection = endpoint.connect(addr, "")?.await?;
|
||||
let mut stream: QuicStream = connection.open_bi().await?.into();
|
||||
stream.writer_mut().write_chunk(header).await?;
|
||||
Ok(stream)
|
||||
});
|
||||
};
|
||||
|
||||
connect(&mut connect_tasks);
|
||||
|
||||
let timer = tokio::time::sleep(Duration::from_millis(200));
|
||||
pin!(timer);
|
||||
|
||||
let mut retry_remain = 5;
|
||||
loop {
|
||||
select! {
|
||||
Some(result) = connect_tasks.join_next() => {
|
||||
match result {
|
||||
Ok(Ok(stream)) => return Ok(stream.into()),
|
||||
_ => {
|
||||
if connect_tasks.is_empty() {
|
||||
if retry_remain == 0 {
|
||||
return Err(anyhow!("failed to connect to nat dst: {:?}", nat_dst).into())
|
||||
}
|
||||
|
||||
retry_remain -= 1;
|
||||
connect(&mut connect_tasks);
|
||||
timer.as_mut().reset(Instant::now() + Duration::from_millis(200))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = &mut timer, if retry_remain > 0 => {
|
||||
retry_remain -= 1;
|
||||
connect(&mut connect_tasks);
|
||||
timer.as_mut().reset(Instant::now() + Duration::from_millis(200));
|
||||
}
|
||||
}
|
||||
|
||||
break result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,17 +595,10 @@ impl QuicStreamReceiver {
|
||||
}
|
||||
};
|
||||
|
||||
let ctx = ctx.clone();
|
||||
tasks.spawn(async move {
|
||||
match Self::establish_stream(stream, ctx).await {
|
||||
Ok(transfer_fut) => {
|
||||
if let Err(e) = transfer_fut.await {
|
||||
warn!("quic stream transfer error: {:?}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("failed to establish quic stream: {:?}", e),
|
||||
}
|
||||
});
|
||||
match Self::establish_stream(stream, ctx.clone()).await {
|
||||
Ok(stream) => drop(tasks.spawn(stream)),
|
||||
Err(e) => warn!("failed to establish quic stream from {:?}: {:?}", connection.remote_address(), e),
|
||||
}
|
||||
}
|
||||
|
||||
res = tasks.join_next(), if !tasks.is_empty() => {
|
||||
@@ -860,7 +816,7 @@ impl QuicProxy {
|
||||
Arc::new(socket),
|
||||
default_runtime().unwrap(),
|
||||
)
|
||||
.unwrap(); // TODO: maybe a different transport config
|
||||
.unwrap();
|
||||
endpoint.set_default_client_config(client_config());
|
||||
self.endpoint = Some(endpoint.clone());
|
||||
|
||||
@@ -889,10 +845,6 @@ impl QuicProxy {
|
||||
NatDstQuicConnector {
|
||||
endpoint: endpoint.clone(),
|
||||
peer_mgr: Arc::downgrade(&peer_mgr),
|
||||
conn_map: Cache::builder()
|
||||
.max_capacity(u8::MAX.into()) // cf. quinn transport config (max_concurrent_bidi_streams)
|
||||
.time_to_idle(Duration::from_secs(600)) // cf. quinn transport config (max_idle_timeout)
|
||||
.build(),
|
||||
},
|
||||
));
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ impl AsyncTcpConnector for Socks5KcpConnector {
|
||||
let ret = c
|
||||
.connect(self.src_addr, addr)
|
||||
.await
|
||||
.map_err(super::fast_socks5::SocksError::Other)?;
|
||||
.map_err(|e| super::fast_socks5::SocksError::Other(e.into()))?;
|
||||
Ok(SocksTcpStream::Kcp(ret))
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user