Compare commits

..

10 Commits

Author SHA1 Message Date
Sijie.Sun
b44053f496 support p2p-only mode (#1598) 2025-11-20 08:20:27 +08:00
Sijie.Sun
5b9ac65477 update readme (#1594) 2025-11-17 16:00:32 +08:00
Mg Pig
d726d46a00 fix: Preserve disable_sym_hole_punching setting on edit (#1589) 2025-11-15 18:57:59 +08:00
Mg Pig
1273426009 feat: Enable core to use local config files while being managed via the web (#1540) 2025-11-08 20:32:00 +08:00
Sijie.Sun
b50744690e easytier-web and uptime use mimalloc as allocator (#1559) 2025-11-08 11:07:33 +08:00
Tunglies
55b93454dc fix: clippy errors with stable toolchain and default features (#1553)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-07 20:08:39 +08:00
Mg Pig
89cc75f674 refactor: replace ConfigSource with bool parameter (#1516) 2025-11-04 13:48:10 +08:00
Mg Pig
6bb2fd9a15 feat(core): Refactor IDN and URL handling logic (#1533)
* feat(core): Refactor IDN and URL handling logic

* feat(tests): add dual_convert option for URL serialization in IDN tests
2025-11-03 22:15:40 +08:00
Mg Pig
8ab98bba8f feat(ui): make port forward config responsive (#1530) 2025-10-31 23:23:36 +08:00
韩嘉乐
26d002bc2b The flowback solution of HarmonyOS 5 failed due to the anti-loop mechanism. (#1514) 2025-10-25 00:17:24 +08:00
58 changed files with 1373 additions and 978 deletions

View File

@@ -122,6 +122,8 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: easytier-ohos
path: ./easytier-contrib/easytier-ohrs/easytier-ohrs.har
path: |
./easytier-contrib/easytier-ohrs/easytier-ohrs.har
./easytier-contrib/easytier-ohrs/dist/arm64-v8a/libeasytier_ohrs.so
retention-days: 5
if-no-files-found: error

157
Cargo.lock generated
View File

@@ -8,15 +8,6 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
[[package]]
name = "addr2line"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
@@ -737,21 +728,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "backtrace"
version = "0.3.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide 0.7.4",
"object",
"rustc-demangle",
]
[[package]]
name = "base62"
version = "2.0.2"
@@ -2004,7 +1980,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.0",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -2171,7 +2147,6 @@ dependencies = [
"nix 0.29.0",
"once_cell",
"openssl",
"ordered_hash_map",
"parking_lot",
"percent-encoding",
"petgraph 0.8.1",
@@ -2200,7 +2175,7 @@ dependencies = [
"service-manager",
"sha2",
"smoltcp",
"socket2",
"socket2 0.5.10",
"stun_codec",
"sys-locale",
"tabled",
@@ -2328,6 +2303,7 @@ dependencies = [
"easytier",
"futures",
"jsonwebtoken",
"mimalloc",
"mockall",
"once_cell",
"parking_lot",
@@ -2370,6 +2346,7 @@ dependencies = [
"image 0.24.9",
"imageproc",
"maxminddb",
"mimalloc",
"once_cell",
"password-auth",
"rand 0.8.5",
@@ -2810,9 +2787,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
@@ -2820,9 +2797,9 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
@@ -2848,9 +2825,9 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-lite"
@@ -2867,9 +2844,9 @@ dependencies = [
[[package]]
name = "futures-macro"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
@@ -2878,15 +2855,15 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-timer"
@@ -2896,9 +2873,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
@@ -3121,12 +3098,6 @@ dependencies = [
"polyval",
]
[[package]]
name = "gimli"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
[[package]]
name = "gio"
version = "0.18.4"
@@ -3736,7 +3707,7 @@ dependencies = [
"http-body",
"hyper",
"pin-project-lite",
"socket2",
"socket2 0.5.10",
"tokio",
"tower-service",
"tracing",
@@ -4069,7 +4040,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
dependencies = [
"socket2",
"socket2 0.5.10",
"widestring",
"windows-sys 0.48.0",
"winreg 0.50.0",
@@ -5432,15 +5403,6 @@ dependencies = [
"objc2-foundation 0.3.1",
]
[[package]]
name = "object"
version = "0.36.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -5547,15 +5509,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "ordered_hash_map"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6c699f8a30f345785be969deed7eee4c73a5de58c7faf61d6a3251ef798ff61"
dependencies = [
"hashbrown 0.15.3",
]
[[package]]
name = "os_info"
version = "3.8.2"
@@ -6448,7 +6401,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"socket2 0.5.10",
"thiserror 2.0.11",
"tokio",
"tracing",
@@ -6486,7 +6439,7 @@ checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285"
dependencies = [
"libc",
"once_cell",
"socket2",
"socket2 0.5.10",
"tracing",
"windows-sys 0.52.0",
]
@@ -7058,12 +7011,6 @@ dependencies = [
"serde_json",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.0"
@@ -7933,6 +7880,16 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
dependencies = [
"libc",
"windows-sys 0.60.2",
]
[[package]]
name = "softbuffer"
version = "0.4.5"
@@ -8344,9 +8301,9 @@ dependencies = [
[[package]]
name = "sync_wrapper"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [
"futures-core",
]
@@ -8853,7 +8810,7 @@ dependencies = [
"getrandom 0.3.2",
"once_cell",
"rustix 1.0.7",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -9046,28 +9003,27 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.45.1"
version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"socket2 0.6.1",
"tokio-macros",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
@@ -9299,7 +9255,7 @@ dependencies = [
"percent-encoding",
"pin-project",
"prost",
"socket2",
"socket2 0.5.10",
"tokio",
"tokio-stream",
"tower 0.4.13",
@@ -10410,7 +10366,7 @@ dependencies = [
"windows-collections",
"windows-core 0.61.2",
"windows-future",
"windows-link",
"windows-link 0.1.3",
"windows-numerics",
]
@@ -10453,7 +10409,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-link",
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
@@ -10465,7 +10421,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-link",
"windows-link 0.1.3",
"windows-threading",
]
@@ -10519,6 +10475,12 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.2.0"
@@ -10526,7 +10488,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.2",
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -10555,7 +10517,7 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -10585,7 +10547,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -10633,6 +10595,15 @@ dependencies = [
"windows-targets 0.53.2",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
@@ -10701,7 +10672,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]

View File

@@ -280,8 +280,6 @@ sudo easytier-core --network-name mysharednode --network-secret mysharednode
- [ZeroTier](https://www.zerotier.com/): A global virtual network for connecting devices.
- [TailScale](https://tailscale.com/): A VPN solution aimed at simplifying network configuration.
- [vpncloud](https://github.com/dswd/vpncloud): A P2P Mesh VPN
- [Candy](https://github.com/lanthora/candy): A reliable, low-latency, and anti-censorship virtual private network
### Contact Us

View File

@@ -281,8 +281,6 @@ sudo easytier-core --network-name mysharednode --network-secret mysharednode
- [ZeroTier](https://www.zerotier.com/):用于连接设备的全球虚拟网络。
- [TailScale](https://tailscale.com/):旨在简化网络配置的 VPN 解决方案。
- [vpncloud](https://github.com/dswd/vpncloud):一个 P2P 网状 VPN
- [Candy](https://github.com/lanthora/candy):一个可靠、低延迟、反审查的虚拟专用网络
### 联系我们

View File

@@ -2,9 +2,8 @@ use std::sync::Mutex;
use dashmap::DashMap;
use easytier::{
common::config::{ConfigLoader as _, TomlConfigLoader},
common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader},
instance_manager::NetworkInstanceManager,
launcher::ConfigSource,
};
static INSTANCE_NAME_ID_MAP: once_cell::sync::Lazy<DashMap<String, uuid::Uuid>> =
@@ -129,13 +128,14 @@ pub unsafe extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char)
return -1;
}
let instance_id = match INSTANCE_MANAGER.run_network_instance(cfg, ConfigSource::FFI) {
Ok(id) => id,
Err(e) => {
set_error_msg(&format!("failed to start instance: {}", e));
return -1;
}
};
let instance_id =
match INSTANCE_MANAGER.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) {
Ok(id) => id,
Err(e) => {
set_error_msg(&format!("failed to start instance: {}", e));
return -1;
}
};
INSTANCE_NAME_ID_MAP.insert(inst_name, instance_id);

View File

@@ -133,16 +133,6 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "async-event"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1222afd3d2bce3995035054046a279ae7aa154d70d0766cea050073f3fd7ddf"
dependencies = [
"loom 0.5.6",
"pin-project-lite",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
@@ -487,9 +477,9 @@ dependencies = [
[[package]]
name = "cidr"
version = "0.2.3"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdf600c45bd958cf2945c445264471cca8b6c8e67bc87b71affd6d7e5682621"
checksum = "bd1b64030216239a2e7c364b13cd96a2097ebf0dfe5025f2dedee14a23f2ab60"
dependencies = [
"serde",
]
@@ -946,16 +936,6 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "diatomic-waker"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28025fb55a9d815acf7b0877555f437254f373036eec6ed265116c7a5c0825e9"
dependencies = [
"loom 0.5.6",
"waker-fn",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -1015,8 +995,7 @@ checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055"
[[package]]
name = "easytier"
version = "2.4.4"
source = "git+https://github.com/EasyTier/EasyTier.git#4445916ba72a8340259d65f0c55f50af325c51d2"
version = "2.4.5"
dependencies = [
"anyhow",
"arc-swap",
@@ -1056,6 +1035,7 @@ dependencies = [
"http_req",
"humansize",
"humantime-serde",
"idna",
"kcp-sys",
"machine-uid",
"multimap",
@@ -1071,6 +1051,7 @@ dependencies = [
"petgraph 0.8.2",
"pin-project-lite",
"pnet",
"prefix-trie",
"prost",
"prost-build",
"prost-reflect",
@@ -1095,7 +1076,6 @@ dependencies = [
"stun_codec",
"sys-locale",
"tabled",
"tachyonix",
"thiserror 1.0.69",
"thunk-rs",
"time",
@@ -1454,19 +1434,6 @@ dependencies = [
"slab",
]
[[package]]
name = "generator"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
dependencies = [
"cc",
"libc",
"log",
"rustversion",
"windows 0.48.0",
]
[[package]]
name = "generator"
version = "0.8.7"
@@ -2262,7 +2229,7 @@ dependencies = [
[[package]]
name = "kcp-sys"
version = "0.1.0"
source = "git+https://github.com/EasyTier/kcp-sys?rev=0f0a0558391ba391c089806c23f369651f6c9eeb#0f0a0558391ba391c089806c23f369651f6c9eeb"
source = "git+https://github.com/EasyTier/kcp-sys?rev=71eff18c573a4a71bf99c7fabc6a8b9f211c84c1#71eff18c573a4a71bf99c7fabc6a8b9f211c84c1"
dependencies = [
"anyhow",
"auto_impl",
@@ -2398,19 +2365,6 @@ version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "loom"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
dependencies = [
"cfg-if",
"generator 0.7.5",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "loom"
version = "0.7.2"
@@ -2418,7 +2372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [
"cfg-if",
"generator 0.8.7",
"generator",
"scoped-tls",
"tracing",
"tracing-subscriber",
@@ -2517,7 +2471,7 @@ dependencies = [
"crossbeam-channel",
"crossbeam-epoch",
"crossbeam-utils",
"loom 0.7.2",
"loom",
"parking_lot",
"portable-atomic",
"rustc_version",
@@ -3141,6 +3095,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85cf4c7c25f1dd66c76b451e9041a8cfce26e4ca754934fa7aed8d5a59a01d20"
dependencies = [
"cidr",
"ipnet",
"num-traits",
]
@@ -4169,19 +4124,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "tachyonix"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86c3eafa053bbcc63bb4bfc5eb26362a33ea0bc2e589f28bce00287d1c167d45"
dependencies = [
"async-event",
"crossbeam-utils",
"diatomic-waker",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "tagptr"
version = "0.2.0"
@@ -4785,12 +4727,6 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "waker-fn"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7"
[[package]]
name = "walkdir"
version = "2.5.0"
@@ -5030,15 +4966,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows"
version = "0.52.0"

View File

@@ -8,7 +8,7 @@ crate-type=["cdylib"]
[dependencies]
ohos-hilog-binding = {version = "*", features = ["redirect"]}
easytier = { git = "https://github.com/EasyTier/EasyTier.git" }
easytier = { path = "../../easytier" }
napi-derive-ohos = "1.1"
napi-ohos = { version = "1.1", default-features = false, features = [
"serde-json",

View File

@@ -1,8 +1,8 @@
mod native_log;
use easytier::common::config::{ConfigLoader, TomlConfigLoader};
use easytier::common::config::{ConfigFileControl, ConfigLoader, TomlConfigLoader};
use easytier::common::constants::EASYTIER_VERSION;
use easytier::instance_manager::NetworkInstanceManager;
use easytier::launcher::ConfigSource;
use napi_derive_ohos::napi;
use ohos_hilog_binding::{hilog_debug, hilog_error};
use std::format;
@@ -17,6 +17,11 @@ pub struct KeyValuePair {
pub value: String,
}
#[napi]
pub fn easytier_version() -> String {
EASYTIER_VERSION.to_string()
}
#[napi]
pub fn set_tun_fd(inst_id: String, fd: i32) -> bool {
match Uuid::try_parse(&inst_id) {
@@ -71,7 +76,7 @@ pub fn run_network_instance(cfg_str: String) -> bool {
return false;
}
INSTANCE_MANAGER
.run_network_instance(cfg, ConfigSource::FFI)
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
.unwrap();
true
}

View File

@@ -57,6 +57,8 @@ once_cell = "1.19"
# EasyTier core
easytier = { path = "../../easytier" }
mimalloc = { version = "*" }
# Testing
[dev-dependencies]
mockall = "0.12"

View File

@@ -8,12 +8,11 @@ use anyhow::Context as _;
use dashmap::DashMap;
use easytier::{
common::{
config::{ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader},
config::{ConfigFileControl, ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader},
scoped_task::ScopedTask,
},
defer,
instance_manager::NetworkInstanceManager,
launcher::ConfigSource,
};
use serde::{Deserialize, Serialize};
use sqlx::any;
@@ -392,7 +391,7 @@ impl HealthChecker {
.delete_network_instance(vec![cfg.get_id()]);
});
self.instance_mgr
.run_network_instance(cfg.clone(), ConfigSource::FFI)
.run_network_instance(cfg.clone(), false, ConfigFileControl::STATIC_CONFIG)
.with_context(|| "failed to run network instance")?;
let now = Instant::now();
@@ -436,7 +435,7 @@ impl HealthChecker {
);
self.instance_mgr
.run_network_instance(cfg.clone(), ConfigSource::Web)
.run_network_instance(cfg.clone(), true, ConfigFileControl::STATIC_CONFIG)
.with_context(|| "failed to run network instance")?;
self.inst_id_map.insert(node_id, cfg.get_id());

View File

@@ -23,6 +23,11 @@ use tracing_subscriber::EnvFilter;
use crate::db::cleanup::{CleanupConfig, CleanupManager};
use mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL_MIMALLOC: MiMalloc = MiMalloc;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
@@ -31,7 +36,7 @@ struct Args {
admin_password: Option<String>,
}
#[tokio::main]
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() -> anyhow::Result<()> {
// 加载配置
let config = AppConfig::default();

View File

@@ -78,7 +78,11 @@ fn generate_network_config(toml_config: String) -> Result<NetworkConfig, String>
}
#[tauri::command]
async fn run_network_instance(app: AppHandle, cfg: NetworkConfig) -> Result<(), String> {
async fn run_network_instance(
app: AppHandle,
cfg: NetworkConfig,
save: bool,
) -> Result<(), String> {
let instance_id = cfg.instance_id().to_string();
app.emit("pre_run_network_instance", cfg.instance_id())
@@ -97,7 +101,7 @@ async fn run_network_instance(app: AppHandle, cfg: NetworkConfig) -> Result<(),
CLIENT_MANAGER
.get()
.unwrap()
.handle_run_network_instance(app.clone(), cfg)
.handle_run_network_instance(app.clone(), cfg, save)
.await
.map_err(|e| e.to_string())?;
@@ -240,12 +244,17 @@ async fn get_config(app: AppHandle, instance_id: String) -> Result<NetworkConfig
}
#[tauri::command]
fn load_configs(configs: Vec<NetworkConfig>, enabled_networks: Vec<String>) -> Result<(), String> {
async fn load_configs(
app: AppHandle,
configs: Vec<NetworkConfig>,
enabled_networks: Vec<String>,
) -> Result<(), String> {
CLIENT_MANAGER
.get()
.unwrap()
.storage
.load_configs(configs, enabled_networks)
.load_configs(app, configs, enabled_networks)
.await
.map_err(|e| e.to_string())?;
Ok(())
}
@@ -304,7 +313,8 @@ mod manager {
use super::*;
use async_trait::async_trait;
use dashmap::{DashMap, DashSet};
use easytier::launcher::{ConfigSource, NetworkConfig};
use easytier::launcher::NetworkConfig;
use easytier::proto::api::manage::RunNetworkInstanceRequest;
use easytier::proto::rpc_impl::bidirect::BidirectRpcManager;
use easytier::proto::rpc_types::controller::BaseController;
use easytier::rpc_service::remote_client::PersistentConfig;
@@ -334,8 +344,9 @@ mod manager {
}
}
pub(super) fn load_configs(
pub(super) async fn load_configs(
&self,
app: AppHandle,
configs: Vec<NetworkConfig>,
enabled_networks: Vec<String>,
) -> anyhow::Result<()> {
@@ -349,21 +360,31 @@ mod manager {
}
self.enabled_networks.clear();
INSTANCE_MANAGER
.filter_network_instance(|_, _| true)
.into_iter()
.for_each(|id| {
self.enabled_networks.insert(id);
});
INSTANCE_MANAGER.iter().for_each(|v| {
self.enabled_networks.insert(*v.key());
});
for id in enabled_networks {
if let Ok(uuid) = id.parse() {
if !self.enabled_networks.contains(&uuid) {
let config = self
.network_configs
.get(&uuid)
.map(|i| i.value().1.gen_config())
.ok_or_else(|| anyhow::anyhow!("Config not found"))??;
INSTANCE_MANAGER.run_network_instance(config, ConfigSource::GUI)?;
.map(|i| i.value().1.clone())
.ok_or_else(|| anyhow::anyhow!("Config not found"))?;
CLIENT_MANAGER
.get()
.unwrap()
.get_rpc_client(app.clone())
.ok_or_else(|| anyhow::anyhow!("RPC client not found"))?
.run_network_instance(
BaseController::default(),
RunNetworkInstanceRequest {
inst_id: None,
config: Some(config),
overwrite: false,
},
)
.await?;
self.enabled_networks.insert(uuid);
}
}
@@ -434,18 +455,14 @@ mod manager {
app: AppHandle,
network_inst_id: Uuid,
disabled: bool,
) -> Result<GUIConfig, anyhow::Error> {
) -> Result<(), anyhow::Error> {
if disabled {
self.enabled_networks.remove(&network_inst_id);
} else {
self.enabled_networks.insert(network_inst_id);
}
self.save_enabled_networks(&app)?;
let cfg = self
.network_configs
.get(&network_inst_id)
.ok_or_else(|| anyhow::anyhow!("Config not found"))?;
Ok(cfg.value().clone())
Ok(())
}
async fn list_network_configs(

View File

@@ -15,8 +15,8 @@ export async function generateNetworkConfig(tomlConfig: string) {
return invoke<NetworkConfig>('generate_network_config', { tomlConfig })
}
export async function runNetworkInstance(cfg: NetworkConfig) {
return invoke('run_network_instance', { cfg })
export async function runNetworkInstance(cfg: NetworkConfig, save: boolean) {
return invoke('run_network_instance', { cfg, save })
}
export async function collectNetworkInfo(instanceId: string) {

View File

@@ -160,7 +160,7 @@ export async function onNetworkInstanceChange(instanceId: string) {
}
catch (e) {
console.error('start vpn service failed, stop all other network insts.', e)
await runNetworkInstance(config);
await runNetworkInstance(config, true); //on android config should always be saved
}
}
}

View File

@@ -5,8 +5,8 @@ export class GUIRemoteClient implements Api.RemoteClient {
async validate_config(config: NetworkTypes.NetworkConfig): Promise<Api.ValidateConfigResponse> {
return backend.validateConfig(config);
}
async run_network(config: NetworkTypes.NetworkConfig): Promise<undefined> {
await backend.runNetworkInstance(config);
async run_network(config: NetworkTypes.NetworkConfig, save: boolean): Promise<undefined> {
await backend.runNetworkInstance(config, save);
}
async get_network_info(inst_id: string): Promise<NetworkTypes.NetworkInstanceRunningInfo | undefined> {
return backend.collectNetworkInfo(inst_id).then(infos => infos.info.map[inst_id]);

View File

@@ -64,6 +64,8 @@ uuid = { version = "1.5.0", features = [
chrono = { version = "0.4.37", features = ["serde"] }
mimalloc = { version = "*" }
[features]
default = []
embed = ["dep:axum-embed"]

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import InputGroup from 'primevue/inputgroup'
import InputGroupAddon from 'primevue/inputgroupaddon'
import { SelectButton, Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button, Password } from 'primevue'
import { SelectButton, Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button, Password, Dialog } from 'primevue'
import {
addRow,
DEFAULT_NETWORK_CONFIG,
@@ -9,7 +9,7 @@ import {
NetworkingMethod,
removeRow
} from '../types/network'
import { defineProps, defineEmits, ref, } from 'vue'
import { defineProps, defineEmits, ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
const props = defineProps<{
@@ -157,6 +157,7 @@ const bool_flags: BoolFlag[] = [
{ field: 'enable_quic_proxy', help: 'enable_quic_proxy_help' },
{ field: 'disable_quic_input', help: 'disable_quic_input_help' },
{ field: 'disable_p2p', help: 'disable_p2p_help' },
{ field: 'p2p_only', help: 'p2p_only_help' },
{ field: 'bind_device', help: 'bind_device_help' },
{ field: 'no_tun', help: 'no_tun_help' },
{ field: 'enable_exit_node', help: 'enable_exit_node_help' },
@@ -172,6 +173,49 @@ const bool_flags: BoolFlag[] = [
const portForwardProtocolOptions = ref(["tcp", "udp"]);
const editingPortForward = ref(false);
const editingPortForwardIndex = ref(-1);
const editingPortForwardData = ref();
function openPortForwardEditor(index: number) {
editingPortForwardIndex.value = index;
// deep copy
editingPortForwardData.value = JSON.parse(JSON.stringify(curNetwork.value.port_forwards[index]));
editingPortForward.value = true;
}
function addPortForward() {
addRow(curNetwork.value.port_forwards)
if (isCompact.value) {
openPortForwardEditor(curNetwork.value.port_forwards.length - 1)
}
}
function savePortForward() {
curNetwork.value.port_forwards[editingPortForwardIndex.value] = editingPortForwardData.value;
editingPortForward.value = false;
}
const portForwardContainer = ref<HTMLElement | null>(null);
const isCompact = ref(false);
onMounted(() => {
if (portForwardContainer.value) {
let resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
isCompact.value = entry.contentRect.width < 540;
}
});
resizeObserver.observe(portForwardContainer.value);
return () => {
if (resizeObserver && portForwardContainer.value) {
resizeObserver.unobserve(portForwardContainer.value);
}
}
}
});
</script>
<template>
@@ -410,14 +454,15 @@ const portForwardProtocolOptions = ref(["tcp", "udp"]);
<Divider />
<Panel :header="t('port_forwards')" toggleable collapsed>
<div class="flex flex-col gap-y-2">
<div ref="portForwardContainer" class="flex flex-col gap-y-2">
<div class="flex flex-row gap-x-9 flex-wrap w-full">
<div class="flex flex-col gap-2 grow p-fluid">
<div class="flex">
<label for="port_forwards">{{ t('port_forwards_help') }}</label>
</div>
<div v-for="(row, index) in curNetwork.port_forwards" class="form-row">
<div style="display: flex; gap: 0.5rem; align-items: flex-end;">
<div v-for="(row, index) in curNetwork.port_forwards" :key="index" class="form-row">
<!-- Wide screen view -->
<div v-if="!isCompact" class="flex gap-2 items-end">
<SelectButton v-model="row.proto" :options="portForwardProtocolOptions" :allow-empty="false" />
<div style="flex-grow: 4;">
<InputGroup>
@@ -444,11 +489,52 @@ const portForwardProtocolOptions = ref(["tcp", "udp"]);
rounded @click="removeRow(index, curNetwork.port_forwards)" />
</div>
</div>
<!-- Small screen view -->
<div v-else class="flex justify-between items-center p-2 border-b">
<span>{{ row.proto }}://{{ row.bind_ip }}:{{ row.bind_port }}/{{ row.dst_ip }}:{{
row.dst_port }}</span>
<div class="flex gap-2">
<Button icon="pi pi-pencil" class="p-button-sm" @click="openPortForwardEditor(index)" />
<Button icon="pi pi-trash" class="p-button-sm p-button-danger"
@click="removeRow(index, curNetwork.port_forwards)" />
</div>
</div>
</div>
<div class="flex justify-content-end mt-4">
<Button icon="pi pi-plus" :label="t('port_forwards_add_btn')" severity="success"
@click="addRow(curNetwork.port_forwards)" />
@click="addPortForward" />
</div>
<Dialog v-model:visible="editingPortForward" modal :header="t('edit_port_forward')"
:style="{ width: '90vw', maxWidth: '600px' }">
<div v-if="editingPortForwardData" class="flex flex-col gap-4">
<SelectButton v-model="editingPortForwardData.proto" :options="portForwardProtocolOptions"
:allow-empty="false" />
<InputGroup>
<InputText v-model="editingPortForwardData.bind_ip"
:placeholder="t('port_forwards_bind_addr')" />
<InputGroupAddon>
<span style="font-weight: bold">:</span>
</InputGroupAddon>
<InputNumber v-model="editingPortForwardData.bind_port" :format="false" :step="1" mode="decimal"
:min="1" :max="65535" class="max-w-20" />
</InputGroup>
<InputGroup>
<InputText v-model="editingPortForwardData.dst_ip" :placeholder="t('port_forwards_dst_addr')" />
<InputGroupAddon>
<span style="font-weight: bold">:</span>
</InputGroupAddon>
<InputNumber v-model="editingPortForwardData.dst_port" :format="false" :step="1" mode="decimal"
:min="1" :max="65535" class="max-w-20" />
</InputGroup>
</div>
<template #footer>
<Button :label="t('web.common.cancel')" icon="pi pi-times" @click="editingPortForward = false"
text />
<Button :label="t('web.common.save')" icon="pi pi-save" @click="savePortForward" />
</template>
</Dialog>
</div>
</div>
</div>

View File

@@ -56,6 +56,23 @@ const onLazyLoadNetworkMetas = async (event: VirtualScrollerLazyEvent) => {
.map(item => item.uuid);
await loadNetworkMetas(instanceIds);
};
const currentNetworkMeta = computed(() => {
if (!instanceId.value) {
return undefined;
}
return networkMetaCache.value[instanceId.value];
});
const currentNetworkControl = {
remoteSave: computed(() => {
return Api.ConfigFilePermission.isRemoveSaveable(currentNetworkMeta.value?.config_permission ?? 0);
}),
editable: computed(() => {
return Api.ConfigFilePermission.isEditable(currentNetworkMeta.value?.config_permission ?? 0);
}),
deletable: computed(() => {
return Api.ConfigFilePermission.isDeletable(currentNetworkMeta.value?.config_permission ?? 0);
})
}
const instanceList = ref<Array<{ uuid: string; meta?: Api.NetworkMeta }>>([]);
const updateInstanceList = () => {
@@ -150,17 +167,12 @@ const loadCurrentNetworkConfig = async () => {
currentNetworkConfig.value = ret;
}
const updateNetworkState = async (disabled: boolean) => {
const stopNetwork = async () => {
if (!selectedInstanceId.value) {
return;
}
if (disabled || !currentNetworkConfig.value) {
await props.api.update_network_instance_state(selectedInstanceId.value.uuid, disabled);
} else if (currentNetworkConfig.value) {
await props.api.delete_network(currentNetworkConfig.value.instance_id);
await props.api.run_network(currentNetworkConfig.value);
}
await props.api.update_network_instance_state(selectedInstanceId.value.uuid, true);
await loadNetworkInstanceIds();
}
@@ -199,7 +211,7 @@ const saveAndRunNewNetwork = async () => {
}
try {
await props.api.delete_network(instanceId.value!);
let ret = await props.api.run_network(currentNetworkConfig.value);
let ret = await props.api.run_network(currentNetworkConfig.value, currentNetworkControl.remoteSave.value);
console.debug("saveAndRunNewNetwork", ret);
delete networkMetaCache.value[currentNetworkConfig.value.instance_id];
@@ -377,7 +389,7 @@ const actionMenu: Ref<MenuItem[]> = ref([
{
label: t('web.device_management.edit_network'),
icon: 'pi pi-pencil',
visible: () => !(networkIsDisabled.value ?? true),
visible: () => !(networkIsDisabled.value ?? true) && currentNetworkControl.editable.value,
command: () => editNetwork()
},
{
@@ -389,6 +401,7 @@ const actionMenu: Ref<MenuItem[]> = ref([
label: t('web.device_management.delete_network'),
icon: 'pi pi-trash',
class: 'p-error',
visible: () => currentNetworkControl.deletable.value,
command: () => confirmDeleteNetwork(new Event('click'))
}
]);
@@ -443,7 +456,7 @@ onUnmounted(() => {
<span class="truncate block">
&nbsp;
<span v-if="slotProps.value.meta">
{{ slotProps.value.meta.instance_name }} ({{ slotProps.value.uuid }})
{{ slotProps.value.meta.network_name }} ({{ slotProps.value.uuid }})
</span>
<span v-else>
{{ slotProps.value.uuid }}
@@ -463,7 +476,7 @@ onUnmounted(() => {
<div class="flex items-center min-w-0">
<div class="mr-4 min-w-0 flex-1">
<span class="truncate block">{{ t('network_name') }}: {{
slotProps.option.meta.instance_name }}</span>
slotProps.option.meta.network_name }}</span>
</div>
<Tag class="my-auto leading-3 shrink-0"
:severity="isRunning(slotProps.option.uuid) ? 'success' : 'info'"
@@ -544,8 +557,9 @@ onUnmounted(() => {
<Message v-else severity="error" class="mb-4">{{ curNetworkInfo?.error_msg }}</Message>
<div class="text-center mt-4">
<Button @click="updateNetworkState(true)" :label="t('web.device_management.disable_network')"
severity="warning" icon="pi pi-power-off" iconPos="left" />
<Button @click="stopNetwork" :disabled="!currentNetworkControl.deletable.value"
:label="t('web.device_management.disable_network')" severity="danger" icon="pi pi-power-off"
iconPos="left" />
</div>
</div>

View File

@@ -106,6 +106,9 @@ disable_quic_input_help: 禁用 QUIC 入站流量,其他开启 QUIC 代理的
disable_p2p: 禁用 P2P
disable_p2p_help: 禁用 P2P 模式,所有流量通过手动指定的服务器中转。
p2p_only: 仅 P2P
p2p_only_help: 仅与已经建立P2P连接的对等节点通信不通过其他节点中转。
bind_device: 仅使用物理网卡
bind_device_help: 仅使用物理网卡,避免 EasyTier 通过其他虚拟网建立连接。
@@ -164,6 +167,7 @@ port_forwards_help: "将本地端口转发到虚拟网络中的远程端口。
port_forwards_bind_addr: "绑定地址0.0.0.0"
port_forwards_dst_addr: "目标地址10.126.126.1"
port_forwards_add_btn: "添加"
edit_port_forward: "编辑端口转发"
mtu: MTU
mtu_help: |
@@ -335,4 +339,4 @@ web:
confirm_password: 确认新密码
language: 语言
theme: 主题
logout: 退出登录
logout: 退出登录

View File

@@ -105,6 +105,9 @@ disable_quic_input_help: Disable inbound QUIC traffic, while nodes with QUIC pro
disable_p2p: Disable P2P
disable_p2p_help: Disable P2P mode; route all traffic through a manually specified relay server.
p2p_only: P2P Only
p2p_only_help: Only communicate with peers that have already established P2P connections, do not relay through other nodes.
bind_device: Bind to Physical Device Only
bind_device_help: Use only the physical network interface to prevent EasyTier from connecting via virtual networks.
@@ -164,6 +167,7 @@ port_forwards_help: "forward local port to remote port in virtual network. e.g.:
port_forwards_bind_addr: "Bind address, e.g.: 0.0.0.0"
port_forwards_dst_addr: "Destination address, e.g.: 10.126.126.1"
port_forwards_add_btn: "Add"
edit_port_forward: "Edit Port Forward"
mtu: MTU
mtu_help: |
@@ -335,4 +339,4 @@ web:
confirm_password: Confirm New Password
language: Language
theme: Theme
logout: Logout
logout: Logout

View File

@@ -26,8 +26,27 @@ export interface CollectNetworkInfoResponse {
}
}
export namespace ConfigFilePermission {
export type Flags = number;
export const READ_ONLY: Flags = 1 << 0;
export const NO_DELETE: Flags = 1 << 1;
export function hasPermission(perm: Flags, flag: Flags): boolean {
return (perm & flag) === flag;
}
export function isRemoveSaveable(perm: Flags): boolean {
return !hasPermission(perm, NO_DELETE);
}
export function isEditable(perm: Flags): boolean {
return !hasPermission(perm, READ_ONLY);
}
export function isDeletable(perm: Flags): boolean {
return !hasPermission(perm, NO_DELETE);
}
}
export interface NetworkMeta {
instance_name: string;
network_name: string;
config_permission: ConfigFilePermission.Flags;
}
export interface GetNetworkMetasResponse {
@@ -36,7 +55,7 @@ export interface GetNetworkMetasResponse {
export interface RemoteClient {
validate_config(config: NetworkConfig): Promise<ValidateConfigResponse>;
run_network(config: NetworkConfig): Promise<undefined>;
run_network(config: NetworkConfig, save: boolean): Promise<undefined>;
get_network_info(inst_id: string): Promise<NetworkInstanceRunningInfo | undefined>;
list_network_instance_ids(): Promise<ListNetworkInstanceIdResponse>;
delete_network(inst_id: string): Promise<undefined>;

View File

@@ -42,6 +42,7 @@ export interface NetworkConfig {
enable_quic_proxy?: boolean
disable_quic_input?: boolean
disable_p2p?: boolean
p2p_only?: boolean
bind_device?: boolean
no_tun?: boolean
enable_exit_node?: boolean
@@ -111,6 +112,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
enable_quic_proxy: false,
disable_quic_input: false,
disable_p2p: false,
p2p_only: false,
bind_device: true,
no_tun: false,
enable_exit_node: false,

View File

@@ -193,9 +193,10 @@ class WebRemoteClient implements Api.RemoteClient {
});
return response;
}
async run_network(config: NetworkTypes.NetworkConfig): Promise<undefined> {
async run_network(config: NetworkTypes.NetworkConfig, save: boolean): Promise<undefined> {
await this.client.post<string>(`/machines/${this.machine_id}/networks`, {
config: config,
save: save
});
}
async get_network_info(inst_id: string): Promise<NetworkTypes.NetworkInstanceRunningInfo | undefined> {

View File

@@ -280,6 +280,7 @@ impl Session {
config: Some(
serde_json::from_str::<NetworkConfig>(&c.network_config).unwrap(),
),
overwrite: false,
},
)
.await;

View File

@@ -155,7 +155,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
(user_id, _): (UserIdInDb, Uuid),
network_inst_id: Uuid,
disabled: bool,
) -> Result<user_running_network_configs::Model, DbErr> {
) -> Result<(), DbErr> {
use entity::user_running_network_configs as urnc;
urnc::Entity::update_many()
@@ -169,15 +169,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
.exec(self.orm_db())
.await?;
urnc::Entity::find()
.filter(urnc::Column::UserId.eq(user_id))
.filter(urnc::Column::NetworkInstanceId.eq(network_inst_id.to_string()))
.one(self.orm_db())
.await?
.ok_or(DbErr::RecordNotFound(format!(
"Network config not found for user {} and network instance {}",
user_id, network_inst_id
)))
Ok(())
}
async fn list_network_configs(

View File

@@ -19,6 +19,8 @@ use easytier::{
utils::{init_logger, setup_panic_handler},
};
use mimalloc::MiMalloc;
mod client_manager;
mod db;
mod migrator;
@@ -27,6 +29,9 @@ mod restful;
#[cfg(feature = "embed")]
mod web;
#[global_allocator]
static GLOBAL_MIMALLOC: MiMalloc = MiMalloc;
rust_i18n::i18n!("locales", fallback = "en");
#[derive(Parser, Debug)]
@@ -161,7 +166,7 @@ async fn get_dual_stack_listener(
Ok((v6_listener, v4_listener))
}
#[tokio::main]
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() {
let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
rust_i18n::set_locale(&locale);

View File

@@ -59,6 +59,7 @@ struct SaveNetworkJsonReq {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct RunNetworkJsonReq {
config: NetworkConfig,
save: bool,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
@@ -132,6 +133,7 @@ impl NetworkApi {
.handle_run_network_instance(
(Self::get_user_id(&auth_session)?, machine_id),
payload.config,
payload.save,
)
.await
.map_err(convert_error)?;

View File

@@ -143,7 +143,6 @@ network-interface = "2.0"
# for ospf route
petgraph = "0.8.1"
hashbrown = "0.15.3"
ordered_hash_map = "0.5.0"
# for wireguard
boringtun = { package = "boringtun-easytier", version = "0.6.1", optional = true }
@@ -297,7 +296,7 @@ thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = f
[dev-dependencies]
serial_test = "3.0.0"
rstest = "0.25.0"
futures-util = "0.3.30"
futures-util = "0.3.31"
maplit = "1.0.2"
tempfile = "3.22.0"

View File

@@ -18,6 +18,9 @@ core_clap:
config_file:
en: "path to the config file, NOTE: the options set by cmdline args will override options in config file"
zh-CN: "配置文件路径,注意:命令行中的配置的选项会覆盖配置文件中的选项"
config_dir:
en: "Load all .toml files in the directory to start network instances, and store the received configurations in this directory."
zh-CN: "加载目录中的所有 .toml 文件以启动网络实例,并将下发的配置保存在此目录中。"
generate_completions:
en: "generate shell completions"
zh-CN: "生成 shell 补全脚本"
@@ -148,6 +151,9 @@ core_clap:
disable_p2p:
en: "disable p2p communication, will only relay packets with peers specified by --peers"
zh-CN: "禁用P2P通信只通过--peers指定的节点转发数据包"
p2p_only:
en: "only communicate with peers that already establish p2p connection"
zh-CN: "仅与已经建立P2P连接的对等节点通信"
disable_udp_hole_punching:
en: "disable udp hole punching"
zh-CN: "禁用UDP打洞功能"

View File

@@ -7,6 +7,7 @@ use std::{
use anyhow::Context;
use serde::{Deserialize, Serialize};
use tokio::io::AsyncReadExt as _;
use crate::{
common::stun::StunInfoCollector,
@@ -34,6 +35,7 @@ pub fn gen_default_flags() -> Flags {
use_smoltcp: false,
relay_network_whitelist: "*".to_string(),
disable_p2p: false,
p2p_only: false,
relay_all_peer_rpc: false,
disable_udp_hole_punching: false,
multi_thread: true,
@@ -829,6 +831,157 @@ impl ConfigLoader for TomlConfigLoader {
}
}
#[derive(Clone, Copy, Default)]
pub struct ConfigFilePermission(u8);
impl ConfigFilePermission {
pub const READ_ONLY: u8 = 1 << 0;
pub const NO_DELETE: u8 = 1 << 1;
pub fn with_flag(self, flag: u8) -> Self {
Self(self.0 | flag)
}
pub fn remove_flag(self, flag: u8) -> Self {
Self(self.0 & !flag)
}
pub fn has_flag(&self, flag: u8) -> bool {
(self.0 & flag) != 0
}
}
impl From<u8> for ConfigFilePermission {
fn from(value: u8) -> Self {
ConfigFilePermission(value)
}
}
impl From<u32> for ConfigFilePermission {
fn from(value: u32) -> Self {
ConfigFilePermission(value as u8)
}
}
impl From<ConfigFilePermission> for u8 {
fn from(value: ConfigFilePermission) -> Self {
value.0
}
}
impl From<ConfigFilePermission> for u32 {
fn from(value: ConfigFilePermission) -> Self {
value.0 as u32
}
}
impl std::fmt::Debug for ConfigFilePermission {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut flags = vec![];
if self.has_flag(ConfigFilePermission::READ_ONLY) {
flags.push("READ_ONLY");
} else {
flags.push("EDITABLE");
}
if self.has_flag(ConfigFilePermission::NO_DELETE) {
flags.push("NO_DELETE");
} else {
flags.push("DELETABLE");
}
write!(f, "{}", flags.join("|"))
}
}
#[derive(Debug, Clone)]
pub struct ConfigFileControl {
pub path: Option<PathBuf>,
pub permission: ConfigFilePermission,
}
impl ConfigFileControl {
pub const STATIC_CONFIG: ConfigFileControl = Self {
path: None,
permission: ConfigFilePermission(
ConfigFilePermission::READ_ONLY | ConfigFilePermission::NO_DELETE,
),
};
pub fn new(path: Option<PathBuf>, permission: ConfigFilePermission) -> Self {
ConfigFileControl { path, permission }
}
pub async fn from_path(path: PathBuf) -> Self {
let read_only = if let Ok(metadata) = tokio::fs::metadata(&path).await {
metadata.permissions().readonly()
} else {
true
};
Self::new(
Some(path),
if read_only {
ConfigFilePermission(ConfigFilePermission::READ_ONLY)
} else {
ConfigFilePermission(0)
},
)
}
pub fn is_read_only(&self) -> bool {
self.permission.has_flag(ConfigFilePermission::READ_ONLY)
}
pub fn set_read_only(&mut self, read_only: bool) {
if read_only {
self.permission = self.permission.with_flag(ConfigFilePermission::READ_ONLY);
} else {
self.permission = self.permission.remove_flag(ConfigFilePermission::READ_ONLY);
}
}
pub fn is_no_delete(&self) -> bool {
self.permission.has_flag(ConfigFilePermission::NO_DELETE)
}
pub fn set_no_delete(&mut self, no_delete: bool) {
if no_delete {
self.permission = self.permission.with_flag(ConfigFilePermission::NO_DELETE);
} else {
self.permission = self.permission.remove_flag(ConfigFilePermission::NO_DELETE);
}
}
pub fn is_deletable(&self) -> bool {
!self.is_no_delete()
}
}
pub async fn load_config_from_file(
config_file: &PathBuf,
config_dir: Option<&PathBuf>,
) -> Result<(TomlConfigLoader, ConfigFileControl), anyhow::Error> {
if config_file.as_os_str() == "-" {
let mut stdin = String::new();
_ = tokio::io::stdin()
.read_to_string(&mut stdin)
.await
.context("failed to read config from stdin")?;
let config = TomlConfigLoader::new_from_str(&stdin)?;
return Ok((config, ConfigFileControl::STATIC_CONFIG));
}
let config = TomlConfigLoader::new(config_file)
.with_context(|| format!("failed to load config file: {:?}", config_file))?;
let mut control = ConfigFileControl::from_path(config_file.clone()).await;
if control.is_read_only() {
control.set_no_delete(true);
} else if let Some(config_dir) = config_dir {
if let Some(config_file_dir) = config_file.parent() {
// if the config file is in the config dir and named as the instance id, it can be saved remotely
if config_file_dir == config_dir
&& config_file.file_stem() == Some(config.get_id().to_string().as_ref())
&& config_file.extension() == Some(std::ffi::OsStr::new("toml"))
{
control.set_no_delete(false);
} else {
control.set_no_delete(true);
}
}
} else {
control.set_no_delete(true);
}
Ok((config, control))
}
#[cfg(test)]
pub mod tests {
use super::*;

View File

@@ -68,7 +68,7 @@ pub async fn socket_addrs(
url: &url::Url,
default_port_number: impl Fn() -> Option<u16>,
) -> Result<Vec<SocketAddr>, Error> {
let host = url.host_str().ok_or(Error::InvalidUrl(url.to_string()))?;
let host = url.host().ok_or(Error::InvalidUrl(url.to_string()))?;
let port = url
.port()
.or_else(default_port_number)
@@ -84,9 +84,12 @@ pub async fn socket_addrs(
};
// if host is an ip address, return it directly
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
return Ok(vec![SocketAddr::new(ip, port)]);
match host {
url::Host::Ipv4(ip) => return Ok(vec![SocketAddr::new(std::net::IpAddr::V4(ip), port)]),
url::Host::Ipv6(ip) => return Ok(vec![SocketAddr::new(std::net::IpAddr::V6(ip), port)]),
_ => {}
}
let host = host.to_string();
if ALLOW_USE_SYSTEM_DNS_RESOLVER.load(std::sync::atomic::Ordering::Relaxed) {
let socket_addr = format!("{}:{}", host, port);
@@ -103,7 +106,7 @@ pub async fn socket_addrs(
}
// use hickory_resolver
let ret = RESOLVER.lookup_ip(host).await.with_context(|| {
let ret = RESOLVER.lookup_ip(&host).await.with_context(|| {
format!(
"hickory dns lookup_ip failed, host: {}, port: {}",
host, port

View File

@@ -84,6 +84,7 @@ pub struct GlobalCtx {
enable_exit_node: bool,
proxy_forward_by_system: bool,
no_tun: bool,
p2p_only: bool,
feature_flags: AtomicCell<PeerFeatureFlag>,
@@ -138,6 +139,7 @@ impl GlobalCtx {
let enable_exit_node = config_fs.get_flags().enable_exit_node || cfg!(target_env = "ohos");
let proxy_forward_by_system = config_fs.get_flags().proxy_forward_by_system;
let no_tun = config_fs.get_flags().no_tun;
let p2p_only = config_fs.get_flags().p2p_only;
let feature_flags = PeerFeatureFlag {
kcp_input: !config_fs.get_flags().disable_kcp_input,
@@ -172,6 +174,7 @@ impl GlobalCtx {
enable_exit_node,
proxy_forward_by_system,
no_tun,
p2p_only,
feature_flags: AtomicCell::new(feature_flags),
quic_proxy_port: AtomicCell::new(None),
@@ -421,6 +424,15 @@ impl GlobalCtx {
.and_then(|acl_v1| acl_v1.group)
.map_or_else(Vec::new, |group| group.declares.to_vec())
}
pub fn p2p_only(&self) -> bool {
self.p2p_only
}
pub fn latency_first(&self) -> bool {
// NOTICE: p2p only is conflict with latency first
self.config.get_flags().latency_first && !self.p2p_only
}
}
#[cfg(test)]

View File

@@ -1,210 +1,70 @@
use idna::domain_to_ascii;
pub fn convert_idn_to_ascii(url_str: &str) -> Result<String, String> {
if !url_str.is_ascii() {
let mut url_parts = url_str.splitn(2, "://");
let scheme = url_parts.next().unwrap_or("");
let rest = url_parts.next().unwrap_or(url_str);
let (host_part, port_part, path_part) = {
let mut path_and_rest = rest.splitn(2, '/');
let host_port_part = path_and_rest.next().unwrap_or("");
let path_part = path_and_rest
.next()
.map(|s| format!("/{}", s))
.unwrap_or_default();
if host_port_part.starts_with('[') {
if let Some(end_bracket_pos) = host_port_part.find(']') {
let host_part = &host_port_part[..end_bracket_pos + 1];
let remaining = &host_port_part[end_bracket_pos + 1..];
if remaining.starts_with(':') {
if let Some(port_str) = remaining.strip_prefix(':') {
if port_str.chars().all(|c| c.is_ascii_digit()) {
(host_part, format!(":{}", port_str), path_part)
} else {
(host_part, String::new(), path_part)
}
} else {
(host_part, String::new(), path_part)
}
} else {
(host_part, String::new(), path_part)
}
} else {
(host_port_part, String::new(), path_part)
}
} else {
let (host_part, port_part) = if let Some(pos) = host_port_part.rfind(':') {
let port_str = &host_port_part[pos + 1..];
if port_str.chars().all(|c| c.is_ascii_digit()) {
(&host_port_part[..pos], format!(":{}", port_str))
} else {
(host_port_part, String::new())
}
} else {
(host_port_part, String::new())
};
(host_part, port_part, path_part)
}
};
use percent_encoding::percent_decode_str;
if !host_part.is_ascii() {
let ascii_host = domain_to_ascii(host_part)
.map_err(|e| format!("Failed to convert IDN to ASCII: {}", e))?;
let result = format!("{}://{}{}{}", scheme, ascii_host, port_part, path_part);
Ok(result)
} else {
Ok(url_str.to_string())
}
} else {
Ok(url_str.to_string())
pub fn convert_idn_to_ascii(mut url: url::Url) -> anyhow::Result<url::Url> {
if url.is_special() {
return Ok(url);
}
}
pub fn safe_convert_idn_to_ascii(url_str: &str) -> String {
convert_idn_to_ascii(url_str).unwrap_or_else(|_| url_str.to_string())
if let Some(domain) = url.domain() {
let domain = percent_decode_str(domain).decode_utf8()?;
let domain = domain_to_ascii(&domain)?;
url.set_host(Some(&domain))?;
}
Ok(url)
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[test]
fn test_ascii_only_urls() {
assert_eq!(
convert_idn_to_ascii("https://example.com").unwrap(),
"https://example.com"
);
assert_eq!(
convert_idn_to_ascii("http://test.org:8080/path").unwrap(),
"http://test.org:8080/path"
);
}
#[rstest]
// test_ascii_only_urls
#[case("example.com", "example.com")]
#[case("test.org:8080/path", "test.org:8080/path")]
// test_unicode_domains
#[case("räksmörgås.nu", "xn--rksmrgs-5wao1o.nu")]
#[case("中文.测试", "xn--fiq228c.xn--0zwm56d")]
// test_unicode_domains_with_port
#[case("räksmörgås.nu:8080", "xn--rksmrgs-5wao1o.nu:8080")]
// test_unicode_domains_with_port_and_path
#[case("例子.测试/path", "xn--fsqu00a.xn--0zwm56d/path")]
#[case("中文.测试:9000/api", "xn--fiq228c.xn--0zwm56d:9000/api")]
#[case("räksmörgås.nu:8080/path", "xn--rksmrgs-5wao1o.nu:8080/path")]
// test_unicode_domains_with_port_and_unicode_path
#[case(
"中文.测试:8000/用户/管理",
"xn--fiq228c.xn--0zwm56d:8000/%E7%94%A8%E6%88%B7/%E7%AE%A1%E7%90%86"
)]
// test_ipv6_literals & test_ipv6_with_unicode_path
#[case("[2001:db8::1]:8080", "[2001:db8::1]:8080")]
#[case("[2001:db8::1]/path", "[2001:db8::1]/path")]
#[case(
"[2001:db8::1]/路径/资源",
"[2001:db8::1]/%E8%B7%AF%E5%BE%84/%E8%B5%84%E6%BA%90"
)]
fn test_convert_idn_to_ascii_cases(
#[case] host_part: &str,
#[case] expected_host_part: &str,
#[values("tcp", "udp", "ws", "wss", "wg", "quic", "http", "https")] protocol: &str,
#[values(false, true)] dual_convert: bool,
) {
let input = url::Url::parse(&format!("{}://{}", protocol, host_part)).unwrap();
let input = if dual_convert {
// in case url is serialized/deserialized as string somewhere else
input.to_string().parse().unwrap()
} else {
input
};
let actual = convert_idn_to_ascii(input.clone()).unwrap().to_string();
#[test]
fn test_unicode_domains() {
assert_eq!(
convert_idn_to_ascii("https://räksmörgås.nu").unwrap(),
"https://xn--rksmrgs-5wao1o.nu"
);
assert_eq!(
convert_idn_to_ascii("https://例子.测试").unwrap(),
"https://xn--fsqu00a.xn--0zwm56d"
);
}
let mut expected = format!("{}://{}", protocol, expected_host_part);
#[test]
fn test_chinese_domains() {
assert_eq!(
convert_idn_to_ascii("https://中文.测试").unwrap(),
"https://xn--fiq228c.xn--0zwm56d"
);
assert_eq!(
convert_idn_to_ascii("https://公司.中国").unwrap(),
"https://xn--55qx5d.xn--fiqs8s"
);
assert_eq!(
convert_idn_to_ascii("https://网络.测试").unwrap(),
"https://xn--io0a7i.xn--0zwm56d"
);
}
// ws and wss protocols may automatically add a trailing slash if there's no path after host/port
if input.is_special() && actual.ends_with("/") && !expected_host_part.ends_with("/") {
expected.push('/');
}
#[test]
fn test_unicode_domains_with_port() {
assert_eq!(
convert_idn_to_ascii("https://räksmörgås.nu:8080").unwrap(),
"https://xn--rksmrgs-5wao1o.nu:8080"
);
assert_eq!(
convert_idn_to_ascii("http://例子.测试:3000/path").unwrap(),
"http://xn--fsqu00a.xn--0zwm56d:3000/path"
);
assert_eq!(
convert_idn_to_ascii("https://中文.测试:9000/api").unwrap(),
"https://xn--fiq228c.xn--0zwm56d:9000/api"
);
}
#[test]
fn test_unicode_domains_with_path() {
assert_eq!(
convert_idn_to_ascii("https://räksmörgås.nu/path/to/resource").unwrap(),
"https://xn--rksmrgs-5wao1o.nu/path/to/resource"
);
assert_eq!(
convert_idn_to_ascii("http://例子.测试/api/v1").unwrap(),
"http://xn--fsqu00a.xn--0zwm56d/api/v1"
);
assert_eq!(
convert_idn_to_ascii("https://中文.测试/api/users").unwrap(),
"https://xn--fiq228c.xn--0zwm56d/api/users"
);
}
#[test]
fn test_unicode_domains_with_port_and_path() {
assert_eq!(
convert_idn_to_ascii("https://räksmörgås.nu:8080/path/to/resource").unwrap(),
"https://xn--rksmrgs-5wao1o.nu:8080/path/to/resource"
);
assert_eq!(
convert_idn_to_ascii("http://例子.测试:9000/api/v1/users").unwrap(),
"http://xn--fsqu00a.xn--0zwm56d:9000/api/v1/users"
);
assert_eq!(
convert_idn_to_ascii("https://中文.测试:8000/用户/管理").unwrap(),
"https://xn--fiq228c.xn--0zwm56d:8000/用户/管理"
);
}
#[test]
fn test_ipv6_literals() {
assert_eq!(
convert_idn_to_ascii("https://[2001:db8::1]:8080").unwrap(),
"https://[2001:db8::1]:8080"
);
assert_eq!(
convert_idn_to_ascii("https://[2001:db8::1]/path").unwrap(),
"https://[2001:db8::1]/path"
);
assert_eq!(
convert_idn_to_ascii("https://[2001:db8::1]/路径/资源").unwrap(),
"https://[2001:db8::1]/路径/资源"
);
}
#[test]
fn test_invalid_port_format() {
let result = convert_idn_to_ascii("https://räksmörgås.nu:notaport").unwrap();
assert!(result.contains("xn--") && result.contains(":notaport"));
}
#[test]
fn test_safe_conversion() {
assert_eq!(
safe_convert_idn_to_ascii("https://example.com"),
"https://example.com"
);
assert_eq!(
safe_convert_idn_to_ascii("https://中文.测试"),
"https://xn--fiq228c.xn--0zwm56d"
);
}
#[test]
fn test_edge_cases() {
// Without scheme '://', entire string is treated as host part
let result = convert_idn_to_ascii("räksmörgås.nu").unwrap();
assert_eq!(result, "räksmörgås.nu://xn--rksmrgs-5wao1o.nu");
assert_eq!(
convert_idn_to_ascii("https://test.例子.com").unwrap(),
"https://test.xn--fsqu00a.com"
);
}
#[test]
fn test_ipv6_with_unicode_path() {
assert_eq!(
convert_idn_to_ascii("https://[2001:db8::1]/路径/资源").unwrap(),
"https://[2001:db8::1]/路径/资源"
);
assert_eq!(actual, expected);
}
}

View File

@@ -67,13 +67,10 @@ impl IfConfiguerTrait for MacIfConfiger {
}
async fn remove_ip(&self, name: &str, ip: Option<Ipv4Inet>) -> Result<(), Error> {
if ip.is_none() {
run_shell_cmd(format!("ifconfig {} inet delete", name).as_str()).await
if let Some(ip) = ip {
run_shell_cmd(format!("ifconfig {} inet {} delete", name, ip.address()).as_str()).await
} else {
run_shell_cmd(
format!("ifconfig {} inet {} delete", name, ip.unwrap().address()).as_str(),
)
.await
run_shell_cmd(format!("ifconfig {} inet delete", name).as_str()).await
}
}

View File

@@ -130,11 +130,8 @@ impl DNSTunnelConnector {
for record in response.iter() {
let parsed_record = Self::handle_one_srv_record(record, protocol);
tracing::info!(?parsed_record, ?srv_domain, "parsed_record");
if parsed_record.is_err() {
eprintln!(
"got invalid srv record {:?}",
parsed_record.as_ref().unwrap_err()
);
if let Err(e) = &parsed_record {
eprintln!("got invalid srv record {:?}", e);
continue;
}
responses.insert(parsed_record.unwrap());

View File

@@ -10,7 +10,7 @@ use crate::tunnel::quic::QUICTunnelConnector;
#[cfg(feature = "wireguard")]
use crate::tunnel::wireguard::{WgConfig, WgTunnelConnector};
use crate::{
common::{error::Error, global_ctx::ArcGlobalCtx, network::IPCollector},
common::{error::Error, global_ctx::ArcGlobalCtx, idn, network::IPCollector},
tunnel::{
check_scheme_and_get_socket_addr, ring::RingTunnelConnector, tcp::TcpTunnelConnector,
udp::UdpTunnelConnector, IpVersion, TunnelConnector,
@@ -58,6 +58,7 @@ pub async fn create_connector_by_url(
ip_version: IpVersion,
) -> Result<Box<dyn TunnelConnector + 'static>, Error> {
let url = url::Url::parse(url).map_err(|_| Error::InvalidUrl(url.to_owned()))?;
let url = idn::convert_idn_to_ascii(url)?;
let mut connector: Box<dyn TunnelConnector + 'static> = match url.scheme() {
"tcp" => {
let dst_addr =

View File

@@ -10,7 +10,7 @@ use std::{
use anyhow::Context;
use cidr::Ipv4Inet;
use clap::{command, Args, CommandFactory, Parser, Subcommand};
use clap::{Args, CommandFactory, Parser, Subcommand};
use clap_complete::Shell;
use dashmap::DashMap;
use humansize::format_size;

View File

@@ -17,22 +17,18 @@ use clap_complete::Shell;
use easytier::{
common::{
config::{
get_avaliable_encrypt_methods, ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig,
LoggingConfigLoader, NetworkIdentity, PeerConfig, PortForwardConfig, TomlConfigLoader,
VpnPortalConfig,
get_avaliable_encrypt_methods, load_config_from_file, ConfigFileControl, ConfigLoader,
ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader, NetworkIdentity,
PeerConfig, PortForwardConfig, TomlConfigLoader, VpnPortalConfig,
},
constants::EASYTIER_VERSION,
global_ctx::GlobalCtx,
set_default_machine_id,
stun::MockStunInfoCollector,
},
connector::create_connector_by_url,
defer,
instance_manager::NetworkInstanceManager,
launcher::{add_proxy_network_to_config, ConfigSource},
proto::common::{CompressionAlgoPb, NatType},
launcher::add_proxy_network_to_config,
proto::common::CompressionAlgoPb,
rpc_service::ApiRpcServer,
tunnel::{IpVersion, PROTO_PORT_OFFSET},
tunnel::PROTO_PORT_OFFSET,
utils::{init_logger, setup_panic_handler},
web_client,
};
@@ -136,6 +132,13 @@ struct Cli {
)]
config_file: Option<Vec<PathBuf>>,
#[arg(
long,
env = "ET_CONFIG_DIR",
help = t!("core_clap.config_dir").to_string()
)]
config_dir: Option<PathBuf>,
#[command(flatten)]
network_options: NetworkOptions,
@@ -152,7 +155,7 @@ struct Cli {
check_config: bool,
}
#[derive(Parser, Debug)]
#[derive(Parser, Debug, Default, PartialEq, Eq)]
struct NetworkOptions {
#[arg(
long,
@@ -417,6 +420,15 @@ struct NetworkOptions {
)]
disable_p2p: Option<bool>,
#[arg(
long,
env = "ET_P2P_ONLY",
help = t!("core_clap.p2p_only").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
p2p_only: Option<bool>,
#[arg(
long,
env = "ET_DISABLE_UDP_HOLE_PUNCHING",
@@ -707,6 +719,9 @@ impl Cli {
impl NetworkOptions {
fn can_merge(&self, cfg: &TomlConfigLoader, config_file_count: usize) -> bool {
if (*self) == NetworkOptions::default() {
return false;
}
if config_file_count == 1 {
return true;
}
@@ -928,6 +943,7 @@ impl NetworkOptions {
f.relay_network_whitelist = wl.join(" ");
}
f.disable_p2p = self.disable_p2p.unwrap_or(f.disable_p2p);
f.p2p_only = self.p2p_only.unwrap_or(f.p2p_only);
f.disable_udp_hole_punching = self
.disable_udp_hole_punching
.unwrap_or(f.disable_udp_hole_punching);
@@ -1141,7 +1157,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
defer!(dump_profile(0););
init_logger(&cli.logging_options, true)?;
let manager = Arc::new(NetworkInstanceManager::new());
let manager = Arc::new(NetworkInstanceManager::new().with_config_path(cli.config_dir.clone()));
let _rpc_server = ApiRpcServer::new(
cli.rpc_portal_options.rpc_portal,
@@ -1151,90 +1167,77 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
.serve()
.await?;
if cli.config_server.is_some() {
set_default_machine_id(cli.machine_id);
let config_server_url_s = cli.config_server.clone().unwrap();
let config_server_url = match url::Url::parse(&config_server_url_s) {
Ok(u) => u,
Err(_) => format!(
"udp://config-server.easytier.cn:22020/{}",
config_server_url_s
)
.parse()
.unwrap(),
};
let _web_client = if let Some(config_server_url_s) = cli.config_server.as_ref() {
let wc = web_client::run_web_client(
config_server_url_s,
cli.machine_id.clone(),
cli.network_options.hostname.clone(),
manager.clone(),
)
.await
.inspect(|_| {
println!(
"Web client started successfully...\nserver: {}",
config_server_url_s,
);
let mut c_url = config_server_url.clone();
c_url.set_path("");
let token = config_server_url
.path_segments()
.and_then(|mut x| x.next())
.map(|x| x.to_string())
.unwrap_or_default();
println!("Official config website: https://easytier.cn/web");
})?;
Some(wc)
} else {
None
};
let mut config_files = if let Some(v) = cli.config_file {
v.clone()
} else {
vec![]
};
if let Some(config_dir) = cli.config_dir.as_ref() {
if !config_dir.is_dir() {
anyhow::bail!("config_dir {} is not a directory", config_dir.display());
}
for entry in std::fs::read_dir(config_dir)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
let Some(ext) = path.extension() else {
continue;
};
if ext != "toml" {
continue;
}
config_files.push(path);
}
}
let config_file_count = config_files.len();
let mut crate_cli_network = (config_file_count == 0 && cli.config_server.is_none())
|| cli.network_options.network_name.is_some();
for config_file in config_files {
let (mut cfg, mut control) =
load_config_from_file(&config_file, cli.config_dir.as_ref()).await?;
if cli.network_options.can_merge(&cfg, config_file_count) {
cli.network_options
.merge_into(&mut cfg)
.with_context(|| format!("failed to merge config from cli: {:?}", config_file))?;
crate_cli_network = false;
control.set_read_only(true);
control.set_no_delete(true);
}
println!(
"Entering config client mode...\n server: {}\n token: {}",
c_url, token,
"Starting easytier from config file {:?}({:?}) with config:",
config_file, control.permission
);
println!("Official config website: https://easytier.cn/web");
if token.is_empty() {
panic!("empty token");
}
let config = TomlConfigLoader::default();
let global_ctx = Arc::new(GlobalCtx::new(config));
global_ctx.replace_stun_info_collector(Box::new(MockStunInfoCollector {
udp_nat_type: NatType::Unknown,
}));
let mut flags = global_ctx.get_flags();
flags.bind_device = false;
global_ctx.set_flags(flags);
let hostname = match cli.network_options.hostname {
None => gethostname::gethostname().to_string_lossy().to_string(),
Some(hostname) => hostname.to_string(),
};
let _wc = web_client::WebClient::new(
create_connector_by_url(c_url.as_str(), &global_ctx, IpVersion::Both).await?,
token.to_string(),
hostname,
manager,
);
tokio::signal::ctrl_c().await.unwrap();
return Ok(());
}
let mut crate_cli_network =
cli.config_file.is_none() || cli.network_options.network_name.is_some();
if let Some(config_files) = cli.config_file {
let config_file_count = config_files.len();
for config_file in config_files {
let mut cfg = if config_file == PathBuf::from("-") {
let mut stdin = String::new();
_ = tokio::io::stdin().read_to_string(&mut stdin).await?;
TomlConfigLoader::new_from_str(stdin.as_str())
.with_context(|| "failed to load config from stdin")?
} else {
TomlConfigLoader::new(&config_file)
.with_context(|| format!("failed to load config file: {:?}", config_file))?
};
if cli.network_options.can_merge(&cfg, config_file_count) {
cli.network_options.merge_into(&mut cfg).with_context(|| {
format!("failed to merge config from cli: {:?}", config_file)
})?;
crate_cli_network = false;
}
println!(
"Starting easytier from config file {:?} with config:",
config_file
);
println!("############### TOML ###############\n");
println!("{}", cfg.dump());
println!("-----------------------------------");
manager.run_network_instance(cfg, ConfigSource::File)?;
}
println!("############### TOML ###############\n");
println!("{}", cfg.dump());
println!("-----------------------------------");
manager.run_network_instance(cfg, true, control)?;
}
if crate_cli_network {
@@ -1246,7 +1249,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
println!("############### TOML ###############\n");
println!("{}", cfg.dump());
println!("-----------------------------------");
manager.run_network_instance(cfg, ConfigSource::Cli)?;
manager.run_network_instance(cfg, true, ConfigFileControl::STATIC_CONFIG)?;
}
tokio::select! {

View File

@@ -12,7 +12,7 @@ use thiserror::Error;
use tokio::io::{AsyncRead, AsyncReadExt};
use tokio::net::lookup_host;
use tracing::{debug, error};
use tracing::debug;
/// SOCKS5 reply code
#[derive(Error, Debug)]

View File

@@ -275,7 +275,7 @@ impl IcmpProxy {
}
let peer_manager = self.peer_manager.clone();
let is_latency_first = self.global_ctx.get_flags().latency_first;
let is_latency_first = self.global_ctx.latency_first();
self.tasks.lock().await.spawn(
async move {
while let Some(mut msg) = receiver.recv().await {

View File

@@ -437,7 +437,7 @@ impl UdpProxy {
// forward packets to peer manager
let mut receiver = self.receiver.lock().await.take().unwrap();
let peer_manager = self.peer_manager.clone();
let is_latency_first = self.global_ctx.get_flags().latency_first;
let is_latency_first = self.global_ctx.latency_first();
self.tasks.lock().await.spawn(async move {
while let Some(mut msg) = receiver.recv().await {
let hdr = msg.mut_peer_manager_header().unwrap();

View File

@@ -492,16 +492,18 @@ impl MagicDnsServerInstance {
let mut dns_server = Server::new(dns_config);
dns_server.run().await?;
if !tun_inet.contains(&fake_ip) && tun_dev.is_some() {
let cost = if cfg!(target_os = "windows") {
Some(4)
} else {
None
};
let ifcfg = IfConfiger {};
ifcfg
.add_ipv4_route(tun_dev.as_ref().unwrap(), fake_ip, 32, cost)
.await?;
if !tun_inet.contains(&fake_ip) {
if let Some(tun_dev_name) = &tun_dev {
let cost = if cfg!(target_os = "windows") {
Some(4)
} else {
None
};
let ifcfg = IfConfiger {};
ifcfg
.add_ipv4_route(tun_dev_name, fake_ip, 32, cost)
.await?;
}
}
let data = Arc::new(MagicDnsServerInstanceData {
@@ -544,13 +546,14 @@ impl MagicDnsServerInstance {
if let Err(e) = ret {
tracing::error!("Failed to close system config: {:?}", e);
}
}
if !self.tun_inet.contains(&self.data.fake_ip) && self.data.tun_dev.is_some() {
let ifcfg = IfConfiger {};
let _ = ifcfg
.remove_ipv4_route(self.data.tun_dev.as_ref().unwrap(), self.data.fake_ip, 32)
.await;
if !self.tun_inet.contains(&self.data.fake_ip) {
if let Some(tun_dev_name) = &self.data.tun_dev {
let ifcfg = IfConfiger {};
let _ = ifcfg
.remove_ipv4_route(tun_dev_name, self.data.fake_ip, 32)
.await;
}
}
}
let _ = self

View File

@@ -12,8 +12,8 @@ const MAC_RESOLVER_FILE_HEADER: &str = "# Added by easytier\n";
const ETC_RESOLVER: &str = "/etc/resolver";
const ETC_RESOLV_CONF: &str = "/etc/resolv.conf";
#[derive(Default)]
pub struct DarwinConfigurator {}
impl DarwinConfigurator {
pub fn new() -> Self {
DarwinConfigurator {}

View File

@@ -102,7 +102,7 @@ async fn test_magic_dns_server_instance() {
.await
.unwrap();
let routes = vec![
let routes = [
Route {
hostname: "test1".to_string(),
ipv4_addr: Some(Ipv4Inet::from_str("8.8.8.8/24").unwrap().into()),

View File

@@ -1,23 +1,36 @@
use std::{collections::BTreeMap, sync::Arc};
use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
use dashmap::DashMap;
use crate::{
common::{
config::{ConfigLoader, TomlConfigLoader},
config::{ConfigFileControl, ConfigLoader, TomlConfigLoader},
global_ctx::{EventBusSubscriber, GlobalCtxEvent},
scoped_task::ScopedTask,
},
launcher::{ConfigSource, NetworkInstance, NetworkInstanceRunningInfo},
launcher::{NetworkInstance, NetworkInstanceRunningInfo},
proto::{self},
rpc_service::InstanceRpcService,
};
pub(crate) struct WebClientGuard {
guard: Option<Arc<()>>,
stop_check_notifier: Arc<tokio::sync::Notify>,
}
impl Drop for WebClientGuard {
fn drop(&mut self) {
drop(self.guard.take());
self.stop_check_notifier.notify_one();
}
}
pub struct NetworkInstanceManager {
instance_map: Arc<DashMap<uuid::Uuid, NetworkInstance>>,
instance_stop_tasks: Arc<DashMap<uuid::Uuid, ScopedTask<()>>>,
stop_check_notifier: Arc<tokio::sync::Notify>,
instance_error_messages: Arc<DashMap<uuid::Uuid, String>>,
config_dir: Option<PathBuf>,
web_client_counter: Arc<()>,
}
impl Default for NetworkInstanceManager {
@@ -33,36 +46,29 @@ impl NetworkInstanceManager {
instance_stop_tasks: Arc::new(DashMap::new()),
stop_check_notifier: Arc::new(tokio::sync::Notify::new()),
instance_error_messages: Arc::new(DashMap::new()),
config_dir: None,
web_client_counter: Arc::new(()),
}
}
pub fn with_config_path(mut self, config_dir: Option<PathBuf>) -> Self {
self.config_dir = config_dir;
self
}
fn start_instance_task(&self, instance_id: uuid::Uuid) -> Result<(), anyhow::Error> {
if tokio::runtime::Handle::try_current().is_err() {
return Err(anyhow::anyhow!(
"tokio runtime not found, cannot start instance task"
));
}
let instance = self
.instance_map
.get(&instance_id)
.ok_or_else(|| anyhow::anyhow!("instance {} not found", instance_id))?;
match instance.get_config_source() {
ConfigSource::FFI | ConfigSource::GUI => {
// FFI and GUI have no tokio runtime, so we don't need to spawn a task
return Ok(());
}
_ => {
if tokio::runtime::Handle::try_current().is_err() {
return Err(anyhow::anyhow!(
"tokio runtime not found, cannot start instance task"
));
}
}
}
let instance_stop_notifier = instance.get_stop_notifier();
let instance_event_receiver = match instance.get_config_source() {
ConfigSource::Cli | ConfigSource::File | ConfigSource::Web => {
Some(instance.subscribe_event())
}
ConfigSource::GUI | ConfigSource::FFI => None,
};
let instance_event_receiver = instance.subscribe_event();
let instance_map = self.instance_map.clone();
let instance_stop_tasks = self.instance_stop_tasks.clone();
@@ -76,7 +82,6 @@ impl NetworkInstanceManager {
return;
};
let _t = instance_event_receiver
.flatten()
.map(|event| ScopedTask::from(handle_event(instance_id, event)));
instance_stop_notifier.notified().await;
if let Some(instance) = instance_map.get(&instance_id) {
@@ -97,18 +102,21 @@ impl NetworkInstanceManager {
pub fn run_network_instance(
&self,
cfg: TomlConfigLoader,
source: ConfigSource,
watch_event: bool,
config_file_control: ConfigFileControl,
) -> Result<uuid::Uuid, anyhow::Error> {
let instance_id = cfg.get_id();
if self.instance_map.contains_key(&instance_id) {
anyhow::bail!("instance {} already exists", instance_id);
}
let mut instance = NetworkInstance::new(cfg, source);
let mut instance = NetworkInstance::new(cfg, config_file_control);
instance.start()?;
self.instance_map.insert(instance_id, instance);
self.start_instance_task(instance_id)?;
if watch_event {
self.start_instance_task(instance_id)?;
}
Ok(instance_id)
}
@@ -187,18 +195,20 @@ impl NetworkInstanceManager {
pub fn get_network_instance_name(&self, instance_id: &uuid::Uuid) -> Option<String> {
self.instance_map
.get(instance_id)
.map(|instance| instance.value().get_inst_name())
.map(|instance| instance.value().get_network_name())
}
pub fn filter_network_instance(
pub fn iter(&self) -> dashmap::iter::Iter<'_, uuid::Uuid, NetworkInstance> {
self.instance_map.iter()
}
pub fn get_instance_config_control(
&self,
filter: impl Fn(&uuid::Uuid, &NetworkInstance) -> bool,
) -> Vec<uuid::Uuid> {
instance_id: &uuid::Uuid,
) -> Option<ConfigFileControl> {
self.instance_map
.iter()
.filter(|item| filter(item.key(), item.value()))
.map(|item| *item.key())
.collect()
.get(instance_id)
.map(|instance| instance.value().get_config_file_control().clone())
}
pub fn get_instance_service(
@@ -219,12 +229,33 @@ impl NetworkInstanceManager {
Ok(())
}
pub fn get_config_dir(&self) -> Option<&PathBuf> {
self.config_dir.as_ref()
}
pub(crate) fn register_web_client(&self) -> WebClientGuard {
WebClientGuard {
guard: Some(self.web_client_counter.clone()),
stop_check_notifier: self.stop_check_notifier.clone(),
}
}
pub(crate) fn notify_stop_check(&self) {
self.stop_check_notifier.notify_one();
}
pub async fn wait(&self) {
while self
.instance_map
.iter()
.any(|item| item.value().is_easytier_running())
{
loop {
let local_instance_running = self
.instance_map
.iter()
.any(|item| item.value().is_easytier_running());
let web_client_running = Arc::strong_count(&self.web_client_counter) > 1;
if !local_instance_running && !web_client_running {
break;
}
self.stop_check_notifier.notified().await;
}
}
@@ -429,31 +460,36 @@ mod tests {
c.set_listeners(vec![format!("tcp://0.0.0.0:{}", port).parse().unwrap()]);
})
.unwrap(),
ConfigSource::Cli,
true,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();
let instance_id2 = manager
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str).unwrap(),
ConfigSource::File,
true,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();
let instance_id3 = manager
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str).unwrap(),
ConfigSource::GUI,
false,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();
let instance_id4 = manager
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str).unwrap(),
ConfigSource::Web,
true,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();
let instance_id5 = manager
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str).unwrap(),
ConfigSource::FFI,
false,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();
@@ -491,13 +527,15 @@ mod tests {
assert!(manager
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str).unwrap(),
ConfigSource::Cli,
true,
ConfigFileControl::STATIC_CONFIG
)
.is_err());
assert!(manager
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str).unwrap(),
ConfigSource::File,
true,
ConfigFileControl::STATIC_CONFIG
)
.is_err());
assert!(manager
@@ -507,19 +545,22 @@ mod tests {
c.set_listeners(vec![format!("tcp://0.0.0.0:{}", port).parse().unwrap()]);
})
.unwrap(),
ConfigSource::GUI,
false,
ConfigFileControl::STATIC_CONFIG
)
.is_ok());
assert!(manager
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str).unwrap(),
ConfigSource::Web,
true,
ConfigFileControl::STATIC_CONFIG
)
.is_err());
assert!(manager
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str).unwrap(),
ConfigSource::FFI,
false,
ConfigFileControl::STATIC_CONFIG
)
.is_ok());
@@ -546,7 +587,8 @@ mod tests {
let free_tcp_port =
crate::utils::find_free_tcp_port(10012..65534).expect("no free tcp port found");
for config_source in [ConfigSource::Cli, ConfigSource::File] {
// Test with event watching enabled (for CLI/File/RPC usage) - instance should auto-stop on error
for watch_event in [true] {
let _port_holder =
std::net::TcpListener::bind(format!("0.0.0.0:{}", free_tcp_port)).unwrap();
@@ -561,7 +603,8 @@ mod tests {
manager
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str.as_str()).unwrap(),
config_source.clone(),
watch_event,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();
@@ -570,11 +613,14 @@ mod tests {
assert_eq!(manager.list_network_instance_ids().len(), 1);
}
_ = tokio::time::sleep(std::time::Duration::from_secs(5)) => {
panic!("instance manager with single failed instance({:?}) should not running", config_source);
panic!("instance manager with single failed instance({:?}) should not running", watch_event);
}
}
}
for config_source in [ConfigSource::Web, ConfigSource::GUI, ConfigSource::FFI] {
// Test without event watching (for FFI usage) - instance should remain even if failed
{
let watch_event = false;
let _port_holder =
std::net::TcpListener::bind(format!("0.0.0.0:{}", free_tcp_port)).unwrap();
@@ -589,7 +635,8 @@ mod tests {
manager
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str.as_str()).unwrap(),
config_source.clone(),
watch_event,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();
@@ -616,7 +663,8 @@ mod tests {
manager
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str.as_str()).unwrap(),
ConfigSource::Cli,
true,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();
@@ -625,7 +673,8 @@ mod tests {
manager
.run_network_instance(
TomlConfigLoader::new_from_str(cfg_str.as_str()).unwrap(),
ConfigSource::Cli,
true,
ConfigFileControl::STATIC_CONFIG,
)
.unwrap();

View File

@@ -1,4 +1,4 @@
use crate::common::config::PortForwardConfig;
use crate::common::config::{ConfigFileControl, PortForwardConfig};
use crate::proto::api::{self, manage};
use crate::proto::rpc_types::controller::BaseController;
use crate::rpc_service::InstanceRpcService;
@@ -10,7 +10,6 @@ use crate::{
},
constants::EASYTIER_VERSION,
global_ctx::{EventBusSubscriber, GlobalCtxEvent},
idn::safe_convert_idn_to_ascii,
},
instance::instance::Instance,
proto::api::instance::list_peer_route_pair,
@@ -282,35 +281,21 @@ impl Drop for EasyTierLauncher {
pub type NetworkInstanceRunningInfo = crate::proto::api::manage::NetworkInstanceRunningInfo;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfigSource {
Cli,
File,
Web,
GUI,
FFI,
}
pub struct NetworkInstance {
config: TomlConfigLoader,
launcher: Option<EasyTierLauncher>,
config_source: ConfigSource,
config_file_control: ConfigFileControl,
}
impl NetworkInstance {
pub fn new(config: TomlConfigLoader, source: ConfigSource) -> Self {
pub fn new(config: TomlConfigLoader, config_file_control: ConfigFileControl) -> Self {
Self {
config,
launcher: None,
config_source: source,
config_file_control,
}
}
pub fn get_config_source(&self) -> ConfigSource {
self.config_source.clone()
}
pub fn is_easytier_running(&self) -> bool {
self.launcher.is_some() && self.launcher.as_ref().unwrap().running()
}
@@ -404,6 +389,10 @@ impl NetworkInstance {
self.config.get_inst_name()
}
pub fn get_network_name(&self) -> String {
self.config.get_network_identity().network_name
}
pub fn set_tun_fd(&mut self, tun_fd: i32) {
if let Some(launcher) = self.launcher.as_ref() {
launcher.data.tun_fd.write().unwrap().replace(tun_fd);
@@ -439,6 +428,10 @@ impl NetworkInstance {
.map(|launcher| launcher.data.instance_stop_notifier.clone())
}
pub fn get_config_file_control(&self) -> &ConfigFileControl {
&self.config_file_control
}
pub fn get_latest_error_msg(&self) -> Option<String> {
if let Some(launcher) = self.launcher.as_ref() {
launcher.error_msg.read().unwrap().clone()
@@ -525,13 +518,9 @@ impl NetworkConfig {
{
NetworkingMethod::PublicServer => {
let public_server_url = self.public_server_url.clone().unwrap_or_default();
let converted_public_server_url = safe_convert_idn_to_ascii(&public_server_url);
cfg.set_peers(vec![PeerConfig {
uri: converted_public_server_url.parse().with_context(|| {
format!(
"failed to parse public server uri: {}",
converted_public_server_url
)
uri: public_server_url.parse().with_context(|| {
format!("failed to parse public server uri: {}", public_server_url)
})?,
}]);
}
@@ -541,11 +530,10 @@ impl NetworkConfig {
if peer_url.is_empty() {
continue;
}
let converted_peer_url = safe_convert_idn_to_ascii(peer_url);
peers.push(PeerConfig {
uri: converted_peer_url.parse().with_context(|| {
format!("failed to parse peer uri: {}", converted_peer_url)
})?,
uri: peer_url
.parse()
.with_context(|| format!("failed to parse peer uri: {}", peer_url))?,
});
}
@@ -559,10 +547,11 @@ impl NetworkConfig {
if listener_url.is_empty() {
continue;
}
let converted_listener_url = safe_convert_idn_to_ascii(listener_url);
listener_urls.push(converted_listener_url.parse().with_context(|| {
format!("failed to parse listener uri: {}", converted_listener_url)
})?);
listener_urls.push(
listener_url
.parse()
.with_context(|| format!("failed to parse listener uri: {}", listener_url))?,
);
}
cfg.set_listeners(listener_urls);
@@ -656,12 +645,8 @@ impl NetworkConfig {
self.mapped_listeners
.iter()
.map(|s| {
let converted_s = safe_convert_idn_to_ascii(s);
converted_s
.parse()
.with_context(|| {
format!("mapped listener is not a valid url: {}", converted_s)
})
s.parse()
.with_context(|| format!("mapped listener is not a valid url: {}", s))
.unwrap()
})
.map(|s: url::Url| {
@@ -715,6 +700,10 @@ impl NetworkConfig {
flags.disable_p2p = disable_p2p;
}
if let Some(p2p_only) = self.p2p_only {
flags.p2p_only = p2p_only;
}
if let Some(bind_device) = self.bind_device {
flags.bind_device = bind_device;
}
@@ -889,6 +878,7 @@ impl NetworkConfig {
result.disable_quic_input = Some(flags.disable_quic_input);
result.quic_listen_port = Some(flags.quic_listen_port as i32);
result.disable_p2p = Some(flags.disable_p2p);
result.p2p_only = Some(flags.p2p_only);
result.bind_device = Some(flags.bind_device);
result.no_tun = Some(flags.no_tun);
result.enable_exit_node = Some(flags.enable_exit_node);
@@ -897,6 +887,7 @@ impl NetworkConfig {
result.proxy_forward_by_system = Some(flags.proxy_forward_by_system);
result.disable_encryption = Some(!flags.enable_encryption);
result.disable_udp_hole_punching = Some(flags.disable_udp_hole_punching);
result.disable_sym_hole_punching = Some(flags.disable_sym_hole_punching);
result.enable_magic_dns = Some(flags.accept_dns);
result.mtu = Some(flags.mtu as i32);
result.enable_private_mode = Some(flags.private_mode);
@@ -1123,6 +1114,7 @@ mod tests {
flags.enable_quic_proxy = rng.gen_bool(0.5);
flags.disable_quic_input = rng.gen_bool(0.3);
flags.disable_p2p = rng.gen_bool(0.2);
flags.p2p_only = rng.gen_bool(0.2);
flags.bind_device = rng.gen_bool(0.3);
flags.no_tun = rng.gen_bool(0.1);
flags.enable_exit_node = rng.gen_bool(0.4);

View File

@@ -997,11 +997,20 @@ impl PeerManager {
}
}
fn check_p2p_only_before_send(&self, dst_peer_id: PeerId) -> Result<(), Error> {
if self.global_ctx.p2p_only() && !self.peers.has_peer(dst_peer_id) {
return Err(Error::RouteError(None));
}
Ok(())
}
pub async fn send_msg_for_proxy(
&self,
mut msg: ZCPacket,
dst_peer_id: PeerId,
) -> Result<(), Error> {
self.check_p2p_only_before_send(dst_peer_id)?;
self.self_tx_counters
.compress_tx_bytes_before
.add(msg.buf_len() as u64);
@@ -1199,7 +1208,7 @@ impl PeerManager {
.compress_tx_bytes_after
.add(msg.buf_len() as u64);
let is_latency_first = self.global_ctx.get_flags().latency_first;
let is_latency_first = self.global_ctx.latency_first();
msg.mut_peer_manager_header()
.unwrap()
.set_latency_first(is_latency_first)
@@ -1209,6 +1218,11 @@ impl PeerManager {
let mut msg = Some(msg);
let total_dst_peers = dst_peers.len();
for (i, peer_id) in dst_peers.iter().enumerate() {
if let Err(e) = self.check_p2p_only_before_send(*peer_id) {
errs.push(e);
continue;
}
let mut msg = if i == total_dst_peers - 1 {
msg.take().unwrap()
} else {
@@ -1218,10 +1232,13 @@ impl PeerManager {
let hdr = msg.mut_peer_manager_header().unwrap();
hdr.to_peer_id.set(*peer_id);
if not_send_to_self && *peer_id == self.my_peer_id {
// the packet may be sent to vpn portal, so we just set flags instead of drop it
hdr.set_not_send_to_tun(true);
hdr.set_no_proxy(true);
#[cfg(not(target_env = "ohos"))]
{
if not_send_to_self && *peer_id == self.my_peer_id {
// the packet may be sent to vpn portal, so we just set flags instead of drop it
hdr.set_not_send_to_tun(true);
hdr.set_no_proxy(true);
}
}
self.self_tx_counters

View File

@@ -1,5 +1,5 @@
use std::{
collections::{BTreeMap, BTreeSet, HashMap, VecDeque},
collections::{BTreeMap, BTreeSet, HashMap},
fmt::Debug,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
sync::{
@@ -13,8 +13,6 @@ use arc_swap::ArcSwap;
use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr};
use crossbeam::atomic::AtomicCell;
use dashmap::DashMap;
use ordered_hash_map::OrderedHashMap;
use parking_lot::{lock_api::RwLockUpgradableReadGuard, RwLock};
use petgraph::{
algo::dijkstra,
graph::{Graph, NodeIndex},
@@ -43,7 +41,7 @@ use crate::{
route_foreign_network_infos, route_foreign_network_summary,
sync_route_info_request::ConnInfo, ForeignNetworkRouteInfoEntry,
ForeignNetworkRouteInfoKey, OspfRouteRpc, OspfRouteRpcClientFactory,
OspfRouteRpcServer, PeerGroupInfo, PeerIdVersion, RouteForeignNetworkInfos,
OspfRouteRpcServer, PeerIdVersion, RouteForeignNetworkInfos,
RouteForeignNetworkSummary, RoutePeerInfo, RoutePeerInfos, SyncRouteInfoError,
SyncRouteInfoRequest, SyncRouteInfoResponse,
},
@@ -72,12 +70,6 @@ static REMOVE_DEAD_PEER_INFO_AFTER: Duration = Duration::from_secs(3660);
static AVOID_RELAY_COST: usize = i32::MAX as usize;
static FORCE_USE_CONN_LIST: AtomicBool = AtomicBool::new(true);
// if a peer is unreachable for `REMOVE_UNREACHABLE_PEER_INFO_AFTER` time, we can remove it because
// 1. all the ospf sessions between two zone are already destroy, new created session will resend the peer info.
// 2. all the dst_saved_peer_info_version in all sessions already remove the peer info, the peer info will be propagated
// in another zone when two zone restore the conneciton.
static REMOVE_UNREACHABLE_PEER_INFO_AFTER: Duration = Duration::from_secs(90);
type Version = u32;
#[derive(Debug, Clone)]
@@ -132,9 +124,7 @@ impl RoutePeerInfo {
proxy_cidrs: Vec::new(),
hostname: None,
udp_stun_info: 0,
// ensure this is updated when the peer_infos/conn_info/foreign_network lock is acquired.
// else we may assign a older timestamp than iterate time.
last_update: None,
last_update: Some(SystemTime::now().into()),
version: 0,
easytier_version: EASYTIER_VERSION.to_string(),
feature_flag: None,
@@ -146,12 +136,13 @@ impl RoutePeerInfo {
}
}
pub fn new_updated_self(
pub fn update_self(
&self,
my_peer_id: PeerId,
peer_route_id: u64,
global_ctx: &ArcGlobalCtx,
) -> Self {
Self {
let mut new = Self {
peer_id: my_peer_id,
inst_id: Some(global_ctx.get_id().into()),
cost: 0,
@@ -169,10 +160,9 @@ impl RoutePeerInfo {
.get_stun_info_collector()
.get_stun_info()
.udp_nat_type,
// these two fields should not participate in comparison.
last_update: None,
version: 0,
// following fields do not participate in comparison.
last_update: self.last_update,
version: self.version,
easytier_version: EASYTIER_VERSION.to_string(),
feature_flag: Some(global_ctx.get_feature_flags()),
@@ -186,28 +176,22 @@ impl RoutePeerInfo {
ipv6_addr: global_ctx.get_ipv6().map(|x| x.into()),
groups: global_ctx.get_acl_groups(my_peer_id),
}
}
};
pub fn try_update_new_peer_info(old: &RoutePeerInfo, new: &mut RoutePeerInfo) -> bool {
let need_update_periodically = if let Ok(Ok(d)) =
SystemTime::try_from(old.last_update.unwrap_or_default()).map(|x| x.elapsed())
SystemTime::try_from(new.last_update.unwrap_or_default()).map(|x| x.elapsed())
{
d > UPDATE_PEER_INFO_PERIOD
} else {
true
};
// these two fields should not participate in comparison.
new.version = old.version;
new.last_update = old.last_update;
if *new != *old || need_update_periodically {
if new != *self || need_update_periodically {
new.last_update = Some(SystemTime::now().into());
new.version += 1;
true
} else {
false
}
new
}
}
@@ -277,7 +261,7 @@ type Error = SyncRouteInfoError;
// constructed with all infos synced from all peers.
struct SyncedRouteInfo {
peer_infos: RwLock<OrderedHashMap<PeerId, RoutePeerInfo>>,
peer_infos: DashMap<PeerId, RoutePeerInfo>,
// prost doesn't support unknown fields, so we use DynamicMessage to store raw infos and progate them to other peers.
raw_peer_infos: DashMap<PeerId, DynamicMessage>,
conn_map: DashMap<PeerId, (BTreeSet<PeerId>, AtomicVersion)>,
@@ -309,13 +293,14 @@ impl SyncedRouteInfo {
fn remove_peer(&self, peer_id: PeerId) {
tracing::warn!(?peer_id, "remove_peer from synced_route_info");
self.peer_infos.write().remove(&peer_id);
self.peer_infos.remove(&peer_id);
self.raw_peer_infos.remove(&peer_id);
self.conn_map.remove(&peer_id);
self.foreign_network.retain(|k, _| k.peer_id != peer_id);
self.group_trust_map.remove(&peer_id);
self.group_trust_map_cache.remove(&peer_id);
shrink_dashmap(&self.peer_infos, None);
shrink_dashmap(&self.raw_peer_infos, None);
shrink_dashmap(&self.conn_map, None);
shrink_dashmap(&self.foreign_network, None);
@@ -328,16 +313,10 @@ impl SyncedRouteInfo {
fn fill_empty_peer_info(&self, peer_ids: &BTreeSet<PeerId>) {
let mut need_inc_version = false;
for peer_id in peer_ids {
let guard = self.peer_infos.upgradable_read();
if !guard.contains_key(peer_id) {
let mut peer_info = RoutePeerInfo::new();
let mut guard = RwLockUpgradableReadGuard::upgrade(guard);
peer_info.last_update = Some(SystemTime::now().into());
guard.insert(*peer_id, peer_info);
self.peer_infos.entry(*peer_id).or_insert_with(|| {
need_inc_version = true;
} else {
drop(guard);
}
RoutePeerInfo::new()
});
self.conn_map.entry(*peer_id).or_insert_with(|| {
need_inc_version = true;
@@ -351,7 +330,6 @@ impl SyncedRouteInfo {
fn get_peer_info_version_with_default(&self, peer_id: PeerId) -> Version {
self.peer_infos
.read()
.get(&peer_id)
.map(|x| x.version)
.unwrap_or(0)
@@ -360,9 +338,8 @@ impl SyncedRouteInfo {
fn get_avoid_relay_data(&self, peer_id: PeerId) -> bool {
// if avoid relay, just set all outgoing edges to a large value: AVOID_RELAY_COST.
self.peer_infos
.read()
.get(&peer_id)
.and_then(|x| x.feature_flag)
.and_then(|x| x.value().feature_flag)
.map(|x| x.avoid_relay_data)
.unwrap_or_default()
}
@@ -418,10 +395,7 @@ impl SyncedRouteInfo {
my_peer_route_id,
dst_peer_id,
if route_info.peer_id == dst_peer_id {
self.peer_infos
.read()
.get(&dst_peer_id)
.map(|x| x.peer_route_id)
self.peer_infos.get(&dst_peer_id).map(|x| x.peer_route_id)
} else {
None
},
@@ -435,19 +409,26 @@ impl SyncedRouteInfo {
.unwrap();
assert_eq!(peer_id_raw, route_info.peer_id);
let mut guard = self.peer_infos.write();
// time between peers may not be synchronized, so update last_update to local now.
// note only last_update with larger version will be updated to local saved peer info.
route_info.last_update = Some(SystemTime::now().into());
if guard
.get_mut(&route_info.peer_id)
.is_none_or(|old| route_info.version > old.version)
{
self.raw_peer_infos
.insert(route_info.peer_id, raw_route_info.clone());
guard.insert(route_info.peer_id, route_info);
need_inc_version = true;
}
self.peer_infos
.entry(route_info.peer_id)
.and_modify(|old_entry| {
if route_info.version > old_entry.version {
self.raw_peer_infos
.insert(route_info.peer_id, raw_route_info.clone());
*old_entry = route_info.clone();
need_inc_version = true;
}
})
.or_insert_with(|| {
need_inc_version = true;
self.raw_peer_infos
.insert(route_info.peer_id, raw_route_info.clone());
route_info.clone()
});
}
if need_inc_version {
self.version.inc();
@@ -554,34 +535,15 @@ impl SyncedRouteInfo {
my_peer_route_id: u64,
global_ctx: &ArcGlobalCtx,
) -> bool {
let mut new = RoutePeerInfo::new_updated_self(my_peer_id, my_peer_route_id, global_ctx);
let mut guard = self.peer_infos.upgradable_read();
let old = guard.get(&my_peer_id);
let new_version = old.map(|x| x.version).unwrap_or(0) + 1;
let need_insert_new = if let Some(old) = old {
RoutePeerInfo::try_update_new_peer_info(old, &mut new)
} else {
true
};
if need_insert_new {
let acl_groups = if old.map(|x| x.groups != new.groups).unwrap_or(true) {
Some(new.groups.clone())
} else {
None
};
guard.with_upgraded(|peer_infos| {
new.last_update = Some(SystemTime::now().into());
new.version = new_version;
peer_infos.insert(my_peer_id, new)
});
drop(guard);
if let Some(acl_groups) = acl_groups {
self.update_my_group_trusts(my_peer_id, &acl_groups);
}
let mut old = self.peer_infos.entry(my_peer_id).or_default();
let new = old.update_self(my_peer_id, my_peer_route_id, global_ctx);
let new_version = new.version;
let old_version = old.version;
*old = new;
drop(old);
if new_version != old_version {
self.update_my_group_trusts(my_peer_id);
self.version.inc();
true
} else {
@@ -672,28 +634,6 @@ impl SyncedRouteInfo {
updated
}
fn get_next_last_sync_succ_timestamp(&self) -> SystemTime {
let _peer_info_lock = self.peer_infos.read();
// TODO: add conn and foreign network lock
SystemTime::now()
}
fn check_peer_info_last_update_monotonic_increasing(&self) {
let mut last_update: Option<prost_types::Timestamp> = None;
for peer_info in self.peer_infos.read().values() {
if let Some(last_update) = last_update {
let cur_last_update = peer_info.last_update.unwrap();
assert!(
cur_last_update.seconds > last_update.seconds
|| cur_last_update.nanos >= last_update.nanos,
"peer info last update not monotonic increasing"
);
}
last_update = peer_info.last_update;
}
}
fn is_peer_bidirectly_connected(&self, src_peer_id: PeerId, dst_peer_id: PeerId) -> bool {
self.conn_map
.get(&src_peer_id)
@@ -785,15 +725,13 @@ impl SyncedRouteInfo {
}
}
fn update_my_group_trusts(&self, my_peer_id: PeerId, groups: &[PeerGroupInfo]) {
fn update_my_group_trusts(&self, my_peer_id: PeerId) {
let mut my_group_map = HashMap::new();
let mut my_group_names = Vec::new();
for group in groups.iter() {
for group in self.peer_infos.entry(my_peer_id).or_default().groups.iter() {
my_group_map.insert(group.group_name.clone(), group.group_proof.clone());
my_group_names.push(group.group_name.clone());
}
self.group_trust_map.insert(my_peer_id, my_group_map);
self.group_trust_map_cache
.insert(my_peer_id, Arc::new(my_group_names));
@@ -811,16 +749,21 @@ struct NextHopInfo {
}
// dst_peer_id -> (next_hop_peer_id, cost, path_len)
type NextHopMap = DashMap<PeerId, NextHopInfo>;
#[derive(Debug, Clone, Copy)]
struct PeerIdAndVersion {
peer_id: PeerId,
version: Version,
}
// computed with SyncedRouteInfo. used to get next hop.
#[derive(Debug)]
struct RouteTable {
peer_infos: DashMap<PeerId, RoutePeerInfo>,
next_hop_map: NextHopMap,
ipv4_peer_id_map: DashMap<Ipv4Addr, PeerIdVersion>,
ipv6_peer_id_map: DashMap<Ipv6Addr, PeerIdVersion>,
cidr_peer_id_map: ArcSwap<PrefixMap<Ipv4Cidr, PeerIdVersion>>,
cidr_v6_peer_id_map: ArcSwap<PrefixMap<Ipv6Cidr, PeerIdVersion>>,
ipv4_peer_id_map: DashMap<Ipv4Addr, PeerIdAndVersion>,
ipv6_peer_id_map: DashMap<Ipv6Addr, PeerIdAndVersion>,
cidr_peer_id_map: ArcSwap<PrefixMap<Ipv4Cidr, PeerIdAndVersion>>,
cidr_v6_peer_id_map: ArcSwap<PrefixMap<Ipv6Cidr, PeerIdAndVersion>>,
next_hop_map_version: AtomicVersion,
}
@@ -868,17 +811,18 @@ impl RouteTable {
let mut start_node_idx = None;
let peer_id_to_node_index: PeerIdToNodexIdxMap = DashMap::new();
for (peer_id, info) in synced_info.peer_infos.read().iter() {
let peer_id = *peer_id;
for item in synced_info.peer_infos.iter() {
let peer_id = item.key();
let info = item.value();
if info.version == 0 {
continue;
}
let node_idx = graph.add_node(peer_id);
let node_idx = graph.add_node(*peer_id);
peer_id_to_node_index.insert(peer_id, node_idx);
if peer_id == my_peer_id {
peer_id_to_node_index.insert(*peer_id, node_idx);
if *peer_id == my_peer_id {
start_node_idx = Some(node_idx);
}
}
@@ -1047,18 +991,18 @@ impl RouteTable {
}
let peer_id = item.key();
let Some(info) = synced_info.peer_infos.read().get(peer_id).cloned() else {
let Some(info) = synced_info.peer_infos.get(peer_id) else {
continue;
};
self.peer_infos.insert(*peer_id, info.clone());
let peer_id_and_version = PeerIdVersion {
let peer_id_and_version = PeerIdAndVersion {
peer_id: *peer_id,
version,
};
let is_new_peer_better = |old_peer: &PeerIdVersion| -> bool {
let is_new_peer_better = |old_peer: &PeerIdAndVersion| -> bool {
if peer_id_and_version.version > old_peer.version {
return true;
}
@@ -1250,11 +1194,6 @@ struct SyncRouteSession {
dst_saved_conn_info_version: DashMap<PeerId, VersionAndTouchTime>,
dst_saved_foreign_network_versions: DashMap<ForeignNetworkRouteInfoKey, VersionAndTouchTime>,
// we don't want to send unreachable peer infos to peer, so we keep track of them.
unreachable_peers: parking_lot::Mutex<VecDeque<PeerIdVersion>>,
last_sync_succ_timestamp: AtomicCell<Option<SystemTime>>,
my_session_id: AtomicSessionId,
dst_session_id: AtomicSessionId,
@@ -1279,10 +1218,6 @@ impl SyncRouteSession {
dst_saved_conn_info_version: DashMap::new(),
dst_saved_foreign_network_versions: DashMap::new(),
unreachable_peers: parking_lot::Mutex::new(VecDeque::new()),
last_sync_succ_timestamp: AtomicCell::new(None),
my_session_id: AtomicSessionId::new(rand::random()),
dst_session_id: AtomicSessionId::new(0),
@@ -1428,15 +1363,12 @@ impl SyncRouteSession {
self.need_sync_initiator_info.store(true, Ordering::Relaxed);
}
// return whether session id is updated
fn update_dst_session_id(&self, session_id: SessionId) {
if session_id != self.dst_session_id.load(Ordering::Relaxed) {
tracing::warn!(?self, ?session_id, "session id mismatch, clear saved info.");
self.dst_session_id.store(session_id, Ordering::Relaxed);
self.dst_saved_conn_info_version.clear();
self.dst_saved_peer_info_versions.clear();
self.last_sync_succ_timestamp.store(None);
self.unreachable_peers.lock().clear();
}
}
@@ -1454,16 +1386,6 @@ impl SyncRouteSession {
self.dst_saved_foreign_network_versions.shrink_to_fit();
}
fn update_last_sync_succ_timestamp(&self, next_last_sync_succ_timestamp: SystemTime) {
let _ = self.last_sync_succ_timestamp.fetch_update(|x| {
if x.is_none_or(|old| old < next_last_sync_succ_timestamp) {
Some(Some(next_last_sync_succ_timestamp))
} else {
None
}
});
}
fn short_debug_string(&self) -> String {
format!(
"session_dst_peer: {:?}, my_session_id: {:?}, dst_session_id: {:?}, we_are_initiator: {:?}, dst_is_initiator: {:?}, rpc_tx_count: {:?}, rpc_rx_count: {:?}, task: {:?}",
@@ -1548,7 +1470,7 @@ impl PeerRouteServiceImpl {
foreign_network_my_peer_id_map: DashMap::new(),
synced_route_info: SyncedRouteInfo {
peer_infos: RwLock::new(OrderedHashMap::new()),
peer_infos: DashMap::new(),
raw_peer_infos: DashMap::new(),
conn_map: DashMap::new(),
foreign_network: DashMap::new(),
@@ -1792,72 +1714,21 @@ impl PeerRouteServiceImpl {
}
fn build_route_info(&self, session: &SyncRouteSession) -> Option<Vec<RoutePeerInfo>> {
self.synced_route_info
.check_peer_info_last_update_monotonic_increasing();
let mut route_infos = Vec::new();
let peer_infos = self.synced_route_info.peer_infos.read();
let last_sync_succ_timestamp = session.last_sync_succ_timestamp.load();
for (peer_id, peer_info) in peer_infos.iter().rev() {
// stop iter if last_update of peer info is older than session.latest_scanned_peer_info_timestamp
if let Some(last_update) = peer_info.last_update {
let last_update = TryInto::<SystemTime>::try_into(last_update).unwrap();
if last_sync_succ_timestamp.is_some_and(|t| last_update < t) {
tracing::debug!(
"ignore peer_info {:?} because last_update: {:?} is older than last_sync_succ_timestamp: {:?}, peer_infos_count: {}, my_peer_id: {:?}, session: {:?}",
peer_info,
last_update,
last_sync_succ_timestamp,
peer_infos.len(),
self.my_peer_id,
session
);
continue;
}
}
if session.check_saved_peer_info_update_to_date(peer_info.peer_id, peer_info.version) {
continue;
}
for item in self.synced_route_info.peer_infos.iter() {
// do not send unreachable peer info to dst peer.
if !self.route_table.peer_reachable(*peer_id) {
session.unreachable_peers.lock().push_front(PeerIdVersion {
peer_id: *peer_id,
version: peer_info.version,
});
if !self.route_table.peer_reachable(*item.key()) {
continue;
}
route_infos.push(peer_info.clone());
}
let mut unreachable_peers = session.unreachable_peers.lock();
let cur_len = unreachable_peers.len();
for _ in 0..cur_len {
let peer_id_version = unreachable_peers.pop_back().unwrap();
let peer_id = peer_id_version.peer_id;
let version = peer_id_version.version;
if session.check_saved_peer_info_update_to_date(peer_id, version) {
// already up-to-date, skip
if session
.check_saved_peer_info_update_to_date(item.value().peer_id, item.value().version)
{
continue;
}
let reachable = self.route_table.peer_reachable(peer_id);
match self.synced_route_info.peer_infos.read().get(&peer_id) {
Some(route_info) => {
if reachable {
route_infos.push(route_info.clone());
}
// this round rpc may fail, so keep it and remove the id only when it's in dst_saved_map
unreachable_peers.push_front(peer_id_version);
}
None => {
// if not found in peer info map, forget this peer id.
continue;
}
};
}
unreachable_peers.shrink_to_fit();
route_infos.push(item.value().clone());
}
if route_infos.is_empty() {
None
@@ -1989,7 +1860,6 @@ impl PeerRouteServiceImpl {
let dst_supports_peer_list = self
.synced_route_info
.peer_infos
.read()
.get(&dst_peer_id)
.and_then(|p| p.feature_flag)
.map(|x| x.support_conn_list_sync)
@@ -2018,13 +1888,11 @@ impl PeerRouteServiceImpl {
fn clear_expired_peer(&self) {
let now = SystemTime::now();
let mut to_remove = Vec::new();
for (peer_id, peer_info) in self.synced_route_info.peer_infos.read().iter() {
if let Ok(d) = now.duration_since(peer_info.last_update.unwrap().try_into().unwrap()) {
if d > REMOVE_DEAD_PEER_INFO_AFTER
|| (d > REMOVE_UNREACHABLE_PEER_INFO_AFTER
&& !self.route_table.peer_reachable(*peer_id))
{
to_remove.push(*peer_id);
for item in self.synced_route_info.peer_infos.iter() {
if let Ok(d) = now.duration_since(item.value().last_update.unwrap().try_into().unwrap())
{
if d > REMOVE_DEAD_PEER_INFO_AFTER {
to_remove.push(*item.key());
}
}
}
@@ -2103,8 +1971,6 @@ impl PeerRouteServiceImpl {
let my_peer_id = self.my_peer_id;
let next_last_sync_succ_timestamp =
self.synced_route_info.get_next_last_sync_succ_timestamp();
let (peer_infos, conn_info, foreign_network) =
self.build_sync_request(&session, dst_peer_id);
if peer_infos.is_none()
@@ -2154,11 +2020,6 @@ impl PeerRouteServiceImpl {
.sync_route_info(ctrl, SyncRouteInfoRequest::default())
.await;
tracing::debug!(
"sync_route_info resp: {:?}, req: {:?}, session: {:?}, my_info: {:?}, next_last_sync_succ_timestamp: {:?}",
ret, sync_route_info_req, session, self.global_ctx.network, next_last_sync_succ_timestamp
);
if let Err(e) = &ret {
tracing::error!(
?ret,
@@ -2205,8 +2066,6 @@ impl PeerRouteServiceImpl {
if let Some(foreign_network) = &foreign_network {
session.update_dst_saved_foreign_network_version(foreign_network, dst_peer_id);
}
session.update_last_sync_succ_timestamp(next_last_sync_succ_timestamp);
}
}
false
@@ -2297,9 +2156,9 @@ impl OspfRouteRpc for RouteSessionManager {
let peer_infos = request.peer_infos.map(|x| x.items);
let conn_info = request.conn_info;
let foreign_network = request.foreign_network_infos;
let raw_peer_infos = if peer_infos.is_some() {
let raw_peer_infos = if let Some(peer_infos_ref) = &peer_infos {
let r = get_raw_peer_infos(&mut ctrl.get_raw_input().unwrap()).unwrap();
assert_eq!(r.len(), peer_infos.as_ref().unwrap().len());
assert_eq!(r.len(), peer_infos_ref.len());
Some(r)
} else {
None
@@ -2981,7 +2840,7 @@ mod tests {
peers::{
create_packet_recv_chan,
peer_manager::{PeerManager, RouteAlgoType},
peer_ospf_route::{PeerIdVersion, PeerRouteServiceImpl, FORCE_USE_CONN_LIST},
peer_ospf_route::{PeerIdAndVersion, PeerRouteServiceImpl, FORCE_USE_CONN_LIST},
route_trait::{NextHopPolicy, Route, RouteCostCalculatorInterface},
tests::{connect_peer_manager, create_mock_peer_manager, wait_route_appear},
},
@@ -3064,14 +2923,8 @@ mod tests {
tokio::time::sleep(Duration::from_secs(3)).await;
assert_eq!(
2,
r_a.service_impl.synced_route_info.peer_infos.read().len()
);
assert_eq!(
2,
r_b.service_impl.synced_route_info.peer_infos.read().len()
);
assert_eq!(2, r_a.service_impl.synced_route_info.peer_infos.len());
assert_eq!(2, r_b.service_impl.synced_route_info.peer_infos.len());
for s in r_a.service_impl.sessions.iter() {
assert!(s.value().task.is_running());
@@ -3081,7 +2934,6 @@ mod tests {
r_a.service_impl
.synced_route_info
.peer_infos
.read()
.get(&p_a.my_peer_id())
.unwrap()
.version,
@@ -3138,7 +2990,7 @@ mod tests {
for r in [r_a.clone(), r_b.clone(), r_c.clone()].iter() {
wait_for_condition(
|| async { r.service_impl.synced_route_info.peer_infos.read().len() == 3 },
|| async { r.service_impl.synced_route_info.peer_infos.len() == 3 },
Duration::from_secs(5),
)
.await;
@@ -3243,9 +3095,7 @@ mod tests {
// check peer infos
let peer_info = synced_info
.peer_infos
.read()
.get(&routable_peer.my_peer_id())
.cloned()
.unwrap();
assert_eq!(peer_info.peer_id, routable_peer.my_peer_id());
}
@@ -3275,7 +3125,7 @@ mod tests {
for r in [r_a.clone(), r_b.clone(), r_c.clone()].iter() {
wait_for_condition(
|| async { r.service_impl.synced_route_info.peer_infos.read().len() == 3 },
|| async { r.service_impl.synced_route_info.peer_infos.len() == 3 },
Duration::from_secs(5),
)
.await;
@@ -3542,10 +3392,10 @@ mod tests {
let proxy_cidr: Ipv4Cidr = "192.168.100.0/24".parse().unwrap();
let test_ip = proxy_cidr.first_address();
let mut cidr_peer_id_map: PrefixMap<Ipv4Cidr, PeerIdVersion> = PrefixMap::new();
let mut cidr_peer_id_map: PrefixMap<Ipv4Cidr, PeerIdAndVersion> = PrefixMap::new();
cidr_peer_id_map.insert(
proxy_cidr,
PeerIdVersion {
PeerIdAndVersion {
peer_id: p_c.my_peer_id(),
version: 0,
},

View File

@@ -76,6 +76,8 @@ message NetworkConfig {
repeated PortForwardConfig port_forwards = 48;
optional bool disable_sym_hole_punching = 49;
optional bool p2p_only = 51;
}
message PortForwardConfig {
@@ -112,6 +114,12 @@ message NetworkInstanceRunningInfoMap {
map<string, NetworkInstanceRunningInfo> map = 1;
}
message NetworkMeta {
common.UUID inst_id = 1;
string network_name = 2;
uint32 config_permission = 3;
}
message ValidateConfigRequest { NetworkConfig config = 1; }
message ValidateConfigResponse { string toml_config = 1; }
@@ -119,6 +127,7 @@ message ValidateConfigResponse { string toml_config = 1; }
message RunNetworkInstanceRequest {
common.UUID inst_id = 1;
NetworkConfig config = 2;
bool overwrite = 3;
}
message RunNetworkInstanceResponse { common.UUID inst_id = 1; }
@@ -143,6 +152,14 @@ message DeleteNetworkInstanceResponse {
repeated common.UUID remain_inst_ids = 1;
}
message GetNetworkInstanceConfigRequest { common.UUID inst_id = 1; }
message GetNetworkInstanceConfigResponse { NetworkConfig config = 1; }
message ListNetworkInstanceMetaRequest { repeated common.UUID inst_ids = 1; }
message ListNetworkInstanceMetaResponse { repeated NetworkMeta metas = 1; }
service WebClientService {
rpc ValidateConfig(ValidateConfigRequest) returns (ValidateConfigResponse) {}
rpc RunNetworkInstance(RunNetworkInstanceRequest)
@@ -155,4 +172,8 @@ service WebClientService {
returns (ListNetworkInstanceResponse) {}
rpc DeleteNetworkInstance(DeleteNetworkInstanceRequest)
returns (DeleteNetworkInstanceResponse) {}
rpc GetNetworkInstanceConfig(GetNetworkInstanceConfigRequest)
returns (GetNetworkInstanceConfigResponse) {}
rpc ListNetworkInstanceMeta(ListNetworkInstanceMetaRequest)
returns (ListNetworkInstanceMetaResponse) {}
}

View File

@@ -60,6 +60,8 @@ message FlagsInConfig {
// tld dns zone for magic dns
string tld_dns_zone = 31;
bool p2p_only = 32;
}
message RpcDescriptor {

View File

@@ -1,11 +1,10 @@
use std::sync::Arc;
use std::{collections::HashSet, sync::Arc};
use crate::{
common::config::ConfigLoader,
common::config::{ConfigFileControl, ConfigFilePermission, ConfigLoader},
instance_manager::NetworkInstanceManager,
launcher::ConfigSource,
proto::{
api::manage::*,
api::{config::GetConfigRequest, manage::*},
rpc_types::{self, controller::BaseController},
},
};
@@ -47,11 +46,59 @@ impl WebClientService for InstanceManageRpcService {
if let Some(inst_id) = req.inst_id {
cfg.set_id(inst_id.into());
}
self.manager.run_network_instance(cfg, ConfigSource::Web)?;
println!("instance {} started", id);
Ok(RunNetworkInstanceResponse {
let resp = RunNetworkInstanceResponse {
inst_id: Some(id.into()),
})
};
let mut control = if let Some(control) = self.manager.get_instance_config_control(&id) {
if !req.overwrite {
return Ok(resp);
}
if control.is_read_only() {
return Err(
anyhow::anyhow!("instance {} is read-only, cannot be overwritten", id).into(),
);
}
if let Some(path) = control.path.as_ref() {
let real_control = ConfigFileControl::from_path(path.clone()).await;
if real_control.is_read_only() {
return Err(anyhow::anyhow!(
"config file {} is read-only, cannot be overwritten",
path.display()
)
.into());
}
}
self.manager.delete_network_instance(vec![id])?;
control.clone()
} else if let Some(config_dir) = self.manager.get_config_dir() {
ConfigFileControl::new(
Some(config_dir.join(format!("{}.toml", id))),
ConfigFilePermission::default(),
)
} else {
ConfigFileControl::new(None, ConfigFilePermission::default())
};
if !control.is_read_only() {
if let Some(config_file) = control.path.as_ref() {
if let Err(e) = std::fs::write(config_file, cfg.dump()) {
tracing::warn!(
"failed to write config file {}: {}",
config_file.display(),
e
);
control.set_read_only(true);
}
}
}
self.manager.run_network_instance(cfg, true, control)?;
println!("instance {} started", id);
Ok(resp)
}
async fn retain_network_instance(
@@ -125,12 +172,79 @@ impl WebClientService for InstanceManageRpcService {
_: BaseController,
req: DeleteNetworkInstanceRequest,
) -> Result<DeleteNetworkInstanceResponse, rpc_types::error::Error> {
let remain_inst_ids = self
let inst_ids: HashSet<uuid::Uuid> = req.inst_ids.into_iter().map(Into::into).collect();
let inst_ids = self
.manager
.delete_network_instance(req.inst_ids.into_iter().map(Into::into).collect())?;
.iter()
.filter(|v| inst_ids.contains(v.key()))
.filter(|v| v.get_config_file_control().is_deletable())
.map(|v| *v.key())
.collect::<Vec<_>>();
let config_files = inst_ids
.iter()
.filter_map(|id| {
self.manager
.get_instance_config_control(id)
.and_then(|control| control.path.clone())
})
.collect::<Vec<_>>();
let remain_inst_ids = self.manager.delete_network_instance(inst_ids)?;
println!("instance {:?} retained", remain_inst_ids);
for config_file in config_files {
if let Err(e) = std::fs::remove_file(&config_file) {
tracing::warn!(
"failed to remove config file {}: {}",
config_file.display(),
e
);
}
}
Ok(DeleteNetworkInstanceResponse {
remain_inst_ids: remain_inst_ids.into_iter().map(Into::into).collect(),
})
}
async fn get_network_instance_config(
&self,
_: BaseController,
req: GetNetworkInstanceConfigRequest,
) -> Result<GetNetworkInstanceConfigResponse, rpc_types::error::Error> {
let inst_id: uuid::Uuid = req
.inst_id
.ok_or_else(|| anyhow::anyhow!("instance id is required"))?
.into();
let config = self
.manager
.get_instance_service(&inst_id)
.ok_or_else(|| anyhow::anyhow!("instance service not found"))?
.get_config_service()
.get_config(BaseController::default(), GetConfigRequest::default())
.await?
.config;
Ok(GetNetworkInstanceConfigResponse { config })
}
async fn list_network_instance_meta(
&self,
_: BaseController,
req: ListNetworkInstanceMetaRequest,
) -> Result<ListNetworkInstanceMetaResponse, rpc_types::error::Error> {
let mut metas = Vec::with_capacity(req.inst_ids.len());
for inst_id in req.inst_ids {
let inst_id: uuid::Uuid = (inst_id).into();
let Some(control) = self.manager.get_instance_config_control(&inst_id) else {
continue;
};
let Some(name) = self.manager.get_network_instance_name(&inst_id) else {
continue;
};
let meta = NetworkMeta {
inst_id: Some(inst_id.into()),
network_name: name,
config_permission: control.permission.into(),
};
metas.push(meta);
}
Ok(ListNetworkInstanceMetaResponse { metas })
}
}

View File

@@ -79,18 +79,23 @@ fn get_instance_service(
let id = if let Some(api::instance::instance_identifier::Selector::Id(id)) = selector {
(*id).into()
} else {
let ids = instance_manager.filter_network_instance(|_, i| {
if let Some(api::instance::instance_identifier::Selector::InstanceSelector(selector)) =
selector
{
if let Some(name) = selector.name.as_ref() {
if i.get_inst_name() != *name {
return false;
let ids = instance_manager
.iter()
.filter(|v| {
if let Some(api::instance::instance_identifier::Selector::InstanceSelector(
selector,
)) = selector
{
if let Some(name) = selector.name.as_ref() {
if v.get_inst_name() != *name {
return false;
}
}
}
}
true
});
true
})
.map(|v| *v.key())
.collect::<Vec<_>>();
match ids.len() {
0 => return Err(anyhow::anyhow!("No instance matches the selector")),
1 => ids[0],

View File

@@ -40,6 +40,7 @@ where
&self,
identify: T,
config: NetworkConfig,
save: bool,
) -> Result<(), RemoteClientError<E>> {
let client = self
.get_rpc_client(identify.clone())
@@ -50,18 +51,21 @@ where
RunNetworkInstanceRequest {
inst_id: None,
config: Some(config.clone()),
overwrite: true,
},
)
.await?;
self.get_storage()
.insert_or_update_user_network_config(
identify,
resp.inst_id.unwrap_or_default().into(),
config,
)
.await
.map_err(RemoteClientError::PersistentError)?;
if save {
self.get_storage()
.insert_or_update_user_network_config(
identify,
resp.inst_id.unwrap_or_default().into(),
config,
)
.await
.map_err(RemoteClientError::PersistentError)?;
}
Ok(())
}
@@ -156,13 +160,17 @@ where
let client = self
.get_rpc_client(identify.clone())
.ok_or(RemoteClientError::ClientNotFound)?;
let cfg = self
.get_storage()
.update_network_config_state(identify, inst_id, disabled)
.await
.map_err(RemoteClientError::PersistentError)?;
.handle_get_network_config(identify.clone(), inst_id)
.await?;
if disabled {
self.get_storage()
.insert_or_update_user_network_config(identify.clone(), inst_id, cfg.clone())
.await
.map_err(RemoteClientError::PersistentError)?;
client
.delete_network_instance(
BaseController::default(),
@@ -177,15 +185,18 @@ where
BaseController::default(),
RunNetworkInstanceRequest {
inst_id: Some(inst_id.into()),
config: Some(
cfg.get_network_config()
.map_err(RemoteClientError::PersistentError)?,
),
config: Some(cfg),
overwrite: true,
},
)
.await?;
}
self.get_storage()
.update_network_config_state(identify, inst_id, disabled)
.await
.map_err(RemoteClientError::PersistentError)?;
Ok(())
}
@@ -196,14 +207,38 @@ where
) -> Result<GetNetworkMetasResponse, RemoteClientError<E>> {
let mut metas = std::collections::HashMap::new();
if let Some(client) = self.get_rpc_client(identify.clone()) {
if let Ok(resp) = client
.list_network_instance_meta(
BaseController::default(),
ListNetworkInstanceMetaRequest {
inst_ids: inst_ids.iter().cloned().map(|id| id.into()).collect(),
},
)
.await
{
for meta in resp.metas {
if let Some(inst_id) = meta.inst_id.as_ref() {
let inst_id: uuid::Uuid = (*inst_id).into();
metas.insert(inst_id, meta);
}
}
}
}
for instance_id in inst_ids {
if metas.contains_key(&instance_id) {
continue;
}
let config = self
.handle_get_network_config(identify.clone(), instance_id)
.await?;
metas.insert(
instance_id,
NetworkMeta {
instance_name: config.network_name.unwrap_or_default(),
inst_id: Some(instance_id.into()),
network_name: config.network_name.unwrap_or_default(),
config_permission: 0,
},
);
}
@@ -233,6 +268,22 @@ where
identify: T,
inst_id: uuid::Uuid,
) -> Result<NetworkConfig, RemoteClientError<E>> {
if let Some(client) = self.get_rpc_client(identify.clone()) {
if let Ok(resp) = client
.get_network_instance_config(
BaseController::default(),
GetNetworkInstanceConfigRequest {
inst_id: Some(inst_id.into()),
},
)
.await
{
if let Some(config) = resp.config {
return Ok(config);
}
}
}
let inst_id = inst_id.to_string();
let db_row = self
@@ -277,11 +328,6 @@ pub struct ListNetworkInstanceIdsJsonResp {
disabled_inst_ids: Vec<crate::proto::common::Uuid>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct NetworkMeta {
instance_name: String,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct GetNetworkMetasResponse {
metas: std::collections::HashMap<uuid::Uuid, NetworkMeta>,
@@ -312,7 +358,7 @@ where
identify: T,
network_inst_id: Uuid,
disabled: bool,
) -> Result<C, E>;
) -> Result<(), E>;
async fn list_network_configs(&self, identify: T, props: ListNetworkProps)
-> Result<Vec<C>, E>;

View File

@@ -38,7 +38,8 @@ async fn test_route_peer_info_ipv6() {
global_ctx.set_ipv6(Some(ipv6_cidr));
// Create RoutePeerInfo with IPv6 support
let updated_info = RoutePeerInfo::new_updated_self(123, 456, &global_ctx);
let peer_info = RoutePeerInfo::new();
let updated_info = peer_info.update_self(123, 456, &global_ctx);
// Verify IPv6 address is included
assert!(updated_info.ipv6_addr.is_some());

View File

@@ -2,6 +2,7 @@
use core::panic;
use std::{
future::Future,
sync::{atomic::AtomicU32, Arc},
time::Duration,
};
@@ -1950,6 +1951,107 @@ pub async fn acl_rule_test_subnet_proxy(
drop_insts(insts).await;
}
async fn assert_panics_ext<F, Fut>(f: F, expect_panic: bool)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future + Send + 'static,
{
// Run the async function in a separate task so panics surface as JoinError
let res = tokio::spawn(async move {
f().await;
})
.await;
if expect_panic {
assert!(
res.is_err() && res.as_ref().unwrap_err().is_panic(),
"Expected function to panic, but it didn't",
);
} else {
assert!(res.is_ok(), "Expected function not to panic, but it did");
}
}
#[rstest::rstest]
#[tokio::test]
#[serial_test::serial]
pub async fn p2p_only_test(
#[values(true, false)] has_p2p_conn: bool,
#[values(true, false)] enable_kcp_proxy: bool,
#[values(true, false)] enable_quic_proxy: bool,
) {
use crate::peers::tests::wait_route_appear_with_cost;
let insts = init_three_node_ex(
"udp",
|cfg| {
if cfg.get_inst_name() == "inst1" {
let mut flags = cfg.get_flags();
flags.enable_kcp_proxy = enable_kcp_proxy;
flags.enable_quic_proxy = enable_quic_proxy;
flags.disable_p2p = true;
flags.p2p_only = true;
cfg.set_flags(flags);
} else if cfg.get_inst_name() == "inst3" {
// 添加子网代理配置
cfg.add_proxy_cidr("10.1.2.0/24".parse().unwrap(), None)
.unwrap();
}
cfg
},
false,
)
.await;
if has_p2p_conn {
insts[2]
.get_conn_manager()
.add_connector(RingTunnelConnector::new(
format!("ring://{}", insts[0].id()).parse().unwrap(),
));
wait_route_appear_with_cost(
insts[2].get_peer_manager(),
insts[0].get_peer_manager().my_peer_id(),
Some(1),
)
.await
.unwrap();
}
let target_ip = "10.1.2.4";
for target_ip in ["10.144.144.3", target_ip] {
assert_panics_ext(
|| async {
subnet_proxy_test_icmp(target_ip).await;
},
!has_p2p_conn,
)
.await;
let listen_ip = if target_ip == "10.144.144.3" {
"0.0.0.0"
} else {
"10.1.2.4"
};
assert_panics_ext(
|| async {
subnet_proxy_test_tcp(listen_ip, target_ip).await;
},
!has_p2p_conn,
)
.await;
assert_panics_ext(
|| async {
subnet_proxy_test_udp(listen_ip, target_ip).await;
},
!has_p2p_conn,
)
.await;
}
}
#[rstest::rstest]
#[tokio::test]
#[serial_test::serial]

View File

@@ -35,4 +35,8 @@ impl Controller {
pub fn get_rpc_service(&self) -> InstanceManageRpcService {
InstanceManageRpcService::new(self.manager.clone())
}
pub(super) fn notify_manager_stopping(&self) {
self.manager.notify_stop_check();
}
}

View File

@@ -1,9 +1,17 @@
use std::sync::Arc;
use crate::{
common::scoped_task::ScopedTask, instance_manager::NetworkInstanceManager,
tunnel::TunnelConnector,
common::{
config::TomlConfigLoader, global_ctx::GlobalCtx, scoped_task::ScopedTask,
set_default_machine_id, stun::MockStunInfoCollector,
},
connector::create_connector_by_url,
instance_manager::{NetworkInstanceManager, WebClientGuard},
proto::common::NatType,
tunnel::{IpVersion, TunnelConnector},
};
use anyhow::{Context as _, Result};
use url::Url;
pub mod controller;
pub mod session;
@@ -11,6 +19,7 @@ pub mod session;
pub struct WebClient {
controller: Arc<controller::Controller>,
tasks: ScopedTask<()>,
manager_guard: WebClientGuard,
}
impl WebClient {
@@ -20,6 +29,7 @@ impl WebClient {
hostname: H,
manager: Arc<NetworkInstanceManager>,
) -> Self {
let manager_guard = manager.register_web_client();
let controller = Arc::new(controller::Controller::new(
token.to_string(),
hostname.to_string(),
@@ -31,7 +41,11 @@ impl WebClient {
Self::routine(controller_clone, Box::new(connector)).await;
}));
WebClient { controller, tasks }
WebClient {
controller,
tasks,
manager_guard,
}
}
async fn routine(
@@ -58,3 +72,90 @@ impl WebClient {
}
}
}
pub async fn run_web_client(
config_server_url_s: &str,
machine_id: Option<String>,
hostname: Option<String>,
manager: Arc<NetworkInstanceManager>,
) -> Result<WebClient> {
set_default_machine_id(machine_id);
let config_server_url = match Url::parse(config_server_url_s) {
Ok(u) => u,
Err(_) => format!(
"udp://config-server.easytier.cn:22020/{}",
config_server_url_s
)
.parse()
.unwrap(),
};
let mut c_url = config_server_url.clone();
c_url.set_path("");
let token = config_server_url
.path_segments()
.and_then(|mut x| x.next())
.map(|x| percent_encoding::percent_decode_str(x).decode_utf8())
.transpose()
.with_context(|| "failed to decode config server token")?
.map(|x| x.to_string())
.unwrap_or_default();
if token.is_empty() {
return Err(anyhow::anyhow!("empty token"));
}
let config = TomlConfigLoader::default();
let global_ctx = Arc::new(GlobalCtx::new(config));
global_ctx.replace_stun_info_collector(Box::new(MockStunInfoCollector {
udp_nat_type: NatType::Unknown,
}));
let mut flags = global_ctx.get_flags();
flags.bind_device = false;
global_ctx.set_flags(flags);
let hostname = match hostname {
None => gethostname::gethostname().to_string_lossy().to_string(),
Some(hostname) => hostname,
};
Ok(WebClient::new(
create_connector_by_url(c_url.as_str(), &global_ctx, IpVersion::Both).await?,
token.to_string(),
hostname,
manager.clone(),
))
}
#[cfg(test)]
mod tests {
use std::sync::{atomic::AtomicBool, Arc};
use crate::instance_manager::NetworkInstanceManager;
#[tokio::test]
async fn test_manager_wait() {
let manager = Arc::new(NetworkInstanceManager::new());
let client = super::run_web_client(
format!("ring://{}/test", uuid::Uuid::new_v4()).as_str(),
None,
None,
manager.clone(),
)
.await
.unwrap();
let sleep_finish = Arc::new(AtomicBool::new(false));
let sleep_finish_clone = sleep_finish.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
println!("Dropping client...");
sleep_finish_clone.store(true, std::sync::atomic::Ordering::Relaxed);
drop(client);
println!("Client dropped.");
});
println!("Waiting for manager...");
manager.wait().await;
assert!(sleep_finish.load(std::sync::atomic::Ordering::Relaxed));
println!("Manager stopped.");
}
}

View File

@@ -269,6 +269,7 @@ no_tun = false
use_smoltcp = false
foreign_network_whitelist = "*"
disable_p2p = false
p2p_only = false
relay_all_peer_rpc = false
disable_udp_hole_punching = false