mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-29 16:05:35 +00:00
Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
003fdefc63 | ||
|
|
a25c411249 | ||
|
|
0c11cefc04 | ||
|
|
8181830902 | ||
|
|
0a8c95879b | ||
|
|
c0f42ebe8c | ||
|
|
9d965cae64 | ||
|
|
da28c8badc | ||
|
|
e38b1354b3 | ||
|
|
793b57c2a1 | ||
|
|
4a25ca934b | ||
|
|
13f2ebfe12 | ||
|
|
9ba364ff60 | ||
|
|
ba653da9a0 | ||
|
|
64c4d73044 | ||
|
|
e0745f4bab | ||
|
|
e3ca7ffa54 | ||
|
|
df97f3a64d | ||
|
|
00957e5f9d | ||
|
|
bfa3383aaa | ||
|
|
73bea01f40 | ||
|
|
0378191783 | ||
|
|
d5fa6a608d | ||
|
|
811f151155 | ||
|
|
8428a89d2d | ||
|
|
513695297c | ||
|
|
bfbfa2ef8d | ||
|
|
8e1d079142 | ||
|
|
55f15bb6f0 | ||
|
|
96fd39649a | ||
|
|
74fc8b300d | ||
|
|
baeee40b79 | ||
|
|
4342c8d7a2 | ||
|
|
1178b312fa | ||
|
|
362aa7a9cd | ||
|
|
12a7b5a5c5 | ||
|
|
4eba9b07b6 | ||
|
|
1b48029bdc | ||
|
|
3542e944cb | ||
|
|
852d1c9e14 | ||
|
|
4958394469 | ||
|
|
41b6d65604 | ||
|
|
aae30894dd | ||
|
|
81d169abfc | ||
|
|
9c6c210e89 | ||
|
|
d1c6dcf754 | ||
|
|
97c8c4f55a | ||
|
|
ed8df2d58f | ||
|
|
f66010e6f9 | ||
|
|
d5c4700d32 | ||
|
|
969ecfc4ca |
@@ -42,4 +42,7 @@ EXPOSE 11011/tcp
|
|||||||
# wss
|
# wss
|
||||||
EXPOSE 11012/tcp
|
EXPOSE 11012/tcp
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=5 \
|
||||||
|
CMD ["/usr/local/bin/easytier-cli", "--rpc-portal", "127.0.0.1:15888", "--output", "json", "node", "info"]
|
||||||
|
|
||||||
ENTRYPOINT ["/sbin/tini", "--", "easytier-core"]
|
ENTRYPOINT ["/sbin/tini", "--", "easytier-core"]
|
||||||
|
|||||||
@@ -157,6 +157,9 @@ jobs:
|
|||||||
|
|
||||||
- uses: mlugg/setup-zig@v2
|
- uses: mlugg/setup-zig@v2
|
||||||
if: ${{ contains(matrix.OS, 'ubuntu') }}
|
if: ${{ contains(matrix.OS, 'ubuntu') }}
|
||||||
|
with:
|
||||||
|
version: 0.16.0
|
||||||
|
use-cache: true
|
||||||
|
|
||||||
- uses: taiki-e/install-action@v2
|
- uses: taiki-e/install-action@v2
|
||||||
if: ${{ contains(matrix.OS, 'ubuntu') }}
|
if: ${{ contains(matrix.OS, 'ubuntu') }}
|
||||||
@@ -227,7 +230,7 @@ jobs:
|
|||||||
*) UPX_ARCH="amd64" ;;
|
*) UPX_ARCH="amd64" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
UPX_VERSION=5.1.1
|
UPX_VERSION=4.2.4
|
||||||
UPX_PKG="upx-${UPX_VERSION}-${UPX_ARCH}_linux"
|
UPX_PKG="upx-${UPX_VERSION}-${UPX_ARCH}_linux"
|
||||||
curl -L "https://github.com/upx/upx/releases/download/v${UPX_VERSION}/${UPX_PKG}.tar.xz" -s | tar xJvf -
|
curl -L "https://github.com/upx/upx/releases/download/v${UPX_VERSION}/${UPX_PKG}.tar.xz" -s | tar xJvf -
|
||||||
cp "${UPX_PKG}/upx" .
|
cp "${UPX_PKG}/upx" .
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ on:
|
|||||||
image_tag:
|
image_tag:
|
||||||
description: 'Tag for this image build'
|
description: 'Tag for this image build'
|
||||||
type: string
|
type: string
|
||||||
default: 'v2.6.2'
|
default: 'v2.6.4'
|
||||||
required: true
|
required: true
|
||||||
mark_latest:
|
mark_latest:
|
||||||
description: 'Mark this image as latest'
|
description: 'Mark this image as latest'
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ on:
|
|||||||
version:
|
version:
|
||||||
description: 'Version for this release'
|
description: 'Version for this release'
|
||||||
type: string
|
type: string
|
||||||
default: 'v2.6.2'
|
default: 'v2.6.4'
|
||||||
required: true
|
required: true
|
||||||
make_latest:
|
make_latest:
|
||||||
description: 'Mark this release as latest'
|
description: 'Mark this release as latest'
|
||||||
|
|||||||
@@ -43,3 +43,6 @@ easytier-gui/src-tauri/*.sys
|
|||||||
|
|
||||||
.direnv
|
.direnv
|
||||||
.flake-profile
|
.flake-profile
|
||||||
|
|
||||||
|
# contrib
|
||||||
|
go.sum
|
||||||
|
|||||||
Generated
+154
-111
@@ -1407,8 +1407,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857"
|
checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"prost-types",
|
"prost-types 0.13.5",
|
||||||
"tonic",
|
"tonic",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
@@ -1426,8 +1426,8 @@ dependencies = [
|
|||||||
"hdrhistogram",
|
"hdrhistogram",
|
||||||
"humantime",
|
"humantime",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"prost-types",
|
"prost-types 0.13.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thread_local",
|
"thread_local",
|
||||||
@@ -2229,7 +2229,7 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "easytier"
|
name = "easytier"
|
||||||
version = "2.6.2"
|
version = "2.6.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -2264,7 +2264,6 @@ dependencies = [
|
|||||||
"derivative",
|
"derivative",
|
||||||
"derive_builder",
|
"derive_builder",
|
||||||
"derive_more 2.1.1",
|
"derive_more 2.1.1",
|
||||||
"easytier-rpc-build",
|
|
||||||
"encoding",
|
"encoding",
|
||||||
"flume 0.12.0",
|
"flume 0.12.0",
|
||||||
"forwarded-header-value",
|
"forwarded-header-value",
|
||||||
@@ -2273,6 +2272,7 @@ dependencies = [
|
|||||||
"gethostname 0.5.0",
|
"gethostname 0.5.0",
|
||||||
"git-version",
|
"git-version",
|
||||||
"globwalk",
|
"globwalk",
|
||||||
|
"guarden",
|
||||||
"hickory-client",
|
"hickory-client",
|
||||||
"hickory-proto",
|
"hickory-proto",
|
||||||
"hickory-resolver",
|
"hickory-resolver",
|
||||||
@@ -2290,6 +2290,7 @@ dependencies = [
|
|||||||
"machine-uid",
|
"machine-uid",
|
||||||
"maplit",
|
"maplit",
|
||||||
"mimalloc",
|
"mimalloc",
|
||||||
|
"moka",
|
||||||
"multimap",
|
"multimap",
|
||||||
"natpmp",
|
"natpmp",
|
||||||
"netlink-packet-core",
|
"netlink-packet-core",
|
||||||
@@ -2303,20 +2304,22 @@ dependencies = [
|
|||||||
"ordered_hash_map",
|
"ordered_hash_map",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"paste",
|
"paste",
|
||||||
|
"pbjson",
|
||||||
|
"pbjson-build",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"petgraph 0.8.1",
|
"petgraph",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pnet",
|
"pnet",
|
||||||
"prefix-trie",
|
"prefix-trie",
|
||||||
"prost",
|
"proc-macro2",
|
||||||
|
"prost 0.14.3",
|
||||||
"prost-build",
|
"prost-build",
|
||||||
"prost-reflect",
|
"prost-reflect",
|
||||||
"prost-reflect-build",
|
"prost-reflect-build",
|
||||||
"prost-wkt",
|
|
||||||
"prost-wkt-build",
|
|
||||||
"prost-wkt-types",
|
"prost-wkt-types",
|
||||||
"quinn",
|
"quinn",
|
||||||
"quinn-plaintext",
|
"quinn-plaintext",
|
||||||
|
"quote",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -2356,7 +2359,6 @@ dependencies = [
|
|||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tokio-websockets",
|
"tokio-websockets",
|
||||||
"toml 0.8.19",
|
"toml 0.8.19",
|
||||||
"tonic-build",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"tun-easytier",
|
"tun-easytier",
|
||||||
@@ -2383,6 +2385,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"android_logger",
|
"android_logger",
|
||||||
"easytier",
|
"easytier",
|
||||||
|
"easytier-ffi",
|
||||||
"jni",
|
"jni",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -2394,17 +2397,23 @@ dependencies = [
|
|||||||
name = "easytier-ffi"
|
name = "easytier-ffi"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"easytier",
|
"easytier",
|
||||||
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"percent-encoding",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "easytier-gui"
|
name = "easytier-gui"
|
||||||
version = "2.6.2"
|
version = "2.6.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -2435,14 +2444,6 @@ dependencies = [
|
|||||||
"windows 0.52.0",
|
"windows 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "easytier-rpc-build"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"heck 0.5.0",
|
|
||||||
"prost-build",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "easytier-uptime"
|
name = "easytier-uptime"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -2456,6 +2457,7 @@ dependencies = [
|
|||||||
"dashmap",
|
"dashmap",
|
||||||
"easytier",
|
"easytier",
|
||||||
"futures",
|
"futures",
|
||||||
|
"guarden",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"mimalloc",
|
"mimalloc",
|
||||||
"mockall",
|
"mockall",
|
||||||
@@ -2484,7 +2486,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "easytier-web"
|
name = "easytier-web"
|
||||||
version = "2.6.2"
|
version = "2.6.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -2910,12 +2912,6 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fixedbitset"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fixedbitset"
|
name = "fixedbitset"
|
||||||
version = "0.5.7"
|
version = "0.5.7"
|
||||||
@@ -3590,6 +3586,28 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "guarden"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca87812d87fa82896df1adfb5c111cdeaae3edb6da028f5df002dcbd7df71454"
|
||||||
|
dependencies = [
|
||||||
|
"futures",
|
||||||
|
"guarden-macros",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "guarden-macros"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b42f4b8de91cbd793ce8e6cf8d4821ef02d2d5b4468e0a55a36c65c5581de53"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.7"
|
version = "0.4.7"
|
||||||
@@ -3705,12 +3723,6 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hermit-abi"
|
|
||||||
version = "0.3.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -4026,7 +4038,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2 0.6.1",
|
"socket2 0.6.3",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -4370,9 +4382,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inventory"
|
name = "inventory"
|
||||||
version = "0.3.22"
|
version = "0.3.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227"
|
checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustversion",
|
"rustversion",
|
||||||
]
|
]
|
||||||
@@ -4612,7 +4624,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "kcp-sys"
|
name = "kcp-sys"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/EasyTier/kcp-sys?rev=94964794caaed5d388463137da59b97499619e5f#94964794caaed5d388463137da59b97499619e5f"
|
source = "git+https://github.com/EasyTier/kcp-sys?rev=d7427c22d764deb1860a7d37acc446ed5033464c#d7427c22d764deb1860a7d37acc446ed5033464c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"auto_impl",
|
"auto_impl",
|
||||||
@@ -4695,9 +4707,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.172"
|
version = "0.2.186"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libdbus-sys"
|
name = "libdbus-sys"
|
||||||
@@ -5043,14 +5055,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.0.2"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
|
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hermit-abi 0.3.9",
|
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5086,9 +5097,12 @@ version = "0.12.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926"
|
checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-lock",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"crossbeam-epoch",
|
"crossbeam-epoch",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
|
"event-listener",
|
||||||
|
"futures-util",
|
||||||
"loom",
|
"loom",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
@@ -6141,6 +6155,28 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
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]]
|
[[package]]
|
||||||
name = "pbkdf2"
|
name = "pbkdf2"
|
||||||
version = "0.12.2"
|
version = "0.12.2"
|
||||||
@@ -6176,23 +6212,13 @@ version = "2.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
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]]
|
[[package]]
|
||||||
name = "petgraph"
|
name = "petgraph"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a98c6720655620a521dcc722d0ad66cd8afd5d86e34a89ef691c50b7b24de06"
|
checksum = "7a98c6720655620a521dcc722d0ad66cd8afd5d86e34a89ef691c50b7b24de06"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fixedbitset 0.5.7",
|
"fixedbitset",
|
||||||
"hashbrown 0.15.3",
|
"hashbrown 0.15.3",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.0",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -6551,7 +6577,7 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"hermit-abi 0.5.2",
|
"hermit-abi",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustix 1.0.7",
|
"rustix 1.0.7",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
@@ -6772,24 +6798,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"prost-derive",
|
"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",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost-build"
|
name = "prost-build"
|
||||||
version = "0.13.5"
|
version = "0.14.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
|
checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"log",
|
"log",
|
||||||
"multimap",
|
"multimap",
|
||||||
"once_cell",
|
"petgraph",
|
||||||
"petgraph 0.6.5",
|
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"prost",
|
"prost 0.14.3",
|
||||||
"prost-types",
|
"prost-types 0.14.3",
|
||||||
"regex",
|
"regex",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
@@ -6809,22 +6844,34 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost-reflect"
|
name = "prost-derive"
|
||||||
version = "0.14.5"
|
version = "0.14.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e92b959d24e05a3e2da1d0beb55b48bc8a97059b8336ea617780bd6addbbfb5a"
|
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"anyhow",
|
||||||
"prost",
|
"itertools 0.14.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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",
|
"prost-reflect-derive",
|
||||||
"prost-types",
|
"prost-types 0.14.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost-reflect-build"
|
name = "prost-reflect-build"
|
||||||
version = "0.14.0"
|
version = "0.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "50e2537231d94dd2778920c2ada37dd9eb1ac0325bb3ee3ee651bd44c1134123"
|
checksum = "8214ae2c30bbac390db0134d08300e770ef89b6d4e5abf855e8d300eded87e28"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"prost-build",
|
"prost-build",
|
||||||
"prost-reflect",
|
"prost-reflect",
|
||||||
@@ -6832,9 +6879,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost-reflect-derive"
|
name = "prost-reflect-derive"
|
||||||
version = "0.14.0"
|
version = "0.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f4fce6b22f15cc8d8d400a2b98ad29202b33bd56c7d9ddd815bc803a807ecb65"
|
checksum = "7b6d90e29fa6c0d13c2c19ba5e4b3fb0efbf5975d27bcf4e260b7b15455bcabe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -6847,18 +6894,27 @@ version = "0.13.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
|
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"prost",
|
"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",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost-wkt"
|
name = "prost-wkt"
|
||||||
version = "0.6.1"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "497e1e938f0c09ef9cabe1d49437b4016e03e8f82fbbe5d1c62a9b61b9decae1"
|
checksum = "cd3de5e9c9e84fcb5efa204b8e283d23e615a8bc8c777bf1d6622bb01dc61445"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"inventory",
|
"inventory",
|
||||||
"prost",
|
"prost 0.14.3",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -6867,27 +6923,27 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost-wkt-build"
|
name = "prost-wkt-build"
|
||||||
version = "0.6.1"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07b8bf115b70a7aa5af1fd5d6e9418492e9ccb6e4785e858c938e28d132a884b"
|
checksum = "fe500dc80e757a75e1e8fb7290e448d62dfba3105ece1d058579cb00b58151cd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"prost",
|
"prost 0.14.3",
|
||||||
"prost-build",
|
"prost-build",
|
||||||
"prost-types",
|
"prost-types 0.14.3",
|
||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost-wkt-types"
|
name = "prost-wkt-types"
|
||||||
version = "0.6.1"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8cdde6df0a98311c839392ca2f2f0bcecd545f86a62b4e3c6a49c336e970fe5"
|
checksum = "13807eaa7e15833d06e899008371926201cdcd11d74b6d490f49130cdb3f415e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"prost",
|
"prost 0.14.3",
|
||||||
"prost-build",
|
"prost-build",
|
||||||
"prost-types",
|
"prost-types 0.14.3",
|
||||||
"prost-wkt",
|
"prost-wkt",
|
||||||
"prost-wkt-build",
|
"prost-wkt-build",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -8650,12 +8706,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.1"
|
version = "0.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -9774,9 +9830,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.48.0"
|
version = "1.52.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -9784,7 +9840,7 @@ dependencies = [
|
|||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2 0.6.1",
|
"socket2 0.6.3",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
@@ -9792,9 +9848,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.6.0"
|
version = "2.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -10024,7 +10080,7 @@ dependencies = [
|
|||||||
"hyper-util",
|
"hyper-util",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"socket2 0.5.10",
|
"socket2 0.5.10",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
@@ -10034,19 +10090,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tonic-build"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "568392c5a2bd0020723e3f387891176aabafe36fd9fcd074ad309dfa0c8eb964"
|
|
||||||
dependencies = [
|
|
||||||
"prettyplease",
|
|
||||||
"proc-macro2",
|
|
||||||
"prost-build",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.117",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@@ -10321,7 +10364,7 @@ checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"nom 8.0.0",
|
"nom 8.0.0",
|
||||||
"petgraph 0.8.1",
|
"petgraph",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -10381,9 +10424,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typetag"
|
name = "typetag"
|
||||||
version = "0.2.21"
|
version = "0.2.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "be2212c8a9b9bcfca32024de14998494cf9a5dfa59ea1b829de98bac374b86bf"
|
checksum = "c5a897b12c6c1151ad0b138b8db50252dc301f93bc3b027db05eec82aeed298c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"erased-serde",
|
"erased-serde",
|
||||||
"inventory",
|
"inventory",
|
||||||
@@ -10394,9 +10437,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typetag-impl"
|
name = "typetag-impl"
|
||||||
version = "0.2.21"
|
version = "0.2.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846"
|
checksum = "cf808357c6ed7e13ba0f3277ec8d8f21b2d501274895104263985330c726c1c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ resolver = "2"
|
|||||||
members = [
|
members = [
|
||||||
"easytier",
|
"easytier",
|
||||||
"easytier-gui/src-tauri",
|
"easytier-gui/src-tauri",
|
||||||
"easytier-rpc-build",
|
|
||||||
"easytier-web",
|
"easytier-web",
|
||||||
"easytier-contrib/easytier-ffi",
|
"easytier-contrib/easytier-ffi",
|
||||||
"easytier-contrib/easytier-uptime",
|
"easytier-contrib/easytier-uptime",
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ android_logger = "0.13"
|
|||||||
serde = { version = "1.0.220", features = ["derive"] }
|
serde = { version = "1.0.220", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
easytier = { path = "../../easytier" }
|
easytier = { path = "../../easytier" }
|
||||||
|
easytier-ffi = { path = "../easytier-ffi", default-features = false, features = ["ffi-dataplane"] }
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
- 📱 原生 Android JNI 支持
|
- 📱 原生 Android JNI 支持
|
||||||
- 🔧 支持多种 Android 架构 (arm64-v8a, armeabi-v7a, x86, x86_64)
|
- 🔧 支持多种 Android 架构 (arm64-v8a, armeabi-v7a, x86, x86_64)
|
||||||
- 🛡️ 类型安全的 Java 接口
|
- 🛡️ 类型安全的 Java 接口
|
||||||
|
- 🔌 支持通过 JSON 调用已暴露的 EasyTier RPC 查询/管理接口
|
||||||
- 📝 详细的错误处理和日志记录
|
- 📝 详细的错误处理和日志记录
|
||||||
|
|
||||||
## 支持的架构
|
## 支持的架构
|
||||||
@@ -176,6 +177,20 @@ 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 服务集成
|
### VPN 服务集成
|
||||||
|
|
||||||
如果您要在 Android VPN 服务中使用:
|
如果您要在 Android VPN 服务中使用:
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
use std::{env, path::PathBuf};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||||
|
if !matches!(target_os.as_str(), "android" | "linux") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
let exports = manifest_dir.join("exports.map");
|
||||||
|
println!("cargo:rerun-if-changed={}", exports.display());
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-cdylib-link-arg=-Wl,--version-script={}",
|
||||||
|
exports.display()
|
||||||
|
);
|
||||||
|
println!("cargo:rustc-cdylib-link-arg=-Wl,--exclude-libs,ALL");
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
global:
|
||||||
|
Java_com_easytier_jni_EasyTierJNI_*;
|
||||||
|
Java_com_easytier_jni_EasyTierDataPlaneJNI_*;
|
||||||
|
local:
|
||||||
|
*;
|
||||||
|
};
|
||||||
+451
@@ -0,0 +1,451 @@
|
|||||||
|
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,8 +1,11 @@
|
|||||||
package com.easytier.jni
|
package com.easytier.jni
|
||||||
|
|
||||||
/** EasyTier JNI 接口类 提供 Android 应用调用 EasyTier 网络功能的接口 */
|
fun interface ConfigServerEventCallback {
|
||||||
object EasyTierJNI {
|
fun onEvent(eventJson: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** EasyTier JNI 接口类 提供 Android 应用调用 EasyTier 核心网络功能的接口 */
|
||||||
|
object EasyTierJNI {
|
||||||
init {
|
init {
|
||||||
// 加载本地库
|
// 加载本地库
|
||||||
System.loadLibrary("easytier_android_jni")
|
System.loadLibrary("easytier_android_jni")
|
||||||
@@ -33,6 +36,35 @@ object EasyTierJNI {
|
|||||||
*/
|
*/
|
||||||
@JvmStatic external fun runNetworkInstance(config: String): Int
|
@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 或空数组将停止所有实例
|
* @param instanceNames 要保留的实例名称数组,传入 null 或空数组将停止所有实例
|
||||||
@@ -44,11 +76,48 @@ object EasyTierJNI {
|
|||||||
/**
|
/**
|
||||||
* 收集网络信息
|
* 收集网络信息
|
||||||
* @param maxLength 最大返回条目数
|
* @param maxLength 最大返回条目数
|
||||||
* @return 包含网络信息的字符串数组,每个元素格式为 "key=value"
|
* @return 包含网络信息的 JSON 字符串
|
||||||
* @throws RuntimeException 当操作失败时抛出异常
|
* @throws RuntimeException 当操作失败时抛出异常
|
||||||
*/
|
*/
|
||||||
@JvmStatic external fun collectNetworkInfos(maxLength: Int): String?
|
@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
|
* @return 错误消息字符串,如果没有错误则返回 null
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
use std::{
|
||||||
|
ffi::{CStr, c_char, c_void},
|
||||||
|
sync::{Arc, Mutex, MutexGuard},
|
||||||
|
};
|
||||||
|
|
||||||
|
use easytier_ffi::ConfigServerEventCallback;
|
||||||
|
use jni::JNIEnv;
|
||||||
|
use jni::objects::{GlobalRef, JObject, JValue};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use crate::error;
|
||||||
|
|
||||||
|
pub(crate) struct JniConfigServerCallback {
|
||||||
|
java_vm: jni::JavaVM,
|
||||||
|
callback: GlobalRef,
|
||||||
|
}
|
||||||
|
|
||||||
|
static CONFIG_SERVER_CALLBACK: Lazy<Mutex<Option<Arc<JniConfigServerCallback>>>> =
|
||||||
|
Lazy::new(|| Mutex::new(None));
|
||||||
|
|
||||||
|
pub(crate) fn lock_callback_storage()
|
||||||
|
-> Result<MutexGuard<'static, Option<Arc<JniConfigServerCallback>>>, String> {
|
||||||
|
CONFIG_SERVER_CALLBACK
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("Failed to lock config server callback: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new_callback(
|
||||||
|
env: &mut JNIEnv,
|
||||||
|
callback: &JObject,
|
||||||
|
) -> Result<Arc<JniConfigServerCallback>, String> {
|
||||||
|
let java_vm = env
|
||||||
|
.get_java_vm()
|
||||||
|
.map_err(|e| format!("Failed to get JavaVM: {:?}", e))?;
|
||||||
|
let callback = env
|
||||||
|
.new_global_ref(callback)
|
||||||
|
.map_err(|e| format!("Failed to create callback global ref: {:?}", e))?;
|
||||||
|
Ok(Arc::new(JniConfigServerCallback { java_vm, callback }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn callback_fn(
|
||||||
|
callback: &Option<Arc<JniConfigServerCallback>>,
|
||||||
|
) -> ConfigServerEventCallback {
|
||||||
|
callback
|
||||||
|
.as_ref()
|
||||||
|
.map(|_| config_server_event_callback as unsafe extern "C" fn(*const c_char, *mut c_void))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn user_data(callback: &Option<Arc<JniConfigServerCallback>>) -> *mut c_void {
|
||||||
|
callback
|
||||||
|
.as_ref()
|
||||||
|
.map(|callback| Arc::as_ptr(callback) as *mut c_void)
|
||||||
|
.unwrap_or(std::ptr::null_mut())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JniConfigServerCallback {
|
||||||
|
fn clear_pending_exception(
|
||||||
|
env: &mut JNIEnv,
|
||||||
|
context: &str,
|
||||||
|
error: &dyn std::fmt::Debug,
|
||||||
|
) -> String {
|
||||||
|
match env.exception_check() {
|
||||||
|
Ok(true) => {
|
||||||
|
if let Err(clear_err) = env.exception_clear() {
|
||||||
|
return format!(
|
||||||
|
"{}: {:?}; failed to clear pending Java exception: {:?}",
|
||||||
|
context, error, clear_err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(check_err) => {
|
||||||
|
return format!(
|
||||||
|
"{}: {:?}; failed to check pending Java exception: {:?}",
|
||||||
|
context, error, check_err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("{}: {:?}", context, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_event(&self, event_json: *const c_char) -> Result<(), String> {
|
||||||
|
let event_json = unsafe { CStr::from_ptr(event_json) }
|
||||||
|
.to_str()
|
||||||
|
.map_err(|e| format!("Invalid config server event JSON: {:?}", e))?;
|
||||||
|
let mut env = self
|
||||||
|
.java_vm
|
||||||
|
.attach_current_thread()
|
||||||
|
.map_err(|e| format!("Failed to attach callback thread: {:?}", e))?;
|
||||||
|
let event_json = env.new_string(event_json).map_err(|e| {
|
||||||
|
Self::clear_pending_exception(&mut env, "Failed to create event string", &e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if let Err(e) = env.call_method(
|
||||||
|
self.callback.as_obj(),
|
||||||
|
"onEvent",
|
||||||
|
"(Ljava/lang/String;)V",
|
||||||
|
&[JValue::from(&event_json)],
|
||||||
|
) {
|
||||||
|
return Err(Self::clear_pending_exception(
|
||||||
|
&mut env,
|
||||||
|
"Failed to call config server callback",
|
||||||
|
&e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn config_server_event_callback(
|
||||||
|
event_json: *const c_char,
|
||||||
|
user_data: *mut c_void,
|
||||||
|
) {
|
||||||
|
if event_json.is_null() || user_data.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let callback = unsafe { &*(user_data as *const JniConfigServerCallback) };
|
||||||
|
|
||||||
|
if let Err(error) = callback.on_event(event_json) {
|
||||||
|
error::set_callback_error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
use easytier_ffi::{
|
||||||
|
in_config_server_callback, is_config_server_client_connected, start_config_server_client,
|
||||||
|
stop_config_server_client,
|
||||||
|
};
|
||||||
|
use jni::JNIEnv;
|
||||||
|
use jni::objects::{JClass, JObject, JString};
|
||||||
|
use jni::sys::{JNI_FALSE, JNI_TRUE, jboolean, jint};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
callback, error,
|
||||||
|
strings::{jstring_to_cstring, optional_jstring_to_cstring},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn start_config_server_client_jni(
|
||||||
|
env: &mut JNIEnv,
|
||||||
|
config_server_url: JString,
|
||||||
|
hostname: JString,
|
||||||
|
machine_id: JString,
|
||||||
|
secure_mode: jboolean,
|
||||||
|
callback_obj: JObject,
|
||||||
|
) -> jint {
|
||||||
|
if in_config_server_callback() {
|
||||||
|
error::throw_exception(
|
||||||
|
env,
|
||||||
|
"Cannot start config server client from config server callback",
|
||||||
|
);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config_server_url = match jstring_to_cstring(env, &config_server_url) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
error::throw_exception(env, &format!("Invalid config server URL: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let hostname = match optional_jstring_to_cstring(env, &hostname) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
error::throw_exception(env, &format!("Invalid hostname: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let machine_id = match jstring_to_cstring(env, &machine_id) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
error::throw_exception(env, &format!("Invalid machine ID: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let callback_ref = if callback_obj.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
match callback::new_callback(env, &callback_obj) {
|
||||||
|
Ok(state) => Some(state),
|
||||||
|
Err(e) => {
|
||||||
|
error::throw_exception(env, &e);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut callback_guard = match callback::lock_callback_storage() {
|
||||||
|
Ok(guard) => guard,
|
||||||
|
Err(e) => {
|
||||||
|
error::throw_exception(env, &e);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if callback_guard.is_none() {
|
||||||
|
error::clear_callback_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
let callback_fn = callback::callback_fn(&callback_ref);
|
||||||
|
let user_data = callback::user_data(&callback_ref);
|
||||||
|
let result = unsafe {
|
||||||
|
start_config_server_client(
|
||||||
|
config_server_url.as_ptr(),
|
||||||
|
hostname
|
||||||
|
.as_ref()
|
||||||
|
.map(|value| value.as_ptr())
|
||||||
|
.unwrap_or(ptr::null()),
|
||||||
|
machine_id.as_ptr(),
|
||||||
|
secure_mode == JNI_TRUE,
|
||||||
|
callback_fn,
|
||||||
|
user_data,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if result != 0 {
|
||||||
|
if let Some(error_msg) = error::get_last_error() {
|
||||||
|
error::throw_exception(env, &error_msg);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
*callback_guard = callback_ref;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stop_config_server_client_jni(mut env: JNIEnv, _class: JClass) -> jint {
|
||||||
|
if in_config_server_callback() {
|
||||||
|
let result = stop_config_server_client();
|
||||||
|
if result != 0
|
||||||
|
&& let Some(error_msg) = error::get_last_error()
|
||||||
|
{
|
||||||
|
error::throw_exception(&mut env, &error_msg);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut callback_guard = match callback::lock_callback_storage() {
|
||||||
|
Ok(guard) => guard,
|
||||||
|
Err(e) => {
|
||||||
|
error::throw_exception(&mut env, &e);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = stop_config_server_client();
|
||||||
|
if result != 0 {
|
||||||
|
if let Some(error_msg) = error::get_last_error() {
|
||||||
|
error::throw_exception(&mut env, &error_msg);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
*callback_guard = None;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_config_server_client_connected_jni(_env: JNIEnv, _class: JClass) -> jboolean {
|
||||||
|
if is_config_server_client_connected() != 0 {
|
||||||
|
JNI_TRUE
|
||||||
|
} else {
|
||||||
|
JNI_FALSE
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,673 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
use std::{
|
||||||
|
ffi::{CStr, c_char},
|
||||||
|
ptr,
|
||||||
|
sync::Mutex,
|
||||||
|
};
|
||||||
|
|
||||||
|
use easytier_ffi::{free_string, get_error_msg};
|
||||||
|
use jni::JNIEnv;
|
||||||
|
use jni::objects::JClass;
|
||||||
|
use jni::sys::jstring;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
static JNI_CALLBACK_ERROR: Lazy<Mutex<Option<String>>> = Lazy::new(|| Mutex::new(None));
|
||||||
|
|
||||||
|
pub(crate) fn set_callback_error(error: String) {
|
||||||
|
log::error!("{}", error);
|
||||||
|
if let Ok(mut guard) = JNI_CALLBACK_ERROR.lock() {
|
||||||
|
*guard = Some(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear_callback_error() {
|
||||||
|
if let Ok(mut guard) = JNI_CALLBACK_ERROR.lock() {
|
||||||
|
*guard = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_callback_error() -> Option<String> {
|
||||||
|
JNI_CALLBACK_ERROR
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|mut guard| guard.take())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_ffi_last_error() -> Option<String> {
|
||||||
|
unsafe {
|
||||||
|
let mut error_ptr: *const c_char = ptr::null();
|
||||||
|
get_error_msg(&mut error_ptr);
|
||||||
|
if error_ptr.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let error_cstr = CStr::from_ptr(error_ptr);
|
||||||
|
let error_str = error_cstr.to_string_lossy().into_owned();
|
||||||
|
free_string(error_ptr);
|
||||||
|
Some(error_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_last_error() -> Option<String> {
|
||||||
|
match (get_ffi_last_error(), take_callback_error()) {
|
||||||
|
(Some(ffi_error), Some(callback_error)) => Some(format!(
|
||||||
|
"{}; config server callback error: {}",
|
||||||
|
ffi_error, callback_error
|
||||||
|
)),
|
||||||
|
(Some(ffi_error), None) => Some(ffi_error),
|
||||||
|
(None, Some(callback_error)) => Some(callback_error),
|
||||||
|
(None, None) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn throw_exception(env: &mut JNIEnv, message: &str) {
|
||||||
|
let _ = env.throw_new("java/lang/RuntimeException", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_last_error_jni(env: JNIEnv, _class: JClass) -> jstring {
|
||||||
|
match get_last_error() {
|
||||||
|
Some(error) => match env.new_string(&error) {
|
||||||
|
Ok(jstr) => jstr.into_raw(),
|
||||||
|
Err(_) => ptr::null_mut(),
|
||||||
|
},
|
||||||
|
None => ptr::null_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
use std::{
|
||||||
|
ffi::{CStr, c_char},
|
||||||
|
ptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use easytier_ffi::{call_json_rpc, free_string};
|
||||||
|
use jni::JNIEnv;
|
||||||
|
use jni::objects::{JClass, JString};
|
||||||
|
use jni::sys::jstring;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::{get_last_error, throw_exception},
|
||||||
|
strings::{jstring_to_cstring, optional_jstring_to_cstring},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn call_json_rpc_jni(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
service_name: JString,
|
||||||
|
method_name: JString,
|
||||||
|
domain_name: JString,
|
||||||
|
payload_json: JString,
|
||||||
|
) -> jstring {
|
||||||
|
let service_name_cstr = match jstring_to_cstring(&mut env, &service_name) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid service name: {}", e));
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let method_name_cstr = match jstring_to_cstring(&mut env, &method_name) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid method name: {}", e));
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let domain_name_cstr = match optional_jstring_to_cstring(&mut env, &domain_name) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid domain name: {}", e));
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let payload_json_cstr = match jstring_to_cstring(&mut env, &payload_json) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid payload JSON: {}", e));
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let domain_name_ptr = domain_name_cstr
|
||||||
|
.as_ref()
|
||||||
|
.map_or(ptr::null(), |cstr| cstr.as_ptr());
|
||||||
|
let mut response_ptr: *const c_char = ptr::null();
|
||||||
|
let result = unsafe {
|
||||||
|
call_json_rpc(
|
||||||
|
service_name_cstr.as_ptr(),
|
||||||
|
method_name_cstr.as_ptr(),
|
||||||
|
domain_name_ptr,
|
||||||
|
payload_json_cstr.as_ptr(),
|
||||||
|
&mut response_ptr,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if result != 0 {
|
||||||
|
if let Some(error) = get_last_error() {
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
if response_ptr.is_null() {
|
||||||
|
throw_exception(&mut env, "JSON RPC returned a null response");
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = unsafe { CStr::from_ptr(response_ptr) }
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned();
|
||||||
|
free_string(response_ptr);
|
||||||
|
|
||||||
|
match env.new_string(&response) {
|
||||||
|
Ok(jstr) => jstr.into_raw(),
|
||||||
|
Err(_) => {
|
||||||
|
throw_exception(&mut env, "Failed to create JSON RPC response string");
|
||||||
|
ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
|||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
static LOGGER_INIT: Lazy<()> = Lazy::new(|| {
|
||||||
|
android_logger::init_once(
|
||||||
|
android_logger::Config::default()
|
||||||
|
.with_max_level(log::LevelFilter::Debug)
|
||||||
|
.with_tag("EasyTier-JNI"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
pub(crate) fn init() {
|
||||||
|
Lazy::force(&LOGGER_INIT);
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
use std::{ffi::CStr, ptr};
|
||||||
|
|
||||||
|
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
|
||||||
|
use easytier_ffi::{
|
||||||
|
KeyValuePair, collect_network_infos, free_string, list_instance, parse_config,
|
||||||
|
retain_network_instance, run_network_instance, set_tun_fd,
|
||||||
|
};
|
||||||
|
use jni::JNIEnv;
|
||||||
|
use jni::objects::{JClass, JObjectArray, JString};
|
||||||
|
use jni::sys::{jint, jstring};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::{get_last_error, throw_exception},
|
||||||
|
strings::jstring_to_cstring,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn set_tun_fd_jni(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
inst_name: JString,
|
||||||
|
fd: jint,
|
||||||
|
) -> jint {
|
||||||
|
let inst_name_cstr = match jstring_to_cstring(&mut env, &inst_name) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid instance name: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
unsafe {
|
||||||
|
let result = set_tun_fd(inst_name_cstr.as_ptr(), fd);
|
||||||
|
if result != 0
|
||||||
|
&& let Some(error) = get_last_error()
|
||||||
|
{
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_config_jni(mut env: JNIEnv, _class: JClass, config: JString) -> jint {
|
||||||
|
let config_cstr = match jstring_to_cstring(&mut env, &config) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid config string: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
unsafe {
|
||||||
|
let result = parse_config(config_cstr.as_ptr());
|
||||||
|
if result != 0
|
||||||
|
&& let Some(error) = get_last_error()
|
||||||
|
{
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn run_network_instance_jni(mut env: JNIEnv, _class: JClass, config: JString) -> jint {
|
||||||
|
let config_cstr = match jstring_to_cstring(&mut env, &config) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid config string: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
unsafe {
|
||||||
|
let result = run_network_instance(config_cstr.as_ptr());
|
||||||
|
if result != 0
|
||||||
|
&& let Some(error) = get_last_error()
|
||||||
|
{
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn retain_network_instance_jni(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
instance_names: JObjectArray,
|
||||||
|
) -> jint {
|
||||||
|
if instance_names.is_null() {
|
||||||
|
return retain_all(&mut env);
|
||||||
|
}
|
||||||
|
|
||||||
|
let array_length = match env.get_array_length(&instance_names) {
|
||||||
|
Ok(len) => len as usize,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Failed to get array length: {:?}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if array_length == 0 {
|
||||||
|
return retain_all(&mut env);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut c_strings = Vec::with_capacity(array_length);
|
||||||
|
let mut c_string_ptrs = Vec::with_capacity(array_length);
|
||||||
|
|
||||||
|
for i in 0..array_length {
|
||||||
|
let java_string = match env.get_object_array_element(&instance_names, i as i32) {
|
||||||
|
Ok(obj) => obj,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(
|
||||||
|
&mut env,
|
||||||
|
&format!("Failed to get array element {}: {:?}", i, e),
|
||||||
|
);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if java_string.is_null() {
|
||||||
|
throw_exception(
|
||||||
|
&mut env,
|
||||||
|
&format!("Invalid instance name at index {}: null", i),
|
||||||
|
);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let jstring = JString::from(java_string);
|
||||||
|
let c_string = match jstring_to_cstring(&mut env, &jstring) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(
|
||||||
|
&mut env,
|
||||||
|
&format!("Invalid instance name at index {}: {}", i, e),
|
||||||
|
);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
c_string_ptrs.push(c_string.as_ptr());
|
||||||
|
c_strings.push(c_string);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let result = retain_network_instance(c_string_ptrs.as_ptr(), c_string_ptrs.len());
|
||||||
|
if result != 0
|
||||||
|
&& let Some(error) = get_last_error()
|
||||||
|
{
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn retain_all(env: &mut JNIEnv) -> jint {
|
||||||
|
unsafe {
|
||||||
|
let result = retain_network_instance(ptr::null(), 0);
|
||||||
|
if result != 0
|
||||||
|
&& let Some(error) = get_last_error()
|
||||||
|
{
|
||||||
|
throw_exception(env, &error);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn collect_network_infos_jni(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
max_length: jint,
|
||||||
|
) -> jstring {
|
||||||
|
let max_length = max_length.max(0) as usize;
|
||||||
|
let mut infos = vec![
|
||||||
|
KeyValuePair {
|
||||||
|
key: ptr::null(),
|
||||||
|
value: ptr::null(),
|
||||||
|
};
|
||||||
|
max_length
|
||||||
|
];
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let count = collect_network_infos(infos.as_mut_ptr(), max_length);
|
||||||
|
if count < 0 {
|
||||||
|
if let Some(error) = get_last_error() {
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ret = NetworkInstanceRunningInfoMap::default();
|
||||||
|
for info in infos.iter().take(count as usize) {
|
||||||
|
let key_ptr = info.key;
|
||||||
|
let val_ptr = info.value;
|
||||||
|
if key_ptr.is_null() || val_ptr.is_null() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
|
||||||
|
let val = CStr::from_ptr(val_ptr).to_string_lossy().into_owned();
|
||||||
|
free_string(key_ptr);
|
||||||
|
free_string(val_ptr);
|
||||||
|
let value = match serde_json::from_str::<NetworkInstanceRunningInfo>(&val) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => {
|
||||||
|
throw_exception(&mut env, "Failed to parse JSON");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ret.map.insert(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_str = serde_json::to_string(&ret).unwrap_or_else(|_| "{}".to_string());
|
||||||
|
match env.new_string(&json_str) {
|
||||||
|
Ok(jstr) => jstr.into_raw(),
|
||||||
|
Err(_) => {
|
||||||
|
throw_exception(&mut env, "Failed to create JSON string");
|
||||||
|
ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn list_instances_jni(mut env: JNIEnv, _class: JClass, max_length: jint) -> jstring {
|
||||||
|
let max_length = max_length.max(0) as usize;
|
||||||
|
let mut infos = vec![
|
||||||
|
KeyValuePair {
|
||||||
|
key: ptr::null(),
|
||||||
|
value: ptr::null(),
|
||||||
|
};
|
||||||
|
max_length
|
||||||
|
];
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let count = list_instance(infos.as_mut_ptr(), max_length);
|
||||||
|
if count < 0 {
|
||||||
|
if let Some(error) = get_last_error() {
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ret = serde_json::Map::new();
|
||||||
|
for info in infos.iter().take(count as usize) {
|
||||||
|
let key_ptr = info.key;
|
||||||
|
let val_ptr = info.value;
|
||||||
|
if key_ptr.is_null() || val_ptr.is_null() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
|
||||||
|
let val = CStr::from_ptr(val_ptr).to_string_lossy().into_owned();
|
||||||
|
free_string(key_ptr);
|
||||||
|
free_string(val_ptr);
|
||||||
|
ret.insert(key, serde_json::Value::String(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_str = serde_json::Value::Object(ret).to_string();
|
||||||
|
match env.new_string(&json_str) {
|
||||||
|
Ok(jstr) => jstr.into_raw(),
|
||||||
|
Err(_) => {
|
||||||
|
throw_exception(&mut env, "Failed to create instance list JSON string");
|
||||||
|
ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
use std::ffi::CString;
|
||||||
|
|
||||||
|
use jni::JNIEnv;
|
||||||
|
use jni::objects::JString;
|
||||||
|
|
||||||
|
pub(crate) fn jstring_to_cstring(env: &mut JNIEnv, jstr: &JString) -> Result<CString, String> {
|
||||||
|
let java_str = env
|
||||||
|
.get_string(jstr)
|
||||||
|
.map_err(|e| format!("Failed to get string: {:?}", e))?;
|
||||||
|
let rust_str = java_str.to_str().map_err(|_| "Invalid UTF-8".to_string())?;
|
||||||
|
CString::new(rust_str).map_err(|_| "String contains null byte".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn optional_jstring_to_cstring(
|
||||||
|
env: &mut JNIEnv,
|
||||||
|
jstr: &JString,
|
||||||
|
) -> Result<Option<CString>, String> {
|
||||||
|
if jstr.is_null() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
jstring_to_cstring(env, jstr).map(Some)
|
||||||
|
}
|
||||||
@@ -4,14 +4,25 @@ version = "0.1.0"
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["c-abi", "ffi-dataplane"]
|
||||||
|
c-abi = []
|
||||||
|
ffi-dataplane = ["easytier/ffi-dataplane"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
easytier = { path = "../../easytier" }
|
easytier = { path = "../../easytier" }
|
||||||
|
|
||||||
once_cell = "1.18.0"
|
once_cell = "1.18.0"
|
||||||
dashmap = "6.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 = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
uuid = "1.17.0"
|
uuid = "1.17.0"
|
||||||
|
tokio-util = "0.7"
|
||||||
|
|||||||
@@ -0,0 +1,429 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <unistd.h> // for sleep
|
||||||
|
|
||||||
|
// FFI struct and function declarations
|
||||||
|
typedef struct {
|
||||||
|
const char* key;
|
||||||
|
const char* value;
|
||||||
|
} KeyValuePair;
|
||||||
|
|
||||||
|
typedef void (*config_server_event_callback)(
|
||||||
|
const char* event_json,
|
||||||
|
void* user_data
|
||||||
|
);
|
||||||
|
|
||||||
|
extern int parse_config(const char* cfg_str);
|
||||||
|
extern int run_network_instance(const char* cfg_str);
|
||||||
|
extern void get_error_msg(const char** out);
|
||||||
|
extern void free_string(const char* s);
|
||||||
|
extern int collect_network_infos(KeyValuePair* infos, size_t max_length);
|
||||||
|
extern int start_config_server_client(
|
||||||
|
const char* config_server_url,
|
||||||
|
const char* hostname,
|
||||||
|
const char* machine_id,
|
||||||
|
bool secure_mode,
|
||||||
|
config_server_event_callback callback,
|
||||||
|
void* user_data
|
||||||
|
);
|
||||||
|
extern int stop_config_server_client(void);
|
||||||
|
extern int is_config_server_client_connected(void);
|
||||||
|
|
||||||
|
static void on_config_server_event(const char* event_json, void* user_data) {
|
||||||
|
(void)user_data;
|
||||||
|
printf("config server event: %s\n", event_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
const char* config = "inst_name = \"test\"\nnetwork = \"test_network\"\n";
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
// 调用 parse_config
|
||||||
|
ret = parse_config(config);
|
||||||
|
if (ret != 0) {
|
||||||
|
const char* err = NULL;
|
||||||
|
get_error_msg(&err);
|
||||||
|
if (err) {
|
||||||
|
printf("parse_config error: %s\n", err);
|
||||||
|
free_string(err);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("parse_config success\n");
|
||||||
|
|
||||||
|
// 调用 run_network_instance
|
||||||
|
ret = run_network_instance(config);
|
||||||
|
if (ret != 0) {
|
||||||
|
const char* err = NULL;
|
||||||
|
get_error_msg(&err);
|
||||||
|
if (err) {
|
||||||
|
printf("run_network_instance error: %s\n", err);
|
||||||
|
free_string(err);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("run_network_instance success\n");
|
||||||
|
|
||||||
|
// 周期性调用 collect_network_infos 并打印
|
||||||
|
const size_t max_infos = 8;
|
||||||
|
KeyValuePair* infos = (KeyValuePair*)malloc(sizeof(KeyValuePair) * max_infos);
|
||||||
|
if (!infos) {
|
||||||
|
fprintf(stderr, "malloc failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; ++i) { // 循环5次作为示例
|
||||||
|
memset(infos, 0, sizeof(KeyValuePair) * max_infos);
|
||||||
|
int count = collect_network_infos(infos, max_infos);
|
||||||
|
if (count < 0) {
|
||||||
|
const char* err = NULL;
|
||||||
|
get_error_msg(&err);
|
||||||
|
if (err) {
|
||||||
|
printf("collect_network_infos error: %s\n", err);
|
||||||
|
free_string(err);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
printf("collect_network_infos: %d instance(s)\n", count);
|
||||||
|
for (int j = 0; j < count; ++j) {
|
||||||
|
printf(" [%d] key: %s\n value: %s\n", j, infos[j].key, infos[j].value);
|
||||||
|
free_string(infos[j].key);
|
||||||
|
free_string(infos[j].value);
|
||||||
|
}
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
free(infos);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,593 @@
|
|||||||
|
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
@@ -0,0 +1,360 @@
|
|||||||
|
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():
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module easytierffi-example
|
||||||
|
|
||||||
|
go 1.25
|
||||||
|
|
||||||
|
require github.com/go-webgpu/goffi v0.4.1
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/go-webgpu/goffi v0.4.1 h1:2hQH5XXloxTyTtIleYv+Rajlwzp6UOETURhSZ5+zJxU=
|
||||||
|
github.com/go-webgpu/goffi v0.4.1/go.mod h1:wfoxNsJkU+5RFbV1kNN1kunhc1lFHuJKK3zpgx08/uM=
|
||||||
@@ -0,0 +1,575 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,928 @@
|
|||||||
|
#[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
@@ -0,0 +1,65 @@
|
|||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
ffi::{CString, c_char},
|
||||||
|
};
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
// # Thread Safety
|
||||||
|
// set_error_msg and get_error_msg must be called on the same thread to
|
||||||
|
// get correct error. And since `Handle::block_on` polls the top-level
|
||||||
|
// future on the calling thread, set_error_msg always runs on the same
|
||||||
|
// thread as the corresponding get_error_msg.
|
||||||
|
static ERROR_MSG: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_error_msg(msg: &str) {
|
||||||
|
ERROR_MSG.with(|cell| {
|
||||||
|
let mut buf = cell.borrow_mut();
|
||||||
|
buf.clear();
|
||||||
|
buf.extend_from_slice(msg.as_bytes());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn thread_local_error_msg() -> Option<String> {
|
||||||
|
ERROR_MSG.with(|cell| {
|
||||||
|
let buf = cell.borrow();
|
||||||
|
if buf.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(String::from_utf8_lossy(&buf).into_owned())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) unsafe fn get_error_msg(out: *mut *const c_char) {
|
||||||
|
let msg = match (
|
||||||
|
thread_local_error_msg(),
|
||||||
|
crate::config_server::last_callback_error(),
|
||||||
|
) {
|
||||||
|
(Some(error), Some(callback_error)) => Some(format!(
|
||||||
|
"{}; config server callback error: {}",
|
||||||
|
error, callback_error
|
||||||
|
)),
|
||||||
|
(Some(error), None) => Some(error),
|
||||||
|
(None, Some(callback_error)) => {
|
||||||
|
Some(format!("config server callback error: {}", callback_error))
|
||||||
|
}
|
||||||
|
(None, None) => None,
|
||||||
|
};
|
||||||
|
let cstr = msg.and_then(|msg| CString::new(msg).ok());
|
||||||
|
unsafe {
|
||||||
|
*out = match cstr {
|
||||||
|
Some(s) => s.into_raw() as *const c_char,
|
||||||
|
None => std::ptr::null(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn free_string(s: *const c_char) {
|
||||||
|
if s.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
let _ = CString::from_raw(s as *mut c_char);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
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
@@ -0,0 +1,54 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
use std::ffi::{CStr, c_char};
|
||||||
|
|
||||||
|
pub(crate) unsafe fn c_str_to_string(ptr: *const c_char, name: &str) -> Result<String, String> {
|
||||||
|
if ptr.is_null() {
|
||||||
|
return Err(format!("{} is null", name));
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe { CStr::from_ptr(ptr) }
|
||||||
|
.to_str()
|
||||||
|
.map(|value| value.to_string())
|
||||||
|
.map_err(|err| format!("{} is not valid UTF-8: {}", name, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) unsafe fn optional_c_str_to_string(
|
||||||
|
ptr: *const c_char,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Option<String>, String> {
|
||||||
|
if ptr.is_null() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe { c_str_to_string(ptr, name) }.map(Some)
|
||||||
|
}
|
||||||
@@ -0,0 +1,766 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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)>;
|
||||||
@@ -99,7 +99,7 @@ while true; do
|
|||||||
# 启动后的扫尾工作
|
# 启动后的扫尾工作
|
||||||
if pgrep -f "${EASYTIER}" >/dev/null; then
|
if pgrep -f "${EASYTIER}" >/dev/null; then
|
||||||
|
|
||||||
if ! ip rule show | grep -q "lookup main"; then
|
if ! ip rule show | grep -qE '^[0-9]+:[[:space:]]+from all lookup main$'; then
|
||||||
ip rule add from all lookup main
|
ip rule add from all lookup main
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
id=easytier_magisk
|
id=easytier_magisk
|
||||||
name=EasyTier_Magisk
|
name=EasyTier_Magisk
|
||||||
version=v2.6.2
|
version=v2.6.4
|
||||||
versionCode=1
|
versionCode=1
|
||||||
author=EasyTier
|
author=EasyTier
|
||||||
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
|
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
|
||||||
|
|||||||
+709
-220
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,10 @@ edition = "2024"
|
|||||||
crate-type=["cdylib"]
|
crate-type=["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ohos-hilog-binding = {version = "*", features = ["redirect"]}
|
async-trait = "0.1"
|
||||||
|
base64 = "0.22"
|
||||||
|
flate2 = "1.1"
|
||||||
|
gethostname = "1.1"
|
||||||
easytier = { path = "../../easytier" }
|
easytier = { path = "../../easytier" }
|
||||||
napi-derive-ohos = "1.1"
|
napi-derive-ohos = "1.1"
|
||||||
napi-ohos = { version = "1.1", default-features = false, features = [
|
napi-ohos = { version = "1.1", default-features = false, features = [
|
||||||
@@ -26,10 +29,16 @@ napi-ohos = { version = "1.1", default-features = false, features = [
|
|||||||
"web_stream",
|
"web_stream",
|
||||||
] }
|
] }
|
||||||
once_cell = "1.21.3"
|
once_cell = "1.21.3"
|
||||||
|
ipnet = "2.10"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0.125"
|
serde_json = "1.0.125"
|
||||||
|
prost-reflect = { version = "0.14.5", default-features = false, features = ["derive"] }
|
||||||
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.19"
|
||||||
tracing-core = "0.1.33"
|
tracing-core = "0.1.33"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
|
||||||
|
url = "2.5"
|
||||||
uuid = { version = "1.5.0", features = [
|
uuid = { version = "1.5.0", features = [
|
||||||
"v4",
|
"v4",
|
||||||
"fast-rng",
|
"fast-rng",
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pub(crate) mod repository;
|
||||||
|
pub(crate) mod services;
|
||||||
|
pub(crate) mod storage;
|
||||||
|
pub(crate) mod types;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#[path = "../../config_repo/field_store.rs"]
|
||||||
|
mod field_store;
|
||||||
|
#[path = "../../config_repo/import_export.rs"]
|
||||||
|
mod import_export;
|
||||||
|
#[path = "../../config_repo/legacy_migration.rs"]
|
||||||
|
mod legacy_migration;
|
||||||
|
#[path = "../../config_repo/validation.rs"]
|
||||||
|
mod validation;
|
||||||
|
|
||||||
|
#[path = "../../config_repo.rs"]
|
||||||
|
mod repo;
|
||||||
|
|
||||||
|
pub use repo::*;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub(crate) mod schema_service;
|
||||||
|
pub(crate) mod share_link_service;
|
||||||
@@ -0,0 +1,414 @@
|
|||||||
|
use easytier::proto::ALL_DESCRIPTOR_BYTES;
|
||||||
|
use napi_derive_ohos::napi;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use prost_reflect::{Cardinality, DescriptorPool, FieldDescriptor, Kind, MessageDescriptor};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct FieldOption {
|
||||||
|
pub label: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct ValidationRule {
|
||||||
|
pub rule_type: String,
|
||||||
|
pub arg: String,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct NetworkConfigSchema {
|
||||||
|
pub node_kind: String,
|
||||||
|
pub name: String,
|
||||||
|
pub field_number: i32,
|
||||||
|
pub type_name: Option<String>,
|
||||||
|
pub semantic_type: Option<String>,
|
||||||
|
pub value_kind: String,
|
||||||
|
pub is_list: bool,
|
||||||
|
pub required: bool,
|
||||||
|
pub default_value_text: Option<String>,
|
||||||
|
pub enum_options: Vec<FieldOption>,
|
||||||
|
pub validations: Vec<ValidationRule>,
|
||||||
|
pub children: Vec<NetworkConfigSchema>,
|
||||||
|
pub definitions: Vec<NetworkConfigSchema>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct ConfigFieldMapping {
|
||||||
|
pub field_name: String,
|
||||||
|
pub field_number: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
static DESCRIPTOR_POOL: Lazy<DescriptorPool> = Lazy::new(|| {
|
||||||
|
DescriptorPool::decode(ALL_DESCRIPTOR_BYTES)
|
||||||
|
.expect("easytier descriptor pool should decode from embedded protobuf descriptors")
|
||||||
|
});
|
||||||
|
|
||||||
|
const NETWORK_CONFIG_MESSAGE_NAME: &str = "api.manage.NetworkConfig";
|
||||||
|
|
||||||
|
fn descriptor_pool() -> &'static DescriptorPool {
|
||||||
|
&DESCRIPTOR_POOL
|
||||||
|
}
|
||||||
|
|
||||||
|
fn network_config_descriptor() -> MessageDescriptor {
|
||||||
|
descriptor_pool()
|
||||||
|
.get_message_by_name(NETWORK_CONFIG_MESSAGE_NAME)
|
||||||
|
.expect("api.manage.NetworkConfig descriptor should exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_default_value_text(field: &FieldDescriptor) -> Option<String> {
|
||||||
|
if field.is_list() || field.is_map() {
|
||||||
|
return Some("[]".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
match field.kind() {
|
||||||
|
Kind::Bool => Some("false".to_string()),
|
||||||
|
Kind::String => Some("\"\"".to_string()),
|
||||||
|
Kind::Bytes => Some("\"\"".to_string()),
|
||||||
|
Kind::Int32
|
||||||
|
| Kind::Sint32
|
||||||
|
| Kind::Sfixed32
|
||||||
|
| Kind::Int64
|
||||||
|
| Kind::Sint64
|
||||||
|
| Kind::Sfixed64
|
||||||
|
| Kind::Uint32
|
||||||
|
| Kind::Fixed32
|
||||||
|
| Kind::Uint64
|
||||||
|
| Kind::Fixed64
|
||||||
|
| Kind::Float
|
||||||
|
| Kind::Double => Some("0".to_string()),
|
||||||
|
Kind::Enum(enum_desc) => enum_desc
|
||||||
|
.get_value(0)
|
||||||
|
.map(|value| value.number().to_string()),
|
||||||
|
Kind::Message(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_type_name(field: &FieldDescriptor) -> Option<String> {
|
||||||
|
match field.kind() {
|
||||||
|
Kind::Enum(enum_desc) => Some(enum_desc.full_name().to_string()),
|
||||||
|
Kind::Message(message_desc) => Some(message_desc.full_name().to_string()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_semantic_type(field: &FieldDescriptor) -> Option<String> {
|
||||||
|
match field.name() {
|
||||||
|
"virtual_ipv4" => Some("cidr_ip".to_string()),
|
||||||
|
"network_length" => Some("cidr_mask".to_string()),
|
||||||
|
"peer_urls" => Some("peer[]".to_string()),
|
||||||
|
"proxy_cidrs" => Some("cidr[]".to_string()),
|
||||||
|
"listener_urls" => Some("listener[]".to_string()),
|
||||||
|
"routes" => Some("route[]".to_string()),
|
||||||
|
"exit_nodes" => Some("ip[]".to_string()),
|
||||||
|
"relay_network_whitelist" => Some("network_name[]".to_string()),
|
||||||
|
"mapped_listeners" => Some("mapped_listener[]".to_string()),
|
||||||
|
"port_forwards" => Some("port_forward[]".to_string()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enum_options(kind: Kind) -> Vec<FieldOption> {
|
||||||
|
match kind {
|
||||||
|
Kind::Enum(enum_desc) => enum_desc
|
||||||
|
.values()
|
||||||
|
.map(|value| FieldOption {
|
||||||
|
label: value.name().to_string(),
|
||||||
|
value: value.number().to_string(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_expose_field(field: &FieldDescriptor) -> bool {
|
||||||
|
match field.containing_oneof() {
|
||||||
|
Some(_) => field
|
||||||
|
.field_descriptor_proto()
|
||||||
|
.proto3_optional
|
||||||
|
.unwrap_or(false),
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_validations(field: &FieldDescriptor) -> Vec<ValidationRule> {
|
||||||
|
if field.cardinality() == Cardinality::Required {
|
||||||
|
return vec![ValidationRule {
|
||||||
|
rule_type: "required".to_string(),
|
||||||
|
arg: String::new(),
|
||||||
|
message: format!("{} is required", field.name()),
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind_to_value_kind(field: &FieldDescriptor) -> String {
|
||||||
|
if field.is_map() {
|
||||||
|
return "object".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
match field.kind() {
|
||||||
|
Kind::Bool => "boolean".to_string(),
|
||||||
|
Kind::String | Kind::Bytes => "string".to_string(),
|
||||||
|
Kind::Int32
|
||||||
|
| Kind::Sint32
|
||||||
|
| Kind::Sfixed32
|
||||||
|
| Kind::Int64
|
||||||
|
| Kind::Sint64
|
||||||
|
| Kind::Sfixed64
|
||||||
|
| Kind::Uint32
|
||||||
|
| Kind::Fixed32
|
||||||
|
| Kind::Uint64
|
||||||
|
| Kind::Fixed64
|
||||||
|
| Kind::Float
|
||||||
|
| Kind::Double => "number".to_string(),
|
||||||
|
Kind::Enum(_) => "enum".to_string(),
|
||||||
|
Kind::Message(_) => "object".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_node(
|
||||||
|
node_kind: &str,
|
||||||
|
name: String,
|
||||||
|
field_number: i32,
|
||||||
|
type_name: Option<String>,
|
||||||
|
semantic_type: Option<String>,
|
||||||
|
value_kind: String,
|
||||||
|
is_list: bool,
|
||||||
|
required: bool,
|
||||||
|
default_value_text: Option<String>,
|
||||||
|
enum_options: Vec<FieldOption>,
|
||||||
|
validations: Vec<ValidationRule>,
|
||||||
|
children: Vec<NetworkConfigSchema>,
|
||||||
|
definitions: Vec<NetworkConfigSchema>,
|
||||||
|
) -> NetworkConfigSchema {
|
||||||
|
NetworkConfigSchema {
|
||||||
|
node_kind: node_kind.to_string(),
|
||||||
|
name,
|
||||||
|
field_number,
|
||||||
|
type_name,
|
||||||
|
semantic_type,
|
||||||
|
value_kind,
|
||||||
|
is_list,
|
||||||
|
required,
|
||||||
|
default_value_text,
|
||||||
|
enum_options,
|
||||||
|
validations,
|
||||||
|
children,
|
||||||
|
definitions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_map_entry_node(message_desc: &MessageDescriptor) -> NetworkConfigSchema {
|
||||||
|
let key_field = message_desc.map_entry_key_field();
|
||||||
|
let value_field = message_desc.map_entry_value_field();
|
||||||
|
|
||||||
|
build_node(
|
||||||
|
"object",
|
||||||
|
message_desc.name().to_string(),
|
||||||
|
0,
|
||||||
|
Some(message_desc.full_name().to_string()),
|
||||||
|
None,
|
||||||
|
"object".to_string(),
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
None,
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
vec![
|
||||||
|
build_schema_field_node(&key_field),
|
||||||
|
build_schema_field_node(&value_field),
|
||||||
|
],
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_children(field: &FieldDescriptor) -> Vec<NetworkConfigSchema> {
|
||||||
|
if field.is_map() {
|
||||||
|
if let Kind::Message(message_desc) = field.kind() {
|
||||||
|
return vec![build_map_entry_node(&message_desc)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match field.kind() {
|
||||||
|
Kind::Message(message_desc) => build_message_children(&message_desc),
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_message_children(message_desc: &MessageDescriptor) -> Vec<NetworkConfigSchema> {
|
||||||
|
message_desc
|
||||||
|
.fields()
|
||||||
|
.filter(should_expose_field)
|
||||||
|
.map(|field| build_schema_field_node(&field))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_schema_field_node(field: &FieldDescriptor) -> NetworkConfigSchema {
|
||||||
|
build_node(
|
||||||
|
"field",
|
||||||
|
field.name().to_string(),
|
||||||
|
field.number() as i32,
|
||||||
|
field_type_name(field),
|
||||||
|
field_semantic_type(field),
|
||||||
|
kind_to_value_kind(field),
|
||||||
|
field.is_list() || field.is_map(),
|
||||||
|
field.cardinality() == Cardinality::Required,
|
||||||
|
field_default_value_text(field),
|
||||||
|
enum_options(field.kind()),
|
||||||
|
build_validations(field),
|
||||||
|
field_children(field),
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_definitions() -> Vec<NetworkConfigSchema> {
|
||||||
|
let mut definitions = Vec::new();
|
||||||
|
|
||||||
|
for message_desc in descriptor_pool().all_messages() {
|
||||||
|
let full_name = message_desc.full_name();
|
||||||
|
if full_name == NETWORK_CONFIG_MESSAGE_NAME || message_desc.is_map_entry() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
definitions.push(build_node(
|
||||||
|
"object",
|
||||||
|
full_name.to_string(),
|
||||||
|
0,
|
||||||
|
Some(full_name.to_string()),
|
||||||
|
None,
|
||||||
|
"object".to_string(),
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
None,
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
build_message_children(&message_desc),
|
||||||
|
Vec::new(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for enum_desc in descriptor_pool().all_enums() {
|
||||||
|
definitions.push(build_node(
|
||||||
|
"enum",
|
||||||
|
enum_desc.full_name().to_string(),
|
||||||
|
0,
|
||||||
|
Some(enum_desc.full_name().to_string()),
|
||||||
|
None,
|
||||||
|
"enum".to_string(),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
enum_options(Kind::Enum(enum_desc.clone())),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
definitions.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
definitions
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_network_config_schema() -> NetworkConfigSchema {
|
||||||
|
let network_config = network_config_descriptor();
|
||||||
|
build_node(
|
||||||
|
"schema",
|
||||||
|
network_config.name().to_string(),
|
||||||
|
0,
|
||||||
|
Some(network_config.full_name().to_string()),
|
||||||
|
None,
|
||||||
|
"object".to_string(),
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
None,
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
build_message_children(&network_config),
|
||||||
|
collect_definitions(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_network_config_field_mappings() -> Vec<ConfigFieldMapping> {
|
||||||
|
network_config_descriptor()
|
||||||
|
.fields()
|
||||||
|
.filter(should_expose_field)
|
||||||
|
.map(|field| ConfigFieldMapping {
|
||||||
|
field_name: field.name().to_string(),
|
||||||
|
field_number: field.number() as i32,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_network_config_schema() -> NetworkConfigSchema {
|
||||||
|
build_network_config_schema()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_network_config_field_mappings() -> Vec<ConfigFieldMapping> {
|
||||||
|
build_network_config_field_mappings()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn schema_is_exposed_as_single_tree_type() {
|
||||||
|
let schema = get_network_config_schema();
|
||||||
|
assert_eq!(schema.node_kind, "schema");
|
||||||
|
assert_eq!(schema.name, "NetworkConfig");
|
||||||
|
assert_eq!(
|
||||||
|
schema.type_name.as_deref(),
|
||||||
|
Some("api.manage.NetworkConfig")
|
||||||
|
);
|
||||||
|
|
||||||
|
let virtual_ipv4 = schema
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.find(|field| field.name == "virtual_ipv4")
|
||||||
|
.expect("virtual_ipv4 field");
|
||||||
|
assert_eq!(virtual_ipv4.semantic_type.as_deref(), Some("cidr_ip"));
|
||||||
|
|
||||||
|
let secure_mode = schema
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.find(|field| field.name == "secure_mode")
|
||||||
|
.expect("secure_mode field");
|
||||||
|
assert!(
|
||||||
|
secure_mode
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.any(|field| field.name == "enabled")
|
||||||
|
);
|
||||||
|
|
||||||
|
let secure_mode_definition = schema
|
||||||
|
.definitions
|
||||||
|
.iter()
|
||||||
|
.find(|definition| definition.name == "common.SecureModeConfig")
|
||||||
|
.expect("secure mode definition");
|
||||||
|
assert!(
|
||||||
|
secure_mode_definition
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.any(|field| field.name == "local_private_key")
|
||||||
|
);
|
||||||
|
|
||||||
|
let networking_method_definition = schema
|
||||||
|
.definitions
|
||||||
|
.iter()
|
||||||
|
.find(|definition| definition.name == "api.manage.NetworkingMethod")
|
||||||
|
.expect("networking method enum definition");
|
||||||
|
assert!(
|
||||||
|
networking_method_definition
|
||||||
|
.enum_options
|
||||||
|
.iter()
|
||||||
|
.any(|option| option.label == "PublicServer")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
use crate::config::repository::{get_config_record, save_config_record};
|
||||||
|
use crate::config::services::schema_service::get_network_config_field_mappings;
|
||||||
|
use crate::config::types::stored_config::SharedConfigLinkPayload;
|
||||||
|
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||||
|
use easytier::proto::api::manage::NetworkConfig;
|
||||||
|
use flate2::{Compression, read::ZlibDecoder, write::ZlibEncoder};
|
||||||
|
use gethostname::gethostname;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
const SHARE_LINK_HOST: &str = "easytier.cn";
|
||||||
|
const SHARE_LINK_PATH: &str = "/comp_cfg";
|
||||||
|
|
||||||
|
fn field_name_to_id_map() -> HashMap<String, String> {
|
||||||
|
get_network_config_field_mappings()
|
||||||
|
.into_iter()
|
||||||
|
.map(|mapping| (mapping.field_name, mapping.field_number.to_string()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_id_to_name_map() -> HashMap<String, String> {
|
||||||
|
get_network_config_field_mappings()
|
||||||
|
.into_iter()
|
||||||
|
.map(|mapping| (mapping.field_number.to_string(), mapping.field_name))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prune_empty(value: &serde_json::Value) -> Option<serde_json::Value> {
|
||||||
|
match value {
|
||||||
|
serde_json::Value::Null => None,
|
||||||
|
serde_json::Value::Array(values) if values.is_empty() => None,
|
||||||
|
_ => Some(value.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_config_json(config: &NetworkConfig) -> Result<String, String> {
|
||||||
|
let field_name_to_id = field_name_to_id_map();
|
||||||
|
let raw = serde_json::to_value(config).map_err(|err| err.to_string())?;
|
||||||
|
let mut mapped = serde_json::Map::new();
|
||||||
|
|
||||||
|
for (key, value) in raw.as_object().cloned().unwrap_or_default() {
|
||||||
|
let Some(value) = prune_empty(&value) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let mapped_key = field_name_to_id.get(&key).cloned().unwrap_or(key);
|
||||||
|
mapped.insert(mapped_key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
serde_json::to_string(&mapped).map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unmap_config_json(raw: &str) -> Result<NetworkConfig, String> {
|
||||||
|
let field_id_to_name = field_id_to_name_map();
|
||||||
|
let value = serde_json::from_str::<serde_json::Value>(raw).map_err(|err| err.to_string())?;
|
||||||
|
let mut mapped = serde_json::Map::new();
|
||||||
|
for (key, value) in value.as_object().cloned().unwrap_or_default() {
|
||||||
|
let field_name = field_id_to_name.get(&key).cloned().unwrap_or(key);
|
||||||
|
mapped.insert(field_name, value);
|
||||||
|
}
|
||||||
|
serde_json::from_value(serde_json::Value::Object(mapped)).map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compress_to_base64url(raw: &str) -> Result<String, String> {
|
||||||
|
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
|
||||||
|
encoder
|
||||||
|
.write_all(raw.as_bytes())
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
let compressed = encoder.finish().map_err(|err| err.to_string())?;
|
||||||
|
Ok(URL_SAFE_NO_PAD.encode(compressed))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decompress_from_base64url(raw: &str) -> Result<String, String> {
|
||||||
|
let compressed = URL_SAFE_NO_PAD.decode(raw).map_err(|err| err.to_string())?;
|
||||||
|
let mut decoder = ZlibDecoder::new(compressed.as_slice());
|
||||||
|
let mut out = String::new();
|
||||||
|
decoder
|
||||||
|
.read_to_string(&mut out)
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_config_share_link(
|
||||||
|
config_id: &str,
|
||||||
|
display_name: Option<String>,
|
||||||
|
only_start: bool,
|
||||||
|
) -> Option<String> {
|
||||||
|
let record = get_config_record(config_id)?;
|
||||||
|
let config = serde_json::from_str::<NetworkConfig>(&record.config_json).ok()?;
|
||||||
|
let mapped_json = map_config_json(&config).ok()?;
|
||||||
|
let compressed = compress_to_base64url(&mapped_json).ok()?;
|
||||||
|
let final_name = display_name
|
||||||
|
.or(Some(record.meta.display_name))
|
||||||
|
.filter(|name| !name.is_empty());
|
||||||
|
|
||||||
|
let mut url = Url::parse(&format!("https://{SHARE_LINK_HOST}{SHARE_LINK_PATH}")).ok()?;
|
||||||
|
url.query_pairs_mut().append_pair("cfg", &compressed);
|
||||||
|
if let Some(name) = final_name {
|
||||||
|
url.query_pairs_mut().append_pair("name", &name);
|
||||||
|
}
|
||||||
|
if only_start {
|
||||||
|
url.query_pairs_mut().append_pair("only_start", "true");
|
||||||
|
}
|
||||||
|
Some(url.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_config_share_link(share_link: &str) -> Option<SharedConfigLinkPayload> {
|
||||||
|
let url = Url::parse(share_link).ok()?;
|
||||||
|
if url.host_str()? != SHARE_LINK_HOST || url.path() != SHARE_LINK_PATH {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cfg = url
|
||||||
|
.query_pairs()
|
||||||
|
.find(|(key, _)| key == "cfg")?
|
||||||
|
.1
|
||||||
|
.to_string();
|
||||||
|
let mapped_json = decompress_from_base64url(&cfg).ok()?;
|
||||||
|
let mut config = unmap_config_json(&mapped_json).ok()?;
|
||||||
|
config.instance_id = Some(Uuid::new_v4().to_string());
|
||||||
|
let hostname = gethostname().to_string_lossy().to_string();
|
||||||
|
if !hostname.is_empty() {
|
||||||
|
config.hostname = Some(hostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
let config_json = serde_json::to_string(&config).ok()?;
|
||||||
|
let display_name = url
|
||||||
|
.query_pairs()
|
||||||
|
.find(|(key, _)| key == "name")
|
||||||
|
.map(|(_, value)| value.to_string())
|
||||||
|
.filter(|name| !name.is_empty());
|
||||||
|
let only_start = url
|
||||||
|
.query_pairs()
|
||||||
|
.find(|(key, _)| key == "only_start")
|
||||||
|
.map(|(_, value)| value == "true")
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
Some(SharedConfigLinkPayload {
|
||||||
|
config_json,
|
||||||
|
display_name,
|
||||||
|
only_start,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_config_share_link(
|
||||||
|
share_link: &str,
|
||||||
|
display_name_override: Option<String>,
|
||||||
|
) -> Option<String> {
|
||||||
|
let payload = parse_config_share_link(share_link)?;
|
||||||
|
let config = serde_json::from_str::<NetworkConfig>(&payload.config_json).ok()?;
|
||||||
|
let config_id = config.instance_id.clone()?;
|
||||||
|
let display_name = display_name_override
|
||||||
|
.filter(|name| !name.is_empty())
|
||||||
|
.or(payload.display_name)
|
||||||
|
.unwrap_or_else(|| config_id.clone());
|
||||||
|
|
||||||
|
save_config_record(config_id.clone(), display_name, payload.config_json)?;
|
||||||
|
Some(config_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::config_repo::{create_config_record, init_config_store};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
fn test_root() -> String {
|
||||||
|
let unique = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
std::env::temp_dir()
|
||||||
|
.join(format!("easytier_ohrs_share_test_{unique}"))
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn share_link_roundtrip_works() {
|
||||||
|
assert!(init_config_store(test_root()));
|
||||||
|
create_config_record("cfg-share".to_string(), "share-demo".to_string())
|
||||||
|
.expect("create config");
|
||||||
|
|
||||||
|
let link = build_config_share_link("cfg-share", None, true).expect("share link");
|
||||||
|
let payload = parse_config_share_link(&link).expect("parse link");
|
||||||
|
let config =
|
||||||
|
serde_json::from_str::<NetworkConfig>(&payload.config_json).expect("config json");
|
||||||
|
|
||||||
|
assert!(payload.only_start);
|
||||||
|
assert_eq!(payload.display_name.as_deref(), Some("share-demo"));
|
||||||
|
assert_ne!(config.instance_id.as_deref(), Some("cfg-share"));
|
||||||
|
|
||||||
|
let imported_id = import_config_share_link(&link, None).expect("import link");
|
||||||
|
assert_ne!(imported_id, "cfg-share");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,777 @@
|
|||||||
|
use crate::config::types::stored_config::{
|
||||||
|
SnapshotImportResult, StoredConfigList, StoredConfigMeta,
|
||||||
|
};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
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::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,
|
||||||
|
display_name: String,
|
||||||
|
created_at: String,
|
||||||
|
updated_at: String,
|
||||||
|
favorite: bool,
|
||||||
|
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)
|
||||||
|
.map(|d| d.as_secs().to_string())
|
||||||
|
.unwrap_or_else(|_| "0".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_file_path() -> Option<PathBuf> {
|
||||||
|
CONFIG_DB_PATH
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|guard| guard.as_ref().cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_schema(conn: &Connection) -> rusqlite::Result<()> {
|
||||||
|
conn.execute_batch(
|
||||||
|
"PRAGMA foreign_keys = ON;
|
||||||
|
CREATE TABLE IF NOT EXISTS stored_configs (
|
||||||
|
config_id TEXT PRIMARY KEY,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
favorite INTEGER NOT NULL DEFAULT 0,
|
||||||
|
temporary INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS stored_config_fields (
|
||||||
|
config_id TEXT NOT NULL,
|
||||||
|
field_name TEXT NOT NULL,
|
||||||
|
field_json TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (config_id, field_name),
|
||||||
|
FOREIGN KEY (config_id) REFERENCES stored_configs(config_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_stored_config_fields_config_id
|
||||||
|
ON stored_config_fields(config_id);",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(e) => {
|
||||||
|
ohrs_log_error!("[Rust] failed to open config db {}: {}", path.display(), e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = init_schema(&conn) {
|
||||||
|
ohrs_log_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)?,
|
||||||
|
display_name: row.get(1)?,
|
||||||
|
created_at: row.get(2)?,
|
||||||
|
updated_at: row.get(3)?,
|
||||||
|
favorite: row.get::<_, i64>(4)? != 0,
|
||||||
|
temporary: row.get::<_, i64>(5)? != 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_meta_record(conn: &Connection, config_id: &str) -> Option<StoredConfigMetaRecord> {
|
||||||
|
conn.query_row(
|
||||||
|
"SELECT config_id, display_name, created_at, updated_at, favorite, temporary
|
||||||
|
FROM stored_configs WHERE config_id = ?1",
|
||||||
|
params![config_id],
|
||||||
|
row_to_meta,
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_snapshot_schema(conn: &Connection) -> bool {
|
||||||
|
let has_stored_configs = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'stored_configs'",
|
||||||
|
[],
|
||||||
|
|row| row.get::<_, i64>(0),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.is_some();
|
||||||
|
let has_stored_fields = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'stored_config_fields'",
|
||||||
|
[],
|
||||||
|
|row| row.get::<_, i64>(0),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.is_some();
|
||||||
|
has_stored_configs && has_stored_fields
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_snapshot_tables(
|
||||||
|
src: &Connection,
|
||||||
|
) -> rusqlite::Result<(Vec<StoredConfigMetaRecord>, Vec<SnapshotFieldRow>)> {
|
||||||
|
src.execute_batch("BEGIN DEFERRED TRANSACTION")?;
|
||||||
|
|
||||||
|
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?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})();
|
||||||
|
|
||||||
|
match read_result {
|
||||||
|
Ok(()) => {
|
||||||
|
src.execute_batch("COMMIT")?;
|
||||||
|
Ok((meta_rows, field_rows))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let _ = src.execute_batch("ROLLBACK");
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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", [])?;
|
||||||
|
|
||||||
|
for row in meta_rows {
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO stored_configs (
|
||||||
|
config_id, display_name, created_at, updated_at, favorite, temporary
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||||
|
params![
|
||||||
|
row.config_id,
|
||||||
|
row.display_name,
|
||||||
|
row.created_at,
|
||||||
|
row.updated_at,
|
||||||
|
if row.favorite { 1 } else { 0 },
|
||||||
|
if row.temporary { 1 } else { 0 }
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (config_id, field_name, field_json, updated_at) in field_rows {
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO stored_config_fields (config_id, field_name, field_json, updated_at)
|
||||||
|
VALUES (?1, ?2, ?3, ?4)",
|
||||||
|
params![config_id, field_name, field_json, updated_at],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
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!(
|
||||||
|
"[Rust] failed to create snapshot parent {}: {}",
|
||||||
|
parent.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_meta(record: StoredConfigMetaRecord) -> StoredConfigMeta {
|
||||||
|
StoredConfigMeta {
|
||||||
|
config_id: record.config_id,
|
||||||
|
display_name: record.display_name,
|
||||||
|
created_at: record.created_at,
|
||||||
|
updated_at: record.updated_at,
|
||||||
|
favorite: record.favorite,
|
||||||
|
temporary: record.temporary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_config_meta_store(root_dir: String) -> bool {
|
||||||
|
let root = PathBuf::from(root_dir);
|
||||||
|
if let Err(e) = std::fs::create_dir_all(&root) {
|
||||||
|
ohrs_log_error!(
|
||||||
|
"[Rust] failed to create config db dir {}: {}",
|
||||||
|
root.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let db_path = root.join(CONFIG_DB_FILE_NAME);
|
||||||
|
match CONFIG_DB_PATH.lock() {
|
||||||
|
Ok(mut guard) => {
|
||||||
|
*guard = Some(db_path.clone());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
ohrs_log_error!("[Rust] failed to lock config db path: {}", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if open_db().is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ohrs_log_debug!("[Rust] initialized config db at {}", db_path.display());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export_config_store_snapshot(target_path: String) -> bool {
|
||||||
|
let target = PathBuf::from(target_path);
|
||||||
|
if !ensure_parent_dir(&target) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(src) = open_db() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let mut dst = match Connection::open(&target) {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(e) => {
|
||||||
|
ohrs_log_error!(
|
||||||
|
"[Rust] failed to open snapshot target {}: {}",
|
||||||
|
target.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = init_schema(&dst) {
|
||||||
|
ohrs_log_error!(
|
||||||
|
"[Rust] failed to init snapshot schema {}: {}",
|
||||||
|
target.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
match copy_snapshot_tables(&src, &mut dst) {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(e) => {
|
||||||
|
ohrs_log_error!(
|
||||||
|
"[Rust] failed to export snapshot {}: {}",
|
||||||
|
target.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_config_store_snapshot_with_result(source_path: String) -> SnapshotImportResult {
|
||||||
|
let source = PathBuf::from(source_path);
|
||||||
|
let src = match Connection::open(&source) {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(e) => {
|
||||||
|
ohrs_log_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();
|
||||||
|
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();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
match tx.commit() {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(e) => {
|
||||||
|
ohrs_log_error!("[Rust] failed to commit config store reset: {}", e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_config_meta_entries() -> StoredConfigList {
|
||||||
|
let Some(conn) = open_db() else {
|
||||||
|
return StoredConfigList { configs: vec![] };
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut stmt = match conn.prepare(
|
||||||
|
"SELECT config_id, display_name, created_at, updated_at, favorite, temporary
|
||||||
|
FROM stored_configs
|
||||||
|
ORDER BY updated_at DESC, display_name ASC",
|
||||||
|
) {
|
||||||
|
Ok(stmt) => stmt,
|
||||||
|
Err(e) => {
|
||||||
|
ohrs_log_error!("[Rust] failed to prepare list meta query: {}", e);
|
||||||
|
return StoredConfigList { configs: vec![] };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let rows = match stmt.query_map([], row_to_meta) {
|
||||||
|
Ok(rows) => rows,
|
||||||
|
Err(e) => {
|
||||||
|
ohrs_log_error!("[Rust] failed to list config meta rows: {}", e);
|
||||||
|
return StoredConfigList { configs: vec![] };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let configs = rows.filter_map(Result::ok).map(to_meta).collect();
|
||||||
|
StoredConfigList { configs }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_config_display_name(config_id: &str) -> Option<String> {
|
||||||
|
let conn = open_db()?;
|
||||||
|
load_meta_record(&conn, config_id).map(|record| record.display_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_config_meta(config_id: &str) -> Option<StoredConfigMeta> {
|
||||||
|
let conn = open_db()?;
|
||||||
|
load_meta_record(&conn, config_id).map(to_meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn upsert_config_meta_in_tx(
|
||||||
|
tx: &rusqlite::Transaction<'_>,
|
||||||
|
config_id: String,
|
||||||
|
display_name: String,
|
||||||
|
favorite: bool,
|
||||||
|
temporary: bool,
|
||||||
|
) -> Option<StoredConfigMeta> {
|
||||||
|
let now = now_ts_string();
|
||||||
|
let created_at = tx
|
||||||
|
.query_row(
|
||||||
|
"SELECT config_id, display_name, created_at, updated_at, favorite, temporary
|
||||||
|
FROM stored_configs WHERE config_id = ?1",
|
||||||
|
params![config_id],
|
||||||
|
row_to_meta,
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(|record| record.created_at)
|
||||||
|
.unwrap_or_else(|| now.clone());
|
||||||
|
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO stored_configs (
|
||||||
|
config_id, display_name, created_at, updated_at, favorite, temporary
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||||
|
ON CONFLICT(config_id) DO UPDATE SET
|
||||||
|
display_name = excluded.display_name,
|
||||||
|
updated_at = excluded.updated_at,
|
||||||
|
favorite = excluded.favorite,
|
||||||
|
temporary = excluded.temporary",
|
||||||
|
params![
|
||||||
|
config_id,
|
||||||
|
display_name,
|
||||||
|
created_at,
|
||||||
|
now,
|
||||||
|
if favorite { 1 } else { 0 },
|
||||||
|
if temporary { 1 } else { 0 }
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
tx.query_row(
|
||||||
|
"SELECT config_id, display_name, created_at, updated_at, favorite, temporary
|
||||||
|
FROM stored_configs WHERE config_id = ?1",
|
||||||
|
params![config_id],
|
||||||
|
row_to_meta,
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(to_meta)
|
||||||
|
.or(Some(StoredConfigMeta {
|
||||||
|
config_id,
|
||||||
|
display_name,
|
||||||
|
created_at,
|
||||||
|
updated_at: now,
|
||||||
|
favorite,
|
||||||
|
temporary,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_config_display_name(
|
||||||
|
config_id: String,
|
||||||
|
display_name: String,
|
||||||
|
) -> Option<StoredConfigMeta> {
|
||||||
|
let conn = open_db()?;
|
||||||
|
let mut record = load_meta_record(&conn, &config_id)?;
|
||||||
|
record.display_name = display_name;
|
||||||
|
record.updated_at = now_ts_string();
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE stored_configs
|
||||||
|
SET display_name = ?2, updated_at = ?3
|
||||||
|
WHERE config_id = ?1",
|
||||||
|
params![config_id, record.display_name, record.updated_at],
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
Some(to_meta(record))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn 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()?;
|
||||||
|
|
||||||
|
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()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub(crate) mod config_meta;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub(crate) mod stored_config;
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
use napi_derive_ohos::napi;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct StoredConfigMeta {
|
||||||
|
pub config_id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
pub favorite: bool,
|
||||||
|
pub temporary: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct StoredConfigRecord {
|
||||||
|
pub meta: StoredConfigMeta,
|
||||||
|
pub config_json: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct StoredConfigList {
|
||||||
|
pub configs: Vec<StoredConfigMeta>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct ExportTomlResult {
|
||||||
|
pub toml_text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct SharedConfigLinkPayload {
|
||||||
|
pub config_json: String,
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
pub only_start: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct LocalSocketSyncMessage {
|
||||||
|
pub message_type: String,
|
||||||
|
pub payload_json: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct KeyValuePair {
|
||||||
|
pub key: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,457 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
use crate::config::types::stored_config::{ExportTomlResult, StoredConfigRecord};
|
||||||
|
use easytier::proto::api::manage::NetworkConfig;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
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()
|
||||||
|
.ok()
|
||||||
|
.and_then(|guard| guard.as_ref().cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn kernel_socket_path() -> Option<PathBuf> {
|
||||||
|
config_root_dir().map(|root| root.join(KERNEL_SOCKET_FILE_NAME))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn legacy_config_file_path(config_id: &str) -> Option<PathBuf> {
|
||||||
|
legacy_migration::legacy_config_file_path(&config_root_dir(), CONFIG_DIR_NAME, config_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_config_store(root_dir: String) -> bool {
|
||||||
|
let root = PathBuf::from(root_dir);
|
||||||
|
let configs_dir = root.join(CONFIG_DIR_NAME);
|
||||||
|
if let Err(e) = std::fs::create_dir_all(&configs_dir) {
|
||||||
|
ohrs_log_error!(
|
||||||
|
"[Rust] failed to create config dir {}: {}",
|
||||||
|
configs_dir.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
match CONFIG_ROOT_DIR.lock() {
|
||||||
|
Ok(mut guard) => {
|
||||||
|
*guard = Some(root.clone());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
ohrs_log_error!("[Rust] failed to lock config root dir: {}", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !init_config_meta_store(root.to_string_lossy().into_owned()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ohrs_log_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,
|
||||||
|
config_id,
|
||||||
|
save_config_record,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_config_record(
|
||||||
|
config_id: String,
|
||||||
|
display_name: String,
|
||||||
|
config_json: String,
|
||||||
|
) -> Option<StoredConfigRecord> {
|
||||||
|
let config = match validation::validate_config_json(&config_json, config_id.clone()) {
|
||||||
|
Ok(config) => config,
|
||||||
|
Err(e) => {
|
||||||
|
ohrs_log_error!("[Rust] save_config_record failed {}", e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let normalized_json = match serde_json::to_string(&config) {
|
||||||
|
Ok(raw) => raw,
|
||||||
|
Err(e) => {
|
||||||
|
ohrs_log_error!(
|
||||||
|
"[Rust] failed to serialize normalized config {}: {}",
|
||||||
|
config_id,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let fields = match validation::config_to_top_level_map(&config) {
|
||||||
|
Some(fields) => fields,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let conn = open_db()?;
|
||||||
|
let tx = conn.unchecked_transaction().ok()?;
|
||||||
|
let existing_meta = 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 meta = upsert_config_meta_in_tx(&tx, config_id.clone(), display_name, favorite, temporary)?;
|
||||||
|
|
||||||
|
field_store::replace_config_fields(&tx, &config_id, fields)?;
|
||||||
|
|
||||||
|
tx.commit().ok()?;
|
||||||
|
|
||||||
|
if let Some(legacy_path) = legacy_config_file_path(&config_id) {
|
||||||
|
if legacy_path.exists() {
|
||||||
|
let _ = std::fs::remove_file(legacy_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(StoredConfigRecord {
|
||||||
|
meta,
|
||||||
|
config_json: normalized_json,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_config_json(config_id: &str) -> Option<String> {
|
||||||
|
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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw = match load_config_json(config_id) {
|
||||||
|
Some(raw) => raw,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
let mut value = match serde_json::from_str::<Value>(&raw) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
let new_field_value = match serde_json::from_str::<Value>(json_value) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
let object = match value.as_object_mut() {
|
||||||
|
Some(object) => object,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
object.insert(field.to_string(), new_field_value);
|
||||||
|
|
||||||
|
let normalized = match serde_json::to_string(&value) {
|
||||||
|
Ok(raw) => raw,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let display_name = get_config_meta(config_id)
|
||||||
|
.map(|meta| meta.display_name)
|
||||||
|
.unwrap_or_else(|| config_id.to_string());
|
||||||
|
|
||||||
|
save_config_record(config_id.to_string(), display_name, normalized).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_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());
|
||||||
|
let normalized_json = serde_json::to_string(&config).ok()?;
|
||||||
|
save_config_record(config_id, display_name, normalized_json)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_kernel_with_config_id(config_id: &str) -> bool {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_config_meta_json() -> String {
|
||||||
|
serde_json::to_string(&list_config_meta_entries().configs).unwrap_or_else(|_| "[]".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_config_record(config_id: &str) -> bool {
|
||||||
|
if 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let conn = match open_db() {
|
||||||
|
Some(conn) => conn,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
if let Err(e) = conn.execute(
|
||||||
|
"DELETE FROM stored_config_fields WHERE config_id = ?1",
|
||||||
|
params![config_id],
|
||||||
|
) {
|
||||||
|
ohrs_log_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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_toml_config(
|
||||||
|
toml_text: String,
|
||||||
|
display_name: Option<String>,
|
||||||
|
) -> Option<StoredConfigRecord> {
|
||||||
|
import_export::import_toml_to_record(toml_text, display_name, save_config_record)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use rusqlite::params;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
fn test_root() -> String {
|
||||||
|
let unique = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let dir = std::env::temp_dir().join(format!("easytier_ohrs_test_{}", unique));
|
||||||
|
dir.to_string_lossy().into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_get_export_delete_roundtrip() {
|
||||||
|
let root = test_root();
|
||||||
|
assert!(init_config_store(root.clone()));
|
||||||
|
|
||||||
|
let config_json = crate::build_default_network_config_json().expect("default config");
|
||||||
|
let saved = save_config_record("cfg-1".to_string(), "test-config".to_string(), config_json)
|
||||||
|
.expect("save config");
|
||||||
|
|
||||||
|
assert_eq!(saved.meta.config_id, "cfg-1");
|
||||||
|
assert_eq!(saved.meta.display_name, "test-config");
|
||||||
|
|
||||||
|
let loaded = get_config_record("cfg-1").expect("load config");
|
||||||
|
assert_eq!(loaded.meta.display_name, "test-config");
|
||||||
|
assert!(loaded.config_json.contains("cfg-1"));
|
||||||
|
|
||||||
|
let legacy_json_path = PathBuf::from(&root)
|
||||||
|
.join(CONFIG_DIR_NAME)
|
||||||
|
.join("cfg-1.json");
|
||||||
|
assert!(
|
||||||
|
!legacy_json_path.exists(),
|
||||||
|
"config should no longer be persisted as a per-config json file"
|
||||||
|
);
|
||||||
|
|
||||||
|
let conn = open_db().expect("db should be open");
|
||||||
|
let field_count: i64 = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM stored_config_fields WHERE config_id = ?1",
|
||||||
|
params!["cfg-1"],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.expect("count config fields");
|
||||||
|
assert!(field_count > 0, "config fields should be stored in sqlite");
|
||||||
|
|
||||||
|
let exported = export_config_toml("cfg-1").expect("export toml");
|
||||||
|
assert!(exported.toml_text.contains("instance_id"));
|
||||||
|
|
||||||
|
assert!(delete_config_record("cfg-1"));
|
||||||
|
assert!(get_config_record("cfg-1").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_config_field_updates_only_requested_top_level_field() {
|
||||||
|
let root = test_root();
|
||||||
|
assert!(init_config_store(root));
|
||||||
|
|
||||||
|
let config_json = crate::build_default_network_config_json().expect("default config");
|
||||||
|
save_config_record(
|
||||||
|
"cfg-field".to_string(),
|
||||||
|
"field-config".to_string(),
|
||||||
|
config_json,
|
||||||
|
)
|
||||||
|
.expect("save config");
|
||||||
|
|
||||||
|
let before_network_name = get_config_field_value("cfg-field", "network_name");
|
||||||
|
let before_instance_id = get_config_field_value("cfg-field", "instance_id")
|
||||||
|
.expect("instance id field should exist");
|
||||||
|
|
||||||
|
assert!(set_config_field_value(
|
||||||
|
"cfg-field",
|
||||||
|
"network_name",
|
||||||
|
"\"changed-network\""
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_config_field_value("cfg-field", "network_name"),
|
||||||
|
Some("\"changed-network\"".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get_config_field_value("cfg-field", "instance_id"),
|
||||||
|
Some(before_instance_id)
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
get_config_field_value("cfg-field", "network_name"),
|
||||||
|
before_network_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
use crate::config::storage::config_meta::{now_ts_string, open_db};
|
||||||
|
use rusqlite::{Connection, params};
|
||||||
|
use serde_json::{Map, Value};
|
||||||
|
|
||||||
|
pub(super) fn load_config_map_from_db(config_id: &str) -> Option<Map<String, Value>> {
|
||||||
|
let conn = open_db()?;
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(
|
||||||
|
"SELECT field_name, field_json
|
||||||
|
FROM stored_config_fields
|
||||||
|
WHERE config_id = ?1",
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
let rows = stmt
|
||||||
|
.query_map(params![config_id], |row| {
|
||||||
|
let field_name: String = row.get(0)?;
|
||||||
|
let field_json: String = row.get(1)?;
|
||||||
|
Ok((field_name, field_json))
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let mut object = Map::new();
|
||||||
|
for row in rows {
|
||||||
|
let (field_name, field_json) = row.ok()?;
|
||||||
|
let value = serde_json::from_str::<Value>(&field_json).ok()?;
|
||||||
|
object.insert(field_name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if object.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn replace_config_fields(
|
||||||
|
tx: &Connection,
|
||||||
|
config_id: &str,
|
||||||
|
fields: Map<String, Value>,
|
||||||
|
) -> Option<()> {
|
||||||
|
if let Err(e) = tx.execute(
|
||||||
|
"DELETE FROM stored_config_fields WHERE config_id = ?1",
|
||||||
|
params![config_id],
|
||||||
|
) {
|
||||||
|
ohrs_log_error!(
|
||||||
|
"[Rust] failed to clear existing config fields {}: {}",
|
||||||
|
config_id,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (field_name, value) in fields {
|
||||||
|
let field_json = serde_json::to_string(&value).ok()?;
|
||||||
|
if let Err(e) = tx.execute(
|
||||||
|
"INSERT INTO stored_config_fields (config_id, field_name, field_json, updated_at)
|
||||||
|
VALUES (?1, ?2, ?3, ?4)",
|
||||||
|
params![config_id, field_name, field_json, now_ts_string()],
|
||||||
|
) {
|
||||||
|
ohrs_log_error!("[Rust] failed to persist config field {}: {}", config_id, e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
use crate::config::types::stored_config::{ExportTomlResult, StoredConfigRecord};
|
||||||
|
use easytier::common::config::{ConfigLoader, TomlConfigLoader};
|
||||||
|
use easytier::proto::api::manage::NetworkConfig;
|
||||||
|
|
||||||
|
pub(super) fn export_config_toml_from_record(
|
||||||
|
record: &StoredConfigRecord,
|
||||||
|
) -> Option<ExportTomlResult> {
|
||||||
|
let config = serde_json::from_str::<NetworkConfig>(&record.config_json).ok()?;
|
||||||
|
let toml = config.gen_config().ok()?;
|
||||||
|
Some(ExportTomlResult {
|
||||||
|
toml_text: toml.dump(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn import_toml_to_record(
|
||||||
|
toml_text: String,
|
||||||
|
display_name: Option<String>,
|
||||||
|
save_config_record: impl Fn(String, String, String) -> Option<StoredConfigRecord>,
|
||||||
|
) -> Option<StoredConfigRecord> {
|
||||||
|
let config =
|
||||||
|
NetworkConfig::new_from_config(TomlConfigLoader::new_from_str(&toml_text).ok()?).ok()?;
|
||||||
|
|
||||||
|
let config_id = config.instance_id.clone()?;
|
||||||
|
let name_from_toml = toml_text
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if !trimmed.starts_with("instance_name") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
trimmed.split_once('=').map(|(_, value)| {
|
||||||
|
value
|
||||||
|
.trim()
|
||||||
|
.trim_matches('"')
|
||||||
|
.trim_matches('\'')
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.filter(|name| !name.is_empty());
|
||||||
|
|
||||||
|
let final_name = display_name
|
||||||
|
.filter(|name| !name.is_empty())
|
||||||
|
.or(name_from_toml)
|
||||||
|
.unwrap_or_else(|| config_id.clone());
|
||||||
|
|
||||||
|
let config_json = serde_json::to_string(&config).ok()?;
|
||||||
|
save_config_record(config_id, final_name, config_json)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
use crate::config::storage::config_meta::get_config_meta;
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn migrate_legacy_file_if_needed(
|
||||||
|
root_dir: &Option<PathBuf>,
|
||||||
|
config_dir_name: &str,
|
||||||
|
config_id: &str,
|
||||||
|
save_config_record: impl Fn(
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
) -> Option<crate::config::types::stored_config::StoredConfigRecord>,
|
||||||
|
) -> Option<()> {
|
||||||
|
let legacy_path = legacy_config_file_path(root_dir, config_dir_name, config_id)?;
|
||||||
|
if !legacy_path.exists() {
|
||||||
|
return Some(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw = std::fs::read_to_string(&legacy_path).ok()?;
|
||||||
|
let display_name = get_config_meta(config_id)
|
||||||
|
.map(|meta| meta.display_name)
|
||||||
|
.unwrap_or_else(|| config_id.to_string());
|
||||||
|
save_config_record(config_id.to_string(), display_name, raw)?;
|
||||||
|
|
||||||
|
if let Err(e) = std::fs::remove_file(&legacy_path) {
|
||||||
|
ohrs_log_error!(
|
||||||
|
"[Rust] failed to remove legacy config file {}: {}",
|
||||||
|
legacy_path.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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)?;
|
||||||
|
config.instance_id = Some(requested_id);
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn validate_config_json(
|
||||||
|
config_json: &str,
|
||||||
|
config_id: String,
|
||||||
|
) -> Result<NetworkConfig, String> {
|
||||||
|
let config = serde_json::from_str::<NetworkConfig>(config_json)
|
||||||
|
.map_err(|e| format!("parse config json failed: {}", e))?;
|
||||||
|
let config = normalize_config_id(config, config_id)?;
|
||||||
|
config
|
||||||
|
.gen_config()
|
||||||
|
.map_err(|e| format!("generate toml failed: {}", e))?;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn config_to_top_level_map(config: &NetworkConfig) -> Option<Map<String, Value>> {
|
||||||
|
serde_json::to_value(config).ok()?.as_object().cloned()
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub(crate) mod config_api;
|
||||||
|
pub(crate) mod runtime_api;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn save_config(config_id: String, display_name: String, config_json: String) -> bool {
|
||||||
|
config::repository::save_config_record(config_id, display_name, config_json).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn create_config(config_id: String, display_name: String) -> bool {
|
||||||
|
config::repository::create_config_record(config_id, display_name).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn delete_stored_config_meta(config_id: String) -> bool {
|
||||||
|
config::repository::delete_config_record(&config_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_config(config_id: String) -> Option<String> {
|
||||||
|
config::repository::load_config_json(&config_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_default_config() -> Option<String> {
|
||||||
|
config::repository::get_default_config_json()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_config_field(config_id: String, field: String) -> Option<String> {
|
||||||
|
config::repository::get_config_field_value(&config_id, &field)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_config_field(config_id: String, field: String, json_value: String) -> bool {
|
||||||
|
config::repository::set_config_field_value(&config_id, &field, &json_value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn export_toml(config_id: String) -> Option<String> {
|
||||||
|
config::repository::export_config_toml(&config_id).map(|ret| ret.toml_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn export_config_store_snapshot(target_path: String) -> bool {
|
||||||
|
config::storage::config_meta::export_config_store_snapshot(target_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
use crate::config::repository::{clear_runtime_config_snapshot, get_runtime_config_snapshot};
|
||||||
|
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,
|
||||||
|
runtime_instance_from_running_info,
|
||||||
|
};
|
||||||
|
use crate::{ASYNC_RUNTIME, INSTANCE_MANAGER, WEB_CLIENTS};
|
||||||
|
|
||||||
|
pub(crate) fn start_kernel(
|
||||||
|
config_id: String,
|
||||||
|
start_kernel_with_config_id: impl Fn(&str) -> bool,
|
||||||
|
) -> bool {
|
||||||
|
start_kernel_with_config_id(&config_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stop_kernel(
|
||||||
|
config_id: String,
|
||||||
|
stop_web_client: impl Fn(&str) -> bool,
|
||||||
|
parse_instance_uuid: impl Fn(&str) -> Option<uuid::Uuid>,
|
||||||
|
maybe_stop_local_socket_server: impl Fn(),
|
||||||
|
) -> bool {
|
||||||
|
clear_tun_attached(&config_id);
|
||||||
|
if stop_web_client(&config_id) {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
let ret = INSTANCE_MANAGER
|
||||||
|
.delete_network_instance(vec![instance_id])
|
||||||
|
.map(|_| true)
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
ohrs_log_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
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stop_network_instance(
|
||||||
|
config_ids: Vec<String>,
|
||||||
|
stop_kernel: impl Fn(String) -> bool,
|
||||||
|
) -> bool {
|
||||||
|
let mut ok = true;
|
||||||
|
for config_id in config_ids {
|
||||||
|
ok = stop_kernel(config_id) && ok;
|
||||||
|
}
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn collect_network_infos() -> Vec<KeyValuePair> {
|
||||||
|
let infos = match ASYNC_RUNTIME.block_on(INSTANCE_MANAGER.collect_network_infos()) {
|
||||||
|
Ok(infos) => infos,
|
||||||
|
Err(err) => {
|
||||||
|
ohrs_log_error!("[Rust] collect network infos failed {}", err);
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
infos
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(key, value)| {
|
||||||
|
serde_json::to_string(&value)
|
||||||
|
.ok()
|
||||||
|
.map(|value_json| KeyValuePair {
|
||||||
|
key: key.to_string(),
|
||||||
|
value: value_json,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_tun_fd(
|
||||||
|
config_id: String,
|
||||||
|
fd: i32,
|
||||||
|
parse_instance_uuid: impl Fn(&str) -> Option<uuid::Uuid>,
|
||||||
|
) -> bool {
|
||||||
|
let Some(instance_id) = parse_instance_uuid(&config_id) else {
|
||||||
|
ohrs_log_error!("[Rust] set_tun_fd invalid instance id: {}", config_id);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
INSTANCE_MANAGER
|
||||||
|
.set_tun_fd(&instance_id, fd)
|
||||||
|
.map(|_| {
|
||||||
|
mark_tun_attached(&config_id);
|
||||||
|
ohrs_log_info!(
|
||||||
|
"[Rust] set_tun_fd success instance={} fd={} marked_attached=true",
|
||||||
|
config_id,
|
||||||
|
fd
|
||||||
|
);
|
||||||
|
true
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
ohrs_log_error!("[Rust] set_tun_fd failed {}: {}", config_id, err);
|
||||||
|
false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_runtime_snapshot() -> RuntimeAggregateState {
|
||||||
|
get_runtime_snapshot_inner()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_runtime_snapshot_inner() -> RuntimeAggregateState {
|
||||||
|
let infos = match ASYNC_RUNTIME.block_on(INSTANCE_MANAGER.collect_network_infos()) {
|
||||||
|
Ok(infos) => infos,
|
||||||
|
Err(err) => {
|
||||||
|
ohrs_log_error!("[Rust] collect network infos failed {}", err);
|
||||||
|
return RuntimeAggregateState {
|
||||||
|
instances: vec![],
|
||||||
|
tun: TunAggregateState {
|
||||||
|
active: false,
|
||||||
|
attached_instance_ids: vec![],
|
||||||
|
aggregated_routes: vec![],
|
||||||
|
dns_servers: vec![],
|
||||||
|
need_rebuild: false,
|
||||||
|
},
|
||||||
|
running_instance_count: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut 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(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instances.sort_by(|a, b| {
|
||||||
|
a.display_name
|
||||||
|
.cmp(&b.display_name)
|
||||||
|
.then_with(|| a.instance_id.cmp(&b.instance_id))
|
||||||
|
});
|
||||||
|
let attached_instance_ids = instances
|
||||||
|
.iter()
|
||||||
|
.filter(|instance| instance.tun_required)
|
||||||
|
.map(|instance| instance.instance_id.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let aggregated_routes = aggregate_requested_tun_routes(&instances);
|
||||||
|
let running_instance_count =
|
||||||
|
instances.iter().filter(|instance| instance.running).count() as i32;
|
||||||
|
let tun_active = !attached_instance_ids.is_empty();
|
||||||
|
|
||||||
|
RuntimeAggregateState {
|
||||||
|
instances,
|
||||||
|
tun: TunAggregateState {
|
||||||
|
active: tun_active,
|
||||||
|
attached_instance_ids,
|
||||||
|
aggregated_routes,
|
||||||
|
dns_servers: vec![],
|
||||||
|
need_rebuild: false,
|
||||||
|
},
|
||||||
|
running_instance_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
mod protocol;
|
||||||
|
mod routing;
|
||||||
|
mod socket_server;
|
||||||
|
|
||||||
|
pub(crate) use routing::aggregate_requested_tun_routes;
|
||||||
|
pub use socket_server::{
|
||||||
|
set_snapshot_broadcast_enabled, start_local_socket_server, stop_local_socket_server,
|
||||||
|
};
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
use crate::config::types::stored_config::LocalSocketSyncMessage;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::io::{Error, ErrorKind, Write};
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct TunRequestPayload {
|
||||||
|
pub config_id: String,
|
||||||
|
pub instance_id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub virtual_ipv4: Option<String>,
|
||||||
|
pub virtual_ipv4_cidr: Option<String>,
|
||||||
|
pub aggregated_routes: Vec<String>,
|
||||||
|
pub magic_dns_enabled: bool,
|
||||||
|
pub need_exit_node: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn send_local_socket_message(
|
||||||
|
stream: &mut UnixStream,
|
||||||
|
message_type: &str,
|
||||||
|
payload_json: String,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
let message = LocalSocketSyncMessage {
|
||||||
|
message_type: message_type.to_string(),
|
||||||
|
payload_json,
|
||||||
|
};
|
||||||
|
let mut raw = serde_json::to_vec(&message)
|
||||||
|
.map_err(|err| Error::new(ErrorKind::InvalidData, err.to_string()))?;
|
||||||
|
raw.push(b'\n');
|
||||||
|
stream.write_all(&raw)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn broadcast_local_socket_message(
|
||||||
|
clients: &mut Vec<UnixStream>,
|
||||||
|
message_type: &str,
|
||||||
|
payload_json: &str,
|
||||||
|
) -> bool {
|
||||||
|
let mut active_clients = Vec::with_capacity(clients.len());
|
||||||
|
let mut delivered = false;
|
||||||
|
for mut client in clients.drain(..) {
|
||||||
|
if send_local_socket_message(&mut client, message_type, payload_json.to_string()).is_ok() {
|
||||||
|
delivered = true;
|
||||||
|
active_clients.push(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*clients = active_clients;
|
||||||
|
delivered
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*clients = active_clients;
|
||||||
|
delivered
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
use crate::config::repository::get_runtime_config_route_overrides;
|
||||||
|
use crate::runtime::state::runtime_state::RuntimeInstanceState;
|
||||||
|
use ipnet::IpNet;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
fn normalize_route_cidr(route: &str) -> Option<String> {
|
||||||
|
let normalized = route.split("->").next().unwrap_or(route).trim();
|
||||||
|
normalized
|
||||||
|
.parse::<IpNet>()
|
||||||
|
.ok()
|
||||||
|
.map(|network| match network {
|
||||||
|
IpNet::V4(net) => net.trunc().to_string(),
|
||||||
|
IpNet::V6(net) => net.trunc().to_string(),
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
normalized.parse::<IpAddr>().ok().map(|addr| match addr {
|
||||||
|
IpAddr::V4(ip) => format!("{}/32", ip),
|
||||||
|
IpAddr::V6(ip) => format!("{}/128", ip),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn simplify_routes(routes: Vec<String>) -> Vec<String> {
|
||||||
|
let mut parsed = routes
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|route| normalize_route_cidr(&route))
|
||||||
|
.filter_map(|route| route.parse::<IpNet>().ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
parsed.sort_by(|left, right| {
|
||||||
|
left.prefix_len()
|
||||||
|
.cmp(&right.prefix_len())
|
||||||
|
.then_with(|| left.network().to_string().cmp(&right.network().to_string()))
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut simplified = Vec::<IpNet>::new();
|
||||||
|
'outer: for route in parsed {
|
||||||
|
for existing in &simplified {
|
||||||
|
if existing.contains(&route.network()) && existing.prefix_len() <= route.prefix_len() {
|
||||||
|
continue 'outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
simplified.retain(|existing| {
|
||||||
|
!(route.contains(&existing.network()) && route.prefix_len() <= existing.prefix_len())
|
||||||
|
});
|
||||||
|
simplified.push(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
simplified
|
||||||
|
.into_iter()
|
||||||
|
.map(|route| route.to_string())
|
||||||
|
.filter(|route| seen.insert(route.clone()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn aggregate_tun_routes(instance: &RuntimeInstanceState) -> Vec<String> {
|
||||||
|
let virtual_ipv4_cidr = instance
|
||||||
|
.my_node_info
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|info| info.virtual_ipv4_cidr.clone());
|
||||||
|
let (manual_routes, config_proxy_cidrs) =
|
||||||
|
get_runtime_config_route_overrides(&instance.config_id);
|
||||||
|
let runtime_proxy_cidrs = instance
|
||||||
|
.routes
|
||||||
|
.iter()
|
||||||
|
.flat_map(|route| route.proxy_cidrs.iter().cloned())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut raw_routes = Vec::new();
|
||||||
|
|
||||||
|
if let Some(cidr) = virtual_ipv4_cidr.clone() {
|
||||||
|
raw_routes.push(cidr);
|
||||||
|
}
|
||||||
|
|
||||||
|
raw_routes.extend(manual_routes.iter().cloned());
|
||||||
|
raw_routes.extend(config_proxy_cidrs.iter().cloned());
|
||||||
|
raw_routes.extend(runtime_proxy_cidrs.iter().cloned());
|
||||||
|
simplify_routes(raw_routes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn aggregate_requested_tun_routes(instances: &[RuntimeInstanceState]) -> Vec<String> {
|
||||||
|
let mut aggregated_routes = Vec::new();
|
||||||
|
let mut seen_routes = HashSet::new();
|
||||||
|
for instance in instances.iter().filter(|instance| instance.tun_required) {
|
||||||
|
for route in aggregate_tun_routes(instance) {
|
||||||
|
if seen_routes.insert(route.clone()) {
|
||||||
|
aggregated_routes.push(route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aggregated_routes
|
||||||
|
}
|
||||||
@@ -0,0 +1,332 @@
|
|||||||
|
use super::protocol::{
|
||||||
|
TunRequestPayload, broadcast_local_socket_json_payload_message, broadcast_local_socket_message,
|
||||||
|
};
|
||||||
|
use crate::INSTANCE_MANAGER;
|
||||||
|
use crate::config::repository::kernel_socket_path;
|
||||||
|
use crate::get_runtime_snapshot_inner;
|
||||||
|
use crate::kernel_bridge::routing::aggregate_tun_routes;
|
||||||
|
use easytier::common::global_ctx::{EventBusSubscriber, GlobalCtxEvent};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::io::ErrorKind;
|
||||||
|
use std::os::unix::net::{UnixListener, UnixStream};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::thread::{self, JoinHandle};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
struct LocalSocketState {
|
||||||
|
stop_flag: std::sync::Arc<AtomicBool>,
|
||||||
|
socket_path: PathBuf,
|
||||||
|
worker: JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static LOCAL_SOCKET_STATE: Lazy<Mutex<Option<LocalSocketState>>> = Lazy::new(|| Mutex::new(None));
|
||||||
|
static SNAPSHOT_BROADCAST_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||||
|
const SOCKET_TICK_INTERVAL: Duration = Duration::from_millis(250);
|
||||||
|
const TUN_FAST_CHECK_WINDOW: Duration = Duration::from_secs(8);
|
||||||
|
const EVENT_RECEIVER_SYNC_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
pub fn set_snapshot_broadcast_enabled(enabled: bool) {
|
||||||
|
SNAPSHOT_BROADCAST_ENABLED.store(enabled, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event_needs_tun_refresh(event: &GlobalCtxEvent) -> bool {
|
||||||
|
matches!(
|
||||||
|
event,
|
||||||
|
GlobalCtxEvent::DhcpIpv4Changed(_, _)
|
||||||
|
| GlobalCtxEvent::DhcpIpv4Conflicted(_)
|
||||||
|
| GlobalCtxEvent::PublicIpv6Changed(_, _)
|
||||||
|
| GlobalCtxEvent::PublicIpv6RoutesUpdated(_, _)
|
||||||
|
| GlobalCtxEvent::ProxyCidrsUpdated(_, _)
|
||||||
|
| GlobalCtxEvent::ConfigPatched(_)
|
||||||
|
| GlobalCtxEvent::PeerAdded(_)
|
||||||
|
| GlobalCtxEvent::PeerRemoved(_)
|
||||||
|
| GlobalCtxEvent::PeerConnAdded(_)
|
||||||
|
| GlobalCtxEvent::PeerConnRemoved(_)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drain_tun_refresh_events(receivers: &mut HashMap<String, EventBusSubscriber>) -> bool {
|
||||||
|
let mut refresh_needed = false;
|
||||||
|
let mut closed_receivers = Vec::new();
|
||||||
|
for (instance_id, receiver) in receivers.iter_mut() {
|
||||||
|
loop {
|
||||||
|
match receiver.try_recv() {
|
||||||
|
Ok(event) => {
|
||||||
|
refresh_needed = event_needs_tun_refresh(&event) || refresh_needed;
|
||||||
|
}
|
||||||
|
Err(tokio::sync::broadcast::error::TryRecvError::Empty) => break,
|
||||||
|
Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) => {
|
||||||
|
refresh_needed = 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);
|
||||||
|
}
|
||||||
|
refresh_needed
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match LOCAL_SOCKET_STATE.lock() {
|
||||||
|
Ok(guard) if guard.is_some() => return true,
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
ohrs_log_error!("[Rust] lock localsocket state failed: {}", err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if socket_path.exists() {
|
||||||
|
let _ = std::fs::remove_file(&socket_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let listener = match UnixListener::bind(&socket_path) {
|
||||||
|
Ok(listener) => listener,
|
||||||
|
Err(err) => {
|
||||||
|
ohrs_log_error!(
|
||||||
|
"[Rust] bind localsocket failed {}: {}",
|
||||||
|
socket_path.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(err) = listener.set_nonblocking(true) {
|
||||||
|
ohrs_log_error!("[Rust] set localsocket nonblocking failed: {}", err);
|
||||||
|
let _ = std::fs::remove_file(&socket_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stop_flag = std::sync::Arc::new(AtomicBool::new(false));
|
||||||
|
let worker_stop_flag = stop_flag.clone();
|
||||||
|
let worker = thread::spawn(move || {
|
||||||
|
let mut last_snapshot_json = String::new();
|
||||||
|
let mut delivered_tun_requests = HashSet::new();
|
||||||
|
let mut last_tun_route_signatures = HashMap::<String, String>::new();
|
||||||
|
let mut 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 tun_event_receivers = HashMap::<String, EventBusSubscriber>::new();
|
||||||
|
let mut clients = Vec::<UnixStream>::new();
|
||||||
|
|
||||||
|
while !worker_stop_flag.load(Ordering::Relaxed) {
|
||||||
|
let mut accepted_client = false;
|
||||||
|
loop {
|
||||||
|
match listener.accept() {
|
||||||
|
Ok((stream, _addr)) => {
|
||||||
|
accepted_client = true;
|
||||||
|
clients.push(stream);
|
||||||
|
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);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshot_enabled = SNAPSHOT_BROADCAST_ENABLED.load(Ordering::Relaxed);
|
||||||
|
if clients.is_empty() {
|
||||||
|
if !last_snapshot_json.is_empty() {
|
||||||
|
last_snapshot_json.clear();
|
||||||
|
}
|
||||||
|
delivered_tun_requests.clear();
|
||||||
|
last_tun_route_signatures.clear();
|
||||||
|
tun_event_receivers.clear();
|
||||||
|
last_event_receiver_sync_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);
|
||||||
|
}
|
||||||
|
if drain_tun_refresh_events(&mut tun_event_receivers) {
|
||||||
|
tun_bootstrap_done = false;
|
||||||
|
tun_fast_until = now + TUN_FAST_CHECK_WINDOW;
|
||||||
|
}
|
||||||
|
let should_collect_snapshot = snapshot_enabled
|
||||||
|
|| accepted_client
|
||||||
|
|| (!tun_bootstrap_done && now < tun_fast_until);
|
||||||
|
if !should_collect_snapshot {
|
||||||
|
if !last_snapshot_json.is_empty() {
|
||||||
|
last_snapshot_json.clear();
|
||||||
|
}
|
||||||
|
thread::sleep(SOCKET_TICK_INTERVAL);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshot = get_runtime_snapshot_inner();
|
||||||
|
if snapshot_enabled {
|
||||||
|
let snapshot_json = match serde_json::to_string(&snapshot) {
|
||||||
|
Ok(json) => json,
|
||||||
|
Err(err) => {
|
||||||
|
ohrs_log_error!("[Rust] serialize runtime snapshot failed: {}", err);
|
||||||
|
thread::sleep(SOCKET_TICK_INTERVAL);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if accepted_client || snapshot_json != last_snapshot_json {
|
||||||
|
let _ = broadcast_local_socket_json_payload_message(
|
||||||
|
&mut clients,
|
||||||
|
"runtime_snapshot",
|
||||||
|
&snapshot_json,
|
||||||
|
);
|
||||||
|
last_snapshot_json = snapshot_json;
|
||||||
|
}
|
||||||
|
} else if !last_snapshot_json.is_empty() {
|
||||||
|
last_snapshot_json.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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 = accepted_client
|
||||||
|
|| !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);
|
||||||
|
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 !snapshot_enabled
|
||||||
|
&& (!delivered_tun_requests.is_empty()
|
||||||
|
|| (saw_running_instance && !saw_tun_candidate)
|
||||||
|
|| now >= tun_fast_until)
|
||||||
|
{
|
||||||
|
tun_bootstrap_done = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
thread::sleep(SOCKET_TICK_INTERVAL);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
match LOCAL_SOCKET_STATE.lock() {
|
||||||
|
Ok(mut guard) => {
|
||||||
|
*guard = Some(LocalSocketState {
|
||||||
|
stop_flag,
|
||||||
|
socket_path,
|
||||||
|
worker,
|
||||||
|
});
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
ohrs_log_error!("[Rust] lock localsocket state failed: {}", err);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop_local_socket_server() -> bool {
|
||||||
|
let state = match LOCAL_SOCKET_STATE.lock() {
|
||||||
|
Ok(mut guard) => guard.take(),
|
||||||
|
Err(err) => {
|
||||||
|
ohrs_log_error!("[Rust] lock localsocket state failed: {}", err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(state) = state {
|
||||||
|
state.stop_flag.store(true, Ordering::Relaxed);
|
||||||
|
let _ = state.worker.join();
|
||||||
|
let _ = std::fs::remove_file(state.socket_path);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
@@ -1,114 +1,304 @@
|
|||||||
mod native_log;
|
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)*),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
use easytier::common::config::{ConfigFileControl, ConfigLoader, TomlConfigLoader};
|
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::services::schema_service::{
|
||||||
|
ConfigFieldMapping, NetworkConfigSchema,
|
||||||
|
get_network_config_field_mappings as build_network_config_field_mappings,
|
||||||
|
get_network_config_schema as build_network_config_schema,
|
||||||
|
};
|
||||||
|
use config::services::share_link_service::{
|
||||||
|
build_config_share_link as build_config_share_link_inner,
|
||||||
|
import_config_share_link as import_config_share_link_inner,
|
||||||
|
parse_config_share_link as parse_config_share_link_inner,
|
||||||
|
};
|
||||||
|
use config::storage::config_meta::get_config_display_name;
|
||||||
|
use config::types::stored_config::{KeyValuePair, SharedConfigLinkPayload, SnapshotImportResult};
|
||||||
use easytier::common::constants::EASYTIER_VERSION;
|
use easytier::common::constants::EASYTIER_VERSION;
|
||||||
|
use easytier::common::{
|
||||||
|
MachineIdOptions,
|
||||||
|
config::{ConfigFileControl, ConfigLoader, TomlConfigLoader},
|
||||||
|
};
|
||||||
use easytier::instance_manager::NetworkInstanceManager;
|
use easytier::instance_manager::NetworkInstanceManager;
|
||||||
use easytier::proto::api::manage::NetworkConfig;
|
use easytier::proto::api::manage::NetworkConfig;
|
||||||
|
use easytier::proto::api::manage::NetworkingMethod;
|
||||||
|
use easytier::web_client::{WebClient, WebClientHooks, run_web_client};
|
||||||
|
use kernel_bridge::{
|
||||||
|
set_snapshot_broadcast_enabled, 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 napi_derive_ohos::napi;
|
||||||
use ohos_hilog_binding::{hilog_debug, hilog_error};
|
use runtime::state::runtime_state::RuntimeAggregateState;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::format;
|
use std::format;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tokio::runtime::{Builder, Runtime};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
static INSTANCE_MANAGER: once_cell::sync::Lazy<NetworkInstanceManager> =
|
pub(crate) static INSTANCE_MANAGER: once_cell::sync::Lazy<Arc<NetworkInstanceManager>> =
|
||||||
once_cell::sync::Lazy::new(NetworkInstanceManager::new);
|
once_cell::sync::Lazy::new(|| Arc::new(NetworkInstanceManager::new()));
|
||||||
|
static ASYNC_RUNTIME: once_cell::sync::Lazy<Runtime> = once_cell::sync::Lazy::new(|| {
|
||||||
|
Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("tokio runtime for easytier-ohrs")
|
||||||
|
});
|
||||||
|
static WEB_CLIENTS: once_cell::sync::Lazy<Mutex<HashMap<String, ManagedWebClient>>> =
|
||||||
|
once_cell::sync::Lazy::new(|| Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
#[napi(object)]
|
#[derive(Default)]
|
||||||
pub struct KeyValuePair {
|
struct TrackedWebClientHooks {
|
||||||
pub key: String,
|
instance_ids: Mutex<HashSet<Uuid>>,
|
||||||
pub value: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi]
|
struct ManagedWebClient {
|
||||||
pub fn easytier_version() -> String {
|
_client: WebClient,
|
||||||
EASYTIER_VERSION.to_string()
|
hooks: Arc<TrackedWebClientHooks>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi]
|
#[async_trait::async_trait]
|
||||||
pub fn set_tun_fd(inst_id: String, fd: i32) -> bool {
|
impl WebClientHooks for TrackedWebClientHooks {
|
||||||
match Uuid::try_parse(&inst_id) {
|
async fn post_run_network_instance(&self, id: &Uuid) -> Result<(), String> {
|
||||||
Ok(uuid) => match INSTANCE_MANAGER.set_tun_fd(&uuid, fd) {
|
self.instance_ids
|
||||||
Ok(_) => {
|
.lock()
|
||||||
hilog_debug!("[Rust] set tun fd {} to {}.", fd, inst_id);
|
.map_err(|err| err.to_string())?
|
||||||
true
|
.insert(*id);
|
||||||
}
|
Ok(())
|
||||||
Err(e) => {
|
}
|
||||||
hilog_error!("[Rust] cant set tun fd {} to {}. {}", fd, inst_id, e);
|
|
||||||
false
|
async fn post_remove_network_instances(&self, ids: &[Uuid]) -> Result<(), String> {
|
||||||
}
|
let mut guard = self.instance_ids.lock().map_err(|err| err.to_string())?;
|
||||||
},
|
for id in ids {
|
||||||
Err(e) => {
|
guard.remove(id);
|
||||||
hilog_error!("[Rust] cant covert {} to uuid. {}", inst_id, e);
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi]
|
fn is_config_server_config(config: &NetworkConfig) -> bool {
|
||||||
pub fn default_network_config() -> String {
|
matches!(
|
||||||
match NetworkConfig::new_from_config(TomlConfigLoader::default()) {
|
NetworkingMethod::try_from(config.networking_method.unwrap_or_default())
|
||||||
Ok(result) => serde_json::to_string(&result).unwrap_or_else(|e| format!("ERROR {}", e)),
|
.unwrap_or_default(),
|
||||||
Err(e) => {
|
NetworkingMethod::PublicServer
|
||||||
hilog_error!("[Rust] default_network_config failed {}", e);
|
) && config
|
||||||
format!("ERROR {}", e)
|
.public_server_url
|
||||||
}
|
.as_ref()
|
||||||
}
|
.is_some_and(|url| !url.trim().is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi]
|
fn stop_web_client(config_id: &str) -> bool {
|
||||||
pub fn convert_toml_to_network_config(cfg_str: String) -> String {
|
let managed = match WEB_CLIENTS.lock() {
|
||||||
match TomlConfigLoader::new_from_str(&cfg_str) {
|
Ok(mut guard) => guard.remove(config_id),
|
||||||
Ok(cfg) => match NetworkConfig::new_from_config(cfg) {
|
Err(err) => {
|
||||||
Ok(result) => serde_json::to_string(&result).unwrap_or_else(|e| format!("ERROR {}", e)),
|
ohrs_log_error!("[Rust] stop_web_client lock failed {}", err);
|
||||||
Err(e) => {
|
|
||||||
hilog_error!("[Rust] convert_toml_to_network_config failed {}", e);
|
|
||||||
format!("ERROR {}", e)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
hilog_error!("[Rust] convert_toml_to_network_config failed {}", e);
|
|
||||||
format!("ERROR {}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
pub fn parse_network_config(cfg_json: String) -> bool {
|
|
||||||
match serde_json::from_str::<NetworkConfig>(&cfg_json) {
|
|
||||||
Ok(cfg) => match cfg.gen_config() {
|
|
||||||
Ok(toml) => {
|
|
||||||
hilog_debug!("[Rust] Convert to Toml {}", toml.dump());
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
hilog_error!("[Rust] parse config failed {}", e);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
hilog_error!("[Rust] parse config failed {}", e);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
pub fn run_network_instance(cfg_json: String) -> bool {
|
|
||||||
let cfg = match serde_json::from_str::<NetworkConfig>(&cfg_json) {
|
|
||||||
Ok(cfg) => match cfg.gen_config() {
|
|
||||||
Ok(toml) => toml,
|
|
||||||
Err(e) => {
|
|
||||||
hilog_error!("[Rust] parse config failed {}", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
hilog_error!("[Rust] parse config failed {}", e);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if INSTANCE_MANAGER.list_network_instance_ids().len() > 0 {
|
let Some(managed) = managed else {
|
||||||
hilog_error!("[Rust] there is a running instance!");
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let tracked_ids = managed
|
||||||
|
.hooks
|
||||||
|
.instance_ids
|
||||||
|
.lock()
|
||||||
|
.map(|guard| guard.iter().copied().collect::<Vec<_>>())
|
||||||
|
.unwrap_or_default();
|
||||||
|
drop(managed);
|
||||||
|
|
||||||
|
if tracked_ids.is_empty() {
|
||||||
|
maybe_stop_local_socket_server();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = INSTANCE_MANAGER
|
||||||
|
.delete_network_instance(tracked_ids)
|
||||||
|
.map(|_| true)
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
ohrs_log_error!(
|
||||||
|
"[Rust] stop config server instances failed {}: {}",
|
||||||
|
config_id,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
false
|
||||||
|
});
|
||||||
|
maybe_stop_local_socket_server();
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_local_socket_server_started() -> bool {
|
||||||
|
start_local_socket_server_inner()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_stop_local_socket_server() {
|
||||||
|
let no_local_instances = INSTANCE_MANAGER.list_network_instance_ids().is_empty();
|
||||||
|
let no_web_clients = WEB_CLIENTS
|
||||||
|
.lock()
|
||||||
|
.map(|guard| guard.is_empty())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if no_local_instances && no_web_clients {
|
||||||
|
let _ = stop_local_socket_server_inner();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_config_server_instance(config_id: &str, config: &NetworkConfig) -> bool {
|
||||||
|
if INSTANCE_MANAGER
|
||||||
|
.list_network_instance_ids()
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
ohrs_log_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");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let hooks = Arc::new(TrackedWebClientHooks::default());
|
||||||
|
let secure_mode = config
|
||||||
|
.secure_mode
|
||||||
|
.as_ref()
|
||||||
|
.map(|mode| mode.enabled)
|
||||||
|
.unwrap_or(false);
|
||||||
|
let hostname = config.hostname.clone();
|
||||||
|
|
||||||
|
if !ensure_local_socket_server_started() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = ASYNC_RUNTIME.block_on(run_web_client(
|
||||||
|
&config_server_url,
|
||||||
|
MachineIdOptions::default(),
|
||||||
|
hostname,
|
||||||
|
secure_mode,
|
||||||
|
INSTANCE_MANAGER.clone(),
|
||||||
|
Some(hooks.clone()),
|
||||||
|
));
|
||||||
|
|
||||||
|
let client = match client {
|
||||||
|
Ok(client) => client,
|
||||||
|
Err(err) => {
|
||||||
|
ohrs_log_error!("[Rust] start config server failed {}", err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match WEB_CLIENTS.lock() {
|
||||||
|
Ok(mut guard) => {
|
||||||
|
guard.insert(
|
||||||
|
config_id.to_string(),
|
||||||
|
ManagedWebClient {
|
||||||
|
_client: client,
|
||||||
|
hooks,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
ohrs_log_error!("[Rust] store config server client failed {}", err);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_default_network_config_json() -> Result<String, String> {
|
||||||
|
let config = NetworkConfig::new_from_config(TomlConfigLoader::default())
|
||||||
|
.map_err(|e| format!("default_network_config failed {}", e))?;
|
||||||
|
serde_json::to_string(&config).map_err(|e| format!("default_network_config failed {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_toml_to_network_config_inner(toml_text: &str) -> Result<String, String> {
|
||||||
|
let config = NetworkConfig::new_from_config(
|
||||||
|
TomlConfigLoader::new_from_str(toml_text).map_err(|e| e.to_string())?,
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
serde_json::to_string(&config).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_network_config_inner(cfg_json: &str) -> bool {
|
||||||
|
serde_json::from_str::<NetworkConfig>(cfg_json)
|
||||||
|
.ok()
|
||||||
|
.and_then(|cfg| cfg.gen_config().ok())
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn run_network_instance_from_json(cfg_json: &str) -> bool {
|
||||||
|
let config = match serde_json::from_str::<NetworkConfig>(cfg_json) {
|
||||||
|
Ok(cfg) => cfg,
|
||||||
|
Err(e) => {
|
||||||
|
ohrs_log_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");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cfg = match config.gen_config() {
|
||||||
|
Ok(toml) => toml,
|
||||||
|
Err(e) => {
|
||||||
|
ohrs_log_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!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ensure_local_socket_server_started() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,69 +307,244 @@ pub fn run_network_instance(cfg_json: String) -> bool {
|
|||||||
.list_network_instance_ids()
|
.list_network_instance_ids()
|
||||||
.contains(&inst_id)
|
.contains(&inst_id)
|
||||||
{
|
{
|
||||||
|
ohrs_log_error!("[Rust] instance {} already exists", inst_id);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
INSTANCE_MANAGER
|
|
||||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
|
||||||
.unwrap();
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
match INSTANCE_MANAGER.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) {
|
||||||
pub fn stop_network_instance(inst_names: Vec<String>) {
|
Ok(_) => {
|
||||||
INSTANCE_MANAGER
|
cache_runtime_config_snapshot(inst_id.to_string(), inst_id.to_string(), config);
|
||||||
.delete_network_instance(
|
true
|
||||||
inst_names
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|s| Uuid::parse_str(&s).ok())
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
hilog_debug!("[Rust] stop_network_instance");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
pub fn collect_network_infos() -> Vec<KeyValuePair> {
|
|
||||||
let mut result = Vec::new();
|
|
||||||
match INSTANCE_MANAGER.collect_network_infos_sync() {
|
|
||||||
Ok(map) => {
|
|
||||||
for (uuid, info) in map.iter() {
|
|
||||||
// convert value to json string
|
|
||||||
let value = match serde_json::to_string(&info) {
|
|
||||||
Ok(value) => value,
|
|
||||||
Err(e) => {
|
|
||||||
hilog_error!("[Rust] failed to serialize instance {} info: {}", uuid, e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
result.push(KeyValuePair {
|
|
||||||
key: uuid.clone().to_string(),
|
|
||||||
value: value.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(_) => {}
|
Err(err) => {
|
||||||
}
|
ohrs_log_error!("[Rust] start_kernel failed for {}: {}", inst_id, err);
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
pub fn collect_running_network() -> Vec<String> {
|
|
||||||
INSTANCE_MANAGER
|
|
||||||
.list_network_instance_ids()
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.map(|id| id.to_string())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
pub fn is_running_network(inst_id: String) -> bool {
|
|
||||||
match Uuid::try_parse(&inst_id) {
|
|
||||||
Ok(uuid) => INSTANCE_MANAGER.list_network_instance_ids().contains(&uuid),
|
|
||||||
Err(e) => {
|
|
||||||
hilog_error!("[Rust] cant covert {} to uuid. {}", inst_id, e);
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_config_display_name_by_id(config_id: String) -> Option<String> {
|
||||||
|
get_config_display_name(&config_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn save_config(config_id: String, display_name: String, config_json: String) -> bool {
|
||||||
|
exports::config_api::save_config(config_id, display_name, config_json)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn create_config(config_id: String, display_name: String) -> bool {
|
||||||
|
exports::config_api::create_config(config_id, display_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn rename_stored_config(config_id: String, display_name: String) -> bool {
|
||||||
|
config::storage::config_meta::set_config_display_name(config_id, display_name).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn delete_stored_config_meta(config_id: String) -> bool {
|
||||||
|
exports::config_api::delete_stored_config_meta(config_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_config(config_id: String) -> Option<String> {
|
||||||
|
exports::config_api::get_config(config_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_default_config() -> Option<String> {
|
||||||
|
exports::config_api::get_default_config()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_config_field(config_id: String, field: String) -> Option<String> {
|
||||||
|
exports::config_api::get_config_field(config_id, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn set_config_field(config_id: String, field: String, json_value: String) -> bool {
|
||||||
|
exports::config_api::set_config_field(config_id, field, json_value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn export_toml(config_id: String) -> Option<String> {
|
||||||
|
exports::config_api::export_toml(config_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn export_config_store_snapshot(target_path: String) -> bool {
|
||||||
|
exports::config_api::export_config_store_snapshot(target_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn stop_kernel(config_id: String) -> bool {
|
||||||
|
exports::runtime_api::stop_kernel(
|
||||||
|
config_id,
|
||||||
|
stop_web_client,
|
||||||
|
parse_instance_uuid,
|
||||||
|
maybe_stop_local_socket_server,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn stop_network_instance(config_ids: Vec<String>) -> bool {
|
||||||
|
exports::runtime_api::stop_network_instance(config_ids, stop_kernel)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn easytier_version() -> String {
|
||||||
|
EASYTIER_VERSION.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn default_network_config() -> String {
|
||||||
|
get_default_config().unwrap_or_else(|| "{}".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn convert_toml_to_network_config(toml_text: String) -> String {
|
||||||
|
convert_toml_to_network_config_inner(&toml_text).unwrap_or_else(|err| format!("ERROR: {err}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn parse_network_config(cfg_json: String) -> bool {
|
||||||
|
parse_network_config_inner(&cfg_json)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn run_network_instance(cfg_json: String) -> bool {
|
||||||
|
run_network_instance_from_json(&cfg_json)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn collect_network_infos() -> Vec<KeyValuePair> {
|
||||||
|
exports::runtime_api::collect_network_infos()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn set_tun_fd(config_id: String, fd: i32) -> bool {
|
||||||
|
exports::runtime_api::set_tun_fd(config_id, fd, parse_instance_uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_network_config_schema() -> NetworkConfigSchema {
|
||||||
|
build_network_config_schema()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_network_config_field_mappings() -> Vec<ConfigFieldMapping> {
|
||||||
|
build_network_config_field_mappings()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exported_plain_object_schema_contains_core_networkconfig_metadata() {
|
||||||
|
let schema = get_network_config_schema();
|
||||||
|
assert_eq!(schema.name, "NetworkConfig");
|
||||||
|
assert_eq!(schema.node_kind, "schema");
|
||||||
|
assert!(
|
||||||
|
schema
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.any(|field| field.name == "network_name")
|
||||||
|
);
|
||||||
|
let secure_mode = schema
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.find(|field| field.name == "secure_mode")
|
||||||
|
.expect("secure_mode field");
|
||||||
|
assert!(
|
||||||
|
secure_mode
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.any(|field| field.name == "enabled")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_runtime_snapshot() -> RuntimeAggregateState {
|
||||||
|
exports::runtime_api::get_runtime_snapshot()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn set_kernel_snapshot_enabled(enabled: bool) {
|
||||||
|
set_snapshot_broadcast_enabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_runtime_snapshot_inner() -> RuntimeAggregateState {
|
||||||
|
exports::runtime_api::get_runtime_snapshot_inner()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn build_config_share_link(config_id: String, only_start: Option<bool>) -> Option<String> {
|
||||||
|
build_config_share_link_inner(&config_id, None, only_start.unwrap_or(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn parse_config_share_link(share_link: String) -> Option<SharedConfigLinkPayload> {
|
||||||
|
parse_config_share_link_inner(&share_link)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn import_config_share_link(
|
||||||
|
share_link: String,
|
||||||
|
display_name_override: Option<String>,
|
||||||
|
) -> Option<String> {
|
||||||
|
import_config_share_link_inner(&share_link, display_name_override)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
pub(crate) mod logging;
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub(crate) mod log_manager;
|
||||||
|
pub(crate) mod native_log;
|
||||||
+38
-34
@@ -1,7 +1,5 @@
|
|||||||
|
use super::log_manager;
|
||||||
use napi_derive_ohos::napi;
|
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::collections::HashMap;
|
||||||
use std::panic;
|
use std::panic;
|
||||||
use tracing::{Event, Subscriber};
|
use tracing::{Event, Subscriber};
|
||||||
@@ -10,8 +8,9 @@ use tracing_subscriber::layer::{Context, Layer};
|
|||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
|
|
||||||
static INITIALIZED: std::sync::Once = std::sync::Once::new();
|
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) {
|
fn panic_hook(info: &panic::PanicHookInfo) {
|
||||||
hilog_error!("RUST PANIC: {}", info);
|
log_manager::record_core_log(5, "RustPanic", &format!("{}", info));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
@@ -23,45 +22,40 @@ pub fn init_panic_hook() {
|
|||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
pub fn hilog_global_options(domain: u32, tag: String) {
|
pub fn hilog_global_options(domain: u32, tag: String) {
|
||||||
ohos_hilog_binding::forward_stdio_to_hilog();
|
let _ = domain;
|
||||||
set_global_options(LogOptions {
|
let _ = tag;
|
||||||
domain,
|
|
||||||
tag: Box::leak(tag.clone().into_boxed_str()),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
pub fn init_tracing_subscriber() {
|
pub fn init_tracing_subscriber() {
|
||||||
tracing_subscriber::registry()
|
TRACING_INITIALIZED.call_once(|| {
|
||||||
.with(CallbackLayer {
|
let _ = tracing_subscriber::registry()
|
||||||
callback: Box::new(tracing_callback),
|
.with(CallbackLayer {
|
||||||
})
|
callback: Box::new(tracing_callback),
|
||||||
.init();
|
})
|
||||||
|
.try_init();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tracing_callback(event: &Event, fields: HashMap<String, String>) {
|
fn tracing_callback(event: &Event, fields: HashMap<String, String>) {
|
||||||
let metadata = event.metadata();
|
let metadata = event.metadata();
|
||||||
#[cfg(target_env = "ohos")]
|
let loc = metadata
|
||||||
{
|
.target()
|
||||||
let loc = metadata.target().split("::").last().unwrap();
|
.split("::")
|
||||||
match *metadata.level() {
|
.last()
|
||||||
Level::TRACE => {
|
.unwrap_or(metadata.target());
|
||||||
hilog_debug!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>());
|
let level = match *metadata.level() {
|
||||||
}
|
Level::TRACE => 2,
|
||||||
Level::DEBUG => {
|
Level::DEBUG => 3,
|
||||||
hilog_debug!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>());
|
Level::INFO => 4,
|
||||||
}
|
Level::WARN => 6,
|
||||||
Level::INFO => {
|
Level::ERROR => 5,
|
||||||
hilog_info!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>());
|
};
|
||||||
}
|
if !log_manager::core_log_enabled(level) {
|
||||||
Level::WARN => {
|
return;
|
||||||
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 {
|
struct CallbackLayer {
|
||||||
@@ -70,6 +64,16 @@ struct CallbackLayer {
|
|||||||
|
|
||||||
impl<S: Subscriber> Layer<S> for CallbackLayer {
|
impl<S: Subscriber> Layer<S> for CallbackLayer {
|
||||||
fn on_event(&self, event: &Event, _ctx: Context<S>) {
|
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 提取字段值
|
// 使用 fmt::format::FmtSpan 提取字段值
|
||||||
let mut fields = HashMap::new();
|
let mut fields = HashMap::new();
|
||||||
let mut visitor = FieldCollector(&mut fields);
|
let mut visitor = FieldCollector(&mut fields);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub(crate) mod state;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub(crate) mod runtime_state;
|
||||||
@@ -0,0 +1,464 @@
|
|||||||
|
use easytier::proto::{api, common};
|
||||||
|
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()));
|
||||||
|
|
||||||
|
pub fn mark_tun_attached(instance_id: &str) {
|
||||||
|
if let Ok(mut guard) = ATTACHED_TUN_INSTANCE_IDS.lock() {
|
||||||
|
guard.insert(instance_id.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_tun_attached(instance_id: &str) {
|
||||||
|
if let Ok(mut guard) = ATTACHED_TUN_INSTANCE_IDS.lock() {
|
||||||
|
guard.remove(instance_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_tun_attached(instance_id: &str) -> bool {
|
||||||
|
ATTACHED_TUN_INSTANCE_IDS
|
||||||
|
.lock()
|
||||||
|
.map(|guard| guard.contains(instance_id))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct PeerConnStats {
|
||||||
|
pub rx_bytes: i64,
|
||||||
|
pub tx_bytes: i64,
|
||||||
|
pub rx_packets: i64,
|
||||||
|
pub tx_packets: i64,
|
||||||
|
pub latency_us: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct PeerConnInfo {
|
||||||
|
pub conn_id: String,
|
||||||
|
pub my_peer_id: i64,
|
||||||
|
pub peer_id: i64,
|
||||||
|
pub features: Vec<String>,
|
||||||
|
pub tunnel_type: Option<String>,
|
||||||
|
pub local_addr: Option<String>,
|
||||||
|
pub remote_addr: Option<String>,
|
||||||
|
pub resolved_remote_addr: Option<String>,
|
||||||
|
pub stats: Option<PeerConnStats>,
|
||||||
|
pub loss_rate: Option<f64>,
|
||||||
|
pub is_client: bool,
|
||||||
|
pub network_name: Option<String>,
|
||||||
|
pub is_closed: bool,
|
||||||
|
pub secure_auth_level: Option<i32>,
|
||||||
|
pub peer_identity_type: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct PeerInfo {
|
||||||
|
pub peer_id: i64,
|
||||||
|
pub default_conn_id: Option<String>,
|
||||||
|
pub directly_connected_conns: Vec<String>,
|
||||||
|
pub conns: Vec<PeerConnInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct RouteView {
|
||||||
|
pub peer_id: i64,
|
||||||
|
pub hostname: Option<String>,
|
||||||
|
pub ipv4: Option<String>,
|
||||||
|
pub ipv4_cidr: Option<String>,
|
||||||
|
pub ipv6_cidr: Option<String>,
|
||||||
|
pub proxy_cidrs: Vec<String>,
|
||||||
|
pub next_hop_peer_id: Option<i64>,
|
||||||
|
pub cost: Option<i32>,
|
||||||
|
pub path_latency: Option<i64>,
|
||||||
|
pub udp_nat_type: Option<i32>,
|
||||||
|
pub tcp_nat_type: Option<i32>,
|
||||||
|
pub inst_id: Option<String>,
|
||||||
|
pub version: Option<String>,
|
||||||
|
pub is_public_server: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct MyNodeInfo {
|
||||||
|
pub virtual_ipv4: Option<String>,
|
||||||
|
pub virtual_ipv4_cidr: Option<String>,
|
||||||
|
pub hostname: Option<String>,
|
||||||
|
pub version: Option<String>,
|
||||||
|
pub peer_id: Option<i64>,
|
||||||
|
pub listeners: Vec<String>,
|
||||||
|
pub vpn_portal_cfg: Option<String>,
|
||||||
|
pub udp_nat_type: Option<i32>,
|
||||||
|
pub tcp_nat_type: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct RuntimeInstanceState {
|
||||||
|
pub config_id: String,
|
||||||
|
pub instance_id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub running: bool,
|
||||||
|
pub tun_required: bool,
|
||||||
|
pub tun_attached: bool,
|
||||||
|
pub magic_dns_enabled: bool,
|
||||||
|
pub need_exit_node: bool,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
pub my_node_info: Option<MyNodeInfo>,
|
||||||
|
pub events: Vec<String>,
|
||||||
|
pub routes: Vec<RouteView>,
|
||||||
|
pub peers: Vec<PeerInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct TunAggregateState {
|
||||||
|
pub active: bool,
|
||||||
|
pub attached_instance_ids: Vec<String>,
|
||||||
|
pub aggregated_routes: Vec<String>,
|
||||||
|
pub dns_servers: Vec<String>,
|
||||||
|
pub need_rebuild: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct RuntimeAggregateState {
|
||||||
|
pub instances: Vec<RuntimeInstanceState>,
|
||||||
|
pub tun: TunAggregateState,
|
||||||
|
pub running_instance_count: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stringify_ipv4_inet(value: Option<common::Ipv4Inet>) -> Option<String> {
|
||||||
|
value.map(|v| v.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stringify_ipv6_inet(value: Option<common::Ipv6Inet>) -> Option<String> {
|
||||||
|
value.map(|v| v.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stringify_url(value: Option<common::Url>) -> Option<String> {
|
||||||
|
value.map(|v| v.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stringify_uuid(value: Option<common::Uuid>) -> Option<String> {
|
||||||
|
value.map(|v| v.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn optional_i32_to_i64(value: Option<i32>) -> Option<i64> {
|
||||||
|
value.map(|v| v as i64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn route_to_view(route: api::instance::Route) -> RouteView {
|
||||||
|
let stun = route.stun_info;
|
||||||
|
let feature_flag = route.feature_flag;
|
||||||
|
RouteView {
|
||||||
|
peer_id: route.peer_id as i64,
|
||||||
|
hostname: (!route.hostname.is_empty()).then_some(route.hostname),
|
||||||
|
ipv4: route
|
||||||
|
.ipv4_addr
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|inet| inet.address.as_ref())
|
||||||
|
.map(|addr| addr.to_string()),
|
||||||
|
ipv4_cidr: stringify_ipv4_inet(route.ipv4_addr),
|
||||||
|
ipv6_cidr: stringify_ipv6_inet(route.ipv6_addr),
|
||||||
|
proxy_cidrs: route.proxy_cidrs,
|
||||||
|
next_hop_peer_id: optional_u32_to_i64(route.next_hop_peer_id_latency_first)
|
||||||
|
.or_else(|| Some(route.next_hop_peer_id as i64)),
|
||||||
|
cost: Some(route.cost),
|
||||||
|
path_latency: optional_i32_to_i64(route.path_latency_latency_first)
|
||||||
|
.or_else(|| Some(route.path_latency as i64)),
|
||||||
|
udp_nat_type: stun.as_ref().map(|info| info.udp_nat_type),
|
||||||
|
tcp_nat_type: stun.as_ref().map(|info| info.tcp_nat_type),
|
||||||
|
inst_id: (!route.inst_id.is_empty()).then_some(route.inst_id),
|
||||||
|
version: (!route.version.is_empty()).then_some(route.version),
|
||||||
|
is_public_server: feature_flag.map(|flag| flag.is_public_server),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peer_conn_to_view(conn: api::instance::PeerConnInfo) -> PeerConnInfo {
|
||||||
|
let stats = conn.stats.map(|stats| PeerConnStats {
|
||||||
|
rx_bytes: stats.rx_bytes as i64,
|
||||||
|
tx_bytes: stats.tx_bytes as i64,
|
||||||
|
rx_packets: stats.rx_packets as i64,
|
||||||
|
tx_packets: stats.tx_packets as i64,
|
||||||
|
latency_us: stats.latency_us as i64,
|
||||||
|
});
|
||||||
|
|
||||||
|
PeerConnInfo {
|
||||||
|
conn_id: conn.conn_id,
|
||||||
|
my_peer_id: conn.my_peer_id as i64,
|
||||||
|
peer_id: conn.peer_id as i64,
|
||||||
|
features: conn.features,
|
||||||
|
tunnel_type: conn.tunnel.as_ref().map(|t| t.tunnel_type.clone()),
|
||||||
|
local_addr: conn
|
||||||
|
.tunnel
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|t| stringify_url(t.local_addr.clone())),
|
||||||
|
remote_addr: conn
|
||||||
|
.tunnel
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|t| stringify_url(t.remote_addr.clone())),
|
||||||
|
resolved_remote_addr: conn
|
||||||
|
.tunnel
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|t| stringify_url(t.resolved_remote_addr.clone())),
|
||||||
|
stats,
|
||||||
|
loss_rate: Some(conn.loss_rate as f64),
|
||||||
|
is_client: conn.is_client,
|
||||||
|
network_name: (!conn.network_name.is_empty()).then_some(conn.network_name),
|
||||||
|
is_closed: conn.is_closed,
|
||||||
|
secure_auth_level: Some(conn.secure_auth_level),
|
||||||
|
peer_identity_type: Some(conn.peer_identity_type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peer_to_view(peer: api::instance::PeerInfo) -> PeerInfo {
|
||||||
|
PeerInfo {
|
||||||
|
peer_id: peer.peer_id as i64,
|
||||||
|
default_conn_id: stringify_uuid(peer.default_conn_id),
|
||||||
|
directly_connected_conns: peer
|
||||||
|
.directly_connected_conns
|
||||||
|
.into_iter()
|
||||||
|
.map(|id| id.to_string())
|
||||||
|
.collect(),
|
||||||
|
conns: peer.conns.into_iter().map(peer_conn_to_view).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn my_node_info_to_view(info: api::manage::MyNodeInfo) -> MyNodeInfo {
|
||||||
|
MyNodeInfo {
|
||||||
|
virtual_ipv4: info
|
||||||
|
.virtual_ipv4
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|inet| inet.address.as_ref())
|
||||||
|
.map(|addr| addr.to_string()),
|
||||||
|
virtual_ipv4_cidr: stringify_ipv4_inet(info.virtual_ipv4),
|
||||||
|
hostname: (!info.hostname.is_empty()).then_some(info.hostname),
|
||||||
|
version: (!info.version.is_empty()).then_some(info.version),
|
||||||
|
peer_id: Some(info.peer_id as i64),
|
||||||
|
listeners: info
|
||||||
|
.listeners
|
||||||
|
.into_iter()
|
||||||
|
.map(|url| url.to_string())
|
||||||
|
.collect(),
|
||||||
|
vpn_portal_cfg: info.vpn_portal_cfg,
|
||||||
|
udp_nat_type: info.stun_info.as_ref().map(|stun| stun.udp_nat_type),
|
||||||
|
tcp_nat_type: info.stun_info.as_ref().map(|stun| stun.tcp_nat_type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runtime_instance_from_running_info(
|
||||||
|
config_id: String,
|
||||||
|
display_name: String,
|
||||||
|
magic_dns_enabled: bool,
|
||||||
|
need_exit_node: bool,
|
||||||
|
info: api::manage::NetworkInstanceRunningInfo,
|
||||||
|
) -> RuntimeInstanceState {
|
||||||
|
let tun_attached = info.running && is_tun_attached(&config_id);
|
||||||
|
let tun_required = info.running && (info.dev_name != "no_tun" || tun_attached);
|
||||||
|
|
||||||
|
RuntimeInstanceState {
|
||||||
|
config_id: config_id.clone(),
|
||||||
|
instance_id: config_id,
|
||||||
|
display_name,
|
||||||
|
running: info.running,
|
||||||
|
tun_required,
|
||||||
|
tun_attached,
|
||||||
|
magic_dns_enabled,
|
||||||
|
need_exit_node,
|
||||||
|
error_message: info.error_msg,
|
||||||
|
my_node_info: info.my_node_info.map(my_node_info_to_view),
|
||||||
|
events: info.events,
|
||||||
|
routes: info.routes.into_iter().map(route_to_view).collect(),
|
||||||
|
peers: info.peers.into_iter().map(peer_to_view).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||||
|
guarden = "0.1"
|
||||||
|
|
||||||
# Axum web framework
|
# Axum web framework
|
||||||
axum = { version = "0.8.4", features = ["macros"] }
|
axum = { version = "0.8.4", features = ["macros"] }
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ use easytier::{
|
|||||||
common::config::{
|
common::config::{
|
||||||
ConfigFileControl, ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader,
|
ConfigFileControl, ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader,
|
||||||
},
|
},
|
||||||
defer,
|
|
||||||
instance_manager::NetworkInstanceManager,
|
instance_manager::NetworkInstanceManager,
|
||||||
};
|
};
|
||||||
|
use guarden::defer;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::any;
|
use sqlx::any;
|
||||||
use tokio_util::task::AbortOnDropHandle;
|
use tokio_util::task::AbortOnDropHandle;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "easytier-gui",
|
"name": "easytier-gui",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.6.2",
|
"version": "2.6.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "easytier-gui"
|
name = "easytier-gui"
|
||||||
version = "2.6.2"
|
version = "2.6.4"
|
||||||
description = "EasyTier GUI"
|
description = "EasyTier GUI"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|||||||
@@ -490,10 +490,18 @@ async fn init_web_client(app: AppHandle, url: Option<String>) -> Result<(), Stri
|
|||||||
.ok_or_else(|| "Instance manager is not available".to_string())?;
|
.ok_or_else(|| "Instance manager is not available".to_string())?;
|
||||||
|
|
||||||
let hooks = Arc::new(manager::GuiHooks { app: app.clone() });
|
let hooks = Arc::new(manager::GuiHooks { app: app.clone() });
|
||||||
|
let machine_id_state_dir = app
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.with_context(|| "Failed to resolve machine id state directory")
|
||||||
|
.map_err(|e| format!("{:#}", e))?;
|
||||||
|
|
||||||
let web_client = web_client::run_web_client(
|
let web_client = web_client::run_web_client(
|
||||||
url.as_str(),
|
url.as_str(),
|
||||||
None,
|
easytier::common::MachineIdOptions {
|
||||||
|
explicit_machine_id: None,
|
||||||
|
state_dir: Some(machine_id_state_dir),
|
||||||
|
},
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
instance_manager,
|
instance_manager,
|
||||||
@@ -646,7 +654,8 @@ mod manager {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(super) enum PersistedConfigSource {
|
pub(super) enum PersistedConfigSource {
|
||||||
User,
|
User,
|
||||||
Webhook,
|
#[serde(alias = "webhook")]
|
||||||
|
Web,
|
||||||
#[serde(other)]
|
#[serde(other)]
|
||||||
#[default]
|
#[default]
|
||||||
Legacy,
|
Legacy,
|
||||||
@@ -656,15 +665,15 @@ mod manager {
|
|||||||
pub(super) fn from_runtime_source(source: ConfigSource) -> Self {
|
pub(super) fn from_runtime_source(source: ConfigSource) -> Self {
|
||||||
match source {
|
match source {
|
||||||
ConfigSource::User => Self::User,
|
ConfigSource::User => Self::User,
|
||||||
ConfigSource::Webhook => Self::Webhook,
|
ConfigSource::Web => Self::Web,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_persisted(self, incoming: Self) -> Self {
|
fn merge_persisted(self, incoming: Self) -> Self {
|
||||||
match (self, incoming) {
|
match (self, incoming) {
|
||||||
// Older runtimes report missing source as `user`. Keep the stronger persisted
|
// Older runtimes report missing source as `user`. Keep the stronger persisted
|
||||||
// ownership until webhook sync or an explicit user save repairs it.
|
// ownership until web sync or an explicit user save repairs it.
|
||||||
(Self::Webhook, Self::User) | (Self::Legacy, Self::User) => self,
|
(Self::Web, Self::User) | (Self::Legacy, Self::User) => self,
|
||||||
(_, next) => next,
|
(_, next) => next,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -672,13 +681,13 @@ mod manager {
|
|||||||
fn to_runtime_source(self) -> ConfigSource {
|
fn to_runtime_source(self) -> ConfigSource {
|
||||||
match self {
|
match self {
|
||||||
Self::User | Self::Legacy => ConfigSource::User,
|
Self::User | Self::Legacy => ConfigSource::User,
|
||||||
Self::Webhook => ConfigSource::Webhook,
|
Self::Web => ConfigSource::Web,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, target_os = "android"))]
|
#[cfg(any(test, target_os = "android"))]
|
||||||
fn is_webhook_like(self) -> bool {
|
fn is_web_like(self) -> bool {
|
||||||
matches!(self, Self::Webhook)
|
matches!(self, Self::Web)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -910,7 +919,7 @@ mod manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
pub fn get_enabled_instances_with_webhook_like_tun_ids(
|
pub fn get_enabled_instances_with_web_like_tun_ids(
|
||||||
&self,
|
&self,
|
||||||
) -> impl Iterator<Item = uuid::Uuid> + '_ {
|
) -> impl Iterator<Item = uuid::Uuid> + '_ {
|
||||||
self.storage
|
self.storage
|
||||||
@@ -918,7 +927,7 @@ mod manager {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
||||||
.filter(|v| !v.config.no_tun())
|
.filter(|v| !v.config.no_tun())
|
||||||
.filter(|v| v.source.is_webhook_like())
|
.filter(|v| v.source.is_web_like())
|
||||||
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -926,12 +935,11 @@ mod manager {
|
|||||||
pub(super) async fn disable_instances_with_tun(
|
pub(super) async fn disable_instances_with_tun(
|
||||||
&self,
|
&self,
|
||||||
app: &AppHandle,
|
app: &AppHandle,
|
||||||
webhook_only: bool,
|
web_only: bool,
|
||||||
) -> Result<(), easytier::rpc_service::remote_client::RemoteClientError<anyhow::Error>>
|
) -> Result<(), easytier::rpc_service::remote_client::RemoteClientError<anyhow::Error>>
|
||||||
{
|
{
|
||||||
let inst_ids: Vec<uuid::Uuid> = if webhook_only {
|
let inst_ids: Vec<uuid::Uuid> = if web_only {
|
||||||
self.get_enabled_instances_with_webhook_like_tun_ids()
|
self.get_enabled_instances_with_web_like_tun_ids().collect()
|
||||||
.collect()
|
|
||||||
} else {
|
} else {
|
||||||
self.get_enabled_instances_with_tun_ids().collect()
|
self.get_enabled_instances_with_tun_ids().collect()
|
||||||
};
|
};
|
||||||
@@ -969,7 +977,7 @@ mod manager {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
PersistedConfigSource::Webhook => {
|
PersistedConfigSource::Web => {
|
||||||
self.disable_instances_with_tun(app, true)
|
self.disable_instances_with_tun(app, true)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
@@ -1179,26 +1187,46 @@ mod manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn persisted_source_merge_keeps_legacy_and_webhook_over_ambiguous_user() {
|
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() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::User),
|
PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::User),
|
||||||
PersistedConfigSource::Legacy
|
PersistedConfigSource::Legacy
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
PersistedConfigSource::Webhook.merge_persisted(PersistedConfigSource::User),
|
PersistedConfigSource::Web.merge_persisted(PersistedConfigSource::User),
|
||||||
PersistedConfigSource::Webhook
|
PersistedConfigSource::Web
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::Webhook),
|
PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::Web),
|
||||||
PersistedConfigSource::Webhook
|
PersistedConfigSource::Web
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn only_webhook_configs_are_webhook_like() {
|
fn only_web_configs_are_web_like() {
|
||||||
assert!(!PersistedConfigSource::Legacy.is_webhook_like());
|
assert!(!PersistedConfigSource::Legacy.is_web_like());
|
||||||
assert!(!PersistedConfigSource::User.is_webhook_like());
|
assert!(!PersistedConfigSource::User.is_web_like());
|
||||||
assert!(PersistedConfigSource::Webhook.is_webhook_like());
|
assert!(PersistedConfigSource::Web.is_web_like());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"createUpdaterArtifacts": false
|
"createUpdaterArtifacts": false
|
||||||
},
|
},
|
||||||
"productName": "easytier-gui",
|
"productName": "easytier-gui",
|
||||||
"version": "2.6.2",
|
"version": "2.6.4",
|
||||||
"identifier": "com.kkrainbow.easytier",
|
"identifier": "com.kkrainbow.easytier",
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"shell": {
|
"shell": {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { Api, NetworkTypes } from 'easytier-frontend-lib'
|
import { Api, NetworkTypes } from 'easytier-frontend-lib'
|
||||||
import { GetNetworkMetasResponse } from 'node_modules/easytier-frontend-lib/dist/modules/api'
|
import { GetNetworkMetasResponse } from 'node_modules/easytier-frontend-lib/dist/modules/api'
|
||||||
|
import { type ConfigSource, normalizeConfigSource } from './config_source'
|
||||||
|
|
||||||
type NetworkConfig = NetworkTypes.NetworkConfig
|
type NetworkConfig = NetworkTypes.NetworkConfig
|
||||||
type ValidateConfigResponse = Api.ValidateConfigResponse
|
type ValidateConfigResponse = Api.ValidateConfigResponse
|
||||||
type ListNetworkInstanceIdResponse = Api.ListNetworkInstanceIdResponse
|
type ListNetworkInstanceIdResponse = Api.ListNetworkInstanceIdResponse
|
||||||
type ConfigSource = 'user' | 'webhook' | 'legacy'
|
|
||||||
interface ServiceOptions {
|
interface ServiceOptions {
|
||||||
config_dir: string
|
config_dir: string
|
||||||
rpc_portal: string
|
rpc_portal: string
|
||||||
@@ -32,14 +31,14 @@ function parseStoredConfigs(raw: string | null): StoredGuiConfig[] {
|
|||||||
if (entry && typeof entry === 'object' && 'config' in entry) {
|
if (entry && typeof entry === 'object' && 'config' in entry) {
|
||||||
const { config, source } = entry as {
|
const { config, source } = entry as {
|
||||||
config?: NetworkConfig
|
config?: NetworkConfig
|
||||||
source?: ConfigSource
|
source?: unknown
|
||||||
}
|
}
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return [{
|
return [{
|
||||||
config: NetworkTypes.normalizeNetworkConfig(config),
|
config: NetworkTypes.normalizeNetworkConfig(config),
|
||||||
source: source === 'user' || source === 'webhook' ? source : 'legacy',
|
source: normalizeConfigSource(source),
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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,10 +2,11 @@ import { Event, listen } from "@tauri-apps/api/event";
|
|||||||
import { type } from "@tauri-apps/plugin-os";
|
import { type } from "@tauri-apps/plugin-os";
|
||||||
import { NetworkTypes } from "easytier-frontend-lib"
|
import { NetworkTypes } from "easytier-frontend-lib"
|
||||||
import { Utils } from "easytier-frontend-lib";
|
import { Utils } from "easytier-frontend-lib";
|
||||||
|
import { normalizeConfigSource } from './config_source'
|
||||||
|
|
||||||
interface StoredGuiConfig {
|
interface StoredGuiConfig {
|
||||||
config: NetworkTypes.NetworkConfig
|
config: NetworkTypes.NetworkConfig
|
||||||
source?: 'user' | 'webhook' | 'legacy'
|
source?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
const EVENTS = Object.freeze({
|
const EVENTS = Object.freeze({
|
||||||
@@ -24,7 +25,7 @@ function onSaveConfigs(event: Event<StoredGuiConfig[]>) {
|
|||||||
'networkList',
|
'networkList',
|
||||||
JSON.stringify(event.payload.map(({ config, source }) => ({
|
JSON.stringify(event.payload.map(({ config, source }) => ({
|
||||||
config: NetworkTypes.normalizeNetworkConfig(config),
|
config: NetworkTypes.normalizeNetworkConfig(config),
|
||||||
source: source ?? 'legacy',
|
source: normalizeConfigSource(source),
|
||||||
}))),
|
}))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface ServiceMode extends WebClientConfig {
|
|||||||
rpc_portal: string
|
rpc_portal: string
|
||||||
file_log_level: 'off' | 'warn' | 'info' | 'debug' | 'trace'
|
file_log_level: 'off' | 'warn' | 'info' | 'debug' | 'trace'
|
||||||
file_log_dir: string
|
file_log_dir: string
|
||||||
|
installed_core_version?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RemoteMode {
|
export interface RemoteMode {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { useToast, useConfirm } from 'primevue'
|
|||||||
import { loadMode, saveMode, WebClientConfig, type Mode } from '~/composables/mode'
|
import { loadMode, saveMode, WebClientConfig, type Mode } from '~/composables/mode'
|
||||||
import { saveLastNetworkInstanceId, loadLastNetworkInstanceId } from '~/composables/config'
|
import { saveLastNetworkInstanceId, loadLastNetworkInstanceId } from '~/composables/config'
|
||||||
import ModeSwitcher from '~/components/ModeSwitcher.vue'
|
import ModeSwitcher from '~/components/ModeSwitcher.vue'
|
||||||
import { getServiceStatus } from '~/composables/backend'
|
import { getEasytierVersion, getServiceStatus } from '~/composables/backend'
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
const confirm = useConfirm()
|
const confirm = useConfirm()
|
||||||
@@ -85,6 +85,20 @@ async function onUninstallService() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripModeMetadata(mode: Mode) {
|
||||||
|
if (mode.mode !== 'service') {
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceConfig = { ...mode }
|
||||||
|
delete serviceConfig.installed_core_version
|
||||||
|
return serviceConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
function modeConfigChanged(next: Mode) {
|
||||||
|
return JSON.stringify(stripModeMetadata(next)) !== JSON.stringify(stripModeMetadata(currentMode.value))
|
||||||
|
}
|
||||||
|
|
||||||
async function onStopService() {
|
async function onStopService() {
|
||||||
isModeSaving.value = true
|
isModeSaving.value = true
|
||||||
manualDisconnect.value = true
|
manualDisconnect.value = true
|
||||||
@@ -134,13 +148,14 @@ async function initWithMode(mode: Mode) {
|
|||||||
}
|
}
|
||||||
url = mode.remote_rpc_address
|
url = mode.remote_rpc_address
|
||||||
break;
|
break;
|
||||||
case 'service':
|
case 'service': {
|
||||||
if (!mode.config_dir || !mode.file_log_dir || !mode.file_log_level || !mode.rpc_portal) {
|
if (!mode.config_dir || !mode.file_log_dir || !mode.file_log_level || !mode.rpc_portal) {
|
||||||
toast.add({ severity: 'error', summary: t('error'), detail: t('mode.service_config_empty'), life: 10000 })
|
toast.add({ severity: 'error', summary: t('error'), detail: t('mode.service_config_empty'), life: 10000 })
|
||||||
return initWithMode({ ...mode, mode: 'normal' });
|
return initWithMode({ ...mode, mode: 'normal' });
|
||||||
}
|
}
|
||||||
let serviceStatus = await getServiceStatus()
|
let serviceStatus = await getServiceStatus()
|
||||||
if (serviceStatus === "NotInstalled" || JSON.stringify(mode) !== JSON.stringify(currentMode.value)) {
|
const coreVersion = await getEasytierVersion()
|
||||||
|
if (serviceStatus === "NotInstalled" || modeConfigChanged(mode) || mode.installed_core_version !== coreVersion) {
|
||||||
mode.config_server_url = mode.config_server_url || undefined
|
mode.config_server_url = mode.config_server_url || undefined
|
||||||
await initService({
|
await initService({
|
||||||
config_dir: mode.config_dir,
|
config_dir: mode.config_dir,
|
||||||
@@ -149,6 +164,7 @@ async function initWithMode(mode: Mode) {
|
|||||||
rpc_portal: mode.rpc_portal,
|
rpc_portal: mode.rpc_portal,
|
||||||
config_server: mode.config_server_url,
|
config_server: mode.config_server_url,
|
||||||
})
|
})
|
||||||
|
mode.installed_core_version = coreVersion
|
||||||
serviceStatus = await getServiceStatus()
|
serviceStatus = await getServiceStatus()
|
||||||
}
|
}
|
||||||
if (serviceStatus === "Stopped") {
|
if (serviceStatus === "Stopped") {
|
||||||
@@ -157,6 +173,7 @@ async function initWithMode(mode: Mode) {
|
|||||||
url = "tcp://" + mode.rpc_portal.replace("0.0.0.0", "127.0.0.1")
|
url = "tcp://" + mode.rpc_portal.replace("0.0.0.0", "127.0.0.1")
|
||||||
retrys = 5
|
retrys = 5
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'normal':
|
case 'normal':
|
||||||
url = mode.rpc_portal;
|
url = mode.rpc_portal;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "easytier-rpc-build"
|
|
||||||
description = "Protobuf RPC Service Generator for EasyTier"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition.workspace = true
|
|
||||||
homepage = "https://github.com/EasyTier/EasyTier"
|
|
||||||
repository = "https://github.com/EasyTier/EasyTier"
|
|
||||||
authors = ["kkrainbow"]
|
|
||||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
|
||||||
categories = ["network-programming", "command-line-utilities"]
|
|
||||||
license-file = "LICENSE"
|
|
||||||
readme = "README.md"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
heck = "0.5"
|
|
||||||
prost-build = "0.13"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
internal-namespace = []
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../LICENSE
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# Introduction
|
|
||||||
|
|
||||||
This is a protobuf rpc service stub generator for [EasyTier](https://github.com/EasyTier/EasyTier) project.
|
|
||||||
@@ -1,449 +0,0 @@
|
|||||||
extern crate heck;
|
|
||||||
extern crate prost_build;
|
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
#[cfg(feature = "internal-namespace")]
|
|
||||||
const NAMESPACE: &str = "crate::proto::rpc_types";
|
|
||||||
|
|
||||||
#[cfg(not(feature = "internal-namespace"))]
|
|
||||||
const NAMESPACE: &str = "easytier::proto::rpc_types";
|
|
||||||
|
|
||||||
/// The service generator to be used with `prost-build` to generate RPC implementations for
|
|
||||||
/// `prost-simple-rpc`.
|
|
||||||
///
|
|
||||||
/// See the crate-level documentation for more info.
|
|
||||||
#[allow(missing_copy_implementations)]
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
pub struct ServiceGenerator {
|
|
||||||
_private: (),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl prost_build::ServiceGenerator for ServiceGenerator {
|
|
||||||
fn generate(&mut self, service: prost_build::Service, mut buf: &mut String) {
|
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
let descriptor_name = format!("{}Descriptor", service.name);
|
|
||||||
let server_name = format!("{}Server", service.name);
|
|
||||||
let client_name = format!("{}Client", service.name);
|
|
||||||
let method_descriptor_name = format!("{}MethodDescriptor", service.name);
|
|
||||||
|
|
||||||
let mut trait_methods = String::new();
|
|
||||||
let mut weak_impl_methods = String::new();
|
|
||||||
let mut enum_methods = String::new();
|
|
||||||
let mut list_enum_methods = String::new();
|
|
||||||
let mut client_methods = String::new();
|
|
||||||
let mut client_own_methods = String::new();
|
|
||||||
let mut match_name_methods = String::new();
|
|
||||||
let mut match_proto_name_methods = String::new();
|
|
||||||
let mut match_input_type_methods = String::new();
|
|
||||||
let mut match_input_proto_type_methods = String::new();
|
|
||||||
let mut match_output_type_methods = String::new();
|
|
||||||
let mut match_output_proto_type_methods = String::new();
|
|
||||||
let mut match_handle_methods = String::new();
|
|
||||||
// generate trait default method Xxx::json_call_method match branch
|
|
||||||
let mut match_trait_json_methods = String::new();
|
|
||||||
|
|
||||||
let mut match_method_try_from = String::new();
|
|
||||||
|
|
||||||
for (idx, method) in service.methods.iter().enumerate() {
|
|
||||||
assert!(
|
|
||||||
!method.client_streaming,
|
|
||||||
"Client streaming not yet supported for method {}",
|
|
||||||
method.proto_name
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!method.server_streaming,
|
|
||||||
"Server streaming not yet supported for method {}",
|
|
||||||
method.proto_name
|
|
||||||
);
|
|
||||||
|
|
||||||
ServiceGenerator::write_comments(&mut trait_methods, 4, &method.comments).unwrap();
|
|
||||||
writeln!(
|
|
||||||
trait_methods,
|
|
||||||
r#" async fn {name}(&self, ctrl: Self::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}>;"#,
|
|
||||||
name = method.name,
|
|
||||||
input_type = method.input_type,
|
|
||||||
output_type = method.output_type,
|
|
||||||
namespace = NAMESPACE,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
writeln!(
|
|
||||||
weak_impl_methods,
|
|
||||||
r#" async fn {method_name}(&self, ctrl: Self::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}> {{
|
|
||||||
let Some(service) = self.upgrade() else {{
|
|
||||||
return Err({namespace}::error::Error::Shutdown);
|
|
||||||
}};
|
|
||||||
service.{method_name}(ctrl, input).await
|
|
||||||
}}"#,
|
|
||||||
method_name = method.name,
|
|
||||||
input_type = method.input_type,
|
|
||||||
output_type = method.output_type,
|
|
||||||
namespace = NAMESPACE,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
ServiceGenerator::write_comments(&mut enum_methods, 4, &method.comments).unwrap();
|
|
||||||
writeln!(
|
|
||||||
enum_methods,
|
|
||||||
" {name} = {index},",
|
|
||||||
name = method.proto_name,
|
|
||||||
index = idx + 1
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
writeln!(
|
|
||||||
match_method_try_from,
|
|
||||||
" {index} => Ok({service_name}MethodDescriptor::{name}),",
|
|
||||||
service_name = service.name,
|
|
||||||
name = method.proto_name,
|
|
||||||
index = idx + 1,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
writeln!(
|
|
||||||
list_enum_methods,
|
|
||||||
" {service_name}MethodDescriptor::{name},",
|
|
||||||
service_name = service.name,
|
|
||||||
name = method.proto_name
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
writeln!(
|
|
||||||
client_methods,
|
|
||||||
r#" async fn {name}(&self, ctrl: H::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}> {{
|
|
||||||
{client_name}Client::{name}_inner(self.0.clone(), ctrl, input).await
|
|
||||||
}}"#,
|
|
||||||
name = method.name,
|
|
||||||
input_type = method.input_type,
|
|
||||||
output_type = method.output_type,
|
|
||||||
client_name = service.name,
|
|
||||||
namespace = NAMESPACE,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
writeln!(
|
|
||||||
client_own_methods,
|
|
||||||
r#" async fn {name}_inner(handler: H, ctrl: H::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}> {{
|
|
||||||
{namespace}::__rt::call_method(handler, ctrl, {method_descriptor_name}::{proto_name}, input).await
|
|
||||||
}}"#,
|
|
||||||
name = method.name,
|
|
||||||
method_descriptor_name = method_descriptor_name,
|
|
||||||
proto_name = method.proto_name,
|
|
||||||
input_type = method.input_type,
|
|
||||||
output_type = method.output_type,
|
|
||||||
namespace = NAMESPACE,
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
let case = format!(
|
|
||||||
" {service_name}MethodDescriptor::{proto_name} => ",
|
|
||||||
service_name = service.name,
|
|
||||||
proto_name = method.proto_name
|
|
||||||
);
|
|
||||||
|
|
||||||
writeln!(match_name_methods, "{}{:?},", case, method.name).unwrap();
|
|
||||||
writeln!(match_proto_name_methods, "{}{:?},", case, method.proto_name).unwrap();
|
|
||||||
writeln!(
|
|
||||||
match_input_type_methods,
|
|
||||||
"{}::std::any::TypeId::of::<{}>(),",
|
|
||||||
case, method.input_type
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
writeln!(
|
|
||||||
match_input_proto_type_methods,
|
|
||||||
"{}{:?},",
|
|
||||||
case, method.input_proto_type
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
writeln!(
|
|
||||||
match_output_type_methods,
|
|
||||||
"{}::std::any::TypeId::of::<{}>(),",
|
|
||||||
case, method.output_type
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
writeln!(
|
|
||||||
match_output_proto_type_methods,
|
|
||||||
"{}{:?},",
|
|
||||||
case, method.output_proto_type
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
write!(
|
|
||||||
match_handle_methods,
|
|
||||||
r#"{} {{
|
|
||||||
let decoded: {input_type} = {namespace}::__rt::decode(input)?;
|
|
||||||
let ret = service.{name}(ctrl, decoded).await?;
|
|
||||||
{namespace}::__rt::encode(ret)
|
|
||||||
}}
|
|
||||||
"#,
|
|
||||||
case,
|
|
||||||
input_type = method.input_type,
|
|
||||||
name = method.name,
|
|
||||||
namespace = NAMESPACE,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
write!(
|
|
||||||
match_trait_json_methods,
|
|
||||||
r#" "{name}" | "{proto_name}" => {{
|
|
||||||
let req: {input_type} = ::serde_json::from_value(json).map_err(|e| {namespace}::error::Error::MalformatRpcPacket(format!("json error: {{}}", e)))?;
|
|
||||||
let resp = self.{typed_method}(ctrl, req).await?;
|
|
||||||
Ok(::serde_json::to_value(resp).map_err(|e| {namespace}::error::Error::MalformatRpcPacket(format!("json error: {{}}", e)))?)
|
|
||||||
}}
|
|
||||||
"#,
|
|
||||||
name = method.name,
|
|
||||||
proto_name = method.proto_name,
|
|
||||||
input_type = method.input_type,
|
|
||||||
typed_method = method.name,
|
|
||||||
namespace = NAMESPACE,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
ServiceGenerator::write_comments(&mut buf, 0, &service.comments).unwrap();
|
|
||||||
write!(
|
|
||||||
buf,
|
|
||||||
r#"
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
|
||||||
pub trait {name} {{
|
|
||||||
type Controller: {namespace}::controller::Controller;
|
|
||||||
|
|
||||||
{trait_methods}
|
|
||||||
|
|
||||||
async fn json_call_method(
|
|
||||||
&self,
|
|
||||||
ctrl: Self::Controller,
|
|
||||||
method_name: &str,
|
|
||||||
json: ::serde_json::Value,
|
|
||||||
) -> {namespace}::error::Result<::serde_json::Value> {{
|
|
||||||
match method_name {{
|
|
||||||
{match_trait_json_methods}
|
|
||||||
_ => Err({namespace}::error::Error::InvalidMethodIndex(0, method_name.to_string())),
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl<T> {name} for ::std::sync::Weak<T>
|
|
||||||
where
|
|
||||||
T: Send + Sync + 'static,
|
|
||||||
::std::sync::Arc<T>: {name},
|
|
||||||
{{
|
|
||||||
type Controller = <::std::sync::Arc<T> as {name}>::Controller;
|
|
||||||
|
|
||||||
{weak_impl_methods}
|
|
||||||
}}
|
|
||||||
|
|
||||||
/// A service descriptor for a `{name}`.
|
|
||||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Default)]
|
|
||||||
pub struct {descriptor_name};
|
|
||||||
|
|
||||||
/// Methods available on a `{name}`.
|
|
||||||
///
|
|
||||||
/// This can be used as a key when routing requests for servers/clients of a `{name}`.
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
|
||||||
#[repr(u8)]
|
|
||||||
pub enum {method_descriptor_name} {{
|
|
||||||
{enum_methods}
|
|
||||||
}}
|
|
||||||
|
|
||||||
impl std::convert::TryFrom<u8> for {method_descriptor_name} {{
|
|
||||||
type Error = {namespace}::error::Error;
|
|
||||||
fn try_from(value: u8) -> {namespace}::error::Result<Self> {{
|
|
||||||
match value {{
|
|
||||||
{match_method_try_from}
|
|
||||||
_ => Err({namespace}::error::Error::InvalidMethodIndex(value, "{name}".to_string())),
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
/// A client for a `{name}`.
|
|
||||||
///
|
|
||||||
/// This implements the `{name}` trait by dispatching all method calls to the supplied `Handler`.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct {client_name}<H>(H) where H: {namespace}::handler::Handler;
|
|
||||||
|
|
||||||
impl<H> {client_name}<H> where H: {namespace}::handler::Handler<Descriptor = {descriptor_name}> {{
|
|
||||||
/// Creates a new client instance that delegates all method calls to the supplied handler.
|
|
||||||
pub fn new(handler: H) -> {client_name}<H> {{
|
|
||||||
{client_name}(handler)
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
impl<H> {client_name}<H> where H: {namespace}::handler::Handler<Descriptor = {descriptor_name}> {{
|
|
||||||
{client_own_methods}
|
|
||||||
}}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl<H> {name} for {client_name}<H> where H: {namespace}::handler::Handler<Descriptor = {descriptor_name}> {{
|
|
||||||
type Controller = H::Controller;
|
|
||||||
|
|
||||||
{client_methods}
|
|
||||||
}}
|
|
||||||
|
|
||||||
pub struct {client_name}Factory<C: {namespace}::controller::Controller>(std::marker::PhantomData<C>);
|
|
||||||
|
|
||||||
impl<C: {namespace}::controller::Controller> Clone for {client_name}Factory<C> {{
|
|
||||||
fn clone(&self) -> Self {{
|
|
||||||
Self(std::marker::PhantomData)
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
impl<C> {namespace}::__rt::RpcClientFactory for {client_name}Factory<C> where C: {namespace}::controller::Controller {{
|
|
||||||
type Descriptor = {descriptor_name};
|
|
||||||
type ClientImpl = Box<dyn {name}<Controller = C> + Send + Sync + 'static>;
|
|
||||||
type Controller = C;
|
|
||||||
|
|
||||||
fn new(handler: impl {namespace}::handler::Handler<Descriptor = Self::Descriptor, Controller = Self::Controller>) -> Self::ClientImpl {{
|
|
||||||
Box::new({client_name}::new(handler))
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
/// A server for a `{name}`.
|
|
||||||
///
|
|
||||||
/// This implements the `Server` trait by handling requests and dispatch them to methods on the
|
|
||||||
/// supplied `{name}`.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct {server_name}<A>(A) where A: {name} + Clone + Send + 'static;
|
|
||||||
|
|
||||||
impl<T> {server_name}<::std::sync::Weak<T>>
|
|
||||||
where
|
|
||||||
T: Send + Sync + 'static,
|
|
||||||
::std::sync::Arc<T>: {name},
|
|
||||||
{{
|
|
||||||
pub fn new_arc(service: ::std::sync::Arc<T>) -> {server_name}<::std::sync::Weak<T>> {{
|
|
||||||
{server_name}(::std::sync::Arc::downgrade(&service))
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
impl<A> {server_name}<A> where A: {name} + Clone + Send + 'static {{
|
|
||||||
/// Creates a new server instance that dispatches all calls to the supplied service.
|
|
||||||
pub fn new(service: A) -> {server_name}<A> {{
|
|
||||||
{server_name}(service)
|
|
||||||
}}
|
|
||||||
|
|
||||||
async fn call_inner(
|
|
||||||
service: A,
|
|
||||||
method: {method_descriptor_name},
|
|
||||||
ctrl: A::Controller,
|
|
||||||
input: ::bytes::Bytes)
|
|
||||||
-> {namespace}::error::Result<::bytes::Bytes> {{
|
|
||||||
match method {{
|
|
||||||
{match_handle_methods}
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
impl {namespace}::descriptor::ServiceDescriptor for {descriptor_name} {{
|
|
||||||
type Method = {method_descriptor_name};
|
|
||||||
fn name(&self) -> &'static str {{ {name:?} }}
|
|
||||||
fn proto_name(&self) -> &'static str {{ {proto_name:?} }}
|
|
||||||
fn package(&self) -> &'static str {{ {package:?} }}
|
|
||||||
fn methods(&self) -> &'static [Self::Method] {{
|
|
||||||
&[ {list_enum_methods} ]
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl<A> {namespace}::handler::Handler for {server_name}<A>
|
|
||||||
where
|
|
||||||
A: {name} + Clone + Send + Sync + 'static {{
|
|
||||||
type Descriptor = {descriptor_name};
|
|
||||||
type Controller = A::Controller;
|
|
||||||
|
|
||||||
async fn call(
|
|
||||||
&self,
|
|
||||||
ctrl: A::Controller,
|
|
||||||
method: {method_descriptor_name},
|
|
||||||
input: ::bytes::Bytes)
|
|
||||||
-> {namespace}::error::Result<::bytes::Bytes> {{
|
|
||||||
{server_name}::call_inner(self.0.clone(), method, ctrl, input).await
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
impl {namespace}::descriptor::MethodDescriptor for {method_descriptor_name} {{
|
|
||||||
fn name(&self) -> &'static str {{
|
|
||||||
match *self {{
|
|
||||||
{match_name_methods}
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
fn proto_name(&self) -> &'static str {{
|
|
||||||
match *self {{
|
|
||||||
{match_proto_name_methods}
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
fn input_type(&self) -> ::std::any::TypeId {{
|
|
||||||
match *self {{
|
|
||||||
{match_input_type_methods}
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
fn input_proto_type(&self) -> &'static str {{
|
|
||||||
match *self {{
|
|
||||||
{match_input_proto_type_methods}
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
fn output_type(&self) -> ::std::any::TypeId {{
|
|
||||||
match *self {{
|
|
||||||
{match_output_type_methods}
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
fn output_proto_type(&self) -> &'static str {{
|
|
||||||
match *self {{
|
|
||||||
{match_output_proto_type_methods}
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
fn index(&self) -> u8 {{
|
|
||||||
*self as u8
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
"#,
|
|
||||||
name = service.name,
|
|
||||||
descriptor_name = descriptor_name,
|
|
||||||
server_name = server_name,
|
|
||||||
client_name = client_name,
|
|
||||||
method_descriptor_name = method_descriptor_name,
|
|
||||||
proto_name = service.proto_name,
|
|
||||||
package = service.package,
|
|
||||||
trait_methods = trait_methods,
|
|
||||||
weak_impl_methods = weak_impl_methods,
|
|
||||||
enum_methods = enum_methods,
|
|
||||||
list_enum_methods = list_enum_methods,
|
|
||||||
client_own_methods = client_own_methods,
|
|
||||||
client_methods = client_methods,
|
|
||||||
match_name_methods = match_name_methods,
|
|
||||||
match_proto_name_methods = match_proto_name_methods,
|
|
||||||
match_input_type_methods = match_input_type_methods,
|
|
||||||
match_input_proto_type_methods = match_input_proto_type_methods,
|
|
||||||
match_output_type_methods = match_output_type_methods,
|
|
||||||
match_output_proto_type_methods = match_output_proto_type_methods,
|
|
||||||
match_handle_methods = match_handle_methods,
|
|
||||||
match_trait_json_methods = match_trait_json_methods,
|
|
||||||
namespace = NAMESPACE,
|
|
||||||
).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServiceGenerator {
|
|
||||||
fn write_comments<W>(
|
|
||||||
mut write: W,
|
|
||||||
indent: usize,
|
|
||||||
comments: &prost_build::Comments,
|
|
||||||
) -> fmt::Result
|
|
||||||
where
|
|
||||||
W: fmt::Write,
|
|
||||||
{
|
|
||||||
for comment in &comments.leading {
|
|
||||||
for line in comment.lines().filter(|s| !s.is_empty()) {
|
|
||||||
writeln!(write, "{}///{}", " ".repeat(indent), line)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "easytier-web"
|
name = "easytier-web"
|
||||||
version = "2.6.2"
|
version = "2.6.4"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
||||||
|
|
||||||
@@ -53,6 +53,7 @@ clap = { version = "4.4.8", features = [
|
|||||||
"unicode",
|
"unicode",
|
||||||
"derive",
|
"derive",
|
||||||
"wrap_help",
|
"wrap_help",
|
||||||
|
"env",
|
||||||
] }
|
] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ const bool_flags: BoolFlag[] = [
|
|||||||
{ field: 'latency_first', help: 'latency_first_help' },
|
{ field: 'latency_first', help: 'latency_first_help' },
|
||||||
{ field: 'use_smoltcp', help: 'use_smoltcp_help' },
|
{ field: 'use_smoltcp', help: 'use_smoltcp_help' },
|
||||||
{ field: 'disable_ipv6', help: 'disable_ipv6_help' },
|
{ field: 'disable_ipv6', help: 'disable_ipv6_help' },
|
||||||
|
{ field: 'ipv6_public_addr_auto', help: 'ipv6_public_addr_auto_help' },
|
||||||
{ field: 'enable_kcp_proxy', help: 'enable_kcp_proxy_help' },
|
{ field: 'enable_kcp_proxy', help: 'enable_kcp_proxy_help' },
|
||||||
{ field: 'disable_kcp_input', help: 'disable_kcp_input_help' },
|
{ field: 'disable_kcp_input', help: 'disable_kcp_input_help' },
|
||||||
{ field: 'enable_quic_proxy', help: 'enable_quic_proxy_help' },
|
{ field: 'enable_quic_proxy', help: 'enable_quic_proxy_help' },
|
||||||
@@ -98,6 +99,8 @@ const bool_flags: BoolFlag[] = [
|
|||||||
{ field: 'disable_encryption', help: 'disable_encryption_help' },
|
{ field: 'disable_encryption', help: 'disable_encryption_help' },
|
||||||
{ field: 'disable_tcp_hole_punching', help: 'disable_tcp_hole_punching_help' },
|
{ field: 'disable_tcp_hole_punching', help: 'disable_tcp_hole_punching_help' },
|
||||||
{ field: 'disable_udp_hole_punching', help: 'disable_udp_hole_punching_help' },
|
{ field: 'disable_udp_hole_punching', help: 'disable_udp_hole_punching_help' },
|
||||||
|
{ field: 'enable_udp_broadcast_relay', help: 'enable_udp_broadcast_relay_help' },
|
||||||
|
{ field: 'disable_upnp', help: 'disable_upnp_help' },
|
||||||
{ field: 'disable_sym_hole_punching', help: 'disable_sym_hole_punching_help' },
|
{ field: 'disable_sym_hole_punching', help: 'disable_sym_hole_punching_help' },
|
||||||
{ field: 'enable_magic_dns', help: 'enable_magic_dns_help' },
|
{ field: 'enable_magic_dns', help: 'enable_magic_dns_help' },
|
||||||
{ field: 'enable_private_mode', help: 'enable_private_mode_help' },
|
{ field: 'enable_private_mode', help: 'enable_private_mode_help' },
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { AutoComplete, Button, Dialog, InputNumber, InputText } from 'primevue'
|
import { AutoComplete, Button, Dialog, InputNumber, InputText } from 'primevue'
|
||||||
import InputGroup from 'primevue/inputgroup'
|
import InputGroup from 'primevue/inputgroup'
|
||||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -13,25 +13,8 @@ const props = defineProps<{
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const url = defineModel<string>({ required: true })
|
const url = defineModel<string>({ required: true })
|
||||||
const editing = ref(false)
|
const editing = ref(false)
|
||||||
const container = ref<HTMLElement | null>(null)
|
|
||||||
const internalCompact = ref(false)
|
|
||||||
const hostFocused = ref(false)
|
const hostFocused = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (container.value) {
|
|
||||||
const observer = new ResizeObserver(entries => {
|
|
||||||
for (const entry of entries) {
|
|
||||||
internalCompact.value = entry.contentRect.width < 400
|
|
||||||
}
|
|
||||||
})
|
|
||||||
observer.observe(container.value)
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
observer.disconnect()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const parseUrl = (val: string | null | undefined): { proto: string; host: string; port: number | null } => {
|
const parseUrl = (val: string | null | undefined): { proto: string; host: string; port: number | null } => {
|
||||||
const getValidPort = (portStr: string, proto: string) => {
|
const getValidPort = (portStr: string, proto: string) => {
|
||||||
const p = parseInt(portStr)
|
const p = parseInt(portStr)
|
||||||
@@ -169,28 +152,30 @@ const onProtoChange = (newProto: string) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="container" class="w-full">
|
<div class="url-input-container w-full min-w-0 overflow-hidden">
|
||||||
<InputGroup v-if="!internalCompact" class="w-full">
|
<InputGroup class="url-input-full w-full min-w-0">
|
||||||
<AutoComplete :model-value="internalValue.proto" :suggestions="filteredProtos" dropdown
|
<AutoComplete :model-value="internalValue.proto" :suggestions="filteredProtos" dropdown
|
||||||
class="max-w-32 proto-autocomplete-in-group" @complete="searchProtos"
|
class="max-w-32 proto-autocomplete-in-group" @complete="searchProtos"
|
||||||
@update:model-value="onProtoChange" />
|
@update:model-value="onProtoChange" />
|
||||||
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="grow"
|
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="grow min-w-0"
|
||||||
@focus="onHostFocus" @blur="onHostBlur" />
|
@focus="onHostFocus" @blur="onHostBlur" />
|
||||||
<template v-if="!isNoPortProto">
|
<template v-if="!isNoPortProto">
|
||||||
<InputGroupAddon>
|
<InputGroupAddon>
|
||||||
<span style="font-weight: bold">:</span>
|
<span style="font-weight: bold">:</span>
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
<InputNumber v-model="internalValue.port" :format="false" :min="1" :max="65535" class="max-w-24"
|
<InputNumber v-model="internalValue.port" :format="false" :min="1" :max="65535" class="max-w-24"
|
||||||
:placeholder="String(protos[internalValue.proto] ?? 11010)"
|
:placeholder="String(protos[internalValue.proto] ?? 11010)" fluid />
|
||||||
fluid />
|
|
||||||
</template>
|
</template>
|
||||||
|
<!-- Rendered in both responsive branches; keep action slot content free of side effects and duplicate IDs. -->
|
||||||
<slot name="actions"></slot>
|
<slot name="actions"></slot>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
<div v-else class="flex justify-between items-center p-2 border rounded w-full">
|
<div
|
||||||
<span class="truncate mr-2">{{ url }}</span>
|
class="url-input-compact flex justify-between items-center p-2 border rounded w-full min-w-0 overflow-hidden">
|
||||||
<div class="flex items-center">
|
<span class="truncate mr-2 min-w-0 flex-1 overflow-hidden">{{ url }}</span>
|
||||||
<Button icon="pi pi-pencil" class="p-button-sm p-button-text" @click="editing = true" />
|
<div class="flex items-center shrink-0">
|
||||||
|
<Button icon="pi pi-pencil" class="p-button-sm p-button-text" :aria-label="t('web.common.edit')"
|
||||||
|
@click="editing = true" />
|
||||||
<slot name="actions"></slot>
|
<slot name="actions"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -222,6 +207,28 @@ const onProtoChange = (newProto: string) => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.url-input-container {
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-input-full {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-input-compact {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (min-width: 400px) {
|
||||||
|
.url-input-full {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-input-compact {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.proto-autocomplete-in-group,
|
.proto-autocomplete-in-group,
|
||||||
.proto-autocomplete-in-group :deep(.p-autocomplete-input),
|
.proto-autocomplete-in-group :deep(.p-autocomplete-input),
|
||||||
.proto-autocomplete-in-group :deep(.p-autocomplete-dropdown) {
|
.proto-autocomplete-in-group :deep(.p-autocomplete-dropdown) {
|
||||||
|
|||||||
@@ -104,6 +104,9 @@ use_smoltcp_help: 使用用户态 TCP/IP 协议栈,避免操作系统防火墙
|
|||||||
disable_ipv6: 禁用IPv6
|
disable_ipv6: 禁用IPv6
|
||||||
disable_ipv6_help: 禁用此节点的IPv6功能,仅使用IPv4进行网络通信。
|
disable_ipv6_help: 禁用此节点的IPv6功能,仅使用IPv4进行网络通信。
|
||||||
|
|
||||||
|
ipv6_public_addr_auto: 自动获取公网 IPv6
|
||||||
|
ipv6_public_addr_auto_help: 自动从共享了 IPv6 子网的对等节点获取一个公网 IPv6 地址。
|
||||||
|
|
||||||
enable_kcp_proxy: 启用 KCP 代理
|
enable_kcp_proxy: 启用 KCP 代理
|
||||||
enable_kcp_proxy_help: 将 TCP 流量转为 KCP 流量,降低传输延迟,提升传输速度。
|
enable_kcp_proxy_help: 将 TCP 流量转为 KCP 流量,降低传输延迟,提升传输速度。
|
||||||
|
|
||||||
@@ -157,6 +160,12 @@ disable_tcp_hole_punching_help: 禁用TCP打洞功能
|
|||||||
disable_udp_hole_punching: 禁用UDP打洞
|
disable_udp_hole_punching: 禁用UDP打洞
|
||||||
disable_udp_hole_punching_help: 禁用UDP打洞功能
|
disable_udp_hole_punching_help: 禁用UDP打洞功能
|
||||||
|
|
||||||
|
enable_udp_broadcast_relay: UDP 广播中继
|
||||||
|
enable_udp_broadcast_relay_help: "仅 Windows:捕获物理网卡上的本机 UDP 广播包并转发给 EasyTier 对等节点,帮助局域网游戏发现房间。需要管理员权限。"
|
||||||
|
|
||||||
|
disable_upnp: 禁用 UPnP
|
||||||
|
disable_upnp_help: 禁用符合条件监听器的运行时 UPnP/NAT-PMP 端口映射;自动端口映射默认开启。
|
||||||
|
|
||||||
disable_sym_hole_punching: 禁用对称NAT打洞
|
disable_sym_hole_punching: 禁用对称NAT打洞
|
||||||
disable_sym_hole_punching_help: 禁用对称NAT的打洞(生日攻击),将对称NAT视为锥形NAT处理
|
disable_sym_hole_punching_help: 禁用对称NAT的打洞(生日攻击),将对称NAT视为锥形NAT处理
|
||||||
|
|
||||||
@@ -254,6 +263,7 @@ event:
|
|||||||
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
||||||
PortForwardAdded: 端口转发添加
|
PortForwardAdded: 端口转发添加
|
||||||
ProxyCidrsUpdated: 子网代理CIDR更新
|
ProxyCidrsUpdated: 子网代理CIDR更新
|
||||||
|
UdpBroadcastRelayStartResult: UDP广播中继启动结果
|
||||||
|
|
||||||
web:
|
web:
|
||||||
login:
|
login:
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ use_smoltcp_help: Use a user-space TCP/IP stack to avoid issues with operating s
|
|||||||
disable_ipv6: Disable IPv6
|
disable_ipv6: Disable IPv6
|
||||||
disable_ipv6_help: Disable IPv6 functionality for this node, only use IPv4 for network communication.
|
disable_ipv6_help: Disable IPv6 functionality for this node, only use IPv4 for network communication.
|
||||||
|
|
||||||
|
ipv6_public_addr_auto: Auto Public IPv6
|
||||||
|
ipv6_public_addr_auto_help: Auto-obtain a public IPv6 address from a peer that shares its IPv6 subnet.
|
||||||
|
|
||||||
enable_kcp_proxy: Enable KCP Proxy
|
enable_kcp_proxy: Enable KCP Proxy
|
||||||
enable_kcp_proxy_help: Convert TCP traffic to KCP traffic to reduce latency and boost transmission speed.
|
enable_kcp_proxy_help: Convert TCP traffic to KCP traffic to reduce latency and boost transmission speed.
|
||||||
|
|
||||||
@@ -156,6 +159,12 @@ disable_tcp_hole_punching_help: Disable tcp hole punching
|
|||||||
disable_udp_hole_punching: Disable UDP Hole Punching
|
disable_udp_hole_punching: Disable UDP Hole Punching
|
||||||
disable_udp_hole_punching_help: Disable udp hole punching
|
disable_udp_hole_punching_help: Disable udp hole punching
|
||||||
|
|
||||||
|
enable_udp_broadcast_relay: UDP Broadcast Relay
|
||||||
|
enable_udp_broadcast_relay_help: "Windows only: capture local UDP broadcast packets from physical interfaces and forward them to EasyTier peers. Helps games to find rooms in local network. Requires administrator privileges."
|
||||||
|
|
||||||
|
disable_upnp: Disable UPnP
|
||||||
|
disable_upnp_help: Disable runtime UPnP/NAT-PMP port mapping for eligible listeners; automatic port mapping is enabled by default.
|
||||||
|
|
||||||
disable_sym_hole_punching: Disable Symmetric NAT Hole Punching
|
disable_sym_hole_punching: Disable Symmetric NAT Hole Punching
|
||||||
disable_sym_hole_punching_help: Disable special hole punching handling for symmetric NAT (based on birthday attack), treat symmetric NAT as cone NAT
|
disable_sym_hole_punching_help: Disable special hole punching handling for symmetric NAT (based on birthday attack), treat symmetric NAT as cone NAT
|
||||||
|
|
||||||
@@ -254,6 +263,7 @@ event:
|
|||||||
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
||||||
PortForwardAdded: PortForwardAdded
|
PortForwardAdded: PortForwardAdded
|
||||||
ProxyCidrsUpdated: ProxyCidrsUpdated
|
ProxyCidrsUpdated: ProxyCidrsUpdated
|
||||||
|
UdpBroadcastRelayStartResult: UDP Broadcast Relay Start Result
|
||||||
|
|
||||||
web:
|
web:
|
||||||
login:
|
login:
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export interface NetworkConfig {
|
|||||||
|
|
||||||
use_smoltcp?: boolean
|
use_smoltcp?: boolean
|
||||||
disable_ipv6?: boolean
|
disable_ipv6?: boolean
|
||||||
|
ipv6_public_addr_auto?: boolean
|
||||||
enable_kcp_proxy?: boolean
|
enable_kcp_proxy?: boolean
|
||||||
disable_kcp_input?: boolean
|
disable_kcp_input?: boolean
|
||||||
enable_quic_proxy?: boolean
|
enable_quic_proxy?: boolean
|
||||||
@@ -132,6 +133,8 @@ export interface NetworkConfig {
|
|||||||
disable_encryption?: boolean
|
disable_encryption?: boolean
|
||||||
disable_tcp_hole_punching?: boolean
|
disable_tcp_hole_punching?: boolean
|
||||||
disable_udp_hole_punching?: boolean
|
disable_udp_hole_punching?: boolean
|
||||||
|
disable_upnp?: boolean
|
||||||
|
enable_udp_broadcast_relay?: boolean
|
||||||
disable_sym_hole_punching?: boolean
|
disable_sym_hole_punching?: boolean
|
||||||
|
|
||||||
enable_relay_network_whitelist?: boolean
|
enable_relay_network_whitelist?: boolean
|
||||||
@@ -190,6 +193,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
|||||||
|
|
||||||
use_smoltcp: false,
|
use_smoltcp: false,
|
||||||
disable_ipv6: false,
|
disable_ipv6: false,
|
||||||
|
ipv6_public_addr_auto: false,
|
||||||
enable_kcp_proxy: false,
|
enable_kcp_proxy: false,
|
||||||
disable_kcp_input: false,
|
disable_kcp_input: false,
|
||||||
enable_quic_proxy: false,
|
enable_quic_proxy: false,
|
||||||
@@ -207,6 +211,8 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
|||||||
disable_encryption: false,
|
disable_encryption: false,
|
||||||
disable_tcp_hole_punching: false,
|
disable_tcp_hole_punching: false,
|
||||||
disable_udp_hole_punching: false,
|
disable_udp_hole_punching: false,
|
||||||
|
disable_upnp: false,
|
||||||
|
enable_udp_broadcast_relay: false,
|
||||||
disable_sym_hole_punching: false,
|
disable_sym_hole_punching: false,
|
||||||
enable_relay_network_whitelist: false,
|
enable_relay_network_whitelist: false,
|
||||||
relay_network_whitelist: [],
|
relay_network_whitelist: [],
|
||||||
@@ -443,4 +449,6 @@ export enum EventType {
|
|||||||
PortForwardAdded = 'PortForwardAdded', // PortForwardConfigPb
|
PortForwardAdded = 'PortForwardAdded', // PortForwardConfigPb
|
||||||
|
|
||||||
ProxyCidrsUpdated = 'ProxyCidrsUpdated', // string[], string[]
|
ProxyCidrsUpdated = 'ProxyCidrsUpdated', // string[], string[]
|
||||||
|
|
||||||
|
UdpBroadcastRelayStartResult = 'UdpBroadcastRelayStartResult', // { capture_backend?: string, error?: string }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use session::{Location, Session};
|
|||||||
use storage::{Storage, StorageToken};
|
use storage::{Storage, StorageToken};
|
||||||
|
|
||||||
use crate::FeatureFlags;
|
use crate::FeatureFlags;
|
||||||
use crate::webhook::SharedWebhookConfig;
|
use crate::webhook::{ManagedNetworkConfig, SharedWebhookConfig};
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
use crate::db::{Db, UserIdInDb, entity::user_running_network_configs};
|
use crate::db::{Db, UserIdInDb, entity::user_running_network_configs};
|
||||||
@@ -146,20 +146,7 @@ impl ClientManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_sessions(&self) -> Vec<StorageToken> {
|
pub async fn list_sessions(&self) -> Vec<StorageToken> {
|
||||||
let sessions = self
|
self.storage.list_clients()
|
||||||
.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(
|
pub fn get_session_by_machine_id(
|
||||||
@@ -197,6 +184,22 @@ impl ClientManager {
|
|||||||
self.storage.list_user_clients(user_id)
|
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> {
|
pub async fn get_heartbeat_requests(&self, client_url: &url::Url) -> Option<HeartbeatRequest> {
|
||||||
let s = self.client_sessions.get(client_url)?.clone();
|
let s = self.client_sessions.get(client_url)?.clone();
|
||||||
s.data().read().await.req()
|
s.data().read().await.req()
|
||||||
@@ -365,6 +368,7 @@ mod tests {
|
|||||||
let _c = WebClient::new(
|
let _c = WebClient::new(
|
||||||
connector,
|
connector,
|
||||||
"test",
|
"test",
|
||||||
|
uuid::Uuid::new_v4(),
|
||||||
"test",
|
"test",
|
||||||
false,
|
false,
|
||||||
Arc::new(NetworkInstanceManager::new()),
|
Arc::new(NetworkInstanceManager::new()),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -114,6 +114,20 @@ impl Storage {
|
|||||||
.unwrap_or_default()
|
.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 {
|
pub fn db(&self) -> &Db {
|
||||||
&self.0.db
|
&self.0.db
|
||||||
}
|
}
|
||||||
@@ -174,4 +188,25 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(storage.get_client_url_by_machine_id(2, &machine_id), None);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user