From 513695297ce91bbeba1bb4b2811a6f05c47e3088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9F=A9=E5=98=89=E4=B9=90?= Date: Sun, 10 May 2026 14:15:31 +0800 Subject: [PATCH] [OHOS] feat: Enhance Rust kernel with config management and routing improvements (#2227) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [OHOS.with ai] 将配置管理/配置分享/路由聚合/实例状态解析下沉至 Rust 内核,收敛职责并提升性能 (#2209) * feat: add ohrs config store and startup error logging * feat: full ability core for ohos * feat: full ability core for ohos * feat: clean code --------- Co-authored-by: FrankHan * fix: 添加缺失文件 * fix: 修复更新路由启动两次TUN问题,并调整日志 * fix: rustfmt * fix: 适配Cidr忽略/32格式路由 * fix: 修复Option适配错误 * fix: rustfmt * fix: rustfmt --------- Co-authored-by: FrankHan --- easytier-contrib/easytier-ohrs/Cargo.lock | 676 ++++++++++++++---- easytier-contrib/easytier-ohrs/Cargo.toml | 10 + easytier-contrib/easytier-ohrs/src/config.rs | 4 + .../src/config/repository/mod.rs | 13 + .../easytier-ohrs/src/config/services/mod.rs | 2 + .../src/config/services/schema_service.rs | 414 +++++++++++ .../src/config/services/share_link_service.rs | 197 +++++ .../src/config/storage/config_meta.rs | 333 +++++++++ .../easytier-ohrs/src/config/storage/mod.rs | 1 + .../easytier-ohrs/src/config/types/mod.rs | 1 + .../src/config/types/stored_config.rs | 68 ++ .../easytier-ohrs/src/config_repo.rs | 349 +++++++++ .../src/config_repo/field_store.rs | 67 ++ .../src/config_repo/import_export.rs | 48 ++ .../src/config_repo/legacy_migration.rs | 45 ++ .../src/config_repo/validation.rs | 30 + easytier-contrib/easytier-ohrs/src/exports.rs | 2 + .../easytier-ohrs/src/exports/config_api.rs | 46 ++ .../easytier-ohrs/src/exports/runtime_api.rs | 184 +++++ .../easytier-ohrs/src/kernel_bridge.rs | 6 + .../src/kernel_bridge/protocol.rs | 50 ++ .../src/kernel_bridge/routing.rs | 105 +++ .../src/kernel_bridge/socket_server.rs | 196 +++++ easytier-contrib/easytier-ohrs/src/lib.rs | 578 +++++++++++---- .../easytier-ohrs/src/platform.rs | 1 + .../easytier-ohrs/src/platform/logging/mod.rs | 1 + .../src/{ => platform/logging}/native_log.rs | 0 easytier-contrib/easytier-ohrs/src/runtime.rs | 1 + .../easytier-ohrs/src/runtime/state/mod.rs | 1 + .../src/runtime/state/runtime_state.rs | 293 ++++++++ easytier/src/proto/mod.rs | 5 +- 31 files changed, 3455 insertions(+), 272 deletions(-) create mode 100644 easytier-contrib/easytier-ohrs/src/config.rs create mode 100644 easytier-contrib/easytier-ohrs/src/config/repository/mod.rs create mode 100644 easytier-contrib/easytier-ohrs/src/config/services/mod.rs create mode 100644 easytier-contrib/easytier-ohrs/src/config/services/schema_service.rs create mode 100644 easytier-contrib/easytier-ohrs/src/config/services/share_link_service.rs create mode 100644 easytier-contrib/easytier-ohrs/src/config/storage/config_meta.rs create mode 100644 easytier-contrib/easytier-ohrs/src/config/storage/mod.rs create mode 100644 easytier-contrib/easytier-ohrs/src/config/types/mod.rs create mode 100644 easytier-contrib/easytier-ohrs/src/config/types/stored_config.rs create mode 100644 easytier-contrib/easytier-ohrs/src/config_repo.rs create mode 100644 easytier-contrib/easytier-ohrs/src/config_repo/field_store.rs create mode 100644 easytier-contrib/easytier-ohrs/src/config_repo/import_export.rs create mode 100644 easytier-contrib/easytier-ohrs/src/config_repo/legacy_migration.rs create mode 100644 easytier-contrib/easytier-ohrs/src/config_repo/validation.rs create mode 100644 easytier-contrib/easytier-ohrs/src/exports.rs create mode 100644 easytier-contrib/easytier-ohrs/src/exports/config_api.rs create mode 100644 easytier-contrib/easytier-ohrs/src/exports/runtime_api.rs create mode 100644 easytier-contrib/easytier-ohrs/src/kernel_bridge.rs create mode 100644 easytier-contrib/easytier-ohrs/src/kernel_bridge/protocol.rs create mode 100644 easytier-contrib/easytier-ohrs/src/kernel_bridge/routing.rs create mode 100644 easytier-contrib/easytier-ohrs/src/kernel_bridge/socket_server.rs create mode 100644 easytier-contrib/easytier-ohrs/src/platform.rs create mode 100644 easytier-contrib/easytier-ohrs/src/platform/logging/mod.rs rename easytier-contrib/easytier-ohrs/src/{ => platform/logging}/native_log.rs (100%) create mode 100644 easytier-contrib/easytier-ohrs/src/runtime.rs create mode 100644 easytier-contrib/easytier-ohrs/src/runtime/state/mod.rs create mode 100644 easytier-contrib/easytier-ohrs/src/runtime/state/runtime_state.rs diff --git a/easytier-contrib/easytier-ohrs/Cargo.lock b/easytier-contrib/easytier-ohrs/Cargo.lock index cc4a5ee2..0c502ba2 100644 --- a/easytier-contrib/easytier-ohrs/Cargo.lock +++ b/easytier-contrib/easytier-ohrs/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -35,7 +26,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -52,6 +43,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy 0.8.27", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -228,6 +231,18 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64 0.22.1", + "http", + "log", + "url", +] + [[package]] name = "auto_impl" version = "1.3.0" @@ -245,21 +260,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - [[package]] name = "base62" version = "2.2.3" @@ -326,6 +326,31 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bon" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.106", +] + [[package]] name = "boringtun-easytier" version = "0.6.1" @@ -471,7 +496,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", ] [[package]] @@ -481,7 +517,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", @@ -498,7 +534,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -678,6 +714,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.3.0" @@ -807,7 +852,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "fiat-crypto", "rustc_version", @@ -939,6 +984,17 @@ dependencies = [ "thiserror 2.0.16", ] +[[package]] +name = "delegate" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "deranged" version = "0.5.3" @@ -1066,6 +1122,17 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + [[package]] name = "dtor" version = "0.0.6" @@ -1083,7 +1150,7 @@ checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055" [[package]] name = "easytier" -version = "2.6.0" +version = "2.6.4" dependencies = [ "anyhow", "arc-swap", @@ -1096,11 +1163,11 @@ dependencies = [ "auto_impl", "base64 0.22.1", "bitflags 2.9.4", + "bon", "boringtun-easytier", "bytecodec", "byteorder", "bytes", - "cfg-if", "cfg_aliases", "chrono", "cidr", @@ -1110,6 +1177,7 @@ dependencies = [ "crossbeam", "dashmap", "dbus", + "delegate", "derivative", "derive_builder", "derive_more", @@ -1118,10 +1186,10 @@ dependencies = [ "flume", "forwarded-header-value", "futures", - "gethostname", + "gethostname 0.5.0", "git-version", "globwalk", - "hashbrown 0.15.5", + "guarden", "hickory-client", "hickory-proto", "hickory-resolver", @@ -1132,13 +1200,15 @@ dependencies = [ "humansize", "humantime-serde", "idna", + "igd-next", "indoc", "itertools 0.14.0", "kcp-sys", "machine-uid", "multimap", + "natpmp", "netlink-packet-core", - "netlink-packet-route", + "netlink-packet-route 0.21.0", "netlink-packet-utils", "netlink-sys", "network-interface", @@ -1205,9 +1275,8 @@ dependencies = [ "wildmatch", "winapi", "windivert", - "windows 0.52.0", + "windows 0.62.2", "windows-service", - "windows-sys 0.52.0", "winreg 0.52.0", "x25519-dalek", "zerocopy 0.7.35", @@ -1219,16 +1288,26 @@ dependencies = [ name = "easytier-ohrs" version = "0.1.0" dependencies = [ + "async-trait", + "base64 0.22.1", "easytier", + "flate2", + "gethostname 1.1.0", + "ipnet", "napi-build-ohos", "napi-derive-ohos", "napi-ohos", "ohos-hilog-binding", "once_cell", + "prost-reflect", + "rusqlite", + "serde", "serde_json", + "tokio", "tracing", "tracing-core", "tracing-subscriber", + "url", "uuid", ] @@ -1390,6 +1469,18 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastbloom" version = "0.14.1" @@ -1621,6 +1712,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.2", + "windows-link 0.2.1", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -1643,11 +1744,25 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasi 0.14.7+wasi-0.2.4", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + [[package]] name = "ghash" version = "0.5.1" @@ -1658,12 +1773,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "git-version" version = "0.3.9" @@ -1714,6 +1823,28 @@ dependencies = [ "walkdir", ] +[[package]] +name = "guarden" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c7272e004bec8ea7fe50b2ec5451858695bb2743e897c353753fcb3415f4ef" +dependencies = [ + "futures", + "guarden-macros", + "tokio", +] + +[[package]] +name = "guarden-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d291d94f41471fe84384a426b3e2c9d22f960a351a5bf26aaa7cd75fbc02c88" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "h2" version = "0.4.12" @@ -1747,6 +1878,9 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] [[package]] name = "hashbrown" @@ -1765,6 +1899,15 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "heapless" version = "0.9.2" @@ -2055,7 +2198,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2 0.6.3", "system-configuration", "tokio", "tower-service", @@ -2075,7 +2218,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.0", + "windows-core 0.62.2", ] [[package]] @@ -2173,6 +2316,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -2200,6 +2349,26 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "igd-next" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bac9a3c8278f43b4cd8463380f4a25653ac843e5b177e1d3eaf849cc9ba10d4d" +dependencies = [ + "attohttpc", + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.10.1", + "tokio", + "url", + "xmltree", +] + [[package]] name = "ignore" version = "0.4.23" @@ -2224,6 +2393,8 @@ checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", "hashbrown 0.16.0", + "serde", + "serde_core", ] [[package]] @@ -2253,17 +2424,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "ip_network" version = "0.4.1" @@ -2426,6 +2586,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libbz2-rs-sys" version = "0.2.2" @@ -2434,9 +2600,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libdbus-sys" @@ -2455,7 +2621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -2494,6 +2660,17 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libz-rs-sys" version = "0.5.2" @@ -2625,13 +2802,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -2743,6 +2920,35 @@ dependencies = [ "tempfile", ] +[[package]] +name = "natpmp" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77366fa8ce34e2e1322dd97da65f11a62f451bd3daae8be6993c00800f61dd07" +dependencies = [ + "async-trait", + "cc", + "netdev", + "tokio", +] + +[[package]] +name = "netdev" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f901362e84cd407be6f8cd9d3a46bccf09136b095792785401ea7d283c79b91d" +dependencies = [ + "dlopen2", + "ipnet", + "libc", + "netlink-packet-core", + "netlink-packet-route 0.17.1", + "netlink-sys", + "once_cell", + "system-configuration", + "windows-sys 0.52.0", +] + [[package]] name = "netlink-packet-core" version = "0.7.0" @@ -2754,6 +2960,20 @@ dependencies = [ "netlink-packet-utils", ] +[[package]] +name = "netlink-packet-route" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053998cea5a306971f88580d0829e90f270f940befd7cf928da179d4187a5a66" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "byteorder", + "libc", + "netlink-packet-core", + "netlink-packet-utils", +] + [[package]] name = "netlink-packet-route" version = "0.21.0" @@ -2908,15 +3128,6 @@ dependencies = [ "libc", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "ohos-hilog-binding" version = "0.1.2" @@ -3232,7 +3443,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -3244,7 +3455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -3523,7 +3734,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.0", + "socket2 0.6.3", "thiserror 2.0.16", "tokio", "tracing", @@ -3574,7 +3785,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.0", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] @@ -3594,6 +3805,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radix_trie" version = "0.2.1" @@ -3625,6 +3842,17 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20 0.10.0", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -3663,6 +3891,12 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rcgen" version = "0.12.1" @@ -3797,6 +4031,20 @@ dependencies = [ "portable-atomic-util", ] +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.9.4", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rust-i18n" version = "3.1.5" @@ -3851,12 +4099,6 @@ dependencies = [ "triomphe", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -4170,7 +4412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -4181,7 +4423,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -4290,12 +4532,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -4615,29 +4857,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", - "socket2 0.6.0", + "socket2 0.6.3", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -4684,6 +4923,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "pin-project-lite", "tokio", ] @@ -5146,7 +5386,16 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] @@ -5221,6 +5470,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.9.4", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" version = "0.3.80" @@ -5375,27 +5658,29 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core 0.52.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -5408,12 +5693,12 @@ dependencies = [ ] [[package]] -name = "windows-core" -version = "0.52.0" +name = "windows-collections" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-targets 0.52.6", + "windows-core 0.62.2", ] [[package]] @@ -5431,15 +5716,15 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.62.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.0", - "windows-result 0.4.0", - "windows-strings 0.5.0", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -5450,14 +5735,25 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -5466,9 +5762,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -5483,9 +5779,9 @@ checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" @@ -5497,6 +5793,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-registry" version = "0.5.3" @@ -5519,11 +5825,11 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -5548,11 +5854,11 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -5606,7 +5912,7 @@ version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -5681,6 +5987,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -5915,6 +6230,94 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.106", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.106", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.9.4", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.1" @@ -5939,6 +6342,15 @@ version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + [[package]] name = "yasna" version = "0.5.2" diff --git a/easytier-contrib/easytier-ohrs/Cargo.toml b/easytier-contrib/easytier-ohrs/Cargo.toml index 20c0c169..b1d65d9e 100644 --- a/easytier-contrib/easytier-ohrs/Cargo.toml +++ b/easytier-contrib/easytier-ohrs/Cargo.toml @@ -7,6 +7,10 @@ edition = "2024" crate-type=["cdylib"] [dependencies] +async-trait = "0.1" +base64 = "0.22" +flate2 = "1.1" +gethostname = "1.1" ohos-hilog-binding = {version = "*", features = ["redirect"]} easytier = { path = "../../easytier" } napi-derive-ohos = "1.1" @@ -26,10 +30,16 @@ napi-ohos = { version = "1.1", default-features = false, features = [ "web_stream", ] } once_cell = "1.21.3" +ipnet = "2.10" +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.125" +prost-reflect = { version = "0.14.5", default-features = false, features = ["derive"] } +rusqlite = { version = "0.32", features = ["bundled"] } tracing-subscriber = "0.3.19" tracing-core = "0.1.33" tracing = "0.1.41" +tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] } +url = "2.5" uuid = { version = "1.5.0", features = [ "v4", "fast-rng", diff --git a/easytier-contrib/easytier-ohrs/src/config.rs b/easytier-contrib/easytier-ohrs/src/config.rs new file mode 100644 index 00000000..af649e50 --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/config.rs @@ -0,0 +1,4 @@ +pub(crate) mod repository; +pub(crate) mod services; +pub(crate) mod storage; +pub(crate) mod types; diff --git a/easytier-contrib/easytier-ohrs/src/config/repository/mod.rs b/easytier-contrib/easytier-ohrs/src/config/repository/mod.rs new file mode 100644 index 00000000..1b66eb24 --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/config/repository/mod.rs @@ -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::*; diff --git a/easytier-contrib/easytier-ohrs/src/config/services/mod.rs b/easytier-contrib/easytier-ohrs/src/config/services/mod.rs new file mode 100644 index 00000000..88b329de --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/config/services/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod schema_service; +pub(crate) mod share_link_service; diff --git a/easytier-contrib/easytier-ohrs/src/config/services/schema_service.rs b/easytier-contrib/easytier-ohrs/src/config/services/schema_service.rs new file mode 100644 index 00000000..d1425b56 --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/config/services/schema_service.rs @@ -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, + pub semantic_type: Option, + pub value_kind: String, + pub is_list: bool, + pub required: bool, + pub default_value_text: Option, + pub enum_options: Vec, + pub validations: Vec, + pub children: Vec, + pub definitions: Vec, +} + +#[derive(Debug, Clone, Serialize)] +#[napi(object)] +pub struct ConfigFieldMapping { + pub field_name: String, + pub field_number: i32, +} + +static DESCRIPTOR_POOL: Lazy = 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 { + 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 { + 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 { + 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 { + 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 { + 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, + semantic_type: Option, + value_kind: String, + is_list: bool, + required: bool, + default_value_text: Option, + enum_options: Vec, + validations: Vec, + children: Vec, + definitions: Vec, +) -> 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 { + 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 { + 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 { + 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 { + 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 { + 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") + ); + } +} diff --git a/easytier-contrib/easytier-ohrs/src/config/services/share_link_service.rs b/easytier-contrib/easytier-ohrs/src/config/services/share_link_service.rs new file mode 100644 index 00000000..33bc65bd --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/config/services/share_link_service.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + let field_id_to_name = field_id_to_name_map(); + let value = serde_json::from_str::(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 { + 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 { + 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, + only_start: bool, +) -> Option { + let record = get_config_record(config_id)?; + let config = serde_json::from_str::(&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 { + 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, +) -> Option { + let payload = parse_config_share_link(share_link)?; + let config = serde_json::from_str::(&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::(&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"); + } +} diff --git a/easytier-contrib/easytier-ohrs/src/config/storage/config_meta.rs b/easytier-contrib/easytier-ohrs/src/config/storage/config_meta.rs new file mode 100644 index 00000000..a9a920c1 --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/config/storage/config_meta.rs @@ -0,0 +1,333 @@ +use crate::config::types::stored_config::{StoredConfigList, StoredConfigMeta}; +use ohos_hilog_binding::{hilog_debug, hilog_error}; +use rusqlite::{Connection, OptionalExtension, params}; +use std::path::PathBuf; +use std::sync::Mutex; +use std::time::{SystemTime, UNIX_EPOCH}; + +static CONFIG_DB_PATH: Mutex> = Mutex::new(None); +const CONFIG_DB_FILE_NAME: &str = "easytier-config-store.db"; + +#[derive(Debug, Clone)] +struct StoredConfigMetaRecord { + config_id: String, + display_name: String, + created_at: String, + updated_at: String, + favorite: bool, + temporary: bool, +} + +pub(crate) fn now_ts_string() -> String { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs().to_string()) + .unwrap_or_else(|_| "0".to_string()) +} + +fn db_file_path() -> Option { + CONFIG_DB_PATH + .lock() + .ok() + .and_then(|guard| guard.as_ref().cloned()) +} + +fn init_schema(conn: &Connection) -> rusqlite::Result<()> { + conn.execute_batch( + "PRAGMA foreign_keys = ON; + CREATE TABLE IF NOT EXISTS stored_configs ( + config_id TEXT PRIMARY KEY, + display_name TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + favorite INTEGER NOT NULL DEFAULT 0, + temporary INTEGER NOT NULL DEFAULT 0 + ); + CREATE TABLE IF NOT EXISTS stored_config_fields ( + config_id TEXT NOT NULL, + field_name TEXT NOT NULL, + field_json TEXT NOT NULL, + updated_at TEXT NOT NULL, + PRIMARY KEY (config_id, field_name), + FOREIGN KEY (config_id) REFERENCES stored_configs(config_id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_stored_config_fields_config_id + ON stored_config_fields(config_id);", + ) +} + +pub(crate) fn open_db() -> Option { + let path = db_file_path()?; + let conn = match Connection::open(&path) { + Ok(conn) => conn, + Err(e) => { + hilog_error!("[Rust] failed to open config db {}: {}", path.display(), e); + return None; + } + }; + + if let Err(e) = init_schema(&conn) { + hilog_error!( + "[Rust] failed to initialize config db {}: {}", + path.display(), + e + ); + return None; + } + + Some(conn) +} + +fn row_to_meta(row: &rusqlite::Row<'_>) -> rusqlite::Result { + 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 { + conn.query_row( + "SELECT config_id, display_name, created_at, updated_at, favorite, temporary + FROM stored_configs WHERE config_id = ?1", + params![config_id], + row_to_meta, + ) + .optional() + .ok() + .flatten() +} + +fn to_meta(record: StoredConfigMetaRecord) -> StoredConfigMeta { + StoredConfigMeta { + config_id: record.config_id, + display_name: record.display_name, + created_at: record.created_at, + updated_at: record.updated_at, + favorite: record.favorite, + temporary: record.temporary, + } +} + +pub fn init_config_meta_store(root_dir: String) -> bool { + let root = PathBuf::from(root_dir); + if let Err(e) = std::fs::create_dir_all(&root) { + hilog_error!( + "[Rust] failed to create config db dir {}: {}", + root.display(), + e + ); + return false; + } + + let db_path = root.join(CONFIG_DB_FILE_NAME); + match CONFIG_DB_PATH.lock() { + Ok(mut guard) => { + *guard = Some(db_path.clone()); + } + Err(e) => { + hilog_error!("[Rust] failed to lock config db path: {}", e); + return false; + } + } + + if open_db().is_none() { + return false; + } + + hilog_debug!("[Rust] initialized config db at {}", db_path.display()); + true +} + +pub fn list_config_meta_entries() -> StoredConfigList { + let Some(conn) = open_db() else { + return StoredConfigList { configs: vec![] }; + }; + + let mut stmt = match conn.prepare( + "SELECT config_id, display_name, created_at, updated_at, favorite, temporary + FROM stored_configs + ORDER BY updated_at DESC, display_name ASC", + ) { + Ok(stmt) => stmt, + Err(e) => { + hilog_error!("[Rust] failed to prepare list meta query: {}", e); + return StoredConfigList { configs: vec![] }; + } + }; + + let rows = match stmt.query_map([], row_to_meta) { + Ok(rows) => rows, + Err(e) => { + hilog_error!("[Rust] failed to list config meta rows: {}", e); + return StoredConfigList { configs: vec![] }; + } + }; + + let configs = rows.filter_map(Result::ok).map(to_meta).collect(); + StoredConfigList { configs } +} + +pub fn get_config_display_name(config_id: &str) -> Option { + let conn = open_db()?; + load_meta_record(&conn, config_id).map(|record| record.display_name) +} + +pub fn get_config_meta(config_id: &str) -> Option { + let conn = open_db()?; + load_meta_record(&conn, config_id).map(to_meta) +} + +pub fn upsert_config_meta( + config_id: String, + display_name: String, + favorite: bool, + temporary: bool, +) -> StoredConfigMeta { + let now = now_ts_string(); + let Some(conn) = open_db() else { + return StoredConfigMeta { + config_id, + display_name, + created_at: now.clone(), + updated_at: now, + favorite, + temporary, + }; + }; + + let created_at = load_meta_record(&conn, &config_id) + .map(|record| record.created_at) + .unwrap_or_else(|| now.clone()); + + if let Err(e) = conn.execute( + "INSERT INTO stored_configs ( + config_id, display_name, created_at, updated_at, favorite, temporary + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6) + ON CONFLICT(config_id) DO UPDATE SET + display_name = excluded.display_name, + updated_at = excluded.updated_at, + favorite = excluded.favorite, + temporary = excluded.temporary", + params![ + config_id, + display_name, + created_at, + now, + if favorite { 1 } else { 0 }, + if temporary { 1 } else { 0 } + ], + ) { + hilog_error!("[Rust] failed to upsert config meta: {}", e); + } + + get_config_meta(&config_id).unwrap_or(StoredConfigMeta { + config_id, + display_name, + created_at, + updated_at: now, + favorite, + temporary, + }) +} + +pub(crate) fn upsert_config_meta_in_tx( + tx: &rusqlite::Transaction<'_>, + config_id: String, + display_name: String, + favorite: bool, + temporary: bool, +) -> Option { + 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 { + let conn = open_db()?; + let mut record = load_meta_record(&conn, &config_id)?; + record.display_name = display_name; + record.updated_at = now_ts_string(); + + conn.execute( + "UPDATE stored_configs + SET display_name = ?2, updated_at = ?3 + WHERE config_id = ?1", + params![config_id, record.display_name, record.updated_at], + ) + .ok()?; + + Some(to_meta(record)) +} + +pub fn delete_config_meta(config_id: &str) -> bool { + let Some(conn) = open_db() else { + return false; + }; + + match conn.execute( + "DELETE FROM stored_configs WHERE config_id = ?1", + params![config_id], + ) { + Ok(rows) => rows > 0, + Err(e) => { + hilog_error!("[Rust] failed to delete config meta {}: {}", config_id, e); + false + } + } +} diff --git a/easytier-contrib/easytier-ohrs/src/config/storage/mod.rs b/easytier-contrib/easytier-ohrs/src/config/storage/mod.rs new file mode 100644 index 00000000..765a7267 --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/config/storage/mod.rs @@ -0,0 +1 @@ +pub(crate) mod config_meta; diff --git a/easytier-contrib/easytier-ohrs/src/config/types/mod.rs b/easytier-contrib/easytier-ohrs/src/config/types/mod.rs new file mode 100644 index 00000000..71a56173 --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/config/types/mod.rs @@ -0,0 +1 @@ +pub(crate) mod stored_config; diff --git a/easytier-contrib/easytier-ohrs/src/config/types/stored_config.rs b/easytier-contrib/easytier-ohrs/src/config/types/stored_config.rs new file mode 100644 index 00000000..86375416 --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/config/types/stored_config.rs @@ -0,0 +1,68 @@ +use napi_derive_ohos::napi; +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +#[napi(object)] +pub struct StoredConfigMeta { + pub config_id: String, + pub display_name: String, + pub created_at: String, + pub updated_at: String, + pub favorite: bool, + pub temporary: bool, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +#[napi(object)] +pub struct StoredConfigRecord { + pub meta: StoredConfigMeta, + pub config_json: String, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +#[napi(object)] +pub struct StoredConfigList { + pub configs: Vec, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +#[napi(object)] +pub struct ExportTomlResult { + pub toml_text: String, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +#[napi(object)] +pub struct StoredConfigSummary { + pub config_id: String, + pub display_name: String, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +#[napi(object)] +pub struct SharedConfigLinkPayload { + pub config_json: String, + pub display_name: Option, + 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, +} diff --git a/easytier-contrib/easytier-ohrs/src/config_repo.rs b/easytier-contrib/easytier-ohrs/src/config_repo.rs new file mode 100644 index 00000000..cbd8bf5b --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/config_repo.rs @@ -0,0 +1,349 @@ +use super::{field_store, import_export, legacy_migration, validation}; +use crate::config::storage::config_meta::{ + delete_config_meta, get_config_meta, init_config_meta_store, list_config_meta_entries, open_db, + upsert_config_meta_in_tx, +}; +use crate::config::types::stored_config::{ExportTomlResult, StoredConfigRecord}; +use easytier::common::config::ConfigLoader; +use easytier::proto::api::manage::NetworkConfig; +use ohos_hilog_binding::{hilog_debug, hilog_error}; +use rusqlite::params; +use serde_json::Value; +use std::path::PathBuf; +use std::sync::Mutex; + +static CONFIG_ROOT_DIR: Mutex> = Mutex::new(None); +pub(crate) const CONFIG_DIR_NAME: &str = "easytier-configs"; +pub(crate) const KERNEL_SOCKET_FILE_NAME: &str = "easytier-kernel.sock"; + +pub(crate) fn config_root_dir() -> Option { + CONFIG_ROOT_DIR + .lock() + .ok() + .and_then(|guard| guard.as_ref().cloned()) +} + +pub(crate) fn kernel_socket_path() -> Option { + config_root_dir().map(|root| root.join(KERNEL_SOCKET_FILE_NAME)) +} + +pub(crate) fn legacy_config_file_path(config_id: &str) -> Option { + legacy_migration::legacy_config_file_path(&config_root_dir(), CONFIG_DIR_NAME, config_id) +} + +pub fn init_config_store(root_dir: String) -> bool { + let root = PathBuf::from(root_dir); + let configs_dir = root.join(CONFIG_DIR_NAME); + if let Err(e) = std::fs::create_dir_all(&configs_dir) { + hilog_error!( + "[Rust] failed to create config dir {}: {}", + configs_dir.display(), + e + ); + return false; + } + + match CONFIG_ROOT_DIR.lock() { + Ok(mut guard) => { + *guard = Some(root.clone()); + } + Err(e) => { + hilog_error!("[Rust] failed to lock config root dir: {}", e); + return false; + } + } + + if !init_config_meta_store(root.to_string_lossy().into_owned()) { + return false; + } + + hilog_debug!( + "[Rust] initialized config repo at {}", + configs_dir.display() + ); + true +} + +fn migrate_legacy_file_if_needed(config_id: &str) -> Option<()> { + legacy_migration::migrate_legacy_file_if_needed( + &config_root_dir(), + CONFIG_DIR_NAME, + config_id, + save_config_record, + ) +} + +pub fn save_config_record( + config_id: String, + display_name: String, + config_json: String, +) -> Option { + let config = match validation::validate_config_json(&config_json, config_id.clone()) { + Ok(config) => config, + Err(e) => { + hilog_error!("[Rust] save_config_record failed {}", e); + return None; + } + }; + + let normalized_json = match serde_json::to_string(&config) { + Ok(raw) => raw, + Err(e) => { + hilog_error!( + "[Rust] failed to serialize normalized config {}: {}", + config_id, + e + ); + return None; + } + }; + + let fields = match validation::config_to_top_level_map(&config) { + Some(fields) => fields, + None => return None, + }; + + let conn = open_db()?; + let tx = conn.unchecked_transaction().ok()?; + let existing_meta = get_config_meta(&config_id); + let favorite = existing_meta + .as_ref() + .map(|meta| meta.favorite) + .unwrap_or(false); + let temporary = existing_meta + .as_ref() + .map(|meta| meta.temporary) + .unwrap_or(false); + let meta = upsert_config_meta_in_tx(&tx, config_id.clone(), display_name, favorite, temporary)?; + + field_store::replace_config_fields(&tx, &config_id, fields)?; + + tx.commit().ok()?; + + if let Some(legacy_path) = legacy_config_file_path(&config_id) { + if legacy_path.exists() { + let _ = std::fs::remove_file(legacy_path); + } + } + + Some(StoredConfigRecord { + meta, + config_json: normalized_json, + }) +} + +pub fn load_config_json(config_id: &str) -> Option { + 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 { + 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 { + migrate_legacy_file_if_needed(config_id)?; + let conn = open_db()?; + conn.query_row( + "SELECT field_json FROM stored_config_fields + WHERE config_id = ?1 AND field_name = ?2", + params![config_id, field], + |row| row.get::<_, String>(0), + ) + .ok() +} + +pub fn set_config_field_value(config_id: &str, field: &str, json_value: &str) -> bool { + if field.contains('.') { + return false; + } + + let raw = match load_config_json(config_id) { + Some(raw) => raw, + None => return false, + }; + let mut value = match serde_json::from_str::(&raw) { + Ok(value) => value, + Err(_) => return false, + }; + let new_field_value = match serde_json::from_str::(json_value) { + Ok(value) => value, + Err(_) => return false, + }; + let object = match value.as_object_mut() { + Some(object) => object, + None => return false, + }; + object.insert(field.to_string(), new_field_value); + + let normalized = match serde_json::to_string(&value) { + Ok(raw) => raw, + Err(_) => return false, + }; + + let display_name = get_config_meta(config_id) + .map(|meta| meta.display_name) + .unwrap_or_else(|| config_id.to_string()); + + save_config_record(config_id.to_string(), display_name, normalized).is_some() +} + +pub fn get_display_name(config_id: &str) -> Option { + get_config_meta(config_id).map(|meta| meta.display_name) +} + +pub fn get_default_config_json() -> Option { + crate::build_default_network_config_json().ok() +} + +pub fn create_config_record(config_id: String, display_name: String) -> Option { + let raw = get_default_config_json()?; + let mut config = serde_json::from_str::(&raw).ok()?; + config.instance_id = Some(config_id.clone()); + let normalized_json = serde_json::to_string(&config).ok()?; + save_config_record(config_id, display_name, normalized_json) +} + +pub fn start_kernel_with_config_id(config_id: &str) -> bool { + let raw = match load_config_json(config_id) { + Some(raw) => raw, + None => return false, + }; + crate::run_network_instance_from_json(&raw) +} + +pub fn list_config_meta_json() -> String { + serde_json::to_string(&list_config_meta_entries().configs).unwrap_or_else(|_| "[]".to_string()) +} + +pub fn delete_config_record(config_id: &str) -> bool { + if let Some(path) = legacy_config_file_path(config_id) { + if path.exists() { + let _ = std::fs::remove_file(path); + } + } + + let conn = match open_db() { + Some(conn) => conn, + None => return false, + }; + if let Err(e) = conn.execute( + "DELETE FROM stored_config_fields WHERE config_id = ?1", + params![config_id], + ) { + hilog_error!("[Rust] failed to delete config fields {}: {}", config_id, e); + return false; + } + + delete_config_meta(config_id) +} + +pub fn export_config_toml(config_id: &str) -> Option { + 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, +) -> Option { + 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 + ); + } +} diff --git a/easytier-contrib/easytier-ohrs/src/config_repo/field_store.rs b/easytier-contrib/easytier-ohrs/src/config_repo/field_store.rs new file mode 100644 index 00000000..c954ac02 --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/config_repo/field_store.rs @@ -0,0 +1,67 @@ +use crate::config::storage::config_meta::{now_ts_string, open_db}; +use ohos_hilog_binding::hilog_error; +use rusqlite::{Connection, params}; +use serde_json::{Map, Value}; + +pub(super) fn load_config_map_from_db(config_id: &str) -> Option> { + 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::(&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, +) -> Option<()> { + if let Err(e) = tx.execute( + "DELETE FROM stored_config_fields WHERE config_id = ?1", + params![config_id], + ) { + hilog_error!( + "[Rust] failed to clear existing config fields {}: {}", + config_id, + e + ); + return None; + } + + for (field_name, value) in fields { + let field_json = serde_json::to_string(&value).ok()?; + if let Err(e) = tx.execute( + "INSERT INTO stored_config_fields (config_id, field_name, field_json, updated_at) + VALUES (?1, ?2, ?3, ?4)", + params![config_id, field_name, field_json, now_ts_string()], + ) { + hilog_error!("[Rust] failed to persist config field {}: {}", config_id, e); + return None; + } + } + + Some(()) +} diff --git a/easytier-contrib/easytier-ohrs/src/config_repo/import_export.rs b/easytier-contrib/easytier-ohrs/src/config_repo/import_export.rs new file mode 100644 index 00000000..7f698aa4 --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/config_repo/import_export.rs @@ -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 { + let config = serde_json::from_str::(&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, + save_config_record: impl Fn(String, String, String) -> Option, +) -> Option { + 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) +} diff --git a/easytier-contrib/easytier-ohrs/src/config_repo/legacy_migration.rs b/easytier-contrib/easytier-ohrs/src/config_repo/legacy_migration.rs new file mode 100644 index 00000000..6efa3b6c --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/config_repo/legacy_migration.rs @@ -0,0 +1,45 @@ +use crate::config::storage::config_meta::get_config_meta; +use ohos_hilog_binding::hilog_error; +use std::path::PathBuf; + +pub(super) fn legacy_config_file_path( + root_dir: &Option, + config_dir_name: &str, + config_id: &str, +) -> Option { + 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, + config_dir_name: &str, + config_id: &str, + save_config_record: impl Fn( + String, + String, + String, + ) -> Option, +) -> Option<()> { + let legacy_path = legacy_config_file_path(root_dir, config_dir_name, config_id)?; + if !legacy_path.exists() { + return Some(()); + } + + let raw = std::fs::read_to_string(&legacy_path).ok()?; + let display_name = get_config_meta(config_id) + .map(|meta| meta.display_name) + .unwrap_or_else(|| config_id.to_string()); + save_config_record(config_id.to_string(), display_name, raw)?; + + if let Err(e) = std::fs::remove_file(&legacy_path) { + hilog_error!( + "[Rust] failed to remove legacy config file {}: {}", + legacy_path.display(), + e + ); + } + Some(()) +} diff --git a/easytier-contrib/easytier-ohrs/src/config_repo/validation.rs b/easytier-contrib/easytier-ohrs/src/config_repo/validation.rs new file mode 100644 index 00000000..cc7551fb --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/config_repo/validation.rs @@ -0,0 +1,30 @@ +use easytier::proto::api::manage::NetworkConfig; +use serde_json::{Map, Value}; + +pub(super) fn normalize_config_id( + mut config: NetworkConfig, + requested_id: String, +) -> Result { + if requested_id.is_empty() { + return Err("config_id is required".to_string()); + } + config.instance_id = Some(requested_id); + Ok(config) +} + +pub(super) fn validate_config_json( + config_json: &str, + config_id: String, +) -> Result { + let config = serde_json::from_str::(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> { + serde_json::to_value(config).ok()?.as_object().cloned() +} diff --git a/easytier-contrib/easytier-ohrs/src/exports.rs b/easytier-contrib/easytier-ohrs/src/exports.rs new file mode 100644 index 00000000..57588876 --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/exports.rs @@ -0,0 +1,2 @@ +pub(crate) mod config_api; +pub(crate) mod runtime_api; diff --git a/easytier-contrib/easytier-ohrs/src/exports/config_api.rs b/easytier-contrib/easytier-ohrs/src/exports/config_api.rs new file mode 100644 index 00000000..7bcf413c --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/exports/config_api.rs @@ -0,0 +1,46 @@ +use crate::config; + +pub(crate) fn init_config_store(root_dir: String) -> bool { + config::repository::init_config_store(root_dir) +} + +pub(crate) fn list_configs() -> String { + config::repository::list_config_meta_json() +} + +pub(crate) fn save_config(config_id: String, display_name: String, config_json: String) -> bool { + config::repository::save_config_record(config_id, display_name, config_json).is_some() +} + +pub(crate) fn create_config(config_id: String, display_name: String) -> bool { + config::repository::create_config_record(config_id, display_name).is_some() +} + +pub(crate) fn delete_stored_config_meta(config_id: String) -> bool { + config::repository::delete_config_record(&config_id) +} + +pub(crate) fn get_config(config_id: String) -> Option { + config::repository::load_config_json(&config_id) +} + +pub(crate) fn get_default_config() -> Option { + config::repository::get_default_config_json() +} + +pub(crate) fn get_config_field(config_id: String, field: String) -> Option { + config::repository::get_config_field_value(&config_id, &field) +} + +pub(crate) fn set_config_field(config_id: String, field: String, json_value: String) -> bool { + config::repository::set_config_field_value(&config_id, &field, &json_value) +} + +pub(crate) fn import_toml(toml_text: String, display_name: Option) -> Option { + config::repository::import_toml_config(toml_text, display_name) + .map(|record| record.meta.config_id) +} + +pub(crate) fn export_toml(config_id: String) -> Option { + config::repository::export_config_toml(&config_id).map(|ret| ret.toml_text) +} diff --git a/easytier-contrib/easytier-ohrs/src/exports/runtime_api.rs b/easytier-contrib/easytier-ohrs/src/exports/runtime_api.rs new file mode 100644 index 00000000..6ca437c4 --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/exports/runtime_api.rs @@ -0,0 +1,184 @@ +use crate::config::repository::load_config_json; +use crate::config::storage::config_meta::get_config_display_name; +use crate::config::types::stored_config::KeyValuePair; +use crate::kernel_bridge::{ + aggregate_requested_tun_routes, start_local_socket_server as start_local_socket_server_inner, + stop_local_socket_server as stop_local_socket_server_inner, +}; +use crate::runtime::state::runtime_state::{ + RuntimeAggregateState, TunAggregateState, clear_tun_attached, mark_tun_attached, + runtime_instance_from_running_info, +}; +use crate::{ASYNC_RUNTIME, EASYTIER_VERSION, INSTANCE_MANAGER, WEB_CLIENTS}; +use easytier::proto::api::manage::NetworkConfig; +use ohos_hilog_binding::{hilog_error, hilog_info}; +use std::sync::Arc; + +pub(crate) fn start_kernel( + config_id: String, + start_kernel_with_config_id: impl Fn(&str) -> bool, +) -> bool { + start_kernel_with_config_id(&config_id) +} + +pub(crate) fn stop_kernel( + config_id: String, + stop_web_client: impl Fn(&str) -> bool, + parse_instance_uuid: impl Fn(&str) -> Option, + maybe_stop_local_socket_server: impl Fn(), +) -> bool { + clear_tun_attached(&config_id); + if stop_web_client(&config_id) { + return true; + } + + let Some(instance_id) = parse_instance_uuid(&config_id) else { + return false; + }; + + let ret = INSTANCE_MANAGER + .delete_network_instance(vec![instance_id]) + .map(|_| true) + .unwrap_or_else(|err| { + hilog_error!("[Rust] stop_kernel failed {}: {}", config_id, err); + false + }); + maybe_stop_local_socket_server(); + ret +} + +pub(crate) fn stop_network_instance( + config_ids: Vec, + 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 { + let infos = match INSTANCE_MANAGER.collect_network_infos_sync() { + Ok(infos) => infos, + Err(err) => { + hilog_error!("[Rust] collect network infos failed {}", err); + return vec![]; + } + }; + + infos + .into_iter() + .filter_map(|(key, value)| { + serde_json::to_string(&value) + .ok() + .map(|value_json| KeyValuePair { + key: key.to_string(), + value: value_json, + }) + }) + .collect() +} + +pub(crate) fn set_tun_fd( + config_id: String, + fd: i32, + parse_instance_uuid: impl Fn(&str) -> Option, +) -> bool { + let Some(instance_id) = parse_instance_uuid(&config_id) else { + hilog_error!("[Rust] set_tun_fd invalid instance id: {}", config_id); + return false; + }; + + INSTANCE_MANAGER + .set_tun_fd(&instance_id, fd) + .map(|_| { + mark_tun_attached(&config_id); + hilog_info!( + "[Rust] set_tun_fd success instance={} fd={} marked_attached=true", + config_id, + fd + ); + true + }) + .unwrap_or_else(|err| { + hilog_error!("[Rust] set_tun_fd failed {}: {}", config_id, err); + false + }) +} + +pub(crate) fn get_runtime_snapshot() -> RuntimeAggregateState { + get_runtime_snapshot_inner() +} + +pub(crate) fn get_runtime_snapshot_inner() -> RuntimeAggregateState { + let infos = match INSTANCE_MANAGER.collect_network_infos_sync() { + Ok(infos) => infos, + Err(err) => { + hilog_error!("[Rust] collect network infos failed {}", err); + return RuntimeAggregateState { + instances: vec![], + tun: TunAggregateState { + active: false, + attached_instance_ids: vec![], + aggregated_routes: vec![], + dns_servers: vec![], + need_rebuild: false, + }, + running_instance_count: 0, + }; + } + }; + + let mut instances = Vec::with_capacity(infos.len()); + for (instance_uuid, info) in infos { + let config_id = instance_uuid.to_string(); + let display_name = get_config_display_name(&config_id).unwrap_or_else(|| config_id.clone()); + let config_json = load_config_json(&config_id); + let stored_config = config_json + .as_deref() + .and_then(|raw| serde_json::from_str::(raw).ok()); + let magic_dns_enabled = stored_config + .as_ref() + .and_then(|cfg| cfg.enable_magic_dns) + .unwrap_or(false); + let need_exit_node = stored_config + .as_ref() + .map(|cfg| !cfg.exit_nodes.is_empty()) + .unwrap_or(false); + instances.push(runtime_instance_from_running_info( + config_id, + display_name, + magic_dns_enabled, + need_exit_node, + info, + )); + } + + instances.sort_by(|a, b| { + a.display_name + .cmp(&b.display_name) + .then_with(|| a.instance_id.cmp(&b.instance_id)) + }); + let attached_instance_ids = instances + .iter() + .filter(|instance| instance.tun_required) + .map(|instance| instance.instance_id.clone()) + .collect::>(); + 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, + } +} diff --git a/easytier-contrib/easytier-ohrs/src/kernel_bridge.rs b/easytier-contrib/easytier-ohrs/src/kernel_bridge.rs new file mode 100644 index 00000000..f01868b0 --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/kernel_bridge.rs @@ -0,0 +1,6 @@ +mod protocol; +mod routing; +mod socket_server; + +pub(crate) use routing::aggregate_requested_tun_routes; +pub use socket_server::{start_local_socket_server, stop_local_socket_server}; diff --git a/easytier-contrib/easytier-ohrs/src/kernel_bridge/protocol.rs b/easytier-contrib/easytier-ohrs/src/kernel_bridge/protocol.rs new file mode 100644 index 00000000..13cc6aed --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/kernel_bridge/protocol.rs @@ -0,0 +1,50 @@ +use crate::config::types::stored_config::LocalSocketSyncMessage; +use serde::Serialize; +use std::io::{Error, ErrorKind, Write}; +use std::os::unix::net::UnixStream; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct TunRequestPayload { + pub config_id: String, + pub instance_id: String, + pub display_name: String, + pub virtual_ipv4: Option, + pub virtual_ipv4_cidr: Option, + pub aggregated_routes: Vec, + 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, + 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 +} diff --git a/easytier-contrib/easytier-ohrs/src/kernel_bridge/routing.rs b/easytier-contrib/easytier-ohrs/src/kernel_bridge/routing.rs new file mode 100644 index 00000000..52a1649f --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/kernel_bridge/routing.rs @@ -0,0 +1,105 @@ +use crate::config::repository::load_config_json; +use crate::runtime::state::runtime_state::RuntimeInstanceState; +use easytier::proto::api::manage::NetworkConfig; +use ipnet::IpNet; +use ohos_hilog_binding::hilog_debug; +use std::collections::HashSet; +use std::net::IpAddr; + +pub(crate) fn load_manual_routes(config_id: &str) -> Vec { + load_config_json(config_id) + .and_then(|raw| serde_json::from_str::(&raw).ok()) + .map(|config| config.routes) + .unwrap_or_default() +} + +fn normalize_route_cidr(route: &str) -> Option { + route + .parse::() + .ok() + .map(|network| match network { + IpNet::V4(net) => net.trunc().to_string(), + IpNet::V6(net) => net.trunc().to_string(), + }) + .or_else(|| { + route.parse::().ok().map(|addr| match addr { + IpAddr::V4(ip) => format!("{}/32", ip), + IpAddr::V6(ip) => format!("{}/128", ip), + }) + }) +} + +fn simplify_routes(routes: Vec) -> Vec { + let mut parsed = routes + .into_iter() + .filter_map(|route| normalize_route_cidr(&route)) + .filter_map(|route| route.parse::().ok()) + .collect::>(); + 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::::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 { + let virtual_ipv4_cidr = instance + .my_node_info + .as_ref() + .and_then(|info| info.virtual_ipv4_cidr.clone()); + let manual_routes = load_manual_routes(&instance.config_id); + let proxy_cidrs = instance + .routes + .iter() + .flat_map(|route| route.proxy_cidrs.iter().cloned()) + .collect::>(); + let mut raw_routes = Vec::new(); + + if let Some(cidr) = virtual_ipv4_cidr.clone() { + raw_routes.push(cidr); + } + + raw_routes.extend(manual_routes.iter().cloned()); + raw_routes.extend(proxy_cidrs.iter().cloned()); + let aggregated_routes = simplify_routes(raw_routes); + hilog_debug!( + "[Rust] aggregate_tun_routes instance={} proxy_cidrs={:?} aggregated_routes={:?}", + instance.instance_id, + proxy_cidrs, + aggregated_routes + ); + aggregated_routes +} + +pub(crate) fn aggregate_requested_tun_routes(instances: &[RuntimeInstanceState]) -> Vec { + 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 +} diff --git a/easytier-contrib/easytier-ohrs/src/kernel_bridge/socket_server.rs b/easytier-contrib/easytier-ohrs/src/kernel_bridge/socket_server.rs new file mode 100644 index 00000000..f91372cc --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/kernel_bridge/socket_server.rs @@ -0,0 +1,196 @@ +use super::protocol::{TunRequestPayload, broadcast_local_socket_message}; +use crate::config::repository::kernel_socket_path; +use crate::get_runtime_snapshot_inner; +use crate::kernel_bridge::routing::aggregate_tun_routes; +use ohos_hilog_binding::{hilog_error, hilog_info}; +use once_cell::sync::Lazy; +use std::collections::{HashMap, HashSet}; +use std::io::ErrorKind; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::path::PathBuf; +use std::sync::Mutex; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::thread::{self, JoinHandle}; +use std::time::Duration; + +struct LocalSocketState { + stop_flag: std::sync::Arc, + socket_path: PathBuf, + worker: JoinHandle<()>, +} + +static LOCAL_SOCKET_STATE: Lazy>> = Lazy::new(|| Mutex::new(None)); + +pub fn start_local_socket_server() -> bool { + let socket_path = match kernel_socket_path() { + Some(path) => path, + None => { + hilog_error!("[Rust] kernel socket path unavailable"); + return false; + } + }; + + match LOCAL_SOCKET_STATE.lock() { + Ok(guard) if guard.is_some() => return true, + Ok(_) => {} + Err(err) => { + hilog_error!("[Rust] lock localsocket state failed: {}", err); + return false; + } + } + + if socket_path.exists() { + let _ = std::fs::remove_file(&socket_path); + } + + let listener = match UnixListener::bind(&socket_path) { + Ok(listener) => listener, + Err(err) => { + hilog_error!( + "[Rust] bind localsocket failed {}: {}", + socket_path.display(), + err + ); + return false; + } + }; + if let Err(err) = listener.set_nonblocking(true) { + hilog_error!("[Rust] set localsocket nonblocking failed: {}", err); + let _ = std::fs::remove_file(&socket_path); + return false; + } + + let stop_flag = std::sync::Arc::new(AtomicBool::new(false)); + let worker_stop_flag = stop_flag.clone(); + let worker = thread::spawn(move || { + let mut last_snapshot_json = String::new(); + let mut delivered_tun_requests = HashSet::new(); + let mut last_tun_route_signatures = HashMap::::new(); + let mut clients = Vec::::new(); + + while !worker_stop_flag.load(Ordering::Relaxed) { + let mut accepted_client = false; + loop { + match listener.accept() { + Ok((stream, _addr)) => { + accepted_client = true; + clients.push(stream); + } + Err(err) if err.kind() == ErrorKind::WouldBlock => break, + Err(err) => { + hilog_error!("[Rust] accept localsocket failed: {}", err); + break; + } + } + } + + let snapshot = get_runtime_snapshot_inner(); + let snapshot_json = match serde_json::to_string(&snapshot) { + Ok(json) => json, + Err(err) => { + hilog_error!("[Rust] serialize runtime snapshot failed: {}", err); + thread::sleep(Duration::from_millis(250)); + continue; + } + }; + + if accepted_client || snapshot_json != last_snapshot_json { + let _ = broadcast_local_socket_message( + &mut clients, + "runtime_snapshot", + &snapshot_json, + ); + last_snapshot_json = snapshot_json; + } + + for instance in snapshot.instances.iter() { + if instance.running && instance.tun_required { + let virtual_ipv4 = instance + .my_node_info + .as_ref() + .and_then(|info| info.virtual_ipv4.clone()); + let virtual_ipv4_cidr = instance + .my_node_info + .as_ref() + .and_then(|info| info.virtual_ipv4_cidr.clone()); + if clients.is_empty() { + continue; + } + if virtual_ipv4.is_none() || virtual_ipv4_cidr.is_none() { + continue; + } + let aggregated_routes = aggregate_tun_routes(instance); + let route_signature = serde_json::to_string(&aggregated_routes) + .unwrap_or_else(|_| "[]".to_string()); + let should_send = !delivered_tun_requests.contains(&instance.instance_id) + || last_tun_route_signatures + .get(&instance.instance_id) + .map(|value| value != &route_signature) + .unwrap_or(true); + if !should_send { + continue; + } + let payload = TunRequestPayload { + config_id: instance.config_id.clone(), + instance_id: instance.instance_id.clone(), + display_name: instance.display_name.clone(), + virtual_ipv4, + virtual_ipv4_cidr, + aggregated_routes, + magic_dns_enabled: instance.magic_dns_enabled, + need_exit_node: instance.need_exit_node, + }; + let payload_json = match serde_json::to_string(&payload) { + Ok(json) => json, + Err(err) => { + hilog_error!("[Rust] serialize tun request failed: {}", err); + continue; + } + }; + if broadcast_local_socket_message(&mut clients, "tun_request", &payload_json) { + delivered_tun_requests.insert(instance.instance_id.clone()); + last_tun_route_signatures + .insert(instance.instance_id.clone(), route_signature); + } + } else { + delivered_tun_requests.remove(&instance.instance_id); + last_tun_route_signatures.remove(&instance.instance_id); + } + } + + thread::sleep(Duration::from_millis(250)); + } + }); + + match LOCAL_SOCKET_STATE.lock() { + Ok(mut guard) => { + *guard = Some(LocalSocketState { + stop_flag, + socket_path, + worker, + }); + true + } + Err(err) => { + hilog_error!("[Rust] lock localsocket state failed: {}", err); + false + } + } +} + +pub fn stop_local_socket_server() -> bool { + let state = match LOCAL_SOCKET_STATE.lock() { + Ok(mut guard) => guard.take(), + Err(err) => { + hilog_error!("[Rust] lock localsocket state failed: {}", err); + return false; + } + }; + + if let Some(state) = state { + state.stop_flag.store(true, Ordering::Relaxed); + let _ = state.worker.join(); + let _ = std::fs::remove_file(state.socket_path); + } + true +} diff --git a/easytier-contrib/easytier-ohrs/src/lib.rs b/easytier-contrib/easytier-ohrs/src/lib.rs index 352ce3c7..48a79968 100644 --- a/easytier-contrib/easytier-ohrs/src/lib.rs +++ b/easytier-contrib/easytier-ohrs/src/lib.rs @@ -1,185 +1,485 @@ -mod native_log; +mod config; +mod exports; +mod kernel_bridge; +mod platform; +mod runtime; -use easytier::common::config::{ConfigFileControl, ConfigLoader, TomlConfigLoader}; +use config::repository::{ + create_config_record, delete_config_record, export_config_toml, get_config_field_value, + get_default_config_json, import_toml_config, init_config_store as init_repo_store, + list_config_meta_json, save_config_record, set_config_field_value, start_kernel_with_config_id, +}; +use config::services::schema_service::{ + ConfigFieldMapping, NetworkConfigSchema, + get_network_config_field_mappings as build_network_config_field_mappings, + get_network_config_schema as build_network_config_schema, +}; +use config::services::share_link_service::{ + build_config_share_link as build_config_share_link_inner, + import_config_share_link as import_config_share_link_inner, + parse_config_share_link as parse_config_share_link_inner, +}; +use config::storage::config_meta::get_config_display_name; +use config::types::stored_config::{KeyValuePair, SharedConfigLinkPayload}; use easytier::common::constants::EASYTIER_VERSION; +use easytier::common::{ + MachineIdOptions, + config::{ConfigFileControl, ConfigLoader, TomlConfigLoader}, +}; use easytier::instance_manager::NetworkInstanceManager; use easytier::proto::api::manage::NetworkConfig; +use easytier::proto::api::manage::NetworkingMethod; +use easytier::web_client::{WebClient, WebClientHooks, run_web_client}; +use kernel_bridge::{ + aggregate_requested_tun_routes, start_local_socket_server as start_local_socket_server_inner, + stop_local_socket_server as stop_local_socket_server_inner, +}; use napi_derive_ohos::napi; -use ohos_hilog_binding::{hilog_debug, hilog_error}; +use ohos_hilog_binding::{hilog_error, hilog_info}; +use runtime::state::runtime_state::{ + RuntimeAggregateState, TunAggregateState, clear_tun_attached, mark_tun_attached, + runtime_instance_from_running_info, +}; +use std::collections::{HashMap, HashSet}; use std::format; +use std::sync::{Arc, Mutex}; +use tokio::runtime::{Builder, Runtime}; use uuid::Uuid; -static INSTANCE_MANAGER: once_cell::sync::Lazy = - once_cell::sync::Lazy::new(NetworkInstanceManager::new); +pub(crate) static INSTANCE_MANAGER: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| Arc::new(NetworkInstanceManager::new())); +static ASYNC_RUNTIME: once_cell::sync::Lazy = 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>> = + once_cell::sync::Lazy::new(|| Mutex::new(HashMap::new())); -#[napi(object)] -pub struct KeyValuePair { - pub key: String, - pub value: String, +#[derive(Default)] +struct TrackedWebClientHooks { + instance_ids: Mutex>, } -#[napi] -pub fn easytier_version() -> String { - EASYTIER_VERSION.to_string() +struct ManagedWebClient { + _client: WebClient, + hooks: Arc, } -#[napi] -pub fn set_tun_fd(inst_id: String, fd: i32) -> bool { - match Uuid::try_parse(&inst_id) { - Ok(uuid) => match INSTANCE_MANAGER.set_tun_fd(&uuid, fd) { - Ok(_) => { - hilog_debug!("[Rust] set tun fd {} to {}.", fd, inst_id); - true - } - Err(e) => { - hilog_error!("[Rust] cant set tun fd {} to {}. {}", fd, inst_id, e); - false - } - }, - Err(e) => { - hilog_error!("[Rust] cant covert {} to uuid. {}", inst_id, e); +#[async_trait::async_trait] +impl WebClientHooks for TrackedWebClientHooks { + async fn post_run_network_instance(&self, id: &Uuid) -> Result<(), String> { + self.instance_ids + .lock() + .map_err(|err| err.to_string())? + .insert(*id); + Ok(()) + } + + async fn post_remove_network_instances(&self, ids: &[Uuid]) -> Result<(), String> { + let mut guard = self.instance_ids.lock().map_err(|err| err.to_string())?; + for id in ids { + guard.remove(id); + } + Ok(()) + } +} + +fn is_config_server_config(config: &NetworkConfig) -> bool { + matches!( + NetworkingMethod::try_from(config.networking_method.unwrap_or_default()) + .unwrap_or_default(), + NetworkingMethod::PublicServer + ) && config + .public_server_url + .as_ref() + .is_some_and(|url| !url.trim().is_empty()) +} + +fn stop_web_client(config_id: &str) -> bool { + let managed = match WEB_CLIENTS.lock() { + Ok(mut guard) => guard.remove(config_id), + Err(err) => { + hilog_error!("[Rust] stop_web_client lock failed {}", err); + return false; + } + }; + + let Some(managed) = managed else { + return false; + }; + + let tracked_ids = managed + .hooks + .instance_ids + .lock() + .map(|guard| guard.iter().copied().collect::>()) + .unwrap_or_default(); + drop(managed); + + if tracked_ids.is_empty() { + maybe_stop_local_socket_server(); + return true; + } + + let ret = INSTANCE_MANAGER + .delete_network_instance(tracked_ids) + .map(|_| true) + .unwrap_or_else(|err| { + hilog_error!( + "[Rust] stop config server instances failed {}: {}", + config_id, + err + ); + false + }); + maybe_stop_local_socket_server(); + ret +} + +fn ensure_local_socket_server_started() -> bool { + start_local_socket_server_inner() +} + +fn maybe_stop_local_socket_server() { + let no_local_instances = INSTANCE_MANAGER.list_network_instance_ids().is_empty(); + let no_web_clients = WEB_CLIENTS + .lock() + .map(|guard| guard.is_empty()) + .unwrap_or(false); + if no_local_instances && no_web_clients { + let _ = stop_local_socket_server_inner(); + } +} + +fn run_config_server_instance(config_id: &str, config: &NetworkConfig) -> bool { + if INSTANCE_MANAGER + .list_network_instance_ids() + .iter() + .next() + .is_some() + { + hilog_error!("[Rust] there is a running instance!"); + return false; + } + + let Some(config_server_url) = config.public_server_url.clone() else { + hilog_error!("[Rust] public_server_url missing for config server mode"); + return false; + }; + let hooks = Arc::new(TrackedWebClientHooks::default()); + let secure_mode = config + .secure_mode + .as_ref() + .map(|mode| mode.enabled) + .unwrap_or(false); + let hostname = config.hostname.clone(); + + if !ensure_local_socket_server_started() { + return false; + } + + let client = ASYNC_RUNTIME.block_on(run_web_client( + &config_server_url, + MachineIdOptions::default(), + hostname, + secure_mode, + INSTANCE_MANAGER.clone(), + Some(hooks.clone()), + )); + + let client = match client { + Ok(client) => client, + Err(err) => { + hilog_error!("[Rust] start config server failed {}", err); + return false; + } + }; + + match WEB_CLIENTS.lock() { + Ok(mut guard) => { + guard.insert( + config_id.to_string(), + ManagedWebClient { + _client: client, + hooks, + }, + ); + true + } + Err(err) => { + hilog_error!("[Rust] store config server client failed {}", err); false } } } -#[napi] -pub fn default_network_config() -> String { - match NetworkConfig::new_from_config(TomlConfigLoader::default()) { - Ok(result) => serde_json::to_string(&result).unwrap_or_else(|e| format!("ERROR {}", e)), - Err(e) => { - hilog_error!("[Rust] default_network_config failed {}", e); - format!("ERROR {}", e) - } - } +pub(crate) fn build_default_network_config_json() -> Result { + let config = NetworkConfig::new_from_config(TomlConfigLoader::default()) + .map_err(|e| format!("default_network_config failed {}", e))?; + serde_json::to_string(&config).map_err(|e| format!("default_network_config failed {}", e)) } -#[napi] -pub fn convert_toml_to_network_config(cfg_str: String) -> String { - match TomlConfigLoader::new_from_str(&cfg_str) { - Ok(cfg) => match NetworkConfig::new_from_config(cfg) { - Ok(result) => serde_json::to_string(&result).unwrap_or_else(|e| format!("ERROR {}", e)), - Err(e) => { - hilog_error!("[Rust] convert_toml_to_network_config failed {}", e); - format!("ERROR {}", e) - } - }, - Err(e) => { - hilog_error!("[Rust] convert_toml_to_network_config failed {}", e); - format!("ERROR {}", e) - } - } +fn convert_toml_to_network_config_inner(toml_text: &str) -> Result { + let config = NetworkConfig::new_from_config( + TomlConfigLoader::new_from_str(toml_text).map_err(|e| e.to_string())?, + ) + .map_err(|e| e.to_string())?; + serde_json::to_string(&config).map_err(|e| e.to_string()) } -#[napi] -pub fn parse_network_config(cfg_json: String) -> bool { - match serde_json::from_str::(&cfg_json) { - Ok(cfg) => match cfg.gen_config() { - Ok(toml) => { - hilog_debug!("[Rust] Convert to Toml {}", toml.dump()); - true - } - Err(e) => { - hilog_error!("[Rust] parse config failed {}", e); - false - } - }, - Err(e) => { - hilog_error!("[Rust] parse config failed {}", e); - false - } - } +fn parse_network_config_inner(cfg_json: &str) -> bool { + serde_json::from_str::(cfg_json) + .ok() + .and_then(|cfg| cfg.gen_config().ok()) + .is_some() } -#[napi] -pub fn run_network_instance(cfg_json: String) -> bool { - let cfg = match serde_json::from_str::(&cfg_json) { - Ok(cfg) => match cfg.gen_config() { - Ok(toml) => toml, - Err(e) => { - hilog_error!("[Rust] parse config failed {}", e); - return false; - } - }, +pub(crate) fn run_network_instance_from_json(cfg_json: &str) -> bool { + let config = match serde_json::from_str::(cfg_json) { + Ok(cfg) => cfg, Err(e) => { hilog_error!("[Rust] parse config failed {}", e); return false; } }; - if INSTANCE_MANAGER.list_network_instance_ids().len() > 0 { + if is_config_server_config(&config) { + let Some(config_id) = config.instance_id.as_deref() else { + hilog_error!("[Rust] config server config missing instance id"); + return false; + }; + return run_config_server_instance(config_id, &config); + } + + let cfg = match config.gen_config() { + Ok(toml) => toml, + Err(e) => { + hilog_error!("[Rust] parse config failed {}", e); + return false; + } + }; + + if !INSTANCE_MANAGER.list_network_instance_ids().is_empty() { hilog_error!("[Rust] there is a running instance!"); return false; } + if !ensure_local_socket_server_started() { + return false; + } + let inst_id = cfg.get_id(); if INSTANCE_MANAGER .list_network_instance_ids() .contains(&inst_id) { + hilog_error!("[Rust] instance {} already exists", inst_id); return false; } - INSTANCE_MANAGER - .run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) - .unwrap(); - true -} -#[napi] -pub fn stop_network_instance(inst_names: Vec) { - INSTANCE_MANAGER - .delete_network_instance( - inst_names - .into_iter() - .filter_map(|s| Uuid::parse_str(&s).ok()) - .collect(), - ) - .unwrap(); - hilog_debug!("[Rust] stop_network_instance"); -} - -#[napi] -pub fn collect_network_infos() -> Vec { - let mut result = Vec::new(); - match INSTANCE_MANAGER.collect_network_infos_sync() { - Ok(map) => { - for (uuid, info) in map.iter() { - // convert value to json string - let value = match serde_json::to_string(&info) { - Ok(value) => value, - Err(e) => { - hilog_error!("[Rust] failed to serialize instance {} info: {}", uuid, e); - continue; - } - }; - result.push(KeyValuePair { - key: uuid.clone().to_string(), - value: value.clone(), - }); - } - } - Err(_) => {} - } - result -} - -#[napi] -pub fn collect_running_network() -> Vec { - INSTANCE_MANAGER - .list_network_instance_ids() - .clone() - .into_iter() - .map(|id| id.to_string()) - .collect() -} - -#[napi] -pub fn is_running_network(inst_id: String) -> bool { - match Uuid::try_parse(&inst_id) { - Ok(uuid) => INSTANCE_MANAGER.list_network_instance_ids().contains(&uuid), - Err(e) => { - hilog_error!("[Rust] cant covert {} to uuid. {}", inst_id, e); + match INSTANCE_MANAGER.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) { + Ok(_) => true, + Err(err) => { + hilog_error!("[Rust] start_kernel failed for {}: {}", inst_id, err); false } } } + +fn parse_instance_uuid(config_id: &str) -> Option { + match Uuid::parse_str(config_id) { + Ok(uuid) => Some(uuid), + Err(err) => { + hilog_error!("[Rust] invalid config_id {}: {}", config_id, err); + None + } + } +} + +#[napi] +pub fn init_config_store(root_dir: String) -> bool { + exports::config_api::init_config_store(root_dir) +} + +#[napi] +pub fn list_configs() -> String { + exports::config_api::list_configs() +} + +#[napi] +pub fn get_config_display_name_by_id(config_id: String) -> Option { + 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 { + exports::config_api::get_config(config_id) +} + +#[napi] +pub fn get_default_config() -> Option { + exports::config_api::get_default_config() +} + +#[napi] +pub fn get_config_field(config_id: String, field: String) -> Option { + exports::config_api::get_config_field(config_id, field) +} + +#[napi] +pub fn set_config_field(config_id: String, field: String, json_value: String) -> bool { + exports::config_api::set_config_field(config_id, field, json_value) +} + +#[napi] +pub fn import_toml(toml_text: String, display_name: Option) -> Option { + exports::config_api::import_toml(toml_text, display_name) +} + +#[napi] +pub fn export_toml(config_id: String) -> Option { + exports::config_api::export_toml(config_id) +} + +#[napi] +pub fn start_kernel(config_id: String) -> bool { + exports::runtime_api::start_kernel(config_id, start_kernel_with_config_id) +} + +#[napi] +pub fn stop_kernel(config_id: String) -> bool { + exports::runtime_api::stop_kernel( + config_id, + stop_web_client, + parse_instance_uuid, + maybe_stop_local_socket_server, + ) +} + +#[napi] +pub fn stop_network_instance(config_ids: Vec) -> 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 { + 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 { + build_network_config_field_mappings() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn exported_plain_object_schema_contains_core_networkconfig_metadata() { + let schema = get_network_config_schema(); + assert_eq!(schema.name, "NetworkConfig"); + assert_eq!(schema.node_kind, "schema"); + assert!( + schema + .children + .iter() + .any(|field| field.name == "network_name") + ); + let secure_mode = schema + .children + .iter() + .find(|field| field.name == "secure_mode") + .expect("secure_mode field"); + assert!( + secure_mode + .children + .iter() + .any(|field| field.name == "enabled") + ); + } +} + +#[napi] +pub fn get_runtime_snapshot() -> RuntimeAggregateState { + exports::runtime_api::get_runtime_snapshot() +} + +pub(crate) fn get_runtime_snapshot_inner() -> RuntimeAggregateState { + exports::runtime_api::get_runtime_snapshot_inner() +} + +#[napi] +pub fn build_config_share_link(config_id: String, only_start: Option) -> Option { + build_config_share_link_inner(&config_id, None, only_start.unwrap_or(false)) +} + +#[napi] +pub fn parse_config_share_link(share_link: String) -> Option { + parse_config_share_link_inner(&share_link) +} + +#[napi] +pub fn import_config_share_link( + share_link: String, + display_name_override: Option, +) -> Option { + import_config_share_link_inner(&share_link, display_name_override) +} diff --git a/easytier-contrib/easytier-ohrs/src/platform.rs b/easytier-contrib/easytier-ohrs/src/platform.rs new file mode 100644 index 00000000..6a79dd07 --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/platform.rs @@ -0,0 +1 @@ +pub(crate) mod logging; diff --git a/easytier-contrib/easytier-ohrs/src/platform/logging/mod.rs b/easytier-contrib/easytier-ohrs/src/platform/logging/mod.rs new file mode 100644 index 00000000..0b44f5b3 --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/platform/logging/mod.rs @@ -0,0 +1 @@ +pub(crate) mod native_log; diff --git a/easytier-contrib/easytier-ohrs/src/native_log.rs b/easytier-contrib/easytier-ohrs/src/platform/logging/native_log.rs similarity index 100% rename from easytier-contrib/easytier-ohrs/src/native_log.rs rename to easytier-contrib/easytier-ohrs/src/platform/logging/native_log.rs diff --git a/easytier-contrib/easytier-ohrs/src/runtime.rs b/easytier-contrib/easytier-ohrs/src/runtime.rs new file mode 100644 index 00000000..33e14d22 --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/runtime.rs @@ -0,0 +1 @@ +pub(crate) mod state; diff --git a/easytier-contrib/easytier-ohrs/src/runtime/state/mod.rs b/easytier-contrib/easytier-ohrs/src/runtime/state/mod.rs new file mode 100644 index 00000000..f84ecc4a --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/runtime/state/mod.rs @@ -0,0 +1 @@ +pub(crate) mod runtime_state; diff --git a/easytier-contrib/easytier-ohrs/src/runtime/state/runtime_state.rs b/easytier-contrib/easytier-ohrs/src/runtime/state/runtime_state.rs new file mode 100644 index 00000000..2a1cb059 --- /dev/null +++ b/easytier-contrib/easytier-ohrs/src/runtime/state/runtime_state.rs @@ -0,0 +1,293 @@ +use easytier::proto::{api, common}; +use napi_derive_ohos::napi; +use serde::Serialize; +use std::collections::HashSet; +use std::sync::Mutex; + +static ATTACHED_TUN_INSTANCE_IDS: once_cell::sync::Lazy>> = + 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, + pub tunnel_type: Option, + pub local_addr: Option, + pub remote_addr: Option, + pub resolved_remote_addr: Option, + pub stats: Option, + pub loss_rate: Option, + pub is_client: bool, + pub network_name: Option, + pub is_closed: bool, + pub secure_auth_level: Option, + pub peer_identity_type: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +#[napi(object)] +pub struct PeerInfo { + pub peer_id: i64, + pub default_conn_id: Option, + pub directly_connected_conns: Vec, + pub conns: Vec, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +#[napi(object)] +pub struct RouteView { + pub peer_id: i64, + pub hostname: Option, + pub ipv4: Option, + pub ipv4_cidr: Option, + pub ipv6_cidr: Option, + pub proxy_cidrs: Vec, + pub next_hop_peer_id: Option, + pub cost: Option, + pub path_latency: Option, + pub udp_nat_type: Option, + pub tcp_nat_type: Option, + pub inst_id: Option, + pub version: Option, + pub is_public_server: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +#[napi(object)] +pub struct MyNodeInfo { + pub virtual_ipv4: Option, + pub virtual_ipv4_cidr: Option, + pub hostname: Option, + pub version: Option, + pub peer_id: Option, + pub listeners: Vec, + pub vpn_portal_cfg: Option, + pub udp_nat_type: Option, + pub tcp_nat_type: Option, +} + +#[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, + pub my_node_info: Option, + pub events: Vec, + pub routes: Vec, + pub peers: Vec, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +#[napi(object)] +pub struct TunAggregateState { + pub active: bool, + pub attached_instance_ids: Vec, + pub aggregated_routes: Vec, + pub dns_servers: Vec, + pub need_rebuild: bool, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +#[napi(object)] +pub struct RuntimeAggregateState { + pub instances: Vec, + pub tun: TunAggregateState, + pub running_instance_count: i32, +} + +fn stringify_ipv4_inet(value: Option) -> Option { + value.map(|v| v.to_string()) +} + +fn stringify_ipv6_inet(value: Option) -> Option { + value.map(|v| v.to_string()) +} + +fn stringify_url(value: Option) -> Option { + value.map(|v| v.to_string()) +} + +fn stringify_uuid(value: Option) -> Option { + value.map(|v| v.to_string()) +} + +fn optional_u32_to_i64(value: Option) -> Option { + value.map(|v| v as i64) +} + +fn optional_i32_to_i64(value: Option) -> Option { + 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(), + } +} diff --git a/easytier/src/proto/mod.rs b/easytier/src/proto/mod.rs index cb455a89..3315a5da 100644 --- a/easytier/src/proto/mod.rs +++ b/easytier/src/proto/mod.rs @@ -14,5 +14,8 @@ pub mod web; pub mod tests; pub mod utils; -const DESCRIPTOR_POOL_BYTES: &[u8] = +pub const DESCRIPTOR_POOL_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/file_descriptor_set.bin")); + +pub const ALL_DESCRIPTOR_BYTES: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/descriptors.bin"));