Compare commits

...
5 Commits
Author SHA1 Message Date
lbl 10eb97e403 chore: 修正 LICENSE 版权声明并补充各清单的 license 字段
- LICENSE 附录版权行由模板占位符改为 Copyright 2023-present vnt-dev
- 各 Cargo.toml 与 package.json 补充 license = Apache-2.0
2026-08-23 14:42:18 +08:00
lbl 7133906ab2 docs: 重写 README,降低普通用户使用门槛
- 快速开始改为两步走:选择程序、填写组网编号与服务端地址
- 新增程序选择对照表、常见问题与安全说明章节
- 说明设置密码后为端到端加密,服务端无法解密通信内容
- 开发者内容(前端构建、桌面端开发)下沉至独立章节
2026-08-23 14:32:58 +08:00
lbl fedba5c8e2 仅当 no_tun = true 时提示迁移,no_tun = false 静默忽略 2026-08-23 14:17:22 +08:00
lbl c2fd1ff870 refactor: remove panic-prone unwrap and expect calls 2026-08-23 12:04:40 +08:00
lbl 2100ec1ca5 fix: retry server network changes periodically 2026-08-23 11:53:39 +08:00
31 changed files with 282 additions and 200 deletions
+1
View File
@@ -2,6 +2,7 @@
name = "vnt2" name = "vnt2"
version = "2.0.2" version = "2.0.2"
edition = "2024" edition = "2024"
license = "Apache-2.0"
[dependencies] [dependencies]
vnt-ipc = { path = "vnt-ipc", optional = true } vnt-ipc = { path = "vnt-ipc", optional = true }
+1 -1
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright [yyyy] [name of copyright owner] Copyright 2023-present vnt-dev
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
+90 -61
View File
@@ -1,74 +1,68 @@
# VNT # VNT
一个简单、高效、能快速组建虚拟局域网的工具 一个简单、高效、能快速组建虚拟局域网的工具。无论设备身在何处,只要接入同一个虚拟网络,即可像在同一局域网内一样互相访问,适用于远程桌面、联机游戏、访问家中 NAS、跨地区协作等场景。
# 快速开始 # 快速开始
### 简单说明 ## 第一步:选择适合的程序
1. vnt2_cli 是一个纯命令行组网工具,可以从命令行参数或配置文件快速启动组网
2. vnt2_ctrl 和vnt2_cli搭配使用,vnt2_cli后台运行时,可以用vnt2_ctrl来获取组网状态
3. vnt2_web 是一个集成web服务的组网工具,带web页面,可以在页面上操作组网
4. `vnt-desktop` 是基于 Tauri 2 的 PC 客户端,内置组网服务、桌面工作台和系统托盘
## 使用vnt2_cli组网 | 程序 | 适用对象 | 特点 |
| --- | --- | --- |
| **VNT 桌面客户端** | 一般用户(推荐) | 图形化界面,操作简单,Windows 安装包开箱即用 |
| **vnt2_web** | 偏好网页管理的用户 | 启动后通过浏览器操作,适合无桌面环境的服务器/NAS |
| **vnt2_cli + vnt2_ctrl** | 进阶用户 | 纯命令行,适合脚本化、自动化部署 |
各平台的安装包请到 [GitHub Releases](https://github.com/vnt-dev/vnt/releases) 下载。
## 第二步:组网(以桌面客户端为例)
1. 安装并打开 VNT 桌面客户端。
2. 新建一个组网配置,只需填写两项:
- **组网编号(-k)**:用于标识虚拟网络的编号,需要互联的设备须填写相同的编号,例如 `123456`
- **服务端地址(-s)**:可使用公共服务端 `101.35.230.139:6660`,也可填写自行部署的服务端。
3. 点击启动。所有使用**相同组网编号 + 相同服务端**的设备会自动组成一个虚拟局域网,每台设备会分配到虚拟 IP(网段由服务端设置)。
4. 验证:在设备 A 上 `ping` 设备 B 的虚拟 IP,能 ping 通即表示组网成功。
> 其他设备(电脑、服务器、手机)使用同样的组网编号和服务端地址加入即可,设备数量不限。
## 用 vnt2_web 组网
1. 启动程序:
```
./vnt2_web
```
2. 从启动日志复制带 `?token=...` 的访问地址(默认监听 `127.0.0.1:19099`);也可以通过 `--token` 或 `VNT_WEB_TOKEN` 指定固定令牌。
3. 在网页上添加组网配置(同样只需组网编号和服务端地址),再点击启动组网。
## 用 vnt2_cli 组网
使用方式和vnt1.0一样,只是增减了一些功能,具体参数请查看 -h
``` ```
# 启动程序 服务端可以使用101.35.230.139:6660 # 启动组网
./vnt2_cli -k 123456 -s 101.35.230.139:6660 ./vnt2_cli -k 123456 -s 101.35.230.139:6660
``` ```
``` ```
# 查看组网信息 # vnt2_cli 后台运行时,使用 vnt2_ctrl 查看组网状态
./vnt2_ctrl info ./vnt2_ctrl info
``` ```
## 使用vnt2_web组网
1. 启动程序 更多参数请查看 `./vnt2_cli -h`。
``` ## 常见问题
# 启动程序
./vnt2_web
```
2. 从启动日志复制带 `?token=...` 的 Web 访问地址;也可以通过 `--token` 或 `VNT_WEB_TOKEN` 指定固定令牌
3. 在页面上添加组网配置,再启动组网
## 前端构建 - **Windows 上提示需要管理员权限?** 创建虚拟网卡需要以管理员身份运行程序,右键选择"以管理员身份运行"即可。
- **ping 不通对方?** 请确认两边组网编号、服务端地址完全一致,且均已启动组网;防火墙可能拦截 ICMP,可使用共享文件夹等实际业务进行验证。
- **没有公网服务器?** 可先使用公共服务端 `101.35.230.139:6660` 体验;正式使用建议自建服务端,以获得更好的稳定性和私密性。
- **不希望修改系统网络设置?** 可将设备模式设为 `no`(无网卡模式),仅提供流量出口和端口映射,无需管理员权限。
web 前端源码位于 `vnt-web/ui/`(Vite + Vue 3 + Pinia + Tailwind CSS v4),构建产物输出到 `vnt-web/static/`,由 RustEmbed 嵌入二进制。 ## 安全说明
项目使用根级 pnpm workspace 统一管理 Web 与桌面前端依赖: - **建议设置组网密码**。设置密码后(命令行 `-p` / `--password`,或配置文件中的 `password`),节点之间的数据采用端到端加密(ChaCha20-Poly1305),**服务端仅负责转发密文,无法解密通信内容**。即使使用公共服务端,通信内容也不会泄露给服务端。
- 同一虚拟网络内的所有设备必须使用**相同的密码**,否则无法互相通信。
- 未设置密码时,节点间数据不加密,经过服务端中继的流量理论上可被服务端查看,请仅在可信网络环境下省略密码。
- 此外,客户端与服务端之间的连接本身支持 tcp-tls、quic、wss 等加密传输协议,并可绑定服务端证书,防止伪造服务端攻击。
``` # 进阶说明
pnpm install
pnpm build:web
```
开发调试使用 `pnpm dev:web`Vite dev server 会将 `/api` 代理到 `127.0.0.1:19099`。启动 `vnt2_web` 时应指定令牌,并在浏览器登录页输入相同令牌。
## PC 客户端
桌面客户端源码位于 `vnt-desktop/`Tauri 2 + Vue 3)。桌面工作台通过 Tauri IPC 直接调用进程内 `vnt-core`;需要浏览器访问时,可在“Web 访问”中按需启用同进程 HTTP 服务,无需单独运行 `vnt2_web`。Tauri 与 Web 端统一使用 `vnt-web/ui/src/` 下的同一套响应式前端代码。
```
pnpm install
pnpm dev:desktop
```
构建安装包使用 `pnpm build:desktop`。更多说明见 `vnt-desktop/README.md`。
# VNT2.0新特性
1. 提升安全性,支持tcp-tls、quic、wss协议连接服务器,和服务端强制使用tls加密,并支持证书绑定,防止伪造服务端攻击
2. 提升流量稳定性,支持使用quic代理流量,支持FEC冗余传输
3. 简化操作,去除了大量vnt1.0的重复和无用的配置参数
4. vnt-link、vnt合二为一
5. 支持无网卡、TUN(三层)和 TAP(二层)模式及端口映射;三种模式的 IPv4 流量可互通
6. 全功能的情况下,减少程序体积
7. 性能提升,支持linux-offload
8. 更规范的api接入,可以轻松自定义客户端
9. 支持同时连接多个服务端,可以容灾和负载均衡
## 虚拟网卡模式 ## 虚拟网卡模式
@@ -80,18 +74,53 @@ pnpm dev:desktop
Linux 和 macOS 使用系统提供的 TUN/TAP 能力。Windows 的 TUN 模式使用随程序提供的 `wintun.dll`;TAP 模式需要管理员权限并预先安装 `tap-windows`(硬件 ID `tap0901`)。Android VpnService 仅支持 TUN。 Linux 和 macOS 使用系统提供的 TUN/TAP 能力。Windows 的 TUN 模式使用随程序提供的 `wintun.dll`;TAP 模式需要管理员权限并预先安装 `tap-windows`(硬件 ID `tap0901`)。Android VpnService 仅支持 TUN。
## VNT2.0 新特性
1. 提升安全性,支持 tcp-tls、quic、wss 协议连接服务器,和服务端强制使用 tls 加密,并支持证书绑定,防止伪造服务端攻击
2. 提升流量稳定性,支持使用 quic 代理流量,支持 FEC 冗余传输
3. 简化操作,去除了大量 vnt1.0 的重复和无用的配置参数
4. vnt-link、vnt 合二为一
5. 支持无网卡、TUN(三层)和 TAP(二层)模式及端口映射;三种模式的 IPv4 流量可互通
6. 全功能的情况下,减少程序体积
7. 性能提升,支持 linux-offload
8. 更规范的 api 接入,可以轻松自定义客户端
9. 支持同时连接多个服务端,可以容灾和负载均衡
# 开发者
## 前端构建
web 前端源码位于 `vnt-web/ui/`Vite + Vue 3 + Pinia + Tailwind CSS v4),构建产物输出到 `vnt-web/static/`,由 RustEmbed 嵌入二进制。
项目使用根级 pnpm workspace 统一管理 Web 与桌面前端依赖:
```
pnpm install
pnpm build:web
```
开发调试使用 `pnpm dev:web`Vite dev server 会将 `/api` 代理到 `127.0.0.1:19099`。启动 `vnt2_web` 时应指定令牌,并在浏览器登录页输入相同令牌。
## PC 客户端开发
桌面客户端源码位于 `vnt-desktop/`Tauri 2 + Vue 3)。桌面工作台通过 Tauri IPC 直接调用进程内 `vnt-core`;需要浏览器访问时,可在"Web 访问"中按需启用同进程 HTTP 服务,无需单独运行 `vnt2_web`。Tauri 与 Web 端统一使用 `vnt-web/ui/src/` 下的同一套响应式前端代码。
```
pnpm install
pnpm dev:desktop
```
构建安装包使用 `pnpm build:desktop`。更多说明见 `vnt-desktop/README.md`。
# 说明 # 说明
vnt2.0整体重构了一遍,和1.0不兼容,同时也可能引入新的bug欢迎反馈 vnt2.0 整体重构了一遍,和 1.0 不兼容,欢迎反馈
其他平台后续再推出 其他平台后续再推出
### 相关库 ### 相关库
1. tun虚拟网卡(https://github.com/tun-rs/tun-rs) 1. tun 虚拟网卡https://github.com/tun-rs/tun-rs
2. 路由设置(https://github.com/tun-rs/route_manager) 2. 路由设置https://github.com/tun-rs/route_manager
3. 用户态协议栈(用于quic代理和无tun模式出口)(https://github.com/rustp2p/tcp_ip) 3. 用户态协议栈用于 quic 代理和无 tun 模式出口)(https://github.com/rustp2p/tcp_ip
4. 打洞通道处理(https://github.com/rustp2p/rustp2p/tree/master/rustp2p-core) 4. 打洞通道处理https://github.com/rustp2p/rustp2p/tree/master/rustp2p-core
5. 安卓端2.0已发布(https://github.com/vnt-dev/VntApp
+1
View File
@@ -1,6 +1,7 @@
{ {
"name": "vnt-workspace", "name": "vnt-workspace",
"private": true, "private": true,
"license": "Apache-2.0",
"packageManager": "[email protected]", "packageManager": "[email protected]",
"scripts": { "scripts": {
"dev:web": "pnpm --filter vnt-web-ui dev", "dev:web": "pnpm --filter vnt-web-ui dev",
+10 -3
View File
@@ -181,7 +181,7 @@ pub fn build_config_from_args_and_file(
) -> anyhow::Result<(Config, CtrlConfig)> { ) -> anyhow::Result<(Config, CtrlConfig)> {
if file if file
.as_ref() .as_ref()
.is_some_and(|config| config.legacy_no_tun.is_some()) .is_some_and(|config| config.legacy_no_tun == Some(true))
{ {
return Err(anyhow!( return Err(anyhow!(
"configuration key 'no_tun' was removed; use device_mode = \"no|tun|tap\"" "configuration key 'no_tun' was removed; use device_mode = \"no|tun|tap\""
@@ -323,7 +323,7 @@ fn build_from_args_only(args: Args) -> anyhow::Result<(Config, CtrlConfig)> {
} }
fn build_from_file_only(file: FileConfig) -> anyhow::Result<(Config, CtrlConfig)> { fn build_from_file_only(file: FileConfig) -> anyhow::Result<(Config, CtrlConfig)> {
if file.legacy_no_tun.is_some() { if file.legacy_no_tun == Some(true) {
return Err(anyhow!( return Err(anyhow!(
"configuration key 'no_tun' was removed; use device_mode = \"no|tun|tap\"" "configuration key 'no_tun' was removed; use device_mode = \"no|tun|tap\""
)); ));
@@ -546,9 +546,16 @@ mod tests {
let legacy: FileConfig = toml::from_str("no_tun = true").unwrap(); let legacy: FileConfig = toml::from_str("no_tun = true").unwrap();
let error = match build_config_from_args_and_file(None, Some(legacy)) { let error = match build_config_from_args_and_file(None, Some(legacy)) {
Err(error) => error, Err(error) => error,
Ok(_) => panic!("legacy no_tun must be rejected"), Ok(_) => panic!("legacy no_tun = true must be rejected"),
}; };
assert!(error.to_string().contains("device_mode")); assert!(error.to_string().contains("device_mode"));
let legacy_false: FileConfig = toml::from_str(
"no_tun = false\nserver = [\"quic://127.0.0.1:29872\"]\nnetwork_code = \"test\"",
)
.unwrap();
build_config_from_args_and_file(None, Some(legacy_false))
.expect("legacy no_tun = false should be ignored");
} }
#[test] #[test]
+1
View File
@@ -2,6 +2,7 @@
name = "vnt-core" name = "vnt-core"
version = "2.0.2" version = "2.0.2"
edition = "2024" edition = "2024"
license = "Apache-2.0"
[dependencies] [dependencies]
+11 -12
View File
@@ -1,4 +1,4 @@
fn main() { fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut config = prost_build::Config::new(); let mut config = prost_build::Config::new();
match protoc_bin_vendored::protoc_bin_path() { match protoc_bin_vendored::protoc_bin_path() {
@@ -14,15 +14,14 @@ fn main() {
config.protoc_arg("--experimental_allow_proto3_optional"); config.protoc_arg("--experimental_allow_proto3_optional");
config config.compile_protos(
.compile_protos( &[
&[ "proto/control_message.proto",
"proto/control_message.proto", "proto/rpc.proto",
"proto/rpc.proto", "proto/client.proto",
"proto/client.proto", "proto/fec.proto",
"proto/fec.proto", ],
], &["proto"],
&["proto"], )?;
) Ok(())
.unwrap();
} }
+2 -2
View File
@@ -79,7 +79,7 @@ impl NetworkManager {
log::info!("绑定出口网卡: {name}"); log::info!("绑定出口网卡: {name}");
} }
let mtu = config.mtu.unwrap_or(DEFAULT_MTU); let mtu = config.mtu.unwrap_or(DEFAULT_MTU);
let packet_crypto = PacketCrypto::new_from_str(config.password.as_deref()); let packet_crypto = PacketCrypto::new_from_str(config.password.as_deref())?;
let packet_compression = PacketCompression::new(config.compress); let packet_compression = PacketCompression::new(config.compress);
let (server_manager_list, tunnel_to_server, server_rpc) = create_server_tunnel( let (server_manager_list, tunnel_to_server, server_rpc) = create_server_tunnel(
app_state.clone(), app_state.clone(),
@@ -163,7 +163,7 @@ impl NetworkManager {
EnhancedTunInbound::Nat( EnhancedTunInbound::Nat(
internal_nat_inbound internal_nat_inbound
.clone() .clone()
.expect("internal_nat_inbound must be Some in no-device mode"), .context("internal NAT is unavailable in no-device mode")?,
), ),
None, None,
), ),
+14 -13
View File
@@ -33,15 +33,16 @@ impl PacketCrypto {
.map(|b| format!("{:02x}", b)) .map(|b| format!("{:02x}", b))
.collect::<String>() .collect::<String>()
} }
pub fn new(key_bytes: [u8; 32]) -> Self { pub fn new(key_bytes: [u8; 32]) -> io::Result<Self> {
let unbound = UnboundKey::new(&CHACHA20_POLY1305, &key_bytes).unwrap(); let unbound = UnboundKey::new(&CHACHA20_POLY1305, &key_bytes)
.map_err(|_| io::Error::other("failed to initialize ChaCha20-Poly1305 key"))?;
let key = LessSafeKey::new(unbound); let key = LessSafeKey::new(unbound);
Self { Ok(Self {
key, key,
seq: Arc::new(AtomicU32::new(rand::random())), seq: Arc::new(AtomicU32::new(rand::random())),
} })
} }
pub fn new_from_str(s: &str) -> Self { pub fn new_from_str(s: &str) -> io::Result<Self> {
let hash = ring::digest::digest(&ring::digest::SHA256, s.as_bytes()); let hash = ring::digest::digest(&ring::digest::SHA256, s.as_bytes());
let mut key_bytes = [0u8; 32]; let mut key_bytes = [0u8; 32];
key_bytes.copy_from_slice(hash.as_ref()); key_bytes.copy_from_slice(hash.as_ref());
@@ -161,7 +162,7 @@ mod tests {
#[test] #[test]
fn test_encrypt_decrypt_in_place() { fn test_encrypt_decrypt_in_place() {
let key = [7u8; 32]; let key = [7u8; 32];
let crypto = PacketCrypto::new(key); let crypto = PacketCrypto::new(key).unwrap();
let payload_len = 20; let payload_len = 20;
let mut pkt = build_test_packet(payload_len); let mut pkt = build_test_packet(payload_len);
@@ -198,7 +199,7 @@ mod tests {
#[test] #[test]
fn test_nonce_unique_per_packet() { fn test_nonce_unique_per_packet() {
let crypto = PacketCrypto::new([7u8; 32]); let crypto = PacketCrypto::new([7u8; 32]).unwrap();
let mut pkt1 = build_test_packet(20); let mut pkt1 = build_test_packet(20);
let mut pkt2 = build_test_packet(20); let mut pkt2 = build_test_packet(20);
@@ -227,7 +228,7 @@ mod tests {
#[test] #[test]
fn test_clone_shares_seq_counter() { fn test_clone_shares_seq_counter() {
let crypto = PacketCrypto::new([9u8; 32]); let crypto = PacketCrypto::new([9u8; 32]).unwrap();
let cloned = crypto.clone(); let cloned = crypto.clone();
let mut pkt1 = build_test_packet(8); let mut pkt1 = build_test_packet(8);
@@ -243,9 +244,9 @@ mod tests {
#[test] #[test]
fn test_cross_version_compat() { fn test_cross_version_compat() {
let key = [7u8; 32]; let key = [7u8; 32];
let crypto = PacketCrypto::new(key); let crypto = PacketCrypto::new(key).unwrap();
// 用相同密钥的另一个实例模拟对端 // 用相同密钥的另一个实例模拟对端
let peer = PacketCrypto::new(key); let peer = PacketCrypto::new(key).unwrap();
// 模拟旧版本发包:seq 固定为 0,nonce 直接由头部计算 // 模拟旧版本发包:seq 固定为 0,nonce 直接由头部计算
let mut pkt = build_test_packet(20); let mut pkt = build_test_packet(20);
@@ -281,7 +282,7 @@ mod tests {
/// 必须导致解密失败,而不是被静默接受。 /// 必须导致解密失败,而不是被静默接受。
#[test] #[test]
fn test_tampered_flags_rejected() { fn test_tampered_flags_rejected() {
let crypto = PacketCrypto::new([7u8; 32]); let crypto = PacketCrypto::new([7u8; 32]).unwrap();
let mut pkt = build_test_packet(20); let mut pkt = build_test_packet(20);
crypto.encrypt_in_place(&mut pkt).expect("encrypt failed"); crypto.encrypt_in_place(&mut pkt).expect("encrypt failed");
@@ -298,7 +299,7 @@ mod tests {
/// AAD 覆盖 msg_type(byte0):中间人篡改消息类型必须导致解密失败。 /// AAD 覆盖 msg_type(byte0):中间人篡改消息类型必须导致解密失败。
#[test] #[test]
fn test_tampered_msg_type_rejected() { fn test_tampered_msg_type_rejected() {
let crypto = PacketCrypto::new([7u8; 32]); let crypto = PacketCrypto::new([7u8; 32]).unwrap();
let mut pkt = build_test_packet(20); let mut pkt = build_test_packet(20);
crypto.encrypt_in_place(&mut pkt).expect("encrypt failed"); crypto.encrypt_in_place(&mut pkt).expect("encrypt failed");
@@ -315,7 +316,7 @@ mod tests {
/// 转发后 ttl 变化的包必须仍能正常解密。 /// 转发后 ttl 变化的包必须仍能正常解密。
#[test] #[test]
fn test_ttl_change_still_decrypts() { fn test_ttl_change_still_decrypts() {
let crypto = PacketCrypto::new([7u8; 32]); let crypto = PacketCrypto::new([7u8; 32]).unwrap();
let payload_len = 20; let payload_len = 20;
let mut pkt = build_test_packet(payload_len); let mut pkt = build_test_packet(payload_len);
+6 -6
View File
@@ -16,12 +16,12 @@ impl PacketCrypto {
chacha20_poly1305::PacketCrypto::key_sign(s) chacha20_poly1305::PacketCrypto::key_sign(s)
} }
pub(crate) fn new_from_str(s: Option<&str>) -> Self { pub(crate) fn new_from_str(s: Option<&str>) -> io::Result<Self> {
Self { let crypto = s
crypto: s .map(chacha20_poly1305::PacketCrypto::new_from_str)
.map(chacha20_poly1305::PacketCrypto::new_from_str) .transpose()?
.map(Arc::new), .map(Arc::new);
} Ok(Self { crypto })
} }
pub(crate) fn encrypt_reserve(&self) -> usize { pub(crate) fn encrypt_reserve(&self) -> usize {
if self.crypto.is_some() { TAG_LEN } else { 0 } if self.crypto.is_some() { TAG_LEN } else { 0 }
@@ -145,7 +145,7 @@ async fn create_quic_endpoint(
quinn::crypto::rustls::QuicClientConfig::try_from(client_config) quinn::crypto::rustls::QuicClientConfig::try_from(client_config)
.context("Failed to create QUIC client config")?, .context("Failed to create QUIC client config")?,
)); ));
client_config.transport_config(build_transport_config()); client_config.transport_config(build_transport_config()?);
let mut endpoint_config = EndpointConfig::default(); let mut endpoint_config = EndpointConfig::default();
endpoint_config.max_udp_payload_size(1300)?; endpoint_config.max_udp_payload_size(1300)?;
let mut endpoint = quinn::Endpoint::new_with_abstract_socket( let mut endpoint = quinn::Endpoint::new_with_abstract_socket(
@@ -159,14 +159,18 @@ async fn create_quic_endpoint(
Ok((inbound, endpoint)) Ok((inbound, endpoint))
} }
fn build_transport_config() -> Arc<TransportConfig> { fn build_transport_config() -> anyhow::Result<Arc<TransportConfig>> {
let mut transport = TransportConfig::default(); let mut transport = TransportConfig::default();
transport.congestion_controller_factory(Arc::new(BbrConfig::default())); transport.congestion_controller_factory(Arc::new(BbrConfig::default()));
transport.keep_alive_interval(Some(Duration::from_secs(5))); transport.keep_alive_interval(Some(Duration::from_secs(5)));
transport.max_idle_timeout(Some(Duration::from_secs(10).try_into().unwrap())); transport.max_idle_timeout(Some(
Duration::from_secs(10)
.try_into()
.context("invalid QUIC idle timeout")?,
));
Arc::new(transport) Ok(Arc::new(transport))
} }
async fn ip_stack_recv_task( async fn ip_stack_recv_task(
+4 -4
View File
@@ -85,9 +85,9 @@ async fn handle_outbound(
} else { } else {
// 创建真实 UDP socket // 创建真实 UDP socket
let bind_addr = if dst.is_ipv4() { let bind_addr = if dst.is_ipv4() {
"0.0.0.0:0".parse().expect("valid IPv4 bind address") SocketAddr::from(([0, 0, 0, 0], 0))
} else { } else {
"[::]:0".parse().expect("valid IPv6 bind address") SocketAddr::from(([0; 8], 0))
}; };
let interface = if dst.ip().is_loopback() { let interface = if dst.ip().is_loopback() {
None None
@@ -223,9 +223,9 @@ where
.next() .next()
.context("UDP NAT destination resolved to no address")?; .context("UDP NAT destination resolved to no address")?;
let bind_addr = if destination.is_ipv4() { let bind_addr = if destination.is_ipv4() {
"0.0.0.0:0".parse().expect("valid IPv4 bind address") SocketAddr::from(([0, 0, 0, 0], 0))
} else { } else {
"[::]:0".parse().expect("valid IPv6 bind address") SocketAddr::from(([0; 8], 0))
}; };
let interface = if destination.ip().is_loopback() { let interface = if destination.ip().is_loopback() {
None None
+37 -32
View File
@@ -18,7 +18,7 @@ use crate::protocol::transmission::TransmissionBytes;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use std::io; use std::io;
use zerocopy::byteorder::{NetworkEndian, U32}; use zerocopy::byteorder::{NetworkEndian, U32};
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Ref, Unaligned}; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
#[derive(Debug, FromBytes, IntoBytes, Unaligned, KnownLayout, Immutable)] #[derive(Debug, FromBytes, IntoBytes, Unaligned, KnownLayout, Immutable)]
#[repr(C)] #[repr(C)]
@@ -167,11 +167,6 @@ impl<B: AsRef<[u8]>> NetPacket<B> {
} }
Ok(NetPacket { buffer }) Ok(NetPacket { buffer })
} }
fn header(&self) -> Ref<&[u8], NetHeader> {
// Safe: NetHeader is Unaligned and length is validated in new()
let (header, _) = Ref::<&[u8], NetHeader>::from_prefix(self.buffer.as_ref()).unwrap();
header
}
pub fn buffer(&self) -> &[u8] { pub fn buffer(&self) -> &[u8] {
self.buffer.as_ref() self.buffer.as_ref()
} }
@@ -182,37 +177,40 @@ impl<B: AsRef<[u8]>> NetPacket<B> {
&self.buffer &self.buffer
} }
pub fn msg_type(&self) -> io::Result<MsgType> { pub fn msg_type(&self) -> io::Result<MsgType> {
self.header().msg_type().try_into() (self.buffer.as_ref()[0] & 0x7F).try_into()
} }
pub fn max_ttl(&self) -> u8 { pub fn max_ttl(&self) -> u8 {
self.header().max_ttl() self.buffer.as_ref()[1] >> 4
} }
pub fn ttl(&self) -> u8 { pub fn ttl(&self) -> u8 {
self.header().curr_ttl() self.buffer.as_ref()[1] & 0x0F
} }
pub fn seq(&self) -> u32 { pub fn seq(&self) -> u32 {
self.header().seq.get() let buf = self.buffer.as_ref();
u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]])
} }
pub fn src_id(&self) -> u32 { pub fn src_id(&self) -> u32 {
self.header().src_id.get() let buf = self.buffer.as_ref();
u32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]])
} }
pub fn dest_id(&self) -> u32 { pub fn dest_id(&self) -> u32 {
self.header().dest_id.get() let buf = self.buffer.as_ref();
u32::from_be_bytes([buf[12], buf[13], buf[14], buf[15]])
} }
pub fn is_compressed(&self) -> bool { pub fn is_compressed(&self) -> bool {
(self.header().flags_byte & COMPRESSED) != 0 (self.buffer.as_ref()[2] & COMPRESSED) != 0
} }
pub fn is_gateway(&self) -> bool { pub fn is_gateway(&self) -> bool {
(self.header().flags_byte & GATEWAY) != 0 (self.buffer.as_ref()[2] & GATEWAY) != 0
} }
pub fn is_fec(&self) -> bool { pub fn is_fec(&self) -> bool {
(self.header().flags_byte & FEC) != 0 (self.buffer.as_ref()[2] & FEC) != 0
} }
pub fn is_ethernet(&self) -> bool { pub fn is_ethernet(&self) -> bool {
(self.header().flags_byte & ETHERNET) != 0 (self.buffer.as_ref()[2] & ETHERNET) != 0
} }
pub fn head(&self) -> &[u8] { pub fn head(&self) -> &[u8] {
&self.buffer.as_ref()[..HEAD_LENGTH] &self.buffer.as_ref()[..HEAD_LENGTH]
@@ -223,47 +221,54 @@ impl<B: AsRef<[u8]>> NetPacket<B> {
} }
impl<B: AsRef<[u8]> + AsMut<[u8]>> NetPacket<B> { impl<B: AsRef<[u8]> + AsMut<[u8]>> NetPacket<B> {
fn header_mut(&mut self) -> Ref<&mut [u8], NetHeader> {
// Safe: NetHeader is Unaligned and length is validated in new()
let (header, _) = Ref::<&mut [u8], NetHeader>::from_prefix(self.buffer.as_mut()).unwrap();
header
}
pub fn set_msg_type(&mut self, msg_type: MsgType) { pub fn set_msg_type(&mut self, msg_type: MsgType) {
self.header_mut().set_msg_type(msg_type.into()); self.buffer.as_mut()[0] = (u8::from(msg_type) & 0x7F) | 0x80;
} }
pub fn decr_ttl(&mut self) { pub fn decr_ttl(&mut self) {
self.header_mut().decr_ttl() let ttl_byte = &mut self.buffer.as_mut()[1];
let current = *ttl_byte & 0x0F;
if current != 0 {
*ttl_byte = (*ttl_byte & 0xF0) | (current - 1);
}
} }
pub fn set_ttl(&mut self, ttl: u8) { pub fn set_ttl(&mut self, ttl: u8) {
self.header_mut().set_ttl(ttl, ttl); self.buffer.as_mut()[1] = (ttl << 4) | (ttl & 0x0F);
} }
pub fn set_seq(&mut self, seq: u32) { pub fn set_seq(&mut self, seq: u32) {
self.header_mut().seq.set(seq); self.buffer.as_mut()[4..8].copy_from_slice(&seq.to_be_bytes());
} }
pub fn set_src_id(&mut self, id: u32) { pub fn set_src_id(&mut self, id: u32) {
self.header_mut().src_id.set(id); self.buffer.as_mut()[8..12].copy_from_slice(&id.to_be_bytes());
} }
pub fn set_dest_id(&mut self, id: u32) { pub fn set_dest_id(&mut self, id: u32) {
self.header_mut().dest_id.set(id); self.buffer.as_mut()[12..16].copy_from_slice(&id.to_be_bytes());
}
fn set_flag(&mut self, mask: u8, value: bool) {
let flags = &mut self.buffer.as_mut()[2];
if value {
*flags |= mask;
} else {
*flags &= !mask;
}
} }
pub fn set_compressed_flag(&mut self, compressed: bool) { pub fn set_compressed_flag(&mut self, compressed: bool) {
self.header_mut().set_flag(COMPRESSED, compressed); self.set_flag(COMPRESSED, compressed);
} }
pub fn set_gateway_flag(&mut self, gateway: bool) { pub fn set_gateway_flag(&mut self, gateway: bool) {
self.header_mut().set_flag(GATEWAY, gateway); self.set_flag(GATEWAY, gateway);
} }
pub fn set_fec_flag(&mut self, fec: bool) { pub fn set_fec_flag(&mut self, fec: bool) {
self.header_mut().set_flag(FEC, fec); self.set_flag(FEC, fec);
} }
pub fn set_ethernet_flag(&mut self, ethernet: bool) { pub fn set_ethernet_flag(&mut self, ethernet: bool) {
self.header_mut().set_flag(ETHERNET, ethernet); self.set_flag(ETHERNET, ethernet);
} }
pub fn set_payload(&mut self, data: &[u8]) -> io::Result<()> { pub fn set_payload(&mut self, data: &[u8]) -> io::Result<()> {
+8 -3
View File
@@ -99,12 +99,17 @@ impl DeviceIOManager {
// 保证失败时调用方状态完整、可以重试 // 保证失败时调用方状态完整、可以重试
let device_mode = device_config.device_mode; let device_mode = device_config.device_mode;
let device = Arc::new(create_device(device_config)?); let device = Arc::new(create_device(device_config)?);
let receiver = receiver.take().unwrap(); let Some(receiver_value) = receiver.take() else {
let enhanced_outbound = enhanced_outbound.take().unwrap(); bail!("device task already started");
};
let Some(enhanced_outbound) = enhanced_outbound.take() else {
*receiver = Some(receiver_value);
bail!("device task already started");
};
let task = create( let task = create(
&self.task_group, &self.task_group,
device, device,
receiver.receiver, receiver_value.receiver,
enhanced_outbound, enhanced_outbound,
device_mode, device_mode,
); );
@@ -357,7 +357,9 @@ pub(crate) async fn query_tcp_public_addr_loop(
let addrs: Vec<SocketAddr> = active_connections.keys().cloned().collect(); let addrs: Vec<SocketAddr> = active_connections.keys().cloned().collect();
for addr in addrs { for addr in addrs {
let (tcp_stream, _) = active_connections.get_mut(&addr).unwrap(); let Some((tcp_stream, _)) = active_connections.get_mut(&addr) else {
continue;
};
let mut buf = [0u8; 1024]; let mut buf = [0u8; 1024];
match tcp_stream.try_read(&mut buf) { match tcp_stream.try_read(&mut buf) {
@@ -149,8 +149,11 @@ pub async fn ping_all(
ping.set_ttl(1); ping.set_ttl(1);
ping.set_src_id(src.into()); ping.set_src_id(src.into());
ping.set_dest_id(id.into()); ping.set_dest_id(id.into());
ping.set_payload(&crate::utils::time::now_ts_ms().to_be_bytes()) if let Err(error) = ping.set_payload(&crate::utils::time::now_ts_ms().to_be_bytes())
.unwrap(); {
log::warn!("failed to build route probe: {error}");
continue;
}
let route_key = route.route_key(); let route_key = route.route_key();
if socket_manager.send_to(ping, &route_key).await.is_ok() { if socket_manager.send_to(ping, &route_key).await.is_ok() {
packet_loss_stats.record_sent(id, route_key); packet_loss_stats.record_sent(id, route_key);
@@ -188,13 +188,15 @@ impl ServerTurnManager {
|| reg.prefix_len != initial_response.prefix_len || reg.prefix_len != initial_response.prefix_len
|| reg.gateway != initial_response.gateway || reg.gateway != initial_response.gateway
{ {
// 该服务器分配的虚拟网络与当前不一致,无法重连, // 该服务器分配的虚拟网络与当前不一致,
// 只结束本服务器的任务,不影响其他服务器 // 断开本次连接并降低重试频率,不影响其他服务器
log::error!( log::error!(
"服务器{}虚拟网络发生变化,放弃重连", "服务器{}虚拟网络发生变化,1分钟后重试",
self.config.server_addr self.config.server_addr
); );
break; self.disconnect();
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
continue;
} }
// 保存服务器版本 // 保存服务器版本
if !reg.server_version.is_empty() { if !reg.server_version.is_empty() {
@@ -224,8 +226,6 @@ impl ServerTurnManager {
already_connected = false; already_connected = false;
tokio::time::sleep(std::time::Duration::from_secs(1)).await; tokio::time::sleep(std::time::Duration::from_secs(1)).await;
} }
self.disconnect();
data_handler.handle_disconnected();
}); });
} }
+4 -1
View File
@@ -151,7 +151,10 @@ impl ServerOutbound {
// 只有一个服务器,直接发送 // 只有一个服务器,直接发送
if map.len() == 1 { if map.len() == 1 {
let (server_id, (ips, _)) = map.iter().next().expect("map has exactly one element"); let (server_id, (ips, _)) = map
.iter()
.next()
.context("connected server map unexpectedly became empty")?;
if ips.is_empty() { if ips.is_empty() {
return Ok(()); return Ok(());
} }
+2 -4
View File
@@ -228,11 +228,9 @@ fn bind_udp(
default_interface: &Option<LocalInterface>, default_interface: &Option<LocalInterface>,
) -> io::Result<UdpSocket> { ) -> io::Result<UdpSocket> {
let addr: SocketAddr = if name_server.is_ipv4() { let addr: SocketAddr = if name_server.is_ipv4() {
"0.0.0.0:0" SocketAddr::from(([0, 0, 0, 0], 0))
.parse()
.expect("valid IPv4 socket address literal")
} else { } else {
"[::]:0".parse().expect("valid IPv6 socket address literal") SocketAddr::from(([0; 8], 0))
}; };
let socket = rust_p2p_core::socket::bind_udp(addr, default_interface.as_ref())?; let socket = rust_p2p_core::socket::bind_udp(addr, default_interface.as_ref())?;
UdpSocket::from_std(socket.into()) UdpSocket::from_std(socket.into())
+1
View File
@@ -1,6 +1,7 @@
{ {
"name": "vnt-desktop", "name": "vnt-desktop",
"private": true, "private": true,
"license": "Apache-2.0",
"version": "2.0.2", "version": "2.0.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
+1
View File
@@ -3,6 +3,7 @@ name = "vnt-desktop"
version = "2.0.2" version = "2.0.2"
description = "VNT virtual network desktop client" description = "VNT virtual network desktop client"
edition = "2024" edition = "2024"
license = "Apache-2.0"
[lib] [lib]
name = "vnt_desktop_lib" name = "vnt_desktop_lib"
+1 -2
View File
@@ -235,7 +235,7 @@ fn toggle_main_window(app: &AppHandle) {
} }
} }
pub fn run() { pub fn run() -> Result<(), tauri::Error> {
tauri::Builder::default() tauri::Builder::default()
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
api_request, api_request,
@@ -320,5 +320,4 @@ pub fn run() {
} }
}) })
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running VNT Desktop");
} }
+2 -2
View File
@@ -1,3 +1,3 @@
fn main() { fn main() -> Result<(), tauri::Error> {
vnt_desktop_lib::run(); vnt_desktop_lib::run()
} }
+1
View File
@@ -2,6 +2,7 @@
name = "vnt-ipc" name = "vnt-ipc"
version = "2.0.2" version = "2.0.2"
edition = "2024" edition = "2024"
license = "Apache-2.0"
[dependencies] [dependencies]
+3 -4
View File
@@ -1,4 +1,4 @@
fn main() { fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut config = prost_build::Config::new(); let mut config = prost_build::Config::new();
match protoc_bin_vendored::protoc_bin_path() { match protoc_bin_vendored::protoc_bin_path() {
@@ -13,7 +13,6 @@ fn main() {
} }
config.protoc_arg("--experimental_allow_proto3_optional"); config.protoc_arg("--experimental_allow_proto3_optional");
config config.compile_protos(&["proto/local_ipc.proto"], &["proto"])?;
.compile_protos(&["proto/local_ipc.proto"], &["proto"]) Ok(())
.unwrap();
} }
+1 -1
View File
@@ -285,5 +285,5 @@ pub fn ts_to_string(ts_secs: i64) -> String {
}; };
let dt_local = dt.to_offset(local_offset); let dt_local = dt.to_offset(local_offset);
let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"); let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
dt_local.format(&format).unwrap() dt_local.format(&format).unwrap_or_default()
} }
+1
View File
@@ -2,6 +2,7 @@
name = "vnt-jni" name = "vnt-jni"
version = "2.0.2" version = "2.0.2"
edition = "2024" edition = "2024"
license = "Apache-2.0"
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
+1
View File
@@ -2,6 +2,7 @@
name = "vnt-web" name = "vnt-web"
version = "2.0.2" version = "2.0.2"
edition = "2024" edition = "2024"
license = "Apache-2.0"
[dependencies] [dependencies]
vnt-core.workspace = true vnt-core.workspace = true
+33 -21
View File
@@ -5,16 +5,20 @@
//! - 找不到 pnpm 时:已有产物则告警并沿用;没有产物则报错并给出指引 //! - 找不到 pnpm 时:已有产物则告警并沿用;没有产物则报错并给出指引
//! - 设置环境变量 VNT_WEB_SKIP_UI_BUILD=1 可完全跳过前端构建 //! - 设置环境变量 VNT_WEB_SKIP_UI_BUILD=1 可完全跳过前端构建
use std::error::Error;
use std::io;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
use std::time::SystemTime; use std::time::SystemTime;
fn main() { fn main() -> Result<(), Box<dyn Error>> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"); let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")?;
let manifest_dir = Path::new(&manifest_dir); let manifest_dir = Path::new(&manifest_dir);
let ui_dir = manifest_dir.join("ui"); let ui_dir = manifest_dir.join("ui");
let static_dir = manifest_dir.join("static"); let static_dir = manifest_dir.join("static");
let workspace_root = manifest_dir.parent().expect("workspace root"); let workspace_root = manifest_dir
.parent()
.ok_or_else(|| io::Error::other("vnt-web manifest directory has no parent"))?;
// UI 源码变化时重新运行本脚本 // UI 源码变化时重新运行本脚本
println!("cargo:rerun-if-changed={}", ui_dir.join("src").display()); println!("cargo:rerun-if-changed={}", ui_dir.join("src").display());
@@ -38,12 +42,12 @@ fn main() {
println!("cargo:rerun-if-env-changed=VNT_WEB_SKIP_UI_BUILD"); println!("cargo:rerun-if-env-changed=VNT_WEB_SKIP_UI_BUILD");
if std::env::var("VNT_WEB_SKIP_UI_BUILD").is_ok() { if std::env::var("VNT_WEB_SKIP_UI_BUILD").is_ok() {
ensure_static_placeholder(&static_dir); ensure_static_placeholder(&static_dir)?;
return; return Ok(());
} }
if static_is_fresh(&ui_dir, &static_dir) { if static_is_fresh(&ui_dir, &static_dir) {
return; return Ok(());
} }
let Some(pnpm) = find_pnpm() else { let Some(pnpm) = find_pnpm() else {
@@ -51,20 +55,22 @@ fn main() {
println!( println!(
"cargo:warning=未找到 pnpm,沿用 vnt-web/static 中已有的前端产物(可能不是最新)" "cargo:warning=未找到 pnpm,沿用 vnt-web/static 中已有的前端产物(可能不是最新)"
); );
return; return Ok(());
} }
panic!( return Err(io::Error::other(
"未找到 pnpm 且 vnt-web/static 没有前端产物。\n\ "未找到 pnpm 且 vnt-web/static 没有前端产物。\n\
Node.js pnpm cargo \n\ Node.js pnpm cargo \n\
static vnt-web/" static vnt-web/",
); )
.into());
}; };
if !ui_dir.join("node_modules").is_dir() { if !ui_dir.join("node_modules").is_dir() {
// ui 依赖使用 workspace catalog,必须在仓库根目录安装 // ui 依赖使用 workspace catalog,必须在仓库根目录安装
run_or_panic(pnpm, &["install", "--frozen-lockfile"], workspace_root); run_command(pnpm, &["install", "--frozen-lockfile"], workspace_root)?;
} }
run_or_panic(pnpm, &["--filter", "vnt-web-ui", "build"], workspace_root); run_command(pnpm, &["--filter", "vnt-web-ui", "build"], workspace_root)?;
Ok(())
} }
/// pnpm 命令名(Windows 上是 pnpm.cmd,由 cmd.exe 执行) /// pnpm 命令名(Windows 上是 pnpm.cmd,由 cmd.exe 执行)
@@ -80,7 +86,7 @@ fn find_pnpm() -> Option<&'static str> {
.find(|cmd| Command::new(cmd).arg("--version").output().is_ok()) .find(|cmd| Command::new(cmd).arg("--version").output().is_ok())
} }
fn run_or_panic(program: &str, args: &[&str], dir: &Path) { fn run_command(program: &str, args: &[&str], dir: &Path) -> io::Result<()> {
println!( println!(
"cargo:warning=执行前端构建: {} {} ({})", "cargo:warning=执行前端构建: {} {} ({})",
program, program,
@@ -91,15 +97,21 @@ fn run_or_panic(program: &str, args: &[&str], dir: &Path) {
.args(args) .args(args)
.current_dir(dir) .current_dir(dir)
.status() .status()
.unwrap_or_else(|e| panic!("执行 {} 失败: {}", program, e)); .map_err(|error| {
io::Error::new(
error.kind(),
format!("执行 {program} 失败(目录 {}):{error}", dir.display()),
)
})?;
if !status.success() { if !status.success() {
panic!( return Err(io::Error::other(format!(
"前端构建失败: {} {} (exit: {:?})", "前端构建失败: {} {} (exit: {:?})",
program, program,
args.join(" "), args.join(" "),
status.code() status.code()
); )));
} }
Ok(())
} }
/// static 产物是否比 UI 源码新 /// static 产物是否比 UI 源码新
@@ -128,16 +140,16 @@ fn newest_mtime(dir: &Path) -> Option<SystemTime> {
} }
/// 跳过构建时保证 static/ 存在,使 rust_embed 可以编译 /// 跳过构建时保证 static/ 存在,使 rust_embed 可以编译
fn ensure_static_placeholder(static_dir: &Path) { fn ensure_static_placeholder(static_dir: &Path) -> io::Result<()> {
if static_dir.join("index.html").is_file() { if static_dir.join("index.html").is_file() {
return; return Ok(());
} }
println!("cargo:warning=VNT_WEB_SKIP_UI_BUILD 已设置且 static 为空,写入占位页面"); println!("cargo:warning=VNT_WEB_SKIP_UI_BUILD 已设置且 static 为空,写入占位页面");
std::fs::create_dir_all(static_dir).expect("创建 static 目录失败"); std::fs::create_dir_all(static_dir)?;
std::fs::write( std::fs::write(
static_dir.join("index.html"), static_dir.join("index.html"),
"<!doctype html><html><body><p>VNT Web UI 未构建。请安装 pnpm 后重新执行 cargo build\ "<!doctype html><html><body><p>VNT Web UI 未构建。请安装 pnpm 后重新执行 cargo build\
VNT_WEB_SKIP_UI_BUILD</p></body></html>", VNT_WEB_SKIP_UI_BUILD</p></body></html>",
) )?;
.expect("写入占位页面失败"); Ok(())
} }
+22 -15
View File
@@ -21,7 +21,7 @@ use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::time::{Instant, SystemTime, UNIX_EPOCH}; use std::time::{Instant, SystemTime, UNIX_EPOCH};
use time::{OffsetDateTime, format_description}; use time::{OffsetDateTime, macros::format_description};
use tokio::fs; use tokio::fs;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@@ -205,7 +205,7 @@ impl HttpAppState {
fn timestamp() -> String { fn timestamp() -> String {
let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc());
let format = format_description::parse("[hour]:[minute]:[second]").unwrap(); let format = format_description!("[hour]:[minute]:[second]");
now.format(&format) now.format(&format)
.unwrap_or_else(|_| "00:00:00".to_string()) .unwrap_or_else(|_| "00:00:00".to_string())
} }
@@ -288,7 +288,7 @@ pub struct StartConfig {
impl StartConfig { impl StartConfig {
fn reject_legacy_no_tun(&self) -> anyhow::Result<()> { fn reject_legacy_no_tun(&self) -> anyhow::Result<()> {
if self.legacy_no_tun.is_some() { if self.legacy_no_tun == Some(true) {
bail!("configuration key 'no_tun' was removed; use device_mode = \"no|tun|tap\"") bail!("configuration key 'no_tun' was removed; use device_mode = \"no|tun|tap\"")
} }
Ok(()) Ok(())
@@ -649,9 +649,10 @@ pub async fn run_http_server(
let handle = service let handle = service
.start_http(addr, token, cancellation.clone()) .start_http(addr, token, cancellation.clone())
.await?; .await?;
shutdown_signal().await; let shutdown_result = shutdown_signal().await;
cancellation.cancel(); cancellation.cancel();
handle.await??; handle.await??;
shutdown_result?;
Ok(()) Ok(())
} }
@@ -751,7 +752,8 @@ fn build_headers_for_path(path: &str) -> HeaderMap {
}; };
headers.insert( headers.insert(
header::CONTENT_TYPE, header::CONTENT_TYPE,
HeaderValue::from_str(mime.as_ref()).unwrap(), HeaderValue::from_str(mime.as_ref())
.unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream")),
); );
if is_gz { if is_gz {
@@ -1309,7 +1311,7 @@ async fn save_config(Json(req): Json<SaveConfigReq>) -> Json<ApiResponse<()>> {
.unwrap_or_else(|| { .unwrap_or_else(|| {
let now = SystemTime::now() let now = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.unwrap() .unwrap_or_default()
.as_millis(); .as_millis();
format!("{}.toml", now) format!("{}.toml", now)
}); });
@@ -1455,28 +1457,29 @@ fn convert_config(cfg: StartConfig) -> anyhow::Result<CoreConfig> {
}) })
} }
async fn shutdown_signal() { async fn shutdown_signal() -> anyhow::Result<()> {
let ctrl_c = async { let ctrl_c = async {
tokio::signal::ctrl_c() tokio::signal::ctrl_c()
.await .await
.expect("failed to install Ctrl+C handler"); .context("failed to install Ctrl+C handler")
}; };
#[cfg(unix)] #[cfg(unix)]
let terminate = async { let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) let mut signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install signal handler") .context("failed to install terminate signal handler")?;
.recv() signal.recv().await;
.await; Ok::<(), anyhow::Error>(())
}; };
#[cfg(not(unix))] #[cfg(not(unix))]
let terminate = std::future::pending::<()>(); let terminate = std::future::pending::<anyhow::Result<()>>();
tokio::select! { tokio::select! {
_ = ctrl_c => {}, result = ctrl_c => result?,
_ = terminate => {}, result = terminate => result?,
} }
Ok(())
} }
async fn get_peers( async fn get_peers(
@@ -1797,6 +1800,10 @@ network_code = "test"
let legacy: StartConfig = toml::from_str(&format!("{base}no_tun = true\n")).unwrap(); let legacy: StartConfig = toml::from_str(&format!("{base}no_tun = true\n")).unwrap();
assert!(legacy.reject_legacy_no_tun().is_err()); assert!(legacy.reject_legacy_no_tun().is_err());
let legacy_false: StartConfig =
toml::from_str(&format!("{base}no_tun = false\n")).unwrap();
assert!(legacy_false.reject_legacy_no_tun().is_ok());
} }
/// 两个实例同时处于 Starting 互不影响 /// 两个实例同时处于 Starting 互不影响
+1
View File
@@ -1,6 +1,7 @@
{ {
"name": "vnt-web-ui", "name": "vnt-web-ui",
"private": true, "private": true,
"license": "Apache-2.0",
"version": "2.0.2", "version": "2.0.2",
"type": "module", "type": "module",
"scripts": { "scripts": {