mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-20 03:22:05 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edc0107fc8 |
@@ -33,19 +33,6 @@ runs:
|
||||
sudo apt-get install -qqy build-essential mold musl-tools
|
||||
shell: bash
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
version: '35.1'
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ inputs.token }}
|
||||
|
||||
- name: Verify protoc version
|
||||
run: |
|
||||
version="$(protoc --version | tr -d '\r')"
|
||||
test "$version" = "libprotoc 35.1"
|
||||
shell: bash
|
||||
|
||||
- name: Setup Frontend Environment
|
||||
if: ${{ inputs.pnpm == 'true' }}
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
@@ -95,3 +82,9 @@ runs:
|
||||
ar x libgcc.a _ctzsi2.o _clz.o _bswapsi2.o
|
||||
ar rcs libctz.a _ctzsi2.o _clz.o _bswapsi2.o
|
||||
shell: bash
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ inputs.token }}
|
||||
@@ -41,8 +41,8 @@ runs:
|
||||
pnpm -r install
|
||||
if [ -n "${{ inputs.build-filter }}" ]; then
|
||||
echo "Building with filter: ${{ inputs.build-filter }}"
|
||||
pnpm -r --workspace-concurrency=1 --filter "${{ inputs.build-filter }}" build
|
||||
pnpm -r --filter "${{ inputs.build-filter }}" build
|
||||
else
|
||||
echo "No build filter provided, building all packages"
|
||||
pnpm -r --workspace-concurrency=1 build
|
||||
pnpm -r build
|
||||
fi
|
||||
|
||||
@@ -34,7 +34,6 @@ easytier-panic.log
|
||||
|
||||
# web
|
||||
node_modules
|
||||
easytier-web/frontend-lib/src/generated/
|
||||
|
||||
.vite
|
||||
|
||||
@@ -44,6 +43,3 @@ easytier-gui/src-tauri/*.sys
|
||||
|
||||
.direnv
|
||||
.flake-profile
|
||||
|
||||
# contrib
|
||||
go.sum
|
||||
|
||||
Generated
+88
-363
@@ -241,28 +241,12 @@ dependencies = [
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ariadne"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36f5e3dca4e09a6f340a61a0e9c7b61e030c69fc27bf29d73218f7e5e3b7638f"
|
||||
dependencies = [
|
||||
"unicode-width 0.1.11",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "ascii"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
@@ -931,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",
|
||||
@@ -1254,12 +1238,6 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chunked_transfer"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
|
||||
|
||||
[[package]]
|
||||
name = "cidr"
|
||||
version = "0.3.1"
|
||||
@@ -1429,8 +1407,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"prost 0.13.5",
|
||||
"prost-types 0.13.5",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"tonic",
|
||||
"tracing-core",
|
||||
]
|
||||
@@ -1448,8 +1426,8 @@ dependencies = [
|
||||
"hdrhistogram",
|
||||
"humantime",
|
||||
"hyper-util",
|
||||
"prost 0.13.5",
|
||||
"prost-types 0.13.5",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thread_local",
|
||||
@@ -2100,16 +2078,6 @@ dependencies = [
|
||||
"dirs-sys 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-next"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"dirs-sys-next",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.3.7"
|
||||
@@ -2133,17 +2101,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys-next"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users 0.4.5",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
@@ -2277,7 +2234,6 @@ dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"ariadne",
|
||||
"async-recursion",
|
||||
"async-ringbuf",
|
||||
"async-stream",
|
||||
@@ -2316,13 +2272,12 @@ dependencies = [
|
||||
"gethostname 0.5.0",
|
||||
"git-version",
|
||||
"globwalk",
|
||||
"guarden 0.2.0",
|
||||
"guarden",
|
||||
"hickory-client",
|
||||
"hickory-proto",
|
||||
"hickory-resolver",
|
||||
"hickory-server",
|
||||
"hmac",
|
||||
"hotpath",
|
||||
"http",
|
||||
"http_req",
|
||||
"humansize",
|
||||
@@ -2349,23 +2304,21 @@ dependencies = [
|
||||
"ordered_hash_map",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
"pbjson",
|
||||
"pbjson-build",
|
||||
"percent-encoding",
|
||||
"petgraph",
|
||||
"petgraph 0.8.1",
|
||||
"pin-project-lite",
|
||||
"pnet",
|
||||
"prefix-trie",
|
||||
"proc-macro2",
|
||||
"prost 0.14.3",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-reflect",
|
||||
"prost-reflect-build",
|
||||
"prost-wkt",
|
||||
"prost-wkt-build",
|
||||
"prost-wkt-types",
|
||||
"quanta",
|
||||
"quinn",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"quinn-plaintext",
|
||||
"quote",
|
||||
"rand 0.8.5",
|
||||
"rcgen",
|
||||
@@ -2377,7 +2330,6 @@ dependencies = [
|
||||
"rstest",
|
||||
"rust-i18n",
|
||||
"rustls",
|
||||
"seahash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
@@ -2433,7 +2385,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"easytier",
|
||||
"easytier-ffi",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
@@ -2445,17 +2396,11 @@ dependencies = [
|
||||
name = "easytier-ffi"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dashmap",
|
||||
"easytier",
|
||||
"log",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -2505,7 +2450,7 @@ dependencies = [
|
||||
"dashmap",
|
||||
"easytier",
|
||||
"futures",
|
||||
"guarden 0.1.2",
|
||||
"guarden",
|
||||
"jsonwebtoken",
|
||||
"mimalloc",
|
||||
"mockall",
|
||||
@@ -2666,12 +2611,6 @@ version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "encoding"
|
||||
version = "0.2.33"
|
||||
@@ -2966,6 +2905,12 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
@@ -3647,18 +3592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca87812d87fa82896df1adfb5c111cdeaae3edb6da028f5df002dcbd7df71454"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"guarden-macros 0.1.2",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "guarden"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8408903291a7d0cc74169d5de4dd1919a9a402a2f67fcd7df3303ed045fae73"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"guarden-macros 0.2.0",
|
||||
"guarden-macros",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -3673,18 +3607,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "guarden-macros"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0ef28f1077c259f9e7e238e234a78ce18cedbf0251fd2135f5fc23c40e79fe"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.7"
|
||||
@@ -3930,61 +3852,6 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hotpath"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc2c28b1fa962e433f800ed1ea0bf53dc028d3745cf2acec6cfd28b65ac96afa"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"cfg-if",
|
||||
"crossbeam-channel",
|
||||
"flate2",
|
||||
"flume 0.12.0",
|
||||
"futures-util",
|
||||
"hdrhistogram",
|
||||
"hotpath-macros",
|
||||
"hotpath-meta",
|
||||
"libc",
|
||||
"object",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"prettytable-rs",
|
||||
"quanta",
|
||||
"regex",
|
||||
"rustc-demangle",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tiny_http",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hotpath-macros"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a585238d8daf746e27df0f24d1bbdcd2410e9febff63f9a0173f90d7e71c50f6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hotpath-macros-meta"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "309f63c2f755dead454dd4b3ea8ab5c947f14f8ea435fbcd37fa820e17290e80"
|
||||
|
||||
[[package]]
|
||||
name = "hotpath-meta"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68faa91a9e1114dff668cd90560f332da6bbde40dae37ec28ea1c43ca5ce3be3"
|
||||
dependencies = [
|
||||
"hotpath-macros-meta",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.29.1"
|
||||
@@ -4170,7 +4037,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.3",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -4514,9 +4381,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "inventory"
|
||||
version = "0.3.24"
|
||||
version = "0.3.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b"
|
||||
checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
@@ -4592,17 +4459,6 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-wsl"
|
||||
version = "0.4.0"
|
||||
@@ -5721,7 +5577,7 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@@ -5953,15 +5809,6 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -6307,28 +6154,6 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pbjson"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8edd1efdd8ab23ba9cb9ace3d9987a72663d5d7c9f74fa00b51d6213645cf6c"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pbjson-build"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ed4d5c6ae95e08ac768883c8401cf0e8deb4e6e1d6a4e1fd3d2ec4f0ec63200"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"itertools 0.14.0",
|
||||
"prost 0.14.3",
|
||||
"prost-types 0.14.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
@@ -6364,13 +6189,23 @@ version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
|
||||
dependencies = [
|
||||
"fixedbitset 0.4.2",
|
||||
"indexmap 2.14.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a98c6720655620a521dcc722d0ad66cd8afd5d86e34a89ef691c50b7b24de06"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"fixedbitset 0.5.7",
|
||||
"hashbrown 0.15.3",
|
||||
"indexmap 2.14.0",
|
||||
"serde",
|
||||
@@ -6832,19 +6667,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettytable-rs"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"is-terminal",
|
||||
"lazy_static",
|
||||
"term",
|
||||
"unicode-width 0.1.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "primeorder"
|
||||
version = "0.13.6"
|
||||
@@ -6875,11 +6697,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]]
|
||||
@@ -6963,33 +6785,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive 0.13.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive 0.14.3",
|
||||
"prost-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-build"
|
||||
version = "0.14.3"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7"
|
||||
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"multimap",
|
||||
"petgraph",
|
||||
"once_cell",
|
||||
"petgraph 0.6.5",
|
||||
"prettyplease",
|
||||
"prost 0.14.3",
|
||||
"prost-types 0.14.3",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"regex",
|
||||
"syn 2.0.117",
|
||||
"tempfile",
|
||||
@@ -7008,38 +6821,23 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-derive"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-reflect"
|
||||
version = "0.16.4"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "590aa145fee8f7a26b5a6055365e7c5e89a5c1caae9869de76ec0ee73181a2f9"
|
||||
checksum = "e92b959d24e05a3e2da1d0beb55b48bc8a97059b8336ea617780bd6addbbfb5a"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"prost 0.14.3",
|
||||
"once_cell",
|
||||
"prost",
|
||||
"prost-reflect-derive",
|
||||
"prost-types 0.14.3",
|
||||
"serde",
|
||||
"serde-value",
|
||||
"prost-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-reflect-build"
|
||||
version = "0.16.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8214ae2c30bbac390db0134d08300e770ef89b6d4e5abf855e8d300eded87e28"
|
||||
checksum = "50e2537231d94dd2778920c2ada37dd9eb1ac0325bb3ee3ee651bd44c1134123"
|
||||
dependencies = [
|
||||
"prost-build",
|
||||
"prost-reflect",
|
||||
@@ -7047,9 +6845,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost-reflect-derive"
|
||||
version = "0.16.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b6d90e29fa6c0d13c2c19ba5e4b3fb0efbf5975d27bcf4e260b7b15455bcabe"
|
||||
checksum = "f4fce6b22f15cc8d8d400a2b98ad29202b33bd56c7d9ddd815bc803a807ecb65"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -7062,27 +6860,18 @@ version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
|
||||
dependencies = [
|
||||
"prost 0.13.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7"
|
||||
dependencies = [
|
||||
"prost 0.14.3",
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-wkt"
|
||||
version = "0.7.1"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd3de5e9c9e84fcb5efa204b8e283d23e615a8bc8c777bf1d6622bb01dc61445"
|
||||
checksum = "497e1e938f0c09ef9cabe1d49437b4016e03e8f82fbbe5d1c62a9b61b9decae1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"inventory",
|
||||
"prost 0.14.3",
|
||||
"prost",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -7091,27 +6880,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost-wkt-build"
|
||||
version = "0.7.1"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe500dc80e757a75e1e8fb7290e448d62dfba3105ece1d058579cb00b58151cd"
|
||||
checksum = "07b8bf115b70a7aa5af1fd5d6e9418492e9ccb6e4785e858c938e28d132a884b"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"prost 0.14.3",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-types 0.14.3",
|
||||
"prost-types",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-wkt-types"
|
||||
version = "0.7.1"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13807eaa7e15833d06e899008371926201cdcd11d74b6d490f49130cdb3f415e"
|
||||
checksum = "c8cdde6df0a98311c839392ca2f2f0bcecd545f86a62b4e3c6a49c336e970fe5"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"prost 0.14.3",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-types 0.14.3",
|
||||
"prost-types",
|
||||
"prost-wkt",
|
||||
"prost-wkt-build",
|
||||
"regex",
|
||||
@@ -7146,21 +6935,6 @@ version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
|
||||
|
||||
[[package]]
|
||||
name = "quanta"
|
||||
version = "0.12.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"raw-cpuid",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"web-sys",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
@@ -7205,6 +6979,18 @@ dependencies = [
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-plaintext"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3e617feaeb6493018fa35fc47ae8b630ac8903d8159e9e747018841b99bad3d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"quinn-proto",
|
||||
"seahash",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.12"
|
||||
@@ -7414,15 +7200,6 @@ dependencies = [
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "11.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
@@ -7788,7 +7565,7 @@ checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"glob",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
@@ -7904,12 +7681,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.0"
|
||||
@@ -9834,17 +9605,6 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "term"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
|
||||
dependencies = [
|
||||
"dirs-next",
|
||||
"rustversion",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.4.1"
|
||||
@@ -10000,18 +9760,6 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "825f6c8a18bc36d56a62f66af7296385b628c9c5543a8663d4c217fc920bfefd"
|
||||
|
||||
[[package]]
|
||||
name = "tiny_http"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"
|
||||
dependencies = [
|
||||
"ascii",
|
||||
"chunked_transfer",
|
||||
"httpdate",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.7.6"
|
||||
@@ -10141,7 +9889,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tokio-websockets"
|
||||
version = "0.13.2"
|
||||
source = "git+https://github.com/EasyTier/tokio-websockets#dc9771c7c215882349c3cb328877550a3593df21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dad543404f98bfc969aeb71994105c592acfc6c43323fddcd016bb208d1c65cb"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -10216,15 +9965,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"
|
||||
@@ -10262,18 +10002,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"
|
||||
@@ -10309,7 +10037,7 @@ dependencies = [
|
||||
"hyper-util",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"prost 0.13.5",
|
||||
"prost",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
@@ -10593,7 +10321,7 @@ checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"nom 8.0.0",
|
||||
"petgraph",
|
||||
"petgraph 0.8.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10653,9 +10381,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "typetag"
|
||||
version = "0.2.22"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5a897b12c6c1151ad0b138b8db50252dc301f93bc3b027db05eec82aeed298c"
|
||||
checksum = "be2212c8a9b9bcfca32024de14998494cf9a5dfa59ea1b829de98bac374b86bf"
|
||||
dependencies = [
|
||||
"erased-serde",
|
||||
"inventory",
|
||||
@@ -10666,9 +10394,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typetag-impl"
|
||||
version = "0.2.22"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf808357c6ed7e13ba0f3277ec8d8f21b2d501274895104263985330c726c1c5"
|
||||
checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -12121,9 +11849,6 @@ name = "winnow"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
@@ -12505,7 +12230,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",
|
||||
@@ -12742,7 +12467,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",
|
||||
|
||||
@@ -27,9 +27,3 @@ lto = true
|
||||
codegen-units = 1
|
||||
opt-level = 3
|
||||
strip = true
|
||||
|
||||
# For hotpath CPU profiling: samply needs debug symbols and unstripped binaries.
|
||||
[profile.hotpath]
|
||||
inherits = "release"
|
||||
strip = false
|
||||
debug = "line-tables-only"
|
||||
|
||||
@@ -1,426 +0,0 @@
|
||||
# hotpath CPU 热点分析与发包链路优化
|
||||
|
||||
## 概述
|
||||
|
||||
本文档记录了使用 hotpath + samply 对 easytier-core 发包链路进行 CPU 热点分析的全过程,包括工具链搭建、热点定位、优化实施、踩坑记录和最终 benchmark 结果。
|
||||
|
||||
## 最终 benchmark 数据
|
||||
|
||||
### 真实性能对比(不带 hotpath,3 runs average)
|
||||
|
||||
origin/main baseline 使用 `git worktree` 从 origin/main 构建,仅添加 bench example +
|
||||
loopback bind fix(TCP/UDP convergence 需要)。无任何优化代码。
|
||||
|
||||
| Tunnel | origin/main baseline | 优化后 | **提升** | 带宽(优化后) |
|
||||
| -------- | -------------------- | -------------- | --------- | -------------- |
|
||||
| **Ring** | 293K pps / 3.3 Gbps | **1,124K pps** | **+284%** | 12.6 Gbps |
|
||||
| **TCP** | 298K pps / 3.3 Gbps | **975K pps** | **+227%** | 10.9 Gbps |
|
||||
| **UDP** | 630K pps / 7.1 Gbps | **1,066K pps** | **+69%** | 11.9 Gbps |
|
||||
|
||||
UDP baseline 本身较高(630K vs 293K/298K),因为 UDP tunnel 的 forward_from_ring_to_udp
|
||||
独立 task 提供了天然的 pipeline overlap,部分隐藏了 channel 开销。
|
||||
|
||||
### 带 hotpath profiling(timing 可见,但有 observer effect)
|
||||
|
||||
| Tunnel | 原始 pps | 优化后 pps | 提升 | MpscTunnelSender::send |
|
||||
| ------ | -------- | ---------- | ----- | ---------------------- |
|
||||
| Ring | 234K | 478K | +104% | 138ns (原 2.23µs) |
|
||||
| UDP | N/A | 440K | — | 294ns |
|
||||
| TCP | N/A | 453K | — | 378ns |
|
||||
|
||||
### hotpath observer effect
|
||||
|
||||
**hotpath 测量基础设施引入了 ~54-57% 的性能开销:**
|
||||
|
||||
| Tunnel | 不带 hotpath | 带 hotpath | hotpath 开销 |
|
||||
| ------ | ------------ | ---------- | ------------ |
|
||||
| Ring | 1,124K pps | 478K pps | **-57%** |
|
||||
| TCP | 975K pps | 453K pps | **-54%** |
|
||||
|
||||
**含义:**
|
||||
|
||||
- timing 数据里的 `send_msg_by_ip: 2.15µs` 是膨胀值,真实成本 ~0.9µs
|
||||
- 所有 timing 数据需要按 ~2.3x 校准才能反映真实开销
|
||||
- hotpath 适用于相对比较(优化前 vs 后),不适用于绝对性能评估
|
||||
- 生产环境部署不应用 hotpath feature 编译
|
||||
|
||||
测试条件:4 threads, 1400B packets, 10s, 宿主机直跑。
|
||||
|
||||
---
|
||||
|
||||
## 工具链搭建
|
||||
|
||||
### hotpath + samply 安装
|
||||
|
||||
```bash
|
||||
# hotpath TUI (console)
|
||||
cargo install hotpath --version 0.18.0 --bin hotpath --features tui
|
||||
|
||||
# hotpath-samply (samply wrapper,autospawn 依赖)
|
||||
cargo install hotpath --version 0.18.0 --bin hotpath-samply
|
||||
|
||||
# samply 本体(hotpath-samply 内部 spawn samply record)
|
||||
cargo install samply
|
||||
```
|
||||
|
||||
### 内核参数
|
||||
|
||||
samply 需要 perf_event 开销,需要调整内核参数:
|
||||
|
||||
```bash
|
||||
echo '1' | sudo tee /proc/sys/kernel/perf_event_paranoid
|
||||
echo '65536' | sudo tee /proc/sys/kernel/perf_event_mlock_kb
|
||||
```
|
||||
|
||||
- `perf_event_paranoid` 默认 2(不允许非 root 采样),需降到 1。
|
||||
- `perf_event_mlock_kb` 默认 516 KB,32 核机器上 samply 的 mmap buffer 总量超限,需增大到 65536。
|
||||
|
||||
### hotpath profile 编译
|
||||
|
||||
```toml
|
||||
# Cargo.toml
|
||||
[profile.hotpath]
|
||||
inherits = "release"
|
||||
strip = false
|
||||
debug = "line-tables-only"
|
||||
```
|
||||
|
||||
samply 需要 debug symbols 且不能 strip。release profile 默认 `strip = true`,必须用单独的 profile。
|
||||
|
||||
### Docker 隔离环境(可选,TCP/UDP bench)
|
||||
|
||||
修复 loopback bind 地址后(见坑 11),TCP/UDP bench 可以直接在宿主机上跑,不需要 Docker:
|
||||
|
||||
```bash
|
||||
# Ring(进程内,无需隔离)
|
||||
HOTPATH_TUNNEL=ring ./target/hotpath/examples/cpu_hotspot_ring
|
||||
|
||||
# TCP/UDP(修复后也支持宿主机直跑)
|
||||
HOTPATH_TUNNEL=tcp ./target/hotpath/examples/cpu_hotspot_ring
|
||||
```
|
||||
|
||||
如果仍有 convergence 问题(多网卡环境),用 Docker 提供独立 netns:
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
-v "$(pwd)/target/hotpath/examples/cpu_hotspot_ring:/bench:ro" \
|
||||
-e HOTPATH_TUNNEL=tcp \
|
||||
-e HOTPATH_BENCH_SECS=10 \
|
||||
fedora:latest \
|
||||
/bench
|
||||
```
|
||||
|
||||
Docker 镜像需要匹配宿主机的 glibc 版本。Fedora 宿主用 `fedora:latest`。
|
||||
|
||||
---
|
||||
|
||||
## 踩坑记录
|
||||
|
||||
### 坑 1:samply 报 "failed to spawn samply: No such file or directory"
|
||||
|
||||
**现象**:hotpath CPU report 显示 `failed to spawn samply: No such file or directory (os error 2)`
|
||||
|
||||
**原因**:hotpath-samply 只是 wrapper,它内部 spawn `samply record --pid <pid>` 来采集 CPU 样本。samply 本体没装。
|
||||
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
cargo install samply
|
||||
```
|
||||
|
||||
如果 autospawn 找不到 hotpath-samply 本身,用环境变量指定完整路径:
|
||||
|
||||
```bash
|
||||
export HOTPATH_SAMPLY_WRAPPER_BIN=~/.cargo/bin/hotpath-samply
|
||||
```
|
||||
|
||||
### 坑 2:samply 报 "Failed to start profiling: mmap failed"
|
||||
|
||||
**现象**:samply 启动后立即报 mmap 失败。
|
||||
|
||||
**原因**:`perf_event_mlock_kb` 默认只有 516 KB。32 核机器上 samply 为每个 CPU core 创建 mmap buffer,总 mmap 量超过限制。
|
||||
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
echo '65536' | sudo tee /proc/sys/kernel/perf_event_mlock_kb
|
||||
```
|
||||
|
||||
### 坑 3:samply 报 "samply exited with status exit status: 1"
|
||||
|
||||
**现象**:samply 被 spawn 了但 exit 1。
|
||||
|
||||
**原因**:同坑 2——`perf_event_paranoid = 2` 时非 root 用户无法使用 perf_event_open。
|
||||
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
echo '1' | sudo tee /proc/sys/kernel/perf_event_paranoid
|
||||
```
|
||||
|
||||
### 坑 4:火焰图全是地址,看不到符号
|
||||
|
||||
**现象**:samply profile 打开后火焰图全是 `0x31dd24` 之类的地址。
|
||||
|
||||
**原因**:samply profile 里存储的是地址(不内联符号化)。符号化在查看时通过 symbol server 动态完成。如果直接下载 raw JSON 上传到 profiler.firefox.com,符号 server 无法访问本地二进制文件。
|
||||
|
||||
**解决**:必须用 `samply load` 本地打开(它启动 symbol server 自动做符号化):
|
||||
|
||||
```bash
|
||||
samply load /tmp/hotpath/<session>/hp.json.gz
|
||||
```
|
||||
|
||||
不要下载 JSON 再上传到 profiler.firefox.com。
|
||||
|
||||
### 坑 5:samply 符号化后 `_dl_mcount_wrapper` 占 18.1%
|
||||
|
||||
**现象**:send_msg_internal inclusive 分析显示 `_dl_mcount_wrapper` 占 18.1% CPU。
|
||||
|
||||
**原因**:nm 的动态符号表里 `_dl_mcount_wrapper`(0x1498d0)到下一个符号(0x1b3e9e)之间有 **425 KB gap**。nm 的 bisect 查找把 gap 内所有地址错误归因到 `_dl_mcount_wrapper`。gap 里实际是 AVX2 优化的 memmove/memcmp/memset 等函数。
|
||||
|
||||
**解决**:用 addr2line 精确解析(而非 nm bisect)。实际开销是 memmove 1.67% + memcmp 0.20% + memset 0.19% = 2.1%,不是 18%。**没有 profiling 钩子**。
|
||||
|
||||
### 坑 6:parking_lot::MutexGuard 不是 Send
|
||||
|
||||
**现象**:使用 `parking_lot::Mutex` 替代 `tokio::sync::Mutex` 后,编译报 31 个 "future cannot be sent between threads safely"。
|
||||
|
||||
**原因**:`parking_lot::MutexGuard` 刻意不实现 `Send`——锁必须在获取它的同一个线程上释放。在 async fn 里 guard 跨 await 点会导致 Future 不是 Send,tokio multi_thread runtime 拒绝 spawn。
|
||||
|
||||
**解决**:自定义 `SpinSink`(AtomicBool spinlock),`SpinGuard` 只持有 `&SpinSink` 引用(SpinSink: Sync via unsafe impl),是 Send。
|
||||
|
||||
### 坑 7:std::sync::MutexGuard 也不是 Send(在某些配置下)
|
||||
|
||||
**现象**:`std::sync::Mutex` 同样报 "future cannot be sent between threads safely"。
|
||||
|
||||
**原因**:Rust 标准库的 `MutexGuard` 的 Send 实现依赖于内部类型。`Pin<Box<dyn ZCPacketSink>>` 包含 trait object,某些配置下 guard 不是 Send。
|
||||
|
||||
**解决**:用自定义 SpinSink 绕过所有标准 Mutex 实现。
|
||||
|
||||
### 坑 8:direct sink path 没有性能提升
|
||||
|
||||
**现象**:去掉 channel 中转(MpscTunnelSender 直接持有 sink),从 3 个 await 点(lock + feed + flush)改为 try_lock + poll_fn 合并。MpscTunnelSender::send 仍然 ~2µs。
|
||||
|
||||
**原因**:瓶颈不在 lock 或 channel,而在 **async fn Future 状态机的固有开销**。每次 `.await` 创建一个 Future struct、poll 它、drop 它。即使 poll 立即返回 Ready,整个 async machinery 开销 ~2µs。RingSink 实际操作只有 ~40ns(2%)。
|
||||
|
||||
**解决**:用 `noop_waker()` 在 async fn 内部同步调用 Sink trait 方法(poll_ready + start_send + poll_flush)。async fn 在第一次 poll 就同步完成返回——绕过所有 async 调度开销。开销从 2µs 降到 ~140ns。
|
||||
|
||||
### 坑 9:sync send 破坏了 TCP/UDP tunnel
|
||||
|
||||
**现象**:把 `send` 从 `async fn` 改为 sync `fn` 后,所有 TCP/UDP 相关测试失败(452 个失败)。
|
||||
|
||||
**原因**:TCP/UDP tunnel 用 channel mode(`MpscTunnel::new`)。sync `send` 的 channel path 只做 `try_send`,channel 满时返回 `BufferFull`(丢包),而不是 `send().await`(等待背压)。丢包导致 TCP/UDP 连接握手失败。
|
||||
|
||||
**解决**:保持 `send` 为 async fn。direct path(ring/UDP/TCP)内部用 noop_waker 同步完成(不 yield)。channel path 仍然走 async `send_async().await`。async fn wrapper 对 direct path 只有 ~100ns 开销(Future struct 创建 + 单次 poll),因为不 yield。
|
||||
|
||||
### 坑 10:poll_flush Pending 返回 Shutdown 导致连接断开
|
||||
|
||||
**现象**:noop_waker 模式下,TCP tunnel 的 `poll_flush` 可能返回 Pending(TCP 写缓冲区满)。返回 `Err(Shutdown)` 导致 PeerConn 认为连接断开。
|
||||
|
||||
**原因**:TCP 的 `FramedWriter::poll_flush` 做实际 socket write(系统调用)。socket 缓冲区满时返回 Pending。数据已经在 BufList 里,不需要 panic。
|
||||
|
||||
**解决**:poll_flush Pending 时返回 `Ok(())`。数据已在 buffer(ring buffer 或 BufList),后续操作会消费它。Pending 只意味着 "还没 flush 到网络",不是 "错误"。
|
||||
|
||||
### 坑 11:TCP/UDP bench convergence 失败
|
||||
|
||||
**现象**:TCP/UDP tunnel 的 bench 中,两个实例无法建立连接(routes did not converge within 15s)。
|
||||
|
||||
**原因**:`set_bind_addr_for_peer_connector`(connector/mod.rs:70-77)收集所有本机 IP 作为 TCP bind 地址,但不包含 `127.0.0.1`。connector 绑定到 `172.17.0.2`(Docker eth0)后连接 `127.0.0.1` 路由不通 → 2 秒超时。
|
||||
|
||||
**解决**:在 bind 地址列表头部加入 `127.0.0.1:0`。connector 遍历所有 bind 地址,loopback 先被尝试,localhost 连接成功。
|
||||
|
||||
### 坑 12:ShardedCounter (#2385) 在高频路径引入回退
|
||||
|
||||
**现象**:cherry-pick PR #2385(ShardedCounter 替代 UnsafeCell)后,pps 下降 17%(246K → 203K)。
|
||||
|
||||
**原因**:ShardedCounter 的 TLS 分片设计优化多线程 contention,但每包调用 16 次 `ShardedCounter::add`(TLS load + store),单次 ~14ns,总 224ns/包。比原来的 `UnsafeCell`(~2ns/次)高 6 倍。每包 16 次的调用频率让 TLS 开销累积。
|
||||
|
||||
**教训**:TLS 分片策略适合 **低频高并发** 场景,不适合 **高频单线程** 的发包热路径。
|
||||
|
||||
### 坑 13:ZCPacket pool 不如 glibc tcache
|
||||
|
||||
**现象**:用 `crossbeam_queue::ArrayQueue` 做 BytesMut 对象池,每包从池取/归还。性能没有提升(甚至 -15%)。
|
||||
|
||||
**原因**:glibc malloc 对 ~1500 bytes 小块分配有 thread-local cache(tcache),单次 alloc ~10-15ns。ArrayQueue 的 pop/push 是 CAS 操作(~20-40ns),比 tcache 更慢。pool 还多了 capacity 检查和 clear 操作。
|
||||
|
||||
**教训**:手动对象池在现代 glibc tcache 面前没有优势。真正需要 pool 的场景是避免 munmap(大块 >128KB 分配),不是小块。
|
||||
|
||||
### 坑 14:Pipeline (FuturesUnordered) 效果微小
|
||||
|
||||
**现象**:用 FuturesUnordered 让多个 send_msg_by_ip 并发(pipeline_depth=4),pps 只提升 1.6%。
|
||||
|
||||
**原因**:try_send fast path 让 MpscTunnelSender::send 立即返回(不 await)。多个 send_msg_by_ip 之间没有自然的时间重叠——它们在 CPU 上是串行的。pipeline 需要利用 await 等待时间,但 fast path 消除了 await。
|
||||
|
||||
### 坑 15:hotpath 测量引入 54% observer effect
|
||||
|
||||
**现象**:同一 binary 带 hotpath feature 和不带 hotpath feature 跑 bench,pps 差距巨大。
|
||||
|
||||
**数据**:
|
||||
|
||||
| Tunnel | 不带 hotpath | 带 hotpath | hotpath 开销 |
|
||||
| ------ | ------------ | ---------- | ------------ |
|
||||
| Ring | 1,124K pps | 478K pps | **-57%** |
|
||||
| TCP | 975K pps | 453K pps | **-54%** |
|
||||
|
||||
**原因**:hotpath `#[measure]` / `#[measure_all]` 在每个标注的 async fn 上包装 Future struct,每次 poll 记录开始/结束时间(quanta::Instant ~5ns × 2)、更新统计(atomic 操作)。measure_all 覆盖的 impl 块内所有方法都被插桩。当有 ~30 个 measure 点在发包热路径上时,累计开销超过 50%。
|
||||
|
||||
**教训**:
|
||||
|
||||
- hotpath timing 数据**适用于相对比较**(优化前 vs 后),**不适用于绝对性能评估**
|
||||
- 生产环境**不应**用 hotpath feature 编译
|
||||
- 要获取真实 pps,编译不带 `--features hotpath` 的版本
|
||||
- timing 数据按 ~2.3x 校准可近似真实开销
|
||||
|
||||
---
|
||||
|
||||
## 优化实施记录
|
||||
|
||||
### 真实提升(不带 hotpath,origin/main baseline 对比)
|
||||
|
||||
baseline 构建:`git worktree` 从 origin/main,仅添加 bench example + loopback bind fix。
|
||||
|
||||
| Tunnel | baseline | 优化后 | 提升 |
|
||||
| ------ | -------- | -------------- | --------- |
|
||||
| Ring | 293K pps | **1,124K pps** | **+284%** |
|
||||
| TCP | 298K pps | **975K pps** | **+227%** |
|
||||
| UDP | 630K pps | **1,066K pps** | **+69%** |
|
||||
|
||||
### 有效优化(按贡献排序)
|
||||
|
||||
| 优化 | 带 hotpath pps 变化 | 真实提升来源 | 机制 |
|
||||
| ------------------------ | ------------------- | ------------------------ | ---------------------------------------------------------- |
|
||||
| **noop_waker sync send** | **+90%** | **核心突破** | RingSink/FramedWriter 直接 sync poll,绕过 async machinery |
|
||||
| try_send fast path | +7% | 次要 | 跳过 tokio mpsc semaphore |
|
||||
| #2385 ZCPacket safe init | +5% (TCP) | TCP 专属 | copy_nonoverlapping 无 aliasing 检查 |
|
||||
| metrics batch + sync | +1.6% | 小幅 | batch CounterHandle + sync fast path |
|
||||
| #2381 advance (零拷贝) | ~0% | 代码质量 | Buf::advance 消除 split_off Arc churn |
|
||||
| channel 32→1024 | ~0% | 减少 fallback | 更大 buffer |
|
||||
| 接收侧 try_recv | ~0% (单向) | 双向有价值 | 消除 recv().await async overhead |
|
||||
| loopback bind fix | — | TCP/UDP convergence 必需 | 127.0.0.1 加入 bind 地址列表 |
|
||||
|
||||
### 验证无效并回退
|
||||
|
||||
| 尝试 | 结果 | 原因 |
|
||||
| ---------------------------------- | -------- | ----------------------------------- |
|
||||
| ShardedCounter (#2385) | -17% pps | TLS 分片高频开销 > UnsafeCell |
|
||||
| ZCPacket pool | -15% pps | glibc tcache 比 ArrayQueue CAS 更快 |
|
||||
| Allocator 切换 (jemalloc/mimalloc) | ~0% | 小块分配 tcache 都已足够 |
|
||||
| Pipeline (FuturesUnordered) | +1.6% | try_send 消除了 await 空隙 |
|
||||
| dashmap 合并 | ~0% | contains_key 本身 ~50ns |
|
||||
|
||||
### noop_waker 技术详解
|
||||
|
||||
核心原理:async fn `send()` 内部用 `noop_waker()` 构造 dummy Context,直接调 Sink trait 的 `poll_ready` + `start_send` + `poll_flush`。RingSink 在 ring buffer 不满时所有操作立即返回 Ready——noop_waker 永远不会被触发。
|
||||
|
||||
```rust
|
||||
pub async fn send(&self, item: ZCPacket) -> Result<(), TunnelError> {
|
||||
if let Some(sink) = &self.direct_sink {
|
||||
if let Some(mut guard) = sink.try_lock() {
|
||||
let waker = futures::task::noop_waker();
|
||||
let mut cx = std::task::Context::from_waker(&waker);
|
||||
match guard.as_mut().poll_ready(&mut cx) {
|
||||
Poll::Ready(Ok(())) => {
|
||||
guard.as_mut().start_send(item)?;
|
||||
match guard.as_mut().poll_flush(&mut cx) {
|
||||
Poll::Ready(Err(e)) => return Err(e),
|
||||
_ => return Ok(()), // Ready(Ok) 或 Pending 都返回 Ok
|
||||
}
|
||||
}
|
||||
// ...
|
||||
}
|
||||
}
|
||||
return Err(TunnelError::BufferFull);
|
||||
}
|
||||
// Channel mode: async with backpressure
|
||||
self.send_async(item).await
|
||||
}
|
||||
```
|
||||
|
||||
**为什么 Pending 返回 Ok**:poll_flush Pending 意味着数据已在 buffer(ring buffer 或 BufList)但还没 flush 到网络。forward task 或下一次 send 会消费它。这是安全的——数据不丢、不乱序。
|
||||
|
||||
**适用范围**:所有 Sink 的 `start_send` 是同步内存操作的 tunnel:
|
||||
|
||||
- Ring tunnel: RingSink → ring buffer(内存)
|
||||
- UDP tunnel: RingSink → ring buffer → forward_from_ring_to_udp task → socket
|
||||
- TCP tunnel: FramedWriter → BufList(内存)→ poll_flush 时 write socket
|
||||
|
||||
---
|
||||
|
||||
## hotpath measure 布点
|
||||
|
||||
### 当前覆盖
|
||||
|
||||
```
|
||||
send_msg_by_ip ✅ measure
|
||||
├─ try_compress_and_encrypt ✅ measure
|
||||
├─ get_msg_dst_peer_ipv4 ✅ measure
|
||||
├─ run_nic_packet_process_pipeline ✅ measure
|
||||
├─ send_msg_internal ✅ measure
|
||||
│ ├─ PeerMap::send_msg_directly ✅ measure_all
|
||||
│ ├─ PeerMap::get_peer_by_id ✅ measure_all
|
||||
│ ├─ PeerMap::get_gateway_peer_id ✅ measure_all
|
||||
│ ├─ PeerMap::has_peer ✅ measure_all
|
||||
│ ├─ record_tx_fast ❌ (sync fn, 无 measure)
|
||||
│ └─ Peer::send_msg ✅ measure
|
||||
│ └─ PeerConn::send_msg ✅ measure
|
||||
│ └─ MpscTunnelSender::send ✅ measure
|
||||
├─ MpscTunnel::forward_one_round ✅ measure
|
||||
│ ├─ RingSink::poll_ready ✅ measure_all
|
||||
│ ├─ RingSink::start_send ✅ measure_all
|
||||
│ └─ RingSink::poll_flush ✅ measure_all
|
||||
└─ CidrSet::* ✅ measure_all
|
||||
```
|
||||
|
||||
### 布点排除项(避免与已有 PR 冲突)
|
||||
|
||||
| 文件 | 排除原因 |
|
||||
| ---------------------- | -------------------- |
|
||||
| stats_manager.rs | PR #2385 重写中 |
|
||||
| traffic_metrics.rs | 依赖 stats_manager |
|
||||
| peer_manager.rs (部分) | advisor/001-002 改动 |
|
||||
| peer_conn.rs (部分) | advisor/001-002 改动 |
|
||||
| tunnel/mpsc.rs (部分) | perf/001 改动 |
|
||||
| packet_def.rs | perf/001-003 改动 |
|
||||
| peer_ospf_route.rs | advisor/003-004 改动 |
|
||||
|
||||
---
|
||||
|
||||
## 运行方式
|
||||
|
||||
### Ring tunnel bench
|
||||
|
||||
```bash
|
||||
cargo build --profile hotpath --features hotpath --example cpu_hotspot_ring
|
||||
HOTPATH_BENCH_SECS=15 ./target/hotpath/examples/cpu_hotspot_ring
|
||||
```
|
||||
|
||||
### TCP/UDP bench(需要 Docker 隔离)
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
-v "$(pwd)/target/hotpath/examples/cpu_hotspot_ring:/bench:ro" \
|
||||
-e HOTPATH_TUNNEL=tcp \
|
||||
-e HOTPATH_BENCH_SECS=10 \
|
||||
fedora:latest \
|
||||
/bench
|
||||
```
|
||||
|
||||
### 带 samply CPU profiling
|
||||
|
||||
```bash
|
||||
export PATH=$HOME/.cargo/bin:$PATH
|
||||
cargo run --profile hotpath --features hotpath,hotpath-cpu --example cpu_hotspot_ring
|
||||
|
||||
# 另一终端查看 CPU top
|
||||
hotpath console
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
| ---------------------------- | ---- | ----------------------- |
|
||||
| `HOTPATH_BENCH_SECS` | 30 | 打流持续秒数 |
|
||||
| `HOTPATH_PKT_SIZE` | 1400 | 包大小 |
|
||||
| `HOTPATH_TUNNEL` | ring | ring / udp / tcp |
|
||||
| `HOTPATH_PIPELINE` | 1 | pipeline 深度 |
|
||||
| `HOTPATH_SAMPLY_WRAPPER_BIN` | — | hotpath-samply 完整路径 |
|
||||
| `HOTPATH_SAMPLY_BIN` | — | samply 本体完整路径 |
|
||||
@@ -13,5 +13,4 @@ log = "0.4"
|
||||
android_logger = "0.13"
|
||||
serde = { version = "1.0.220", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
easytier = { path = "../../easytier" }
|
||||
easytier-ffi = { path = "../easytier-ffi", default-features = false, features = ["ffi-dataplane"] }
|
||||
easytier = { path = "../../easytier" }
|
||||
@@ -8,7 +8,6 @@
|
||||
- 📱 原生 Android JNI 支持
|
||||
- 🔧 支持多种 Android 架构 (arm64-v8a, armeabi-v7a, x86, x86_64)
|
||||
- 🛡️ 类型安全的 Java 接口
|
||||
- 🔌 支持通过 JSON 调用已暴露的 EasyTier RPC 查询/管理接口
|
||||
- 📝 详细的错误处理和日志记录
|
||||
|
||||
## 支持的架构
|
||||
@@ -177,20 +176,6 @@ public class EasyTierManager {
|
||||
}
|
||||
```
|
||||
|
||||
### 通用 JSON RPC
|
||||
|
||||
`EasyTierJNI.callJsonRpc(serviceName, methodName, domainName, payloadJson)` 可以调用已暴露的
|
||||
EasyTier RPC 服务,payload 和返回值均为 protobuf JSON。该接口不支持
|
||||
`api.manage.WebClientService`;实例启动、保留、删除、信息收集仍使用专用 JNI API。
|
||||
|
||||
```java
|
||||
String response = EasyTierJNI.callJsonRpc(
|
||||
"api.logger.LoggerRpcService",
|
||||
"get_logger_config",
|
||||
"{}"
|
||||
);
|
||||
```
|
||||
|
||||
### VPN 服务集成
|
||||
|
||||
如果您要在 Android VPN 服务中使用:
|
||||
@@ -279,4 +264,4 @@ public class EasyTierVpnService extends VpnService {
|
||||
|
||||
- [EasyTier 主项目](https://github.com/EasyTier/EasyTier)
|
||||
- [Android NDK 文档](https://developer.android.com/ndk)
|
||||
- [Rust JNI 文档](https://docs.rs/jni/)
|
||||
- [Rust JNI 文档](https://docs.rs/jni/)
|
||||
@@ -1,17 +0,0 @@
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
fn main() {
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
if !matches!(target_os.as_str(), "android" | "linux") {
|
||||
return;
|
||||
}
|
||||
|
||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let exports = manifest_dir.join("exports.map");
|
||||
println!("cargo:rerun-if-changed={}", exports.display());
|
||||
println!(
|
||||
"cargo:rustc-cdylib-link-arg=-Wl,--version-script={}",
|
||||
exports.display()
|
||||
);
|
||||
println!("cargo:rustc-cdylib-link-arg=-Wl,--exclude-libs,ALL");
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
global:
|
||||
Java_com_easytier_jni_EasyTierJNI_*;
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_*;
|
||||
local:
|
||||
*;
|
||||
};
|
||||
-451
@@ -1,451 +0,0 @@
|
||||
package com.easytier.jni
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* EasyTier data-plane API for Android.
|
||||
*
|
||||
* Dataplane APIs do not create or start an EasyTier instance by themselves.
|
||||
* Start an instance with [EasyTierJNI.runNetworkInstance] first, then pass the
|
||||
* same `instanceName` to [EasyTierDataPlane.tcpConnect],
|
||||
* [EasyTierDataPlane.tcpBind], or [EasyTierDataPlane.udpBind]. If that instance
|
||||
* is not running, the native start call fails and the coroutine wrapper throws
|
||||
* the last EasyTier FFI error.
|
||||
*
|
||||
* Typical setup:
|
||||
* ```
|
||||
* val instanceName = "android-dataplane-demo"
|
||||
* val config = """
|
||||
* instance_name = "$instanceName"
|
||||
* ipv4 = "10.144.0.1"
|
||||
* listeners = ["tcp://0.0.0.0:11010"]
|
||||
*
|
||||
* [network_identity]
|
||||
* network_name = "android-dataplane-demo"
|
||||
* network_secret = "replace-with-a-real-secret"
|
||||
*
|
||||
* [[peer]]
|
||||
* uri = "tcp://peer.example.com:11010"
|
||||
*
|
||||
* [flags]
|
||||
* no_tun = true
|
||||
* bind_device = false
|
||||
* """.trimIndent()
|
||||
*
|
||||
* EasyTierJNI.runNetworkInstance(config)
|
||||
* ```
|
||||
*
|
||||
* After the instance is running, most callers should use [EasyTierDataPlane]
|
||||
* and the socket/stream classes below. [EasyTierDataPlaneJNI] is the low-level
|
||||
* native op-handle ABI used by the coroutine wrappers.
|
||||
*
|
||||
* TCP client usage:
|
||||
* ```
|
||||
* val stream = EasyTierDataPlane.tcpConnect(instanceName, "10.144.0.2", 8080, 5_000)
|
||||
* try {
|
||||
* stream.write("ping".toByteArray(), 5_000)
|
||||
* val reply = stream.read(4096, 5_000)
|
||||
* } finally {
|
||||
* stream.close()
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* TCP server usage:
|
||||
* ```
|
||||
* val listener = EasyTierDataPlane.tcpBind(instanceName, 8080, 5_000)
|
||||
* try {
|
||||
* val stream = listener.accept(30_000)
|
||||
* try {
|
||||
* stream.write(stream.read(4096, 5_000), 5_000)
|
||||
* } finally {
|
||||
* stream.close()
|
||||
* }
|
||||
* } finally {
|
||||
* listener.close()
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* UDP usage:
|
||||
* ```
|
||||
* val socket = EasyTierDataPlane.udpBind(instanceName, 0, 5_000)
|
||||
* try {
|
||||
* socket.sendTo("10.144.0.2", 9000, "ping".toByteArray(), 5_000)
|
||||
* val packet = socket.recvFrom(4096, 5_000)
|
||||
* } finally {
|
||||
* socket.close()
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Operation model:
|
||||
* - Each suspend function starts one native async op, waits on Dispatchers.IO,
|
||||
* then consumes the op with the matching finish call.
|
||||
* - Coroutine cancellation cancels and frees the native op.
|
||||
* - Returned stream/listener/socket handles must be closed by the caller.
|
||||
* - Input ByteArray data is copied by the native start call; output data is
|
||||
* copied into Kotlin ByteArray before the native buffer is freed.
|
||||
*/
|
||||
|
||||
/** Data-plane IPv4/port pair returned by EasyTier FFI. */
|
||||
data class DataPlaneSocketAddress(val ip: String, val port: Int)
|
||||
|
||||
/** Result of a completed TCP connect op. */
|
||||
data class DataPlaneTcpConnectResult(val handle: Long, val localAddress: DataPlaneSocketAddress)
|
||||
|
||||
/** Result of a completed TCP bind op. */
|
||||
data class DataPlaneTcpBindResult(val handle: Long, val localAddress: DataPlaneSocketAddress)
|
||||
|
||||
/** Result of a completed TCP accept op. */
|
||||
data class DataPlaneTcpAcceptResult(
|
||||
val handle: Long,
|
||||
val localAddress: DataPlaneSocketAddress,
|
||||
val peerAddress: DataPlaneSocketAddress
|
||||
)
|
||||
|
||||
/** Result of a completed TCP read op. */
|
||||
data class DataPlaneTcpReadResult(val data: ByteArray)
|
||||
|
||||
/** Result of a completed UDP bind op. */
|
||||
data class DataPlaneUdpBindResult(val handle: Long, val localAddress: DataPlaneSocketAddress)
|
||||
|
||||
/** Result of a completed UDP recv_from op. */
|
||||
data class DataPlaneUdpRecvResult(
|
||||
val data: ByteArray,
|
||||
val peerAddress: DataPlaneSocketAddress
|
||||
)
|
||||
|
||||
/** TCP data-plane stream handle. Call [close] when the stream is no longer needed. */
|
||||
class DataPlaneTcpStream(
|
||||
val handle: Long,
|
||||
val localAddress: DataPlaneSocketAddress? = null,
|
||||
val peerAddress: DataPlaneSocketAddress? = null
|
||||
) {
|
||||
/** Read up to [maxLength] bytes, waiting at most [timeoutMs] in native code. */
|
||||
suspend fun read(maxLength: Int, timeoutMs: Long): ByteArray =
|
||||
EasyTierDataPlane.tcpRead(this, maxLength, timeoutMs)
|
||||
|
||||
/** Write [data], waiting at most [timeoutMs] in native code. */
|
||||
suspend fun write(data: ByteArray, timeoutMs: Long): Int =
|
||||
EasyTierDataPlane.tcpWrite(this, data, timeoutMs)
|
||||
|
||||
/** Close the native TCP stream handle. */
|
||||
fun close(): Int = EasyTierDataPlaneJNI.dataPlaneTcpClose(handle)
|
||||
}
|
||||
|
||||
/** TCP data-plane listener handle. Call [close] when the listener is no longer needed. */
|
||||
class DataPlaneTcpListener(val handle: Long, val localAddress: DataPlaneSocketAddress) {
|
||||
/** Accept one TCP data-plane stream. */
|
||||
suspend fun accept(timeoutMs: Long): DataPlaneTcpStream =
|
||||
EasyTierDataPlane.tcpAccept(this, timeoutMs)
|
||||
|
||||
/** Close the native TCP listener handle. */
|
||||
fun close(): Int = EasyTierDataPlaneJNI.dataPlaneTcpListenerClose(handle)
|
||||
}
|
||||
|
||||
/** UDP data-plane socket handle. Call [close] when the socket is no longer needed. */
|
||||
class DataPlaneUdpSocket(val handle: Long, val localAddress: DataPlaneSocketAddress) {
|
||||
/** Send one UDP datagram to [dstIp]:[dstPort]. */
|
||||
suspend fun sendTo(
|
||||
dstIp: String,
|
||||
dstPort: Int,
|
||||
data: ByteArray,
|
||||
timeoutMs: Long
|
||||
): Int = EasyTierDataPlane.udpSendTo(this, dstIp, dstPort, data, timeoutMs)
|
||||
|
||||
/** Receive one UDP datagram and its peer address. */
|
||||
suspend fun recvFrom(maxLength: Int, timeoutMs: Long): DataPlaneUdpRecvResult =
|
||||
EasyTierDataPlane.udpRecvFrom(this, maxLength, timeoutMs)
|
||||
|
||||
/** Close the native UDP socket handle. */
|
||||
fun close(): Int = EasyTierDataPlaneJNI.dataPlaneUdpClose(handle)
|
||||
}
|
||||
|
||||
/**
|
||||
* Low-level native data-plane JNI entry points.
|
||||
*
|
||||
* These functions mirror the Rust FFI op-handle ABI directly. They are exposed
|
||||
* for completeness, but most Android callers should use [EasyTierDataPlane]
|
||||
* instead so coroutine cancellation and op cleanup are handled consistently.
|
||||
*/
|
||||
object EasyTierDataPlaneJNI {
|
||||
init {
|
||||
System.loadLibrary("easytier_android_jni")
|
||||
}
|
||||
|
||||
@JvmStatic external fun dataPlaneAsyncOpStatus(handle: Long): Int
|
||||
|
||||
@JvmStatic external fun dataPlaneAsyncOpWait(handle: Long, timeoutMs: Long): Int
|
||||
|
||||
@JvmStatic external fun dataPlaneAsyncOpCancel(handle: Long): Int
|
||||
|
||||
@JvmStatic external fun dataPlaneAsyncOpFree(handle: Long): Int
|
||||
|
||||
@JvmStatic
|
||||
external fun dataPlaneTcpConnectStart(
|
||||
instanceName: String,
|
||||
dstIp: String,
|
||||
dstPort: Int,
|
||||
timeoutMs: Long
|
||||
): Long
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpConnectFinish(op: Long): DataPlaneTcpConnectResult?
|
||||
|
||||
@JvmStatic
|
||||
external fun dataPlaneTcpBindStart(
|
||||
instanceName: String,
|
||||
localPort: Int,
|
||||
timeoutMs: Long
|
||||
): Long
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpBindFinish(op: Long): DataPlaneTcpBindResult?
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpAcceptStart(handle: Long, timeoutMs: Long): Long
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpAcceptFinish(op: Long): DataPlaneTcpAcceptResult?
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpReadStart(handle: Long, maxLength: Int, timeoutMs: Long): Long
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpReadFinish(op: Long): DataPlaneTcpReadResult?
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpWriteStart(handle: Long, data: ByteArray, timeoutMs: Long): Long
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpWriteFinish(op: Long): Int
|
||||
|
||||
@JvmStatic
|
||||
external fun dataPlaneUdpBindStart(
|
||||
instanceName: String,
|
||||
localPort: Int,
|
||||
timeoutMs: Long
|
||||
): Long
|
||||
|
||||
@JvmStatic external fun dataPlaneUdpBindFinish(op: Long): DataPlaneUdpBindResult?
|
||||
|
||||
@JvmStatic
|
||||
external fun dataPlaneUdpSendToStart(
|
||||
handle: Long,
|
||||
dstIp: String,
|
||||
dstPort: Int,
|
||||
data: ByteArray,
|
||||
timeoutMs: Long
|
||||
): Long
|
||||
|
||||
@JvmStatic external fun dataPlaneUdpSendToFinish(op: Long): Int
|
||||
|
||||
@JvmStatic external fun dataPlaneUdpRecvFromStart(handle: Long, maxLength: Int, timeoutMs: Long): Long
|
||||
|
||||
@JvmStatic external fun dataPlaneUdpRecvFromFinish(op: Long): DataPlaneUdpRecvResult?
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpClose(handle: Long): Int
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpListenerClose(handle: Long): Int
|
||||
|
||||
@JvmStatic external fun dataPlaneUdpClose(handle: Long): Int
|
||||
}
|
||||
|
||||
/** Coroutine-friendly Android data-plane API. */
|
||||
object EasyTierDataPlane {
|
||||
private const val DATA_PLANE_OP_PENDING = 0
|
||||
private const val DATA_PLANE_OP_READY = 1
|
||||
private const val DATA_PLANE_OP_FAILED = -1
|
||||
private const val DATA_PLANE_OP_INVALID = -2
|
||||
private const val DATA_PLANE_WAIT_SLICE_MS = 50L
|
||||
|
||||
/** Connect to a TCP endpoint through the named EasyTier instance. */
|
||||
@JvmStatic
|
||||
suspend fun tcpConnect(
|
||||
instanceName: String,
|
||||
dstIp: String,
|
||||
dstPort: Int,
|
||||
timeoutMs: Long
|
||||
): DataPlaneTcpStream {
|
||||
val op =
|
||||
requireOp(
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpConnectStart(
|
||||
instanceName,
|
||||
dstIp,
|
||||
dstPort,
|
||||
timeoutMs
|
||||
)
|
||||
)
|
||||
val result = awaitOp(op) {
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpConnectFinish(it) ?: throw lastDataPlaneException()
|
||||
}
|
||||
return DataPlaneTcpStream(result.handle, result.localAddress)
|
||||
}
|
||||
|
||||
/** Bind a TCP data-plane listener on [localPort]. Port 0 asks EasyTier to allocate one. */
|
||||
@JvmStatic
|
||||
suspend fun tcpBind(
|
||||
instanceName: String,
|
||||
localPort: Int,
|
||||
timeoutMs: Long
|
||||
): DataPlaneTcpListener {
|
||||
val op =
|
||||
requireOp(
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpBindStart(
|
||||
instanceName,
|
||||
localPort,
|
||||
timeoutMs
|
||||
)
|
||||
)
|
||||
val result = awaitOp(op) {
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpBindFinish(it) ?: throw lastDataPlaneException()
|
||||
}
|
||||
return DataPlaneTcpListener(result.handle, result.localAddress)
|
||||
}
|
||||
|
||||
/** Accept one TCP stream from [listener]. */
|
||||
@JvmStatic
|
||||
suspend fun tcpAccept(listener: DataPlaneTcpListener, timeoutMs: Long): DataPlaneTcpStream {
|
||||
val op =
|
||||
requireOp(
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpAcceptStart(listener.handle, timeoutMs)
|
||||
)
|
||||
val result = awaitOp(op) {
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpAcceptFinish(it) ?: throw lastDataPlaneException()
|
||||
}
|
||||
return DataPlaneTcpStream(result.handle, result.localAddress, result.peerAddress)
|
||||
}
|
||||
|
||||
/** Read up to [maxLength] bytes from [stream]. */
|
||||
@JvmStatic
|
||||
suspend fun tcpRead(
|
||||
stream: DataPlaneTcpStream,
|
||||
maxLength: Int,
|
||||
timeoutMs: Long
|
||||
): ByteArray {
|
||||
val op =
|
||||
requireOp(
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpReadStart(
|
||||
stream.handle,
|
||||
maxLength,
|
||||
timeoutMs
|
||||
)
|
||||
)
|
||||
return awaitOp(op) {
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpReadFinish(it)?.data
|
||||
?: throw lastDataPlaneException()
|
||||
}
|
||||
}
|
||||
|
||||
/** Write [data] to [stream]. */
|
||||
@JvmStatic
|
||||
suspend fun tcpWrite(stream: DataPlaneTcpStream, data: ByteArray, timeoutMs: Long): Int {
|
||||
val op =
|
||||
requireOp(
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpWriteStart(
|
||||
stream.handle,
|
||||
data,
|
||||
timeoutMs
|
||||
)
|
||||
)
|
||||
return awaitOp(op) { EasyTierDataPlaneJNI.dataPlaneTcpWriteFinish(it) }
|
||||
}
|
||||
|
||||
/** Bind a UDP data-plane socket on [localPort]. Port 0 asks EasyTier to allocate one. */
|
||||
@JvmStatic
|
||||
suspend fun udpBind(
|
||||
instanceName: String,
|
||||
localPort: Int,
|
||||
timeoutMs: Long
|
||||
): DataPlaneUdpSocket {
|
||||
val op =
|
||||
requireOp(
|
||||
EasyTierDataPlaneJNI.dataPlaneUdpBindStart(
|
||||
instanceName,
|
||||
localPort,
|
||||
timeoutMs
|
||||
)
|
||||
)
|
||||
val result = awaitOp(op) {
|
||||
EasyTierDataPlaneJNI.dataPlaneUdpBindFinish(it) ?: throw lastDataPlaneException()
|
||||
}
|
||||
return DataPlaneUdpSocket(result.handle, result.localAddress)
|
||||
}
|
||||
|
||||
/** Send one UDP datagram through [socket]. */
|
||||
@JvmStatic
|
||||
suspend fun udpSendTo(
|
||||
socket: DataPlaneUdpSocket,
|
||||
dstIp: String,
|
||||
dstPort: Int,
|
||||
data: ByteArray,
|
||||
timeoutMs: Long
|
||||
): Int {
|
||||
val op =
|
||||
requireOp(
|
||||
EasyTierDataPlaneJNI.dataPlaneUdpSendToStart(
|
||||
socket.handle,
|
||||
dstIp,
|
||||
dstPort,
|
||||
data,
|
||||
timeoutMs
|
||||
)
|
||||
)
|
||||
return awaitOp(op) { EasyTierDataPlaneJNI.dataPlaneUdpSendToFinish(it) }
|
||||
}
|
||||
|
||||
/** Receive one UDP datagram through [socket]. */
|
||||
@JvmStatic
|
||||
suspend fun udpRecvFrom(
|
||||
socket: DataPlaneUdpSocket,
|
||||
maxLength: Int,
|
||||
timeoutMs: Long
|
||||
): DataPlaneUdpRecvResult {
|
||||
val op =
|
||||
requireOp(
|
||||
EasyTierDataPlaneJNI.dataPlaneUdpRecvFromStart(
|
||||
socket.handle,
|
||||
maxLength,
|
||||
timeoutMs
|
||||
)
|
||||
)
|
||||
return awaitOp(op) {
|
||||
EasyTierDataPlaneJNI.dataPlaneUdpRecvFromFinish(it) ?: throw lastDataPlaneException()
|
||||
}
|
||||
}
|
||||
|
||||
private fun requireOp(op: Long): Long {
|
||||
if (op == 0L) {
|
||||
throw lastDataPlaneException()
|
||||
}
|
||||
return op
|
||||
}
|
||||
|
||||
private suspend fun <T> awaitOp(op: Long, finish: (Long) -> T): T =
|
||||
withContext(Dispatchers.IO) {
|
||||
var consumed = false
|
||||
try {
|
||||
awaitReady(op)
|
||||
val result = finish(op)
|
||||
consumed = true
|
||||
result
|
||||
} catch (e: CancellationException) {
|
||||
EasyTierDataPlaneJNI.dataPlaneAsyncOpCancel(op)
|
||||
throw e
|
||||
} finally {
|
||||
if (!consumed) {
|
||||
EasyTierDataPlaneJNI.dataPlaneAsyncOpFree(op)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun awaitReady(op: Long) {
|
||||
while (true) {
|
||||
currentCoroutineContext().ensureActive()
|
||||
when (EasyTierDataPlaneJNI.dataPlaneAsyncOpWait(op, DATA_PLANE_WAIT_SLICE_MS)) {
|
||||
DATA_PLANE_OP_READY, DATA_PLANE_OP_FAILED -> return
|
||||
DATA_PLANE_OP_PENDING -> Unit
|
||||
DATA_PLANE_OP_INVALID -> throw RuntimeException("Data-plane async operation is invalid")
|
||||
else -> throw RuntimeException("Unknown data-plane async operation status")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun lastDataPlaneException(): RuntimeException {
|
||||
return RuntimeException(EasyTierJNI.getLastError() ?: "EasyTier data-plane call failed")
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
package com.easytier.jni
|
||||
|
||||
fun interface ConfigServerEventCallback {
|
||||
fun onEvent(eventJson: String)
|
||||
}
|
||||
|
||||
/** EasyTier JNI 接口类 提供 Android 应用调用 EasyTier 核心网络功能的接口 */
|
||||
/** EasyTier JNI 接口类 提供 Android 应用调用 EasyTier 网络功能的接口 */
|
||||
object EasyTierJNI {
|
||||
|
||||
init {
|
||||
// 加载本地库
|
||||
System.loadLibrary("easytier_android_jni")
|
||||
@@ -36,35 +33,6 @@ object EasyTierJNI {
|
||||
*/
|
||||
@JvmStatic external fun runNetworkInstance(config: String): Int
|
||||
|
||||
/**
|
||||
* 启动配置服务器客户端
|
||||
* @param url 配置服务器 URL
|
||||
* @param hostname 主机名,传入 null 使用系统主机名
|
||||
* @param machineId 稳定机器 ID,由调用方负责持久化
|
||||
* @param secureMode 是否启用 secure mode
|
||||
* @param callback 远程配置应用/删除事件回调
|
||||
* @return 0 表示成功,-1 表示失败
|
||||
* @throws RuntimeException 当客户端启动失败时抛出异常
|
||||
*/
|
||||
@JvmStatic
|
||||
external fun startConfigServerClient(
|
||||
url: String,
|
||||
hostname: String?,
|
||||
machineId: String,
|
||||
secureMode: Boolean,
|
||||
callback: ConfigServerEventCallback?
|
||||
): Int
|
||||
|
||||
/**
|
||||
* 停止配置服务器客户端
|
||||
* @return 0 表示成功,-1 表示失败
|
||||
* @throws RuntimeException 当客户端停止失败时抛出异常
|
||||
*/
|
||||
@JvmStatic external fun stopConfigServerClient(): Int
|
||||
|
||||
/** 查询配置服务器客户端是否已连接 */
|
||||
@JvmStatic external fun isConfigServerClientConnected(): Boolean
|
||||
|
||||
/**
|
||||
* 保留指定的网络实例,停止其他实例
|
||||
* @param instanceNames 要保留的实例名称数组,传入 null 或空数组将停止所有实例
|
||||
@@ -76,48 +44,11 @@ object EasyTierJNI {
|
||||
/**
|
||||
* 收集网络信息
|
||||
* @param maxLength 最大返回条目数
|
||||
* @return 包含网络信息的 JSON 字符串
|
||||
* @return 包含网络信息的字符串数组,每个元素格式为 "key=value"
|
||||
* @throws RuntimeException 当操作失败时抛出异常
|
||||
*/
|
||||
@JvmStatic external fun collectNetworkInfos(maxLength: Int): String?
|
||||
|
||||
/**
|
||||
* 列出当前运行的实例名称和实例 ID。
|
||||
* @param maxLength 最大返回条目数
|
||||
* @return JSON 对象,key 为 instance name,value 为 instance id
|
||||
* @throws RuntimeException 当操作失败时抛出异常
|
||||
*/
|
||||
@JvmStatic external fun listInstances(maxLength: Int): String?
|
||||
|
||||
/**
|
||||
* 调用暴露的 EasyTier RPC 方法,输入和输出均为 protobuf JSON 字符串。
|
||||
*
|
||||
* 不支持 api.manage.WebClientService;实例启动、保留、删除、信息收集请继续使用专用 JNI API。
|
||||
* payloadJson 需要包含目标 RPC 所需的 instance selector。
|
||||
*
|
||||
* @param serviceName RPC 服务名,例如 api.instance.PeerManageRpcService
|
||||
* @param methodName RPC 方法名,支持 snake_case 或 proto 方法名
|
||||
* @param domainName 仅 TcpProxyRpcService 使用;传 null 或空字符串默认 tcp
|
||||
* @param payloadJson protobuf JSON 请求体
|
||||
* @return protobuf JSON 响应体
|
||||
* @throws RuntimeException 当 RPC 调用失败时抛出异常
|
||||
*/
|
||||
@JvmStatic
|
||||
external fun callJsonRpc(
|
||||
serviceName: String,
|
||||
methodName: String,
|
||||
domainName: String?,
|
||||
payloadJson: String
|
||||
): String?
|
||||
|
||||
/**
|
||||
* 调用不需要 domainName 的 EasyTier RPC 方法。
|
||||
*/
|
||||
@JvmStatic
|
||||
fun callJsonRpc(serviceName: String, methodName: String, payloadJson: String): String? {
|
||||
return callJsonRpc(serviceName, methodName, null, payloadJson)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后的错误消息
|
||||
* @return 错误消息字符串,如果没有错误则返回 null
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
use std::{
|
||||
ffi::{CStr, c_char, c_void},
|
||||
sync::{Arc, Mutex, MutexGuard},
|
||||
};
|
||||
|
||||
use easytier_ffi::ConfigServerEventCallback;
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{GlobalRef, JObject, JValue};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::error;
|
||||
|
||||
pub(crate) struct JniConfigServerCallback {
|
||||
java_vm: jni::JavaVM,
|
||||
callback: GlobalRef,
|
||||
}
|
||||
|
||||
static CONFIG_SERVER_CALLBACK: Lazy<Mutex<Option<Arc<JniConfigServerCallback>>>> =
|
||||
Lazy::new(|| Mutex::new(None));
|
||||
|
||||
pub(crate) fn lock_callback_storage()
|
||||
-> Result<MutexGuard<'static, Option<Arc<JniConfigServerCallback>>>, String> {
|
||||
CONFIG_SERVER_CALLBACK
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock config server callback: {}", e))
|
||||
}
|
||||
|
||||
pub(crate) fn new_callback(
|
||||
env: &mut JNIEnv,
|
||||
callback: &JObject,
|
||||
) -> Result<Arc<JniConfigServerCallback>, String> {
|
||||
let java_vm = env
|
||||
.get_java_vm()
|
||||
.map_err(|e| format!("Failed to get JavaVM: {:?}", e))?;
|
||||
let callback = env
|
||||
.new_global_ref(callback)
|
||||
.map_err(|e| format!("Failed to create callback global ref: {:?}", e))?;
|
||||
Ok(Arc::new(JniConfigServerCallback { java_vm, callback }))
|
||||
}
|
||||
|
||||
pub(crate) fn callback_fn(
|
||||
callback: &Option<Arc<JniConfigServerCallback>>,
|
||||
) -> ConfigServerEventCallback {
|
||||
callback
|
||||
.as_ref()
|
||||
.map(|_| config_server_event_callback as unsafe extern "C" fn(*const c_char, *mut c_void))
|
||||
}
|
||||
|
||||
pub(crate) fn user_data(callback: &Option<Arc<JniConfigServerCallback>>) -> *mut c_void {
|
||||
callback
|
||||
.as_ref()
|
||||
.map(|callback| Arc::as_ptr(callback) as *mut c_void)
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
impl JniConfigServerCallback {
|
||||
fn clear_pending_exception(
|
||||
env: &mut JNIEnv,
|
||||
context: &str,
|
||||
error: &dyn std::fmt::Debug,
|
||||
) -> String {
|
||||
match env.exception_check() {
|
||||
Ok(true) => {
|
||||
if let Err(clear_err) = env.exception_clear() {
|
||||
return format!(
|
||||
"{}: {:?}; failed to clear pending Java exception: {:?}",
|
||||
context, error, clear_err
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(check_err) => {
|
||||
return format!(
|
||||
"{}: {:?}; failed to check pending Java exception: {:?}",
|
||||
context, error, check_err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
format!("{}: {:?}", context, error)
|
||||
}
|
||||
|
||||
fn on_event(&self, event_json: *const c_char) -> Result<(), String> {
|
||||
let event_json = unsafe { CStr::from_ptr(event_json) }
|
||||
.to_str()
|
||||
.map_err(|e| format!("Invalid config server event JSON: {:?}", e))?;
|
||||
let mut env = self
|
||||
.java_vm
|
||||
.attach_current_thread()
|
||||
.map_err(|e| format!("Failed to attach callback thread: {:?}", e))?;
|
||||
let event_json = env.new_string(event_json).map_err(|e| {
|
||||
Self::clear_pending_exception(&mut env, "Failed to create event string", &e)
|
||||
})?;
|
||||
|
||||
if let Err(e) = env.call_method(
|
||||
self.callback.as_obj(),
|
||||
"onEvent",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[JValue::from(&event_json)],
|
||||
) {
|
||||
return Err(Self::clear_pending_exception(
|
||||
&mut env,
|
||||
"Failed to call config server callback",
|
||||
&e,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn config_server_event_callback(
|
||||
event_json: *const c_char,
|
||||
user_data: *mut c_void,
|
||||
) {
|
||||
if event_json.is_null() || user_data.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
let callback = unsafe { &*(user_data as *const JniConfigServerCallback) };
|
||||
|
||||
if let Err(error) = callback.on_event(event_json) {
|
||||
error::set_callback_error(error);
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
use std::ptr;
|
||||
|
||||
use easytier_ffi::{
|
||||
in_config_server_callback, is_config_server_client_connected, start_config_server_client,
|
||||
stop_config_server_client,
|
||||
};
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JObject, JString};
|
||||
use jni::sys::{JNI_FALSE, JNI_TRUE, jboolean, jint};
|
||||
|
||||
use crate::{
|
||||
callback, error,
|
||||
strings::{jstring_to_cstring, optional_jstring_to_cstring},
|
||||
};
|
||||
|
||||
pub(crate) fn start_config_server_client_jni(
|
||||
env: &mut JNIEnv,
|
||||
config_server_url: JString,
|
||||
hostname: JString,
|
||||
machine_id: JString,
|
||||
secure_mode: jboolean,
|
||||
callback_obj: JObject,
|
||||
) -> jint {
|
||||
if in_config_server_callback() {
|
||||
error::throw_exception(
|
||||
env,
|
||||
"Cannot start config server client from config server callback",
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
|
||||
let config_server_url = match jstring_to_cstring(env, &config_server_url) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
error::throw_exception(env, &format!("Invalid config server URL: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let hostname = match optional_jstring_to_cstring(env, &hostname) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
error::throw_exception(env, &format!("Invalid hostname: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let machine_id = match jstring_to_cstring(env, &machine_id) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
error::throw_exception(env, &format!("Invalid machine ID: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let callback_ref = if callback_obj.is_null() {
|
||||
None
|
||||
} else {
|
||||
match callback::new_callback(env, &callback_obj) {
|
||||
Ok(state) => Some(state),
|
||||
Err(e) => {
|
||||
error::throw_exception(env, &e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut callback_guard = match callback::lock_callback_storage() {
|
||||
Ok(guard) => guard,
|
||||
Err(e) => {
|
||||
error::throw_exception(env, &e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
if callback_guard.is_none() {
|
||||
error::clear_callback_error();
|
||||
}
|
||||
|
||||
let callback_fn = callback::callback_fn(&callback_ref);
|
||||
let user_data = callback::user_data(&callback_ref);
|
||||
let result = unsafe {
|
||||
start_config_server_client(
|
||||
config_server_url.as_ptr(),
|
||||
hostname
|
||||
.as_ref()
|
||||
.map(|value| value.as_ptr())
|
||||
.unwrap_or(ptr::null()),
|
||||
machine_id.as_ptr(),
|
||||
secure_mode == JNI_TRUE,
|
||||
callback_fn,
|
||||
user_data,
|
||||
)
|
||||
};
|
||||
if result != 0 {
|
||||
if let Some(error_msg) = error::get_last_error() {
|
||||
error::throw_exception(env, &error_msg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
*callback_guard = callback_ref;
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn stop_config_server_client_jni(mut env: JNIEnv, _class: JClass) -> jint {
|
||||
if in_config_server_callback() {
|
||||
let result = stop_config_server_client();
|
||||
if result != 0
|
||||
&& let Some(error_msg) = error::get_last_error()
|
||||
{
|
||||
error::throw_exception(&mut env, &error_msg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
let mut callback_guard = match callback::lock_callback_storage() {
|
||||
Ok(guard) => guard,
|
||||
Err(e) => {
|
||||
error::throw_exception(&mut env, &e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let result = stop_config_server_client();
|
||||
if result != 0 {
|
||||
if let Some(error_msg) = error::get_last_error() {
|
||||
error::throw_exception(&mut env, &error_msg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
*callback_guard = None;
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn is_config_server_client_connected_jni(_env: JNIEnv, _class: JClass) -> jboolean {
|
||||
if is_config_server_client_connected() != 0 {
|
||||
JNI_TRUE
|
||||
} else {
|
||||
JNI_FALSE
|
||||
}
|
||||
}
|
||||
@@ -1,673 +0,0 @@
|
||||
use std::{
|
||||
ffi::{CStr, c_char},
|
||||
ptr,
|
||||
};
|
||||
|
||||
use easytier_ffi::{
|
||||
data_plane_async_op_cancel, data_plane_async_op_free, data_plane_async_op_status,
|
||||
data_plane_async_op_wait, data_plane_free_bytes, data_plane_tcp_accept_finish,
|
||||
data_plane_tcp_accept_start, data_plane_tcp_bind_finish, data_plane_tcp_bind_start,
|
||||
data_plane_tcp_close, data_plane_tcp_connect_finish, data_plane_tcp_connect_start,
|
||||
data_plane_tcp_listener_close, data_plane_tcp_read_finish, data_plane_tcp_read_start,
|
||||
data_plane_tcp_write_finish, data_plane_tcp_write_start, data_plane_udp_bind_finish,
|
||||
data_plane_udp_bind_start, data_plane_udp_close, data_plane_udp_recv_from_finish,
|
||||
data_plane_udp_recv_from_start, data_plane_udp_send_to_finish, data_plane_udp_send_to_start,
|
||||
free_string,
|
||||
};
|
||||
use jni::{
|
||||
JNIEnv,
|
||||
objects::{JByteArray, JClass, JObject, JString, JValue},
|
||||
sys::{jint, jlong, jobject},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::{get_last_error, throw_exception},
|
||||
strings::jstring_to_cstring,
|
||||
};
|
||||
|
||||
const SOCKET_ADDR_CLASS: &str = "com/easytier/jni/DataPlaneSocketAddress";
|
||||
const TCP_CONNECT_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneTcpConnectResult";
|
||||
const TCP_BIND_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneTcpBindResult";
|
||||
const TCP_ACCEPT_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneTcpAcceptResult";
|
||||
const TCP_READ_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneTcpReadResult";
|
||||
const UDP_BIND_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneUdpBindResult";
|
||||
const UDP_RECV_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneUdpRecvResult";
|
||||
|
||||
fn timeout_from_jlong(timeout_ms: jlong) -> u64 {
|
||||
timeout_ms.max(0) as u64
|
||||
}
|
||||
|
||||
fn port_from_jint(env: &mut JNIEnv, value: jint, name: &str) -> Option<u16> {
|
||||
match u16::try_from(value) {
|
||||
Ok(port) => Some(port),
|
||||
Err(_) => {
|
||||
throw_exception(env, &format!("Invalid {}: {}", name, value));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn len_from_jint(env: &mut JNIEnv, value: jint, name: &str) -> Option<u32> {
|
||||
match u32::try_from(value) {
|
||||
Ok(len) => Some(len),
|
||||
Err(_) => {
|
||||
throw_exception(env, &format!("Invalid {}: {}", name, value));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn throw_last(env: &mut JNIEnv) {
|
||||
let message = get_last_error().unwrap_or_else(|| "EasyTier data-plane call failed".to_string());
|
||||
throw_exception(env, &message);
|
||||
}
|
||||
|
||||
unsafe fn take_ffi_string(ptr: *const c_char) -> String {
|
||||
if ptr.is_null() {
|
||||
return String::new();
|
||||
}
|
||||
let value = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
free_string(ptr);
|
||||
value
|
||||
}
|
||||
|
||||
fn new_socket_addr<'local>(
|
||||
env: &mut JNIEnv<'local>,
|
||||
ip: String,
|
||||
port: u16,
|
||||
) -> Option<JObject<'local>> {
|
||||
let class = match env.find_class(SOCKET_ADDR_CLASS) {
|
||||
Ok(class) => class,
|
||||
Err(err) => {
|
||||
throw_exception(
|
||||
env,
|
||||
&format!("Failed to find socket address class: {:?}", err),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let ip = match env.new_string(ip) {
|
||||
Ok(ip) => ip,
|
||||
Err(err) => {
|
||||
throw_exception(env, &format!("Failed to create IP string: {:?}", err));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
match env.new_object(
|
||||
class,
|
||||
"(Ljava/lang/String;I)V",
|
||||
&[JValue::Object(&ip), JValue::Int(port as jint)],
|
||||
) {
|
||||
Ok(addr) => Some(addr),
|
||||
Err(err) => {
|
||||
throw_exception(env, &format!("Failed to create socket address: {:?}", err));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_handle_addr_result(
|
||||
env: &mut JNIEnv,
|
||||
class_name: &str,
|
||||
handle: u64,
|
||||
ip: String,
|
||||
port: u16,
|
||||
) -> jobject {
|
||||
let Some(addr) = new_socket_addr(env, ip, port) else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
let class = match env.find_class(class_name) {
|
||||
Ok(class) => class,
|
||||
Err(err) => {
|
||||
throw_exception(env, &format!("Failed to find result class: {:?}", err));
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let sig = format!("(JL{};)V", SOCKET_ADDR_CLASS);
|
||||
match env.new_object(
|
||||
class,
|
||||
sig.as_str(),
|
||||
&[JValue::Long(handle as jlong), JValue::Object(&addr)],
|
||||
) {
|
||||
Ok(result) => result.into_raw(),
|
||||
Err(err) => {
|
||||
throw_exception(env, &format!("Failed to create result object: {:?}", err));
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn close_tcp_stream_on_null(result: jobject, handle: u64) -> jobject {
|
||||
if result.is_null() {
|
||||
let _ = data_plane_tcp_close(handle);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn close_tcp_listener_on_null(result: jobject, handle: u64) -> jobject {
|
||||
if result.is_null() {
|
||||
let _ = data_plane_tcp_listener_close(handle);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn close_udp_socket_on_null(result: jobject, handle: u64) -> jobject {
|
||||
if result.is_null() {
|
||||
let _ = data_plane_udp_close(handle);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn read_owned_bytes(ptr: *const u8, len: u32) -> Vec<u8> {
|
||||
if ptr.is_null() || len == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let bytes = unsafe { std::slice::from_raw_parts(ptr, len as usize) }.to_vec();
|
||||
data_plane_free_bytes(ptr, len);
|
||||
bytes
|
||||
}
|
||||
|
||||
pub(crate) fn async_op_status_jni(_env: JNIEnv, _class: JClass, handle: jlong) -> jint {
|
||||
data_plane_async_op_status(handle as u64)
|
||||
}
|
||||
|
||||
pub(crate) fn async_op_wait_jni(
|
||||
_env: JNIEnv,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
timeout_ms: jlong,
|
||||
) -> jint {
|
||||
data_plane_async_op_wait(handle as u64, timeout_ms.max(0) as u64)
|
||||
}
|
||||
|
||||
pub(crate) fn async_op_cancel_jni(_env: JNIEnv, _class: JClass, handle: jlong) -> jint {
|
||||
data_plane_async_op_cancel(handle as u64)
|
||||
}
|
||||
|
||||
pub(crate) fn async_op_free_jni(_env: JNIEnv, _class: JClass, handle: jlong) -> jint {
|
||||
data_plane_async_op_free(handle as u64)
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_connect_start_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
inst_name: JString,
|
||||
dst_ip: JString,
|
||||
dst_port: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
let inst_name = match jstring_to_cstring(&mut env, &inst_name) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
throw_exception(&mut env, &format!("Invalid instance name: {}", err));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let dst_ip = match jstring_to_cstring(&mut env, &dst_ip) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
throw_exception(&mut env, &format!("Invalid destination IP: {}", err));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let Some(dst_port) = port_from_jint(&mut env, dst_port, "destination port") else {
|
||||
return 0;
|
||||
};
|
||||
let op = unsafe {
|
||||
data_plane_tcp_connect_start(
|
||||
inst_name.as_ptr(),
|
||||
dst_ip.as_ptr(),
|
||||
dst_port,
|
||||
timeout_ms.max(0) as u64,
|
||||
)
|
||||
};
|
||||
if op == 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
op as jlong
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_connect_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject {
|
||||
let mut ip: *const c_char = ptr::null();
|
||||
let mut port = 0u16;
|
||||
let handle = unsafe { data_plane_tcp_connect_finish(op as u64, &mut ip, &mut port) };
|
||||
if handle == 0 {
|
||||
throw_last(&mut env);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
close_tcp_stream_on_null(
|
||||
new_handle_addr_result(
|
||||
&mut env,
|
||||
TCP_CONNECT_RESULT_CLASS,
|
||||
handle,
|
||||
unsafe { take_ffi_string(ip) },
|
||||
port,
|
||||
),
|
||||
handle,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_bind_start_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
inst_name: JString,
|
||||
local_port: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
let inst_name = match jstring_to_cstring(&mut env, &inst_name) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
throw_exception(&mut env, &format!("Invalid instance name: {}", err));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let Some(local_port) = port_from_jint(&mut env, local_port, "local port") else {
|
||||
return 0;
|
||||
};
|
||||
let op = unsafe {
|
||||
data_plane_tcp_bind_start(
|
||||
inst_name.as_ptr(),
|
||||
local_port,
|
||||
timeout_from_jlong(timeout_ms),
|
||||
)
|
||||
};
|
||||
if op == 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
op as jlong
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_bind_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject {
|
||||
let mut ip: *const c_char = ptr::null();
|
||||
let mut port = 0u16;
|
||||
let handle = unsafe { data_plane_tcp_bind_finish(op as u64, &mut ip, &mut port) };
|
||||
if handle == 0 {
|
||||
throw_last(&mut env);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
close_tcp_listener_on_null(
|
||||
new_handle_addr_result(
|
||||
&mut env,
|
||||
TCP_BIND_RESULT_CLASS,
|
||||
handle,
|
||||
unsafe { take_ffi_string(ip) },
|
||||
port,
|
||||
),
|
||||
handle,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_accept_start_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
let op = unsafe { data_plane_tcp_accept_start(handle as u64, timeout_from_jlong(timeout_ms)) };
|
||||
if op == 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
op as jlong
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_accept_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject {
|
||||
let mut local_ip: *const c_char = ptr::null();
|
||||
let mut local_port = 0u16;
|
||||
let mut peer_ip: *const c_char = ptr::null();
|
||||
let mut peer_port = 0u16;
|
||||
let handle = unsafe {
|
||||
data_plane_tcp_accept_finish(
|
||||
op as u64,
|
||||
&mut local_ip,
|
||||
&mut local_port,
|
||||
&mut peer_ip,
|
||||
&mut peer_port,
|
||||
)
|
||||
};
|
||||
if handle == 0 {
|
||||
throw_last(&mut env);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let Some(local_addr) =
|
||||
new_socket_addr(&mut env, unsafe { take_ffi_string(local_ip) }, local_port)
|
||||
else {
|
||||
free_string(peer_ip);
|
||||
let _ = data_plane_tcp_close(handle);
|
||||
return ptr::null_mut();
|
||||
};
|
||||
let Some(peer_addr) = new_socket_addr(&mut env, unsafe { take_ffi_string(peer_ip) }, peer_port)
|
||||
else {
|
||||
let _ = data_plane_tcp_close(handle);
|
||||
return ptr::null_mut();
|
||||
};
|
||||
let class = match env.find_class(TCP_ACCEPT_RESULT_CLASS) {
|
||||
Ok(class) => class,
|
||||
Err(err) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Failed to find accept result class: {:?}", err),
|
||||
);
|
||||
let _ = data_plane_tcp_close(handle);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let sig = format!("(JL{};L{};)V", SOCKET_ADDR_CLASS, SOCKET_ADDR_CLASS);
|
||||
let result = match env.new_object(
|
||||
class,
|
||||
sig.as_str(),
|
||||
&[
|
||||
JValue::Long(handle as jlong),
|
||||
JValue::Object(&local_addr),
|
||||
JValue::Object(&peer_addr),
|
||||
],
|
||||
) {
|
||||
Ok(result) => result.into_raw(),
|
||||
Err(err) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Failed to create accept result: {:?}", err),
|
||||
);
|
||||
ptr::null_mut()
|
||||
}
|
||||
};
|
||||
close_tcp_stream_on_null(result, handle)
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_read_start_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
max_len: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
let Some(max_len) = len_from_jint(&mut env, max_len, "max length") else {
|
||||
return 0;
|
||||
};
|
||||
let op = unsafe {
|
||||
data_plane_tcp_read_start(handle as u64, max_len, timeout_from_jlong(timeout_ms))
|
||||
};
|
||||
if op == 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
op as jlong
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_read_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject {
|
||||
let mut ptr: *const u8 = ptr::null();
|
||||
let mut len = 0u32;
|
||||
let ret = unsafe { data_plane_tcp_read_finish(op as u64, &mut ptr, &mut len) };
|
||||
if ret < 0 {
|
||||
throw_last(&mut env);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let bytes = read_owned_bytes(ptr, len);
|
||||
let array = match env.byte_array_from_slice(&bytes) {
|
||||
Ok(array) => array,
|
||||
Err(err) => {
|
||||
throw_exception(&mut env, &format!("Failed to create byte array: {:?}", err));
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let class = match env.find_class(TCP_READ_RESULT_CLASS) {
|
||||
Ok(class) => class,
|
||||
Err(err) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Failed to find read result class: {:?}", err),
|
||||
);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
match env.new_object(class, "([B)V", &[JValue::Object(&array)]) {
|
||||
Ok(result) => result.into_raw(),
|
||||
Err(err) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Failed to create read result: {:?}", err),
|
||||
);
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_write_start_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
data: JByteArray,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
let data = match env.convert_byte_array(&data) {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
throw_exception(&mut env, &format!("Invalid write buffer: {:?}", err));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let ptr = if data.is_empty() {
|
||||
ptr::null()
|
||||
} else {
|
||||
data.as_ptr()
|
||||
};
|
||||
let op = unsafe {
|
||||
data_plane_tcp_write_start(
|
||||
handle as u64,
|
||||
ptr,
|
||||
data.len() as u32,
|
||||
timeout_from_jlong(timeout_ms),
|
||||
)
|
||||
};
|
||||
if op == 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
op as jlong
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_write_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jint {
|
||||
let ret = data_plane_tcp_write_finish(op as u64);
|
||||
if ret < 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub(crate) fn udp_bind_start_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
inst_name: JString,
|
||||
local_port: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
let inst_name = match jstring_to_cstring(&mut env, &inst_name) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
throw_exception(&mut env, &format!("Invalid instance name: {}", err));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let Some(local_port) = port_from_jint(&mut env, local_port, "local port") else {
|
||||
return 0;
|
||||
};
|
||||
let op = unsafe {
|
||||
data_plane_udp_bind_start(
|
||||
inst_name.as_ptr(),
|
||||
local_port,
|
||||
timeout_from_jlong(timeout_ms),
|
||||
)
|
||||
};
|
||||
if op == 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
op as jlong
|
||||
}
|
||||
|
||||
pub(crate) fn udp_bind_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject {
|
||||
let mut ip: *const c_char = ptr::null();
|
||||
let mut port = 0u16;
|
||||
let handle = unsafe { data_plane_udp_bind_finish(op as u64, &mut ip, &mut port) };
|
||||
if handle == 0 {
|
||||
throw_last(&mut env);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
close_udp_socket_on_null(
|
||||
new_handle_addr_result(
|
||||
&mut env,
|
||||
UDP_BIND_RESULT_CLASS,
|
||||
handle,
|
||||
unsafe { take_ffi_string(ip) },
|
||||
port,
|
||||
),
|
||||
handle,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn udp_send_to_start_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
dst_ip: JString,
|
||||
dst_port: jint,
|
||||
data: JByteArray,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
let dst_ip = match jstring_to_cstring(&mut env, &dst_ip) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
throw_exception(&mut env, &format!("Invalid destination IP: {}", err));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let Some(dst_port) = port_from_jint(&mut env, dst_port, "destination port") else {
|
||||
return 0;
|
||||
};
|
||||
let data = match env.convert_byte_array(&data) {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
throw_exception(&mut env, &format!("Invalid UDP send buffer: {:?}", err));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let ptr = if data.is_empty() {
|
||||
ptr::null()
|
||||
} else {
|
||||
data.as_ptr()
|
||||
};
|
||||
let op = unsafe {
|
||||
data_plane_udp_send_to_start(
|
||||
handle as u64,
|
||||
dst_ip.as_ptr(),
|
||||
dst_port,
|
||||
ptr,
|
||||
data.len() as u32,
|
||||
timeout_from_jlong(timeout_ms),
|
||||
)
|
||||
};
|
||||
if op == 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
op as jlong
|
||||
}
|
||||
|
||||
pub(crate) fn udp_send_to_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jint {
|
||||
let ret = data_plane_udp_send_to_finish(op as u64);
|
||||
if ret < 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub(crate) fn udp_recv_from_start_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
max_len: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
let Some(max_len) = len_from_jint(&mut env, max_len, "max length") else {
|
||||
return 0;
|
||||
};
|
||||
let op = unsafe {
|
||||
data_plane_udp_recv_from_start(handle as u64, max_len, timeout_from_jlong(timeout_ms))
|
||||
};
|
||||
if op == 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
op as jlong
|
||||
}
|
||||
|
||||
pub(crate) fn udp_recv_from_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject {
|
||||
let mut ptr: *const u8 = ptr::null();
|
||||
let mut len = 0u32;
|
||||
let mut ip: *const c_char = ptr::null();
|
||||
let mut port = 0u16;
|
||||
let ret = unsafe {
|
||||
data_plane_udp_recv_from_finish(op as u64, &mut ptr, &mut len, &mut ip, &mut port)
|
||||
};
|
||||
if ret < 0 {
|
||||
throw_last(&mut env);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let bytes = read_owned_bytes(ptr, len);
|
||||
let array = match env.byte_array_from_slice(&bytes) {
|
||||
Ok(array) => array,
|
||||
Err(err) => {
|
||||
free_string(ip);
|
||||
throw_exception(&mut env, &format!("Failed to create byte array: {:?}", err));
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let Some(peer_addr) = new_socket_addr(&mut env, unsafe { take_ffi_string(ip) }, port) else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
let class = match env.find_class(UDP_RECV_RESULT_CLASS) {
|
||||
Ok(class) => class,
|
||||
Err(err) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Failed to find UDP recv result class: {:?}", err),
|
||||
);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let sig = format!("([BL{};)V", SOCKET_ADDR_CLASS);
|
||||
match env.new_object(
|
||||
class,
|
||||
sig.as_str(),
|
||||
&[JValue::Object(&array), JValue::Object(&peer_addr)],
|
||||
) {
|
||||
Ok(result) => result.into_raw(),
|
||||
Err(err) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Failed to create UDP recv result: {:?}", err),
|
||||
);
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_close_jni(mut env: JNIEnv, _class: JClass, handle: jlong) -> jint {
|
||||
let ret = data_plane_tcp_close(handle as u64);
|
||||
if ret != 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_listener_close_jni(mut env: JNIEnv, _class: JClass, handle: jlong) -> jint {
|
||||
let ret = data_plane_tcp_listener_close(handle as u64);
|
||||
if ret != 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub(crate) fn udp_close_jni(mut env: JNIEnv, _class: JClass, handle: jlong) -> jint {
|
||||
let ret = data_plane_udp_close(handle as u64);
|
||||
if ret != 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
ret
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
use std::{
|
||||
ffi::{CStr, c_char},
|
||||
ptr,
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use easytier_ffi::{free_string, get_error_msg};
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::JClass;
|
||||
use jni::sys::jstring;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static JNI_CALLBACK_ERROR: Lazy<Mutex<Option<String>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
pub(crate) fn set_callback_error(error: String) {
|
||||
log::error!("{}", error);
|
||||
if let Ok(mut guard) = JNI_CALLBACK_ERROR.lock() {
|
||||
*guard = Some(error);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_callback_error() {
|
||||
if let Ok(mut guard) = JNI_CALLBACK_ERROR.lock() {
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn take_callback_error() -> Option<String> {
|
||||
JNI_CALLBACK_ERROR
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|mut guard| guard.take())
|
||||
}
|
||||
|
||||
fn get_ffi_last_error() -> Option<String> {
|
||||
unsafe {
|
||||
let mut error_ptr: *const c_char = ptr::null();
|
||||
get_error_msg(&mut error_ptr);
|
||||
if error_ptr.is_null() {
|
||||
None
|
||||
} else {
|
||||
let error_cstr = CStr::from_ptr(error_ptr);
|
||||
let error_str = error_cstr.to_string_lossy().into_owned();
|
||||
free_string(error_ptr);
|
||||
Some(error_str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_last_error() -> Option<String> {
|
||||
match (get_ffi_last_error(), take_callback_error()) {
|
||||
(Some(ffi_error), Some(callback_error)) => Some(format!(
|
||||
"{}; config server callback error: {}",
|
||||
ffi_error, callback_error
|
||||
)),
|
||||
(Some(ffi_error), None) => Some(ffi_error),
|
||||
(None, Some(callback_error)) => Some(callback_error),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn throw_exception(env: &mut JNIEnv, message: &str) {
|
||||
let _ = env.throw_new("java/lang/RuntimeException", message);
|
||||
}
|
||||
|
||||
pub(crate) fn get_last_error_jni(env: JNIEnv, _class: JClass) -> jstring {
|
||||
match get_last_error() {
|
||||
Some(error) => match env.new_string(&error) {
|
||||
Ok(jstr) => jstr.into_raw(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
},
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
use std::{
|
||||
ffi::{CStr, c_char},
|
||||
ptr,
|
||||
};
|
||||
|
||||
use easytier_ffi::{call_json_rpc, free_string};
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JString};
|
||||
use jni::sys::jstring;
|
||||
|
||||
use crate::{
|
||||
error::{get_last_error, throw_exception},
|
||||
strings::{jstring_to_cstring, optional_jstring_to_cstring},
|
||||
};
|
||||
|
||||
pub(crate) fn call_json_rpc_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
service_name: JString,
|
||||
method_name: JString,
|
||||
domain_name: JString,
|
||||
payload_json: JString,
|
||||
) -> jstring {
|
||||
let service_name_cstr = match jstring_to_cstring(&mut env, &service_name) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid service name: {}", e));
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let method_name_cstr = match jstring_to_cstring(&mut env, &method_name) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid method name: {}", e));
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let domain_name_cstr = match optional_jstring_to_cstring(&mut env, &domain_name) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid domain name: {}", e));
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let payload_json_cstr = match jstring_to_cstring(&mut env, &payload_json) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid payload JSON: {}", e));
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
|
||||
let domain_name_ptr = domain_name_cstr
|
||||
.as_ref()
|
||||
.map_or(ptr::null(), |cstr| cstr.as_ptr());
|
||||
let mut response_ptr: *const c_char = ptr::null();
|
||||
let result = unsafe {
|
||||
call_json_rpc(
|
||||
service_name_cstr.as_ptr(),
|
||||
method_name_cstr.as_ptr(),
|
||||
domain_name_ptr,
|
||||
payload_json_cstr.as_ptr(),
|
||||
&mut response_ptr,
|
||||
)
|
||||
};
|
||||
|
||||
if result != 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
if response_ptr.is_null() {
|
||||
throw_exception(&mut env, "JSON RPC returned a null response");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let response = unsafe { CStr::from_ptr(response_ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
free_string(response_ptr);
|
||||
|
||||
match env.new_string(&response) {
|
||||
Ok(jstr) => jstr.into_raw(),
|
||||
Err(_) => {
|
||||
throw_exception(&mut env, "Failed to create JSON RPC response string");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static LOGGER_INIT: Lazy<()> = Lazy::new(|| {
|
||||
android_logger::init_once(
|
||||
android_logger::Config::default()
|
||||
.with_max_level(log::LevelFilter::Debug)
|
||||
.with_tag("EasyTier-JNI"),
|
||||
);
|
||||
});
|
||||
|
||||
pub(crate) fn init() {
|
||||
Lazy::force(&LOGGER_INIT);
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
use std::{ffi::CStr, ptr};
|
||||
|
||||
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
|
||||
use easytier_ffi::{
|
||||
KeyValuePair, collect_network_infos, free_string, list_instance, parse_config,
|
||||
retain_network_instance, run_network_instance, set_tun_fd,
|
||||
};
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JObjectArray, JString};
|
||||
use jni::sys::{jint, jstring};
|
||||
|
||||
use crate::{
|
||||
error::{get_last_error, throw_exception},
|
||||
strings::jstring_to_cstring,
|
||||
};
|
||||
|
||||
pub(crate) fn set_tun_fd_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
inst_name: JString,
|
||||
fd: jint,
|
||||
) -> jint {
|
||||
let inst_name_cstr = match jstring_to_cstring(&mut env, &inst_name) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid instance name: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
unsafe {
|
||||
let result = set_tun_fd(inst_name_cstr.as_ptr(), fd);
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_config_jni(mut env: JNIEnv, _class: JClass, config: JString) -> jint {
|
||||
let config_cstr = match jstring_to_cstring(&mut env, &config) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid config string: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
unsafe {
|
||||
let result = parse_config(config_cstr.as_ptr());
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run_network_instance_jni(mut env: JNIEnv, _class: JClass, config: JString) -> jint {
|
||||
let config_cstr = match jstring_to_cstring(&mut env, &config) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid config string: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
unsafe {
|
||||
let result = run_network_instance(config_cstr.as_ptr());
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn retain_network_instance_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
instance_names: JObjectArray,
|
||||
) -> jint {
|
||||
if instance_names.is_null() {
|
||||
return retain_all(&mut env);
|
||||
}
|
||||
|
||||
let array_length = match env.get_array_length(&instance_names) {
|
||||
Ok(len) => len as usize,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Failed to get array length: {:?}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if array_length == 0 {
|
||||
return retain_all(&mut env);
|
||||
}
|
||||
|
||||
let mut c_strings = Vec::with_capacity(array_length);
|
||||
let mut c_string_ptrs = Vec::with_capacity(array_length);
|
||||
|
||||
for i in 0..array_length {
|
||||
let java_string = match env.get_object_array_element(&instance_names, i as i32) {
|
||||
Ok(obj) => obj,
|
||||
Err(e) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Failed to get array element {}: {:?}", i, e),
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if java_string.is_null() {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Invalid instance name at index {}: null", i),
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
|
||||
let jstring = JString::from(java_string);
|
||||
let c_string = match jstring_to_cstring(&mut env, &jstring) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Invalid instance name at index {}: {}", i, e),
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
c_string_ptrs.push(c_string.as_ptr());
|
||||
c_strings.push(c_string);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let result = retain_network_instance(c_string_ptrs.as_ptr(), c_string_ptrs.len());
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn retain_all(env: &mut JNIEnv) -> jint {
|
||||
unsafe {
|
||||
let result = retain_network_instance(ptr::null(), 0);
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn collect_network_infos_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
max_length: jint,
|
||||
) -> jstring {
|
||||
let max_length = max_length.max(0) as usize;
|
||||
let mut infos = vec![
|
||||
KeyValuePair {
|
||||
key: ptr::null(),
|
||||
value: ptr::null(),
|
||||
};
|
||||
max_length
|
||||
];
|
||||
|
||||
unsafe {
|
||||
let count = collect_network_infos(infos.as_mut_ptr(), max_length);
|
||||
if count < 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let mut ret = NetworkInstanceRunningInfoMap::default();
|
||||
for info in infos.iter().take(count as usize) {
|
||||
let key_ptr = info.key;
|
||||
let val_ptr = info.value;
|
||||
if key_ptr.is_null() || val_ptr.is_null() {
|
||||
break;
|
||||
}
|
||||
|
||||
let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
|
||||
let val = CStr::from_ptr(val_ptr).to_string_lossy().into_owned();
|
||||
free_string(key_ptr);
|
||||
free_string(val_ptr);
|
||||
let value = match serde_json::from_str::<NetworkInstanceRunningInfo>(&val) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
throw_exception(&mut env, "Failed to parse JSON");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
ret.map.insert(key, value);
|
||||
}
|
||||
|
||||
let json_str = serde_json::to_string(&ret).unwrap_or_else(|_| "{}".to_string());
|
||||
match env.new_string(&json_str) {
|
||||
Ok(jstr) => jstr.into_raw(),
|
||||
Err(_) => {
|
||||
throw_exception(&mut env, "Failed to create JSON string");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn list_instances_jni(mut env: JNIEnv, _class: JClass, max_length: jint) -> jstring {
|
||||
let max_length = max_length.max(0) as usize;
|
||||
let mut infos = vec![
|
||||
KeyValuePair {
|
||||
key: ptr::null(),
|
||||
value: ptr::null(),
|
||||
};
|
||||
max_length
|
||||
];
|
||||
|
||||
unsafe {
|
||||
let count = list_instance(infos.as_mut_ptr(), max_length);
|
||||
if count < 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let mut ret = serde_json::Map::new();
|
||||
for info in infos.iter().take(count as usize) {
|
||||
let key_ptr = info.key;
|
||||
let val_ptr = info.value;
|
||||
if key_ptr.is_null() || val_ptr.is_null() {
|
||||
break;
|
||||
}
|
||||
|
||||
let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
|
||||
let val = CStr::from_ptr(val_ptr).to_string_lossy().into_owned();
|
||||
free_string(key_ptr);
|
||||
free_string(val_ptr);
|
||||
ret.insert(key, serde_json::Value::String(val));
|
||||
}
|
||||
|
||||
let json_str = serde_json::Value::Object(ret).to_string();
|
||||
match env.new_string(&json_str) {
|
||||
Ok(jstr) => jstr.into_raw(),
|
||||
Err(_) => {
|
||||
throw_exception(&mut env, "Failed to create instance list JSON string");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
use std::ffi::CString;
|
||||
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::JString;
|
||||
|
||||
pub(crate) fn jstring_to_cstring(env: &mut JNIEnv, jstr: &JString) -> Result<CString, String> {
|
||||
let java_str = env
|
||||
.get_string(jstr)
|
||||
.map_err(|e| format!("Failed to get string: {:?}", e))?;
|
||||
let rust_str = java_str.to_str().map_err(|_| "Invalid UTF-8".to_string())?;
|
||||
CString::new(rust_str).map_err(|_| "String contains null byte".to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn optional_jstring_to_cstring(
|
||||
env: &mut JNIEnv,
|
||||
jstr: &JString,
|
||||
) -> Result<Option<CString>, String> {
|
||||
if jstr.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
jstring_to_cstring(env, jstr).map(Some)
|
||||
}
|
||||
@@ -4,25 +4,14 @@ version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["c-abi", "ffi-dataplane"]
|
||||
c-abi = []
|
||||
ffi-dataplane = ["easytier/ffi-dataplane"]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
easytier = { path = "../../easytier" }
|
||||
|
||||
once_cell = "1.18.0"
|
||||
dashmap = "6.0"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "io-util", "time", "sync", "macros"] }
|
||||
async-trait = "0.1"
|
||||
log = "0.4"
|
||||
percent-encoding = "2.3"
|
||||
url = "2"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
uuid = "1.17.0"
|
||||
tokio-util = "0.7"
|
||||
|
||||
@@ -1,429 +0,0 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define DATA_PLANE_OP_PENDING 0
|
||||
#define DATA_PLANE_OP_READY 1
|
||||
#define DATA_PLANE_OP_FAILED -1
|
||||
#define DATA_PLANE_OP_INVALID -2
|
||||
|
||||
extern int run_network_instance(const char *cfg_str);
|
||||
extern void get_error_msg(const char **out);
|
||||
extern void free_string(const char *s);
|
||||
|
||||
extern int data_plane_async_op_status(uint64_t op);
|
||||
extern int data_plane_async_op_wait(uint64_t op, uint64_t timeout_ms);
|
||||
extern int data_plane_async_op_cancel(uint64_t op);
|
||||
extern int data_plane_async_op_free(uint64_t op);
|
||||
extern void data_plane_free_bytes(const uint8_t *ptr, uint32_t len);
|
||||
|
||||
extern uint64_t data_plane_tcp_connect_start(
|
||||
const char *inst_name,
|
||||
const char *dst_ip,
|
||||
uint16_t dst_port,
|
||||
uint64_t timeout_ms);
|
||||
extern uint64_t data_plane_tcp_connect_finish(
|
||||
uint64_t op,
|
||||
const char **out_local_ip,
|
||||
uint16_t *out_local_port);
|
||||
extern uint64_t data_plane_tcp_bind_start(
|
||||
const char *inst_name,
|
||||
uint16_t local_port,
|
||||
uint64_t timeout_ms);
|
||||
extern uint64_t data_plane_tcp_bind_finish(
|
||||
uint64_t op,
|
||||
const char **out_local_ip,
|
||||
uint16_t *out_local_port);
|
||||
extern uint64_t data_plane_tcp_accept_start(uint64_t listener, uint64_t timeout_ms);
|
||||
extern uint64_t data_plane_tcp_accept_finish(
|
||||
uint64_t op,
|
||||
const char **out_local_ip,
|
||||
uint16_t *out_local_port,
|
||||
const char **out_peer_ip,
|
||||
uint16_t *out_peer_port);
|
||||
extern uint64_t data_plane_tcp_read_start(
|
||||
uint64_t stream,
|
||||
uint32_t max_len,
|
||||
uint64_t timeout_ms);
|
||||
extern int data_plane_tcp_read_finish(
|
||||
uint64_t op,
|
||||
const uint8_t **out_buf,
|
||||
uint32_t *out_len);
|
||||
extern uint64_t data_plane_tcp_write_start(
|
||||
uint64_t stream,
|
||||
const uint8_t *buf,
|
||||
uint32_t len,
|
||||
uint64_t timeout_ms);
|
||||
extern int data_plane_tcp_write_finish(uint64_t op);
|
||||
extern int data_plane_tcp_close(uint64_t stream);
|
||||
extern int data_plane_tcp_listener_close(uint64_t listener);
|
||||
|
||||
extern uint64_t data_plane_udp_bind_start(
|
||||
const char *inst_name,
|
||||
uint16_t local_port,
|
||||
uint64_t timeout_ms);
|
||||
extern uint64_t data_plane_udp_bind_finish(
|
||||
uint64_t op,
|
||||
const char **out_local_ip,
|
||||
uint16_t *out_local_port);
|
||||
extern uint64_t data_plane_udp_send_to_start(
|
||||
uint64_t socket,
|
||||
const char *dst_ip,
|
||||
uint16_t dst_port,
|
||||
const uint8_t *buf,
|
||||
uint32_t len,
|
||||
uint64_t timeout_ms);
|
||||
extern int data_plane_udp_send_to_finish(uint64_t op);
|
||||
extern uint64_t data_plane_udp_recv_from_start(
|
||||
uint64_t socket,
|
||||
uint32_t max_len,
|
||||
uint64_t timeout_ms);
|
||||
extern int data_plane_udp_recv_from_finish(
|
||||
uint64_t op,
|
||||
const uint8_t **out_buf,
|
||||
uint32_t *out_len,
|
||||
const char **out_ip,
|
||||
uint16_t *out_port);
|
||||
extern int data_plane_udp_close(uint64_t socket);
|
||||
|
||||
static void print_last_error(const char *prefix) {
|
||||
const char *err = NULL;
|
||||
get_error_msg(&err);
|
||||
if (err) {
|
||||
fprintf(stderr, "%s: %s\n", prefix, err);
|
||||
free_string(err);
|
||||
} else {
|
||||
fprintf(stderr, "%s\n", prefix);
|
||||
}
|
||||
}
|
||||
|
||||
static int parse_ip_port(const char *value, char *ip, size_t ip_len, uint16_t *port) {
|
||||
const char *colon = strrchr(value, ':');
|
||||
if (!colon || colon == value || !colon[1]) {
|
||||
fprintf(stderr, "expected IPv4 target in IP:PORT form, got %s\n", value);
|
||||
return -1;
|
||||
}
|
||||
size_t host_len = (size_t)(colon - value);
|
||||
if (host_len >= ip_len) {
|
||||
fprintf(stderr, "IP address is too long: %s\n", value);
|
||||
return -1;
|
||||
}
|
||||
char *end = NULL;
|
||||
long parsed_port = strtol(colon + 1, &end, 10);
|
||||
if (!end || *end != '\0' || parsed_port < 0 || parsed_port > 65535) {
|
||||
fprintf(stderr, "invalid port in %s\n", value);
|
||||
return -1;
|
||||
}
|
||||
memcpy(ip, value, host_len);
|
||||
ip[host_len] = '\0';
|
||||
*port = (uint16_t)parsed_port;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int wait_op(uint64_t op, uint64_t timeout_ms) {
|
||||
uint64_t waited = 0;
|
||||
while (waited < timeout_ms) {
|
||||
int status = data_plane_async_op_wait(op, 50);
|
||||
if (status != DATA_PLANE_OP_PENDING) {
|
||||
return status;
|
||||
}
|
||||
waited += 50;
|
||||
}
|
||||
return data_plane_async_op_status(op);
|
||||
}
|
||||
|
||||
static int wait_or_cancel(uint64_t op, uint64_t timeout_ms, const char *what) {
|
||||
int status = wait_op(op, timeout_ms);
|
||||
if (status == DATA_PLANE_OP_READY || status == DATA_PLANE_OP_FAILED) {
|
||||
return status;
|
||||
}
|
||||
if (status == DATA_PLANE_OP_PENDING) {
|
||||
fprintf(stderr, "%s did not finish within %llu ms\n", what, (unsigned long long)timeout_ms);
|
||||
data_plane_async_op_cancel(op);
|
||||
data_plane_async_op_free(op);
|
||||
return DATA_PLANE_OP_INVALID;
|
||||
}
|
||||
fprintf(stderr, "%s returned invalid op status %d\n", what, status);
|
||||
return status;
|
||||
}
|
||||
|
||||
static int async_tcp_read_once(uint64_t stream, uint64_t timeout_ms) {
|
||||
uint64_t op = data_plane_tcp_read_start(stream, 512, timeout_ms);
|
||||
if (!op) {
|
||||
print_last_error("tcp read start failed");
|
||||
return -1;
|
||||
}
|
||||
if (wait_or_cancel(op, timeout_ms + 1000, "tcp read") == DATA_PLANE_OP_INVALID) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const uint8_t *buf = NULL;
|
||||
uint32_t len = 0;
|
||||
int ret = data_plane_tcp_read_finish(op, &buf, &len);
|
||||
if (ret < 0) {
|
||||
print_last_error("tcp read finish failed");
|
||||
return -1;
|
||||
}
|
||||
printf("tcp read %d bytes: %.*s\n", ret, ret, buf ? (const char *)buf : "");
|
||||
data_plane_free_bytes(buf, len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int async_tcp_write_all(uint64_t stream, const char *data, uint64_t timeout_ms) {
|
||||
uint64_t op = data_plane_tcp_write_start(
|
||||
stream,
|
||||
(const uint8_t *)data,
|
||||
(uint32_t)strlen(data),
|
||||
timeout_ms);
|
||||
if (!op) {
|
||||
print_last_error("tcp write start failed");
|
||||
return -1;
|
||||
}
|
||||
if (wait_or_cancel(op, timeout_ms + 1000, "tcp write") == DATA_PLANE_OP_INVALID) {
|
||||
return -1;
|
||||
}
|
||||
int ret = data_plane_tcp_write_finish(op);
|
||||
if (ret < 0) {
|
||||
print_last_error("tcp write finish failed");
|
||||
return -1;
|
||||
}
|
||||
printf("tcp wrote %d bytes\n", ret);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int run_tcp_connect_demo(const char *inst, const char *target) {
|
||||
char ip[128];
|
||||
uint16_t port = 0;
|
||||
if (parse_ip_port(target, ip, sizeof(ip), &port) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t op = data_plane_tcp_connect_start(inst, ip, port, 30000);
|
||||
if (!op) {
|
||||
print_last_error("tcp connect start failed");
|
||||
return -1;
|
||||
}
|
||||
if (wait_or_cancel(op, 31000, "tcp connect") == DATA_PLANE_OP_INVALID) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *local_ip = NULL;
|
||||
uint16_t local_port = 0;
|
||||
uint64_t stream = data_plane_tcp_connect_finish(op, &local_ip, &local_port);
|
||||
if (!stream) {
|
||||
print_last_error("tcp connect finish failed");
|
||||
return -1;
|
||||
}
|
||||
printf("tcp connected from %s:%u to %s:%u, handle=%llu\n",
|
||||
local_ip,
|
||||
local_port,
|
||||
ip,
|
||||
port,
|
||||
(unsigned long long)stream);
|
||||
free_string(local_ip);
|
||||
|
||||
int ret = async_tcp_read_once(stream, 10000);
|
||||
data_plane_tcp_close(stream);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int run_tcp_listen_demo(const char *inst, const char *port_text) {
|
||||
uint16_t port = (uint16_t)strtoul(port_text, NULL, 10);
|
||||
uint64_t op = data_plane_tcp_bind_start(inst, port, 30000);
|
||||
if (!op) {
|
||||
print_last_error("tcp bind start failed");
|
||||
return -1;
|
||||
}
|
||||
if (wait_or_cancel(op, 31000, "tcp bind") == DATA_PLANE_OP_INVALID) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *local_ip = NULL;
|
||||
uint16_t local_port = 0;
|
||||
uint64_t listener = data_plane_tcp_bind_finish(op, &local_ip, &local_port);
|
||||
if (!listener) {
|
||||
print_last_error("tcp bind finish failed");
|
||||
return -1;
|
||||
}
|
||||
printf("tcp listening on %s:%u, handle=%llu\n",
|
||||
local_ip,
|
||||
local_port,
|
||||
(unsigned long long)listener);
|
||||
free_string(local_ip);
|
||||
|
||||
op = data_plane_tcp_accept_start(listener, 60000);
|
||||
if (!op) {
|
||||
print_last_error("tcp accept start failed");
|
||||
data_plane_tcp_listener_close(listener);
|
||||
return -1;
|
||||
}
|
||||
if (wait_or_cancel(op, 61000, "tcp accept") == DATA_PLANE_OP_INVALID) {
|
||||
data_plane_tcp_listener_close(listener);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *peer_ip = NULL;
|
||||
uint16_t peer_port = 0;
|
||||
local_ip = NULL;
|
||||
local_port = 0;
|
||||
uint64_t stream = data_plane_tcp_accept_finish(
|
||||
op,
|
||||
&local_ip,
|
||||
&local_port,
|
||||
&peer_ip,
|
||||
&peer_port);
|
||||
data_plane_tcp_listener_close(listener);
|
||||
if (!stream) {
|
||||
print_last_error("tcp accept finish failed");
|
||||
return -1;
|
||||
}
|
||||
printf("tcp accepted %s:%u -> %s:%u, stream=%llu\n",
|
||||
peer_ip,
|
||||
peer_port,
|
||||
local_ip,
|
||||
local_port,
|
||||
(unsigned long long)stream);
|
||||
free_string(local_ip);
|
||||
free_string(peer_ip);
|
||||
|
||||
int ret = async_tcp_read_once(stream, 10000);
|
||||
if (ret == 0) {
|
||||
ret = async_tcp_write_all(stream, "pong", 10000);
|
||||
}
|
||||
data_plane_tcp_close(stream);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int run_udp_demo(const char *inst, const char *target) {
|
||||
char ip[128];
|
||||
uint16_t port = 0;
|
||||
if (parse_ip_port(target, ip, sizeof(ip), &port) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t op = data_plane_udp_bind_start(inst, 0, 30000);
|
||||
if (!op) {
|
||||
print_last_error("udp bind start failed");
|
||||
return -1;
|
||||
}
|
||||
if (wait_or_cancel(op, 31000, "udp bind") == DATA_PLANE_OP_INVALID) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *local_ip = NULL;
|
||||
uint16_t local_port = 0;
|
||||
uint64_t socket = data_plane_udp_bind_finish(op, &local_ip, &local_port);
|
||||
if (!socket) {
|
||||
print_last_error("udp bind finish failed");
|
||||
return -1;
|
||||
}
|
||||
printf("udp bound on %s:%u, handle=%llu\n",
|
||||
local_ip,
|
||||
local_port,
|
||||
(unsigned long long)socket);
|
||||
free_string(local_ip);
|
||||
|
||||
const char payload[] = "ping";
|
||||
op = data_plane_udp_send_to_start(
|
||||
socket,
|
||||
ip,
|
||||
port,
|
||||
(const uint8_t *)payload,
|
||||
(uint32_t)strlen(payload),
|
||||
10000);
|
||||
if (!op) {
|
||||
print_last_error("udp send start failed");
|
||||
data_plane_udp_close(socket);
|
||||
return -1;
|
||||
}
|
||||
if (wait_or_cancel(op, 11000, "udp send") == DATA_PLANE_OP_INVALID) {
|
||||
data_plane_udp_close(socket);
|
||||
return -1;
|
||||
}
|
||||
int sent = data_plane_udp_send_to_finish(op);
|
||||
if (sent < 0) {
|
||||
print_last_error("udp send finish failed");
|
||||
data_plane_udp_close(socket);
|
||||
return -1;
|
||||
}
|
||||
printf("udp sent %d bytes to %s:%u\n", sent, ip, port);
|
||||
|
||||
op = data_plane_udp_recv_from_start(socket, 512, 30000);
|
||||
if (!op) {
|
||||
print_last_error("udp recv start failed");
|
||||
data_plane_udp_close(socket);
|
||||
return -1;
|
||||
}
|
||||
if (wait_or_cancel(op, 31000, "udp recv") == DATA_PLANE_OP_INVALID) {
|
||||
data_plane_udp_close(socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const uint8_t *buf = NULL;
|
||||
uint32_t len = 0;
|
||||
const char *peer_ip = NULL;
|
||||
uint16_t peer_port = 0;
|
||||
int ret = data_plane_udp_recv_from_finish(op, &buf, &len, &peer_ip, &peer_port);
|
||||
if (ret < 0) {
|
||||
print_last_error("udp recv finish failed");
|
||||
data_plane_udp_close(socket);
|
||||
return -1;
|
||||
}
|
||||
printf("udp received %d bytes from %s:%u: %.*s\n",
|
||||
ret,
|
||||
peer_ip,
|
||||
peer_port,
|
||||
ret,
|
||||
buf ? (const char *)buf : "");
|
||||
data_plane_free_bytes(buf, len);
|
||||
free_string(peer_ip);
|
||||
data_plane_udp_close(socket);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void print_usage(void) {
|
||||
printf("Set EASYTIER_FFI_CONFIG and EASYTIER_FFI_INSTANCE to run the async data-plane demo.\n");
|
||||
printf("Optional demos:\n");
|
||||
printf(" EASYTIER_FFI_TARGET=10.0.0.2:22 async TCP connect/read\n");
|
||||
printf(" EASYTIER_FFI_LISTEN_PORT=12345 async TCP bind/accept/read/write\n");
|
||||
printf(" EASYTIER_FFI_UDP_TARGET=10.0.0.2:9000 async UDP bind/send_to/recv_from\n");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const char *config = getenv("EASYTIER_FFI_CONFIG");
|
||||
const char *instance = getenv("EASYTIER_FFI_INSTANCE");
|
||||
if (!config || !instance) {
|
||||
print_usage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (run_network_instance(config) != 0) {
|
||||
print_last_error("run_network_instance failed");
|
||||
return 1;
|
||||
}
|
||||
printf("network instance started: %s\n", instance);
|
||||
|
||||
int failed = 0;
|
||||
const char *target = getenv("EASYTIER_FFI_TARGET");
|
||||
if (target) {
|
||||
failed |= run_tcp_connect_demo(instance, target) != 0;
|
||||
}
|
||||
|
||||
const char *listen_port = getenv("EASYTIER_FFI_LISTEN_PORT");
|
||||
if (listen_port) {
|
||||
failed |= run_tcp_listen_demo(instance, listen_port) != 0;
|
||||
}
|
||||
|
||||
const char *udp_target = getenv("EASYTIER_FFI_UDP_TARGET");
|
||||
if (udp_target) {
|
||||
failed |= run_udp_demo(instance, udp_target) != 0;
|
||||
}
|
||||
|
||||
if (!target && !listen_port && !udp_target) {
|
||||
printf("No dataplane demo env var was set; nothing else to run.\n");
|
||||
print_usage();
|
||||
}
|
||||
|
||||
return failed ? 1 : 0;
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h> // for sleep
|
||||
|
||||
// FFI struct and function declarations
|
||||
typedef struct {
|
||||
const char* key;
|
||||
const char* value;
|
||||
} KeyValuePair;
|
||||
|
||||
typedef void (*config_server_event_callback)(
|
||||
const char* event_json,
|
||||
void* user_data
|
||||
);
|
||||
|
||||
extern int parse_config(const char* cfg_str);
|
||||
extern int run_network_instance(const char* cfg_str);
|
||||
extern void get_error_msg(const char** out);
|
||||
extern void free_string(const char* s);
|
||||
extern int collect_network_infos(KeyValuePair* infos, size_t max_length);
|
||||
extern int start_config_server_client(
|
||||
const char* config_server_url,
|
||||
const char* hostname,
|
||||
const char* machine_id,
|
||||
bool secure_mode,
|
||||
config_server_event_callback callback,
|
||||
void* user_data
|
||||
);
|
||||
extern int stop_config_server_client(void);
|
||||
extern int is_config_server_client_connected(void);
|
||||
|
||||
static void on_config_server_event(const char* event_json, void* user_data) {
|
||||
(void)user_data;
|
||||
printf("config server event: %s\n", event_json);
|
||||
}
|
||||
|
||||
int main() {
|
||||
const char* config = "inst_name = \"test\"\nnetwork = \"test_network\"\n";
|
||||
int ret;
|
||||
|
||||
// 调用 parse_config
|
||||
ret = parse_config(config);
|
||||
if (ret != 0) {
|
||||
const char* err = NULL;
|
||||
get_error_msg(&err);
|
||||
if (err) {
|
||||
printf("parse_config error: %s\n", err);
|
||||
free_string(err);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
printf("parse_config success\n");
|
||||
|
||||
// 调用 run_network_instance
|
||||
ret = run_network_instance(config);
|
||||
if (ret != 0) {
|
||||
const char* err = NULL;
|
||||
get_error_msg(&err);
|
||||
if (err) {
|
||||
printf("run_network_instance error: %s\n", err);
|
||||
free_string(err);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
printf("run_network_instance success\n");
|
||||
|
||||
// 周期性调用 collect_network_infos 并打印
|
||||
const size_t max_infos = 8;
|
||||
KeyValuePair* infos = (KeyValuePair*)malloc(sizeof(KeyValuePair) * max_infos);
|
||||
if (!infos) {
|
||||
fprintf(stderr, "malloc failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; ++i) { // 循环5次作为示例
|
||||
memset(infos, 0, sizeof(KeyValuePair) * max_infos);
|
||||
int count = collect_network_infos(infos, max_infos);
|
||||
if (count < 0) {
|
||||
const char* err = NULL;
|
||||
get_error_msg(&err);
|
||||
if (err) {
|
||||
printf("collect_network_infos error: %s\n", err);
|
||||
free_string(err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
printf("collect_network_infos: %d instance(s)\n", count);
|
||||
for (int j = 0; j < count; ++j) {
|
||||
printf(" [%d] key: %s\n value: %s\n", j, infos[j].key, infos[j].value);
|
||||
free_string(infos[j].key);
|
||||
free_string(infos[j].value);
|
||||
}
|
||||
sleep(1);
|
||||
}
|
||||
free(infos);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
# 1. Go FFI Demo
|
||||
|
||||
This demo wraps EasyTier FFI data-plane TCP as Go `net.Conn` and `net.Listener`.
|
||||
It can connect to an SSH server through EasyTier and read its banner, or accept a
|
||||
TCP connection from another EasyTier peer and run a small ping/pong exchange.
|
||||
The async op-handle wrapper is in `easytier_async.go`; the original synchronous
|
||||
wrapper stays in `easytier.go`.
|
||||
|
||||
## 1.1. Build the FFI library
|
||||
|
||||
Run from the repository root:
|
||||
|
||||
```sh
|
||||
cargo build -p easytier-ffi --features ffi-dataplane
|
||||
```
|
||||
|
||||
The demo loads the debug library by default:
|
||||
|
||||
```text
|
||||
target/debug/libeasytier_ffi.so
|
||||
```
|
||||
|
||||
To use another library path, export `EASYTIER_FFI_LIB=/path/to/libeasytier_ffi.so`.
|
||||
|
||||
## 1.2. Configure the EasyTier config
|
||||
|
||||
`EASYTIER_FFI_CONFIG` is a string of the EasyTier config in TOML format which is passed to the FFI library. For example:
|
||||
|
||||
```sh
|
||||
export EASYTIER_FFI_CONFIG='instance_name = "default"
|
||||
ipv4 = "10.0.0.1"
|
||||
|
||||
[network_identity]
|
||||
network_name = "testnet"
|
||||
network_secret = "mysecret"
|
||||
|
||||
[flags]
|
||||
no_tun = true # disable tun device to avoid permission issues.
|
||||
bind_device = false # allow loopback peers in local examples.
|
||||
|
||||
[[peer]]
|
||||
uri = "tcp://123.123.123.123:11010"
|
||||
'
|
||||
```
|
||||
|
||||
You should configure with your own real values.
|
||||
|
||||
Set the local instance name and a SSH server target to connect through EasyTier:
|
||||
|
||||
```sh
|
||||
export EASYTIER_FFI_INSTANCE=default
|
||||
export EASYTIER_FFI_TARGET=10.0.0.2:22
|
||||
```
|
||||
|
||||
To run the TCP listen integration test in the same `go test` process as the SSH
|
||||
test, use a separate instance name and config:
|
||||
|
||||
```sh
|
||||
export EASYTIER_FFI_LISTEN_CONFIG='instance_name = "listener"
|
||||
ipv4 = "10.0.0.3"
|
||||
|
||||
[network_identity]
|
||||
network_name = "testnet"
|
||||
network_secret = "mysecret"
|
||||
|
||||
[flags]
|
||||
no_tun = true
|
||||
bind_device = false
|
||||
|
||||
[[peer]]
|
||||
uri = "tcp://123.123.123.123:11010"
|
||||
'
|
||||
export EASYTIER_FFI_LISTEN_INSTANCE=listener
|
||||
export EASYTIER_FFI_LISTEN_PORT=12345
|
||||
```
|
||||
|
||||
## 1.3. Run the demo
|
||||
|
||||
`goffi` is built without cgo on Linux, so run the tests with `CGO_ENABLED=0`:
|
||||
|
||||
```sh
|
||||
cd easytier-contrib/easytier-ffi/examples/go
|
||||
CGO_ENABLED=0 go test -v ./...
|
||||
```
|
||||
|
||||
The synchronous tests use the environment variables above. The async Go tests
|
||||
are self-contained: they start two local EasyTier instances in the same test
|
||||
process with `no_tun = true` and `bind_device = false`, then run TCP and UDP
|
||||
ping/pong over the async data-plane API.
|
||||
|
||||
The synchronous wrapper also exposes `CallJSONRPC(service, method, domain,
|
||||
payload)` for non-lifecycle EasyTier RPCs. For example,
|
||||
`CallJSONRPC("api.logger.LoggerRpcService", "get_logger_config", "", "{}")`
|
||||
returns the logger config as protobuf JSON. Instance lifecycle management RPCs
|
||||
are intentionally filtered; use the dedicated FFI APIs for starting and
|
||||
stopping instances.
|
||||
|
||||
To run only the async tests:
|
||||
|
||||
```sh
|
||||
cd easytier-contrib/easytier-ffi/examples/go
|
||||
CGO_ENABLED=0 go test -run 'TestAsync' -v ./...
|
||||
```
|
||||
|
||||
When the SSH integration environment variables are set, expected synchronous
|
||||
test output includes an SSH banner similar to:
|
||||
|
||||
```text
|
||||
attempt 1: got banner "SSH-2.0-..."
|
||||
PASS
|
||||
```
|
||||
|
||||
For `TestTCPListenIntegration`, connect from another EasyTier peer to the local
|
||||
EasyTier IPv4 address and `EASYTIER_FFI_LISTEN_PORT`, send `ping`, and expect
|
||||
`pong` in response.
|
||||
|
||||
The async test output should include local TCP bind/connect log lines and finish
|
||||
with `PASS` without any extra environment variables.
|
||||
|
||||
## 1.4. C async example
|
||||
|
||||
The C async example is kept separate from the basic C example:
|
||||
|
||||
```sh
|
||||
cargo build -p easytier-ffi --features ffi-dataplane
|
||||
cc -Wall -Wextra -pedantic \
|
||||
../example_data_plane_async.c \
|
||||
-L ../../../../target/debug -leasytier_ffi \
|
||||
-Wl,-rpath,../../../../target/debug \
|
||||
-o /tmp/easytier_data_plane_async
|
||||
|
||||
/tmp/easytier_data_plane_async
|
||||
```
|
||||
|
||||
Without environment variables it prints usage and exits successfully. With
|
||||
`EASYTIER_FFI_CONFIG`, `EASYTIER_FFI_INSTANCE`, and one of
|
||||
`EASYTIER_FFI_TARGET`, `EASYTIER_FFI_LISTEN_PORT`, or `EASYTIER_FFI_UDP_TARGET`,
|
||||
it runs the corresponding async data-plane flow.
|
||||
@@ -1,593 +0,0 @@
|
||||
package easytierffi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-webgpu/goffi/ffi"
|
||||
"github.com/go-webgpu/goffi/types"
|
||||
)
|
||||
|
||||
const defaultTimeout = 30 * time.Second
|
||||
|
||||
type Native struct {
|
||||
lib unsafe.Pointer
|
||||
|
||||
runNetworkInstance symCall
|
||||
callJSONRPC symCall
|
||||
getErrorMsg symCall
|
||||
freeString symCall
|
||||
tcpConnect symCall
|
||||
tcpBind symCall
|
||||
tcpAccept symCall
|
||||
tcpRead symCall
|
||||
tcpWrite symCall
|
||||
tcpClose symCall
|
||||
tcpListenerClose symCall
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
native *Native
|
||||
handle uint64
|
||||
local net.Addr
|
||||
remote net.Addr
|
||||
closed atomic.Bool
|
||||
rd atomicDeadline
|
||||
wd atomicDeadline
|
||||
}
|
||||
|
||||
type Listener struct {
|
||||
native *Native
|
||||
handle uint64
|
||||
addr net.Addr
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
type symCall struct {
|
||||
fn unsafe.Pointer
|
||||
cif types.CallInterface
|
||||
}
|
||||
|
||||
type atomicDeadline struct{ v atomic.Int64 }
|
||||
|
||||
type timeoutError string
|
||||
|
||||
func Open(path string) (*Native, error) {
|
||||
lib, err := ffi.LoadLibrary(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n := &Native{lib: lib}
|
||||
if err := n.bind(); err != nil {
|
||||
ffi.FreeLibrary(lib)
|
||||
return nil, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (n *Native) Close() error {
|
||||
if n.lib == nil {
|
||||
return nil
|
||||
}
|
||||
ffi.FreeLibrary(n.lib)
|
||||
n.lib = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Native) RunNetworkInstance(config string) error {
|
||||
defer pinErrorThread()()
|
||||
cfg := cString(config)
|
||||
cfgPtr := unsafe.Pointer(&cfg[0])
|
||||
var ret int32
|
||||
err := n.runNetworkInstance.call(unsafe.Pointer(&ret), unsafe.Pointer(&cfgPtr))
|
||||
runtime.KeepAlive(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ret != 0 {
|
||||
return n.lastError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Native) CallJSONRPC(serviceName, methodName, domainName, payloadJSON string) (string, error) {
|
||||
defer pinErrorThread()()
|
||||
service := cString(serviceName)
|
||||
method := cString(methodName)
|
||||
payload := cString(payloadJSON)
|
||||
servicePtr := unsafe.Pointer(&service[0])
|
||||
methodPtr := unsafe.Pointer(&method[0])
|
||||
payloadPtr := unsafe.Pointer(&payload[0])
|
||||
var domain []byte
|
||||
var domainPtr unsafe.Pointer
|
||||
if domainName != "" {
|
||||
domain = cString(domainName)
|
||||
domainPtr = unsafe.Pointer(&domain[0])
|
||||
}
|
||||
var response unsafe.Pointer
|
||||
responseArg := unsafe.Pointer(&response)
|
||||
var ret int32
|
||||
err := n.callJSONRPC.call(
|
||||
unsafe.Pointer(&ret),
|
||||
unsafe.Pointer(&servicePtr),
|
||||
unsafe.Pointer(&methodPtr),
|
||||
unsafe.Pointer(&domainPtr),
|
||||
unsafe.Pointer(&payloadPtr),
|
||||
unsafe.Pointer(&responseArg),
|
||||
)
|
||||
runtime.KeepAlive(service)
|
||||
runtime.KeepAlive(method)
|
||||
runtime.KeepAlive(domain)
|
||||
runtime.KeepAlive(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ret != 0 {
|
||||
return "", n.lastError()
|
||||
}
|
||||
if response == nil {
|
||||
return "", errors.New("easytier ffi JSON RPC returned nil response")
|
||||
}
|
||||
defer func() { _ = n.freeCString(response) }()
|
||||
return readCString(response), nil
|
||||
}
|
||||
|
||||
func (n *Native) DialContext(ctx context.Context, instance, network, address string) (net.Conn, error) {
|
||||
if network != "tcp" && network != "tcp4" && network != "tcp6" {
|
||||
return nil, net.UnknownNetworkError(network)
|
||||
}
|
||||
ip, port, err := parseIPPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
timeout := defaultTimeout
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
timeout = time.Until(deadline)
|
||||
}
|
||||
if timeout <= 0 {
|
||||
return nil, context.DeadlineExceeded
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handle, local, err := n.tcpConnectTo(instance, ip.String(), uint16(port), timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Conn{native: n, handle: handle, local: local, remote: &net.TCPAddr{IP: ip, Port: port}}, nil
|
||||
}
|
||||
|
||||
func (n *Native) ListenContext(ctx context.Context, instance, network, address string) (net.Listener, error) {
|
||||
if network != "tcp" && network != "tcp4" && network != "tcp6" {
|
||||
return nil, net.UnknownNetworkError(network)
|
||||
}
|
||||
port, err := parseListenPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
timeout := defaultTimeout
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
timeout = time.Until(deadline)
|
||||
}
|
||||
if timeout <= 0 {
|
||||
return nil, context.DeadlineExceeded
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handle, local, err := n.tcpBindTo(instance, uint16(port), timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Listener{native: n, handle: handle, addr: local}, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Read(b []byte) (int, error) {
|
||||
if c.closed.Load() {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
n, err := c.native.tcpReadFrom(c.handle, b, c.rd.timeout(defaultTimeout))
|
||||
if err != nil {
|
||||
return 0, opError("read", c.remote, err)
|
||||
}
|
||||
if n == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Write(b []byte) (int, error) {
|
||||
if c.closed.Load() {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
n, err := c.native.tcpWriteTo(c.handle, b, c.wd.timeout(defaultTimeout))
|
||||
if err != nil {
|
||||
return 0, opError("write", c.remote, err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
if !c.closed.CompareAndSwap(false, true) {
|
||||
return net.ErrClosed
|
||||
}
|
||||
return c.native.tcpCloseHandle(c.handle)
|
||||
}
|
||||
|
||||
func (c *Conn) LocalAddr() net.Addr { return c.local }
|
||||
func (c *Conn) RemoteAddr() net.Addr { return c.remote }
|
||||
func (c *Conn) SetDeadline(t time.Time) error { c.rd.set(t); c.wd.set(t); return nil }
|
||||
func (c *Conn) SetReadDeadline(t time.Time) error { c.rd.set(t); return nil }
|
||||
func (c *Conn) SetWriteDeadline(t time.Time) error { c.wd.set(t); return nil }
|
||||
|
||||
func (l *Listener) Accept() (net.Conn, error) {
|
||||
if l.closed.Load() {
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
for {
|
||||
handle, local, peer, err := l.native.tcpAcceptFrom(l.handle, defaultTimeout)
|
||||
if err == nil {
|
||||
return &Conn{native: l.native, handle: handle, local: local, remote: peer}, nil
|
||||
}
|
||||
if l.closed.Load() {
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
continue
|
||||
}
|
||||
return nil, opError("accept", l.addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) Close() error {
|
||||
if !l.closed.CompareAndSwap(false, true) {
|
||||
return net.ErrClosed
|
||||
}
|
||||
return l.native.tcpListenerCloseHandle(l.handle)
|
||||
}
|
||||
|
||||
func (l *Listener) Addr() net.Addr { return l.addr }
|
||||
|
||||
func (n *Native) bind() error {
|
||||
return errors.Join(
|
||||
n.bindSym(&n.runNetworkInstance, "run_network_instance", types.SInt32TypeDescriptor, types.PointerTypeDescriptor),
|
||||
n.bindSym(&n.callJSONRPC, "call_json_rpc", types.SInt32TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor),
|
||||
n.bindSym(&n.getErrorMsg, "get_error_msg", types.VoidTypeDescriptor, types.PointerTypeDescriptor),
|
||||
n.bindSym(&n.freeString, "free_string", types.VoidTypeDescriptor, types.PointerTypeDescriptor),
|
||||
n.bindSym(&n.tcpConnect, "data_plane_tcp_connect", types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.UInt16TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor),
|
||||
n.bindSym(&n.tcpBind, "data_plane_tcp_bind", types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.UInt16TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor),
|
||||
n.bindSym(&n.tcpAccept, "data_plane_tcp_accept", types.UInt64TypeDescriptor, types.UInt64TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor),
|
||||
n.bindSym(&n.tcpRead, "data_plane_tcp_read", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.UInt32TypeDescriptor, types.UInt64TypeDescriptor),
|
||||
n.bindSym(&n.tcpWrite, "data_plane_tcp_write", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.UInt32TypeDescriptor, types.UInt64TypeDescriptor),
|
||||
n.bindSym(&n.tcpClose, "data_plane_tcp_close", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor),
|
||||
n.bindSym(&n.tcpListenerClose, "data_plane_tcp_listener_close", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor),
|
||||
)
|
||||
}
|
||||
|
||||
func (n *Native) bindSym(dst *symCall, name string, ret *types.TypeDescriptor, args ...*types.TypeDescriptor) error {
|
||||
sym, err := ffi.GetSymbol(n.lib, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ffi.PrepareCallInterface(&dst.cif, types.DefaultCall, ret, args); err != nil {
|
||||
return err
|
||||
}
|
||||
dst.fn = sym
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *symCall) call(ret unsafe.Pointer, args ...unsafe.Pointer) error {
|
||||
// `ffi.CallFunction` and libffi `ffi_call` are safe to invoke concurrently
|
||||
// because `cif` is prepared once during binding and only read afterwards.
|
||||
return ffi.CallFunction(&s.cif, s.fn, ret, args)
|
||||
}
|
||||
|
||||
func (n *Native) tcpConnectTo(instance, ip string, port uint16, timeout time.Duration) (uint64, *net.TCPAddr, error) {
|
||||
defer pinErrorThread()()
|
||||
inst := cString(instance)
|
||||
dst := cString(ip)
|
||||
instPtr := unsafe.Pointer(&inst[0])
|
||||
dstPtr := unsafe.Pointer(&dst[0])
|
||||
timeoutMS := uint64(timeout / time.Millisecond)
|
||||
var handle uint64
|
||||
var outIP unsafe.Pointer
|
||||
outIPArg := unsafe.Pointer(&outIP)
|
||||
var outPort uint16
|
||||
outPortArg := unsafe.Pointer(&outPort)
|
||||
err := n.tcpConnect.call(
|
||||
unsafe.Pointer(&handle),
|
||||
unsafe.Pointer(&instPtr),
|
||||
unsafe.Pointer(&dstPtr),
|
||||
unsafe.Pointer(&port),
|
||||
unsafe.Pointer(&timeoutMS),
|
||||
unsafe.Pointer(&outIPArg),
|
||||
unsafe.Pointer(&outPortArg),
|
||||
)
|
||||
runtime.KeepAlive(inst)
|
||||
runtime.KeepAlive(dst)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if handle == 0 {
|
||||
return 0, nil, n.lastError()
|
||||
}
|
||||
return handle, n.takeTCPAddr(outIP, outPort), nil
|
||||
}
|
||||
|
||||
func (n *Native) tcpBindTo(instance string, port uint16, timeout time.Duration) (uint64, *net.TCPAddr, error) {
|
||||
defer pinErrorThread()()
|
||||
inst := cString(instance)
|
||||
instPtr := unsafe.Pointer(&inst[0])
|
||||
timeoutMS := uint64(timeout / time.Millisecond)
|
||||
var handle uint64
|
||||
var outIP unsafe.Pointer
|
||||
outIPArg := unsafe.Pointer(&outIP)
|
||||
var outPort uint16
|
||||
outPortArg := unsafe.Pointer(&outPort)
|
||||
err := n.tcpBind.call(
|
||||
unsafe.Pointer(&handle),
|
||||
unsafe.Pointer(&instPtr),
|
||||
unsafe.Pointer(&port),
|
||||
unsafe.Pointer(&timeoutMS),
|
||||
unsafe.Pointer(&outIPArg),
|
||||
unsafe.Pointer(&outPortArg),
|
||||
)
|
||||
runtime.KeepAlive(inst)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if handle == 0 {
|
||||
return 0, nil, n.lastError()
|
||||
}
|
||||
return handle, n.takeTCPAddr(outIP, outPort), nil
|
||||
}
|
||||
|
||||
func (n *Native) tcpAcceptFrom(handle uint64, timeout time.Duration) (uint64, *net.TCPAddr, *net.TCPAddr, error) {
|
||||
defer pinErrorThread()()
|
||||
timeoutMS := uint64(timeout / time.Millisecond)
|
||||
var stream uint64
|
||||
var outLocalIP unsafe.Pointer
|
||||
outLocalIPArg := unsafe.Pointer(&outLocalIP)
|
||||
var outLocalPort uint16
|
||||
outLocalPortArg := unsafe.Pointer(&outLocalPort)
|
||||
var outPeerIP unsafe.Pointer
|
||||
outPeerIPArg := unsafe.Pointer(&outPeerIP)
|
||||
var outPeerPort uint16
|
||||
outPeerPortArg := unsafe.Pointer(&outPeerPort)
|
||||
err := n.tcpAccept.call(
|
||||
unsafe.Pointer(&stream),
|
||||
unsafe.Pointer(&handle),
|
||||
unsafe.Pointer(&timeoutMS),
|
||||
unsafe.Pointer(&outLocalIPArg),
|
||||
unsafe.Pointer(&outLocalPortArg),
|
||||
unsafe.Pointer(&outPeerIPArg),
|
||||
unsafe.Pointer(&outPeerPortArg),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
if stream == 0 {
|
||||
return 0, nil, nil, n.lastError()
|
||||
}
|
||||
return stream, n.takeTCPAddr(outLocalIP, outLocalPort), n.takeTCPAddr(outPeerIP, outPeerPort), nil
|
||||
}
|
||||
|
||||
func (n *Native) tcpReadFrom(handle uint64, buf []byte, timeout time.Duration) (int, error) {
|
||||
if len(buf) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
defer pinErrorThread()()
|
||||
var ret int32
|
||||
bufPtr := unsafe.Pointer(&buf[0])
|
||||
length := uint32(len(buf))
|
||||
timeoutMS := uint64(timeout / time.Millisecond)
|
||||
err := n.tcpRead.call(unsafe.Pointer(&ret), unsafe.Pointer(&handle), unsafe.Pointer(&bufPtr), unsafe.Pointer(&length), unsafe.Pointer(&timeoutMS))
|
||||
runtime.KeepAlive(buf)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if ret < 0 {
|
||||
return 0, n.lastError()
|
||||
}
|
||||
return int(ret), nil
|
||||
}
|
||||
|
||||
func (n *Native) tcpWriteTo(handle uint64, buf []byte, timeout time.Duration) (int, error) {
|
||||
if len(buf) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
defer pinErrorThread()()
|
||||
var ret int32
|
||||
bufPtr := unsafe.Pointer(&buf[0])
|
||||
length := uint32(len(buf))
|
||||
timeoutMS := uint64(timeout / time.Millisecond)
|
||||
err := n.tcpWrite.call(unsafe.Pointer(&ret), unsafe.Pointer(&handle), unsafe.Pointer(&bufPtr), unsafe.Pointer(&length), unsafe.Pointer(&timeoutMS))
|
||||
runtime.KeepAlive(buf)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if ret < 0 {
|
||||
return 0, n.lastError()
|
||||
}
|
||||
return int(ret), nil
|
||||
}
|
||||
|
||||
func (n *Native) tcpCloseHandle(handle uint64) error {
|
||||
defer pinErrorThread()()
|
||||
var ret int32
|
||||
if err := n.tcpClose.call(unsafe.Pointer(&ret), unsafe.Pointer(&handle)); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret != 0 {
|
||||
return n.lastError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Native) tcpListenerCloseHandle(handle uint64) error {
|
||||
defer pinErrorThread()()
|
||||
var ret int32
|
||||
if err := n.tcpListenerClose.call(unsafe.Pointer(&ret), unsafe.Pointer(&handle)); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret != 0 {
|
||||
return n.lastError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pinErrorThread ties an FFI op to the get_error_msg that reads its result: the
|
||||
// Rust side stores the last error in a thread-local, so the goroutine must not
|
||||
// migrate to another OS thread between the two calls. Use as `defer pinErrorThread()()`
|
||||
// at the start of any wrapper that reports failures through lastError.
|
||||
func pinErrorThread() func() {
|
||||
runtime.LockOSThread()
|
||||
return runtime.UnlockOSThread
|
||||
}
|
||||
|
||||
func (n *Native) lastError() error {
|
||||
var out unsafe.Pointer
|
||||
outArg := unsafe.Pointer(&out)
|
||||
if err := n.getErrorMsg.call(nil, unsafe.Pointer(&outArg)); err != nil {
|
||||
return err
|
||||
}
|
||||
if out == nil {
|
||||
return errors.New("easytier ffi call failed")
|
||||
}
|
||||
msg := readCString(out)
|
||||
_ = n.freeCString(out)
|
||||
if strings.Contains(msg, "timed out") {
|
||||
return timeoutError(msg)
|
||||
}
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
func (n *Native) freeCString(ptr unsafe.Pointer) error {
|
||||
if ptr == nil {
|
||||
return nil
|
||||
}
|
||||
return n.freeString.call(nil, unsafe.Pointer(&ptr))
|
||||
}
|
||||
|
||||
func (n *Native) takeTCPAddr(ipPtr unsafe.Pointer, port uint16) *net.TCPAddr {
|
||||
if ipPtr == nil {
|
||||
return nil
|
||||
}
|
||||
ip := net.ParseIP(readCString(ipPtr))
|
||||
_ = n.freeCString(ipPtr)
|
||||
return &net.TCPAddr{IP: ip, Port: int(port)}
|
||||
}
|
||||
|
||||
func (d *atomicDeadline) set(t time.Time) {
|
||||
if t.IsZero() {
|
||||
d.v.Store(0)
|
||||
return
|
||||
}
|
||||
d.v.Store(t.UnixNano())
|
||||
}
|
||||
|
||||
func (d *atomicDeadline) timeout(fallback time.Duration) time.Duration {
|
||||
ns := d.v.Load()
|
||||
if ns == 0 {
|
||||
return fallback
|
||||
}
|
||||
remaining := time.Until(time.Unix(0, ns))
|
||||
if remaining <= 0 {
|
||||
return time.Millisecond
|
||||
}
|
||||
return remaining
|
||||
}
|
||||
|
||||
func (e timeoutError) Error() string { return string(e) }
|
||||
func (e timeoutError) Timeout() bool { return true }
|
||||
func (e timeoutError) Temporary() bool { return true }
|
||||
|
||||
func opError(op string, addr net.Addr, err error) error {
|
||||
return &net.OpError{Op: op, Net: "easytier", Addr: addr, Err: err}
|
||||
}
|
||||
|
||||
func parseIPPort(address string) (net.IP, int, error) {
|
||||
host, portStr, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return nil, 0, fmt.Errorf("easytier ffi requires an IP address, got %q", host)
|
||||
}
|
||||
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return ip, int(port), nil
|
||||
}
|
||||
|
||||
func parseListenPort(address string) (int, error) {
|
||||
host, portStr, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if host != "" {
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return 0, fmt.Errorf("easytier ffi requires an IP address, got %q", host)
|
||||
}
|
||||
if !ip.IsUnspecified() {
|
||||
return 0, fmt.Errorf("easytier ffi listen address must be unspecified, got %q", host)
|
||||
}
|
||||
}
|
||||
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(port), nil
|
||||
}
|
||||
|
||||
func cString(s string) []byte {
|
||||
if strings.ContainsRune(s, 0) {
|
||||
panic("easytier ffi string contains NUL")
|
||||
}
|
||||
return append([]byte(s), 0)
|
||||
}
|
||||
|
||||
func readCString(ptr unsafe.Pointer) string {
|
||||
if ptr == nil {
|
||||
return ""
|
||||
}
|
||||
var b []byte
|
||||
for p := uintptr(ptr); ; p++ {
|
||||
c := *(*byte)(unsafe.Pointer(p))
|
||||
if c == 0 {
|
||||
return string(b)
|
||||
}
|
||||
b = append(b, c)
|
||||
}
|
||||
}
|
||||
|
||||
func defaultLibraryPath() string {
|
||||
if p := os.Getenv("EASYTIER_FFI_LIB"); p != "" {
|
||||
return p
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return "../../../../target/debug/libeasytier_ffi.dylib"
|
||||
case "windows":
|
||||
return "..\\..\\..\\..\\target\\debug\\easytier_ffi.dll"
|
||||
default:
|
||||
return "../../../../target/debug/libeasytier_ffi.so"
|
||||
}
|
||||
}
|
||||
|
||||
var _ net.Conn = (*Conn)(nil)
|
||||
var _ net.Listener = (*Listener)(nil)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,360 +0,0 @@
|
||||
package easytierffi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const asyncLocalTestTimeout = 120 * time.Second
|
||||
|
||||
func TestAsyncSymbolBinding(t *testing.T) {
|
||||
n := openAsyncForTest(t)
|
||||
|
||||
status, err := n.opWaitStatus(0, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status != dataPlaneOpInvalid {
|
||||
t.Fatalf("expected invalid status for op 0, got %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsyncLocalTwoNodeTCPAndUDP(t *testing.T) {
|
||||
n := openAsyncForTest(t)
|
||||
topology := startLocalAsyncTopology(t, n)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), asyncLocalTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
runAsyncTCPPingPong(t, ctx, n, topology)
|
||||
runAsyncUDPPingPong(t, ctx, n, topology)
|
||||
}
|
||||
|
||||
type localAsyncTopology struct {
|
||||
dialerInstance string
|
||||
listenerInstance string
|
||||
listenerIP string
|
||||
}
|
||||
|
||||
func openAsyncForTest(t *testing.T) *AsyncNative {
|
||||
t.Helper()
|
||||
|
||||
libraryPath := defaultLibraryPath()
|
||||
if _, err := os.Stat(libraryPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
t.Skipf("build easytier-ffi with ffi-dataplane before running async tests: %v", err)
|
||||
}
|
||||
t.Fatalf("stat async ffi library: %v", err)
|
||||
}
|
||||
|
||||
n, err := OpenAsync(libraryPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open async ffi library: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := n.Close(); err != nil {
|
||||
t.Errorf("close async native: %v", err)
|
||||
}
|
||||
})
|
||||
return n
|
||||
}
|
||||
|
||||
func startLocalAsyncTopology(t *testing.T, n *AsyncNative) localAsyncTopology {
|
||||
t.Helper()
|
||||
|
||||
suffix := strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
networkName := "ffi-async-" + suffix
|
||||
networkSecret := "ffi-async-secret-" + suffix
|
||||
listenerInstance := "ffi-async-listener-" + suffix
|
||||
dialerInstance := "ffi-async-dialer-" + suffix
|
||||
listenerIP := "10.251.1.2"
|
||||
dialerIP := "10.251.1.1"
|
||||
listenerPort := freeLocalTCPPort(t)
|
||||
listenerEndpoint := fmt.Sprintf("tcp://127.0.0.1:%d", listenerPort)
|
||||
t.Cleanup(func() {
|
||||
if err := n.deleteNetworkInstances([]string{dialerInstance, listenerInstance}); err != nil {
|
||||
t.Errorf("cleanup async test EasyTier instances: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
listenerConfig := localAsyncConfig(
|
||||
listenerInstance,
|
||||
listenerIP,
|
||||
networkName,
|
||||
networkSecret,
|
||||
[]string{listenerEndpoint},
|
||||
nil,
|
||||
)
|
||||
dialerConfig := localAsyncConfig(
|
||||
dialerInstance,
|
||||
dialerIP,
|
||||
networkName,
|
||||
networkSecret,
|
||||
nil,
|
||||
[]string{listenerEndpoint},
|
||||
)
|
||||
|
||||
if err := n.RunNetworkInstance(listenerConfig); err != nil {
|
||||
t.Fatalf("start listener instance: %v", err)
|
||||
}
|
||||
if err := n.RunNetworkInstance(dialerConfig); err != nil {
|
||||
t.Fatalf("start dialer instance: %v", err)
|
||||
}
|
||||
|
||||
return localAsyncTopology{
|
||||
dialerInstance: dialerInstance,
|
||||
listenerInstance: listenerInstance,
|
||||
listenerIP: listenerIP,
|
||||
}
|
||||
}
|
||||
|
||||
func localAsyncConfig(instance, ipv4, networkName, networkSecret string, listeners, peers []string) string {
|
||||
config := fmt.Sprintf(`instance_name = %s
|
||||
ipv4 = %s
|
||||
listeners = %s
|
||||
|
||||
[network_identity]
|
||||
network_name = %s
|
||||
network_secret = %s
|
||||
|
||||
[flags]
|
||||
no_tun = true
|
||||
bind_device = false
|
||||
`,
|
||||
strconv.Quote(instance),
|
||||
strconv.Quote(ipv4),
|
||||
tomlStringList(listeners),
|
||||
strconv.Quote(networkName),
|
||||
strconv.Quote(networkSecret),
|
||||
)
|
||||
for _, peer := range peers {
|
||||
config += fmt.Sprintf("\n[[peer]]\nuri = %s\n", strconv.Quote(peer))
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func tomlStringList(values []string) string {
|
||||
if len(values) == 0 {
|
||||
return "[]"
|
||||
}
|
||||
|
||||
out := "["
|
||||
for i, value := range values {
|
||||
if i > 0 {
|
||||
out += ", "
|
||||
}
|
||||
out += strconv.Quote(value)
|
||||
}
|
||||
return out + "]"
|
||||
}
|
||||
|
||||
func freeLocalTCPPort(t *testing.T) int {
|
||||
t.Helper()
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("allocate local tcp port: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
return listener.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
func runAsyncTCPPingPong(t *testing.T, ctx context.Context, n *AsyncNative, topology localAsyncTopology) {
|
||||
t.Helper()
|
||||
|
||||
listener, listenerAddr := eventuallyTCPListen(t, ctx, n, topology.listenerInstance)
|
||||
|
||||
tcpCtx, cancel := context.WithCancel(ctx)
|
||||
accepted := make(chan error, 1)
|
||||
defer waitForAsyncHelper(t, accepted, "tcp accept helper")
|
||||
defer cancel()
|
||||
defer listener.Close()
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
accepted <- fmt.Errorf("accept tcp stream: %w", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||
|
||||
payload := make([]byte, len("ping"))
|
||||
if _, err := io.ReadFull(conn, payload); err != nil {
|
||||
accepted <- fmt.Errorf("read tcp ping: %w", err)
|
||||
return
|
||||
}
|
||||
if string(payload) != "ping" {
|
||||
accepted <- fmt.Errorf("expected tcp ping, got %q", string(payload))
|
||||
return
|
||||
}
|
||||
if _, err := conn.Write([]byte("pong")); err != nil {
|
||||
accepted <- fmt.Errorf("write tcp pong: %w", err)
|
||||
return
|
||||
}
|
||||
accepted <- nil
|
||||
}()
|
||||
|
||||
conn, err := eventuallyTCPDial(t, tcpCtx, n, topology.dialerInstance, topology.listenerIP, listenerAddr.Port)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||
|
||||
if _, err := conn.Write([]byte("ping")); err != nil {
|
||||
t.Fatalf("write tcp ping: %v", err)
|
||||
}
|
||||
payload := make([]byte, len("pong"))
|
||||
if _, err := io.ReadFull(conn, payload); err != nil {
|
||||
t.Fatalf("read tcp pong: %v", err)
|
||||
}
|
||||
if string(payload) != "pong" {
|
||||
t.Fatalf("expected tcp pong, got %q", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func eventuallyTCPListen(t *testing.T, ctx context.Context, n *AsyncNative, instance string) (net.Listener, *net.TCPAddr) {
|
||||
t.Helper()
|
||||
|
||||
var lastErr error
|
||||
for attempt := 1; ctx.Err() == nil; attempt++ {
|
||||
attemptCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
listener, err := n.ListenContext(attemptCtx, instance, "tcp", "0.0.0.0:0")
|
||||
cancel()
|
||||
if err == nil {
|
||||
addr := listener.Addr().(*net.TCPAddr)
|
||||
t.Logf("async tcp bind succeeded on attempt %d at %s", attempt, addr)
|
||||
return listener, addr
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
t.Logf("attempt %d: async tcp bind failed: %v", attempt, err)
|
||||
waitForRetry(ctx, 500*time.Millisecond)
|
||||
}
|
||||
t.Fatalf("async tcp bind never succeeded: %v", lastErr)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func eventuallyTCPDial(t *testing.T, ctx context.Context, n *AsyncNative, instance, ip string, port int) (net.Conn, error) {
|
||||
t.Helper()
|
||||
|
||||
address := net.JoinHostPort(ip, strconv.Itoa(port))
|
||||
var lastErr error
|
||||
for attempt := 1; ctx.Err() == nil; attempt++ {
|
||||
attemptCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
conn, err := n.DialContext(attemptCtx, instance, "tcp", address)
|
||||
cancel()
|
||||
if err == nil {
|
||||
t.Logf("async tcp connect succeeded on attempt %d to %s", attempt, address)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
t.Logf("attempt %d: async tcp connect failed: %v", attempt, err)
|
||||
waitForRetry(ctx, 500*time.Millisecond)
|
||||
}
|
||||
return nil, fmt.Errorf("async tcp connect never succeeded: %w", lastErr)
|
||||
}
|
||||
|
||||
func runAsyncUDPPingPong(t *testing.T, ctx context.Context, n *AsyncNative, topology localAsyncTopology) {
|
||||
t.Helper()
|
||||
|
||||
dialerSocket, err := n.UDPBindContext(ctx, topology.dialerInstance, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("bind dialer udp socket: %v", err)
|
||||
}
|
||||
|
||||
listenerSocket, err := n.UDPBindContext(ctx, topology.listenerInstance, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("bind listener udp socket: %v", err)
|
||||
}
|
||||
|
||||
udpCtx, cancel := context.WithCancel(ctx)
|
||||
warmupDone := make(chan error, 1)
|
||||
received := make(chan error, 1)
|
||||
defer waitForAsyncHelper(t, received, "udp receive helper")
|
||||
defer cancel()
|
||||
defer listenerSocket.Close()
|
||||
defer dialerSocket.Close()
|
||||
|
||||
go func() {
|
||||
if _, err := listenerSocket.SendTo(udpCtx, []byte("warmup"), dialerSocket.LocalAddr()); err != nil {
|
||||
err = fmt.Errorf("send udp warmup: %w", err)
|
||||
warmupDone <- err
|
||||
received <- err
|
||||
return
|
||||
}
|
||||
warmupDone <- nil
|
||||
|
||||
payload, from, err := listenerSocket.RecvFrom(udpCtx, 512)
|
||||
if err != nil {
|
||||
received <- fmt.Errorf("recv udp ping: %w", err)
|
||||
return
|
||||
}
|
||||
if string(payload) != "ping" {
|
||||
received <- fmt.Errorf("expected udp ping, got %q", string(payload))
|
||||
return
|
||||
}
|
||||
if _, err := listenerSocket.SendTo(udpCtx, []byte("pong"), from); err != nil {
|
||||
received <- fmt.Errorf("send udp pong: %w", err)
|
||||
return
|
||||
}
|
||||
received <- nil
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-warmupDone:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-udpCtx.Done():
|
||||
t.Fatal(udpCtx.Err())
|
||||
}
|
||||
|
||||
target := &net.UDPAddr{IP: net.ParseIP(topology.listenerIP), Port: listenerSocket.LocalAddr().Port}
|
||||
if _, err := dialerSocket.SendTo(udpCtx, []byte("ping"), target); err != nil {
|
||||
t.Fatalf("send udp ping: %v", err)
|
||||
}
|
||||
for {
|
||||
payload, from, err := dialerSocket.RecvFrom(udpCtx, 512)
|
||||
if err != nil {
|
||||
t.Fatalf("recv udp pong: %v", err)
|
||||
}
|
||||
if string(payload) == "pong" {
|
||||
if !from.IP.Equal(target.IP) || from.Port != target.Port {
|
||||
t.Fatalf("expected udp pong from %s, got %s", target, from)
|
||||
}
|
||||
break
|
||||
}
|
||||
t.Logf("skipping udp datagram from %s: %q", from, string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func waitForAsyncHelper(t *testing.T, done <-chan error, name string) {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", name, err)
|
||||
}
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Errorf("%s did not stop", name)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForRetry(ctx context.Context, delay time.Duration) {
|
||||
timer := time.NewTimer(delay)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package easytierffi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSSHIntegration(t *testing.T) {
|
||||
config := os.Getenv("EASYTIER_FFI_CONFIG")
|
||||
instance := os.Getenv("EASYTIER_FFI_INSTANCE")
|
||||
target := os.Getenv("EASYTIER_FFI_TARGET")
|
||||
if config == "" || instance == "" || target == "" {
|
||||
t.Skip("set EASYTIER_FFI_CONFIG, EASYTIER_FFI_INSTANCE and EASYTIER_FFI_TARGET to run integration test")
|
||||
}
|
||||
|
||||
n, err := Open(defaultLibraryPath())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer n.Close()
|
||||
|
||||
if err := n.RunNetworkInstance(config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var lastErr error
|
||||
for attempt := 1; ctx.Err() == nil; attempt++ {
|
||||
conn, err := n.DialContext(ctx, instance, "tcp", target)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
t.Logf("attempt %d: dial failed: %v", attempt, err)
|
||||
time.Sleep(3 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
buf := make([]byte, 128)
|
||||
nn, err := conn.Read(buf)
|
||||
_ = conn.Close()
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
t.Logf("attempt %d: read failed: %v", attempt, err)
|
||||
time.Sleep(3 * time.Second)
|
||||
continue
|
||||
}
|
||||
banner := string(buf[:nn])
|
||||
if !strings.HasPrefix(banner, "SSH-") {
|
||||
t.Fatalf("attempt %d: expected SSH banner, got %q", attempt, banner)
|
||||
}
|
||||
t.Logf("attempt %d: got banner %q", attempt, strings.TrimRight(banner, "\r\n"))
|
||||
return
|
||||
}
|
||||
t.Fatalf("never got SSH banner, last err: %v", lastErr)
|
||||
}
|
||||
|
||||
func TestTCPListenIntegration(t *testing.T) {
|
||||
config := os.Getenv("EASYTIER_FFI_LISTEN_CONFIG")
|
||||
instance := os.Getenv("EASYTIER_FFI_LISTEN_INSTANCE")
|
||||
listenPort := os.Getenv("EASYTIER_FFI_LISTEN_PORT")
|
||||
if config == "" || instance == "" || listenPort == "" {
|
||||
t.Skip("set EASYTIER_FFI_LISTEN_CONFIG, EASYTIER_FFI_LISTEN_INSTANCE and EASYTIER_FFI_LISTEN_PORT to run integration test")
|
||||
}
|
||||
port, err := strconv.ParseUint(listenPort, 10, 16)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n, err := Open(defaultLibraryPath())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer n.Close()
|
||||
|
||||
if err := n.RunNetworkInstance(config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Data-plane readiness is asynchronous: the instance must finish starting
|
||||
// before the data plane accepts binds. Retry until ready or ctx expires.
|
||||
var listener net.Listener
|
||||
for attempt := 1; ; attempt++ {
|
||||
listener, err = n.ListenContext(ctx, instance, "tcp", net.JoinHostPort("0.0.0.0", strconv.Itoa(int(port))))
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
t.Fatalf("bind never succeeded, last err: %v", err)
|
||||
}
|
||||
t.Logf("attempt %d: bind failed: %v", attempt, err)
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
t.Logf("listening on %s; connect from another EasyTier peer and send ping", listener.Addr())
|
||||
|
||||
accepted := make(chan error, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
accepted <- err
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
buf := make([]byte, 4)
|
||||
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||
accepted <- err
|
||||
return
|
||||
}
|
||||
if string(buf) != "ping" {
|
||||
accepted <- fmt.Errorf("expected %q, got %q", "ping", string(buf))
|
||||
return
|
||||
}
|
||||
_, err = conn.Write([]byte("pong"))
|
||||
accepted <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-accepted:
|
||||
_ = listener.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
_ = listener.Close()
|
||||
t.Fatal(ctx.Err())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
module easytierffi-example
|
||||
|
||||
go 1.25
|
||||
|
||||
require github.com/go-webgpu/goffi v0.4.1
|
||||
@@ -1,2 +0,0 @@
|
||||
github.com/go-webgpu/goffi v0.4.1 h1:2hQH5XXloxTyTtIleYv+Rajlwzp6UOETURhSZ5+zJxU=
|
||||
github.com/go-webgpu/goffi v0.4.1/go.mod h1:wfoxNsJkU+5RFbV1kNN1kunhc1lFHuJKK3zpgx08/uM=
|
||||
@@ -1,575 +0,0 @@
|
||||
use std::{
|
||||
cell::Cell,
|
||||
collections::HashSet,
|
||||
ffi::{CString, c_char, c_int, c_void},
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use easytier::{
|
||||
common::{
|
||||
MachineIdOptions,
|
||||
config::{ConfigLoader as _, TomlConfigLoader},
|
||||
},
|
||||
tunnel::TunnelScheme,
|
||||
web_client::{WebClient, WebClientHooks, run_web_client},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
data_plane::remove_data_plane_handles_by_instance_ids,
|
||||
error::set_error_msg,
|
||||
state::{
|
||||
ASYNC_RUNTIME, INSTANCE_MANAGER, INSTANCE_MUTATION_LOCK, INSTANCE_NAME_ID_MAP,
|
||||
lock_remote_instance_mutation, remove_instance_name_ids,
|
||||
},
|
||||
strings::{c_str_to_string, optional_c_str_to_string},
|
||||
types::ConfigServerEventCallback,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static IN_CONFIG_SERVER_CALLBACK: Cell<bool> = const { Cell::new(false) };
|
||||
}
|
||||
|
||||
static CONFIG_SERVER_CLIENT: once_cell::sync::Lazy<Mutex<Option<ManagedConfigServerClient>>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(None));
|
||||
static CONFIG_SERVER_CLIENT_ACTIVE: once_cell::sync::Lazy<AtomicBool> =
|
||||
once_cell::sync::Lazy::new(|| AtomicBool::new(false));
|
||||
static CONFIG_SERVER_CLIENT_STOPPING: once_cell::sync::Lazy<AtomicBool> =
|
||||
once_cell::sync::Lazy::new(|| AtomicBool::new(false));
|
||||
static LAST_CONFIG_SERVER_CALLBACK_ERROR: once_cell::sync::Lazy<Mutex<Option<String>>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(None));
|
||||
|
||||
pub(crate) struct ConfigServerCallbackScope;
|
||||
|
||||
impl ConfigServerCallbackScope {
|
||||
pub(crate) fn enter() -> Self {
|
||||
IN_CONFIG_SERVER_CALLBACK.with(|in_callback| in_callback.set(true));
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConfigServerCallbackScope {
|
||||
fn drop(&mut self) {
|
||||
IN_CONFIG_SERVER_CALLBACK.with(|in_callback| in_callback.set(false));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_config_server_callback() -> bool {
|
||||
IN_CONFIG_SERVER_CALLBACK.with(Cell::get)
|
||||
}
|
||||
|
||||
fn config_server_machine_id_options(machine_id: String) -> MachineIdOptions {
|
||||
MachineIdOptions {
|
||||
explicit_machine_id: Some(machine_id),
|
||||
state_dir: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_config_server_client_options(
|
||||
config_server_url_s: &str,
|
||||
machine_id: &str,
|
||||
) -> Result<(), String> {
|
||||
if machine_id.trim().is_empty() {
|
||||
return Err("machine_id is empty".to_string());
|
||||
}
|
||||
|
||||
let config_server_url = match url::Url::parse(config_server_url_s) {
|
||||
Ok(url) => url,
|
||||
Err(_) => format!(
|
||||
"udp://config-server.easytier.cn:22020/{}",
|
||||
config_server_url_s
|
||||
)
|
||||
.parse()
|
||||
.map_err(|err| format!("failed to parse config server URL: {}", err))?,
|
||||
};
|
||||
|
||||
TunnelScheme::try_from(&config_server_url).map_err(|_| {
|
||||
format!(
|
||||
"unsupported config server scheme: {}",
|
||||
config_server_url.scheme()
|
||||
)
|
||||
})?;
|
||||
|
||||
let token = config_server_url
|
||||
.path_segments()
|
||||
.and_then(|mut segments| segments.next_back())
|
||||
.map(|segment| percent_encoding::percent_decode_str(segment).decode_utf8())
|
||||
.transpose()
|
||||
.map_err(|err| format!("failed to decode config server token: {}", err))?
|
||||
.map(|token| token.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
if token.is_empty() {
|
||||
return Err("empty token".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct ManagedConfigServerClient {
|
||||
client: WebClient,
|
||||
hooks: Arc<ManagedConfigServerClientHooks>,
|
||||
}
|
||||
|
||||
pub(crate) struct ManagedConfigServerClientHooks {
|
||||
pub(crate) instance_ids: Mutex<HashSet<Uuid>>,
|
||||
callback_delivery: Mutex<()>,
|
||||
stopping: AtomicBool,
|
||||
callback: ConfigServerEventCallback,
|
||||
user_data: usize,
|
||||
}
|
||||
|
||||
impl ManagedConfigServerClientHooks {
|
||||
pub(crate) fn new(callback: ConfigServerEventCallback, user_data: *mut c_void) -> Self {
|
||||
Self {
|
||||
instance_ids: Mutex::new(HashSet::new()),
|
||||
callback_delivery: Mutex::new(()),
|
||||
stopping: AtomicBool::new(false),
|
||||
callback,
|
||||
user_data: user_data as usize,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn tracked_instance_ids(&self) -> Vec<Uuid> {
|
||||
self.instance_ids
|
||||
.lock()
|
||||
.map(|guard| guard.iter().copied().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn remove_tracked_instance_ids(&self, ids: &[Uuid]) -> Result<Vec<Uuid>, String> {
|
||||
let mut guard = self.instance_ids.lock().map_err(|err| err.to_string())?;
|
||||
Ok(ids
|
||||
.iter()
|
||||
.filter_map(|id| guard.remove(id).then_some(*id))
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn validate_instance_name(&self, inst_name: &str, inst_id: Uuid) -> Result<(), String> {
|
||||
if let Some(existing_id) = INSTANCE_NAME_ID_MAP.get(inst_name).map(|id| *id)
|
||||
&& existing_id != inst_id
|
||||
{
|
||||
return Err(format!("instance name {} already exists", inst_name));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn commit_instance_name(&self, inst_name: String, inst_id: Uuid) -> Result<(), String> {
|
||||
INSTANCE_NAME_ID_MAP.retain(|_, existing_id| *existing_id != inst_id);
|
||||
self.validate_instance_name(&inst_name, inst_id)?;
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name, inst_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn start_stopping(&self) -> Vec<Uuid> {
|
||||
let _delivery_guard = if in_config_server_callback() {
|
||||
None
|
||||
} else {
|
||||
self.callback_delivery.lock().ok()
|
||||
};
|
||||
let mut guard = match self.instance_ids.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
self.stopping.store(true, Ordering::Release);
|
||||
guard.drain().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn note_callback_error(&self, error: String) {
|
||||
log::warn!("config server event callback failed: {}", error);
|
||||
if let Ok(mut guard) = LAST_CONFIG_SERVER_CALLBACK_ERROR.lock() {
|
||||
*guard = Some(error);
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_event_with_delivery_locked(
|
||||
&self,
|
||||
event: &str,
|
||||
instance_id: Uuid,
|
||||
) -> Result<(), String> {
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(callback) = self.callback else {
|
||||
return Ok(());
|
||||
};
|
||||
let instance_name = INSTANCE_MANAGER
|
||||
.get_instance_name(&instance_id)
|
||||
.unwrap_or_default();
|
||||
let network_name = INSTANCE_MANAGER
|
||||
.get_network_name(&instance_id)
|
||||
.unwrap_or_default();
|
||||
let event_json = serde_json::json!({
|
||||
"event": event,
|
||||
"success": true,
|
||||
"instance_id": instance_id.to_string(),
|
||||
"instance_name": instance_name,
|
||||
"network_name": network_name,
|
||||
"error": null,
|
||||
})
|
||||
.to_string();
|
||||
let event_json = CString::new(event_json).map_err(|err| err.to_string())?;
|
||||
let _callback_scope = ConfigServerCallbackScope::enter();
|
||||
unsafe {
|
||||
callback(event_json.as_ptr(), self.user_data as *mut c_void);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_event(&self, event: &str, instance_id: Uuid) -> Result<(), String> {
|
||||
let _delivery_guard = self
|
||||
.callback_delivery
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?;
|
||||
self.emit_event_with_delivery_locked(event, instance_id)
|
||||
}
|
||||
|
||||
fn wait_for_callback_delivery(&self) {
|
||||
if in_config_server_callback() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(guard) = self.callback_delivery.lock() {
|
||||
drop(guard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl WebClientHooks for ManagedConfigServerClientHooks {
|
||||
fn manages_remote_config_instances(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn pre_run_network_instance(&self, cfg: &TomlConfigLoader) -> Result<(), String> {
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
return Err("config server client is stopping".to_string());
|
||||
}
|
||||
|
||||
let inst_name = cfg.get_inst_name();
|
||||
let inst_id = cfg.get_id();
|
||||
|
||||
self.validate_instance_name(&inst_name, inst_id)
|
||||
}
|
||||
|
||||
async fn post_run_network_instance(&self, id: &Uuid) -> Result<(), String> {
|
||||
let _delivery_guard = self
|
||||
.callback_delivery
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?;
|
||||
let Some(inst_name) = INSTANCE_MANAGER.get_instance_name(id) else {
|
||||
if !self.stopping.load(Ordering::Acquire) {
|
||||
return Err(format!("instance {} not found after start", id));
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
{
|
||||
let _mutation_guard = INSTANCE_MUTATION_LOCK
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?;
|
||||
if INSTANCE_MANAGER.get_instance_name(id).is_none() {
|
||||
if !self.stopping.load(Ordering::Acquire) {
|
||||
return Err(format!("instance {} not found after start", id));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let should_delete = {
|
||||
let mut guard = self.instance_ids.lock().map_err(|err| err.to_string())?;
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
true
|
||||
} else {
|
||||
guard.insert(*id);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if should_delete {
|
||||
if let Err(err) = INSTANCE_MANAGER.delete_network_instance(vec![*id]) {
|
||||
return Err(err.to_string());
|
||||
}
|
||||
remove_instance_name_ids(&[*id]);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
self.remove_tracked_instance_ids(&[*id])?;
|
||||
remove_instance_name_ids(&[*id]);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Err(err) = self.commit_instance_name(inst_name.clone(), *id) {
|
||||
self.remove_tracked_instance_ids(&[*id])?;
|
||||
if let Err(delete_err) = INSTANCE_MANAGER.delete_network_instance(vec![*id]) {
|
||||
return Err(format!(
|
||||
"{}; failed to delete duplicate instance: {}",
|
||||
err, delete_err
|
||||
));
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
self.remove_tracked_instance_ids(&[*id])?;
|
||||
remove_instance_name_ids(&[*id]);
|
||||
return Ok(());
|
||||
}
|
||||
if INSTANCE_MANAGER.get_instance_name(id).is_none() {
|
||||
self.remove_tracked_instance_ids(&[*id])?;
|
||||
remove_instance_name_ids(&[*id]);
|
||||
return Err(format!(
|
||||
"instance {} was removed before post-run completed",
|
||||
id
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
remove_data_plane_handles_by_instance_ids(&[*id]);
|
||||
|
||||
if let Err(err) = self.emit_event_with_delivery_locked("run_network_instance", *id) {
|
||||
self.note_callback_error(err);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn post_remove_network_instances(&self, ids: &[Uuid]) -> Result<(), String> {
|
||||
let removed_ids = {
|
||||
let _mutation_guard = INSTANCE_MUTATION_LOCK
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?;
|
||||
let removed_ids = self.remove_tracked_instance_ids(ids)?;
|
||||
remove_instance_name_ids(ids);
|
||||
remove_data_plane_handles_by_instance_ids(&removed_ids);
|
||||
removed_ids
|
||||
};
|
||||
|
||||
for id in removed_ids {
|
||||
if let Err(err) = self.emit_event("delete_network_instance", id) {
|
||||
self.note_callback_error(err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_config_server_tracked_instance_ids(ids: &[Uuid]) {
|
||||
if ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(guard) = CONFIG_SERVER_CLIENT.lock()
|
||||
&& let Some(managed) = guard.as_ref()
|
||||
&& let Err(err) = managed.hooks.remove_tracked_instance_ids(ids)
|
||||
{
|
||||
log::warn!("failed to remove config server tracked ids: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wait_for_config_server_delivery() {
|
||||
let hooks = CONFIG_SERVER_CLIENT
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.as_ref().map(|managed| managed.hooks.clone()));
|
||||
if let Some(hooks) = hooks {
|
||||
hooks.wait_for_callback_delivery();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn last_callback_error() -> Option<String> {
|
||||
LAST_CONFIG_SERVER_CALLBACK_ERROR
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn clear_last_callback_error() {
|
||||
if let Ok(mut guard) = LAST_CONFIG_SERVER_CALLBACK_ERROR.lock() {
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn is_config_server_active_or_stopping() -> bool {
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.load(Ordering::Acquire)
|
||||
|| CONFIG_SERVER_CLIENT_STOPPING.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_active_for_test(active: bool) {
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(active, Ordering::Release);
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Start the config server client.
|
||||
///
|
||||
/// `config_server_url` must be a valid null-terminated UTF-8 string.
|
||||
/// `hostname` may be null; if non-null it must be a valid null-terminated UTF-8 string.
|
||||
/// `machine_id` must be a valid null-terminated UTF-8 string.
|
||||
/// `event_json` passed to `callback` is valid only during that callback invocation.
|
||||
pub(crate) unsafe fn start_config_server_client(
|
||||
config_server_url: *const c_char,
|
||||
hostname: *const c_char,
|
||||
machine_id: *const c_char,
|
||||
secure_mode: bool,
|
||||
callback: ConfigServerEventCallback,
|
||||
user_data: *mut c_void,
|
||||
) -> c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot start config server client from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let config_server_url = match unsafe { c_str_to_string(config_server_url, "config_server_url") }
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let hostname = match unsafe { optional_c_str_to_string(hostname, "hostname") } {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let machine_id = match unsafe { c_str_to_string(machine_id, "machine_id") } {
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
Ok(value) => value,
|
||||
};
|
||||
if let Err(err) = validate_config_server_client_options(&config_server_url, &machine_id) {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
let mut guard = match CONFIG_SERVER_CLIENT.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock config server client: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if guard.is_some() {
|
||||
set_error_msg("config server client already exists");
|
||||
return -1;
|
||||
}
|
||||
if CONFIG_SERVER_CLIENT_STOPPING.load(Ordering::Acquire) {
|
||||
set_error_msg("config server client is stopping");
|
||||
return -1;
|
||||
}
|
||||
clear_last_callback_error();
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
let data_plane_usage_guard = match crate::data_plane::lock_for_config_server_start() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(true, Ordering::Release);
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
drop(data_plane_usage_guard);
|
||||
|
||||
let hooks = Arc::new(ManagedConfigServerClientHooks::new(callback, user_data));
|
||||
let client = match ASYNC_RUNTIME.block_on(run_web_client(
|
||||
&config_server_url,
|
||||
config_server_machine_id_options(machine_id),
|
||||
hostname,
|
||||
secure_mode,
|
||||
INSTANCE_MANAGER.clone(),
|
||||
Some(hooks.clone()),
|
||||
)) {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||
set_error_msg(&format!("failed to start config server client: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
*guard = Some(ManagedConfigServerClient { client, hooks });
|
||||
0
|
||||
}
|
||||
|
||||
pub(crate) fn stop_config_server_client() -> c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot stop config server client from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let mut guard = match CONFIG_SERVER_CLIENT.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock config server client: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(managed) = guard.as_ref() else {
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||
return 0;
|
||||
};
|
||||
if CONFIG_SERVER_CLIENT_STOPPING.swap(true, Ordering::AcqRel) {
|
||||
set_error_msg("config server client is stopping");
|
||||
return -1;
|
||||
}
|
||||
let hooks = managed.hooks.clone();
|
||||
let managed = guard.take().expect("config server client exists");
|
||||
drop(guard);
|
||||
|
||||
let _remote_mutation_guard = lock_remote_instance_mutation();
|
||||
let tracked_ids = hooks.start_stopping();
|
||||
drop(managed);
|
||||
|
||||
let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
hooks.wait_for_callback_delivery();
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||
CONFIG_SERVER_CLIENT_STOPPING.store(false, Ordering::Release);
|
||||
set_error_msg(&format!("failed to lock instance mutation: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let delete_result = INSTANCE_MANAGER.delete_network_instance(tracked_ids.clone());
|
||||
if delete_result.is_ok() {
|
||||
remove_instance_name_ids(&tracked_ids);
|
||||
remove_data_plane_handles_by_instance_ids(&tracked_ids);
|
||||
}
|
||||
drop(_mutation_guard);
|
||||
hooks.wait_for_callback_delivery();
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||
CONFIG_SERVER_CLIENT_STOPPING.store(false, Ordering::Release);
|
||||
|
||||
if let Err(err) = delete_result {
|
||||
set_error_msg(&format!(
|
||||
"failed to delete config server instances: {}",
|
||||
err
|
||||
));
|
||||
return -1;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
pub(crate) fn is_config_server_client_connected() -> c_int {
|
||||
CONFIG_SERVER_CLIENT
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.as_ref().map(|managed| managed.client.is_connected()))
|
||||
.map(i32::from)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
@@ -1,928 +0,0 @@
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use std::{
|
||||
future::Future,
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::{
|
||||
Arc, RwLock,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use dashmap::DashMap;
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use easytier::launcher::{DataPlaneTcpListener, DataPlaneTcpStream, DataPlaneUdpSocket};
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf};
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use tokio_util::sync::CancellationToken;
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use crate::{
|
||||
config_server::{in_config_server_callback, is_config_server_active_or_stopping},
|
||||
error::{free_string, set_error_msg},
|
||||
state::{INSTANCE_MANAGER, INSTANCE_NAME_ID_MAP},
|
||||
};
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
static NEXT_DATA_PLANE_HANDLE: AtomicU64 = AtomicU64::new(1);
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
static DATA_PLANE_HANDLES: once_cell::sync::Lazy<DashMap<u64, DataPlaneHandle>> =
|
||||
once_cell::sync::Lazy::new(DashMap::new);
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
static DATA_PLANE_USAGE_LOCK: once_cell::sync::Lazy<RwLock<()>> =
|
||||
once_cell::sync::Lazy::new(|| RwLock::new(()));
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) struct DataPlaneHandle {
|
||||
pub(crate) instance_id: uuid::Uuid,
|
||||
pub(crate) runtime: tokio::runtime::Handle,
|
||||
// Cancelled by close() to wake any in-flight op on this handle.
|
||||
pub(crate) close_token: CancellationToken,
|
||||
pub(crate) resource: DataPlaneResource,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) struct TcpHalves {
|
||||
pub(crate) read: tokio::sync::Mutex<ReadHalf<DataPlaneTcpStream>>,
|
||||
pub(crate) write: tokio::sync::Mutex<WriteHalf<DataPlaneTcpStream>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) enum DataPlaneResource {
|
||||
Tcp(Arc<TcpHalves>),
|
||||
TcpListener(Arc<tokio::sync::Mutex<DataPlaneTcpListener>>),
|
||||
Udp(Arc<DataPlaneUdpSocket>),
|
||||
}
|
||||
|
||||
// Several helper functions for FFI data plane operations to facilitate logic reuse.
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn next_handle() -> u64 {
|
||||
NEXT_DATA_PLANE_HANDLE.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn timeout_duration(timeout_ms: u64) -> Duration {
|
||||
Duration::from_millis(timeout_ms)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn cstr_to_string(ptr: *const std::ffi::c_char, name: &str) -> Option<String> {
|
||||
if ptr.is_null() {
|
||||
set_error_msg(&format!("{} is null", name));
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn get_instance_id(inst_name: &str) -> Option<uuid::Uuid> {
|
||||
INSTANCE_NAME_ID_MAP.get(inst_name).map(|id| *id.value())
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn parse_socket_addr(host: &str, port: u16) -> Option<SocketAddr> {
|
||||
let ip = match host.parse::<IpAddr>() {
|
||||
Ok(ip) => ip,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to parse ip address: {}", e));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(SocketAddr::new(ip, port))
|
||||
}
|
||||
|
||||
/// Encode an IP address for FFI return. Returns `*mut c_char` to match
|
||||
/// `CString::into_raw`; caller releases it via `free_string`.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn into_ffi_ip_cstring(ip: IpAddr) -> Option<*mut std::ffi::c_char> {
|
||||
match std::ffi::CString::new(ip.to_string()) {
|
||||
Ok(s) => Some(s.into_raw()),
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to encode ip: {}", e));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn get_runtime_handle(
|
||||
inst_id: &uuid::Uuid,
|
||||
deadline: std::time::Instant,
|
||||
) -> Option<tokio::runtime::Handle> {
|
||||
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||
let Some(rt) = INSTANCE_MANAGER.data_plane_wait_runtime_handle(inst_id, remaining) else {
|
||||
set_error_msg("instance runtime is not ready");
|
||||
return None;
|
||||
};
|
||||
Some(rt)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn insert_tcp_stream_handle(
|
||||
instance_id: uuid::Uuid,
|
||||
runtime: tokio::runtime::Handle,
|
||||
stream: DataPlaneTcpStream,
|
||||
) -> u64 {
|
||||
let (rd, wr) = tokio::io::split(stream);
|
||||
let handle = next_handle();
|
||||
DATA_PLANE_HANDLES.insert(
|
||||
handle,
|
||||
DataPlaneHandle {
|
||||
instance_id,
|
||||
runtime,
|
||||
close_token: CancellationToken::new(),
|
||||
resource: DataPlaneResource::Tcp(Arc::new(TcpHalves {
|
||||
read: tokio::sync::Mutex::new(rd),
|
||||
write: tokio::sync::Mutex::new(wr),
|
||||
})),
|
||||
},
|
||||
);
|
||||
handle
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn insert_tcp_listener_handle(
|
||||
instance_id: uuid::Uuid,
|
||||
runtime: tokio::runtime::Handle,
|
||||
listener: DataPlaneTcpListener,
|
||||
) -> u64 {
|
||||
let handle = next_handle();
|
||||
DATA_PLANE_HANDLES.insert(
|
||||
handle,
|
||||
DataPlaneHandle {
|
||||
instance_id,
|
||||
runtime,
|
||||
close_token: CancellationToken::new(),
|
||||
resource: DataPlaneResource::TcpListener(Arc::new(tokio::sync::Mutex::new(listener))),
|
||||
},
|
||||
);
|
||||
handle
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn insert_udp_socket_handle(
|
||||
instance_id: uuid::Uuid,
|
||||
runtime: tokio::runtime::Handle,
|
||||
socket: DataPlaneUdpSocket,
|
||||
) -> u64 {
|
||||
let handle = next_handle();
|
||||
DATA_PLANE_HANDLES.insert(
|
||||
handle,
|
||||
DataPlaneHandle {
|
||||
instance_id,
|
||||
runtime,
|
||||
close_token: CancellationToken::new(),
|
||||
resource: DataPlaneResource::Udp(Arc::new(socket)),
|
||||
},
|
||||
);
|
||||
handle
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn get_tcp_stream(
|
||||
handle: u64,
|
||||
) -> Option<(Arc<TcpHalves>, tokio::runtime::Handle, CancellationToken)> {
|
||||
get_tcp_stream_with_instance(handle)
|
||||
.map(|(halves, runtime, close_token, _)| (halves, runtime, close_token))
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn get_tcp_stream_with_instance(
|
||||
handle: u64,
|
||||
) -> Option<(
|
||||
Arc<TcpHalves>,
|
||||
tokio::runtime::Handle,
|
||||
CancellationToken,
|
||||
uuid::Uuid,
|
||||
)> {
|
||||
let Some(h) = DATA_PLANE_HANDLES.get(&handle) else {
|
||||
set_error_msg("tcp stream handle not found");
|
||||
return None;
|
||||
};
|
||||
match &h.resource {
|
||||
DataPlaneResource::Tcp(halves) => Some((
|
||||
halves.clone(),
|
||||
h.runtime.clone(),
|
||||
h.close_token.clone(),
|
||||
h.instance_id,
|
||||
)),
|
||||
DataPlaneResource::TcpListener(_) | DataPlaneResource::Udp(_) => {
|
||||
set_error_msg("handle is not a tcp stream");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn get_tcp_listener(
|
||||
handle: u64,
|
||||
) -> Option<(
|
||||
Arc<tokio::sync::Mutex<DataPlaneTcpListener>>,
|
||||
tokio::runtime::Handle,
|
||||
CancellationToken,
|
||||
uuid::Uuid,
|
||||
)> {
|
||||
let Some(h) = DATA_PLANE_HANDLES.get(&handle) else {
|
||||
set_error_msg("tcp listener handle not found");
|
||||
return None;
|
||||
};
|
||||
match &h.resource {
|
||||
DataPlaneResource::TcpListener(listener) => Some((
|
||||
listener.clone(),
|
||||
h.runtime.clone(),
|
||||
h.close_token.clone(),
|
||||
h.instance_id,
|
||||
)),
|
||||
DataPlaneResource::Tcp(_) | DataPlaneResource::Udp(_) => {
|
||||
set_error_msg("handle is not a tcp listener");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn get_udp_socket(
|
||||
handle: u64,
|
||||
) -> Option<(
|
||||
Arc<DataPlaneUdpSocket>,
|
||||
tokio::runtime::Handle,
|
||||
CancellationToken,
|
||||
)> {
|
||||
get_udp_socket_with_instance(handle)
|
||||
.map(|(socket, runtime, close_token, _)| (socket, runtime, close_token))
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn get_udp_socket_with_instance(
|
||||
handle: u64,
|
||||
) -> Option<(
|
||||
Arc<DataPlaneUdpSocket>,
|
||||
tokio::runtime::Handle,
|
||||
CancellationToken,
|
||||
uuid::Uuid,
|
||||
)> {
|
||||
let Some(h) = DATA_PLANE_HANDLES.get(&handle) else {
|
||||
set_error_msg("udp socket handle not found");
|
||||
return None;
|
||||
};
|
||||
match &h.resource {
|
||||
DataPlaneResource::Udp(socket) => Some((
|
||||
socket.clone(),
|
||||
h.runtime.clone(),
|
||||
h.close_token.clone(),
|
||||
h.instance_id,
|
||||
)),
|
||||
DataPlaneResource::Tcp(_) | DataPlaneResource::TcpListener(_) => {
|
||||
set_error_msg("handle is not a udp socket");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn remove_data_plane_handles_by_instance_ids(ids: &[Uuid]) {
|
||||
if ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let _data_plane_usage_guard = DATA_PLANE_USAGE_LOCK
|
||||
.write()
|
||||
.unwrap_or_else(|err| err.into_inner());
|
||||
|
||||
DATA_PLANE_HANDLES.retain(|_, handle| {
|
||||
if ids.contains(&handle.instance_id) {
|
||||
handle.close_token.cancel();
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
crate::data_plane_async::remove_ops_by_instance_ids(ids);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ffi-dataplane"))]
|
||||
pub(crate) fn remove_data_plane_handles_by_instance_ids(_ids: &[uuid::Uuid]) {}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn data_plane_rejected() -> bool {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot use data plane from config server callback");
|
||||
true
|
||||
} else if is_config_server_active_or_stopping() {
|
||||
set_error_msg("cannot use data plane while config server client is active");
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn enter_data_plane_operation() -> Option<std::sync::RwLockReadGuard<'static, ()>> {
|
||||
if data_plane_rejected() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let guard = match DATA_PLANE_USAGE_LOCK.read() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock data plane usage: {}", err));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if data_plane_rejected() {
|
||||
return None;
|
||||
}
|
||||
Some(guard)
|
||||
}
|
||||
|
||||
/// Run an IO op on the resource's owning runtime, supporting
|
||||
/// timeout and cancellation.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
async fn run_with_cancel<T, F>(
|
||||
close_token: &CancellationToken,
|
||||
timeout_ms: u64,
|
||||
error_prefix: &str,
|
||||
op: F,
|
||||
) -> Option<Result<T, std::io::Error>>
|
||||
where
|
||||
F: Future<Output = Result<T, std::io::Error>>,
|
||||
{
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = close_token.cancelled() => {
|
||||
set_error_msg(&format!("{}: handle closed", error_prefix));
|
||||
None
|
||||
}
|
||||
res = tokio::time::timeout(timeout_duration(timeout_ms), op) => match res {
|
||||
Ok(r) => Some(r),
|
||||
Err(_) => {
|
||||
set_error_msg(&format!("{} timed out", error_prefix));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn lock_for_config_server_start()
|
||||
-> Result<std::sync::RwLockWriteGuard<'static, ()>, String> {
|
||||
let guard = DATA_PLANE_USAGE_LOCK
|
||||
.write()
|
||||
.map_err(|err| format!("failed to lock data plane usage: {}", err))?;
|
||||
if !DATA_PLANE_HANDLES.is_empty() || crate::data_plane_async::has_live_ops() {
|
||||
return Err("cannot start config server client while data plane is in use".to_string());
|
||||
}
|
||||
Ok(guard)
|
||||
}
|
||||
/// # Safety
|
||||
/// Open a TCP stream through an EasyTier instance data plane. Returns 0 on
|
||||
/// failure. On success, writes the local socket address chosen for this
|
||||
/// connection into `out_local_ip` (a heap-allocated C string the caller must
|
||||
/// release via `free_string`) and `out_local_port`. Both out pointers must be
|
||||
/// non-null.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_tcp_connect(
|
||||
inst_name: *const std::ffi::c_char,
|
||||
dst_ip: *const std::ffi::c_char,
|
||||
dst_port: std::ffi::c_ushort,
|
||||
timeout_ms: u64,
|
||||
out_local_ip: *mut *const std::ffi::c_char,
|
||||
out_local_port: *mut std::ffi::c_ushort,
|
||||
) -> u64 {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return 0,
|
||||
};
|
||||
if out_local_ip.is_null() || out_local_port.is_null() {
|
||||
set_error_msg("output pointer is null");
|
||||
return 0;
|
||||
}
|
||||
let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else {
|
||||
return 0;
|
||||
};
|
||||
let Some(dst_ip) = (unsafe { cstr_to_string(dst_ip, "dst_ip") }) else {
|
||||
return 0;
|
||||
};
|
||||
let Some(inst_id) = get_instance_id(&inst_name) else {
|
||||
set_error_msg("instance not found");
|
||||
return 0;
|
||||
};
|
||||
let Some(dst_addr) = parse_socket_addr(&dst_ip, dst_port) else {
|
||||
return 0;
|
||||
};
|
||||
let deadline = std::time::Instant::now() + timeout_duration(timeout_ms);
|
||||
let Some(runtime) = get_runtime_handle(&inst_id, deadline) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||
let result =
|
||||
runtime.block_on(INSTANCE_MANAGER.data_plane_tcp_connect(&inst_id, dst_addr, remaining));
|
||||
match result {
|
||||
Ok(stream) => {
|
||||
let local_addr = stream.local_addr();
|
||||
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
|
||||
return 0;
|
||||
};
|
||||
let handle = insert_tcp_stream_handle(inst_id, runtime, stream);
|
||||
unsafe {
|
||||
*out_local_ip = local_ip as *const std::ffi::c_char;
|
||||
*out_local_port = local_addr.port();
|
||||
}
|
||||
handle
|
||||
}
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to connect tcp data plane: {}", e));
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Bind a TCP listener through an EasyTier instance data plane. Returns 0 on
|
||||
/// failure. The local address actually bound is written into `out_local_ip` /
|
||||
/// `out_local_port`; the caller must release `*out_local_ip` via `free_string`.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_tcp_bind(
|
||||
inst_name: *const std::ffi::c_char,
|
||||
local_port: std::ffi::c_ushort,
|
||||
timeout_ms: u64,
|
||||
out_local_ip: *mut *const std::ffi::c_char,
|
||||
out_local_port: *mut std::ffi::c_ushort,
|
||||
) -> u64 {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return 0,
|
||||
};
|
||||
if out_local_ip.is_null() || out_local_port.is_null() {
|
||||
set_error_msg("output pointer is null");
|
||||
return 0;
|
||||
}
|
||||
let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else {
|
||||
return 0;
|
||||
};
|
||||
let Some(inst_id) = get_instance_id(&inst_name) else {
|
||||
set_error_msg("instance not found");
|
||||
return 0;
|
||||
};
|
||||
let deadline = std::time::Instant::now() + timeout_duration(timeout_ms);
|
||||
let Some(runtime) = get_runtime_handle(&inst_id, deadline) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||
let result =
|
||||
runtime.block_on(INSTANCE_MANAGER.data_plane_tcp_bind(&inst_id, local_port, remaining));
|
||||
match result {
|
||||
Ok(listener) => {
|
||||
let local_addr = listener.local_addr();
|
||||
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
|
||||
return 0;
|
||||
};
|
||||
let handle = insert_tcp_listener_handle(inst_id, runtime, listener);
|
||||
unsafe {
|
||||
*out_local_ip = local_ip as *const std::ffi::c_char;
|
||||
*out_local_port = local_addr.port();
|
||||
}
|
||||
handle
|
||||
}
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to bind tcp data plane: {}", e));
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Accept one connection from a TCP data-plane listener. Returns a TCP stream
|
||||
/// handle, or 0 on failure. Local and peer addresses are written into out
|
||||
/// parameters; returned IP strings must be released via `free_string`.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_tcp_accept(
|
||||
handle: u64,
|
||||
timeout_ms: u64,
|
||||
out_local_ip: *mut *const std::ffi::c_char,
|
||||
out_local_port: *mut std::ffi::c_ushort,
|
||||
out_peer_ip: *mut *const std::ffi::c_char,
|
||||
out_peer_port: *mut std::ffi::c_ushort,
|
||||
) -> u64 {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return 0,
|
||||
};
|
||||
if out_local_ip.is_null()
|
||||
|| out_local_port.is_null()
|
||||
|| out_peer_ip.is_null()
|
||||
|| out_peer_port.is_null()
|
||||
{
|
||||
set_error_msg("output pointer is null");
|
||||
return 0;
|
||||
}
|
||||
let Some((listener, runtime, close_token, instance_id)) = get_tcp_listener(handle) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let ret = runtime.block_on(async move {
|
||||
let mut listener = listener.lock().await;
|
||||
run_with_cancel(
|
||||
&close_token,
|
||||
timeout_ms,
|
||||
"tcp data plane accept",
|
||||
listener.accept(),
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
||||
match ret {
|
||||
Some(Ok((stream, peer_addr))) => {
|
||||
let local_addr = stream.local_addr();
|
||||
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
|
||||
return 0;
|
||||
};
|
||||
let Some(peer_ip) = into_ffi_ip_cstring(peer_addr.ip()) else {
|
||||
free_string(local_ip);
|
||||
return 0;
|
||||
};
|
||||
let stream_handle = insert_tcp_stream_handle(instance_id, runtime, stream);
|
||||
unsafe {
|
||||
*out_local_ip = local_ip as *const std::ffi::c_char;
|
||||
*out_local_port = local_addr.port();
|
||||
*out_peer_ip = peer_ip as *const std::ffi::c_char;
|
||||
*out_peer_port = peer_addr.port();
|
||||
}
|
||||
stream_handle
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
set_error_msg(&format!("failed to accept tcp data plane: {}", e));
|
||||
0
|
||||
}
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Read from a TCP data-plane stream.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_tcp_read(
|
||||
handle: u64,
|
||||
buf: *mut std::ffi::c_uchar,
|
||||
len: u32,
|
||||
timeout_ms: u64,
|
||||
) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
if buf.is_null() {
|
||||
set_error_msg("buf is null");
|
||||
return -1;
|
||||
}
|
||||
let Some((halves, runtime, close_token)) = get_tcp_stream(handle) else {
|
||||
return -1;
|
||||
};
|
||||
// Safety: caller-owned buffer outlives this blocking call.
|
||||
let buf = unsafe { std::slice::from_raw_parts_mut(buf, len as usize) };
|
||||
runtime.block_on(async move {
|
||||
let mut rd = halves.read.lock().await;
|
||||
match run_with_cancel(
|
||||
&close_token,
|
||||
timeout_ms,
|
||||
"failed to read tcp data plane",
|
||||
rd.read(buf),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(Ok(n)) => n as std::ffi::c_int,
|
||||
Some(Err(e)) => {
|
||||
set_error_msg(&format!("failed to read tcp data plane: {}", e));
|
||||
-1
|
||||
}
|
||||
None => -1,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Write to a TCP data-plane stream.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_tcp_write(
|
||||
handle: u64,
|
||||
buf: *const std::ffi::c_uchar,
|
||||
len: u32,
|
||||
timeout_ms: u64,
|
||||
) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
if buf.is_null() {
|
||||
set_error_msg("buf is null");
|
||||
return -1;
|
||||
}
|
||||
let Some((halves, runtime, close_token)) = get_tcp_stream(handle) else {
|
||||
return -1;
|
||||
};
|
||||
let total = len as usize;
|
||||
// Safety: caller-owned buffer outlives this blocking call.
|
||||
let buf = unsafe { std::slice::from_raw_parts(buf, total) };
|
||||
runtime.block_on(async move {
|
||||
let mut wr = halves.write.lock().await;
|
||||
// Use `write_all` to honor `net.Conn::Write` semantics on the Go side
|
||||
// (must write everything or return an error); single `write()` can
|
||||
// silently short-write and corrupt streams that the caller assumes are
|
||||
// fully written.
|
||||
match run_with_cancel(
|
||||
&close_token,
|
||||
timeout_ms,
|
||||
"failed to write tcp data plane",
|
||||
wr.write_all(buf),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(Ok(())) => total as std::ffi::c_int,
|
||||
Some(Err(e)) => {
|
||||
set_error_msg(&format!("failed to write tcp data plane: {}", e));
|
||||
-1
|
||||
}
|
||||
None => -1,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn data_plane_tcp_close(handle: u64) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
crate::data_plane_async::cancel_ops_for_handle(handle);
|
||||
let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| {
|
||||
matches!(e.resource, DataPlaneResource::Tcp(_))
|
||||
}) else {
|
||||
set_error_msg(if DATA_PLANE_HANDLES.contains_key(&handle) {
|
||||
"handle is not a tcp stream"
|
||||
} else {
|
||||
"tcp stream handle not found"
|
||||
});
|
||||
return -1;
|
||||
};
|
||||
h.close_token.cancel();
|
||||
if let DataPlaneResource::Tcp(halves) = h.resource {
|
||||
// Best-effort half-close; if write half is in use, the in-flight call
|
||||
// observes the cancel token and releases the lock shortly after.
|
||||
h.runtime.spawn(async move {
|
||||
if let Ok(mut wr) = halves.write.try_lock() {
|
||||
let _ = wr.shutdown().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn data_plane_tcp_listener_close(handle: u64) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
crate::data_plane_async::cancel_ops_for_handle(handle);
|
||||
let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| {
|
||||
matches!(e.resource, DataPlaneResource::TcpListener(_))
|
||||
}) else {
|
||||
set_error_msg(if DATA_PLANE_HANDLES.contains_key(&handle) {
|
||||
"handle is not a tcp listener"
|
||||
} else {
|
||||
"tcp listener handle not found"
|
||||
});
|
||||
return -1;
|
||||
};
|
||||
h.close_token.cancel();
|
||||
0
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Bind a UDP socket through an EasyTier instance data plane. Returns 0 on
|
||||
/// failure. The local address actually bound (which may differ from the
|
||||
/// requested port when `local_port == 0`) is written into `out_local_ip` /
|
||||
/// `out_local_port`; the caller must release `*out_local_ip` via `free_string`.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_udp_bind(
|
||||
inst_name: *const std::ffi::c_char,
|
||||
local_port: std::ffi::c_ushort,
|
||||
timeout_ms: u64,
|
||||
out_local_ip: *mut *const std::ffi::c_char,
|
||||
out_local_port: *mut std::ffi::c_ushort,
|
||||
) -> u64 {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return 0,
|
||||
};
|
||||
if out_local_ip.is_null() || out_local_port.is_null() {
|
||||
set_error_msg("output pointer is null");
|
||||
return 0;
|
||||
}
|
||||
let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else {
|
||||
return 0;
|
||||
};
|
||||
let Some(inst_id) = get_instance_id(&inst_name) else {
|
||||
set_error_msg("instance not found");
|
||||
return 0;
|
||||
};
|
||||
let deadline = std::time::Instant::now() + timeout_duration(timeout_ms);
|
||||
let Some(runtime) = get_runtime_handle(&inst_id, deadline) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||
let result =
|
||||
runtime.block_on(INSTANCE_MANAGER.data_plane_udp_bind(&inst_id, local_port, remaining));
|
||||
match result {
|
||||
Ok(socket) => {
|
||||
let local_addr = socket.local_addr();
|
||||
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
|
||||
return 0;
|
||||
};
|
||||
let handle = insert_udp_socket_handle(inst_id, runtime, socket);
|
||||
unsafe {
|
||||
*out_local_ip = local_ip as *const std::ffi::c_char;
|
||||
*out_local_port = local_addr.port();
|
||||
}
|
||||
handle
|
||||
}
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to bind udp data plane: {}", e));
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Send a datagram through a UDP data-plane socket.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_udp_send_to(
|
||||
handle: u64,
|
||||
dst_ip: *const std::ffi::c_char,
|
||||
dst_port: std::ffi::c_ushort,
|
||||
buf: *const std::ffi::c_uchar,
|
||||
len: u32,
|
||||
timeout_ms: u64,
|
||||
) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
if buf.is_null() {
|
||||
set_error_msg("buf is null");
|
||||
return -1;
|
||||
}
|
||||
let Some(dst_ip) = (unsafe { cstr_to_string(dst_ip, "dst_ip") }) else {
|
||||
return -1;
|
||||
};
|
||||
let Some(dst_addr) = parse_socket_addr(&dst_ip, dst_port) else {
|
||||
return -1;
|
||||
};
|
||||
let Some((socket, runtime, close_token)) = get_udp_socket(handle) else {
|
||||
return -1;
|
||||
};
|
||||
let total = len as usize;
|
||||
// Safety: caller-owned buffer outlives this blocking call.
|
||||
let buf = unsafe { std::slice::from_raw_parts(buf, total) };
|
||||
runtime.block_on(async move {
|
||||
match run_with_cancel(
|
||||
&close_token,
|
||||
timeout_ms,
|
||||
"failed to send udp data plane",
|
||||
socket.send_to(buf, dst_addr),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(Ok(n)) => n as std::ffi::c_int,
|
||||
Some(Err(e)) => {
|
||||
set_error_msg(&format!("failed to send udp data plane: {}", e));
|
||||
-1
|
||||
}
|
||||
None => -1,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Receive a datagram from a UDP data-plane socket.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_udp_recv_from(
|
||||
handle: u64,
|
||||
buf: *mut std::ffi::c_uchar,
|
||||
len: u32,
|
||||
out_ip: *mut *const std::ffi::c_char,
|
||||
out_port: *mut std::ffi::c_ushort,
|
||||
timeout_ms: u64,
|
||||
) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
if buf.is_null() || out_ip.is_null() || out_port.is_null() {
|
||||
set_error_msg("output pointer is null");
|
||||
return -1;
|
||||
}
|
||||
let Some((socket, runtime, close_token)) = get_udp_socket(handle) else {
|
||||
return -1;
|
||||
};
|
||||
let total = len as usize;
|
||||
// Safety: caller-owned buffer outlives this blocking call.
|
||||
let buf = unsafe { std::slice::from_raw_parts_mut(buf, total) };
|
||||
let ret = runtime.block_on(run_with_cancel(
|
||||
&close_token,
|
||||
timeout_ms,
|
||||
"udp data plane receive",
|
||||
socket.recv_from(buf),
|
||||
));
|
||||
|
||||
match ret {
|
||||
Some(Ok((n, addr))) => {
|
||||
// The returned ip pointer must be released by the caller via
|
||||
// `free_string` (which calls `CString::from_raw`, matching
|
||||
// `CString::into_raw` here).
|
||||
let Some(ip_cstr) = into_ffi_ip_cstring(addr.ip()) else {
|
||||
return -1;
|
||||
};
|
||||
unsafe {
|
||||
*out_ip = ip_cstr as *const std::ffi::c_char;
|
||||
*out_port = addr.port() as std::ffi::c_ushort;
|
||||
}
|
||||
n as std::ffi::c_int
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
set_error_msg(&format!("failed to receive udp data plane: {}", e));
|
||||
-1
|
||||
}
|
||||
None => -1,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn data_plane_udp_close(handle: u64) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
crate::data_plane_async::cancel_ops_for_handle(handle);
|
||||
let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| {
|
||||
matches!(e.resource, DataPlaneResource::Udp(_))
|
||||
}) else {
|
||||
set_error_msg(if DATA_PLANE_HANDLES.contains_key(&handle) {
|
||||
"handle is not a udp socket"
|
||||
} else {
|
||||
"udp socket handle not found"
|
||||
});
|
||||
return -1;
|
||||
};
|
||||
h.close_token.cancel();
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "ffi-dataplane"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::{sync::mpsc, time::Duration};
|
||||
|
||||
#[test]
|
||||
fn config_server_start_waits_for_data_plane_operation() {
|
||||
let read_guard = DATA_PLANE_USAGE_LOCK.read().unwrap();
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
let waiter = std::thread::spawn(move || {
|
||||
let _write_guard = lock_for_config_server_start().unwrap();
|
||||
done_tx.send(()).unwrap();
|
||||
});
|
||||
|
||||
assert!(done_rx.recv_timeout(Duration::from_millis(100)).is_err());
|
||||
drop(read_guard);
|
||||
done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
waiter.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instance_cleanup_waits_for_data_plane_operation() {
|
||||
let read_guard = DATA_PLANE_USAGE_LOCK.read().unwrap();
|
||||
let instance_id = Uuid::new_v4();
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
let cleaner = std::thread::spawn(move || {
|
||||
remove_data_plane_handles_by_instance_ids(&[instance_id]);
|
||||
done_tx.send(()).unwrap();
|
||||
});
|
||||
|
||||
assert!(done_rx.recv_timeout(Duration::from_millis(100)).is_err());
|
||||
drop(read_guard);
|
||||
done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
cleaner.join().unwrap();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,65 +0,0 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ffi::{CString, c_char},
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
// # Thread Safety
|
||||
// set_error_msg and get_error_msg must be called on the same thread to
|
||||
// get correct error. And since `Handle::block_on` polls the top-level
|
||||
// future on the calling thread, set_error_msg always runs on the same
|
||||
// thread as the corresponding get_error_msg.
|
||||
static ERROR_MSG: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
pub(crate) fn set_error_msg(msg: &str) {
|
||||
ERROR_MSG.with(|cell| {
|
||||
let mut buf = cell.borrow_mut();
|
||||
buf.clear();
|
||||
buf.extend_from_slice(msg.as_bytes());
|
||||
});
|
||||
}
|
||||
|
||||
fn thread_local_error_msg() -> Option<String> {
|
||||
ERROR_MSG.with(|cell| {
|
||||
let buf = cell.borrow();
|
||||
if buf.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(String::from_utf8_lossy(&buf).into_owned())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn get_error_msg(out: *mut *const c_char) {
|
||||
let msg = match (
|
||||
thread_local_error_msg(),
|
||||
crate::config_server::last_callback_error(),
|
||||
) {
|
||||
(Some(error), Some(callback_error)) => Some(format!(
|
||||
"{}; config server callback error: {}",
|
||||
error, callback_error
|
||||
)),
|
||||
(Some(error), None) => Some(error),
|
||||
(None, Some(callback_error)) => {
|
||||
Some(format!("config server callback error: {}", callback_error))
|
||||
}
|
||||
(None, None) => None,
|
||||
};
|
||||
let cstr = msg.and_then(|msg| CString::new(msg).ok());
|
||||
unsafe {
|
||||
*out = match cstr {
|
||||
Some(s) => s.into_raw() as *const c_char,
|
||||
None => std::ptr::null(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn free_string(s: *const c_char) {
|
||||
if s.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
let _ = CString::from_raw(s as *mut c_char);
|
||||
}
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
use std::ffi::{CString, c_char, c_int};
|
||||
|
||||
use easytier::common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader};
|
||||
|
||||
use crate::{
|
||||
config_server::{
|
||||
in_config_server_callback, remove_config_server_tracked_instance_ids,
|
||||
wait_for_config_server_delivery,
|
||||
},
|
||||
data_plane::remove_data_plane_handles_by_instance_ids,
|
||||
error::set_error_msg,
|
||||
state::{
|
||||
INSTANCE_MANAGER, INSTANCE_MUTATION_LOCK, INSTANCE_NAME_ID_MAP, instance_name_exists,
|
||||
lock_remote_instance_mutation,
|
||||
},
|
||||
types::KeyValuePair,
|
||||
};
|
||||
|
||||
/// # Safety
|
||||
/// Set the tun fd
|
||||
pub(crate) unsafe fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_int {
|
||||
let inst_name = unsafe {
|
||||
assert!(!inst_name.is_null());
|
||||
std::ffi::CStr::from_ptr(inst_name)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
};
|
||||
if !INSTANCE_NAME_ID_MAP.contains_key(&inst_name) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let inst_id = *INSTANCE_NAME_ID_MAP
|
||||
.get(&inst_name)
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.value();
|
||||
|
||||
match INSTANCE_MANAGER.set_tun_fd(&inst_id, fd) {
|
||||
Ok(_) => 0,
|
||||
Err(_) => -1,
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Parse the config
|
||||
pub(crate) unsafe fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
||||
let cfg_str = unsafe {
|
||||
assert!(!cfg_str.is_null());
|
||||
std::ffi::CStr::from_ptr(cfg_str)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
};
|
||||
|
||||
if let Err(e) = TomlConfigLoader::new_from_str(&cfg_str) {
|
||||
set_error_msg(&format!("failed to parse config: {:?}", e));
|
||||
return -1;
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Run the network instance
|
||||
pub(crate) unsafe fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot run network instance from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let cfg_str = unsafe {
|
||||
assert!(!cfg_str.is_null());
|
||||
std::ffi::CStr::from_ptr(cfg_str)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
};
|
||||
let cfg = match TomlConfigLoader::new_from_str(&cfg_str) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to parse config: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let inst_name = cfg.get_inst_name();
|
||||
|
||||
wait_for_config_server_delivery();
|
||||
let _remote_mutation_guard = lock_remote_instance_mutation();
|
||||
let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock instance mutation: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if instance_name_exists(&inst_name) {
|
||||
set_error_msg("instance already exists");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let instance_id =
|
||||
match INSTANCE_MANAGER.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to start instance: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name, instance_id);
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
unsafe fn parse_instance_names(
|
||||
inst_names: *const *const c_char,
|
||||
length: usize,
|
||||
) -> Option<Vec<String>> {
|
||||
if length == 0 {
|
||||
return Some(Vec::new());
|
||||
}
|
||||
if inst_names.is_null() {
|
||||
set_error_msg("inst_names is null");
|
||||
return None;
|
||||
}
|
||||
|
||||
let names = unsafe { std::slice::from_raw_parts(inst_names, length) };
|
||||
let mut parsed = Vec::with_capacity(length);
|
||||
for (index, &name) in names.iter().enumerate() {
|
||||
if name.is_null() {
|
||||
set_error_msg(&format!("inst_names[{}] is null", index));
|
||||
return None;
|
||||
}
|
||||
parsed.push(
|
||||
unsafe { std::ffi::CStr::from_ptr(name) }
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
);
|
||||
}
|
||||
Some(parsed)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Retain the network instance
|
||||
pub(crate) unsafe fn retain_network_instance(
|
||||
inst_names: *const *const std::ffi::c_char,
|
||||
length: usize,
|
||||
) -> std::ffi::c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot retain network instances from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
wait_for_config_server_delivery();
|
||||
let _remote_mutation_guard = lock_remote_instance_mutation();
|
||||
let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock instance mutation: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if length == 0 {
|
||||
let removed_ids = INSTANCE_MANAGER.list_network_instance_ids();
|
||||
if let Err(e) = INSTANCE_MANAGER.delete_network_instance(removed_ids.clone()) {
|
||||
set_error_msg(&format!("failed to delete instances: {}", e));
|
||||
return -1;
|
||||
}
|
||||
remove_config_server_tracked_instance_ids(&removed_ids);
|
||||
remove_data_plane_handles_by_instance_ids(&removed_ids);
|
||||
INSTANCE_NAME_ID_MAP.clear();
|
||||
return 0;
|
||||
}
|
||||
|
||||
let Some(inst_names) = (unsafe { parse_instance_names(inst_names, length) }) else {
|
||||
return -1;
|
||||
};
|
||||
|
||||
let removed_ids = INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.into_iter()
|
||||
.filter(|id| {
|
||||
INSTANCE_MANAGER
|
||||
.get_instance_name(id)
|
||||
.is_none_or(|name| !inst_names.contains(&name))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Err(e) = INSTANCE_MANAGER.delete_network_instance(removed_ids.clone()) {
|
||||
set_error_msg(&format!("failed to delete instances: {}", e));
|
||||
return -1;
|
||||
}
|
||||
|
||||
remove_config_server_tracked_instance_ids(&removed_ids);
|
||||
remove_data_plane_handles_by_instance_ids(&removed_ids);
|
||||
INSTANCE_NAME_ID_MAP.retain(|k, _| inst_names.contains(k));
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Delete named network instances.
|
||||
pub(crate) unsafe fn delete_network_instance(
|
||||
inst_names: *const *const std::ffi::c_char,
|
||||
length: usize,
|
||||
) -> std::ffi::c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot delete network instances from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
wait_for_config_server_delivery();
|
||||
let _remote_mutation_guard = lock_remote_instance_mutation();
|
||||
let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock instance mutation: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if length == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let Some(inst_names) = (unsafe { parse_instance_names(inst_names, length) }) else {
|
||||
return -1;
|
||||
};
|
||||
|
||||
let removed_ids = inst_names
|
||||
.iter()
|
||||
.filter_map(|name| INSTANCE_NAME_ID_MAP.get(name).map(|id| *id.value()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Err(e) = INSTANCE_MANAGER.delete_network_instance(removed_ids.clone()) {
|
||||
set_error_msg(&format!("failed to delete instances: {}", e));
|
||||
return -1;
|
||||
}
|
||||
|
||||
remove_config_server_tracked_instance_ids(&removed_ids);
|
||||
remove_data_plane_handles_by_instance_ids(&removed_ids);
|
||||
for name in inst_names {
|
||||
INSTANCE_NAME_ID_MAP.remove(&name);
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Collect the network infos
|
||||
pub(crate) unsafe fn collect_network_infos(
|
||||
infos: *mut KeyValuePair,
|
||||
max_length: usize,
|
||||
) -> std::ffi::c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot collect network infos from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if max_length == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let infos = unsafe {
|
||||
assert!(!infos.is_null());
|
||||
std::slice::from_raw_parts_mut(infos, max_length)
|
||||
};
|
||||
|
||||
let collected_infos = match INSTANCE_MANAGER.collect_network_infos_sync() {
|
||||
Ok(infos) => infos,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to collect network infos: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let mut index = 0;
|
||||
for (instance_id, value) in collected_infos.iter() {
|
||||
if index >= max_length {
|
||||
break;
|
||||
}
|
||||
let Some(key) = INSTANCE_MANAGER.get_instance_name(instance_id) else {
|
||||
continue;
|
||||
};
|
||||
// convert value to json string
|
||||
let value = match serde_json::to_string(&value) {
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to serialize instance info: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
infos[index] = KeyValuePair {
|
||||
key: std::ffi::CString::new(key).unwrap().into_raw(),
|
||||
value: std::ffi::CString::new(value).unwrap().into_raw(),
|
||||
};
|
||||
index += 1;
|
||||
}
|
||||
|
||||
index as std::ffi::c_int
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// List the instance names and IDs known by the FFI instance manager.
|
||||
pub(crate) unsafe fn list_instance(infos: *mut KeyValuePair, max_length: usize) -> std::ffi::c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot list instances from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if max_length == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if infos.is_null() {
|
||||
set_error_msg("infos is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let infos = unsafe { std::slice::from_raw_parts_mut(infos, max_length) };
|
||||
let mut instances = INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.into_iter()
|
||||
.filter_map(|id| {
|
||||
INSTANCE_MANAGER
|
||||
.get_instance_name(&id)
|
||||
.map(|name| (name, id))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
instances.sort_by(|(left_name, left_id), (right_name, right_id)| {
|
||||
left_name
|
||||
.cmp(right_name)
|
||||
.then_with(|| left_id.to_string().cmp(&right_id.to_string()))
|
||||
});
|
||||
|
||||
let encoded_instances = match instances
|
||||
.into_iter()
|
||||
.take(max_length)
|
||||
.map(|(name, id)| {
|
||||
let key = CString::new(name)
|
||||
.map_err(|err| format!("failed to encode instance name: {}", err))?;
|
||||
let value = CString::new(id.to_string())
|
||||
.map_err(|err| format!("failed to encode instance id: {}", err))?;
|
||||
Ok((key, value))
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let count = encoded_instances.len();
|
||||
for (index, (key, value)) in encoded_instances.into_iter().enumerate() {
|
||||
infos[index] = KeyValuePair {
|
||||
key: key.into_raw(),
|
||||
value: value.into_raw(),
|
||||
};
|
||||
}
|
||||
|
||||
count as std::ffi::c_int
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
use std::ffi::{CString, c_char, c_int};
|
||||
|
||||
use crate::{
|
||||
config_server::in_config_server_callback,
|
||||
error::set_error_msg,
|
||||
state::{ASYNC_RUNTIME, INSTANCE_MANAGER},
|
||||
strings::{c_str_to_string, optional_c_str_to_string},
|
||||
};
|
||||
|
||||
/// # Safety
|
||||
/// See `crate::call_json_rpc`.
|
||||
pub(crate) unsafe fn call_json_rpc(
|
||||
service_name: *const c_char,
|
||||
method_name: *const c_char,
|
||||
domain_name: *const c_char,
|
||||
payload_json: *const c_char,
|
||||
out_response_json: *mut *const c_char,
|
||||
) -> c_int {
|
||||
if out_response_json.is_null() {
|
||||
set_error_msg("out_response_json is null");
|
||||
return -1;
|
||||
}
|
||||
unsafe {
|
||||
*out_response_json = std::ptr::null();
|
||||
}
|
||||
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot call JSON RPC from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let service_name = match unsafe { c_str_to_string(service_name, "service_name") } {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let method_name = match unsafe { c_str_to_string(method_name, "method_name") } {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let domain_name = match unsafe { optional_c_str_to_string(domain_name, "domain_name") } {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let payload_json = match unsafe { c_str_to_string(payload_json, "payload_json") } {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let payload = match serde_json::from_str::<serde_json::Value>(&payload_json) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to parse payload_json: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let response = match ASYNC_RUNTIME.block_on(easytier::rpc_service::call_json_rpc(
|
||||
&INSTANCE_MANAGER,
|
||||
&service_name,
|
||||
&method_name,
|
||||
domain_name.as_deref(),
|
||||
payload,
|
||||
)) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("RPC Error: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let response_json = match serde_json::to_string(&response) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to serialize RPC response: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let response_json = match CString::new(response_json) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to allocate RPC response: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
unsafe {
|
||||
*out_response_json = response_json.into_raw();
|
||||
}
|
||||
0
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,54 +0,0 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use easytier::instance_manager::NetworkInstanceManager;
|
||||
use tokio::runtime::{Builder, Runtime};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) static INSTANCE_NAME_ID_MAP: once_cell::sync::Lazy<DashMap<String, Uuid>> =
|
||||
once_cell::sync::Lazy::new(DashMap::new);
|
||||
pub(crate) static INSTANCE_MANAGER: once_cell::sync::Lazy<Arc<NetworkInstanceManager>> =
|
||||
once_cell::sync::Lazy::new(|| Arc::new(NetworkInstanceManager::new()));
|
||||
pub(crate) 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-ffi")
|
||||
});
|
||||
pub(crate) static INSTANCE_MUTATION_LOCK: once_cell::sync::Lazy<Mutex<()>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(()));
|
||||
|
||||
pub(crate) fn remove_instance_name_ids(ids: &[Uuid]) {
|
||||
if ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
INSTANCE_NAME_ID_MAP.retain(|_, instance_id| !ids.contains(instance_id));
|
||||
}
|
||||
|
||||
pub(crate) fn lock_remote_instance_mutation() -> tokio::sync::OwnedMutexGuard<()> {
|
||||
INSTANCE_MANAGER
|
||||
.remote_mutation_lock()
|
||||
.blocking_lock_owned()
|
||||
}
|
||||
|
||||
pub(crate) fn instance_name_exists(inst_name: &str) -> bool {
|
||||
find_instance_id_by_name(inst_name).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn find_instance_id_by_name(inst_name: &str) -> Option<Uuid> {
|
||||
INSTANCE_NAME_ID_MAP
|
||||
.get(inst_name)
|
||||
.map(|id| *id)
|
||||
.or_else(|| {
|
||||
INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.into_iter()
|
||||
.find(|id| {
|
||||
INSTANCE_MANAGER
|
||||
.get_instance_name(id)
|
||||
.is_some_and(|name| name == inst_name)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
use std::ffi::{CStr, c_char};
|
||||
|
||||
pub(crate) unsafe fn c_str_to_string(ptr: *const c_char, name: &str) -> Result<String, String> {
|
||||
if ptr.is_null() {
|
||||
return Err(format!("{} is null", name));
|
||||
}
|
||||
|
||||
unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map(|value| value.to_string())
|
||||
.map_err(|err| format!("{} is not valid UTF-8: {}", name, err))
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn optional_c_str_to_string(
|
||||
ptr: *const c_char,
|
||||
name: &str,
|
||||
) -> Result<Option<String>, String> {
|
||||
if ptr.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
unsafe { c_str_to_string(ptr, name) }.map(Some)
|
||||
}
|
||||
@@ -1,766 +0,0 @@
|
||||
use crate::{
|
||||
config_server::{
|
||||
ConfigServerCallbackScope, ManagedConfigServerClientHooks, set_active_for_test,
|
||||
},
|
||||
state::{
|
||||
INSTANCE_MANAGER, INSTANCE_NAME_ID_MAP, find_instance_id_by_name,
|
||||
lock_remote_instance_mutation, remove_instance_name_ids,
|
||||
},
|
||||
*,
|
||||
};
|
||||
use easytier::{
|
||||
common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader},
|
||||
web_client::WebClientHooks,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
ffi::{CStr, CString, c_char, c_void},
|
||||
sync::{Mutex, mpsc},
|
||||
time::Duration,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn test_parse_config() {
|
||||
let cfg_str = r#"
|
||||
inst_name = "test"
|
||||
network = "test_network"
|
||||
"#;
|
||||
let cstr = std::ffi::CString::new(cfg_str).unwrap();
|
||||
unsafe {
|
||||
assert_eq!(parse_config(cstr.as_ptr()), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_network_instance() {
|
||||
let cfg_str = r#"
|
||||
inst_name = "test"
|
||||
network = "test_network"
|
||||
"#;
|
||||
let cstr = std::ffi::CString::new(cfg_str).unwrap();
|
||||
unsafe {
|
||||
assert_eq!(run_network_instance(cstr.as_ptr()), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_error_msg_returns_config_server_callback_error() {
|
||||
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
|
||||
let callback_error = format!("callback delivery failed {}", Uuid::new_v4());
|
||||
crate::config_server::clear_last_callback_error();
|
||||
hooks.note_callback_error(callback_error.clone());
|
||||
|
||||
unsafe {
|
||||
let mut error_ptr: *const c_char = std::ptr::null();
|
||||
get_error_msg(&mut error_ptr);
|
||||
assert!(!error_ptr.is_null());
|
||||
let error_msg = CStr::from_ptr(error_ptr).to_string_lossy().into_owned();
|
||||
free_string(error_ptr);
|
||||
assert!(error_msg.contains(&callback_error));
|
||||
}
|
||||
|
||||
crate::config_server::clear_last_callback_error();
|
||||
}
|
||||
|
||||
unsafe extern "C" fn record_config_server_event(event_json: *const c_char, user_data: *mut c_void) {
|
||||
let events = unsafe { &*(user_data as *const Mutex<Vec<String>>) };
|
||||
events.lock().unwrap().push(
|
||||
unsafe { CStr::from_ptr(event_json) }
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
);
|
||||
}
|
||||
|
||||
fn take_last_error() -> Option<String> {
|
||||
unsafe {
|
||||
let mut error_ptr: *const c_char = std::ptr::null();
|
||||
get_error_msg(&mut error_ptr);
|
||||
if error_ptr.is_null() {
|
||||
None
|
||||
} else {
|
||||
let error = CStr::from_ptr(error_ptr).to_string_lossy().into_owned();
|
||||
free_string(error_ptr);
|
||||
Some(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn free_key_value_pairs(infos: &[KeyValuePair]) {
|
||||
for info in infos {
|
||||
free_string(info.key);
|
||||
free_string(info.value);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_instance_returns_instance_names_and_ids() {
|
||||
let instance_id = Uuid::new_v4();
|
||||
let instance_name = format!("list-instance-{}", instance_id);
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(instance_id);
|
||||
cfg.set_inst_name(instance_name.clone());
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
INSTANCE_NAME_ID_MAP.insert(instance_name.clone(), instance_id);
|
||||
|
||||
let mut infos = vec![
|
||||
KeyValuePair {
|
||||
key: std::ptr::null(),
|
||||
value: std::ptr::null(),
|
||||
};
|
||||
16
|
||||
];
|
||||
let count = unsafe { list_instance(infos.as_mut_ptr(), infos.len()) };
|
||||
assert!(count > 0);
|
||||
|
||||
let mut found = false;
|
||||
for info in infos.iter().take(count as usize) {
|
||||
let key = unsafe { CStr::from_ptr(info.key) }.to_string_lossy();
|
||||
let value = unsafe { CStr::from_ptr(info.value) }.to_string_lossy();
|
||||
if key == instance_name {
|
||||
assert_eq!(value, instance_id.to_string());
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
free_key_value_pairs(&infos[..count as usize]);
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[instance_id]);
|
||||
assert!(found);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_instance_allows_zero_length() {
|
||||
assert_eq!(unsafe { list_instance(std::ptr::null_mut(), 0) }, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_instance_rejects_null_output_pointer() {
|
||||
assert_eq!(unsafe { list_instance(std::ptr::null_mut(), 1) }, -1);
|
||||
assert!(take_last_error().unwrap().contains("infos is null"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_json_rpc_returns_logger_response() {
|
||||
let service = CString::new("api.logger.LoggerRpcService").unwrap();
|
||||
let method = CString::new("get_logger_config").unwrap();
|
||||
let payload = CString::new("{}").unwrap();
|
||||
let mut response_ptr: *const c_char = std::ptr::null();
|
||||
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
call_json_rpc(
|
||||
service.as_ptr(),
|
||||
method.as_ptr(),
|
||||
std::ptr::null(),
|
||||
payload.as_ptr(),
|
||||
&mut response_ptr,
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert!(!response_ptr.is_null());
|
||||
let response = unsafe { CStr::from_ptr(response_ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
free_string(response_ptr);
|
||||
let response: Value = serde_json::from_str(&response).unwrap();
|
||||
assert!(response.get("level").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_json_rpc_rejects_instance_management_service() {
|
||||
let service = CString::new("api.manage.WebClientService").unwrap();
|
||||
let method = CString::new("list_network_instance").unwrap();
|
||||
let payload = CString::new("{}").unwrap();
|
||||
let mut response_ptr: *const c_char = std::ptr::null();
|
||||
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
call_json_rpc(
|
||||
service.as_ptr(),
|
||||
method.as_ptr(),
|
||||
std::ptr::null(),
|
||||
payload.as_ptr(),
|
||||
&mut response_ptr,
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert!(response_ptr.is_null());
|
||||
assert!(take_last_error().unwrap().contains("not exposed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_json_rpc_rejects_malformed_payload_json() {
|
||||
let service = CString::new("api.logger.LoggerRpcService").unwrap();
|
||||
let method = CString::new("get_logger_config").unwrap();
|
||||
let payload = CString::new("{").unwrap();
|
||||
let mut response_ptr: *const c_char = std::ptr::null();
|
||||
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
call_json_rpc(
|
||||
service.as_ptr(),
|
||||
method.as_ptr(),
|
||||
std::ptr::null(),
|
||||
payload.as_ptr(),
|
||||
&mut response_ptr,
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert!(response_ptr.is_null());
|
||||
assert!(
|
||||
take_last_error()
|
||||
.unwrap()
|
||||
.contains("failed to parse payload_json")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_json_rpc_rejects_null_output_pointer() {
|
||||
let service = CString::new("api.logger.LoggerRpcService").unwrap();
|
||||
let method = CString::new("get_logger_config").unwrap();
|
||||
let payload = CString::new("{}").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
call_json_rpc(
|
||||
service.as_ptr(),
|
||||
method.as_ptr(),
|
||||
std::ptr::null(),
|
||||
payload.as_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert!(
|
||||
take_last_error()
|
||||
.unwrap()
|
||||
.contains("out_response_json is null")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_emit_run_event() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
&events as *const _ as *mut c_void,
|
||||
);
|
||||
let instance_id = Uuid::new_v4();
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(instance_id);
|
||||
let inst_name = format!("test-{}", instance_id);
|
||||
cfg.set_inst_name(inst_name.clone());
|
||||
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
|
||||
hooks.post_run_network_instance(&instance_id).await.unwrap();
|
||||
|
||||
let duplicate_cfg = TomlConfigLoader::default();
|
||||
duplicate_cfg.set_inst_name(inst_name);
|
||||
duplicate_cfg.set_id(Uuid::new_v4());
|
||||
assert!(
|
||||
hooks
|
||||
.pre_run_network_instance(&duplicate_cfg)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
assert_eq!(hooks.tracked_instance_ids(), vec![instance_id]);
|
||||
let events = events.lock().unwrap();
|
||||
assert_eq!(events.len(), 1);
|
||||
let event: Value = serde_json::from_str(&events[0]).unwrap();
|
||||
assert_eq!(event["event"], "run_network_instance");
|
||||
assert_eq!(event["success"], true);
|
||||
assert_eq!(event["instance_id"], instance_id.to_string());
|
||||
assert!(event["error"].is_null());
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[instance_id]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_emit_delete_events_for_tracked_instances() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
&events as *const _ as *mut c_void,
|
||||
);
|
||||
let instance_id_1 = Uuid::new_v4();
|
||||
let instance_id_2 = Uuid::new_v4();
|
||||
let unknown_instance_id = Uuid::new_v4();
|
||||
for id in [instance_id_1, instance_id_2] {
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(id);
|
||||
cfg.set_inst_name(format!("test-{}", id));
|
||||
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
hooks
|
||||
.post_run_network_instance(&instance_id_1)
|
||||
.await
|
||||
.unwrap();
|
||||
hooks
|
||||
.post_run_network_instance(&instance_id_2)
|
||||
.await
|
||||
.unwrap();
|
||||
events.lock().unwrap().clear();
|
||||
|
||||
hooks
|
||||
.post_remove_network_instances(&[instance_id_1, unknown_instance_id, instance_id_2])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(hooks.tracked_instance_ids().is_empty());
|
||||
let events = events.lock().unwrap();
|
||||
assert_eq!(events.len(), 2);
|
||||
let event_ids = events
|
||||
.iter()
|
||||
.map(|event| {
|
||||
let event: Value = serde_json::from_str(event).unwrap();
|
||||
assert_eq!(event["event"], "delete_network_instance");
|
||||
assert_eq!(event["success"], true);
|
||||
assert!(event["error"].is_null());
|
||||
event["instance_id"].as_str().unwrap().to_string()
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(
|
||||
event_ids,
|
||||
HashSet::from([instance_id_1.to_string(), instance_id_2.to_string()])
|
||||
);
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id_1, instance_id_2])
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[instance_id_1, instance_id_2]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_remove_untracked_name_mapping_without_event() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
&events as *const _ as *mut c_void,
|
||||
);
|
||||
let local_id = Uuid::new_v4();
|
||||
let inst_name = format!("local-{}", local_id);
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name.clone(), local_id);
|
||||
|
||||
hooks
|
||||
.post_remove_network_instances(&[local_id])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(INSTANCE_NAME_ID_MAP.get(&inst_name).is_none());
|
||||
assert!(events.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_reject_duplicate_instance_name() {
|
||||
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
|
||||
let inst_name = format!("test-{}", Uuid::new_v4());
|
||||
let existing_id = Uuid::new_v4();
|
||||
let new_id = Uuid::new_v4();
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name.clone(), existing_id);
|
||||
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_inst_name(inst_name.clone());
|
||||
cfg.set_id(new_id);
|
||||
|
||||
assert!(hooks.pre_run_network_instance(&cfg).await.is_err());
|
||||
assert_eq!(*INSTANCE_NAME_ID_MAP.get(&inst_name).unwrap(), existing_id);
|
||||
INSTANCE_NAME_ID_MAP.remove(&inst_name);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_remove_overwritten_id_before_duplicate_name_error() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
&events as *const _ as *mut c_void,
|
||||
);
|
||||
let old_name = format!("old-{}", Uuid::new_v4());
|
||||
let duplicate_name = format!("duplicate-{}", Uuid::new_v4());
|
||||
let overwritten_id = Uuid::new_v4();
|
||||
let duplicate_id = Uuid::new_v4();
|
||||
hooks.instance_ids.lock().unwrap().insert(overwritten_id);
|
||||
INSTANCE_NAME_ID_MAP.insert(old_name.clone(), overwritten_id);
|
||||
INSTANCE_NAME_ID_MAP.insert(duplicate_name.clone(), duplicate_id);
|
||||
|
||||
hooks
|
||||
.post_remove_network_instances(&[overwritten_id])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_inst_name(duplicate_name.clone());
|
||||
cfg.set_id(overwritten_id);
|
||||
|
||||
assert!(hooks.pre_run_network_instance(&cfg).await.is_err());
|
||||
assert!(hooks.tracked_instance_ids().is_empty());
|
||||
assert!(INSTANCE_NAME_ID_MAP.get(&old_name).is_none());
|
||||
assert_eq!(
|
||||
*INSTANCE_NAME_ID_MAP.get(&duplicate_name).unwrap(),
|
||||
duplicate_id
|
||||
);
|
||||
assert_eq!(events.lock().unwrap().len(), 1);
|
||||
INSTANCE_NAME_ID_MAP.remove(&duplicate_name);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_remove_tracked_state_before_overwrite_retry() {
|
||||
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
|
||||
let inst_name = format!("test-{}", Uuid::new_v4());
|
||||
let instance_id = Uuid::new_v4();
|
||||
hooks.instance_ids.lock().unwrap().insert(instance_id);
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name.clone(), instance_id);
|
||||
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_inst_name(inst_name.clone());
|
||||
cfg.set_id(instance_id);
|
||||
|
||||
hooks
|
||||
.post_remove_network_instances(&[instance_id])
|
||||
.await
|
||||
.unwrap();
|
||||
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||
|
||||
assert!(hooks.tracked_instance_ids().is_empty());
|
||||
assert!(INSTANCE_NAME_ID_MAP.get(&inst_name).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_reject_post_run_after_external_delete() {
|
||||
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
|
||||
let instance_id = Uuid::new_v4();
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(instance_id);
|
||||
cfg.set_inst_name(format!("test-{}", instance_id));
|
||||
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
.unwrap();
|
||||
|
||||
assert!(hooks.post_run_network_instance(&instance_id).await.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_instance_id_by_name_resolves_uncommitted_manager_instance_name() {
|
||||
let instance_id = Uuid::new_v4();
|
||||
let inst_name = format!("test-{}", instance_id);
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(instance_id);
|
||||
cfg.set_inst_name(inst_name.clone());
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(find_instance_id_by_name(&inst_name), Some(instance_id));
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[instance_id]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_network_instance_removes_only_named_instances() {
|
||||
let keep_id = Uuid::new_v4();
|
||||
let delete_id = Uuid::new_v4();
|
||||
let keep_name = format!("keep-{}", keep_id);
|
||||
let delete_name = format!("delete-{}", delete_id);
|
||||
|
||||
for (id, name) in [
|
||||
(keep_id, keep_name.clone()),
|
||||
(delete_id, delete_name.clone()),
|
||||
] {
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(id);
|
||||
cfg.set_inst_name(name.clone());
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
INSTANCE_NAME_ID_MAP.insert(name, id);
|
||||
}
|
||||
|
||||
let delete_name = CString::new(delete_name.clone()).unwrap();
|
||||
let inst_names = [delete_name.as_ptr()];
|
||||
assert_eq!(
|
||||
unsafe { delete_network_instance(inst_names.as_ptr(), inst_names.len()) },
|
||||
0
|
||||
);
|
||||
|
||||
assert_eq!(find_instance_id_by_name(&keep_name), Some(keep_id));
|
||||
assert!(find_instance_id_by_name(delete_name.to_str().unwrap()).is_none());
|
||||
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![keep_id])
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[keep_id]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retain_and_delete_network_instance_reject_invalid_name_pointers() {
|
||||
assert_eq!(unsafe { retain_network_instance(std::ptr::null(), 1) }, -1);
|
||||
assert_eq!(unsafe { delete_network_instance(std::ptr::null(), 1) }, -1);
|
||||
|
||||
let inst_names = [std::ptr::null()];
|
||||
assert_eq!(
|
||||
unsafe { retain_network_instance(inst_names.as_ptr(), inst_names.len()) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { delete_network_instance(inst_names.as_ptr(), inst_names.len()) },
|
||||
-1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffi_remote_mutation_lock_uses_manager_lock() {
|
||||
let manager_guard = INSTANCE_MANAGER
|
||||
.remote_mutation_lock()
|
||||
.blocking_lock_owned();
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
let waiter = std::thread::spawn(move || {
|
||||
let _ffi_guard = lock_remote_instance_mutation();
|
||||
done_tx.send(()).unwrap();
|
||||
});
|
||||
|
||||
assert!(done_rx.recv_timeout(Duration::from_millis(100)).is_err());
|
||||
drop(manager_guard);
|
||||
done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
waiter.join().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_suppress_late_run_events_while_stopping() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
&events as *const _ as *mut c_void,
|
||||
);
|
||||
hooks.start_stopping();
|
||||
|
||||
hooks
|
||||
.post_run_network_instance(&Uuid::new_v4())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(hooks.tracked_instance_ids().is_empty());
|
||||
assert!(events.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_server_callback_context_rejects_nested_blocking_ffi_calls() {
|
||||
let _callback_scope = ConfigServerCallbackScope::enter();
|
||||
assert_eq!(is_config_server_client_connected(), 0);
|
||||
let service = CString::new("api.logger.LoggerRpcService").unwrap();
|
||||
let method = CString::new("get_logger_config").unwrap();
|
||||
let payload = CString::new("{}").unwrap();
|
||||
let mut response_ptr: *const c_char = std::ptr::null();
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
call_json_rpc(
|
||||
service.as_ptr(),
|
||||
method.as_ptr(),
|
||||
std::ptr::null(),
|
||||
payload.as_ptr(),
|
||||
&mut response_ptr,
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert!(response_ptr.is_null());
|
||||
assert_eq!(
|
||||
unsafe { collect_network_infos(std::ptr::null_mut(), 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(unsafe { list_instance(std::ptr::null_mut(), 0) }, -1);
|
||||
let cfg = CString::new("inst_name = \"callback-test\"\nlisteners = []").unwrap();
|
||||
assert_eq!(unsafe { run_network_instance(cfg.as_ptr()) }, -1);
|
||||
assert_eq!(unsafe { retain_network_instance(std::ptr::null(), 0) }, -1);
|
||||
assert_eq!(unsafe { delete_network_instance(std::ptr::null(), 0) }, -1);
|
||||
let url = CString::new("ring://test/token").unwrap();
|
||||
let machine_id = CString::new("test-machine").unwrap();
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
start_config_server_client(
|
||||
url.as_ptr(),
|
||||
std::ptr::null(),
|
||||
machine_id.as_ptr(),
|
||||
false,
|
||||
None,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert_eq!(stop_config_server_client(), -1);
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
{
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_tcp_connect(
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_tcp_bind(
|
||||
std::ptr::null(),
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_tcp_accept(
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_read(0, std::ptr::null_mut(), 0, 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_write(0, std::ptr::null(), 0, 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(data_plane_tcp_close(0), -1);
|
||||
assert_eq!(data_plane_tcp_listener_close(0), -1);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_udp_bind(
|
||||
std::ptr::null(),
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_udp_send_to(0, std::ptr::null(), 0, std::ptr::null(), 0, 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_udp_recv_from(
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert_eq!(data_plane_udp_close(0), -1);
|
||||
assert_eq!(data_plane_async_op_status(0), -2);
|
||||
assert_eq!(data_plane_async_op_wait(0, 0), -2);
|
||||
assert_eq!(data_plane_async_op_cancel(0), -2);
|
||||
assert_eq!(data_plane_async_op_free(0), -2);
|
||||
data_plane_free_bytes(std::ptr::null(), 0);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_connect_start(std::ptr::null(), std::ptr::null(), 0, 0) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_bind_start(std::ptr::null(), 0, 0) },
|
||||
0
|
||||
);
|
||||
assert_eq!(unsafe { data_plane_tcp_accept_start(0, 0) }, 0);
|
||||
assert_eq!(unsafe { data_plane_tcp_read_start(0, 0, 0) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_write_start(0, std::ptr::null(), 0, 0) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_udp_bind_start(std::ptr::null(), 0, 0) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_udp_send_to_start(0, std::ptr::null(), 0, std::ptr::null(), 0, 0) },
|
||||
0
|
||||
);
|
||||
assert_eq!(unsafe { data_plane_udp_recv_from_start(0, 0, 0) }, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[test]
|
||||
fn active_config_server_rejects_data_plane() {
|
||||
set_active_for_test(true);
|
||||
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_tcp_connect(
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_read(0, std::ptr::null_mut(), 0, 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_connect_start(std::ptr::null(), std::ptr::null(), 0, 0) },
|
||||
0
|
||||
);
|
||||
assert_eq!(unsafe { data_plane_tcp_read_start(0, 0, 0) }, 0);
|
||||
|
||||
set_active_for_test(false);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[test]
|
||||
fn async_op_invalid_handle_helpers_are_stable() {
|
||||
assert_eq!(data_plane_async_op_status(u64::MAX), -2);
|
||||
assert_eq!(data_plane_async_op_wait(u64::MAX, 1), -2);
|
||||
assert_eq!(data_plane_async_op_cancel(u64::MAX), -2);
|
||||
assert_eq!(data_plane_async_op_free(u64::MAX), -2);
|
||||
data_plane_free_bytes(std::ptr::null(), 0);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
use std::ffi::{c_char, c_void};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct KeyValuePair {
|
||||
pub key: *const c_char,
|
||||
pub value: *const c_char,
|
||||
}
|
||||
|
||||
pub type ConfigServerEventCallback = Option<unsafe extern "C" fn(*const c_char, *mut c_void)>;
|
||||
+64
-112
@@ -1237,18 +1237,18 @@ dependencies = [
|
||||
"ordered_hash_map",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
"pbjson",
|
||||
"pbjson-build",
|
||||
"percent-encoding",
|
||||
"petgraph",
|
||||
"petgraph 0.8.2",
|
||||
"pin-project-lite",
|
||||
"pnet",
|
||||
"prefix-trie",
|
||||
"proc-macro2",
|
||||
"prost 0.14.3",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-reflect 0.16.4",
|
||||
"prost-reflect",
|
||||
"prost-reflect-build",
|
||||
"prost-wkt",
|
||||
"prost-wkt-build",
|
||||
"prost-wkt-types",
|
||||
"quinn",
|
||||
"quinn-plaintext",
|
||||
@@ -1318,8 +1318,9 @@ dependencies = [
|
||||
"napi-build-ohos",
|
||||
"napi-derive-ohos",
|
||||
"napi-ohos",
|
||||
"ohos-hilog-binding",
|
||||
"once_cell",
|
||||
"prost-reflect 0.14.7",
|
||||
"prost-reflect",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3164,6 +3165,22 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ohos-hilog-binding"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "860d1e3c2c5e3217d819a16c815d2d4dcbc7610285d2612d08745a29c353a503"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"ohos-hilogs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ohos-hilogs-sys"
|
||||
version = "0.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed07615005d0f8d7bcf901f89c8ff4870666a9bdb00382f588af383f40c160b7"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -3291,28 +3308,6 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pbjson"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8edd1efdd8ab23ba9cb9ace3d9987a72663d5d7c9f74fa00b51d6213645cf6c"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pbjson-build"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ed4d5c6ae95e08ac768883c8401cf0e8deb4e6e1d6a4e1fd3d2ec4f0ec63200"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"itertools 0.14.0",
|
||||
"prost 0.14.3",
|
||||
"prost-types 0.14.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
@@ -3339,6 +3334,16 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.8.2"
|
||||
@@ -3626,33 +3631,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive 0.13.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive 0.14.3",
|
||||
"prost-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-build"
|
||||
version = "0.14.3"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7"
|
||||
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"multimap",
|
||||
"petgraph",
|
||||
"once_cell",
|
||||
"petgraph 0.7.1",
|
||||
"prettyplease",
|
||||
"prost 0.14.3",
|
||||
"prost-types 0.14.3",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"regex",
|
||||
"syn 2.0.106",
|
||||
"tempfile",
|
||||
@@ -3671,19 +3667,6 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-derive"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-reflect"
|
||||
version = "0.14.7"
|
||||
@@ -3691,30 +3674,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5edd582b62f5cde844716e66d92565d7faf7ab1445c8cebce6e00fba83ddb2"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"prost 0.13.5",
|
||||
"prost-reflect-derive 0.14.0",
|
||||
"prost-types 0.13.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-reflect"
|
||||
version = "0.16.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "590aa145fee8f7a26b5a6055365e7c5e89a5c1caae9869de76ec0ee73181a2f9"
|
||||
dependencies = [
|
||||
"prost 0.14.3",
|
||||
"prost-reflect-derive 0.16.0",
|
||||
"prost-types 0.14.3",
|
||||
"prost",
|
||||
"prost-reflect-derive",
|
||||
"prost-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-reflect-build"
|
||||
version = "0.16.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8214ae2c30bbac390db0134d08300e770ef89b6d4e5abf855e8d300eded87e28"
|
||||
checksum = "50e2537231d94dd2778920c2ada37dd9eb1ac0325bb3ee3ee651bd44c1134123"
|
||||
dependencies = [
|
||||
"prost-build",
|
||||
"prost-reflect 0.16.4",
|
||||
"prost-reflect",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3728,44 +3700,24 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-reflect-derive"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b6d90e29fa6c0d13c2c19ba5e4b3fb0efbf5975d27bcf4e260b7b15455bcabe"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
|
||||
dependencies = [
|
||||
"prost 0.13.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7"
|
||||
dependencies = [
|
||||
"prost 0.14.3",
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-wkt"
|
||||
version = "0.7.1"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd3de5e9c9e84fcb5efa204b8e283d23e615a8bc8c777bf1d6622bb01dc61445"
|
||||
checksum = "497e1e938f0c09ef9cabe1d49437b4016e03e8f82fbbe5d1c62a9b61b9decae1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"inventory",
|
||||
"prost 0.14.3",
|
||||
"prost",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -3774,27 +3726,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost-wkt-build"
|
||||
version = "0.7.1"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe500dc80e757a75e1e8fb7290e448d62dfba3105ece1d058579cb00b58151cd"
|
||||
checksum = "07b8bf115b70a7aa5af1fd5d6e9418492e9ccb6e4785e858c938e28d132a884b"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"prost 0.14.3",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-types 0.14.3",
|
||||
"prost-types",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-wkt-types"
|
||||
version = "0.7.1"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13807eaa7e15833d06e899008371926201cdcd11d74b6d490f49130cdb3f415e"
|
||||
checksum = "c8cdde6df0a98311c839392ca2f2f0bcecd545f86a62b4e3c6a49c336e970fe5"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"prost 0.14.3",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-types 0.14.3",
|
||||
"prost-types",
|
||||
"prost-wkt",
|
||||
"prost-wkt-build",
|
||||
"regex",
|
||||
@@ -3883,9 +3835,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -4022,9 +3974,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
version = "1.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -4034,9 +3986,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
||||
@@ -11,6 +11,7 @@ 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"
|
||||
napi-ohos = { version = "1.1", default-features = false, features = [
|
||||
|
||||
@@ -1,49 +1,13 @@
|
||||
use crate::config::types::stored_config::{
|
||||
SnapshotImportResult, StoredConfigList, StoredConfigMeta,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use crate::config::types::stored_config::{StoredConfigList, StoredConfigMeta};
|
||||
use ohos_hilog_binding::{hilog_debug, hilog_error};
|
||||
use rusqlite::{Connection, OptionalExtension, params};
|
||||
use std::collections::HashSet;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
use std::sync::Mutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
static CONFIG_DB_PATH: Mutex<Option<PathBuf>> = Mutex::new(None);
|
||||
static CONFIG_DB_CONNECTION: Lazy<Mutex<Option<CachedConfigDb>>> = Lazy::new(|| Mutex::new(None));
|
||||
const CONFIG_DB_FILE_NAME: &str = "easytier-config-store.db";
|
||||
|
||||
struct CachedConfigDb {
|
||||
path: PathBuf,
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
pub(crate) struct ConfigDbGuard<'a> {
|
||||
guard: MutexGuard<'a, Option<CachedConfigDb>>,
|
||||
}
|
||||
|
||||
impl Deref for ConfigDbGuard<'_> {
|
||||
type Target = Connection;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self
|
||||
.guard
|
||||
.as_ref()
|
||||
.expect("config db connection guard must contain a connection")
|
||||
.conn
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for ConfigDbGuard<'_> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self
|
||||
.guard
|
||||
.as_mut()
|
||||
.expect("config db connection guard must contain a connection")
|
||||
.conn
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct StoredConfigMetaRecord {
|
||||
config_id: String,
|
||||
@@ -54,30 +18,6 @@ struct StoredConfigMetaRecord {
|
||||
temporary: bool,
|
||||
}
|
||||
|
||||
type SnapshotFieldRow = (String, String, String, String);
|
||||
|
||||
fn snapshot_import_ok() -> SnapshotImportResult {
|
||||
SnapshotImportResult {
|
||||
ok: true,
|
||||
error_code: String::new(),
|
||||
error_message: String::new(),
|
||||
snapshot_invalid: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot_import_err(
|
||||
error_code: &str,
|
||||
error_message: impl Into<String>,
|
||||
snapshot_invalid: bool,
|
||||
) -> SnapshotImportResult {
|
||||
SnapshotImportResult {
|
||||
ok: false,
|
||||
error_code: error_code.to_string(),
|
||||
error_message: error_message.into(),
|
||||
snapshot_invalid,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn now_ts_string() -> String {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
@@ -113,176 +53,31 @@ fn init_schema(conn: &Connection) -> rusqlite::Result<()> {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_stored_config_fields_config_id
|
||||
ON stored_config_fields(config_id);",
|
||||
)?;
|
||||
|
||||
ensure_column(
|
||||
conn,
|
||||
"stored_configs",
|
||||
"favorite",
|
||||
"ALTER TABLE stored_configs ADD COLUMN favorite INTEGER NOT NULL DEFAULT 0;",
|
||||
)?;
|
||||
ensure_column(
|
||||
conn,
|
||||
"stored_configs",
|
||||
"temporary",
|
||||
"ALTER TABLE stored_configs ADD COLUMN temporary INTEGER NOT NULL DEFAULT 0;",
|
||||
)?;
|
||||
ensure_column(
|
||||
conn,
|
||||
"stored_config_fields",
|
||||
"updated_at",
|
||||
"ALTER TABLE stored_config_fields ADD COLUMN updated_at TEXT NOT NULL DEFAULT '0';",
|
||||
)?;
|
||||
|
||||
if !validate_store_schema(conn)? {
|
||||
return Err(rusqlite::Error::InvalidQuery);
|
||||
}
|
||||
|
||||
conn.execute_batch("PRAGMA user_version = 1;")
|
||||
)
|
||||
}
|
||||
|
||||
fn table_columns(conn: &Connection, table_name: &str) -> rusqlite::Result<HashSet<String>> {
|
||||
let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", table_name))?;
|
||||
let rows = stmt.query_map([], |row| row.get::<_, String>(1))?;
|
||||
let mut columns = HashSet::new();
|
||||
for row in rows {
|
||||
columns.insert(row?);
|
||||
}
|
||||
Ok(columns)
|
||||
}
|
||||
|
||||
fn ensure_column(
|
||||
conn: &Connection,
|
||||
table_name: &str,
|
||||
column_name: &str,
|
||||
alter_sql: &str,
|
||||
) -> rusqlite::Result<()> {
|
||||
let columns = table_columns(conn, table_name)?;
|
||||
if !columns.contains(column_name) {
|
||||
conn.execute_batch(alter_sql)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_store_schema(conn: &Connection) -> rusqlite::Result<bool> {
|
||||
let meta_columns = table_columns(conn, "stored_configs")?;
|
||||
let field_columns = table_columns(conn, "stored_config_fields")?;
|
||||
let required_meta = [
|
||||
"config_id",
|
||||
"display_name",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"favorite",
|
||||
"temporary",
|
||||
];
|
||||
let required_fields = ["config_id", "field_name", "field_json", "updated_at"];
|
||||
|
||||
Ok(required_meta
|
||||
.iter()
|
||||
.all(|column| meta_columns.contains(*column))
|
||||
&& required_fields
|
||||
.iter()
|
||||
.all(|column| field_columns.contains(*column)))
|
||||
}
|
||||
|
||||
fn move_db_file_if_exists(path: &Path) -> bool {
|
||||
if !path.exists() {
|
||||
return true;
|
||||
}
|
||||
let target = PathBuf::from(format!(
|
||||
"{}.corrupt.{}",
|
||||
path.to_string_lossy(),
|
||||
now_ts_string()
|
||||
));
|
||||
match std::fs::rename(path, &target) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
ohrs_log_error!(
|
||||
"[Rust] failed to move corrupt config db {} to {}: {}",
|
||||
path.display(),
|
||||
target.display(),
|
||||
e
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn recover_config_db_files(path: &Path) -> bool {
|
||||
let main_ok = move_db_file_if_exists(path);
|
||||
let wal_ok = move_db_file_if_exists(Path::new(&format!("{}-wal", path.to_string_lossy())));
|
||||
let shm_ok = move_db_file_if_exists(Path::new(&format!("{}-shm", path.to_string_lossy())));
|
||||
main_ok && wal_ok && shm_ok
|
||||
}
|
||||
|
||||
fn open_connection(path: &Path) -> Option<Connection> {
|
||||
let conn = match Connection::open(path) {
|
||||
pub(crate) fn open_db() -> Option<Connection> {
|
||||
let path = db_file_path()?;
|
||||
let conn = match Connection::open(&path) {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => {
|
||||
ohrs_log_error!("[Rust] failed to open config db {}: {}", path.display(), e);
|
||||
hilog_error!("[Rust] failed to open config db {}: {}", path.display(), e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = init_schema(&conn) {
|
||||
ohrs_log_error!(
|
||||
hilog_error!(
|
||||
"[Rust] failed to initialize config db {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
drop(conn);
|
||||
if !recover_config_db_files(path) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let recovered = match Connection::open(path) {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => {
|
||||
ohrs_log_error!(
|
||||
"[Rust] failed to open recovered config db {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if let Err(e) = init_schema(&recovered) {
|
||||
ohrs_log_error!(
|
||||
"[Rust] failed to initialize recovered config db {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
return Some(recovered);
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(conn)
|
||||
}
|
||||
|
||||
pub(crate) fn open_db() -> Option<ConfigDbGuard<'static>> {
|
||||
let path = db_file_path()?;
|
||||
let mut guard = match CONFIG_DB_CONNECTION.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(e) => {
|
||||
ohrs_log_error!("[Rust] failed to lock config db connection: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let should_open = guard
|
||||
.as_ref()
|
||||
.map(|cached| cached.path != path || !cached.path.exists())
|
||||
.unwrap_or(true);
|
||||
if should_open {
|
||||
let conn = open_connection(&path)?;
|
||||
|
||||
*guard = Some(CachedConfigDb { path, conn });
|
||||
}
|
||||
|
||||
Some(ConfigDbGuard { guard })
|
||||
}
|
||||
|
||||
fn row_to_meta(row: &rusqlite::Row<'_>) -> rusqlite::Result<StoredConfigMetaRecord> {
|
||||
Ok(StoredConfigMetaRecord {
|
||||
config_id: row.get(0)?,
|
||||
@@ -330,64 +125,38 @@ fn validate_snapshot_schema(conn: &Connection) -> bool {
|
||||
has_stored_configs && has_stored_fields
|
||||
}
|
||||
|
||||
fn read_snapshot_tables(
|
||||
src: &Connection,
|
||||
) -> rusqlite::Result<(Vec<StoredConfigMetaRecord>, Vec<SnapshotFieldRow>)> {
|
||||
src.execute_batch("BEGIN DEFERRED TRANSACTION")?;
|
||||
|
||||
fn copy_snapshot_tables(src: &Connection, dst: &mut Connection) -> rusqlite::Result<()> {
|
||||
let mut meta_rows = Vec::<StoredConfigMetaRecord>::new();
|
||||
let mut field_rows = Vec::<SnapshotFieldRow>::new();
|
||||
|
||||
let read_result = (|| -> rusqlite::Result<()> {
|
||||
{
|
||||
let mut stmt = src.prepare(
|
||||
"SELECT config_id, display_name, created_at, updated_at, favorite, temporary
|
||||
FROM stored_configs",
|
||||
)?;
|
||||
let rows = stmt.query_map([], row_to_meta)?;
|
||||
for row in rows {
|
||||
meta_rows.push(row?);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut stmt = src.prepare(
|
||||
"SELECT config_id, field_name, field_json, updated_at
|
||||
FROM stored_config_fields",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
row.get::<_, String>(3)?,
|
||||
))
|
||||
})?;
|
||||
for row in rows {
|
||||
field_rows.push(row?);
|
||||
}
|
||||
{
|
||||
let mut stmt = src.prepare(
|
||||
"SELECT config_id, display_name, created_at, updated_at, favorite, temporary
|
||||
FROM stored_configs",
|
||||
)?;
|
||||
let rows = stmt.query_map([], row_to_meta)?;
|
||||
for row in rows {
|
||||
meta_rows.push(row?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
match read_result {
|
||||
Ok(()) => {
|
||||
src.execute_batch("COMMIT")?;
|
||||
Ok((meta_rows, field_rows))
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = src.execute_batch("ROLLBACK");
|
||||
Err(err)
|
||||
let mut field_rows = Vec::<(String, String, String, String)>::new();
|
||||
{
|
||||
let mut stmt = src.prepare(
|
||||
"SELECT config_id, field_name, field_json, updated_at
|
||||
FROM stored_config_fields",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
row.get::<_, String>(3)?,
|
||||
))
|
||||
})?;
|
||||
for row in rows {
|
||||
field_rows.push(row?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_snapshot_tables(
|
||||
dst: &mut Connection,
|
||||
meta_rows: Vec<StoredConfigMetaRecord>,
|
||||
field_rows: Vec<SnapshotFieldRow>,
|
||||
) -> rusqlite::Result<()> {
|
||||
let tx = dst.unchecked_transaction()?;
|
||||
tx.execute("DELETE FROM stored_config_fields", [])?;
|
||||
tx.execute("DELETE FROM stored_configs", [])?;
|
||||
@@ -419,17 +188,12 @@ fn write_snapshot_tables(
|
||||
tx.commit()
|
||||
}
|
||||
|
||||
fn copy_snapshot_tables(src: &Connection, dst: &mut Connection) -> rusqlite::Result<()> {
|
||||
let (meta_rows, field_rows) = read_snapshot_tables(src)?;
|
||||
write_snapshot_tables(dst, meta_rows, field_rows)
|
||||
}
|
||||
|
||||
fn ensure_parent_dir(path: &Path) -> bool {
|
||||
match path.parent() {
|
||||
Some(parent) => match std::fs::create_dir_all(parent) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
ohrs_log_error!(
|
||||
hilog_error!(
|
||||
"[Rust] failed to create snapshot parent {}: {}",
|
||||
parent.display(),
|
||||
e
|
||||
@@ -455,7 +219,7 @@ fn to_meta(record: StoredConfigMetaRecord) -> StoredConfigMeta {
|
||||
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) {
|
||||
ohrs_log_error!(
|
||||
hilog_error!(
|
||||
"[Rust] failed to create config db dir {}: {}",
|
||||
root.display(),
|
||||
e
|
||||
@@ -469,7 +233,7 @@ pub fn init_config_meta_store(root_dir: String) -> bool {
|
||||
*guard = Some(db_path.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
ohrs_log_error!("[Rust] failed to lock config db path: {}", e);
|
||||
hilog_error!("[Rust] failed to lock config db path: {}", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -478,7 +242,7 @@ pub fn init_config_meta_store(root_dir: String) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
ohrs_log_debug!("[Rust] initialized config db at {}", db_path.display());
|
||||
hilog_debug!("[Rust] initialized config db at {}", db_path.display());
|
||||
true
|
||||
}
|
||||
|
||||
@@ -493,7 +257,7 @@ pub fn export_config_store_snapshot(target_path: String) -> bool {
|
||||
let mut dst = match Connection::open(&target) {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => {
|
||||
ohrs_log_error!(
|
||||
hilog_error!(
|
||||
"[Rust] failed to open snapshot target {}: {}",
|
||||
target.display(),
|
||||
e
|
||||
@@ -502,7 +266,7 @@ pub fn export_config_store_snapshot(target_path: String) -> bool {
|
||||
}
|
||||
};
|
||||
if let Err(e) = init_schema(&dst) {
|
||||
ohrs_log_error!(
|
||||
hilog_error!(
|
||||
"[Rust] failed to init snapshot schema {}: {}",
|
||||
target.display(),
|
||||
e
|
||||
@@ -512,7 +276,7 @@ pub fn export_config_store_snapshot(target_path: String) -> bool {
|
||||
match copy_snapshot_tables(&src, &mut dst) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
ohrs_log_error!(
|
||||
hilog_error!(
|
||||
"[Rust] failed to export snapshot {}: {}",
|
||||
target.display(),
|
||||
e
|
||||
@@ -522,92 +286,34 @@ pub fn export_config_store_snapshot(target_path: String) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn import_config_store_snapshot_with_result(source_path: String) -> SnapshotImportResult {
|
||||
pub fn import_config_store_snapshot(source_path: String) -> bool {
|
||||
let source = PathBuf::from(source_path);
|
||||
let src = match Connection::open(&source) {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => {
|
||||
ohrs_log_error!(
|
||||
hilog_error!(
|
||||
"[Rust] failed to open snapshot source {}: {}",
|
||||
source.display(),
|
||||
e
|
||||
);
|
||||
return snapshot_import_err("source_open_failed", e.to_string(), false);
|
||||
}
|
||||
};
|
||||
if !validate_snapshot_schema(&src) {
|
||||
ohrs_log_error!("[Rust] invalid snapshot schema {}", source.display());
|
||||
return snapshot_import_err(
|
||||
"invalid_snapshot_schema",
|
||||
format!("invalid snapshot schema: {}", source.display()),
|
||||
true,
|
||||
);
|
||||
}
|
||||
let (meta_rows, field_rows) = match read_snapshot_tables(&src) {
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
ohrs_log_error!(
|
||||
"[Rust] failed to read snapshot source {}: {}",
|
||||
source.display(),
|
||||
e
|
||||
);
|
||||
return snapshot_import_err("invalid_snapshot_data", e.to_string(), true);
|
||||
}
|
||||
};
|
||||
let Some(mut dst) = open_db() else {
|
||||
return snapshot_import_err(
|
||||
"destination_open_failed",
|
||||
"failed to open local config store",
|
||||
false,
|
||||
);
|
||||
};
|
||||
match write_snapshot_tables(&mut dst, meta_rows, field_rows) {
|
||||
Ok(_) => snapshot_import_ok(),
|
||||
Err(e) => {
|
||||
ohrs_log_error!(
|
||||
"[Rust] failed to import snapshot {}: {}",
|
||||
source.display(),
|
||||
e
|
||||
);
|
||||
snapshot_import_err("destination_write_failed", e.to_string(), false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn import_config_store_snapshot(source_path: String) -> bool {
|
||||
import_config_store_snapshot_with_result(source_path).ok
|
||||
}
|
||||
|
||||
pub fn reset_config_meta_store() -> bool {
|
||||
let Some(conn) = open_db() else {
|
||||
return false;
|
||||
};
|
||||
let tx = match conn.unchecked_transaction() {
|
||||
Ok(tx) => tx,
|
||||
Err(e) => {
|
||||
ohrs_log_error!(
|
||||
"[Rust] failed to start config store reset transaction: {}",
|
||||
e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = tx.execute("DELETE FROM stored_config_fields", []) {
|
||||
ohrs_log_error!("[Rust] failed to reset config fields: {}", e);
|
||||
let _ = tx.rollback();
|
||||
if !validate_snapshot_schema(&src) {
|
||||
hilog_error!("[Rust] invalid snapshot schema {}", source.display());
|
||||
return false;
|
||||
}
|
||||
if let Err(e) = tx.execute("DELETE FROM stored_configs", []) {
|
||||
ohrs_log_error!("[Rust] failed to reset config meta: {}", e);
|
||||
let _ = tx.rollback();
|
||||
let Some(mut dst) = open_db() else {
|
||||
return false;
|
||||
}
|
||||
|
||||
match tx.commit() {
|
||||
};
|
||||
match copy_snapshot_tables(&src, &mut dst) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
ohrs_log_error!("[Rust] failed to commit config store reset: {}", e);
|
||||
hilog_error!(
|
||||
"[Rust] failed to import snapshot {}: {}",
|
||||
source.display(),
|
||||
e
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -625,7 +331,7 @@ pub fn list_config_meta_entries() -> StoredConfigList {
|
||||
) {
|
||||
Ok(stmt) => stmt,
|
||||
Err(e) => {
|
||||
ohrs_log_error!("[Rust] failed to prepare list meta query: {}", e);
|
||||
hilog_error!("[Rust] failed to prepare list meta query: {}", e);
|
||||
return StoredConfigList { configs: vec![] };
|
||||
}
|
||||
};
|
||||
@@ -633,7 +339,7 @@ pub fn list_config_meta_entries() -> StoredConfigList {
|
||||
let rows = match stmt.query_map([], row_to_meta) {
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
ohrs_log_error!("[Rust] failed to list config meta rows: {}", e);
|
||||
hilog_error!("[Rust] failed to list config meta rows: {}", e);
|
||||
return StoredConfigList { configs: vec![] };
|
||||
}
|
||||
};
|
||||
@@ -652,6 +358,59 @@ pub fn get_config_meta(config_id: &str) -> Option<StoredConfigMeta> {
|
||||
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,
|
||||
@@ -733,45 +492,19 @@ pub fn set_config_display_name(
|
||||
Some(to_meta(record))
|
||||
}
|
||||
|
||||
pub fn set_config_favorite(config_id: String, favorite: bool) -> Option<StoredConfigMeta> {
|
||||
let conn = open_db()?;
|
||||
let now = now_ts_string();
|
||||
let tx = conn.unchecked_transaction().ok()?;
|
||||
pub fn delete_config_meta(config_id: &str) -> bool {
|
||||
let Some(conn) = open_db() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if favorite {
|
||||
tx.execute(
|
||||
"UPDATE stored_configs
|
||||
SET favorite = 0,
|
||||
updated_at = CASE WHEN favorite != 0 THEN ?1 ELSE updated_at END
|
||||
WHERE favorite != 0 AND config_id <> ?2",
|
||||
params![now, config_id.clone()],
|
||||
)
|
||||
.ok()?;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
let rows = tx
|
||||
.execute(
|
||||
"UPDATE stored_configs
|
||||
SET favorite = ?2, updated_at = ?3
|
||||
WHERE config_id = ?1",
|
||||
params![config_id.clone(), if favorite { 1 } else { 0 }, now],
|
||||
)
|
||||
.ok()?;
|
||||
if rows == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let meta = 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)?;
|
||||
tx.commit().ok()?;
|
||||
Some(meta)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,14 @@ 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)]
|
||||
@@ -58,13 +66,3 @@ pub struct KeyValuePair {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[napi(object)]
|
||||
pub struct SnapshotImportResult {
|
||||
pub ok: bool,
|
||||
pub error_code: String,
|
||||
pub error_message: String,
|
||||
pub snapshot_invalid: bool,
|
||||
}
|
||||
|
||||
@@ -1,74 +1,21 @@
|
||||
use super::{field_store, import_export, legacy_migration, validation};
|
||||
use crate::config::storage::config_meta::{
|
||||
get_config_meta, init_config_meta_store, list_config_meta_entries, open_db,
|
||||
reset_config_meta_store, upsert_config_meta_in_tx,
|
||||
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 once_cell::sync::Lazy;
|
||||
use ohos_hilog_binding::{hilog_debug, hilog_error};
|
||||
use rusqlite::params;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Instant;
|
||||
|
||||
static CONFIG_ROOT_DIR: Mutex<Option<PathBuf>> = Mutex::new(None);
|
||||
static RUNTIME_CONFIG_SNAPSHOTS: Lazy<Mutex<HashMap<String, RuntimeConfigSnapshot>>> =
|
||||
Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
pub(crate) const CONFIG_DIR_NAME: &str = "easytier-configs";
|
||||
pub(crate) const KERNEL_SOCKET_FILE_NAME: &str = "easytier-kernel.sock";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RuntimeConfigSnapshot {
|
||||
pub display_name: String,
|
||||
pub config: NetworkConfig,
|
||||
}
|
||||
|
||||
pub(crate) fn cache_runtime_config_snapshot(
|
||||
config_id: String,
|
||||
display_name: String,
|
||||
config: NetworkConfig,
|
||||
) {
|
||||
if let Ok(mut guard) = RUNTIME_CONFIG_SNAPSHOTS.lock() {
|
||||
guard.insert(
|
||||
config_id,
|
||||
RuntimeConfigSnapshot {
|
||||
display_name,
|
||||
config,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_runtime_config_snapshot(config_id: &str) {
|
||||
if let Ok(mut guard) = RUNTIME_CONFIG_SNAPSHOTS.lock() {
|
||||
guard.remove(config_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_runtime_config_snapshot(config_id: &str) -> Option<RuntimeConfigSnapshot> {
|
||||
RUNTIME_CONFIG_SNAPSHOTS
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.get(config_id).cloned())
|
||||
}
|
||||
|
||||
pub(crate) fn get_runtime_config_route_overrides(config_id: &str) -> (Vec<String>, Vec<String>) {
|
||||
RUNTIME_CONFIG_SNAPSHOTS
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| {
|
||||
guard.get(config_id).map(|snapshot| {
|
||||
(
|
||||
snapshot.config.routes.clone(),
|
||||
snapshot.config.proxy_cidrs.clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn config_root_dir() -> Option<PathBuf> {
|
||||
CONFIG_ROOT_DIR
|
||||
.lock()
|
||||
@@ -88,7 +35,7 @@ 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) {
|
||||
ohrs_log_error!(
|
||||
hilog_error!(
|
||||
"[Rust] failed to create config dir {}: {}",
|
||||
configs_dir.display(),
|
||||
e
|
||||
@@ -101,7 +48,7 @@ pub fn init_config_store(root_dir: String) -> bool {
|
||||
*guard = Some(root.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
ohrs_log_error!("[Rust] failed to lock config root dir: {}", e);
|
||||
hilog_error!("[Rust] failed to lock config root dir: {}", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -110,27 +57,14 @@ pub fn init_config_store(root_dir: String) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
ohrs_log_debug!(
|
||||
hilog_debug!(
|
||||
"[Rust] initialized config repo at {}",
|
||||
configs_dir.display()
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn reset_config_store() -> bool {
|
||||
if !reset_config_meta_store() {
|
||||
return false;
|
||||
}
|
||||
if let Ok(mut guard) = RUNTIME_CONFIG_SNAPSHOTS.lock() {
|
||||
guard.clear();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn migrate_legacy_file_if_needed(config_id: &str) -> Option<()> {
|
||||
if validation::validate_config_id(config_id).is_err() {
|
||||
return None;
|
||||
}
|
||||
legacy_migration::migrate_legacy_file_if_needed(
|
||||
&config_root_dir(),
|
||||
CONFIG_DIR_NAME,
|
||||
@@ -147,7 +81,7 @@ pub fn save_config_record(
|
||||
let config = match validation::validate_config_json(&config_json, config_id.clone()) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
ohrs_log_error!("[Rust] save_config_record failed {}", e);
|
||||
hilog_error!("[Rust] save_config_record failed {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
@@ -155,7 +89,7 @@ pub fn save_config_record(
|
||||
let normalized_json = match serde_json::to_string(&config) {
|
||||
Ok(raw) => raw,
|
||||
Err(e) => {
|
||||
ohrs_log_error!(
|
||||
hilog_error!(
|
||||
"[Rust] failed to serialize normalized config {}: {}",
|
||||
config_id,
|
||||
e
|
||||
@@ -171,15 +105,15 @@ pub fn save_config_record(
|
||||
|
||||
let conn = open_db()?;
|
||||
let tx = conn.unchecked_transaction().ok()?;
|
||||
let existing_meta = tx
|
||||
.query_row(
|
||||
"SELECT favorite, temporary FROM stored_configs WHERE config_id = ?1",
|
||||
params![config_id.clone()],
|
||||
|row| Ok((row.get::<_, i64>(0)? != 0, row.get::<_, i64>(1)? != 0)),
|
||||
)
|
||||
.ok();
|
||||
let favorite = existing_meta.map(|meta| meta.0).unwrap_or(false);
|
||||
let temporary = existing_meta.map(|meta| meta.1).unwrap_or(false);
|
||||
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)?;
|
||||
@@ -199,52 +133,30 @@ pub fn save_config_record(
|
||||
}
|
||||
|
||||
pub fn load_config_json(config_id: &str) -> Option<String> {
|
||||
validation::validate_config_id(config_id).ok()?;
|
||||
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> {
|
||||
validation::validate_config_id(config_id).ok()?;
|
||||
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> {
|
||||
let total_start = Instant::now();
|
||||
validation::validate_config_id(config_id).ok()?;
|
||||
migrate_legacy_file_if_needed(config_id)?;
|
||||
let open_start = Instant::now();
|
||||
let conn = open_db()?;
|
||||
let open_elapsed = open_start.elapsed();
|
||||
let query_start = Instant::now();
|
||||
let result = conn
|
||||
.query_row(
|
||||
"SELECT field_json FROM stored_config_fields
|
||||
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();
|
||||
ohrs_log_debug!(
|
||||
"[Rust] get_config_field_value config={} field={} found={} open_ms={} query_ms={} total_ms={} len={}",
|
||||
config_id,
|
||||
field,
|
||||
result.is_some(),
|
||||
open_elapsed.as_millis(),
|
||||
query_start.elapsed().as_millis(),
|
||||
total_start.elapsed().as_millis(),
|
||||
result.as_ref().map(|value| value.len()).unwrap_or(0)
|
||||
);
|
||||
result
|
||||
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 validation::validate_config_id(config_id).is_err() {
|
||||
return false;
|
||||
}
|
||||
if field.contains('.') {
|
||||
return false;
|
||||
}
|
||||
@@ -279,12 +191,15 @@ pub fn set_config_field_value(config_id: &str, field: &str, json_value: &str) ->
|
||||
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> {
|
||||
validation::validate_config_id(&config_id).ok()?;
|
||||
let raw = get_default_config_json()?;
|
||||
let mut config = serde_json::from_str::<NetworkConfig>(&raw).ok()?;
|
||||
config.instance_id = Some(config_id.clone());
|
||||
@@ -293,21 +208,11 @@ pub fn create_config_record(config_id: String, display_name: String) -> Option<S
|
||||
}
|
||||
|
||||
pub fn start_kernel_with_config_id(config_id: &str) -> bool {
|
||||
if validation::validate_config_id(config_id).is_err() {
|
||||
return false;
|
||||
}
|
||||
let raw = match load_config_json(config_id) {
|
||||
Some(raw) => raw,
|
||||
None => return false,
|
||||
};
|
||||
let display_name = get_config_meta(config_id)
|
||||
.map(|meta| meta.display_name)
|
||||
.unwrap_or_else(|| config_id.to_string());
|
||||
let started = crate::run_network_instance_from_json(&raw);
|
||||
if started && let Ok(config) = serde_json::from_str::<NetworkConfig>(&raw) {
|
||||
cache_runtime_config_snapshot(config_id.to_string(), display_name, config);
|
||||
}
|
||||
started
|
||||
crate::run_network_instance_from_json(&raw)
|
||||
}
|
||||
|
||||
pub fn list_config_meta_json() -> String {
|
||||
@@ -315,9 +220,6 @@ pub fn list_config_meta_json() -> String {
|
||||
}
|
||||
|
||||
pub fn delete_config_record(config_id: &str) -> bool {
|
||||
if validation::validate_config_id(config_id).is_err() {
|
||||
return false;
|
||||
}
|
||||
if let Some(path) = legacy_config_file_path(config_id) {
|
||||
if path.exists() {
|
||||
let _ = std::fs::remove_file(path);
|
||||
@@ -332,24 +234,14 @@ pub fn delete_config_record(config_id: &str) -> bool {
|
||||
"DELETE FROM stored_config_fields WHERE config_id = ?1",
|
||||
params![config_id],
|
||||
) {
|
||||
ohrs_log_error!("[Rust] failed to delete config fields {}: {}", config_id, e);
|
||||
hilog_error!("[Rust] failed to delete config fields {}: {}", config_id, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
match conn.execute(
|
||||
"DELETE FROM stored_configs WHERE config_id = ?1",
|
||||
params![config_id],
|
||||
) {
|
||||
Ok(rows) => rows > 0,
|
||||
Err(e) => {
|
||||
ohrs_log_error!("[Rust] failed to delete config meta {}: {}", config_id, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
delete_config_meta(config_id)
|
||||
}
|
||||
|
||||
pub fn export_config_toml(config_id: &str) -> Option<ExportTomlResult> {
|
||||
validation::validate_config_id(config_id).ok()?;
|
||||
let record = get_config_record(config_id)?;
|
||||
import_export::export_config_toml_from_record(&record)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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};
|
||||
|
||||
@@ -42,7 +43,7 @@ pub(super) fn replace_config_fields(
|
||||
"DELETE FROM stored_config_fields WHERE config_id = ?1",
|
||||
params![config_id],
|
||||
) {
|
||||
ohrs_log_error!(
|
||||
hilog_error!(
|
||||
"[Rust] failed to clear existing config fields {}: {}",
|
||||
config_id,
|
||||
e
|
||||
@@ -57,7 +58,7 @@ pub(super) fn replace_config_fields(
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
params![config_id, field_name, field_json, now_ts_string()],
|
||||
) {
|
||||
ohrs_log_error!("[Rust] failed to persist config field {}: {}", config_id, e);
|
||||
hilog_error!("[Rust] failed to persist config field {}: {}", config_id, e);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
use crate::config::storage::config_meta::get_config_meta;
|
||||
use ohos_hilog_binding::hilog_error;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::validation;
|
||||
|
||||
pub(super) fn legacy_config_file_path(
|
||||
root_dir: &Option<PathBuf>,
|
||||
config_dir_name: &str,
|
||||
config_id: &str,
|
||||
) -> Option<PathBuf> {
|
||||
if !validation::is_valid_config_id(config_id) {
|
||||
ohrs_log_error!("[Rust] invalid legacy config_id {}", config_id);
|
||||
return None;
|
||||
}
|
||||
root_dir.as_ref().map(|root| {
|
||||
root.join(config_dir_name)
|
||||
.join(format!("{}.json", config_id))
|
||||
@@ -40,7 +35,7 @@ pub(super) fn migrate_legacy_file_if_needed(
|
||||
save_config_record(config_id.to_string(), display_name, raw)?;
|
||||
|
||||
if let Err(e) = std::fs::remove_file(&legacy_path) {
|
||||
ohrs_log_error!(
|
||||
hilog_error!(
|
||||
"[Rust] failed to remove legacy config file {}: {}",
|
||||
legacy_path.display(),
|
||||
e
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use serde_json::{Map, Value};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(super) fn validate_config_id(config_id: &str) -> Result<(), String> {
|
||||
if config_id.is_empty() {
|
||||
return Err("config_id is required".to_string());
|
||||
}
|
||||
Uuid::parse_str(config_id)
|
||||
.map(|_| ())
|
||||
.map_err(|e| format!("invalid config_id {}: {}", config_id, e))
|
||||
}
|
||||
|
||||
pub(super) fn is_valid_config_id(config_id: &str) -> bool {
|
||||
validate_config_id(config_id).is_ok()
|
||||
}
|
||||
|
||||
pub(super) fn normalize_config_id(
|
||||
mut config: NetworkConfig,
|
||||
requested_id: String,
|
||||
) -> Result<NetworkConfig, String> {
|
||||
validate_config_id(&requested_id)?;
|
||||
if requested_id.is_empty() {
|
||||
return Err("config_id is required".to_string());
|
||||
}
|
||||
config.instance_id = Some(requested_id);
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
use crate::config;
|
||||
use crate::config::types::stored_config::SnapshotImportResult;
|
||||
|
||||
pub(crate) fn init_config_store(root_dir: String) -> bool {
|
||||
config::repository::init_config_store(root_dir)
|
||||
}
|
||||
|
||||
pub(crate) fn reset_config_store() -> bool {
|
||||
config::repository::reset_config_store()
|
||||
}
|
||||
|
||||
pub(crate) fn list_configs() -> String {
|
||||
config::repository::list_config_meta_json()
|
||||
}
|
||||
@@ -41,10 +36,6 @@ pub(crate) fn set_config_field(config_id: String, field: String, json_value: Str
|
||||
config::repository::set_config_field_value(&config_id, &field, &json_value)
|
||||
}
|
||||
|
||||
pub(crate) fn set_config_favorite(config_id: String, favorite: bool) -> bool {
|
||||
config::storage::config_meta::set_config_favorite(config_id, favorite).is_some()
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -61,9 +52,3 @@ pub(crate) fn export_config_store_snapshot(target_path: String) -> bool {
|
||||
pub(crate) fn import_config_store_snapshot(source_path: String) -> bool {
|
||||
config::storage::config_meta::import_config_store_snapshot(source_path)
|
||||
}
|
||||
|
||||
pub(crate) fn import_config_store_snapshot_with_result(
|
||||
source_path: String,
|
||||
) -> SnapshotImportResult {
|
||||
config::storage::config_meta::import_config_store_snapshot_with_result(source_path)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
use crate::config::repository::{clear_runtime_config_snapshot, get_runtime_config_snapshot};
|
||||
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, RuntimeInstanceState, TunAggregateState, clear_tun_attached,
|
||||
is_tun_attached, mark_tun_attached, runtime_instance_from_config_snapshot,
|
||||
RuntimeAggregateState, TunAggregateState, clear_tun_attached, mark_tun_attached,
|
||||
runtime_instance_from_running_info,
|
||||
};
|
||||
use crate::{ASYNC_RUNTIME, INSTANCE_MANAGER, WEB_CLIENTS};
|
||||
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,
|
||||
@@ -26,12 +29,9 @@ pub(crate) fn stop_kernel(
|
||||
) -> bool {
|
||||
clear_tun_attached(&config_id);
|
||||
if stop_web_client(&config_id) {
|
||||
clear_runtime_config_snapshot(&config_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
let _ = stop_local_socket_server_inner();
|
||||
|
||||
let Some(instance_id) = parse_instance_uuid(&config_id) else {
|
||||
return false;
|
||||
};
|
||||
@@ -40,20 +40,9 @@ pub(crate) fn stop_kernel(
|
||||
.delete_network_instance(vec![instance_id])
|
||||
.map(|_| true)
|
||||
.unwrap_or_else(|err| {
|
||||
ohrs_log_error!("[Rust] stop_kernel failed {}: {}", config_id, err);
|
||||
hilog_error!("[Rust] stop_kernel failed {}: {}", config_id, err);
|
||||
false
|
||||
});
|
||||
if ret {
|
||||
clear_runtime_config_snapshot(&config_id);
|
||||
}
|
||||
let has_active_instances = !INSTANCE_MANAGER.list_network_instance_ids().is_empty();
|
||||
let has_web_clients = WEB_CLIENTS
|
||||
.lock()
|
||||
.map(|guard| !guard.is_empty())
|
||||
.unwrap_or(false);
|
||||
if has_active_instances || has_web_clients {
|
||||
let _ = start_local_socket_server_inner();
|
||||
}
|
||||
maybe_stop_local_socket_server();
|
||||
ret
|
||||
}
|
||||
@@ -70,10 +59,10 @@ pub(crate) fn stop_network_instance(
|
||||
}
|
||||
|
||||
pub(crate) fn collect_network_infos() -> Vec<KeyValuePair> {
|
||||
let infos = match ASYNC_RUNTIME.block_on(INSTANCE_MANAGER.collect_network_infos()) {
|
||||
let infos = match INSTANCE_MANAGER.collect_network_infos_sync() {
|
||||
Ok(infos) => infos,
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] collect network infos failed {}", err);
|
||||
hilog_error!("[Rust] collect network infos failed {}", err);
|
||||
return vec![];
|
||||
}
|
||||
};
|
||||
@@ -97,7 +86,7 @@ pub(crate) fn set_tun_fd(
|
||||
parse_instance_uuid: impl Fn(&str) -> Option<uuid::Uuid>,
|
||||
) -> bool {
|
||||
let Some(instance_id) = parse_instance_uuid(&config_id) else {
|
||||
ohrs_log_error!("[Rust] set_tun_fd invalid instance id: {}", config_id);
|
||||
hilog_error!("[Rust] set_tun_fd invalid instance id: {}", config_id);
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -105,7 +94,7 @@ pub(crate) fn set_tun_fd(
|
||||
.set_tun_fd(&instance_id, fd)
|
||||
.map(|_| {
|
||||
mark_tun_attached(&config_id);
|
||||
ohrs_log_info!(
|
||||
hilog_info!(
|
||||
"[Rust] set_tun_fd success instance={} fd={} marked_attached=true",
|
||||
config_id,
|
||||
fd
|
||||
@@ -113,16 +102,20 @@ pub(crate) fn set_tun_fd(
|
||||
true
|
||||
})
|
||||
.unwrap_or_else(|err| {
|
||||
ohrs_log_error!("[Rust] set_tun_fd failed {}: {}", config_id, err);
|
||||
hilog_error!("[Rust] set_tun_fd failed {}: {}", config_id, err);
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn collect_runtime_state() -> RuntimeAggregateState {
|
||||
let infos = match ASYNC_RUNTIME.block_on(INSTANCE_MANAGER.collect_network_infos()) {
|
||||
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) => {
|
||||
ohrs_log_error!("[Rust] collect network infos failed {}", err);
|
||||
hilog_error!("[Rust] collect network infos failed {}", err);
|
||||
return RuntimeAggregateState {
|
||||
instances: vec![],
|
||||
tun: TunAggregateState {
|
||||
@@ -136,67 +129,30 @@ pub(crate) fn collect_runtime_state() -> RuntimeAggregateState {
|
||||
};
|
||||
}
|
||||
};
|
||||
let mut live_infos = infos
|
||||
.into_iter()
|
||||
.map(|(instance_id, info)| (instance_id.to_string(), info))
|
||||
.collect::<std::collections::HashMap<_, _>>();
|
||||
let mut active_config_ids = live_infos.keys().cloned().collect::<Vec<_>>();
|
||||
if let Ok(guard) = WEB_CLIENTS.lock() {
|
||||
for config_id in guard.keys() {
|
||||
if !active_config_ids.iter().any(|value| value == config_id) {
|
||||
active_config_ids.push(config_id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut instances = Vec::with_capacity(active_config_ids.len());
|
||||
for config_id in active_config_ids {
|
||||
if let Some(info) = live_infos.remove(&config_id) {
|
||||
let snapshot = get_runtime_config_snapshot(&config_id);
|
||||
let display_name = snapshot
|
||||
.as_ref()
|
||||
.map(|snapshot| snapshot.display_name.clone())
|
||||
.unwrap_or_else(|| config_id.clone());
|
||||
let magic_dns_enabled = snapshot
|
||||
.as_ref()
|
||||
.and_then(|snapshot| snapshot.config.enable_magic_dns)
|
||||
.unwrap_or(false);
|
||||
let need_exit_node = snapshot
|
||||
.as_ref()
|
||||
.map(|snapshot| !snapshot.config.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,
|
||||
));
|
||||
} else if let Some(snapshot) = get_runtime_config_snapshot(&config_id) {
|
||||
instances.push(runtime_instance_from_config_snapshot(
|
||||
config_id,
|
||||
snapshot.display_name,
|
||||
snapshot.config,
|
||||
true,
|
||||
));
|
||||
} else {
|
||||
let tun_attached = is_tun_attached(&config_id);
|
||||
instances.push(RuntimeInstanceState {
|
||||
config_id: config_id.clone(),
|
||||
instance_id: config_id.clone(),
|
||||
display_name: config_id.clone(),
|
||||
running: true,
|
||||
tun_required: tun_attached,
|
||||
tun_attached,
|
||||
magic_dns_enabled: false,
|
||||
need_exit_node: false,
|
||||
error_message: None,
|
||||
my_node_info: None,
|
||||
events: Vec::new(),
|
||||
routes: Vec::new(),
|
||||
peers: Vec::new(),
|
||||
});
|
||||
}
|
||||
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| {
|
||||
|
||||
@@ -32,13 +32,6 @@ pub(crate) fn send_local_socket_message(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn shrink_clients_if_sparse(clients: &mut Vec<UnixStream>) {
|
||||
let sparse_limit = clients.len().saturating_mul(2).max(4);
|
||||
if clients.capacity() > sparse_limit {
|
||||
clients.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn broadcast_local_socket_message(
|
||||
clients: &mut Vec<UnixStream>,
|
||||
message_type: &str,
|
||||
@@ -52,42 +45,6 @@ pub(crate) fn broadcast_local_socket_message(
|
||||
active_clients.push(client);
|
||||
}
|
||||
}
|
||||
shrink_clients_if_sparse(&mut active_clients);
|
||||
*clients = active_clients;
|
||||
delivered
|
||||
}
|
||||
|
||||
pub(crate) fn send_local_socket_json_payload_message(
|
||||
stream: &mut UnixStream,
|
||||
message_type: &str,
|
||||
payload_json: &str,
|
||||
) -> std::io::Result<()> {
|
||||
let message_type_json = serde_json::to_string(message_type)
|
||||
.map_err(|err| Error::new(ErrorKind::InvalidData, err.to_string()))?;
|
||||
let mut raw = Vec::with_capacity(message_type_json.len() + payload_json.len() + 38);
|
||||
raw.extend_from_slice(b"{\"messageType\":");
|
||||
raw.extend_from_slice(message_type_json.as_bytes());
|
||||
raw.extend_from_slice(b",\"payloadJson\":");
|
||||
raw.extend_from_slice(payload_json.as_bytes());
|
||||
raw.extend_from_slice(b"}\n");
|
||||
stream.write_all(&raw)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn broadcast_local_socket_json_payload_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_json_payload_message(&mut client, message_type, payload_json).is_ok() {
|
||||
delivered = true;
|
||||
active_clients.push(client);
|
||||
}
|
||||
}
|
||||
shrink_clients_if_sparse(&mut active_clients);
|
||||
*clients = active_clients;
|
||||
delivered
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
use crate::config::repository::get_runtime_config_route_overrides;
|
||||
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> {
|
||||
let normalized = route.split("->").next().unwrap_or(route).trim();
|
||||
normalized
|
||||
route
|
||||
.parse::<IpNet>()
|
||||
.ok()
|
||||
.map(|network| match network {
|
||||
@@ -14,7 +22,7 @@ fn normalize_route_cidr(route: &str) -> Option<String> {
|
||||
IpNet::V6(net) => net.trunc().to_string(),
|
||||
})
|
||||
.or_else(|| {
|
||||
normalized.parse::<IpAddr>().ok().map(|addr| match addr {
|
||||
route.parse::<IpAddr>().ok().map(|addr| match addr {
|
||||
IpAddr::V4(ip) => format!("{}/32", ip),
|
||||
IpAddr::V6(ip) => format!("{}/128", ip),
|
||||
})
|
||||
@@ -59,9 +67,8 @@ pub(crate) fn aggregate_tun_routes(instance: &RuntimeInstanceState) -> Vec<Strin
|
||||
.my_node_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.virtual_ipv4_cidr.clone());
|
||||
let (manual_routes, config_proxy_cidrs) =
|
||||
get_runtime_config_route_overrides(&instance.config_id);
|
||||
let runtime_proxy_cidrs = instance
|
||||
let manual_routes = load_manual_routes(&instance.config_id);
|
||||
let proxy_cidrs = instance
|
||||
.routes
|
||||
.iter()
|
||||
.flat_map(|route| route.proxy_cidrs.iter().cloned())
|
||||
@@ -73,9 +80,15 @@ pub(crate) fn aggregate_tun_routes(instance: &RuntimeInstanceState) -> Vec<Strin
|
||||
}
|
||||
|
||||
raw_routes.extend(manual_routes.iter().cloned());
|
||||
raw_routes.extend(config_proxy_cidrs.iter().cloned());
|
||||
raw_routes.extend(runtime_proxy_cidrs.iter().cloned());
|
||||
simplify_routes(raw_routes)
|
||||
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> {
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
use super::protocol::{
|
||||
TunRequestPayload, broadcast_local_socket_json_payload_message, broadcast_local_socket_message,
|
||||
};
|
||||
use crate::collect_runtime_state_inner;
|
||||
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 crate::runtime::state::runtime_state::{
|
||||
PeerConnInfo as RuntimePeerConnInfo, RuntimeAggregateState, peer_conn_to_view,
|
||||
};
|
||||
use crate::{ASYNC_RUNTIME, INSTANCE_MANAGER};
|
||||
use easytier::common::global_ctx::{EventBusSubscriber, GlobalCtxEvent};
|
||||
use easytier::proto::api::instance::ListPeerRequest;
|
||||
use easytier::proto::rpc_types::controller::BaseController;
|
||||
use ohos_hilog_binding::{hilog_error, hilog_info};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Serialize;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::hash::Hash;
|
||||
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, Instant};
|
||||
use std::time::Duration;
|
||||
|
||||
struct LocalSocketState {
|
||||
stop_flag: std::sync::Arc<AtomicBool>,
|
||||
@@ -30,287 +20,12 @@ struct LocalSocketState {
|
||||
}
|
||||
|
||||
static LOCAL_SOCKET_STATE: Lazy<Mutex<Option<LocalSocketState>>> = Lazy::new(|| Mutex::new(None));
|
||||
const SOCKET_TICK_INTERVAL: Duration = Duration::from_millis(250);
|
||||
const TRAFFIC_STATS_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const INSTANCE_POLL_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const TUN_FAST_CHECK_WINDOW: Duration = Duration::from_secs(8);
|
||||
const EVENT_RECEIVER_SYNC_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TrafficStatsPayload {
|
||||
instances: Vec<InstanceTrafficStats>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct InstanceTrafficStats {
|
||||
config_id: String,
|
||||
instance_id: String,
|
||||
rx_bytes: i64,
|
||||
tx_bytes: i64,
|
||||
peers: Vec<PeerTrafficStats>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PeerTrafficStats {
|
||||
peer_id: i64,
|
||||
rx_bytes: i64,
|
||||
tx_bytes: i64,
|
||||
total_bytes: i64,
|
||||
latency_us: i64,
|
||||
loss_rate: f64,
|
||||
}
|
||||
|
||||
struct PendingPeerEvent {
|
||||
event: &'static str,
|
||||
instance_id: String,
|
||||
peer_id: i64,
|
||||
conn: Option<RuntimePeerConnInfo>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DrainedKernelEvents {
|
||||
tun_refresh: bool,
|
||||
topology_lost: bool,
|
||||
peer_events: Vec<PendingPeerEvent>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RuntimePeerEventPayload {
|
||||
event: &'static str,
|
||||
config_id: String,
|
||||
instance_id: String,
|
||||
peer_id: i64,
|
||||
conn: Option<RuntimePeerConnInfo>,
|
||||
}
|
||||
|
||||
fn shrink_hash_map_if_sparse<K: Eq + Hash, V>(map: &mut HashMap<K, V>) {
|
||||
let sparse_limit = map.len().saturating_mul(2).max(8);
|
||||
if map.capacity() > sparse_limit {
|
||||
map.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
fn shrink_hash_set_if_sparse<T: Eq + Hash>(set: &mut HashSet<T>) {
|
||||
let sparse_limit = set.len().saturating_mul(2).max(8);
|
||||
if set.capacity() > sparse_limit {
|
||||
set.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_tun_event_receivers(receivers: &mut HashMap<String, EventBusSubscriber>) {
|
||||
let mut active_instance_ids = HashSet::new();
|
||||
for instance in INSTANCE_MANAGER.iter() {
|
||||
let instance_id = instance.key().to_string();
|
||||
active_instance_ids.insert(instance_id.clone());
|
||||
if !receivers.contains_key(&instance_id)
|
||||
&& let Some(receiver) = instance.value().subscribe_event()
|
||||
{
|
||||
receivers.insert(instance_id, receiver);
|
||||
}
|
||||
}
|
||||
receivers.retain(|instance_id, _| active_instance_ids.contains(instance_id));
|
||||
shrink_hash_map_if_sparse(receivers);
|
||||
}
|
||||
|
||||
fn event_needs_tun_refresh(event: &GlobalCtxEvent) -> bool {
|
||||
matches!(
|
||||
event,
|
||||
GlobalCtxEvent::DhcpIpv4Changed(_, _)
|
||||
| GlobalCtxEvent::ProxyCidrsUpdated(_, _)
|
||||
| GlobalCtxEvent::PublicIpv6RoutesUpdated(_, _)
|
||||
)
|
||||
}
|
||||
|
||||
fn drain_kernel_events(receivers: &mut HashMap<String, EventBusSubscriber>) -> DrainedKernelEvents {
|
||||
let mut drained = DrainedKernelEvents::default();
|
||||
let mut closed_receivers = Vec::new();
|
||||
for (instance_id, receiver) in receivers.iter_mut() {
|
||||
loop {
|
||||
match receiver.try_recv() {
|
||||
Ok(event) => {
|
||||
drained.tun_refresh = event_needs_tun_refresh(&event) || drained.tun_refresh;
|
||||
match event {
|
||||
GlobalCtxEvent::PeerAdded(peer_id) => {
|
||||
drained.peer_events.push(PendingPeerEvent {
|
||||
event: "peer_added",
|
||||
instance_id: instance_id.clone(),
|
||||
peer_id: peer_id as i64,
|
||||
conn: None,
|
||||
});
|
||||
}
|
||||
GlobalCtxEvent::PeerRemoved(peer_id) => {
|
||||
drained.peer_events.push(PendingPeerEvent {
|
||||
event: "peer_removed",
|
||||
instance_id: instance_id.clone(),
|
||||
peer_id: peer_id as i64,
|
||||
conn: None,
|
||||
});
|
||||
}
|
||||
GlobalCtxEvent::PeerConnAdded(conn_info) => {
|
||||
let peer_id = conn_info.peer_id as i64;
|
||||
drained.peer_events.push(PendingPeerEvent {
|
||||
event: "peer_conn_added",
|
||||
instance_id: instance_id.clone(),
|
||||
peer_id,
|
||||
conn: Some(peer_conn_to_view(conn_info)),
|
||||
});
|
||||
}
|
||||
GlobalCtxEvent::PeerConnRemoved(conn_info) => {
|
||||
let peer_id = conn_info.peer_id as i64;
|
||||
drained.peer_events.push(PendingPeerEvent {
|
||||
event: "peer_conn_removed",
|
||||
instance_id: instance_id.clone(),
|
||||
peer_id,
|
||||
conn: Some(peer_conn_to_view(conn_info)),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::TryRecvError::Empty) => break,
|
||||
Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) => {
|
||||
drained.topology_lost = true;
|
||||
continue;
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::TryRecvError::Closed) => {
|
||||
closed_receivers.push(instance_id.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for instance_id in closed_receivers {
|
||||
receivers.remove(&instance_id);
|
||||
}
|
||||
drained
|
||||
}
|
||||
|
||||
fn broadcast_runtime_peer_events(
|
||||
clients: &mut Vec<UnixStream>,
|
||||
peer_events: Vec<PendingPeerEvent>,
|
||||
) {
|
||||
for event in peer_events {
|
||||
let payload = RuntimePeerEventPayload {
|
||||
event: event.event,
|
||||
config_id: event.instance_id.clone(),
|
||||
instance_id: event.instance_id,
|
||||
peer_id: event.peer_id,
|
||||
conn: event.conn,
|
||||
};
|
||||
match serde_json::to_string(&payload) {
|
||||
Ok(json) => {
|
||||
let _ = broadcast_local_socket_json_payload_message(
|
||||
clients,
|
||||
"runtime_peer_event",
|
||||
&json,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] serialize runtime peer event failed: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tun_candidate_ids(snapshot: &RuntimeAggregateState) -> HashSet<String> {
|
||||
snapshot
|
||||
.instances
|
||||
.iter()
|
||||
.filter(|instance| instance.running && instance.tun_required)
|
||||
.map(|instance| instance.instance_id.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn collect_traffic_stats() -> TrafficStatsPayload {
|
||||
let services = INSTANCE_MANAGER
|
||||
.iter()
|
||||
.filter_map(|instance| {
|
||||
instance
|
||||
.value()
|
||||
.get_api_service()
|
||||
.map(|api_service| (instance.key().to_string(), api_service))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let instances = ASYNC_RUNTIME.block_on(async {
|
||||
let mut instances = Vec::new();
|
||||
for (instance_id, api_service) in services {
|
||||
let peers = match api_service
|
||||
.get_peer_manage_service()
|
||||
.list_peer(BaseController::default(), ListPeerRequest::default())
|
||||
.await
|
||||
{
|
||||
Ok(response) => response.peer_infos,
|
||||
Err(err) => {
|
||||
ohrs_log_debug!(
|
||||
"[Rust] collect traffic stats list_peer failed instance={}: {}",
|
||||
instance_id,
|
||||
err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut instance_rx_bytes = 0i64;
|
||||
let mut instance_tx_bytes = 0i64;
|
||||
let mut peer_stats = Vec::with_capacity(peers.len());
|
||||
|
||||
for peer in peers {
|
||||
let mut peer_rx_bytes = 0i64;
|
||||
let mut peer_tx_bytes = 0i64;
|
||||
let mut latency_us = i64::MAX;
|
||||
let mut loss_rate = 0f64;
|
||||
|
||||
for conn in peer.conns {
|
||||
if let Some(stats) = conn.stats {
|
||||
let rx_bytes = stats.rx_bytes as i64;
|
||||
let tx_bytes = stats.tx_bytes as i64;
|
||||
peer_rx_bytes += rx_bytes;
|
||||
peer_tx_bytes += tx_bytes;
|
||||
latency_us = latency_us.min(stats.latency_us as i64);
|
||||
}
|
||||
loss_rate = loss_rate.max(conn.loss_rate as f64);
|
||||
}
|
||||
|
||||
instance_rx_bytes += peer_rx_bytes;
|
||||
instance_tx_bytes += peer_tx_bytes;
|
||||
peer_stats.push(PeerTrafficStats {
|
||||
peer_id: peer.peer_id as i64,
|
||||
rx_bytes: peer_rx_bytes,
|
||||
tx_bytes: peer_tx_bytes,
|
||||
total_bytes: peer_rx_bytes + peer_tx_bytes,
|
||||
latency_us: if latency_us == i64::MAX {
|
||||
-1
|
||||
} else {
|
||||
latency_us
|
||||
},
|
||||
loss_rate,
|
||||
});
|
||||
}
|
||||
|
||||
instances.push(InstanceTrafficStats {
|
||||
config_id: instance_id.clone(),
|
||||
instance_id,
|
||||
rx_bytes: instance_rx_bytes,
|
||||
tx_bytes: instance_tx_bytes,
|
||||
peers: peer_stats,
|
||||
});
|
||||
}
|
||||
instances
|
||||
});
|
||||
|
||||
TrafficStatsPayload { instances }
|
||||
}
|
||||
|
||||
pub fn start_local_socket_server() -> bool {
|
||||
let socket_path = match kernel_socket_path() {
|
||||
Some(path) => path,
|
||||
None => {
|
||||
ohrs_log_error!("[Rust] kernel socket path unavailable");
|
||||
hilog_error!("[Rust] kernel socket path unavailable");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -319,7 +34,7 @@ pub fn start_local_socket_server() -> bool {
|
||||
Ok(guard) if guard.is_some() => return true,
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] lock localsocket state failed: {}", err);
|
||||
hilog_error!("[Rust] lock localsocket state failed: {}", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -331,7 +46,7 @@ pub fn start_local_socket_server() -> bool {
|
||||
let listener = match UnixListener::bind(&socket_path) {
|
||||
Ok(listener) => listener,
|
||||
Err(err) => {
|
||||
ohrs_log_error!(
|
||||
hilog_error!(
|
||||
"[Rust] bind localsocket failed {}: {}",
|
||||
socket_path.display(),
|
||||
err
|
||||
@@ -340,7 +55,7 @@ pub fn start_local_socket_server() -> bool {
|
||||
}
|
||||
};
|
||||
if let Err(err) = listener.set_nonblocking(true) {
|
||||
ohrs_log_error!("[Rust] set localsocket nonblocking failed: {}", err);
|
||||
hilog_error!("[Rust] set localsocket nonblocking failed: {}", err);
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
return false;
|
||||
}
|
||||
@@ -348,208 +63,102 @@ pub fn start_local_socket_server() -> bool {
|
||||
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_topology_json = String::new();
|
||||
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 tun_fast_until = Instant::now() + TUN_FAST_CHECK_WINDOW;
|
||||
let mut tun_bootstrap_done = false;
|
||||
let mut last_event_receiver_sync_at: Option<Instant> = None;
|
||||
let mut last_traffic_stats_at: Option<Instant> = None;
|
||||
let mut last_instance_poll_at: Option<Instant> = None;
|
||||
let mut tun_event_receivers = HashMap::<String, EventBusSubscriber>::new();
|
||||
let mut clients = Vec::<UnixStream>::new();
|
||||
|
||||
while !worker_stop_flag.load(Ordering::Relaxed) {
|
||||
let mut full_topology_dirty = false;
|
||||
let mut accepted_client = false;
|
||||
loop {
|
||||
match listener.accept() {
|
||||
Ok((stream, _addr)) => {
|
||||
accepted_client = true;
|
||||
full_topology_dirty = true;
|
||||
clients.push(stream);
|
||||
tun_fast_until = Instant::now() + TUN_FAST_CHECK_WINDOW;
|
||||
tun_bootstrap_done = false;
|
||||
}
|
||||
Err(err) if err.kind() == ErrorKind::WouldBlock => break,
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] accept localsocket failed: {}", err);
|
||||
hilog_error!("[Rust] accept localsocket failed: {}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if clients.is_empty() {
|
||||
if !last_topology_json.is_empty() {
|
||||
last_topology_json.clear();
|
||||
last_topology_json.shrink_to_fit();
|
||||
}
|
||||
delivered_tun_requests.clear();
|
||||
shrink_hash_set_if_sparse(&mut delivered_tun_requests);
|
||||
last_tun_route_signatures.clear();
|
||||
shrink_hash_map_if_sparse(&mut last_tun_route_signatures);
|
||||
tun_event_receivers.clear();
|
||||
shrink_hash_map_if_sparse(&mut tun_event_receivers);
|
||||
clients.shrink_to_fit();
|
||||
last_event_receiver_sync_at = None;
|
||||
last_traffic_stats_at = None;
|
||||
last_instance_poll_at = None;
|
||||
tun_bootstrap_done = false;
|
||||
thread::sleep(SOCKET_TICK_INTERVAL);
|
||||
continue;
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
let should_sync_event_receivers = accepted_client
|
||||
|| last_event_receiver_sync_at
|
||||
.map(|last| now.duration_since(last) >= EVENT_RECEIVER_SYNC_INTERVAL)
|
||||
.unwrap_or(true);
|
||||
if should_sync_event_receivers {
|
||||
sync_tun_event_receivers(&mut tun_event_receivers);
|
||||
last_event_receiver_sync_at = Some(now);
|
||||
}
|
||||
let drained_events = drain_kernel_events(&mut tun_event_receivers);
|
||||
let tun_refresh = drained_events.tun_refresh;
|
||||
let topology_lost = drained_events.topology_lost;
|
||||
let peer_events = drained_events.peer_events;
|
||||
if topology_lost {
|
||||
full_topology_dirty = true;
|
||||
}
|
||||
if tun_refresh {
|
||||
tun_bootstrap_done = false;
|
||||
tun_fast_until = now + TUN_FAST_CHECK_WINDOW;
|
||||
}
|
||||
if !peer_events.is_empty() {
|
||||
broadcast_runtime_peer_events(&mut clients, peer_events);
|
||||
}
|
||||
let should_collect_traffic_stats = last_traffic_stats_at
|
||||
.map(|last| now.duration_since(last) >= TRAFFIC_STATS_INTERVAL)
|
||||
.unwrap_or(true);
|
||||
if should_collect_traffic_stats {
|
||||
last_traffic_stats_at = Some(now);
|
||||
match serde_json::to_string(&collect_traffic_stats()) {
|
||||
Ok(json) => {
|
||||
let _ = broadcast_local_socket_json_payload_message(
|
||||
&mut clients,
|
||||
"traffic_stats",
|
||||
&json,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] serialize traffic stats failed: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
let should_poll_instance = last_instance_poll_at
|
||||
.map(|last| now.duration_since(last) >= INSTANCE_POLL_INTERVAL)
|
||||
.unwrap_or(true);
|
||||
let should_collect_topology = accepted_client
|
||||
|| full_topology_dirty
|
||||
|| tun_refresh
|
||||
|| should_poll_instance
|
||||
|| (!tun_bootstrap_done && now < tun_fast_until);
|
||||
if !should_collect_topology {
|
||||
thread::sleep(SOCKET_TICK_INTERVAL);
|
||||
continue;
|
||||
}
|
||||
|
||||
let snapshot = collect_runtime_state_inner();
|
||||
last_instance_poll_at = Some(now);
|
||||
match serde_json::to_string(&snapshot) {
|
||||
Ok(json) => {
|
||||
if accepted_client || full_topology_dirty || json != last_topology_json {
|
||||
let _ = broadcast_local_socket_json_payload_message(
|
||||
&mut clients,
|
||||
"runtime_topology",
|
||||
&json,
|
||||
);
|
||||
last_topology_json = json;
|
||||
}
|
||||
}
|
||||
let snapshot = get_runtime_snapshot_inner();
|
||||
let snapshot_json = match serde_json::to_string(&snapshot) {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] serialize runtime topology failed: {}", 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;
|
||||
}
|
||||
|
||||
let active_tun_candidate_ids = tun_candidate_ids(&snapshot);
|
||||
delivered_tun_requests
|
||||
.retain(|instance_id| active_tun_candidate_ids.contains(instance_id));
|
||||
last_tun_route_signatures
|
||||
.retain(|instance_id, _| active_tun_candidate_ids.contains(instance_id));
|
||||
shrink_hash_set_if_sparse(&mut delivered_tun_requests);
|
||||
shrink_hash_map_if_sparse(&mut last_tun_route_signatures);
|
||||
let mut saw_running_instance = false;
|
||||
let mut saw_tun_candidate = false;
|
||||
for instance in snapshot.instances.iter() {
|
||||
if instance.running {
|
||||
saw_running_instance = true;
|
||||
}
|
||||
if !(instance.running && instance.tun_required) {
|
||||
continue;
|
||||
}
|
||||
|
||||
saw_tun_candidate = true;
|
||||
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(&(
|
||||
&virtual_ipv4,
|
||||
&virtual_ipv4_cidr,
|
||||
&aggregated_routes,
|
||||
instance.magic_dns_enabled,
|
||||
instance.need_exit_node,
|
||||
))
|
||||
.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) => {
|
||||
ohrs_log_error!("[Rust] serialize tun request failed: {}", err);
|
||||
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 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
if !delivered_tun_requests.is_empty()
|
||||
|| (saw_running_instance && !saw_tun_candidate)
|
||||
|| now >= tun_fast_until
|
||||
{
|
||||
tun_bootstrap_done = true;
|
||||
}
|
||||
|
||||
thread::sleep(SOCKET_TICK_INTERVAL);
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -563,7 +172,7 @@ pub fn start_local_socket_server() -> bool {
|
||||
true
|
||||
}
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] lock localsocket state failed: {}", err);
|
||||
hilog_error!("[Rust] lock localsocket state failed: {}", err);
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -573,7 +182,7 @@ pub fn stop_local_socket_server() -> bool {
|
||||
let state = match LOCAL_SOCKET_STATE.lock() {
|
||||
Ok(mut guard) => guard.take(),
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] lock localsocket state failed: {}", err);
|
||||
hilog_error!("[Rust] lock localsocket state failed: {}", err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,46 +1,14 @@
|
||||
macro_rules! ohrs_log_error {
|
||||
($($arg:tt)*) => {{
|
||||
if $crate::platform::logging::log_manager::app_log_enabled(5) {
|
||||
$crate::platform::logging::log_manager::record_app_log(
|
||||
5,
|
||||
"RustOhrs",
|
||||
&std::format!($($arg)*),
|
||||
);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! ohrs_log_info {
|
||||
($($arg:tt)*) => {{
|
||||
if $crate::platform::logging::log_manager::app_log_enabled(4) {
|
||||
$crate::platform::logging::log_manager::record_app_log(
|
||||
4,
|
||||
"RustOhrs",
|
||||
&std::format!($($arg)*),
|
||||
);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! ohrs_log_debug {
|
||||
($($arg:tt)*) => {{
|
||||
if $crate::platform::logging::log_manager::app_log_enabled(3) {
|
||||
$crate::platform::logging::log_manager::record_app_log(
|
||||
3,
|
||||
"RustOhrs",
|
||||
&std::format!($($arg)*),
|
||||
);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
mod config;
|
||||
mod exports;
|
||||
mod kernel_bridge;
|
||||
mod platform;
|
||||
mod runtime;
|
||||
|
||||
use config::repository::{cache_runtime_config_snapshot, start_kernel_with_config_id};
|
||||
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,
|
||||
@@ -52,7 +20,7 @@ use config::services::share_link_service::{
|
||||
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, SnapshotImportResult};
|
||||
use config::types::stored_config::{KeyValuePair, SharedConfigLinkPayload};
|
||||
use easytier::common::constants::EASYTIER_VERSION;
|
||||
use easytier::common::{
|
||||
MachineIdOptions,
|
||||
@@ -63,11 +31,15 @@ use easytier::proto::api::manage::NetworkConfig;
|
||||
use easytier::proto::api::manage::NetworkingMethod;
|
||||
use easytier::web_client::{WebClient, WebClientHooks, run_web_client};
|
||||
use kernel_bridge::{
|
||||
start_local_socket_server as start_local_socket_server_inner,
|
||||
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 runtime::state::runtime_state::RuntimeAggregateState;
|
||||
use ohos_hilog_binding::{hilog_error, hilog_info};
|
||||
use runtime::state::runtime_state::{
|
||||
RuntimeAggregateState, TunAggregateState, clear_tun_attached, mark_tun_attached,
|
||||
runtime_instance_from_running_info,
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::format;
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -129,7 +101,7 @@ fn stop_web_client(config_id: &str) -> bool {
|
||||
let managed = match WEB_CLIENTS.lock() {
|
||||
Ok(mut guard) => guard.remove(config_id),
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] stop_web_client lock failed {}", err);
|
||||
hilog_error!("[Rust] stop_web_client lock failed {}", err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -155,7 +127,7 @@ fn stop_web_client(config_id: &str) -> bool {
|
||||
.delete_network_instance(tracked_ids)
|
||||
.map(|_| true)
|
||||
.unwrap_or_else(|err| {
|
||||
ohrs_log_error!(
|
||||
hilog_error!(
|
||||
"[Rust] stop config server instances failed {}: {}",
|
||||
config_id,
|
||||
err
|
||||
@@ -188,12 +160,12 @@ fn run_config_server_instance(config_id: &str, config: &NetworkConfig) -> bool {
|
||||
.next()
|
||||
.is_some()
|
||||
{
|
||||
ohrs_log_error!("[Rust] there is a running instance!");
|
||||
hilog_error!("[Rust] there is a running instance!");
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(config_server_url) = config.public_server_url.clone() else {
|
||||
ohrs_log_error!("[Rust] public_server_url missing for config server mode");
|
||||
hilog_error!("[Rust] public_server_url missing for config server mode");
|
||||
return false;
|
||||
};
|
||||
let hooks = Arc::new(TrackedWebClientHooks::default());
|
||||
@@ -220,7 +192,7 @@ fn run_config_server_instance(config_id: &str, config: &NetworkConfig) -> bool {
|
||||
let client = match client {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] start config server failed {}", err);
|
||||
hilog_error!("[Rust] start config server failed {}", err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -237,7 +209,7 @@ fn run_config_server_instance(config_id: &str, config: &NetworkConfig) -> bool {
|
||||
true
|
||||
}
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] store config server client failed {}", err);
|
||||
hilog_error!("[Rust] store config server client failed {}", err);
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -268,33 +240,29 @@ 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) => {
|
||||
ohrs_log_error!("[Rust] parse config failed {}", 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 {
|
||||
ohrs_log_error!("[Rust] config server config missing instance id");
|
||||
hilog_error!("[Rust] config server config missing instance id");
|
||||
return false;
|
||||
};
|
||||
let started = run_config_server_instance(config_id, &config);
|
||||
if started {
|
||||
cache_runtime_config_snapshot(config_id.to_string(), config_id.to_string(), config);
|
||||
}
|
||||
return started;
|
||||
return run_config_server_instance(config_id, &config);
|
||||
}
|
||||
|
||||
let cfg = match config.gen_config() {
|
||||
Ok(toml) => toml,
|
||||
Err(e) => {
|
||||
ohrs_log_error!("[Rust] parse config failed {}", e);
|
||||
hilog_error!("[Rust] parse config failed {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if !INSTANCE_MANAGER.list_network_instance_ids().is_empty() {
|
||||
ohrs_log_error!("[Rust] there is a running instance!");
|
||||
hilog_error!("[Rust] there is a running instance!");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -307,17 +275,14 @@ pub(crate) fn run_network_instance_from_json(cfg_json: &str) -> bool {
|
||||
.list_network_instance_ids()
|
||||
.contains(&inst_id)
|
||||
{
|
||||
ohrs_log_error!("[Rust] instance {} already exists", inst_id);
|
||||
hilog_error!("[Rust] instance {} already exists", inst_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
match INSTANCE_MANAGER.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) {
|
||||
Ok(_) => {
|
||||
cache_runtime_config_snapshot(inst_id.to_string(), inst_id.to_string(), config);
|
||||
true
|
||||
}
|
||||
Ok(_) => true,
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] start_kernel failed for {}: {}", inst_id, err);
|
||||
hilog_error!("[Rust] start_kernel failed for {}: {}", inst_id, err);
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -327,7 +292,7 @@ fn parse_instance_uuid(config_id: &str) -> Option<Uuid> {
|
||||
match Uuid::parse_str(config_id) {
|
||||
Ok(uuid) => Some(uuid),
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] invalid config_id {}: {}", config_id, err);
|
||||
hilog_error!("[Rust] invalid config_id {}: {}", config_id, err);
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -338,11 +303,6 @@ pub fn init_config_store(root_dir: String) -> bool {
|
||||
exports::config_api::init_config_store(root_dir)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn reset_config_store() -> bool {
|
||||
exports::config_api::reset_config_store()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn list_configs() -> String {
|
||||
exports::config_api::list_configs()
|
||||
@@ -393,11 +353,6 @@ pub fn set_config_field(config_id: String, field: String, json_value: String) ->
|
||||
exports::config_api::set_config_field(config_id, field, json_value)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn set_config_favorite(config_id: String, favorite: bool) -> bool {
|
||||
exports::config_api::set_config_favorite(config_id, favorite)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn import_toml(toml_text: String, display_name: Option<String>) -> Option<String> {
|
||||
exports::config_api::import_toml(toml_text, display_name)
|
||||
@@ -418,11 +373,6 @@ pub fn import_config_store_snapshot(source_path: String) -> bool {
|
||||
exports::config_api::import_config_store_snapshot(source_path)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn import_config_store_snapshot_with_result(source_path: String) -> SnapshotImportResult {
|
||||
exports::config_api::import_config_store_snapshot_with_result(source_path)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn start_kernel(config_id: String) -> bool {
|
||||
exports::runtime_api::start_kernel(config_id, start_kernel_with_config_id)
|
||||
@@ -517,8 +467,13 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn collect_runtime_state_inner() -> RuntimeAggregateState {
|
||||
exports::runtime_api::collect_runtime_state()
|
||||
#[napi]
|
||||
pub fn get_runtime_snapshot() -> RuntimeAggregateState {
|
||||
exports::runtime_api::get_runtime_snapshot()
|
||||
}
|
||||
|
||||
pub(crate) fn get_runtime_snapshot_inner() -> RuntimeAggregateState {
|
||||
exports::runtime_api::get_runtime_snapshot_inner()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
use napi_derive_ohos::napi;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::{self, Metadata, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
const LOG_DIR_NAME: &str = "easytier-logs";
|
||||
const LOG_FILE_PREFIX: &str = "easytier-";
|
||||
const LOG_FILE_SUFFIX: &str = ".log";
|
||||
const MAX_LOG_FILES: usize = 10;
|
||||
const MAX_MEMORY_LINES: usize = 500;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[napi(object)]
|
||||
pub struct LogFileInfo {
|
||||
pub file_name: String,
|
||||
pub display_name: String,
|
||||
pub size_bytes: i64,
|
||||
pub modified_ms: i64,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LogOptions {
|
||||
core_log: bool,
|
||||
debug_log: bool,
|
||||
}
|
||||
|
||||
impl Default for LogOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
core_log: false,
|
||||
debug_log: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct LogManagerState {
|
||||
log_dir: Option<PathBuf>,
|
||||
active_file: Option<PathBuf>,
|
||||
lines: VecDeque<String>,
|
||||
options: LogOptions,
|
||||
}
|
||||
|
||||
static LOG_MANAGER: Lazy<Mutex<LogManagerState>> =
|
||||
Lazy::new(|| Mutex::new(LogManagerState::default()));
|
||||
static CORE_LOG_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
static DEBUG_LOG_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn now_millis() -> u128 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn sanitize_name(raw: &str) -> String {
|
||||
let value = raw
|
||||
.chars()
|
||||
.map(|ch| {
|
||||
if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
|
||||
ch
|
||||
} else {
|
||||
'-'
|
||||
}
|
||||
})
|
||||
.collect::<String>();
|
||||
if value.is_empty() {
|
||||
"process".to_string()
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
fn log_dir(root_dir: &str) -> PathBuf {
|
||||
Path::new(root_dir).join(LOG_DIR_NAME)
|
||||
}
|
||||
|
||||
fn is_log_file(path: &Path) -> bool {
|
||||
path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(|name| name.starts_with(LOG_FILE_PREFIX) && name.ends_with(LOG_FILE_SUFFIX))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn sorted_log_files(dir: &Path) -> Vec<PathBuf> {
|
||||
let mut files = fs::read_dir(dir)
|
||||
.ok()
|
||||
.into_iter()
|
||||
.flat_map(|entries| entries.filter_map(|entry| entry.ok()))
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| is_log_file(path))
|
||||
.collect::<Vec<_>>();
|
||||
files.sort_by(|left, right| {
|
||||
left.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or_default()
|
||||
.cmp(
|
||||
right
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
});
|
||||
files
|
||||
}
|
||||
|
||||
fn current_log_state() -> Option<(PathBuf, Option<PathBuf>)> {
|
||||
LOG_MANAGER.lock().ok().and_then(|guard| {
|
||||
guard
|
||||
.log_dir
|
||||
.clone()
|
||||
.map(|dir| (dir, guard.active_file.clone()))
|
||||
})
|
||||
}
|
||||
|
||||
fn file_name(path: &Path) -> Option<String> {
|
||||
path.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.map(|value| value.to_string())
|
||||
}
|
||||
|
||||
fn latest_process_log_file(dir: &Path, process_name: &str) -> Option<PathBuf> {
|
||||
let suffix = format!("-{}{}", sanitize_name(process_name), LOG_FILE_SUFFIX);
|
||||
sorted_log_files(dir).into_iter().rev().find(|path| {
|
||||
path.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.map(|value| value.ends_with(&suffix))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
fn modified_millis(metadata: &Metadata) -> i64 {
|
||||
metadata
|
||||
.modified()
|
||||
.ok()
|
||||
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
|
||||
.map(|duration| duration.as_millis().min(i64::MAX as u128) as i64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn resolve_log_file(dir: &Path, requested_name: &str) -> Option<PathBuf> {
|
||||
if requested_name.contains('/')
|
||||
|| requested_name.contains('\\')
|
||||
|| requested_name.contains("..")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
sorted_log_files(dir).into_iter().find(|path| {
|
||||
path.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.map(|value| value == requested_name)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
fn cleanup_old_logs(dir: &Path) {
|
||||
let files = sorted_log_files(dir);
|
||||
let overflow = files.len().saturating_sub(MAX_LOG_FILES);
|
||||
for path in files.into_iter().take(overflow) {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
|
||||
fn push_memory_line(state: &mut LogManagerState, line: String) {
|
||||
state.lines.push_back(line);
|
||||
while state.lines.len() > MAX_MEMORY_LINES {
|
||||
state.lines.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
fn append_log_file(path: &Path, line: &str) {
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
|
||||
let _ = writeln!(file, "{}", line);
|
||||
}
|
||||
}
|
||||
|
||||
fn should_record_debug(level: i32) -> bool {
|
||||
level <= 3
|
||||
}
|
||||
|
||||
fn format_line(level: i32, target: &str, message: &str) -> String {
|
||||
format!("{}[{}] {}", level, target, message.replace('\n', "\\n"))
|
||||
}
|
||||
|
||||
pub(crate) fn configure(core_log: bool, debug_log: bool) {
|
||||
CORE_LOG_ENABLED.store(core_log, Ordering::Relaxed);
|
||||
DEBUG_LOG_ENABLED.store(debug_log, Ordering::Relaxed);
|
||||
if let Ok(mut guard) = LOG_MANAGER.lock() {
|
||||
guard.options.core_log = core_log;
|
||||
guard.options.debug_log = debug_log;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn app_log_enabled(level: i32) -> bool {
|
||||
!should_record_debug(level) || DEBUG_LOG_ENABLED.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub(crate) fn core_log_enabled(level: i32) -> bool {
|
||||
CORE_LOG_ENABLED.load(Ordering::Relaxed) && app_log_enabled(level)
|
||||
}
|
||||
|
||||
pub(crate) fn record_app_log(level: i32, target: &str, message: &str) {
|
||||
if !app_log_enabled(level) {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut guard) = LOG_MANAGER.lock() {
|
||||
let line = format_line(level, target, message);
|
||||
if let Some(path) = guard.active_file.as_ref() {
|
||||
append_log_file(path, &line);
|
||||
}
|
||||
push_memory_line(&mut guard, line);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn record_core_log(level: i32, target: &str, message: &str) {
|
||||
if !core_log_enabled(level) {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut guard) = LOG_MANAGER.lock() {
|
||||
let line = format_line(level, target, message);
|
||||
if let Some(path) = guard.active_file.as_ref() {
|
||||
append_log_file(path, &line);
|
||||
}
|
||||
push_memory_line(&mut guard, line);
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn init_log_manager(root_dir: String, process_name: String) -> bool {
|
||||
let dir = log_dir(&root_dir);
|
||||
if fs::create_dir_all(&dir).is_err() {
|
||||
return false;
|
||||
}
|
||||
if LOG_MANAGER
|
||||
.lock()
|
||||
.map(|guard| guard.active_file.is_some())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
cleanup_old_logs(&dir);
|
||||
return true;
|
||||
}
|
||||
|
||||
let sanitized_process_name = sanitize_name(&process_name);
|
||||
let active_file = if sanitized_process_name == "ui" {
|
||||
dir.join(format!(
|
||||
"{}{}-{}-{}{}",
|
||||
LOG_FILE_PREFIX,
|
||||
now_millis(),
|
||||
std::process::id(),
|
||||
sanitized_process_name,
|
||||
LOG_FILE_SUFFIX
|
||||
))
|
||||
} else if let Some(path) = latest_process_log_file(&dir, "ui") {
|
||||
path
|
||||
} else {
|
||||
dir.join(format!(
|
||||
"{}{}-{}-{}{}",
|
||||
LOG_FILE_PREFIX,
|
||||
now_millis(),
|
||||
std::process::id(),
|
||||
sanitized_process_name,
|
||||
LOG_FILE_SUFFIX
|
||||
))
|
||||
};
|
||||
if OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&active_file)
|
||||
.is_err()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Ok(mut guard) = LOG_MANAGER.lock() {
|
||||
guard.log_dir = Some(dir.clone());
|
||||
guard.active_file = Some(active_file);
|
||||
guard.lines.clear();
|
||||
}
|
||||
cleanup_old_logs(&dir);
|
||||
true
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn configure_log_manager(core_log: bool, debug_log: bool) {
|
||||
configure(core_log, debug_log);
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn write_app_log(level: i32, target: String, message: String) {
|
||||
record_app_log(level, &target, &message);
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn drain_log_lines() -> Vec<String> {
|
||||
LOG_MANAGER
|
||||
.lock()
|
||||
.map(|mut guard| guard.lines.drain(..).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn list_log_files() -> Vec<LogFileInfo> {
|
||||
let Some((log_dir, active_file)) = current_log_state() else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let active_name = active_file.as_ref().and_then(|path| file_name(path));
|
||||
let mut files = sorted_log_files(&log_dir);
|
||||
files.reverse();
|
||||
files
|
||||
.into_iter()
|
||||
.filter_map(|path| {
|
||||
let file_name = file_name(&path)?;
|
||||
let active = active_name
|
||||
.as_ref()
|
||||
.map(|name| name == &file_name)
|
||||
.unwrap_or(false);
|
||||
let metadata = fs::metadata(&path).ok();
|
||||
Some(LogFileInfo {
|
||||
file_name,
|
||||
display_name: if active {
|
||||
"当前启动日志".to_string()
|
||||
} else {
|
||||
"历史日志".to_string()
|
||||
},
|
||||
size_bytes: metadata
|
||||
.as_ref()
|
||||
.map(|value| value.len().min(i64::MAX as u64) as i64)
|
||||
.unwrap_or(0),
|
||||
modified_ms: metadata.as_ref().map(modified_millis).unwrap_or_default(),
|
||||
active,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn read_log_file(file_name: String) -> Option<String> {
|
||||
let (log_dir, _) = current_log_state()?;
|
||||
let path = resolve_log_file(&log_dir, &file_name)?;
|
||||
fs::read_to_string(path).ok()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn export_log_file(file_name: String, target_path: String) -> bool {
|
||||
let Some((log_dir, _)) = current_log_state() else {
|
||||
return false;
|
||||
};
|
||||
let Some(path) = resolve_log_file(&log_dir, &file_name) else {
|
||||
return false;
|
||||
};
|
||||
fs::copy(path, target_path).is_ok()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn export_log_archive(target_path: String) -> bool {
|
||||
let log_dir = LOG_MANAGER
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.log_dir.clone());
|
||||
let Some(log_dir) = log_dir else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let files = sorted_log_files(&log_dir);
|
||||
let mut output = match OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&target_path)
|
||||
{
|
||||
Ok(file) => file,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
for path in files {
|
||||
let name = path
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("unknown.log");
|
||||
let _ = writeln!(output, "===== {} =====", name);
|
||||
if let Ok(content) = fs::read_to_string(&path) {
|
||||
let _ = writeln!(output, "{}", content);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
pub(crate) mod log_manager;
|
||||
pub(crate) mod native_log;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::log_manager;
|
||||
use napi_derive_ohos::napi;
|
||||
use ohos_hilog_binding::{
|
||||
LogOptions, hilog_debug, hilog_error, hilog_info, hilog_warn, set_global_options,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::panic;
|
||||
use tracing::{Event, Subscriber};
|
||||
@@ -8,9 +10,8 @@ use tracing_subscriber::layer::{Context, Layer};
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
static INITIALIZED: std::sync::Once = std::sync::Once::new();
|
||||
static TRACING_INITIALIZED: std::sync::Once = std::sync::Once::new();
|
||||
fn panic_hook(info: &panic::PanicHookInfo) {
|
||||
log_manager::record_core_log(5, "RustPanic", &format!("{}", info));
|
||||
hilog_error!("RUST PANIC: {}", info);
|
||||
}
|
||||
|
||||
#[napi]
|
||||
@@ -22,40 +23,45 @@ pub fn init_panic_hook() {
|
||||
|
||||
#[napi]
|
||||
pub fn hilog_global_options(domain: u32, tag: String) {
|
||||
let _ = domain;
|
||||
let _ = tag;
|
||||
ohos_hilog_binding::forward_stdio_to_hilog();
|
||||
set_global_options(LogOptions {
|
||||
domain,
|
||||
tag: Box::leak(tag.clone().into_boxed_str()),
|
||||
})
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn init_tracing_subscriber() {
|
||||
TRACING_INITIALIZED.call_once(|| {
|
||||
let _ = tracing_subscriber::registry()
|
||||
.with(CallbackLayer {
|
||||
callback: Box::new(tracing_callback),
|
||||
})
|
||||
.try_init();
|
||||
});
|
||||
tracing_subscriber::registry()
|
||||
.with(CallbackLayer {
|
||||
callback: Box::new(tracing_callback),
|
||||
})
|
||||
.init();
|
||||
}
|
||||
|
||||
fn tracing_callback(event: &Event, fields: HashMap<String, String>) {
|
||||
let metadata = event.metadata();
|
||||
let loc = metadata
|
||||
.target()
|
||||
.split("::")
|
||||
.last()
|
||||
.unwrap_or(metadata.target());
|
||||
let level = match *metadata.level() {
|
||||
Level::TRACE => 2,
|
||||
Level::DEBUG => 3,
|
||||
Level::INFO => 4,
|
||||
Level::WARN => 6,
|
||||
Level::ERROR => 5,
|
||||
};
|
||||
if !log_manager::core_log_enabled(level) {
|
||||
return;
|
||||
#[cfg(target_env = "ohos")]
|
||||
{
|
||||
let loc = metadata.target().split("::").last().unwrap();
|
||||
match *metadata.level() {
|
||||
Level::TRACE => {
|
||||
hilog_debug!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>());
|
||||
}
|
||||
Level::DEBUG => {
|
||||
hilog_debug!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>());
|
||||
}
|
||||
Level::INFO => {
|
||||
hilog_info!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>());
|
||||
}
|
||||
Level::WARN => {
|
||||
hilog_warn!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>());
|
||||
}
|
||||
Level::ERROR => {
|
||||
hilog_error!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
}
|
||||
let values = fields.values().cloned().collect::<Vec<_>>().join(" ");
|
||||
log_manager::record_core_log(level, &format!("Rust:{}", loc), &values);
|
||||
}
|
||||
|
||||
struct CallbackLayer {
|
||||
@@ -64,16 +70,6 @@ struct CallbackLayer {
|
||||
|
||||
impl<S: Subscriber> Layer<S> for CallbackLayer {
|
||||
fn on_event(&self, event: &Event, _ctx: Context<S>) {
|
||||
let level = match *event.metadata().level() {
|
||||
Level::TRACE => 2,
|
||||
Level::DEBUG => 3,
|
||||
Level::INFO => 4,
|
||||
Level::WARN => 6,
|
||||
Level::ERROR => 5,
|
||||
};
|
||||
if !log_manager::core_log_enabled(level) {
|
||||
return;
|
||||
}
|
||||
// 使用 fmt::format::FmtSpan 提取字段值
|
||||
let mut fields = HashMap::new();
|
||||
let mut visitor = FieldCollector(&mut fields);
|
||||
|
||||
@@ -3,7 +3,6 @@ use napi_derive_ohos::napi;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Mutex;
|
||||
use url::Url;
|
||||
|
||||
static ATTACHED_TUN_INSTANCE_IDS: once_cell::sync::Lazy<Mutex<HashSet<String>>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(HashSet::new()));
|
||||
@@ -159,136 +158,6 @@ fn stringify_uuid(value: Option<common::Uuid>) -> Option<String> {
|
||||
value.map(|v| v.to_string())
|
||||
}
|
||||
|
||||
fn non_empty_string(value: Option<String>) -> Option<String> {
|
||||
value.and_then(|raw| {
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn config_virtual_ipv4_cidr(config: &api::manage::NetworkConfig) -> Option<String> {
|
||||
non_empty_string(config.virtual_ipv4.clone())
|
||||
.map(|ipv4| format!("{}/{}", ipv4, config.network_length.unwrap_or(24)))
|
||||
}
|
||||
|
||||
fn config_endpoint_urls(config: &api::manage::NetworkConfig) -> Vec<String> {
|
||||
let mut urls = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
if let Some(url) = non_empty_string(config.public_server_url.clone())
|
||||
&& seen.insert(url.clone())
|
||||
{
|
||||
urls.push(url);
|
||||
}
|
||||
for raw in &config.peer_urls {
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let value = trimmed.to_string();
|
||||
if seen.insert(value.clone()) {
|
||||
urls.push(value);
|
||||
}
|
||||
}
|
||||
urls
|
||||
}
|
||||
|
||||
fn endpoint_url(url: &str) -> Option<Url> {
|
||||
Url::parse(url).ok()
|
||||
}
|
||||
|
||||
fn endpoint_scheme(url: &str) -> Option<String> {
|
||||
endpoint_url(url)
|
||||
.map(|parsed| parsed.scheme().to_string())
|
||||
.or_else(|| {
|
||||
let scheme = url.split("://").next().unwrap_or("").trim();
|
||||
(!scheme.is_empty()).then_some(scheme.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn endpoint_label(url: &str) -> String {
|
||||
if let Some(parsed) = endpoint_url(url)
|
||||
&& let Some(host) = parsed.host_str()
|
||||
{
|
||||
return format!("[Config] {}", host);
|
||||
}
|
||||
format!("[Config] {}", url)
|
||||
}
|
||||
|
||||
fn endpoint_remote_display(url: &str) -> String {
|
||||
if let Some(parsed) = endpoint_url(url)
|
||||
&& let Some(host) = parsed.host_str()
|
||||
{
|
||||
return parsed
|
||||
.port()
|
||||
.map(|port| format!("{}:{}", host, port))
|
||||
.unwrap_or_else(|| host.to_string());
|
||||
}
|
||||
url.to_string()
|
||||
}
|
||||
|
||||
fn configured_peer_id(index: usize) -> i64 {
|
||||
9_000_000 + index as i64
|
||||
}
|
||||
|
||||
fn configured_route_views(endpoints: &[String], public_server_url: Option<&str>) -> Vec<RouteView> {
|
||||
endpoints
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, endpoint)| RouteView {
|
||||
peer_id: configured_peer_id(index),
|
||||
hostname: Some(endpoint_label(endpoint)),
|
||||
ipv4: Some(endpoint_remote_display(endpoint)),
|
||||
ipv4_cidr: None,
|
||||
ipv6_cidr: None,
|
||||
proxy_cidrs: Vec::new(),
|
||||
next_hop_peer_id: None,
|
||||
cost: Some(0),
|
||||
path_latency: None,
|
||||
udp_nat_type: None,
|
||||
tcp_nat_type: None,
|
||||
inst_id: None,
|
||||
version: None,
|
||||
is_public_server: public_server_url.map(|url| url == endpoint),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn configured_peer_views(endpoints: &[String]) -> Vec<PeerInfo> {
|
||||
endpoints
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, endpoint)| {
|
||||
let conn_id = format!("configured-peer-{}", index);
|
||||
PeerInfo {
|
||||
peer_id: configured_peer_id(index),
|
||||
default_conn_id: Some(conn_id.clone()),
|
||||
directly_connected_conns: vec![conn_id.clone()],
|
||||
conns: vec![PeerConnInfo {
|
||||
conn_id,
|
||||
my_peer_id: 0,
|
||||
peer_id: configured_peer_id(index),
|
||||
features: Vec::new(),
|
||||
tunnel_type: endpoint_scheme(endpoint),
|
||||
local_addr: None,
|
||||
remote_addr: Some(endpoint.clone()),
|
||||
resolved_remote_addr: Some(endpoint_remote_display(endpoint)),
|
||||
stats: None,
|
||||
loss_rate: None,
|
||||
is_client: true,
|
||||
network_name: None,
|
||||
is_closed: false,
|
||||
secure_auth_level: None,
|
||||
peer_identity_type: None,
|
||||
}],
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn optional_u32_to_i64(value: Option<u32>) -> Option<i64> {
|
||||
value.map(|v| v as i64)
|
||||
}
|
||||
@@ -324,7 +193,7 @@ fn route_to_view(route: api::instance::Route) -> RouteView {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn peer_conn_to_view(conn: api::instance::PeerConnInfo) -> PeerConnInfo {
|
||||
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,
|
||||
@@ -422,43 +291,3 @@ pub fn runtime_instance_from_running_info(
|
||||
peers: info.peers.into_iter().map(peer_to_view).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn runtime_instance_from_config_snapshot(
|
||||
config_id: String,
|
||||
display_name: String,
|
||||
config: api::manage::NetworkConfig,
|
||||
running: bool,
|
||||
) -> RuntimeInstanceState {
|
||||
let tun_attached = running && is_tun_attached(&config_id);
|
||||
let tun_required =
|
||||
running && (config.dev_name.as_deref().unwrap_or("") != "no_tun" || tun_attached);
|
||||
let endpoint_urls = config_endpoint_urls(&config);
|
||||
let public_server_url = non_empty_string(config.public_server_url.clone());
|
||||
let my_node_info = MyNodeInfo {
|
||||
virtual_ipv4: non_empty_string(config.virtual_ipv4.clone()),
|
||||
virtual_ipv4_cidr: config_virtual_ipv4_cidr(&config),
|
||||
hostname: non_empty_string(config.hostname.clone()),
|
||||
version: None,
|
||||
peer_id: None,
|
||||
listeners: config.listener_urls.clone(),
|
||||
vpn_portal_cfg: None,
|
||||
udp_nat_type: None,
|
||||
tcp_nat_type: None,
|
||||
};
|
||||
|
||||
RuntimeInstanceState {
|
||||
config_id: config_id.clone(),
|
||||
instance_id: config_id,
|
||||
display_name,
|
||||
running,
|
||||
tun_required,
|
||||
tun_attached,
|
||||
magic_dns_enabled: config.enable_magic_dns.unwrap_or(false),
|
||||
need_exit_node: !config.exit_nodes.is_empty(),
|
||||
error_message: None,
|
||||
my_node_info: Some(my_node_info),
|
||||
events: Vec::new(),
|
||||
routes: configured_route_views(&endpoint_urls, public_server_url.as_deref()),
|
||||
peers: configured_peer_views(&endpoint_urls),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
||||
"scripts": {
|
||||
"dev": "pnpm --dir ../easytier-web/frontend-lib build && vite",
|
||||
"build": "pnpm --dir ../easytier-web/frontend-lib build && vue-tsc --noEmit && vite build",
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"lint": "eslint . --ignore-pattern src-tauri",
|
||||
|
||||
@@ -654,8 +654,7 @@ mod manager {
|
||||
#[derive(Default)]
|
||||
pub(super) enum PersistedConfigSource {
|
||||
User,
|
||||
#[serde(alias = "webhook")]
|
||||
Web,
|
||||
Webhook,
|
||||
#[serde(other)]
|
||||
#[default]
|
||||
Legacy,
|
||||
@@ -665,15 +664,15 @@ mod manager {
|
||||
pub(super) fn from_runtime_source(source: ConfigSource) -> Self {
|
||||
match source {
|
||||
ConfigSource::User => Self::User,
|
||||
ConfigSource::Web => Self::Web,
|
||||
ConfigSource::Webhook => Self::Webhook,
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_persisted(self, incoming: Self) -> Self {
|
||||
match (self, incoming) {
|
||||
// Older runtimes report missing source as `user`. Keep the stronger persisted
|
||||
// ownership until web sync or an explicit user save repairs it.
|
||||
(Self::Web, Self::User) | (Self::Legacy, Self::User) => self,
|
||||
// ownership until webhook sync or an explicit user save repairs it.
|
||||
(Self::Webhook, Self::User) | (Self::Legacy, Self::User) => self,
|
||||
(_, next) => next,
|
||||
}
|
||||
}
|
||||
@@ -681,13 +680,13 @@ mod manager {
|
||||
fn to_runtime_source(self) -> ConfigSource {
|
||||
match self {
|
||||
Self::User | Self::Legacy => ConfigSource::User,
|
||||
Self::Web => ConfigSource::Web,
|
||||
Self::Webhook => ConfigSource::Webhook,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, target_os = "android"))]
|
||||
fn is_web_like(self) -> bool {
|
||||
matches!(self, Self::Web)
|
||||
fn is_webhook_like(self) -> bool {
|
||||
matches!(self, Self::Webhook)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -919,7 +918,7 @@ mod manager {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn get_enabled_instances_with_web_like_tun_ids(
|
||||
pub fn get_enabled_instances_with_webhook_like_tun_ids(
|
||||
&self,
|
||||
) -> impl Iterator<Item = uuid::Uuid> + '_ {
|
||||
self.storage
|
||||
@@ -927,7 +926,7 @@ mod manager {
|
||||
.iter()
|
||||
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
||||
.filter(|v| !v.config.no_tun())
|
||||
.filter(|v| v.source.is_web_like())
|
||||
.filter(|v| v.source.is_webhook_like())
|
||||
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
||||
}
|
||||
|
||||
@@ -935,11 +934,12 @@ mod manager {
|
||||
pub(super) async fn disable_instances_with_tun(
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
web_only: bool,
|
||||
webhook_only: bool,
|
||||
) -> Result<(), easytier::rpc_service::remote_client::RemoteClientError<anyhow::Error>>
|
||||
{
|
||||
let inst_ids: Vec<uuid::Uuid> = if web_only {
|
||||
self.get_enabled_instances_with_web_like_tun_ids().collect()
|
||||
let inst_ids: Vec<uuid::Uuid> = if webhook_only {
|
||||
self.get_enabled_instances_with_webhook_like_tun_ids()
|
||||
.collect()
|
||||
} else {
|
||||
self.get_enabled_instances_with_tun_ids().collect()
|
||||
};
|
||||
@@ -977,7 +977,7 @@ mod manager {
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
PersistedConfigSource::Web => {
|
||||
PersistedConfigSource::Webhook => {
|
||||
self.disable_instances_with_tun(app, true)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -1187,46 +1187,26 @@ mod manager {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stored_gui_config_deserializes_webhook_source_as_web() {
|
||||
let stored: StoredGuiConfig = serde_json::from_value(serde_json::json!({
|
||||
"config": NetworkConfig::default(),
|
||||
"source": "webhook",
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(stored.source, PersistedConfigSource::Web);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stored_gui_config_defaults_unknown_source_to_legacy() {
|
||||
let stored: StoredGuiConfig = serde_json::from_value(serde_json::json!({
|
||||
"config": NetworkConfig::default(),
|
||||
"source": "unknown",
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(stored.source, PersistedConfigSource::Legacy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persisted_source_merge_keeps_legacy_and_web_over_ambiguous_user() {
|
||||
fn persisted_source_merge_keeps_legacy_and_webhook_over_ambiguous_user() {
|
||||
assert_eq!(
|
||||
PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::User),
|
||||
PersistedConfigSource::Legacy
|
||||
);
|
||||
assert_eq!(
|
||||
PersistedConfigSource::Web.merge_persisted(PersistedConfigSource::User),
|
||||
PersistedConfigSource::Web
|
||||
PersistedConfigSource::Webhook.merge_persisted(PersistedConfigSource::User),
|
||||
PersistedConfigSource::Webhook
|
||||
);
|
||||
assert_eq!(
|
||||
PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::Web),
|
||||
PersistedConfigSource::Web
|
||||
PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::Webhook),
|
||||
PersistedConfigSource::Webhook
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_web_configs_are_web_like() {
|
||||
assert!(!PersistedConfigSource::Legacy.is_web_like());
|
||||
assert!(!PersistedConfigSource::User.is_web_like());
|
||||
assert!(PersistedConfigSource::Web.is_web_like());
|
||||
fn only_webhook_configs_are_webhook_like() {
|
||||
assert!(!PersistedConfigSource::Legacy.is_webhook_like());
|
||||
assert!(!PersistedConfigSource::User.is_webhook_like());
|
||||
assert!(PersistedConfigSource::Webhook.is_webhook_like());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { Api, NetworkTypes } from 'easytier-frontend-lib'
|
||||
import { GetNetworkMetasResponse } from 'node_modules/easytier-frontend-lib/dist/modules/api'
|
||||
import { type ConfigSource, normalizeConfigSource } from './config_source'
|
||||
|
||||
|
||||
type NetworkConfig = NetworkTypes.NetworkConfig
|
||||
type ValidateConfigResponse = Api.ValidateConfigResponse
|
||||
type ListNetworkInstanceIdResponse = Api.ListNetworkInstanceIdResponse
|
||||
type ConfigSource = 'user' | 'webhook' | 'legacy'
|
||||
interface ServiceOptions {
|
||||
config_dir: string
|
||||
rpc_portal: string
|
||||
@@ -31,14 +32,14 @@ function parseStoredConfigs(raw: string | null): StoredGuiConfig[] {
|
||||
if (entry && typeof entry === 'object' && 'config' in entry) {
|
||||
const { config, source } = entry as {
|
||||
config?: NetworkConfig
|
||||
source?: unknown
|
||||
source?: ConfigSource
|
||||
}
|
||||
if (!config) {
|
||||
return []
|
||||
}
|
||||
return [{
|
||||
config: NetworkTypes.normalizeNetworkConfig(config),
|
||||
source: normalizeConfigSource(source),
|
||||
source: source === 'user' || source === 'webhook' ? source : 'legacy',
|
||||
}]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
export type ConfigSource = 'user' | 'web' | 'legacy'
|
||||
|
||||
export function normalizeConfigSource(source: unknown): ConfigSource {
|
||||
if (source === 'user' || source === 'web' || source === 'legacy') {
|
||||
return source
|
||||
}
|
||||
|
||||
if (source === 'webhook') {
|
||||
return 'web'
|
||||
}
|
||||
|
||||
return 'legacy'
|
||||
}
|
||||
@@ -2,11 +2,10 @@ import { Event, listen } from "@tauri-apps/api/event";
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import { NetworkTypes } from "easytier-frontend-lib"
|
||||
import { Utils } from "easytier-frontend-lib";
|
||||
import { normalizeConfigSource } from './config_source'
|
||||
|
||||
interface StoredGuiConfig {
|
||||
config: NetworkTypes.NetworkConfig
|
||||
source?: unknown
|
||||
source?: 'user' | 'webhook' | 'legacy'
|
||||
}
|
||||
|
||||
const EVENTS = Object.freeze({
|
||||
@@ -25,7 +24,7 @@ function onSaveConfigs(event: Event<StoredGuiConfig[]>) {
|
||||
'networkList',
|
||||
JSON.stringify(event.payload.map(({ config, source }) => ({
|
||||
config: NetworkTypes.normalizeNetworkConfig(config),
|
||||
source: normalizeConfigSource(source),
|
||||
source: source ?? 'legacy',
|
||||
}))),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,18 +13,12 @@
|
||||
"./*.css": "./dist/*.css"
|
||||
},
|
||||
"scripts": {
|
||||
"codegen:proto": "node scripts/codegen-proto.mjs",
|
||||
"dev": "pnpm codegen:proto && vite",
|
||||
"build": "pnpm codegen:proto && vue-tsc -b && vite build",
|
||||
"test": "pnpm test:config-ui && pnpm test:network-config",
|
||||
"test:config-ui": "pnpm codegen:proto && vitest run --config vitest.config.ts",
|
||||
"test:network-config": "pnpm build && node scripts/test-network-config.mjs",
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primeuix/themes": "^1.2.3",
|
||||
"@protobuf-ts/runtime": "2.11.1",
|
||||
"@protobuf-ts/runtime-rpc": "2.11.1",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"axios": "^1.13.5",
|
||||
"chart.js": "^4.5.0",
|
||||
@@ -39,13 +33,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||
"@protobuf-ts/plugin": "2.11.1",
|
||||
"@protobuf-ts/protoc": "2.11.1",
|
||||
"@types/node": "^22.8.6",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vue/test-utils": "^2.4.11",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"happy-dom": "16.8.1",
|
||||
"postcss": "^8.4.47",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-nested": "^7.0.2",
|
||||
@@ -53,11 +43,10 @@
|
||||
"typescript": "~5.6.3",
|
||||
"vite": "^5.4.21",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vitest": "^2.1.9",
|
||||
"vue-tsc": "^2.1.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"primevue": "^4.3.9",
|
||||
"vue": "^3.5.12"
|
||||
"vue": "^3.5.12",
|
||||
"primevue": "^4.3.9"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+10
-2482
File diff suppressed because it is too large
Load Diff
@@ -1,121 +0,0 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync, statSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { delimiter, dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const protoRoot = resolve(root, '../../easytier/src/proto')
|
||||
const generatedRoot = resolve(root, 'src/generated')
|
||||
const outDir = resolve(generatedRoot, 'proto')
|
||||
const nodeBinDir = resolve(root, 'node_modules/.bin')
|
||||
|
||||
const protocWrapper = require.resolve('@protobuf-ts/protoc/protoc.js')
|
||||
const protobufTsPluginRoot = dirname(require.resolve('@protobuf-ts/plugin/package.json'))
|
||||
|
||||
const protoFiles = [
|
||||
'common.proto',
|
||||
'acl.proto',
|
||||
'api_instance.proto',
|
||||
'api_manage.proto',
|
||||
'peer_rpc.proto',
|
||||
'error.proto',
|
||||
]
|
||||
|
||||
function installGeneratedFiles(fromDir, toDir) {
|
||||
mkdirSync(toDir, { recursive: true })
|
||||
|
||||
for (const entry of readdirSync(fromDir)) {
|
||||
const source = resolve(fromDir, entry)
|
||||
const target = resolve(toDir, entry)
|
||||
|
||||
if (statSync(source).isDirectory()) {
|
||||
installGeneratedFiles(source, target)
|
||||
continue
|
||||
}
|
||||
|
||||
renameSync(source, target)
|
||||
}
|
||||
}
|
||||
|
||||
function findExecutableInPath(command, extensions = ['']) {
|
||||
const envPath = process.env[pathEnvKey()]
|
||||
if (typeof envPath !== 'string') return undefined
|
||||
|
||||
const nodeBinSuffix = ['node_modules/.bin', 'node_modules\\.bin']
|
||||
for (const entry of envPath.split(delimiter)) {
|
||||
if (!entry || nodeBinSuffix.some((suffix) => entry.endsWith(suffix))) continue
|
||||
|
||||
for (const extension of extensions) {
|
||||
const candidate = resolve(entry, `${command}${extension}`)
|
||||
if (existsSync(candidate)) return candidate
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function pathEnvKey() {
|
||||
return Object.keys(process.env).find((key) => key.toLowerCase() === 'path') ?? 'PATH'
|
||||
}
|
||||
|
||||
function withNodeBinPath() {
|
||||
const key = pathEnvKey()
|
||||
const currentPath = process.env[key]
|
||||
|
||||
return {
|
||||
...process.env,
|
||||
[key]: currentPath ? `${nodeBinDir}${delimiter}${currentPath}` : nodeBinDir,
|
||||
}
|
||||
}
|
||||
|
||||
function getProtocCommand() {
|
||||
const extensions = process.platform === 'win32' ? ['.exe'] : ['']
|
||||
const systemProtoc = findExecutableInPath('protoc', extensions)
|
||||
|
||||
if (systemProtoc) {
|
||||
return {
|
||||
command: systemProtoc,
|
||||
argsPrefix: ['--proto_path', protobufTsPluginRoot],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
command: process.execPath,
|
||||
argsPrefix: [protocWrapper],
|
||||
}
|
||||
}
|
||||
|
||||
mkdirSync(generatedRoot, { recursive: true })
|
||||
const tmpDir = mkdtempSync(resolve(generatedRoot, '.proto-'))
|
||||
const protocCommand = getProtocCommand()
|
||||
|
||||
try {
|
||||
const result = spawnSync(protocCommand.command, [
|
||||
...protocCommand.argsPrefix,
|
||||
'-I',
|
||||
protoRoot,
|
||||
`--ts_out=${tmpDir}`,
|
||||
'--ts_opt=use_proto_field_name,server_none,client_none,ts_nocheck',
|
||||
...protoFiles.map((file) => resolve(protoRoot, file)),
|
||||
], {
|
||||
cwd: root,
|
||||
env: withNodeBinPath(),
|
||||
stdio: 'inherit',
|
||||
shell: false,
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
throw result.error
|
||||
}
|
||||
|
||||
const status = result.status ?? 1
|
||||
if (status === 0) {
|
||||
installGeneratedFiles(tmpDir, outDir)
|
||||
}
|
||||
|
||||
process.exit(status)
|
||||
} finally {
|
||||
rmSync(tmpDir, { recursive: true, force: true })
|
||||
}
|
||||
@@ -1,478 +0,0 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
import ts from 'typescript'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const projectRoot = path.resolve(__dirname, '..')
|
||||
const generatedApiManagePath = path.join(projectRoot, 'src/generated/proto/api_manage.ts')
|
||||
const distPath = path.join(projectRoot, 'dist/easytier-frontend-lib.js')
|
||||
|
||||
const { NetworkTypes } = await import(pathToFileURL(distPath))
|
||||
|
||||
const {
|
||||
AclAction,
|
||||
AclChainType,
|
||||
AclProtocol,
|
||||
CompressionAlgoPb,
|
||||
DEFAULT_NETWORK_CONFIG,
|
||||
NetworkingMethod,
|
||||
normalizeNetworkConfig,
|
||||
toBackendNetworkConfig,
|
||||
} = NetworkTypes
|
||||
|
||||
const BOOLEAN_CONFIG_FIELDS = [
|
||||
'dhcp',
|
||||
'enable_vpn_portal',
|
||||
'advanced_settings',
|
||||
'latency_first',
|
||||
'use_smoltcp',
|
||||
'disable_ipv6',
|
||||
'enable_kcp_proxy',
|
||||
'disable_kcp_input',
|
||||
'disable_p2p',
|
||||
'bind_device',
|
||||
'no_tun',
|
||||
'enable_exit_node',
|
||||
'relay_all_peer_rpc',
|
||||
'multi_thread',
|
||||
'enable_relay_network_whitelist',
|
||||
'enable_manual_routes',
|
||||
'proxy_forward_by_system',
|
||||
'disable_encryption',
|
||||
'enable_socks5',
|
||||
'disable_udp_hole_punching',
|
||||
'enable_magic_dns',
|
||||
'enable_private_mode',
|
||||
'enable_quic_proxy',
|
||||
'disable_quic_input',
|
||||
'disable_sym_hole_punching',
|
||||
'p2p_only',
|
||||
'lazy_p2p',
|
||||
'need_p2p',
|
||||
'disable_upnp',
|
||||
'ipv6_public_addr_provider',
|
||||
'ipv6_public_addr_auto',
|
||||
'disable_relay_data',
|
||||
'enable_udp_broadcast_relay',
|
||||
'disable_tcp_hole_punching',
|
||||
]
|
||||
|
||||
function readGeneratedNetworkConfigFields() {
|
||||
const source = ts.createSourceFile(
|
||||
generatedApiManagePath,
|
||||
fs.readFileSync(generatedApiManagePath, 'utf8'),
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
)
|
||||
|
||||
for (const statement of source.statements) {
|
||||
if (!ts.isInterfaceDeclaration(statement) || statement.name.text !== 'NetworkConfig') {
|
||||
continue
|
||||
}
|
||||
|
||||
return statement.members
|
||||
.filter(ts.isPropertySignature)
|
||||
.map((member) => member.name.getText(source).replace(/^['"]|['"]$/g, ''))
|
||||
}
|
||||
|
||||
throw new Error(`NetworkConfig interface not found in ${generatedApiManagePath}`)
|
||||
}
|
||||
|
||||
function expectNoCamelCaseKeys(value, pathSegments = []) {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item, index) => expectNoCamelCaseKeys(item, [...pathSegments, String(index)]))
|
||||
return
|
||||
}
|
||||
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
assert.equal(
|
||||
/[A-Z]/.test(key),
|
||||
false,
|
||||
`JSON key should use proto field name: ${[...pathSegments, key].join('.')}`,
|
||||
)
|
||||
expectNoCamelCaseKeys(child, [...pathSegments, key])
|
||||
}
|
||||
}
|
||||
|
||||
function allFieldFixture() {
|
||||
return {
|
||||
...DEFAULT_NETWORK_CONFIG(),
|
||||
instance_id: '11111111-2222-3333-4444-555555555555',
|
||||
dhcp: false,
|
||||
virtual_ipv4: '10.9.8.7',
|
||||
network_length: 25,
|
||||
hostname: 'frontend-e2e',
|
||||
network_name: 'full-field-network',
|
||||
network_secret: 'full-field-secret',
|
||||
networking_method: NetworkingMethod.Manual,
|
||||
public_server_url: 'tcp://public.example:11010',
|
||||
peer_urls: [' tcp://peer-a:11010 ', '', 'udp://peer-b:11010'],
|
||||
proxy_cidrs: ['10.10.0.0/16', '192.168.2.0/24->10.99.0.0/24'],
|
||||
enable_vpn_portal: true,
|
||||
vpn_portal_listen_port: 23000,
|
||||
vpn_portal_client_network_addr: '10.88.0.0',
|
||||
vpn_portal_client_network_len: 24,
|
||||
advanced_settings: true,
|
||||
listener_urls: ['tcp://0.0.0.0:12010', 'udp://0.0.0.0:12010'],
|
||||
latency_first: true,
|
||||
dev_name: 'et-full',
|
||||
use_smoltcp: true,
|
||||
disable_ipv6: true,
|
||||
enable_kcp_proxy: true,
|
||||
disable_kcp_input: true,
|
||||
disable_p2p: true,
|
||||
bind_device: false,
|
||||
no_tun: true,
|
||||
enable_exit_node: true,
|
||||
relay_all_peer_rpc: true,
|
||||
multi_thread: false,
|
||||
enable_relay_network_whitelist: true,
|
||||
relay_network_whitelist: ['10.0.0.0/8', 'fd00::/8'],
|
||||
enable_manual_routes: true,
|
||||
routes: ['10.20.0.0/16', 'fd00:20::/64'],
|
||||
exit_nodes: ['10.9.8.1', 'fd00::1'],
|
||||
proxy_forward_by_system: true,
|
||||
disable_encryption: true,
|
||||
enable_socks5: true,
|
||||
socks5_port: 1081,
|
||||
disable_udp_hole_punching: true,
|
||||
mtu: 1280,
|
||||
mapped_listeners: ['tcp://127.0.0.1:13010'],
|
||||
enable_magic_dns: true,
|
||||
enable_private_mode: true,
|
||||
enable_quic_proxy: true,
|
||||
disable_quic_input: true,
|
||||
quic_listen_port: 14010,
|
||||
port_forwards: [
|
||||
{
|
||||
proto: 'tcp',
|
||||
bind_ip: '127.0.0.1',
|
||||
bind_port: 8080,
|
||||
dst_ip: '10.9.8.7',
|
||||
dst_port: 80,
|
||||
},
|
||||
{
|
||||
proto: 'udp',
|
||||
bind_ip: '0.0.0.0',
|
||||
bind_port: 5353,
|
||||
dst_ip: '10.9.8.8',
|
||||
dst_port: 53,
|
||||
},
|
||||
],
|
||||
disable_sym_hole_punching: true,
|
||||
p2p_only: true,
|
||||
data_compress_algo: CompressionAlgoPb.Zstd,
|
||||
encryption_algorithm: 'aes-gcm',
|
||||
disable_tcp_hole_punching: true,
|
||||
secure_mode: {
|
||||
enabled: true,
|
||||
local_private_key: 'private-key',
|
||||
local_public_key: 'public-key',
|
||||
},
|
||||
acl: {
|
||||
acl_v1: {
|
||||
group: {
|
||||
declares: [
|
||||
{
|
||||
group_name: 'ops',
|
||||
group_secret: 'ops-secret',
|
||||
},
|
||||
],
|
||||
members: ['node-a', 'node-b'],
|
||||
},
|
||||
chains: [
|
||||
{
|
||||
name: 'forward-chain',
|
||||
chain_type: AclChainType.Forward,
|
||||
description: 'forward traffic',
|
||||
enabled: true,
|
||||
default_action: AclAction.Drop,
|
||||
rules: [
|
||||
{
|
||||
name: 'allow-web',
|
||||
description: 'allow web traffic',
|
||||
priority: 100,
|
||||
enabled: true,
|
||||
protocol: AclProtocol.TCP,
|
||||
ports: ['80', '443'],
|
||||
source_ips: ['10.0.0.0/8'],
|
||||
destination_ips: ['10.9.8.7/32'],
|
||||
source_ports: ['1024-65535'],
|
||||
action: AclAction.Allow,
|
||||
rate_limit: 1000,
|
||||
burst_limit: 2000,
|
||||
stateful: true,
|
||||
source_groups: ['ops'],
|
||||
destination_groups: ['web'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
credential_file: '/tmp/easytier-credential.toml',
|
||||
lazy_p2p: true,
|
||||
need_p2p: true,
|
||||
instance_recv_bps_limit: '9007199254740993',
|
||||
disable_upnp: true,
|
||||
ipv6_public_addr_provider: true,
|
||||
ipv6_public_addr_auto: true,
|
||||
ipv6_public_addr_prefix: '2001:db8:1::/64',
|
||||
disable_relay_data: true,
|
||||
enable_udp_broadcast_relay: true,
|
||||
socket_mark: 1234,
|
||||
}
|
||||
}
|
||||
|
||||
function assertFixtureCoversGeneratedFields() {
|
||||
const generatedFields = readGeneratedNetworkConfigFields()
|
||||
const fixtureFields = new Set(Object.keys(allFieldFixture()))
|
||||
const missing = generatedFields.filter((field) => !fixtureFields.has(field))
|
||||
|
||||
assert.deepEqual(missing, [], 'all generated NetworkConfig fields should be represented in the fixture')
|
||||
}
|
||||
|
||||
function assertFullFieldRoundTrip() {
|
||||
const input = allFieldFixture()
|
||||
const normalized = normalizeNetworkConfig(input)
|
||||
|
||||
assert.equal(normalized.peer_urls.join(','), 'tcp://peer-a:11010,udp://peer-b:11010')
|
||||
assert.equal(normalized.instance_recv_bps_limit, '9007199254740993')
|
||||
assert.equal(normalized.data_compress_algo, CompressionAlgoPb.Zstd)
|
||||
assert.equal(normalized.acl.acl_v1.chains[0].chain_type, AclChainType.Forward)
|
||||
assert.equal(normalized.acl.acl_v1.chains[0].rules[0].protocol, AclProtocol.TCP)
|
||||
|
||||
const backend = toBackendNetworkConfig(normalized)
|
||||
expectNoCamelCaseKeys(backend)
|
||||
|
||||
for (const field of readGeneratedNetworkConfigFields()) {
|
||||
assert.ok(field in backend, `backend JSON should include fixture field ${field}`)
|
||||
}
|
||||
|
||||
assert.equal(backend.networking_method, 'Manual')
|
||||
assert.equal(backend.public_server_url, '')
|
||||
assert.deepEqual(backend.peer_urls, ['tcp://peer-a:11010', 'udp://peer-b:11010'])
|
||||
assert.equal(backend.data_compress_algo, 'Zstd')
|
||||
assert.equal(backend.instance_recv_bps_limit, '9007199254740993')
|
||||
assert.equal(backend.secure_mode.enabled, true)
|
||||
assert.equal(backend.secure_mode.local_private_key, 'private-key')
|
||||
assert.equal(backend.acl.acl_v1.chains[0].chain_type, 'Forward')
|
||||
assert.equal(backend.acl.acl_v1.chains[0].default_action, 'Drop')
|
||||
assert.equal(backend.acl.acl_v1.chains[0].rules[0].protocol, 'TCP')
|
||||
assert.equal(backend.acl.acl_v1.chains[0].rules[0].action, 'Allow')
|
||||
assert.equal(backend.port_forwards[1].proto, 'udp')
|
||||
assert.equal(backend.socket_mark, 1234)
|
||||
}
|
||||
|
||||
function assertBooleanFieldValuesPreserved() {
|
||||
const input = allFieldFixture()
|
||||
const normalized = normalizeNetworkConfig(input)
|
||||
const backend = toBackendNetworkConfig(normalized)
|
||||
|
||||
for (const field of BOOLEAN_CONFIG_FIELDS) {
|
||||
assert.equal(
|
||||
normalized[field],
|
||||
input[field],
|
||||
`normalized config should preserve boolean field ${field}`,
|
||||
)
|
||||
assert.equal(
|
||||
backend[field],
|
||||
input[field],
|
||||
`backend JSON should preserve boolean field ${field}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function assertEnumCompatibility() {
|
||||
const normalized = normalizeNetworkConfig({
|
||||
...DEFAULT_NETWORK_CONFIG(),
|
||||
networking_method: 'Manual',
|
||||
data_compress_algo: 'Zstd',
|
||||
acl: {
|
||||
acl_v1: {
|
||||
group: { declares: [], members: [] },
|
||||
chains: [
|
||||
{
|
||||
chain_type: 'Forward',
|
||||
default_action: 'Drop',
|
||||
rules: [
|
||||
{
|
||||
protocol: 'TCP',
|
||||
action: 'Allow',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(normalized.data_compress_algo, CompressionAlgoPb.Zstd)
|
||||
assert.equal(normalized.acl.acl_v1.chains[0].chain_type, AclChainType.Forward)
|
||||
assert.equal(normalized.acl.acl_v1.chains[0].default_action, AclAction.Drop)
|
||||
assert.equal(normalized.acl.acl_v1.chains[0].rules[0].protocol, AclProtocol.TCP)
|
||||
assert.equal(normalized.acl.acl_v1.chains[0].rules[0].action, AclAction.Allow)
|
||||
|
||||
const backend = toBackendNetworkConfig({
|
||||
...DEFAULT_NETWORK_CONFIG(),
|
||||
data_compress_algo: 'Zstd',
|
||||
acl: {
|
||||
acl_v1: {
|
||||
group: { declares: [], members: [] },
|
||||
chains: [
|
||||
{
|
||||
chain_type: 'Forward',
|
||||
default_action: 'Drop',
|
||||
rules: [
|
||||
{
|
||||
protocol: 'TCP',
|
||||
action: 'Allow',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(backend.data_compress_algo, 'Zstd')
|
||||
assert.equal(backend.acl.acl_v1.chains[0].chain_type, 'Forward')
|
||||
assert.equal(backend.acl.acl_v1.chains[0].rules[0].protocol, 'TCP')
|
||||
}
|
||||
|
||||
function assertAclDefaultsAndExplicitZero() {
|
||||
const partialAcl = normalizeNetworkConfig({
|
||||
...DEFAULT_NETWORK_CONFIG(),
|
||||
acl: {
|
||||
acl_v1: {
|
||||
group: { declares: [], members: [] },
|
||||
chains: [{ rules: [{}] }],
|
||||
},
|
||||
},
|
||||
})
|
||||
const defaultedChain = partialAcl.acl.acl_v1.chains[0]
|
||||
|
||||
assert.equal(defaultedChain.chain_type, AclChainType.UnspecifiedChain)
|
||||
assert.equal(defaultedChain.default_action, AclAction.Allow)
|
||||
assert.equal(defaultedChain.rules[0].protocol, AclProtocol.Any)
|
||||
assert.equal(defaultedChain.rules[0].action, AclAction.Allow)
|
||||
|
||||
const explicitZero = normalizeNetworkConfig({
|
||||
...DEFAULT_NETWORK_CONFIG(),
|
||||
acl: {
|
||||
acl_v1: {
|
||||
group: { declares: [], members: [] },
|
||||
chains: [
|
||||
{
|
||||
chain_type: 0,
|
||||
default_action: 0,
|
||||
rules: [{ protocol: 0, action: 0 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
const zeroChain = explicitZero.acl.acl_v1.chains[0]
|
||||
|
||||
assert.equal(zeroChain.chain_type, AclChainType.UnspecifiedChain)
|
||||
assert.equal(zeroChain.default_action, AclAction.Noop)
|
||||
assert.equal(zeroChain.rules[0].protocol, AclProtocol.Unspecified)
|
||||
assert.equal(zeroChain.rules[0].action, AclAction.Noop)
|
||||
}
|
||||
|
||||
function assertNetworkingMethodNormalization() {
|
||||
const publicServer = normalizeNetworkConfig({
|
||||
...DEFAULT_NETWORK_CONFIG(),
|
||||
networking_method: 'PublicServer',
|
||||
public_server_url: ' tcp://public.example:11010 ',
|
||||
peer_urls: ['tcp://manual.example:11010'],
|
||||
})
|
||||
|
||||
assert.equal(publicServer.networking_method, NetworkingMethod.Manual)
|
||||
assert.equal(publicServer.public_server_url, '')
|
||||
assert.deepEqual(publicServer.peer_urls, ['tcp://public.example:11010'])
|
||||
|
||||
const standalone = normalizeNetworkConfig({
|
||||
...DEFAULT_NETWORK_CONFIG(),
|
||||
networking_method: 'Standalone',
|
||||
peer_urls: ['tcp://manual.example:11010'],
|
||||
})
|
||||
|
||||
assert.equal(standalone.networking_method, NetworkingMethod.Manual)
|
||||
assert.deepEqual(standalone.peer_urls, [])
|
||||
|
||||
const missing = normalizeNetworkConfig({
|
||||
...DEFAULT_NETWORK_CONFIG(),
|
||||
networking_method: undefined,
|
||||
peer_urls: [' tcp://one ', '', 'udp://two '],
|
||||
})
|
||||
|
||||
assert.deepEqual(missing.peer_urls, ['tcp://one', 'udp://two'])
|
||||
}
|
||||
|
||||
function assertNumberBoundaries() {
|
||||
const safeLimit = normalizeNetworkConfig({
|
||||
...DEFAULT_NETWORK_CONFIG(),
|
||||
instance_recv_bps_limit: '12345',
|
||||
})
|
||||
assert.equal(safeLimit.instance_recv_bps_limit, 12345)
|
||||
|
||||
const largeLimit = normalizeNetworkConfig({
|
||||
...DEFAULT_NETWORK_CONFIG(),
|
||||
instance_recv_bps_limit: '9007199254740993',
|
||||
})
|
||||
assert.equal(largeLimit.instance_recv_bps_limit, '9007199254740993')
|
||||
assert.equal(toBackendNetworkConfig(largeLimit).instance_recv_bps_limit, '9007199254740993')
|
||||
|
||||
const invalidNumbers = normalizeNetworkConfig({
|
||||
...DEFAULT_NETWORK_CONFIG(),
|
||||
mtu: Number.NaN,
|
||||
instance_recv_bps_limit: Number.POSITIVE_INFINITY,
|
||||
})
|
||||
assert.equal(invalidNumbers.mtu, null)
|
||||
assert.equal(invalidNumbers.instance_recv_bps_limit, null)
|
||||
|
||||
const emptyLimit = normalizeNetworkConfig({
|
||||
...DEFAULT_NETWORK_CONFIG(),
|
||||
instance_recv_bps_limit: '',
|
||||
})
|
||||
assert.equal(emptyLimit.instance_recv_bps_limit, null)
|
||||
|
||||
const zeroLimit = normalizeNetworkConfig({
|
||||
...DEFAULT_NETWORK_CONFIG(),
|
||||
instance_recv_bps_limit: '0',
|
||||
})
|
||||
assert.equal(zeroLimit.instance_recv_bps_limit, null)
|
||||
assert.equal(toBackendNetworkConfig({
|
||||
...DEFAULT_NETWORK_CONFIG(),
|
||||
instance_recv_bps_limit: 0,
|
||||
}).instance_recv_bps_limit, undefined)
|
||||
|
||||
const oversizedLimit = normalizeNetworkConfig({
|
||||
...DEFAULT_NETWORK_CONFIG(),
|
||||
instance_recv_bps_limit: '18446744073709551616',
|
||||
})
|
||||
assert.equal(oversizedLimit.instance_recv_bps_limit, null)
|
||||
}
|
||||
|
||||
const tests = [
|
||||
assertFixtureCoversGeneratedFields,
|
||||
assertFullFieldRoundTrip,
|
||||
assertBooleanFieldValuesPreserved,
|
||||
assertEnumCompatibility,
|
||||
assertAclDefaultsAndExplicitZero,
|
||||
assertNetworkingMethodNormalization,
|
||||
assertNumberBoundaries,
|
||||
]
|
||||
|
||||
for (const test of tests) {
|
||||
test()
|
||||
console.log(`ok ${test.name}`)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
normalizeNetworkConfig,
|
||||
removeRow
|
||||
} from '../types/network'
|
||||
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AclManager from './acl/AclManager.vue'
|
||||
import UrlListInput from './UrlListInput.vue'
|
||||
@@ -134,7 +134,6 @@ function savePortForward() {
|
||||
const portForwardContainer = ref<HTMLElement | null>(null);
|
||||
const isCompact = ref(false);
|
||||
|
||||
const UINT64_MAX = (1n << 64n) - 1n
|
||||
|
||||
onMounted(() => {
|
||||
if (portForwardContainer.value) {
|
||||
@@ -162,39 +161,6 @@ function syncNormalizedNetwork(network: NetworkConfig | undefined): void {
|
||||
}
|
||||
|
||||
watch(() => curNetwork.value, syncNormalizedNetwork, { immediate: true, deep: false })
|
||||
|
||||
function parseInstanceRecvBpsLimitInput(value: string): number | string | null | undefined {
|
||||
const trimmed = value.trim()
|
||||
if (trimmed.length === 0) {
|
||||
return null
|
||||
}
|
||||
if (!/^\d+$/.test(trimmed)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const limit = BigInt(trimmed)
|
||||
if (limit === 0n) {
|
||||
return null
|
||||
}
|
||||
if (limit > UINT64_MAX) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return limit <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(limit) : limit.toString()
|
||||
}
|
||||
|
||||
const instanceRecvBpsLimitInput = computed<string>({
|
||||
get: () => {
|
||||
const limit = curNetwork.value.instance_recv_bps_limit
|
||||
return limit == null ? '' : String(limit)
|
||||
},
|
||||
set: (value) => {
|
||||
const limit = parseInstanceRecvBpsLimitInput(value)
|
||||
if (limit !== undefined) {
|
||||
curNetwork.value.instance_recv_bps_limit = limit
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -351,9 +317,9 @@ const instanceRecvBpsLimitInput = computed<string>({
|
||||
<span class="pi pi-question-circle ml-2 self-center"
|
||||
v-tooltip="t('instance_recv_bps_limit_help')"></span>
|
||||
</div>
|
||||
<InputText id="instance_recv_bps_limit" v-model="instanceRecvBpsLimitInput"
|
||||
aria-describedby="instance_recv_bps_limit-help" inputmode="numeric" pattern="[0-9]*"
|
||||
:placeholder="t('instance_recv_bps_limit_placeholder')" fluid />
|
||||
<InputNumber id="instance_recv_bps_limit" v-model="curNetwork.instance_recv_bps_limit"
|
||||
aria-describedby="instance_recv_bps_limit-help" :format="false"
|
||||
:placeholder="t('instance_recv_bps_limit_placeholder')" :min="1" fluid />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,49 +1,166 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import {
|
||||
NetworkConfig as NetworkConfigPb,
|
||||
NetworkingMethod,
|
||||
type NetworkConfig as ProtoNetworkConfig,
|
||||
type PortForwardConfig,
|
||||
} from '../generated/proto/api_manage'
|
||||
import {
|
||||
Action as AclAction,
|
||||
ChainType as AclChainType,
|
||||
Protocol as AclProtocol,
|
||||
type Acl,
|
||||
type AclV1,
|
||||
type Chain as AclChain,
|
||||
type GroupIdentity,
|
||||
type GroupInfo,
|
||||
type Rule as AclRule,
|
||||
} from '../generated/proto/acl'
|
||||
import { CompressionAlgoPb, NatType, type SecureModeConfig } from '../generated/proto/common'
|
||||
import { prepareNetworkConfigForProtoJson } from './networkCompat'
|
||||
|
||||
export { AclAction, AclChainType, AclProtocol, CompressionAlgoPb, NatType, NetworkingMethod }
|
||||
export type { Acl, AclChain, AclRule, AclV1, GroupIdentity, GroupInfo, PortForwardConfig, SecureModeConfig }
|
||||
|
||||
export type NetworkConfig = Omit<
|
||||
ProtoNetworkConfig,
|
||||
'instance_id' | 'instance_recv_bps_limit' | 'mtu' | 'networking_method'
|
||||
> & {
|
||||
instance_id: string
|
||||
mtu: number | null
|
||||
instance_recv_bps_limit: number | string | null
|
||||
networking_method: NetworkingMethod | string
|
||||
export enum NetworkingMethod {
|
||||
PublicServer = 0,
|
||||
Manual = 1,
|
||||
Standalone = 2,
|
||||
}
|
||||
|
||||
const UINT64_MAX = (1n << 64n) - 1n
|
||||
export interface SecureModeConfig {
|
||||
enabled: boolean
|
||||
// Keep protocol compatibility with backend/import-export flows even though the GUI
|
||||
// does not render secure-mode or credential inputs.
|
||||
local_private_key?: string
|
||||
local_public_key?: string
|
||||
}
|
||||
|
||||
interface NetworkingConfigFields {
|
||||
export enum AclProtocol {
|
||||
Unspecified = 0,
|
||||
TCP = 1,
|
||||
UDP = 2,
|
||||
ICMP = 3,
|
||||
ICMPv6 = 4,
|
||||
Any = 5,
|
||||
}
|
||||
|
||||
export enum AclAction {
|
||||
Noop = 0,
|
||||
Allow = 1,
|
||||
Drop = 2,
|
||||
}
|
||||
|
||||
export enum AclChainType {
|
||||
UnspecifiedChain = 0,
|
||||
Inbound = 1,
|
||||
Outbound = 2,
|
||||
Forward = 3,
|
||||
}
|
||||
|
||||
export interface AclRule {
|
||||
name: string
|
||||
description: string
|
||||
priority: number
|
||||
enabled: boolean
|
||||
protocol: AclProtocol
|
||||
ports: string[]
|
||||
source_ips: string[]
|
||||
destination_ips: string[]
|
||||
source_ports: string[]
|
||||
action: AclAction
|
||||
rate_limit: number
|
||||
burst_limit: number
|
||||
stateful: boolean
|
||||
source_groups: string[]
|
||||
destination_groups: string[]
|
||||
}
|
||||
|
||||
export interface AclChain {
|
||||
name: string
|
||||
chain_type: AclChainType
|
||||
description: string
|
||||
enabled: boolean
|
||||
rules: AclRule[]
|
||||
default_action: AclAction
|
||||
}
|
||||
|
||||
export interface GroupIdentity {
|
||||
group_name: string
|
||||
group_secret: string
|
||||
}
|
||||
|
||||
export interface GroupInfo {
|
||||
declares: GroupIdentity[]
|
||||
members: string[]
|
||||
}
|
||||
|
||||
export interface AclV1 {
|
||||
chains: AclChain[]
|
||||
group?: GroupInfo
|
||||
}
|
||||
|
||||
export interface Acl {
|
||||
acl_v1?: AclV1
|
||||
}
|
||||
|
||||
export interface NetworkConfig {
|
||||
instance_id: string
|
||||
|
||||
dhcp: boolean
|
||||
virtual_ipv4: string
|
||||
network_length: number
|
||||
hostname?: string
|
||||
network_name: string
|
||||
network_secret?: string
|
||||
credential_file?: string
|
||||
secure_mode?: SecureModeConfig
|
||||
|
||||
networking_method: NetworkingMethod
|
||||
|
||||
public_server_url: string
|
||||
peer_urls: string[]
|
||||
public_server_url?: string
|
||||
networking_method?: NetworkingMethod | string
|
||||
|
||||
proxy_cidrs: string[]
|
||||
|
||||
enable_vpn_portal: boolean
|
||||
vpn_portal_listen_port: number
|
||||
vpn_portal_client_network_addr: string
|
||||
vpn_portal_client_network_len: number
|
||||
|
||||
advanced_settings: boolean
|
||||
|
||||
listener_urls: string[]
|
||||
latency_first: boolean
|
||||
|
||||
dev_name: string
|
||||
|
||||
use_smoltcp?: boolean
|
||||
disable_ipv6?: boolean
|
||||
ipv6_public_addr_auto?: boolean
|
||||
enable_kcp_proxy?: boolean
|
||||
disable_kcp_input?: boolean
|
||||
enable_quic_proxy?: boolean
|
||||
disable_quic_input?: boolean
|
||||
disable_p2p?: boolean
|
||||
p2p_only?: boolean
|
||||
lazy_p2p?: boolean
|
||||
bind_device?: boolean
|
||||
no_tun?: boolean
|
||||
enable_exit_node?: boolean
|
||||
relay_all_peer_rpc?: boolean
|
||||
need_p2p?: boolean
|
||||
multi_thread?: boolean
|
||||
proxy_forward_by_system?: boolean
|
||||
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
|
||||
relay_network_whitelist: string[]
|
||||
|
||||
enable_manual_routes: boolean
|
||||
routes: string[]
|
||||
|
||||
exit_nodes: string[]
|
||||
|
||||
enable_socks5?: boolean
|
||||
socks5_port: number
|
||||
|
||||
mtu: number | null
|
||||
instance_recv_bps_limit: number | null
|
||||
mapped_listeners: string[]
|
||||
|
||||
enable_magic_dns?: boolean
|
||||
enable_private_mode?: boolean
|
||||
|
||||
port_forwards: PortForwardConfig[]
|
||||
acl?: Acl
|
||||
}
|
||||
|
||||
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
return {
|
||||
...NetworkConfigPb.create(),
|
||||
|
||||
instance_id: uuidv4(),
|
||||
|
||||
dhcp: true,
|
||||
@@ -126,82 +243,33 @@ function cleanPeerUrls(urls: string[] | undefined): string[] {
|
||||
return (urls ?? []).map((url) => url.trim()).filter((url) => url.length > 0)
|
||||
}
|
||||
|
||||
function normalizeUint64ForInput(v: bigint | number | string | null | undefined): number | string | null {
|
||||
if (v == null) return null
|
||||
|
||||
try {
|
||||
const n = typeof v === 'bigint' ? v : BigInt(v)
|
||||
if (n === 0n || n > UINT64_MAX) return null
|
||||
return n <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(n) : n.toString()
|
||||
} catch {
|
||||
return null
|
||||
export function normalizeNetworkConfig(config: NetworkConfig): NetworkConfig {
|
||||
const normalized: NetworkConfig = {
|
||||
...config,
|
||||
peer_urls: cleanPeerUrls(config.peer_urls),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeNumberForInput(v: number | string | null | undefined): number | null {
|
||||
if (v == null) return null
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
const publicServerUrl = normalized.public_server_url?.trim() ?? ''
|
||||
|
||||
function toBackendUint64(v: number | bigint | string | null | undefined): bigint | undefined {
|
||||
if (v == null || v === '') return undefined
|
||||
try {
|
||||
const n = typeof v === 'bigint' ? v : BigInt(v)
|
||||
return n > 0n && n <= UINT64_MAX ? n : undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function applyNetworkingMethod(config: NetworkingConfigFields): void {
|
||||
config.peer_urls = cleanPeerUrls(config.peer_urls)
|
||||
|
||||
const publicServerUrl = config.public_server_url?.trim() ?? ''
|
||||
const networkingMethod = config.networking_method ?? NetworkingMethod.Manual
|
||||
|
||||
switch (networkingMethod) {
|
||||
switch (normalized.networking_method) {
|
||||
case NetworkingMethod.PublicServer:
|
||||
config.peer_urls = publicServerUrl ? [publicServerUrl] : []
|
||||
normalized.peer_urls = publicServerUrl ? [publicServerUrl] : []
|
||||
break
|
||||
case NetworkingMethod.Manual:
|
||||
break
|
||||
case NetworkingMethod.Standalone:
|
||||
default:
|
||||
config.peer_urls = []
|
||||
normalized.peer_urls = []
|
||||
break
|
||||
}
|
||||
|
||||
config.networking_method = NetworkingMethod.Manual
|
||||
config.public_server_url = ''
|
||||
}
|
||||
|
||||
export function normalizeNetworkConfig(config: NetworkConfig): NetworkConfig {
|
||||
const normalized = NetworkConfigPb.fromJson(prepareNetworkConfigForProtoJson(config) as any, {
|
||||
ignoreUnknownFields: true,
|
||||
}) as unknown as NetworkConfig
|
||||
|
||||
applyNetworkingMethod(normalized)
|
||||
normalized.mtu = normalizeNumberForInput(normalized.mtu)
|
||||
normalized.instance_recv_bps_limit = normalizeUint64ForInput(
|
||||
normalized.instance_recv_bps_limit as any,
|
||||
)
|
||||
|
||||
normalized.networking_method = NetworkingMethod.Manual
|
||||
normalized.public_server_url = ''
|
||||
return normalized
|
||||
}
|
||||
|
||||
export function toBackendNetworkConfig(config: NetworkConfig): NetworkConfig {
|
||||
const backend = NetworkConfigPb.fromJson(prepareNetworkConfigForProtoJson(config) as any, {
|
||||
ignoreUnknownFields: true,
|
||||
})
|
||||
|
||||
applyNetworkingMethod(backend)
|
||||
backend.mtu = normalizeNumberForInput(config.mtu) ?? undefined
|
||||
backend.instance_recv_bps_limit = toBackendUint64(config.instance_recv_bps_limit)
|
||||
|
||||
return NetworkConfigPb.toJson(backend, {
|
||||
useProtoFieldName: true,
|
||||
}) as unknown as NetworkConfig
|
||||
return normalizeNetworkConfig(config)
|
||||
}
|
||||
|
||||
export interface NetworkInstance {
|
||||
@@ -329,6 +397,14 @@ export interface PeerConnStats {
|
||||
latency_us: number
|
||||
}
|
||||
|
||||
export interface PortForwardConfig {
|
||||
bind_ip: string,
|
||||
bind_port: number,
|
||||
dst_ip: string,
|
||||
dst_port: number,
|
||||
proto: string
|
||||
}
|
||||
|
||||
// 添加新行
|
||||
export const addRow = (rows: PortForwardConfig[]) => {
|
||||
rows.push({
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import {
|
||||
Action as AclAction,
|
||||
ChainType as AclChainType,
|
||||
Protocol as AclProtocol,
|
||||
} from '../generated/proto/acl'
|
||||
import type { NetworkConfig } from './network'
|
||||
|
||||
const UINT64_MAX = (1n << 64n) - 1n
|
||||
|
||||
type JsonRecord = Record<string, unknown>
|
||||
|
||||
export function prepareNetworkConfigForProtoJson(config: NetworkConfig): NetworkConfig {
|
||||
const prepared = dropUnsupportedJsonValues(applyLegacyAclDefaults(config)) as NetworkConfig
|
||||
normalizeLegacyOptionalUint64(prepared as JsonRecord, 'instance_recv_bps_limit')
|
||||
return prepared
|
||||
}
|
||||
|
||||
function applyLegacyAclDefaults(config: NetworkConfig): NetworkConfig {
|
||||
const acl = config.acl
|
||||
const aclV1 = acl?.acl_v1
|
||||
if (!Array.isArray(aclV1?.chains)) return config
|
||||
|
||||
return {
|
||||
...config,
|
||||
acl: {
|
||||
...acl,
|
||||
acl_v1: {
|
||||
...aclV1,
|
||||
chains: aclV1.chains.map((chain) => ({
|
||||
...chain,
|
||||
chain_type: chain.chain_type ?? AclChainType.UnspecifiedChain,
|
||||
default_action: chain.default_action ?? AclAction.Allow,
|
||||
rules: (chain.rules ?? []).map((rule) => ({
|
||||
...rule,
|
||||
protocol: rule.protocol ?? AclProtocol.Any,
|
||||
action: rule.action ?? AclAction.Allow,
|
||||
})),
|
||||
})),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function dropUnsupportedJsonValues(value: unknown): unknown {
|
||||
if (value === undefined) return undefined
|
||||
if (typeof value === 'number' && !Number.isFinite(value)) return undefined
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(dropUnsupportedJsonValues).filter((v) => v !== undefined)
|
||||
}
|
||||
|
||||
if (isJsonRecord(value)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.map(([k, v]) => [k, dropUnsupportedJsonValues(v)])
|
||||
.filter(([, v]) => v !== undefined),
|
||||
)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
function isJsonRecord(value: unknown): value is JsonRecord {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
function normalizeLegacyOptionalUint64(obj: JsonRecord, key: string): void {
|
||||
const value = obj[key]
|
||||
if (typeof value !== 'string') return
|
||||
|
||||
const trimmed = value.trim()
|
||||
if (!isPositiveUint64String(trimmed)) {
|
||||
delete obj[key]
|
||||
return
|
||||
}
|
||||
|
||||
obj[key] = trimmed
|
||||
}
|
||||
|
||||
function isPositiveUint64String(value: string): boolean {
|
||||
if (!/^\d+$/.test(value)) return false
|
||||
|
||||
const n = BigInt(value)
|
||||
return n > 0n && n <= UINT64_MAX
|
||||
}
|
||||
@@ -1,563 +0,0 @@
|
||||
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent, h, nextTick, reactive } from 'vue'
|
||||
import Config from '../src/components/Config.vue'
|
||||
import {
|
||||
DEFAULT_NETWORK_CONFIG,
|
||||
toBackendNetworkConfig,
|
||||
type NetworkConfig,
|
||||
} from '../src/types/network'
|
||||
|
||||
const CONFIG_FLAG_FIELDS = [
|
||||
'latency_first',
|
||||
'use_smoltcp',
|
||||
'disable_ipv6',
|
||||
'ipv6_public_addr_auto',
|
||||
'enable_kcp_proxy',
|
||||
'disable_kcp_input',
|
||||
'enable_quic_proxy',
|
||||
'disable_quic_input',
|
||||
'disable_p2p',
|
||||
'p2p_only',
|
||||
'lazy_p2p',
|
||||
'bind_device',
|
||||
'no_tun',
|
||||
'enable_exit_node',
|
||||
'relay_all_peer_rpc',
|
||||
'need_p2p',
|
||||
'multi_thread',
|
||||
'proxy_forward_by_system',
|
||||
'disable_encryption',
|
||||
'disable_tcp_hole_punching',
|
||||
'disable_udp_hole_punching',
|
||||
'enable_udp_broadcast_relay',
|
||||
'disable_upnp',
|
||||
'disable_sym_hole_punching',
|
||||
'enable_magic_dns',
|
||||
'enable_private_mode',
|
||||
] as const satisfies readonly (keyof NetworkConfig)[]
|
||||
|
||||
const CONFIG_CHECKBOX_FIELDS = [
|
||||
['dhcp', '#virtual_ip_auto'],
|
||||
...CONFIG_FLAG_FIELDS.map((field) => [field, `#${field}`] as const),
|
||||
] as const satisfies readonly (readonly [keyof NetworkConfig, string])[]
|
||||
|
||||
const CONFIG_TOGGLE_FIELDS = [
|
||||
'enable_vpn_portal',
|
||||
'enable_relay_network_whitelist',
|
||||
'enable_manual_routes',
|
||||
'enable_socks5',
|
||||
] as const satisfies readonly (keyof NetworkConfig)[]
|
||||
|
||||
const CONFIG_UI_BOOLEAN_FIELDS = [
|
||||
...CONFIG_CHECKBOX_FIELDS.map(([field]) => field),
|
||||
...CONFIG_TOGGLE_FIELDS,
|
||||
] as const satisfies readonly (keyof NetworkConfig)[]
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string, values?: unknown[]) => values ? `${key}:${values.join(',')}` : key,
|
||||
}),
|
||||
}))
|
||||
|
||||
const PassThrough = defineComponent({
|
||||
name: 'PassThrough',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', slots.default?.())
|
||||
},
|
||||
})
|
||||
|
||||
const PanelStub = defineComponent({
|
||||
name: 'Panel',
|
||||
props: {
|
||||
header: String,
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
return () => h('section', { 'data-stub': 'panel', 'data-header': props.header }, slots.default?.())
|
||||
},
|
||||
})
|
||||
|
||||
const DividerStub = defineComponent({
|
||||
name: 'Divider',
|
||||
setup() {
|
||||
return () => h('hr', { 'data-stub': 'divider' })
|
||||
},
|
||||
})
|
||||
|
||||
function splitList(value: string): string[] {
|
||||
return value.split(',').map((item) => item.trim()).filter((item) => item.length > 0)
|
||||
}
|
||||
|
||||
const InputTextStub = defineComponent({
|
||||
name: 'InputText',
|
||||
props: {
|
||||
modelValue: [String, Number],
|
||||
id: String,
|
||||
disabled: Boolean,
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { attrs, emit }) {
|
||||
return () => h('input', {
|
||||
...attrs,
|
||||
id: props.id,
|
||||
disabled: props.disabled,
|
||||
value: props.modelValue ?? '',
|
||||
'data-stub': 'input-text',
|
||||
onInput: (event: Event) => emit('update:modelValue', (event.target as HTMLInputElement).value),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const PasswordStub = defineComponent({
|
||||
name: 'Password',
|
||||
props: {
|
||||
modelValue: [String, Number],
|
||||
id: String,
|
||||
disabled: Boolean,
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { attrs, emit }) {
|
||||
return () => h('input', {
|
||||
...attrs,
|
||||
id: props.id,
|
||||
disabled: props.disabled,
|
||||
type: 'password',
|
||||
value: props.modelValue ?? '',
|
||||
'data-stub': 'password',
|
||||
onInput: (event: Event) => emit('update:modelValue', (event.target as HTMLInputElement).value),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const InputNumberStub = defineComponent({
|
||||
name: 'InputNumber',
|
||||
props: {
|
||||
modelValue: Number,
|
||||
id: String,
|
||||
inputId: String,
|
||||
disabled: Boolean,
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { attrs, emit }) {
|
||||
return () => h('input', {
|
||||
...attrs,
|
||||
id: props.id ?? props.inputId,
|
||||
disabled: props.disabled,
|
||||
type: 'number',
|
||||
value: props.modelValue ?? '',
|
||||
'data-stub': 'input-number',
|
||||
onInput: (event: Event) => {
|
||||
const value = (event.target as HTMLInputElement).value
|
||||
emit('update:modelValue', value === '' ? null : Number(value))
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const CheckboxStub = defineComponent({
|
||||
name: 'Checkbox',
|
||||
props: {
|
||||
modelValue: Boolean,
|
||||
inputId: String,
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { attrs, emit }) {
|
||||
return () => h('input', {
|
||||
...attrs,
|
||||
id: props.inputId,
|
||||
checked: props.modelValue,
|
||||
type: 'checkbox',
|
||||
'data-stub': 'checkbox',
|
||||
onChange: (event: Event) => emit('update:modelValue', (event.target as HTMLInputElement).checked),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const ToggleButtonStub = defineComponent({
|
||||
name: 'ToggleButton',
|
||||
props: {
|
||||
modelValue: Boolean,
|
||||
onIcon: String,
|
||||
offIcon: String,
|
||||
onLabel: String,
|
||||
offLabel: String,
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
return () => h('button', {
|
||||
type: 'button',
|
||||
'aria-pressed': String(Boolean(props.modelValue)),
|
||||
'data-stub': 'toggle-button',
|
||||
onClick: () => emit('update:modelValue', !props.modelValue),
|
||||
}, props.modelValue ? props.onLabel : props.offLabel)
|
||||
},
|
||||
})
|
||||
|
||||
const AutoCompleteStub = defineComponent({
|
||||
name: 'AutoComplete',
|
||||
props: {
|
||||
modelValue: Array,
|
||||
id: String,
|
||||
multiple: Boolean,
|
||||
},
|
||||
emits: ['update:modelValue', 'complete'],
|
||||
setup(props, { attrs, emit }) {
|
||||
return () => h('input', {
|
||||
...attrs,
|
||||
id: props.id,
|
||||
value: (props.modelValue ?? []).join(','),
|
||||
'data-stub': 'auto-complete',
|
||||
onInput: (event: Event) => emit('update:modelValue', splitList((event.target as HTMLInputElement).value)),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const UrlListInputStub = defineComponent({
|
||||
name: 'UrlListInput',
|
||||
props: {
|
||||
modelValue: Array,
|
||||
id: String,
|
||||
addLabel: String,
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { attrs, emit }) {
|
||||
return () => h('input', {
|
||||
...attrs,
|
||||
id: props.id,
|
||||
value: (props.modelValue ?? []).join(','),
|
||||
'data-stub': 'url-list-input',
|
||||
'data-add-label': props.addLabel,
|
||||
onInput: (event: Event) => emit('update:modelValue', splitList((event.target as HTMLInputElement).value)),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const SelectButtonStub = defineComponent({
|
||||
name: 'SelectButton',
|
||||
props: {
|
||||
modelValue: String,
|
||||
options: Array,
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
return () => h('select', {
|
||||
value: props.modelValue,
|
||||
'data-stub': 'select-button',
|
||||
onChange: (event: Event) => emit('update:modelValue', (event.target as HTMLSelectElement).value),
|
||||
}, (props.options ?? []).map((option) => h('option', { value: option as string }, option as string)))
|
||||
},
|
||||
})
|
||||
|
||||
const ButtonStub = defineComponent({
|
||||
name: 'Button',
|
||||
props: {
|
||||
label: String,
|
||||
icon: String,
|
||||
disabled: Boolean,
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { slots, emit }) {
|
||||
return () => h('button', {
|
||||
type: 'button',
|
||||
disabled: props.disabled,
|
||||
'data-label': props.label ?? props.icon,
|
||||
onClick: (event: MouseEvent) => emit('click', event),
|
||||
}, slots.default?.() ?? props.label ?? props.icon)
|
||||
},
|
||||
})
|
||||
|
||||
const DialogStub = defineComponent({
|
||||
name: 'Dialog',
|
||||
props: {
|
||||
visible: Boolean,
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
return () => h('div', { hidden: !props.visible, 'data-stub': 'dialog' }, [
|
||||
slots.default?.(),
|
||||
slots.footer?.(),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
const AclManagerStub = defineComponent({
|
||||
name: 'AclManager',
|
||||
props: {
|
||||
modelValue: Object,
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props) {
|
||||
return () => h('pre', { 'data-stub': 'acl-manager' }, JSON.stringify(props.modelValue))
|
||||
},
|
||||
})
|
||||
|
||||
function makeConfig(): NetworkConfig {
|
||||
const config = DEFAULT_NETWORK_CONFIG()
|
||||
|
||||
return {
|
||||
...config,
|
||||
dhcp: false,
|
||||
virtual_ipv4: '10.1.2.3',
|
||||
network_length: 24,
|
||||
network_name: 'mesh-a',
|
||||
network_secret: 'secret-a',
|
||||
peer_urls: ['tcp://peer-a:11010', 'udp://peer-b:11010'],
|
||||
latency_first: true,
|
||||
use_smoltcp: true,
|
||||
disable_ipv6: true,
|
||||
no_tun: true,
|
||||
hostname: 'host-a',
|
||||
proxy_cidrs: ['10.10.0.0/16', '172.16.1.0/24'],
|
||||
enable_vpn_portal: true,
|
||||
vpn_portal_client_network_addr: '10.144.0.0',
|
||||
vpn_portal_listen_port: 22023,
|
||||
listener_urls: ['tcp://0.0.0.0:12010'],
|
||||
dev_name: 'tun-test',
|
||||
mtu: 1280,
|
||||
instance_recv_bps_limit: '9007199254740993',
|
||||
enable_relay_network_whitelist: true,
|
||||
relay_network_whitelist: ['network-a'],
|
||||
enable_manual_routes: true,
|
||||
routes: ['192.168.0.0/16'],
|
||||
enable_socks5: true,
|
||||
socks5_port: 1086,
|
||||
exit_nodes: ['exit-a'],
|
||||
mapped_listeners: ['tcp://127.0.0.1:22000'],
|
||||
port_forwards: [{
|
||||
proto: 'udp',
|
||||
bind_ip: '0.0.0.0',
|
||||
bind_port: 18080,
|
||||
dst_ip: '10.0.0.2',
|
||||
dst_port: 8080,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
function mountConfig(config: NetworkConfig = makeConfig()) {
|
||||
const curNetwork = reactive(config) as NetworkConfig
|
||||
const wrapper = mount(Config, {
|
||||
props: {
|
||||
curNetwork,
|
||||
hostname: 'host-from-prop',
|
||||
},
|
||||
global: {
|
||||
directives: {
|
||||
tooltip: () => {},
|
||||
},
|
||||
stubs: {
|
||||
AclManager: AclManagerStub,
|
||||
AutoComplete: AutoCompleteStub,
|
||||
Button: ButtonStub,
|
||||
Checkbox: CheckboxStub,
|
||||
Dialog: DialogStub,
|
||||
Divider: DividerStub,
|
||||
InputGroup: PassThrough,
|
||||
InputGroupAddon: PassThrough,
|
||||
InputNumber: InputNumberStub,
|
||||
InputText: InputTextStub,
|
||||
Panel: PanelStub,
|
||||
Password: PasswordStub,
|
||||
SelectButton: SelectButtonStub,
|
||||
ToggleButton: ToggleButtonStub,
|
||||
UrlListInput: UrlListInputStub,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return { curNetwork, wrapper }
|
||||
}
|
||||
|
||||
function input(wrapper: VueWrapper, selector: string): HTMLInputElement {
|
||||
return wrapper.find(selector).element as HTMLInputElement
|
||||
}
|
||||
|
||||
async function setInput(wrapper: VueWrapper, selector: string, value: string) {
|
||||
await wrapper.find(selector).setValue(value)
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
describe('Config.vue network config projection', () => {
|
||||
it('projects config values into the visible form controls', async () => {
|
||||
const { curNetwork, wrapper } = mountConfig()
|
||||
await nextTick()
|
||||
|
||||
expect(input(wrapper, '#network_name').value).toBe('mesh-a')
|
||||
expect(input(wrapper, '#network_secret').value).toBe('secret-a')
|
||||
expect(input(wrapper, '#virtual_ip').value).toBe('10.1.2.3')
|
||||
expect(input(wrapper, '#initial_nodes').value).toBe('tcp://peer-a:11010,udp://peer-b:11010')
|
||||
expect(input(wrapper, '#virtual_ip_auto').checked).toBe(false)
|
||||
expect(input(wrapper, '#latency_first').checked).toBe(true)
|
||||
expect(input(wrapper, '#use_smoltcp').checked).toBe(true)
|
||||
expect(input(wrapper, '#disable_ipv6').checked).toBe(true)
|
||||
expect(input(wrapper, '#no_tun').checked).toBe(true)
|
||||
|
||||
expect(input(wrapper, '#hostname').value).toBe('host-a')
|
||||
expect(input(wrapper, '#subnet-proxy').value).toBe('10.10.0.0/16,172.16.1.0/24')
|
||||
expect(input(wrapper, 'input[placeholder="vpn_portal_client_network"]').value).toBe('10.144.0.0')
|
||||
expect(input(wrapper, '#dev_name').value).toBe('tun-test')
|
||||
expect(input(wrapper, '#mtu').value).toBe('1280')
|
||||
expect(input(wrapper, '#instance_recv_bps_limit').value).toBe('9007199254740993')
|
||||
expect(input(wrapper, '#relay_network_whitelist').value).toBe('network-a')
|
||||
expect(input(wrapper, '#routes').value).toBe('192.168.0.0/16')
|
||||
expect(input(wrapper, '#socks5_port').value).toBe('1086')
|
||||
expect(input(wrapper, '#exit_nodes').value).toBe('exit-a')
|
||||
expect(input(wrapper, 'input[data-add-label="add_listener_url"]').value).toBe('tcp://0.0.0.0:12010')
|
||||
expect(input(wrapper, 'input[data-add-label="add_mapped_listener"]').value).toBe('tcp://127.0.0.1:22000')
|
||||
|
||||
expect(wrapper.find<HTMLSelectElement>('select[data-stub="select-button"]').element.value).toBe('udp')
|
||||
expect(input(wrapper, 'input[placeholder="port_forwards_bind_addr"]').value).toBe('0.0.0.0')
|
||||
expect(input(wrapper, 'input[placeholder="port_forwards_dst_addr"]').value).toBe('10.0.0.2')
|
||||
expect(wrapper.findComponent(AclManagerStub).props('modelValue')).toStrictEqual(curNetwork.acl)
|
||||
})
|
||||
|
||||
it('projects form edits back into config and backend JSON', async () => {
|
||||
const { curNetwork, wrapper } = mountConfig()
|
||||
await nextTick()
|
||||
|
||||
await wrapper.find('#virtual_ip_auto').setValue(false)
|
||||
await setInput(wrapper, '#network_name', 'mesh-edited')
|
||||
await setInput(wrapper, '#network_secret', 'secret-edited')
|
||||
await setInput(wrapper, '#virtual_ip', '10.7.7.7')
|
||||
await setInput(wrapper, '#initial_nodes', ' tcp://peer-x:11010, , udp://peer-y:11010 ')
|
||||
await wrapper.find('#no_tun').setValue(false)
|
||||
await wrapper.find('#disable_ipv6').setValue(false)
|
||||
await setInput(wrapper, '#hostname', 'host-edited')
|
||||
await setInput(wrapper, '#subnet-proxy', '10.7.0.0/16,172.17.0.0/16')
|
||||
await setInput(wrapper, 'input[placeholder="vpn_portal_client_network"]', '10.200.0.0')
|
||||
await setInput(wrapper, 'input[data-add-label="add_listener_url"]', 'tcp://0.0.0.0:13010')
|
||||
await setInput(wrapper, '#dev_name', 'tun-edited')
|
||||
await setInput(wrapper, '#mtu', '1260')
|
||||
await setInput(wrapper, '#instance_recv_bps_limit', '9007199254740993')
|
||||
await setInput(wrapper, '#relay_network_whitelist', 'network-edited')
|
||||
await setInput(wrapper, '#routes', '192.168.10.0/24')
|
||||
await setInput(wrapper, '#socks5_port', '1089')
|
||||
await setInput(wrapper, '#exit_nodes', 'exit-edited')
|
||||
await setInput(wrapper, 'input[data-add-label="add_mapped_listener"]', 'tcp://127.0.0.1:23000')
|
||||
await wrapper.find('select[data-stub="select-button"]').setValue('tcp')
|
||||
await setInput(wrapper, 'input[placeholder="port_forwards_bind_addr"]', '127.0.0.1')
|
||||
await setInput(wrapper, 'input[placeholder="port_forwards_dst_addr"]', '10.9.0.2')
|
||||
|
||||
const portNumbers = wrapper.findAll<HTMLInputElement>('input#horizontal-buttons')
|
||||
await portNumbers[1].setValue('19090')
|
||||
await portNumbers[2].setValue('9090')
|
||||
|
||||
expect(curNetwork).toMatchObject({
|
||||
dhcp: false,
|
||||
virtual_ipv4: '10.7.7.7',
|
||||
network_name: 'mesh-edited',
|
||||
network_secret: 'secret-edited',
|
||||
peer_urls: ['tcp://peer-x:11010', 'udp://peer-y:11010'],
|
||||
no_tun: false,
|
||||
disable_ipv6: false,
|
||||
hostname: 'host-edited',
|
||||
proxy_cidrs: ['10.7.0.0/16', '172.17.0.0/16'],
|
||||
vpn_portal_client_network_addr: '10.200.0.0',
|
||||
listener_urls: ['tcp://0.0.0.0:13010'],
|
||||
dev_name: 'tun-edited',
|
||||
mtu: 1260,
|
||||
instance_recv_bps_limit: '9007199254740993',
|
||||
relay_network_whitelist: ['network-edited'],
|
||||
routes: ['192.168.10.0/24'],
|
||||
socks5_port: 1089,
|
||||
exit_nodes: ['exit-edited'],
|
||||
mapped_listeners: ['tcp://127.0.0.1:23000'],
|
||||
port_forwards: [{
|
||||
proto: 'tcp',
|
||||
bind_ip: '127.0.0.1',
|
||||
bind_port: 19090,
|
||||
dst_ip: '10.9.0.2',
|
||||
dst_port: 9090,
|
||||
}],
|
||||
})
|
||||
|
||||
const backend = toBackendNetworkConfig(curNetwork)
|
||||
expect(backend).toMatchObject({
|
||||
virtual_ipv4: '10.7.7.7',
|
||||
network_name: 'mesh-edited',
|
||||
network_secret: 'secret-edited',
|
||||
peer_urls: ['tcp://peer-x:11010', 'udp://peer-y:11010'],
|
||||
listener_urls: ['tcp://0.0.0.0:13010'],
|
||||
mtu: 1260,
|
||||
instance_recv_bps_limit: '9007199254740993',
|
||||
port_forwards: [{
|
||||
proto: 'tcp',
|
||||
bind_ip: '127.0.0.1',
|
||||
bind_port: 19090,
|
||||
dst_ip: '10.9.0.2',
|
||||
dst_port: 9090,
|
||||
}],
|
||||
})
|
||||
})
|
||||
|
||||
it('round-trips every visible boolean config control into backend JSON', async () => {
|
||||
const config = makeConfig()
|
||||
const originalFlagValues = new Map(
|
||||
CONFIG_UI_BOOLEAN_FIELDS.map((field, index) => {
|
||||
const value = index % 2 === 0
|
||||
config[field] = value
|
||||
return [field, value]
|
||||
}),
|
||||
)
|
||||
|
||||
const { curNetwork, wrapper } = mountConfig(config)
|
||||
await nextTick()
|
||||
|
||||
for (const [field, selector] of CONFIG_CHECKBOX_FIELDS) {
|
||||
const value = originalFlagValues.get(field)
|
||||
expect(input(wrapper, selector).checked, `${field} should project into UI`).toBe(value)
|
||||
await wrapper.find(selector).setValue(!value)
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
const toggleButtons = wrapper.findAll('button[data-stub="toggle-button"]')
|
||||
expect(toggleButtons).toHaveLength(CONFIG_TOGGLE_FIELDS.length)
|
||||
for (const [index, field] of CONFIG_TOGGLE_FIELDS.entries()) {
|
||||
const value = originalFlagValues.get(field)
|
||||
expect(toggleButtons[index].attributes('aria-pressed'), `${field} should project into UI`)
|
||||
.toBe(String(value))
|
||||
await toggleButtons[index].trigger('click')
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
const backend = toBackendNetworkConfig(curNetwork) as Record<string, unknown>
|
||||
for (const [field, value] of originalFlagValues) {
|
||||
const expectedValue = !value
|
||||
expect(curNetwork[field], `${field} should update config`).toBe(expectedValue)
|
||||
expect(backend[field], `${field} should be preserved in backend JSON`).toBe(expectedValue)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps uint64 input editable without losing large values', async () => {
|
||||
const { curNetwork, wrapper } = mountConfig()
|
||||
await nextTick()
|
||||
|
||||
await setInput(wrapper, '#instance_recv_bps_limit', '1234')
|
||||
expect(curNetwork.instance_recv_bps_limit).toBe(1234)
|
||||
|
||||
await setInput(wrapper, '#instance_recv_bps_limit', 'not-a-number')
|
||||
expect(curNetwork.instance_recv_bps_limit).toBe(1234)
|
||||
|
||||
await setInput(wrapper, '#instance_recv_bps_limit', '0')
|
||||
expect(curNetwork.instance_recv_bps_limit).toBeNull()
|
||||
expect(input(wrapper, '#instance_recv_bps_limit').value).toBe('')
|
||||
|
||||
await setInput(wrapper, '#instance_recv_bps_limit', '9007199254740993')
|
||||
expect(curNetwork.instance_recv_bps_limit).toBe('9007199254740993')
|
||||
|
||||
await setInput(wrapper, '#instance_recv_bps_limit', '18446744073709551616')
|
||||
expect(curNetwork.instance_recv_bps_limit).toBe('9007199254740993')
|
||||
|
||||
await setInput(wrapper, '#instance_recv_bps_limit', '')
|
||||
expect(curNetwork.instance_recv_bps_limit).toBeNull()
|
||||
})
|
||||
|
||||
it('emits runNetwork with the current projected config', async () => {
|
||||
const { curNetwork, wrapper } = mountConfig()
|
||||
await nextTick()
|
||||
|
||||
await setInput(wrapper, '#network_name', 'mesh-running')
|
||||
await wrapper.find('button[data-label="run_network"]').trigger('click')
|
||||
|
||||
expect(wrapper.emitted('runNetwork')?.[0]).toEqual([curNetwork])
|
||||
expect((wrapper.emitted('runNetwork')?.[0][0] as NetworkConfig).network_name).toBe('mesh-running')
|
||||
})
|
||||
})
|
||||
@@ -1,228 +0,0 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import RemoteManagement from '../src/components/RemoteManagement.vue'
|
||||
import {
|
||||
DEFAULT_NETWORK_CONFIG,
|
||||
type NetworkConfig,
|
||||
} from '../src/types/network'
|
||||
|
||||
const BOOLEAN_CONFIG_FIELDS = [
|
||||
'dhcp',
|
||||
'enable_vpn_portal',
|
||||
'advanced_settings',
|
||||
'latency_first',
|
||||
'use_smoltcp',
|
||||
'disable_ipv6',
|
||||
'enable_kcp_proxy',
|
||||
'disable_kcp_input',
|
||||
'disable_p2p',
|
||||
'bind_device',
|
||||
'no_tun',
|
||||
'enable_exit_node',
|
||||
'relay_all_peer_rpc',
|
||||
'multi_thread',
|
||||
'enable_relay_network_whitelist',
|
||||
'enable_manual_routes',
|
||||
'proxy_forward_by_system',
|
||||
'disable_encryption',
|
||||
'enable_socks5',
|
||||
'disable_udp_hole_punching',
|
||||
'enable_magic_dns',
|
||||
'enable_private_mode',
|
||||
'enable_quic_proxy',
|
||||
'disable_quic_input',
|
||||
'disable_sym_hole_punching',
|
||||
'p2p_only',
|
||||
'lazy_p2p',
|
||||
'need_p2p',
|
||||
'disable_upnp',
|
||||
'ipv6_public_addr_provider',
|
||||
'ipv6_public_addr_auto',
|
||||
'disable_relay_data',
|
||||
'enable_udp_broadcast_relay',
|
||||
'disable_tcp_hole_punching',
|
||||
] as const satisfies readonly (keyof NetworkConfig)[]
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('primevue', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
|
||||
const PassThrough = defineComponent({
|
||||
name: 'PassThrough',
|
||||
props: {
|
||||
label: String,
|
||||
value: String,
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
return () => h('div', {
|
||||
'data-label': props.label,
|
||||
'data-value': props.value,
|
||||
'data-stub': 'pass-through',
|
||||
}, slots.default?.())
|
||||
},
|
||||
})
|
||||
|
||||
const ButtonStub = defineComponent({
|
||||
name: 'Button',
|
||||
props: {
|
||||
label: String,
|
||||
icon: String,
|
||||
disabled: Boolean,
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { slots, emit }) {
|
||||
return () => h('button', {
|
||||
type: 'button',
|
||||
disabled: props.disabled,
|
||||
'data-label': props.label ?? props.icon,
|
||||
onClick: (event: MouseEvent) => emit('click', event),
|
||||
}, slots.default?.() ?? props.label ?? props.icon)
|
||||
},
|
||||
})
|
||||
|
||||
const SelectStub = defineComponent({
|
||||
name: 'Select',
|
||||
props: {
|
||||
modelValue: Object,
|
||||
options: Array,
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { slots }) {
|
||||
return () => h('div', { 'data-stub': 'select' }, [
|
||||
slots.value?.({ value: props.modelValue, placeholder: '' }),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
const MenuStub = defineComponent({
|
||||
name: 'Menu',
|
||||
setup(_, { expose }) {
|
||||
expose({ toggle: vi.fn() })
|
||||
return () => h('div', { 'data-stub': 'menu' })
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
Button: ButtonStub,
|
||||
ConfirmPopup: PassThrough,
|
||||
Divider: PassThrough,
|
||||
IftaLabel: PassThrough,
|
||||
Menu: MenuStub,
|
||||
Message: PassThrough,
|
||||
Select: SelectStub,
|
||||
Tag: PassThrough,
|
||||
useConfirm: () => ({ require: vi.fn() }),
|
||||
useToast: () => ({ add: vi.fn() }),
|
||||
}
|
||||
})
|
||||
|
||||
const INSTANCE_ID = '00000000-0000-0000-0000-000000000001'
|
||||
const INSTANCE_UUID = {
|
||||
part1: 0,
|
||||
part2: 0,
|
||||
part3: 0,
|
||||
part4: 1,
|
||||
}
|
||||
|
||||
function makeFlagConfig(): NetworkConfig {
|
||||
const config = {
|
||||
...DEFAULT_NETWORK_CONFIG(),
|
||||
instance_id: INSTANCE_ID,
|
||||
network_name: 'mesh-save',
|
||||
}
|
||||
|
||||
BOOLEAN_CONFIG_FIELDS.forEach((field, index) => {
|
||||
config[field] = index % 2 === 0
|
||||
})
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
function cloneConfig(config: NetworkConfig): NetworkConfig {
|
||||
return JSON.parse(JSON.stringify(config)) as NetworkConfig
|
||||
}
|
||||
|
||||
function snapshotBooleanConfigFields(config: NetworkConfig): Record<string, unknown> {
|
||||
return Object.fromEntries(
|
||||
BOOLEAN_CONFIG_FIELDS.map((field) => [field, config[field]]),
|
||||
)
|
||||
}
|
||||
|
||||
async function settleRemoteManagement() {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
}
|
||||
}
|
||||
|
||||
describe('RemoteManagement config save', () => {
|
||||
it('saves the current network config without dropping boolean fields', async () => {
|
||||
const config = makeFlagConfig()
|
||||
const expectedFlags = snapshotBooleanConfigFields(config)
|
||||
const api = {
|
||||
delete_network: vi.fn(),
|
||||
generate_config: vi.fn(),
|
||||
get_network_config: vi.fn(async () => cloneConfig(config)),
|
||||
get_network_info: vi.fn(),
|
||||
get_network_metas: vi.fn(async (instanceIds: string[]) => ({
|
||||
metas: Object.fromEntries(instanceIds.map((id) => [id, {
|
||||
config_permission: 0xffffffff,
|
||||
inst_id: INSTANCE_UUID,
|
||||
instance_name: 'mesh-save',
|
||||
network_name: 'mesh-save',
|
||||
source: 2,
|
||||
}])),
|
||||
})),
|
||||
list_network_instance_ids: vi.fn(async () => ({
|
||||
disabled_inst_ids: [INSTANCE_UUID],
|
||||
running_inst_ids: [],
|
||||
})),
|
||||
parse_config: vi.fn(),
|
||||
run_network: vi.fn(),
|
||||
save_config: vi.fn(async () => undefined),
|
||||
update_network_instance_state: vi.fn(),
|
||||
validate_config: vi.fn(),
|
||||
}
|
||||
|
||||
const wrapper = mount(RemoteManagement, {
|
||||
props: {
|
||||
api,
|
||||
instanceId: INSTANCE_ID,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
Config: true,
|
||||
ConfigEditDialog: true,
|
||||
Status: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
await settleRemoteManagement()
|
||||
|
||||
const saveButton = wrapper.find('button[data-label="web.device_management.save_config"]')
|
||||
expect(saveButton.exists()).toBe(true)
|
||||
expect(saveButton.attributes('disabled')).toBeUndefined()
|
||||
|
||||
await saveButton.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(api.save_config).toHaveBeenCalledOnce()
|
||||
const savedConfig = api.save_config.mock.calls[0][0] as NetworkConfig
|
||||
|
||||
for (const field of BOOLEAN_CONFIG_FIELDS) {
|
||||
expect(savedConfig[field], `${field} should be saved`).toBe(expectedFlags[field])
|
||||
}
|
||||
} finally {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,9 +0,0 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
class ResizeObserverStub {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||||
@@ -1,12 +0,0 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import ViteYaml from '@modyfi/vite-plugin-yaml'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue(), ViteYaml()],
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
include: ['tests/**/*.spec.ts'],
|
||||
setupFiles: ['./tests/setup.ts'],
|
||||
},
|
||||
})
|
||||
@@ -4,8 +4,8 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "pnpm --dir ../frontend-lib build && vite",
|
||||
"build": "pnpm --dir ../frontend-lib build && vue-tsc -b && vite build",
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -32,4 +32,4 @@
|
||||
"vite-plugin-singlefile": "^2.0.3",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ use session::{Location, Session};
|
||||
use storage::{Storage, StorageToken};
|
||||
|
||||
use crate::FeatureFlags;
|
||||
use crate::webhook::{ManagedNetworkConfig, SharedWebhookConfig};
|
||||
use crate::webhook::SharedWebhookConfig;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use crate::db::{Db, UserIdInDb, entity::user_running_network_configs};
|
||||
@@ -146,7 +146,20 @@ impl ClientManager {
|
||||
}
|
||||
|
||||
pub async fn list_sessions(&self) -> Vec<StorageToken> {
|
||||
self.storage.list_clients()
|
||||
let sessions = self
|
||||
.client_sessions
|
||||
.iter()
|
||||
.map(|item| item.value().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut ret: Vec<StorageToken> = vec![];
|
||||
for s in sessions {
|
||||
if let Some(t) = s.get_token().await {
|
||||
ret.push(t);
|
||||
}
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn get_session_by_machine_id(
|
||||
@@ -184,22 +197,6 @@ impl ClientManager {
|
||||
self.storage.list_user_clients(user_id)
|
||||
}
|
||||
|
||||
pub async fn reconcile_managed_network_configs(
|
||||
&self,
|
||||
user_id: UserIdInDb,
|
||||
machine_id: uuid::Uuid,
|
||||
desired_configs: Vec<ManagedNetworkConfig>,
|
||||
) -> anyhow::Result<()> {
|
||||
session::SessionRpcService::reconcile_web_source_configs(
|
||||
&self.storage,
|
||||
user_id,
|
||||
machine_id,
|
||||
desired_configs,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_heartbeat_requests(&self, client_url: &url::Url) -> Option<HeartbeatRequest> {
|
||||
let s = self.client_sessions.get(client_url)?.clone();
|
||||
s.data().read().await.req()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -114,20 +114,6 @@ impl Storage {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn list_clients(&self) -> Vec<StorageToken> {
|
||||
self.0
|
||||
.user_clients_map
|
||||
.iter()
|
||||
.flat_map(|user_clients| {
|
||||
user_clients
|
||||
.value()
|
||||
.iter()
|
||||
.map(|info| info.value().storage_token.clone())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn db(&self) -> &Db {
|
||||
&self.0.db
|
||||
}
|
||||
@@ -188,25 +174,4 @@ mod tests {
|
||||
|
||||
assert_eq!(storage.get_client_url_by_machine_id(2, &machine_id), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_clients_returns_current_storage_tokens() {
|
||||
let storage = Storage::new(Db::memory_db().await);
|
||||
let user1_token = make_storage_token(1, uuid::Uuid::new_v4(), "tcp://127.0.0.1:1001");
|
||||
let user2_token = make_storage_token(2, uuid::Uuid::new_v4(), "tcp://127.0.0.1:1002");
|
||||
|
||||
storage.update_client(user1_token.clone(), 10);
|
||||
storage.update_client(user2_token.clone(), 20);
|
||||
|
||||
let tokens = storage.list_clients();
|
||||
assert_eq!(tokens.len(), 2);
|
||||
assert!(tokens.iter().any(|token| token.token == user1_token.token));
|
||||
assert!(tokens.iter().any(|token| token.token == user2_token.token));
|
||||
|
||||
storage.remove_client(&user1_token);
|
||||
|
||||
let tokens = storage.list_clients();
|
||||
assert_eq!(tokens.len(), 1);
|
||||
assert_eq!(tokens[0].token, user2_token.token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,7 +331,7 @@ mod tests {
|
||||
(user_id, device_id),
|
||||
inst_id,
|
||||
network_config,
|
||||
ConfigSource::Web,
|
||||
ConfigSource::Webhook,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -344,10 +344,10 @@ mod tests {
|
||||
.unwrap();
|
||||
println!("device: {}, {:?}", device_id, result2);
|
||||
assert_eq!(result2.network_config, network_config_json);
|
||||
assert_eq!(result2.get_network_config_source(), ConfigSource::Web);
|
||||
assert_eq!(result2.get_network_config_source(), ConfigSource::Webhook);
|
||||
assert_eq!(
|
||||
result2.get_runtime_network_config_source(),
|
||||
ConfigSource::Web
|
||||
ConfigSource::Webhook
|
||||
);
|
||||
|
||||
assert_eq!(result.create_time, result2.create_time);
|
||||
@@ -373,7 +373,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unknown_network_config_source_defaults_to_user_runtime_source() {
|
||||
async fn test_legacy_network_config_defaults_to_user_runtime_source() {
|
||||
let db = Db::memory_db().await;
|
||||
let user_id = 1;
|
||||
let inst_id = uuid::Uuid::new_v4();
|
||||
@@ -384,11 +384,11 @@ mod tests {
|
||||
device_id: Set(device_id.to_string()),
|
||||
network_instance_id: Set(inst_id.to_string()),
|
||||
network_config: Set(serde_json::to_string(&NetworkConfig {
|
||||
network_name: Some("unknown-source".to_string()),
|
||||
network_name: Some("legacy".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap()),
|
||||
source: Set("unknown".to_string()),
|
||||
source: Set("legacy".to_string()),
|
||||
disabled: Set(false),
|
||||
create_time: Set(sqlx::types::chrono::Local::now().fixed_offset()),
|
||||
update_time: Set(sqlx::types::chrono::Local::now().fixed_offset()),
|
||||
|
||||
@@ -48,7 +48,7 @@ impl MigrationTrait for Migration {
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
'user',
|
||||
'legacy',
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20260514_000004_rename_web_config_source"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
db.execute_unprepared(
|
||||
r#"
|
||||
UPDATE user_running_network_configs
|
||||
SET source = 'web'
|
||||
WHERE source = 'webhook';
|
||||
|
||||
UPDATE user_running_network_configs
|
||||
SET source = 'user'
|
||||
WHERE source = 'legacy';
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
db.execute_unprepared(
|
||||
r#"
|
||||
UPDATE user_running_network_configs
|
||||
SET source = 'webhook'
|
||||
WHERE source = 'web';
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ use sea_orm_migration::prelude::*;
|
||||
mod m20241029_000001_init;
|
||||
mod m20260403_000002_scope_network_config_unique;
|
||||
mod m20260421_000003_add_network_config_source;
|
||||
mod m20260514_000004_rename_web_config_source;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -14,7 +13,6 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20241029_000001_init::Migration),
|
||||
Box::new(m20260403_000002_scope_network_config_unique::Migration),
|
||||
Box::new(m20260421_000003_add_network_config_source::Migration),
|
||||
Box::new(m20260514_000004_rename_web_config_source::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ use axum::http::StatusCode;
|
||||
use axum::routing::{delete, post};
|
||||
use axum::{Json, Router, extract::State, routing::get};
|
||||
use axum_login::AuthUser;
|
||||
use easytier::common::config::ConfigSource as RuntimeConfigSource;
|
||||
use easytier::launcher::NetworkConfig;
|
||||
use easytier::proto::common::Void;
|
||||
use easytier::proto::{api::manage::*, web::*};
|
||||
@@ -61,7 +60,6 @@ struct SaveNetworkJsonReq {
|
||||
struct RunNetworkJsonReq {
|
||||
config: NetworkConfig,
|
||||
save: bool,
|
||||
source: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
@@ -84,17 +82,6 @@ struct RemoveNetworkJsonReq {
|
||||
inst_ids: Vec<uuid::Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
struct ManagedNetworkConfigJson {
|
||||
instance_id: uuid::Uuid,
|
||||
network_config: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
struct ReconcileManagedNetworkConfigsJsonReq {
|
||||
managed_network_configs: Vec<ManagedNetworkConfigJson>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
struct ListMachineItem {
|
||||
client_url: Option<url::Url>,
|
||||
@@ -143,11 +130,10 @@ impl NetworkApi {
|
||||
Json(payload): Json<RunNetworkJsonReq>,
|
||||
) -> Result<Json<Void>, HttpHandleError> {
|
||||
client_mgr
|
||||
.handle_run_network_instance_with_source(
|
||||
.handle_run_network_instance(
|
||||
(Self::get_user_id(&auth_session)?, machine_id),
|
||||
payload.config,
|
||||
payload.save,
|
||||
RuntimeConfigSource::Web,
|
||||
)
|
||||
.await
|
||||
.map_err(convert_error)?;
|
||||
@@ -288,11 +274,10 @@ impl NetworkApi {
|
||||
));
|
||||
}
|
||||
client_mgr
|
||||
.handle_save_network_config_with_source(
|
||||
.handle_save_network_config(
|
||||
(Self::get_user_id(&auth_session)?, machine_id),
|
||||
inst_id,
|
||||
payload.config,
|
||||
RuntimeConfigSource::Web,
|
||||
)
|
||||
.await
|
||||
.map_err(convert_error)
|
||||
@@ -317,17 +302,8 @@ impl NetworkApi {
|
||||
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
|
||||
Json(payload): Json<RunNetworkJsonReq>,
|
||||
) -> Result<Json<Void>, HttpHandleError> {
|
||||
let source = payload
|
||||
.source
|
||||
.and_then(RuntimeConfigSource::from_rpc)
|
||||
.unwrap_or(RuntimeConfigSource::Web);
|
||||
client_mgr
|
||||
.handle_run_network_instance_with_source(
|
||||
(user_id, machine_id),
|
||||
payload.config,
|
||||
payload.save,
|
||||
source,
|
||||
)
|
||||
.handle_run_network_instance((user_id, machine_id), payload.config, payload.save)
|
||||
.await
|
||||
.map_err(convert_error)?;
|
||||
Ok(Void::default().into())
|
||||
@@ -343,31 +319,6 @@ impl NetworkApi {
|
||||
.map_err(convert_error)
|
||||
}
|
||||
|
||||
async fn handle_reconcile_managed_network_configs_internal(
|
||||
State(client_mgr): AppState,
|
||||
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
|
||||
Json(payload): Json<ReconcileManagedNetworkConfigsJsonReq>,
|
||||
) -> Result<Json<Void>, HttpHandleError> {
|
||||
let desired = payload
|
||||
.managed_network_configs
|
||||
.into_iter()
|
||||
.map(|item| crate::webhook::ManagedNetworkConfig {
|
||||
instance_id: item.instance_id.to_string(),
|
||||
network_config: item.network_config,
|
||||
})
|
||||
.collect();
|
||||
client_mgr
|
||||
.reconcile_managed_network_configs(user_id, machine_id, desired)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
other_error(err.to_string()).into(),
|
||||
)
|
||||
})?;
|
||||
Ok(Void::default().into())
|
||||
}
|
||||
|
||||
async fn handle_list_network_instance_ids_internal(
|
||||
State(client_mgr): AppState,
|
||||
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
|
||||
@@ -396,7 +347,6 @@ impl NetworkApi {
|
||||
.route(
|
||||
"/api/internal/users/:user-id/machines/:machine-id/networks",
|
||||
post(Self::handle_run_network_instance_internal)
|
||||
.put(Self::handle_reconcile_managed_network_configs_internal)
|
||||
.get(Self::handle_list_network_instance_ids_internal),
|
||||
)
|
||||
.route(
|
||||
|
||||
@@ -16,7 +16,6 @@ pub struct ProxyRpcRequest {
|
||||
pub service_name: String,
|
||||
pub method_name: String,
|
||||
pub payload: serde_json::Value,
|
||||
pub scope: Option<String>,
|
||||
}
|
||||
|
||||
macro_rules! match_service {
|
||||
@@ -36,7 +35,6 @@ async fn handle_proxy_rpc_by_session(
|
||||
service_name,
|
||||
method_name,
|
||||
payload,
|
||||
scope,
|
||||
} = req;
|
||||
|
||||
let resp = match service_name.as_str() {
|
||||
@@ -76,20 +74,12 @@ async fn handle_proxy_rpc_by_session(
|
||||
payload,
|
||||
session
|
||||
),
|
||||
"api.instance.TcpProxyRpcService" => {
|
||||
let client = if let Some(ref domain) = scope {
|
||||
session.scoped_client_with_domain::<
|
||||
easytier::proto::api::instance::TcpProxyRpcClientFactory<BaseController>,
|
||||
>(domain.clone())
|
||||
} else {
|
||||
session.scoped_client::<
|
||||
easytier::proto::api::instance::TcpProxyRpcClientFactory<BaseController>,
|
||||
>()
|
||||
};
|
||||
client
|
||||
.json_call_method(BaseController::default(), &method_name, payload)
|
||||
.await
|
||||
}
|
||||
"api.instance.TcpProxyRpcService" => match_service!(
|
||||
easytier::proto::api::instance::TcpProxyRpcClientFactory<BaseController>,
|
||||
method_name,
|
||||
payload,
|
||||
session
|
||||
),
|
||||
"api.instance.AclManageRpcService" => match_service!(
|
||||
easytier::proto::api::instance::AclManageRpcClientFactory<BaseController>,
|
||||
method_name,
|
||||
|
||||
@@ -57,8 +57,6 @@ pub struct ValidateTokenRequest {
|
||||
pub os_distribution: Option<String>,
|
||||
pub web_instance_id: Option<String>,
|
||||
pub web_instance_api_base_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub applied_config_revision: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -68,8 +66,7 @@ pub struct ValidateTokenResponse {
|
||||
pub pre_approved: bool,
|
||||
#[serde(default)]
|
||||
pub binding_version: u64,
|
||||
#[serde(default)]
|
||||
pub managed_network_configs: Option<Vec<ManagedNetworkConfig>>,
|
||||
pub managed_network_configs: Vec<ManagedNetworkConfig>,
|
||||
pub config_revision: String,
|
||||
}
|
||||
|
||||
@@ -187,17 +184,3 @@ impl WebhookConfig {
|
||||
}
|
||||
|
||||
pub type SharedWebhookConfig = Arc<WebhookConfig>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validate_token_response_allows_missing_managed_configs() {
|
||||
let resp: ValidateTokenResponse =
|
||||
serde_json::from_str(r#"{"valid":true,"config_revision":"rev-1"}"#).unwrap();
|
||||
assert!(resp.valid);
|
||||
assert_eq!(resp.config_revision, "rev-1");
|
||||
assert!(resp.managed_network_configs.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
+14
-29
@@ -51,9 +51,7 @@ time = "0.3"
|
||||
toml = "0.8.12"
|
||||
chrono = { version = "0.4.37", features = ["serde"] }
|
||||
|
||||
guarden = "0.2"
|
||||
hotpath = { version = "0.18", default-features = false, optional = true }
|
||||
quanta = "0.12"
|
||||
guarden = "0.1"
|
||||
|
||||
delegate = "0.13.5"
|
||||
|
||||
@@ -84,9 +82,7 @@ pin-project-lite = "0.2.13"
|
||||
atomic_refcell = "0.1.13"
|
||||
|
||||
quinn = { version = "0.11.8", optional = true, features = ["ring"] }
|
||||
quinn-proto = { version = "0.11.12", optional = true }
|
||||
quinn-udp = { version = "0.5", optional = true }
|
||||
seahash = { version = "4.1.0", optional = true }
|
||||
quinn-plaintext = { version = "0.3.0", optional = true }
|
||||
|
||||
rustls = { version = "0.23.0", features = [
|
||||
"ring", "tls12"
|
||||
@@ -94,7 +90,7 @@ rustls = { version = "0.23.0", features = [
|
||||
rcgen = { version = "0.12.1", optional = true }
|
||||
|
||||
# for websocket
|
||||
tokio-websockets = { version = "0.13.2", git = "https://github.com/EasyTier/tokio-websockets", optional = true, features = [
|
||||
tokio-websockets = { version = "0.13.2", optional = true, features = [
|
||||
"rustls-webpki-roots",
|
||||
"client",
|
||||
"server",
|
||||
@@ -131,13 +127,10 @@ uuid = { version = "1.5.0", features = [
|
||||
once_cell = "1.18.0"
|
||||
|
||||
# for rpc
|
||||
prost = "0.14.3"
|
||||
prost-reflect = { version = "0.16.4", default-features = false, features = ["derive", "serde"] }
|
||||
prost-wkt-types = "0.7.1"
|
||||
pbjson = "0.9.0"
|
||||
|
||||
prost = "0.13.5"
|
||||
prost-wkt = "0.6"
|
||||
prost-wkt-types = "0.6"
|
||||
anyhow = "1.0"
|
||||
ariadne = "0.5"
|
||||
|
||||
url = { version = "2.5", features = ["serde"] }
|
||||
percent-encoding = "2.3.1"
|
||||
@@ -233,6 +226,10 @@ zstd = { version = "0.13", optional = true }
|
||||
|
||||
kcp-sys = { git = "https://github.com/EasyTier/kcp-sys", rev = "d7427c22d764deb1860a7d37acc446ed5033464c", optional = true }
|
||||
|
||||
prost-reflect = { version = "0.14.5", default-features = false, features = [
|
||||
"derive",
|
||||
] }
|
||||
|
||||
# for http connector
|
||||
http_req = { git = "https://github.com/EasyTier/http_req.git", default-features = false, features = [
|
||||
"rust-tls",
|
||||
@@ -323,9 +320,9 @@ cfg_aliases = "0.2.1"
|
||||
indoc = "2.0"
|
||||
globwalk = "0.8.1"
|
||||
regex = "1"
|
||||
prost-build = "0.14.3"
|
||||
prost-reflect-build = "0.16.0"
|
||||
pbjson-build = "0.9.0"
|
||||
prost-build = "0.13.5"
|
||||
prost-wkt-build = "0.6"
|
||||
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 = [
|
||||
@@ -362,7 +359,6 @@ default = [
|
||||
"faketcp",
|
||||
"magic-dns",
|
||||
"zstd",
|
||||
"udp-gso",
|
||||
]
|
||||
full = [
|
||||
"websocket",
|
||||
@@ -379,8 +375,7 @@ full = [
|
||||
"zstd",
|
||||
]
|
||||
wireguard = ["dep:boringtun", "dep:ring"]
|
||||
quic = ["dep:quinn", "dep:quinn-proto", "dep:seahash", "dep:rustls", "dep:rcgen"]
|
||||
udp-gso = ["dep:quinn-udp"]
|
||||
quic = ["dep:quinn", "dep:quinn-plaintext", "dep:rustls", "dep:rcgen"]
|
||||
kcp = ["dep:kcp-sys"]
|
||||
mimalloc = ["dep:mimalloc"]
|
||||
aes-gcm = ["dep:aes-gcm"]
|
||||
@@ -396,7 +391,6 @@ websocket = [
|
||||
]
|
||||
smoltcp = ["dep:smoltcp"]
|
||||
socks5 = ["smoltcp"]
|
||||
ffi-dataplane = ["socks5"]
|
||||
jemalloc = ["dep:jemallocator", "dep:jemalloc-sys"]
|
||||
jemalloc-prof = [
|
||||
"jemalloc",
|
||||
@@ -406,15 +400,6 @@ jemalloc-prof = [
|
||||
"jemalloc-sys/stats",
|
||||
]
|
||||
tracing = ["tokio/tracing", "dep:console-subscriber"]
|
||||
hotpath = [
|
||||
"dep:hotpath",
|
||||
"hotpath/hotpath",
|
||||
"hotpath/tokio",
|
||||
"hotpath/parking_lot",
|
||||
"hotpath/flume",
|
||||
]
|
||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
|
||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
|
||||
magic-dns = ["dep:hickory-client", "dep:hickory-server"]
|
||||
faketcp = ["dep:flume"]
|
||||
zstd = ["dep:zstd"]
|
||||
|
||||
+24
-9
@@ -2,6 +2,7 @@ mod rpc;
|
||||
|
||||
use crate::rpc::ServiceGenerator;
|
||||
use cfg_aliases::cfg_aliases;
|
||||
use prost_wkt_build::{FileDescriptorSet, Message as _};
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::io::Cursor;
|
||||
use std::{env, path::PathBuf};
|
||||
@@ -173,15 +174,32 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("cargo:rerun-if-changed={proto_file}");
|
||||
}
|
||||
|
||||
let out = PathBuf::from(env::var("OUT_DIR")?);
|
||||
let descriptor = out.join("descriptors.bin");
|
||||
let out = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let descriptor_file = out.join("descriptors.bin");
|
||||
|
||||
let mut config = prost_build::Config::new();
|
||||
config
|
||||
.type_attribute(".", "#[derive(serde::Serialize,serde::Deserialize)]")
|
||||
.extern_path(".google.protobuf.Any", "::prost_wkt_types::Any")
|
||||
.extern_path(".google.protobuf.Timestamp", "::prost_wkt_types::Timestamp")
|
||||
.extern_path(".google.protobuf.Value", "::prost_wkt_types::Value")
|
||||
.file_descriptor_set_path(&descriptor)
|
||||
.file_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)]")
|
||||
.type_attribute(
|
||||
"peer_rpc.RouteForeignNetworkSummary.Info",
|
||||
"#[derive(Hash, Eq)]",
|
||||
)
|
||||
.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)]")
|
||||
.field_attribute(".api.manage.NetworkConfig", "#[serde(default)]")
|
||||
.service_generator(Box::new(ServiceGenerator::default()))
|
||||
.btree_map(["."])
|
||||
.skip_debug([".common.Ipv4Addr", ".common.Ipv6Addr", ".common.UUID"]);
|
||||
@@ -192,12 +210,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.file_descriptor_set_bytes("crate::proto::DESCRIPTOR_POOL_BYTES")
|
||||
.compile_protos_with_config(config, &proto_files_reflect, &["src/proto/"])?;
|
||||
|
||||
let descriptor = std::fs::read(descriptor)?;
|
||||
pbjson_build::Builder::new()
|
||||
.register_descriptors(&descriptor)?
|
||||
.preserve_proto_field_names()
|
||||
.btree_map(["."])
|
||||
.build(&["."])?;
|
||||
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();
|
||||
Ok(())
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
//! CPU hotspot benchmark for hotpath profiling.
|
||||
//!
|
||||
//! Builds two no-tun EasyTier instances connected via an in-process ring
|
||||
//! tunnel, lets routes converge, then floods data-plane packets through
|
||||
//! `send_msg_by_ip` so that `hotpath-cpu` / samply can collect meaningful
|
||||
//! CPU samples.
|
||||
//!
|
||||
//! Build & run:
|
||||
//! cargo run --profile hotpath --features hotpath,hotpath-cpu \
|
||||
//! --example cpu_hotspot_ring
|
||||
//!
|
||||
//! Prerequisites: hotpath-samply + samply must be installed and on PATH.
|
||||
//! See bench/006-hotpath-cpu-top.md for install instructions.
|
||||
//!
|
||||
//! Then in another terminal:
|
||||
//! hotpath console
|
||||
|
||||
#[cfg(feature = "mimalloc")]
|
||||
#[global_allocator]
|
||||
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
#[cfg(feature = "jemalloc")]
|
||||
#[global_allocator]
|
||||
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
|
||||
|
||||
use std::net::IpAddr;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bytes::BytesMut;
|
||||
|
||||
use easytier::common::config::{ConfigLoader, PeerConfig, TomlConfigLoader};
|
||||
use easytier::instance::instance::Instance;
|
||||
use easytier::tunnel::packet_def::ZCPacket;
|
||||
use easytier::tunnel::ring::RingTunnelConnector;
|
||||
use easytier::tunnel::udp::UdpTunnelConnector;
|
||||
|
||||
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
|
||||
#[cfg_attr(feature = "hotpath", hotpath::main)]
|
||||
async fn main() {
|
||||
let duration = std::env::var("HOTPATH_BENCH_SECS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(30u64);
|
||||
|
||||
let pkt_size: usize = std::env::var("HOTPATH_PKT_SIZE")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(1400);
|
||||
|
||||
let tunnel_type = std::env::var("HOTPATH_TUNNEL")
|
||||
.ok()
|
||||
.unwrap_or_else(|| "ring".to_string());
|
||||
|
||||
let (inst_a_config, inst_b_config) = match tunnel_type.as_str() {
|
||||
"udp" => {
|
||||
let mut a = no_tun_config("hot-a", "10.144.144.1");
|
||||
a.set_listeners(vec!["udp://0.0.0.0:35521".parse().unwrap()]);
|
||||
(a, no_tun_config("hot-b", "10.144.144.2"))
|
||||
}
|
||||
"tcp" => {
|
||||
let mut a = no_tun_config("hot-a", "10.144.144.1");
|
||||
a.set_listeners(vec!["tcp://0.0.0.0:35522".parse().unwrap()]);
|
||||
(a, no_tun_config("hot-b", "10.144.144.2"))
|
||||
}
|
||||
_ => (
|
||||
no_tun_config("hot-a", "10.144.144.1"),
|
||||
no_tun_config("hot-b", "10.144.144.2"),
|
||||
),
|
||||
};
|
||||
|
||||
let mut inst_a = Instance::new(inst_a_config);
|
||||
let mut inst_b = Instance::new(inst_b_config);
|
||||
|
||||
inst_a.run().await.expect("inst_a run");
|
||||
inst_b.run().await.expect("inst_b run");
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
match tunnel_type.as_str() {
|
||||
"ring" => {
|
||||
let ring_url = format!("ring://{}", inst_a.id());
|
||||
inst_b
|
||||
.get_conn_manager()
|
||||
.add_connector(RingTunnelConnector::new(ring_url.parse().unwrap()));
|
||||
}
|
||||
"udp" => {
|
||||
inst_b.get_conn_manager().add_connector(
|
||||
UdpTunnelConnector::new("udp://127.0.0.1:35521".parse().unwrap()),
|
||||
);
|
||||
}
|
||||
"tcp" => {
|
||||
inst_b.get_conn_manager().add_connector(
|
||||
easytier::tunnel::tcp::TcpTunnelConnector::new(
|
||||
"tcp://127.0.0.1:35522".parse().unwrap(),
|
||||
),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let dst: IpAddr = "10.144.144.2".parse().unwrap();
|
||||
let src = "10.144.144.1";
|
||||
|
||||
let converged = tokio::time::timeout(Duration::from_secs(15), async {
|
||||
loop {
|
||||
let a = inst_a.get_peer_manager().list_routes().await;
|
||||
let b = inst_b.get_peer_manager().list_routes().await;
|
||||
if a.len() >= 1 && b.len() >= 1 {
|
||||
return true;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.is_ok();
|
||||
|
||||
if !converged {
|
||||
eprintln!("warning: routes did not converge within 15s");
|
||||
}
|
||||
|
||||
println!(
|
||||
"cpu_hotspot_ring: flooding {}s, pkt_size={}, tunnel={} (converged={})",
|
||||
duration, pkt_size, tunnel_type, converged
|
||||
);
|
||||
|
||||
let pm = inst_a.get_peer_manager();
|
||||
let send_pkt = make_data_packet(src, "10.144.144.2", pkt_size);
|
||||
|
||||
let batch_threshold: u32 = std::env::var("HOTPATH_BATCH")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(1);
|
||||
|
||||
// After convergence, enable batch flush for writev optimization
|
||||
if converged && batch_threshold > 1 {
|
||||
pm.set_peer_conn_batch_threshold(batch_threshold);
|
||||
println!("cpu_hotspot_ring: batch_threshold={}", batch_threshold);
|
||||
}
|
||||
|
||||
let pipeline_depth: usize = std::env::var("HOTPATH_PIPELINE")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(1);
|
||||
|
||||
println!(
|
||||
"cpu_hotspot_ring: pipeline_depth={}",
|
||||
pipeline_depth
|
||||
);
|
||||
|
||||
let sender_task = tokio::spawn(async move {
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
|
||||
let mut sent: u64 = 0;
|
||||
let start = Instant::now();
|
||||
let mut in_flight = FuturesUnordered::new();
|
||||
|
||||
loop {
|
||||
while in_flight.len() < pipeline_depth {
|
||||
let pkt = send_pkt.clone();
|
||||
in_flight.push(pm.send_msg_by_ip(pkt, dst, false));
|
||||
}
|
||||
in_flight.next().await;
|
||||
sent += 1;
|
||||
if sent % 10000 == 0 {
|
||||
let elapsed = start.elapsed().as_secs_f64();
|
||||
let pps = sent as f64 / elapsed;
|
||||
let mbps = pps * pkt_size as f64 * 8.0 / 1_000_000.0;
|
||||
println!("sent {} pkts ({:.0} pps, {:.0} Mbps)", sent, pps, mbps);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(duration)).await;
|
||||
sender_task.abort();
|
||||
|
||||
println!("cpu_hotspot_ring: done");
|
||||
}
|
||||
|
||||
fn make_data_packet(src: &str, dst: &str, total_size: usize) -> ZCPacket {
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
let hdr_len = 28;
|
||||
let payload_len = total_size.saturating_sub(hdr_len);
|
||||
let ip_total_len = (hdr_len + payload_len) as u16;
|
||||
|
||||
let mut buf = BytesMut::with_capacity(total_size);
|
||||
|
||||
buf.extend_from_slice(&[
|
||||
0x45,
|
||||
0x00,
|
||||
(ip_total_len >> 8) as u8,
|
||||
(ip_total_len & 0xff) as u8,
|
||||
0x00,
|
||||
0x00,
|
||||
0x40,
|
||||
0x00,
|
||||
0x40,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
]);
|
||||
let src: Ipv4Addr = src.parse().unwrap();
|
||||
buf.extend_from_slice(&src.octets());
|
||||
let dst: Ipv4Addr = dst.parse().unwrap();
|
||||
buf.extend_from_slice(&dst.octets());
|
||||
|
||||
let udp_len = (8 + payload_len) as u16;
|
||||
buf.extend_from_slice(&[
|
||||
0x30,
|
||||
0x39,
|
||||
0xD4,
|
||||
0x31,
|
||||
(udp_len >> 8) as u8,
|
||||
(udp_len & 0xff) as u8,
|
||||
0x00,
|
||||
0x00,
|
||||
]);
|
||||
|
||||
buf.resize(total_size, 0xAA);
|
||||
|
||||
ZCPacket::new_with_payload(&buf)
|
||||
}
|
||||
|
||||
fn no_tun_config(name: &str, ipv4: &str) -> TomlConfigLoader {
|
||||
let config = TomlConfigLoader::default();
|
||||
config.set_inst_name(name.to_owned());
|
||||
config.set_ipv4(Some(ipv4.parse().unwrap()));
|
||||
let mut flags = config.get_flags();
|
||||
flags.no_tun = true;
|
||||
config.set_flags(flags);
|
||||
config
|
||||
}
|
||||
@@ -205,9 +205,6 @@ core_clap:
|
||||
bind_device:
|
||||
en: "bind the connector socket to physical devices to avoid routing issues. e.g.: subnet proxy segment conflicts with a node's segment, after binding the physical device, it can communicate with the node normally."
|
||||
zh-CN: "将连接器的套接字绑定到物理设备以避免路由问题。比如子网代理网段与某节点的网段冲突,绑定物理设备后可以与该节点正常通信。"
|
||||
socket_mark:
|
||||
en: "Linux only: set SO_MARK (fwmark) on EasyTier's underlay sockets (TCP, UDP, QUIC, WebSocket, WireGuard, and the FakeTCP decoy socket) so the host can policy-route or filter them with 'ip rule fwmark ...', nftables ('meta mark'), or iptables ('-m mark'). Any value is applied verbatim (0 is a valid mark); omit the flag to leave SO_MARK untouched. Requires CAP_NET_ADMIN. Note: FakeTCP payload travels via raw TUN writes which the kernel does not tag — mark those separately on the TUN device if needed."
|
||||
zh-CN: "仅 Linux: 在 EasyTier 的底层套接字 (TCP、UDP、QUIC、WebSocket、WireGuard 以及 FakeTCP 诱饵套接字) 上设置 SO_MARK (fwmark),使主机能用 'ip rule fwmark ...'、nftables ('meta mark') 或 iptables ('-m mark') 策略路由/过滤这些数据包。任何值都会原样应用 (0 也是合法的 mark);不传该参数即保持 SO_MARK 不变。需要 CAP_NET_ADMIN 权限。注意:FakeTCP 的实际载荷通过原始 TUN 写入,内核不会为其打标记;如有需要请在 TUN 设备上单独打标记。"
|
||||
enable_kcp_proxy:
|
||||
en: "proxy tcp streams with kcp, improving the latency and throughput on the network with udp packet loss."
|
||||
zh-CN: "使用 KCP 代理 TCP 流,提高在 UDP 丢包网络上的延迟和吞吐量。"
|
||||
|
||||
@@ -3,11 +3,9 @@ use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
str::FromStr as _,
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use hotpath::instant::Instant;
|
||||
|
||||
use crate::common::{config::ConfigLoader, global_ctx::ArcGlobalCtx, token_bucket::TokenBucket};
|
||||
use crate::proto::acl::*;
|
||||
use anyhow::Context as _;
|
||||
@@ -109,10 +107,10 @@ impl AclCacheKey {
|
||||
|
||||
// Cache entry with timestamp for LRU cleanup
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AclCacheEntry {
|
||||
pub struct AclCacheEntry {
|
||||
pub action: Action,
|
||||
pub matched_rule: RuleId,
|
||||
pub last_access: Instant,
|
||||
pub last_access: std::time::Instant,
|
||||
// New fields to track rule characteristics for proper cache behavior
|
||||
pub conn_track_key: Option<String>,
|
||||
pub rate_limit_keys: Vec<RateLimitKey>,
|
||||
@@ -412,7 +410,7 @@ impl AclProcessor {
|
||||
}
|
||||
|
||||
// Remove oldest entries (LRU cleanup)
|
||||
let mut entries: Vec<(AclCacheKey, Instant)> = cache
|
||||
let mut entries: Vec<(AclCacheKey, std::time::Instant)> = cache
|
||||
.iter()
|
||||
.map(|entry| (entry.key().clone(), entry.value().last_access))
|
||||
.collect();
|
||||
@@ -433,7 +431,7 @@ impl AclProcessor {
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn process_packet_with_cache_entry(
|
||||
pub fn process_packet_with_cache_entry(
|
||||
&self,
|
||||
packet_info: &PacketInfo,
|
||||
cache_entry: &AclCacheEntry,
|
||||
|
||||
@@ -129,7 +129,6 @@ impl Compressor for DefaultCompressor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure(impl_type = "DefaultCompressor"))]
|
||||
async fn decompress(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> {
|
||||
let pm_header = zc_packet.peer_manager_header().unwrap();
|
||||
if !pm_header.is_compressed() {
|
||||
|
||||
+63
-376
@@ -6,11 +6,9 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use ariadne::{CharSet, Config as AriadneConfig, IndexType, Label, Report, ReportKind, Source};
|
||||
use base64::{Engine as _, prelude::BASE64_STANDARD};
|
||||
use clap::ValueEnum;
|
||||
use clap::builder::PossibleValue;
|
||||
use prost_reflect::{DynamicMessage, ReflectMessage, SerializeOptions};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumString, VariantArray};
|
||||
use tokio::io::AsyncReadExt as _;
|
||||
@@ -75,58 +73,9 @@ pub fn gen_default_flags() -> Flags {
|
||||
disable_upnp: false,
|
||||
disable_relay_data: false,
|
||||
enable_udp_broadcast_relay: false,
|
||||
socket_mark: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn flags_to_dynamic_message(flags: &Flags) -> DynamicMessage {
|
||||
let mut message = DynamicMessage::new(flags.descriptor());
|
||||
message
|
||||
.transcode_from(flags)
|
||||
.expect("FlagsInConfig should transcode to DynamicMessage");
|
||||
message
|
||||
}
|
||||
|
||||
fn flags_to_full_json_map(flags: &DynamicMessage) -> serde_json::Map<String, serde_json::Value> {
|
||||
let options = SerializeOptions::new()
|
||||
.use_proto_field_name(true)
|
||||
.skip_default_fields(false);
|
||||
|
||||
match flags
|
||||
.serialize_with_options(serde_json::value::Serializer, &options)
|
||||
.expect("FlagsInConfig should serialize to JSON")
|
||||
{
|
||||
serde_json::Value::Object(map) => map,
|
||||
_ => unreachable!("FlagsInConfig should serialize to a JSON object"),
|
||||
}
|
||||
}
|
||||
|
||||
fn flags_diff_from_default(flags: &Flags) -> serde_json::Map<String, serde_json::Value> {
|
||||
let default_flags = gen_default_flags();
|
||||
let default_message = flags_to_dynamic_message(&default_flags);
|
||||
let current_message = flags_to_dynamic_message(flags);
|
||||
let default_map = flags_to_full_json_map(&default_message);
|
||||
let current_map = flags_to_full_json_map(¤t_message);
|
||||
|
||||
current_message
|
||||
.descriptor()
|
||||
.fields()
|
||||
.filter_map(|field| {
|
||||
let key = field.name();
|
||||
let value_changed = default_map.get(key) != current_map.get(key);
|
||||
let presence_changed =
|
||||
default_message.has_field(&field) != current_message.has_field(&field);
|
||||
if value_changed || presence_changed {
|
||||
current_map
|
||||
.get(key)
|
||||
.map(|value| (key.to_string(), value.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn mapped_listener_allows_implicit_port(url: &url::Url) -> bool {
|
||||
TunnelScheme::try_from(url)
|
||||
.ok()
|
||||
@@ -328,20 +277,20 @@ pub struct NetworkIdentity {
|
||||
pub enum ConfigSource {
|
||||
#[default]
|
||||
User,
|
||||
Web,
|
||||
Webhook,
|
||||
}
|
||||
|
||||
impl ConfigSource {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::User => "user",
|
||||
Self::Web => "web",
|
||||
Self::Webhook => "webhook",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_rpc(source: i32) -> Option<Self> {
|
||||
match RpcConfigSource::try_from(source).ok() {
|
||||
Some(RpcConfigSource::Web) => Some(Self::Web),
|
||||
Some(RpcConfigSource::Webhook) => Some(Self::Webhook),
|
||||
Some(RpcConfigSource::User) => Some(Self::User),
|
||||
_ => None,
|
||||
}
|
||||
@@ -350,7 +299,7 @@ impl ConfigSource {
|
||||
pub fn to_rpc(self) -> i32 {
|
||||
match self {
|
||||
Self::User => RpcConfigSource::User as i32,
|
||||
Self::Web => RpcConfigSource::Web as i32,
|
||||
Self::Webhook => RpcConfigSource::Webhook as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,7 +310,7 @@ impl std::str::FromStr for ConfigSource {
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"user" => Ok(Self::User),
|
||||
"web" => Ok(Self::Web),
|
||||
"webhook" => Ok(Self::Webhook),
|
||||
other => Err(format!("unknown network config source: {other}")),
|
||||
}
|
||||
}
|
||||
@@ -619,35 +568,6 @@ struct Config {
|
||||
source: Option<ConfigSourceConfig>,
|
||||
}
|
||||
|
||||
fn format_toml_parse_error(source_name: &str, config_str: &str, error: &toml::de::Error) -> String {
|
||||
let message = format!("failed to parse config TOML from {source_name}");
|
||||
|
||||
let Some(span) = error.span() else {
|
||||
return format!("{message}\ndetail: {error}");
|
||||
};
|
||||
|
||||
let mut output = Vec::new();
|
||||
let report = Report::build(ReportKind::Error, (source_name, span.clone()))
|
||||
.with_config(
|
||||
AriadneConfig::default()
|
||||
.with_color(false)
|
||||
.with_char_set(CharSet::Ascii)
|
||||
.with_index_type(IndexType::Byte),
|
||||
)
|
||||
.with_message(&message)
|
||||
.with_label(Label::new((source_name, span)).with_message(error.message()))
|
||||
.finish();
|
||||
|
||||
if report
|
||||
.write((source_name, Source::from(config_str)), &mut output)
|
||||
.is_ok()
|
||||
{
|
||||
String::from_utf8_lossy(&output).into_owned()
|
||||
} else {
|
||||
format!("{message}\ndetail: {error}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TomlConfigLoader {
|
||||
config: Arc<Mutex<Config>>,
|
||||
@@ -670,79 +590,50 @@ impl TomlConfigLoader {
|
||||
}
|
||||
|
||||
pub fn new_from_str(config_str: &str) -> Result<Self, anyhow::Error> {
|
||||
Self::new_from_str_with_source("inline config", config_str)
|
||||
}
|
||||
|
||||
pub fn new(config_path: &PathBuf) -> Result<Self, anyhow::Error> {
|
||||
let config_str = std::fs::read_to_string(config_path)
|
||||
.with_context(|| format!("failed to read config file: {}", config_path.display()))?;
|
||||
|
||||
let source_name = config_path.display().to_string();
|
||||
Self::new_from_str_with_source(&source_name, &config_str)
|
||||
}
|
||||
|
||||
pub(crate) fn new_from_str_with_source(
|
||||
source_name: &str,
|
||||
config_str: &str,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let mut config = toml::de::from_str::<Config>(config_str).map_err(|err| {
|
||||
let message = format_toml_parse_error(source_name, config_str, &err);
|
||||
anyhow::Error::new(err).context(message)
|
||||
})?;
|
||||
let mut config = toml::de::from_str::<Config>(config_str)
|
||||
.with_context(|| format!("failed to parse config file: {}", config_str))?;
|
||||
|
||||
Self::normalize_config_source(&mut config);
|
||||
|
||||
Self::new_from_config(config).map_err(|err| {
|
||||
let message = format!("failed to load config from {source_name}: {err}");
|
||||
err.context(message)
|
||||
})
|
||||
}
|
||||
|
||||
fn new_from_config(mut config: Config) -> Result<Self, anyhow::Error> {
|
||||
config.flags_struct = Some(
|
||||
Self::gen_flags(config.flags.clone().unwrap_or_default())
|
||||
.context("failed to parse flags")?,
|
||||
);
|
||||
let has_network_identity = config.network_identity.is_some();
|
||||
config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default()));
|
||||
|
||||
let config = TomlConfigLoader {
|
||||
config: Arc::new(Mutex::new(config)),
|
||||
};
|
||||
|
||||
let old_ns = config.get_network_identity();
|
||||
|
||||
// Detect credential mode: secure_mode enabled + no network_secret in TOML
|
||||
let is_credential = has_network_identity
|
||||
&& config
|
||||
.get_secure_mode()
|
||||
.map(|sm| sm.enabled)
|
||||
.unwrap_or(false)
|
||||
&& old_ns
|
||||
.network_secret
|
||||
.as_deref()
|
||||
.is_none_or(|s| s.is_empty());
|
||||
|
||||
if is_credential {
|
||||
config.set_network_identity(NetworkIdentity::new_credential(old_ns.network_name));
|
||||
} else {
|
||||
config.set_network_identity(NetworkIdentity::new(
|
||||
old_ns.network_name,
|
||||
old_ns.network_secret.unwrap_or_default(),
|
||||
));
|
||||
}
|
||||
config.set_network_identity(NetworkIdentity::new(
|
||||
old_ns.network_name,
|
||||
old_ns.network_secret.unwrap_or_default(),
|
||||
));
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn gen_flags(
|
||||
flags_hashmap: serde_json::Map<String, serde_json::Value>,
|
||||
) -> serde_json::Result<Flags> {
|
||||
let mut merged_hashmap = match serde_json::to_value(gen_default_flags()) {
|
||||
Ok(serde_json::Value::Object(map)) => map,
|
||||
_ => serde_json::Map::new(),
|
||||
};
|
||||
merged_hashmap.extend(flags_hashmap);
|
||||
serde_json::from_value(serde_json::Value::Object(merged_hashmap))
|
||||
pub fn new(config_path: &PathBuf) -> Result<Self, anyhow::Error> {
|
||||
let config_str = std::fs::read_to_string(config_path)
|
||||
.with_context(|| format!("failed to read config file: {:?}", config_path))?;
|
||||
let ret = Self::new_from_str(&config_str)?;
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn gen_flags(mut flags_hashmap: serde_json::Map<String, serde_json::Value>) -> Flags {
|
||||
let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap();
|
||||
let default_flags_hashmap =
|
||||
serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&default_flags_json)
|
||||
.unwrap();
|
||||
|
||||
let mut merged_hashmap = serde_json::Map::new();
|
||||
for (key, value) in default_flags_hashmap {
|
||||
if let Some(v) = flags_hashmap.remove(&key) {
|
||||
merged_hashmap.insert(key, v);
|
||||
} else {
|
||||
merged_hashmap.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::from_value(serde_json::Value::Object(merged_hashmap)).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1142,9 +1033,28 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
}
|
||||
|
||||
fn dump(&self) -> String {
|
||||
let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap();
|
||||
let default_flags_hashmap =
|
||||
serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&default_flags_json)
|
||||
.unwrap();
|
||||
|
||||
let cur_flags_json = serde_json::to_string(&self.get_flags()).unwrap();
|
||||
let cur_flags_hashmap =
|
||||
serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&cur_flags_json)
|
||||
.unwrap();
|
||||
|
||||
let mut flag_map: serde_json::Map<String, serde_json::Value> = Default::default();
|
||||
for (key, value) in default_flags_hashmap {
|
||||
if let Some(v) = cur_flags_hashmap.get(&key)
|
||||
&& *v != value
|
||||
{
|
||||
flag_map.insert(key, v.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut config = self.config.lock().unwrap().clone();
|
||||
Self::normalize_config_source(&mut config);
|
||||
config.flags = Some(flags_diff_from_default(&self.get_flags()));
|
||||
config.flags = Some(flag_map);
|
||||
if config.stun_servers == Some(StunInfoCollector::get_default_servers()) {
|
||||
config.stun_servers = None;
|
||||
}
|
||||
@@ -1280,13 +1190,13 @@ pub async fn load_config_from_file(
|
||||
.read_to_string(&mut stdin)
|
||||
.await
|
||||
.context("failed to read config from stdin")?;
|
||||
let config = TomlConfigLoader::new_from_str_with_source("stdin", &stdin)?;
|
||||
let config = TomlConfigLoader::new_from_str(&stdin)?;
|
||||
return Ok((config, ConfigFileControl::STATIC_CONFIG));
|
||||
}
|
||||
|
||||
let config_str = tokio::fs::read_to_string(config_file)
|
||||
.await
|
||||
.with_context(|| format!("failed to read config file: {}", config_file.display()))?;
|
||||
.with_context(|| format!("failed to read config file: {:?}", config_file))?;
|
||||
|
||||
let (expanded_config_str, uses_env_vars) = if disable_env_parsing {
|
||||
(config_str.clone(), false)
|
||||
@@ -1308,8 +1218,8 @@ pub async fn load_config_from_file(
|
||||
);
|
||||
}
|
||||
|
||||
let source_name = config_file.display().to_string();
|
||||
let config = TomlConfigLoader::new_from_str_with_source(&source_name, &expanded_config_str)?;
|
||||
let config = TomlConfigLoader::new_from_str(&expanded_config_str)
|
||||
.with_context(|| format!("failed to load config file: {:?}", config_file))?;
|
||||
|
||||
let mut control = ConfigFileControl::from_path(config_file.clone()).await;
|
||||
|
||||
@@ -1349,190 +1259,6 @@ pub mod tests {
|
||||
use std::path::PathBuf;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn invalid_toml_error_includes_location_and_source_line() {
|
||||
let error = TomlConfigLoader::new_from_str("dhcp = \"yes\"").unwrap_err();
|
||||
let display = error.to_string();
|
||||
|
||||
assert!(display.contains("failed to parse config TOML"));
|
||||
assert!(display.contains("inline config"));
|
||||
assert!(display.contains("dhcp = \"yes\""));
|
||||
assert!(display.contains("^"));
|
||||
assert!(display.contains("invalid type: string"));
|
||||
assert!(!display.contains("<unknown>"));
|
||||
assert!(
|
||||
error
|
||||
.chain()
|
||||
.any(|err| err.downcast_ref::<toml::de::Error>().is_some())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_file_toml_error_includes_config_source() {
|
||||
let mut config_file = NamedTempFile::new().unwrap();
|
||||
writeln!(config_file, "dhcp = \"yes\"").unwrap();
|
||||
|
||||
let error = TomlConfigLoader::new(&config_file.path().to_path_buf()).unwrap_err();
|
||||
let error = error.to_string();
|
||||
|
||||
assert!(error.contains(config_file.path().to_string_lossy().as_ref()));
|
||||
assert!(error.contains("failed to parse config TOML"));
|
||||
assert!(error.contains("dhcp = \"yes\""));
|
||||
assert!(error.contains("^"));
|
||||
assert!(error.contains("invalid type: string"));
|
||||
assert!(!error.contains("<unknown>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_stdin_toml_error_includes_config_source_in_display() {
|
||||
let error = TomlConfigLoader::new_from_str_with_source("stdin", "dhcp = \"yes\"")
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
|
||||
assert!(error.contains("stdin"));
|
||||
assert!(error.contains("failed to parse config TOML"));
|
||||
assert!(error.contains("dhcp = \"yes\""));
|
||||
assert!(error.contains("^"));
|
||||
assert!(error.contains("invalid type: string"));
|
||||
assert!(!error.contains("<unknown>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_toml_error_handles_non_ascii_before_error() {
|
||||
let error = TomlConfigLoader::new_from_str("hostname = \"节点\"\ndhcp = \"yes\"")
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
|
||||
assert!(error.contains("dhcp = \"yes\""));
|
||||
assert!(error.contains("^"));
|
||||
assert!(error.contains("invalid type: string"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_toml_error_handles_non_ascii_before_error_on_same_line() {
|
||||
let error = TomlConfigLoader::new_from_str("hostname = \"节点\" dhcp = \"yes\"")
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
|
||||
assert!(error.contains("failed to parse config TOML"));
|
||||
assert!(error.contains("inline config:1:"));
|
||||
assert!(error.contains("hostname = \"节点\" dhcp = \"yes\""));
|
||||
assert!(error.contains("expected newline"));
|
||||
assert!(!error.contains("<unknown>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_file_flags_error_includes_config_source_in_display() {
|
||||
let mut config_file = NamedTempFile::new().unwrap();
|
||||
writeln!(config_file, "[flags]").unwrap();
|
||||
writeln!(config_file, "socket_mark = \"bad\"").unwrap();
|
||||
|
||||
let error = TomlConfigLoader::new(&config_file.path().to_path_buf()).unwrap_err();
|
||||
|
||||
let display = error.to_string();
|
||||
assert!(display.contains(config_file.path().to_string_lossy().as_ref()));
|
||||
assert!(display.contains("failed to load config"));
|
||||
assert!(display.contains("failed to parse flags"));
|
||||
|
||||
// with_context preserves the cause chain so callers can inspect the root reason.
|
||||
let chain: Vec<String> = error.chain().map(|e| e.to_string()).collect();
|
||||
assert!(chain.iter().any(|m| m.contains("failed to parse flags")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn socket_mark_config_file_roundtrip_none_some_and_zero() {
|
||||
// Omitting the flag leaves socket_mark unset (None) -> SO_MARK untouched.
|
||||
let cfg = TomlConfigLoader::new_from_str(
|
||||
r#"
|
||||
[network_identity]
|
||||
network_name = "n"
|
||||
network_secret = "s"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(cfg.get_flags().socket_mark, None);
|
||||
|
||||
// socket_mark = 0 is a legitimate value distinct from "unset".
|
||||
let cfg = TomlConfigLoader::new_from_str(
|
||||
r#"
|
||||
[network_identity]
|
||||
network_name = "n"
|
||||
network_secret = "s"
|
||||
|
||||
[flags]
|
||||
socket_mark = 0
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(cfg.get_flags().socket_mark, Some(0));
|
||||
|
||||
// A non-zero mark round-trips as Some(v).
|
||||
let cfg = TomlConfigLoader::new_from_str(
|
||||
r#"
|
||||
[network_identity]
|
||||
network_name = "n"
|
||||
network_secret = "s"
|
||||
|
||||
[flags]
|
||||
socket_mark = 66
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(cfg.get_flags().socket_mark, Some(66));
|
||||
|
||||
// set_flags(None) must serialize back through gen_config without
|
||||
// resurrecting a value (guards the gen_flags merge against dropping
|
||||
// the key when the serialized default is null).
|
||||
cfg.set_flags(Flags {
|
||||
socket_mark: None,
|
||||
..cfg.get_flags()
|
||||
});
|
||||
assert_eq!(cfg.get_flags().socket_mark, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dump_preserves_flags_that_differ_from_easytier_defaults() {
|
||||
let cfg = TomlConfigLoader::default();
|
||||
let mut flags = gen_default_flags();
|
||||
flags.dev_name = "et_test".to_string();
|
||||
flags.enable_quic_proxy = true;
|
||||
flags.disable_tcp_hole_punching = true;
|
||||
flags.disable_sym_hole_punching = true;
|
||||
flags.multi_thread = false;
|
||||
flags.bind_device = false;
|
||||
flags.enable_ipv6 = false;
|
||||
flags.relay_network_whitelist = "".to_string();
|
||||
flags.mtu = 0;
|
||||
flags.socket_mark = Some(0);
|
||||
cfg.set_flags(flags);
|
||||
|
||||
let dumped = cfg.dump();
|
||||
|
||||
assert!(dumped.contains("dev_name = \"et_test\""));
|
||||
assert!(dumped.contains("enable_quic_proxy = true"));
|
||||
assert!(dumped.contains("disable_tcp_hole_punching = true"));
|
||||
assert!(dumped.contains("disable_sym_hole_punching = true"));
|
||||
assert!(dumped.contains("multi_thread = false"));
|
||||
assert!(dumped.contains("bind_device = false"));
|
||||
assert!(dumped.contains("enable_ipv6 = false"));
|
||||
assert!(dumped.contains("relay_network_whitelist = \"\""));
|
||||
assert!(dumped.contains("mtu = 0"));
|
||||
assert!(dumped.contains("socket_mark = 0"));
|
||||
|
||||
let reloaded = TomlConfigLoader::new_from_str(&dumped).unwrap();
|
||||
let reloaded_flags = reloaded.get_flags();
|
||||
assert_eq!(reloaded_flags.dev_name, "et_test");
|
||||
assert!(reloaded_flags.enable_quic_proxy);
|
||||
assert!(reloaded_flags.disable_tcp_hole_punching);
|
||||
assert!(reloaded_flags.disable_sym_hole_punching);
|
||||
assert!(!reloaded_flags.multi_thread);
|
||||
assert!(!reloaded_flags.bind_device);
|
||||
assert!(!reloaded_flags.enable_ipv6);
|
||||
assert_eq!(reloaded_flags.relay_network_whitelist, "");
|
||||
assert_eq!(reloaded_flags.mtu, 0);
|
||||
assert_eq!(reloaded_flags.socket_mark, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stun_servers_config() {
|
||||
let config = TomlConfigLoader::default();
|
||||
@@ -1571,53 +1297,14 @@ stun_servers = [
|
||||
let config = TomlConfigLoader::default();
|
||||
assert_eq!(config.get_network_config_source(), ConfigSource::User);
|
||||
|
||||
config.set_network_config_source(Some(ConfigSource::Web));
|
||||
config.set_network_config_source(Some(ConfigSource::Webhook));
|
||||
let dumped = config.dump();
|
||||
|
||||
assert!(dumped.contains("[source]"));
|
||||
assert!(dumped.contains("source = \"web\""));
|
||||
assert!(dumped.contains("source = \"webhook\""));
|
||||
|
||||
let loaded = TomlConfigLoader::new_from_str(&dumped).unwrap();
|
||||
assert_eq!(loaded.get_network_config_source(), ConfigSource::Web);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toml_credential_mode_omits_network_secret() {
|
||||
for network_secret in ["", r#"network_secret = """#] {
|
||||
let config = TomlConfigLoader::new_from_str(&format!(
|
||||
r#"
|
||||
[network_identity]
|
||||
network_name = "credential-network"
|
||||
{network_secret}
|
||||
|
||||
[secure_mode]
|
||||
enabled = true
|
||||
"#
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let identity = config.get_network_identity();
|
||||
assert_eq!(identity.network_name, "credential-network");
|
||||
assert_eq!(identity.network_secret, None);
|
||||
assert_eq!(identity.network_secret_digest, None);
|
||||
assert!(!config.dump().contains("network_secret"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toml_secure_mode_without_network_identity_uses_default_secret() {
|
||||
let config = TomlConfigLoader::new_from_str(
|
||||
r#"
|
||||
[secure_mode]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let identity = config.get_network_identity();
|
||||
assert_eq!(identity.network_name, "default");
|
||||
assert_eq!(identity.network_secret.as_deref(), Some(""));
|
||||
assert!(identity.network_secret_digest.is_some());
|
||||
assert_eq!(loaded.get_network_config_source(), ConfigSource::Webhook);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use dashmap::DashMap;
|
||||
use hotpath::instant::Instant;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cell::UnsafeCell;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time::interval;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
|
||||
@@ -2,13 +2,12 @@ use std::collections::BTreeSet;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::proto::common::{NatType, StunInfo};
|
||||
use anyhow::Context;
|
||||
use chrono::Local;
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use hotpath::instant::Instant;
|
||||
use rand::seq::IteratorRandom;
|
||||
use socket2::{SockAddr, SockRef};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
@@ -1313,7 +1312,7 @@ impl StunInfoCollectorTrait for MockStunInfoCollector {
|
||||
StunInfo {
|
||||
udp_nat_type: self.udp_nat_type as i32,
|
||||
tcp_nat_type: NatType::Unknown as i32,
|
||||
last_update_time: Local::now().timestamp(),
|
||||
last_update_time: std::time::Instant::now().elapsed().as_secs() as i64,
|
||||
min_port: 100,
|
||||
max_port: 200,
|
||||
public_ip: vec!["127.0.0.1".to_string(), "::1".to_string()],
|
||||
|
||||
@@ -8,11 +8,9 @@ use std::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use hotpath::instant::Instant;
|
||||
|
||||
use crate::{
|
||||
common::{
|
||||
PeerId, dns::socket_addrs, error::Error, global_ctx::ArcGlobalCtx,
|
||||
|
||||
@@ -2,11 +2,10 @@ use std::{
|
||||
collections::BTreeSet,
|
||||
future::Future,
|
||||
sync::{Arc, Weak},
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use dashmap::DashSet;
|
||||
use hotpath::instant::Instant;
|
||||
use tokio::{sync::mpsc, task::JoinSet, time::timeout};
|
||||
|
||||
use crate::{
|
||||
|
||||
@@ -70,8 +70,6 @@ async fn set_bind_addr_for_peer_connector(
|
||||
let ips = global_ctx.get_ip_collector().collect_ip_addrs().await;
|
||||
if is_ipv4 {
|
||||
let mut bind_addrs = vec![];
|
||||
// Always include loopback so localhost connections work
|
||||
bind_addrs.push(std::net::SocketAddr::from(([127, 0, 0, 1], 0)));
|
||||
for ipv4 in ips.interface_ipv4s {
|
||||
let socket_addr = SocketAddrV4::new(ipv4.into(), 0).into();
|
||||
bind_addrs.push(socket_addr);
|
||||
@@ -270,7 +268,6 @@ pub async fn create_connector_by_url(
|
||||
IpScheme::FakeTcp => tunnel::fake_tcp::FakeTcpTunnelConnector::new(url).boxed(),
|
||||
};
|
||||
connector.set_resolved_addr(resolved_addr.addr);
|
||||
connector.set_socket_mark(global_ctx.config.get_flags().socket_mark);
|
||||
if global_ctx.config.get_flags().bind_device {
|
||||
set_bind_addr_for_peer_connector(
|
||||
&mut connector,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::{Context, Error};
|
||||
use hotpath::instant::Instant;
|
||||
use rand::Rng as _;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user