Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10eb97e403 | ||
|
|
7133906ab2 | ||
|
|
fedba5c8e2 | ||
|
|
c2fd1ff870 | ||
|
|
2100ec1ca5 | ||
|
|
298c3965e2 | ||
|
|
619a5bcf18 | ||
|
|
1acb5e8a86 |
Generated
+6
-6
@@ -6294,7 +6294,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vnt-core"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -6340,7 +6340,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vnt-desktop"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"log",
|
||||
@@ -6361,7 +6361,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vnt-ipc"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cli-table",
|
||||
@@ -6379,7 +6379,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vnt-jni"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hostname",
|
||||
@@ -6396,7 +6396,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vnt-web"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -6421,7 +6421,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vnt2"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
[package]
|
||||
name = "vnt2"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
vnt-ipc = { path = "vnt-ipc", optional = true }
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
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");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,74 +1,68 @@
|
||||
# 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 后台运行时,使用 vnt2_ctrl 查看组网状态
|
||||
./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。
|
||||
|
||||
## 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)
|
||||
2. 路由设置(https://github.com/tun-rs/route_manager)
|
||||
3. 用户态协议栈(用于quic代理和无tun模式出口)(https://github.com/rustp2p/tcp_ip)
|
||||
4. 打洞通道处理(https://github.com/rustp2p/rustp2p/tree/master/rustp2p-core)
|
||||
|
||||
|
||||
|
||||
|
||||
1. tun 虚拟网卡(https://github.com/tun-rs/tun-rs)
|
||||
2. 路由设置(https://github.com/tun-rs/route_manager)
|
||||
3. 用户态协议栈(用于 quic 代理和无 tun 模式出口)(https://github.com/rustp2p/tcp_ip)
|
||||
4. 打洞通道处理(https://github.com/rustp2p/rustp2p/tree/master/rustp2p-core)
|
||||
5. 安卓端2.0已发布(https://github.com/vnt-dev/VntApp)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "vnt-workspace",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"packageManager": "[email protected]",
|
||||
"scripts": {
|
||||
"dev:web": "pnpm --filter vnt-web-ui dev",
|
||||
|
||||
Generated
+216
@@ -74,6 +74,9 @@ importers:
|
||||
pinia:
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.4([email protected])
|
||||
qrcode:
|
||||
specifier: 1.5.4
|
||||
version: 1.5.4
|
||||
vue:
|
||||
specifier: 'catalog:'
|
||||
version: 3.5.41
|
||||
@@ -661,9 +664,31 @@ packages:
|
||||
'@vue/[email protected]':
|
||||
resolution: {integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ufbM3smX/Jbnpk5wcQjzd1MgBpzmqfNETUAyZNrGwU9foRlyHoGzMMBBCRzEhQLBjZfFDE1W2ufPXX2vdWkV8Q==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -671,10 +696,20 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@@ -700,17 +735,29 @@ packages:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
||||
hasBin: true
|
||||
@@ -789,6 +836,10 @@ packages:
|
||||
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
@@ -800,6 +851,22 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||
|
||||
@@ -819,10 +886,26 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||
|
||||
@@ -831,6 +914,9 @@ packages:
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -839,6 +925,14 @@ packages:
|
||||
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==}
|
||||
engines: {node: '>=16'}
|
||||
@@ -907,6 +1001,24 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@babel/[email protected]': {}
|
||||
@@ -1306,14 +1418,40 @@ snapshots:
|
||||
|
||||
'@vue/[email protected]': {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 6.2.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -1356,13 +1494,22 @@ snapshots:
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.5
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
locate-path: 5.0.0
|
||||
path-exists: 4.0.0
|
||||
|
||||
[email protected]:
|
||||
optional: true
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -1414,6 +1561,10 @@ snapshots:
|
||||
lightningcss-win32-arm64-msvc: 1.32.0
|
||||
lightningcss-win32-x64-msvc: 1.32.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
p-locate: 4.1.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@@ -1422,6 +1573,18 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
p-try: 2.2.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
p-limit: 2.3.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -1433,12 +1596,24 @@ snapshots:
|
||||
'@vue/devtools-api': 7.7.10
|
||||
vue: 3.5.41
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
nanoid: 3.3.18
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
dijkstrajs: 1.0.3
|
||||
pngjs: 5.0.0
|
||||
yargs: 15.4.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -1473,10 +1648,22 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc': 4.62.5
|
||||
fsevents: 2.3.3
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
copy-anything: 4.1.0
|
||||
@@ -1515,3 +1702,32 @@ snapshots:
|
||||
'@vue/runtime-dom': 3.5.41
|
||||
'@vue/server-renderer': 3.5.41
|
||||
'@vue/shared': 3.5.41
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
camelcase: 5.3.1
|
||||
decamelize: 1.2.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
cliui: 6.0.0
|
||||
decamelize: 1.2.0
|
||||
find-up: 4.1.0
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
require-main-filename: 2.0.0
|
||||
set-blocking: 2.0.0
|
||||
string-width: 4.2.3
|
||||
which-module: 2.0.1
|
||||
y18n: 4.0.3
|
||||
yargs-parser: 18.1.3
|
||||
|
||||
+10
-3
@@ -181,7 +181,7 @@ pub fn build_config_from_args_and_file(
|
||||
) -> anyhow::Result<(Config, CtrlConfig)> {
|
||||
if file
|
||||
.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!(
|
||||
"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)> {
|
||||
if file.legacy_no_tun.is_some() {
|
||||
if file.legacy_no_tun == Some(true) {
|
||||
return Err(anyhow!(
|
||||
"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 error = match build_config_from_args_and_file(None, Some(legacy)) {
|
||||
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"));
|
||||
|
||||
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]
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
[package]
|
||||
name = "vnt-core"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
|
||||
|
||||
+11
-12
@@ -1,4 +1,4 @@
|
||||
fn main() {
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut config = prost_build::Config::new();
|
||||
|
||||
match protoc_bin_vendored::protoc_bin_path() {
|
||||
@@ -14,15 +14,14 @@ fn main() {
|
||||
|
||||
config.protoc_arg("--experimental_allow_proto3_optional");
|
||||
|
||||
config
|
||||
.compile_protos(
|
||||
&[
|
||||
"proto/control_message.proto",
|
||||
"proto/rpc.proto",
|
||||
"proto/client.proto",
|
||||
"proto/fec.proto",
|
||||
],
|
||||
&["proto"],
|
||||
)
|
||||
.unwrap();
|
||||
config.compile_protos(
|
||||
&[
|
||||
"proto/control_message.proto",
|
||||
"proto/rpc.proto",
|
||||
"proto/client.proto",
|
||||
"proto/fec.proto",
|
||||
],
|
||||
&["proto"],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ impl NetworkManager {
|
||||
log::info!("绑定出口网卡: {name}");
|
||||
}
|
||||
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 (server_manager_list, tunnel_to_server, server_rpc) = create_server_tunnel(
|
||||
app_state.clone(),
|
||||
@@ -163,7 +163,7 @@ impl NetworkManager {
|
||||
EnhancedTunInbound::Nat(
|
||||
internal_nat_inbound
|
||||
.clone()
|
||||
.expect("internal_nat_inbound must be Some in no-device mode"),
|
||||
.context("internal NAT is unavailable in no-device mode")?,
|
||||
),
|
||||
None,
|
||||
),
|
||||
|
||||
@@ -33,15 +33,16 @@ impl PacketCrypto {
|
||||
.map(|b| format!("{:02x}", b))
|
||||
.collect::<String>()
|
||||
}
|
||||
pub fn new(key_bytes: [u8; 32]) -> Self {
|
||||
let unbound = UnboundKey::new(&CHACHA20_POLY1305, &key_bytes).unwrap();
|
||||
pub fn new(key_bytes: [u8; 32]) -> io::Result<Self> {
|
||||
let unbound = UnboundKey::new(&CHACHA20_POLY1305, &key_bytes)
|
||||
.map_err(|_| io::Error::other("failed to initialize ChaCha20-Poly1305 key"))?;
|
||||
let key = LessSafeKey::new(unbound);
|
||||
Self {
|
||||
Ok(Self {
|
||||
key,
|
||||
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 mut key_bytes = [0u8; 32];
|
||||
key_bytes.copy_from_slice(hash.as_ref());
|
||||
@@ -161,7 +162,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_in_place() {
|
||||
let key = [7u8; 32];
|
||||
let crypto = PacketCrypto::new(key);
|
||||
let crypto = PacketCrypto::new(key).unwrap();
|
||||
|
||||
let payload_len = 20;
|
||||
let mut pkt = build_test_packet(payload_len);
|
||||
@@ -198,7 +199,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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 pkt2 = build_test_packet(20);
|
||||
@@ -227,7 +228,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_clone_shares_seq_counter() {
|
||||
let crypto = PacketCrypto::new([9u8; 32]);
|
||||
let crypto = PacketCrypto::new([9u8; 32]).unwrap();
|
||||
let cloned = crypto.clone();
|
||||
|
||||
let mut pkt1 = build_test_packet(8);
|
||||
@@ -243,9 +244,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_cross_version_compat() {
|
||||
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 直接由头部计算
|
||||
let mut pkt = build_test_packet(20);
|
||||
@@ -281,7 +282,7 @@ mod tests {
|
||||
/// 必须导致解密失败,而不是被静默接受。
|
||||
#[test]
|
||||
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);
|
||||
crypto.encrypt_in_place(&mut pkt).expect("encrypt failed");
|
||||
@@ -298,7 +299,7 @@ mod tests {
|
||||
/// AAD 覆盖 msg_type(byte0):中间人篡改消息类型必须导致解密失败。
|
||||
#[test]
|
||||
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);
|
||||
crypto.encrypt_in_place(&mut pkt).expect("encrypt failed");
|
||||
@@ -315,7 +316,7 @@ mod tests {
|
||||
/// 转发后 ttl 变化的包必须仍能正常解密。
|
||||
#[test]
|
||||
fn test_ttl_change_still_decrypts() {
|
||||
let crypto = PacketCrypto::new([7u8; 32]);
|
||||
let crypto = PacketCrypto::new([7u8; 32]).unwrap();
|
||||
|
||||
let payload_len = 20;
|
||||
let mut pkt = build_test_packet(payload_len);
|
||||
|
||||
@@ -16,12 +16,12 @@ impl PacketCrypto {
|
||||
chacha20_poly1305::PacketCrypto::key_sign(s)
|
||||
}
|
||||
|
||||
pub(crate) fn new_from_str(s: Option<&str>) -> Self {
|
||||
Self {
|
||||
crypto: s
|
||||
.map(chacha20_poly1305::PacketCrypto::new_from_str)
|
||||
.map(Arc::new),
|
||||
}
|
||||
pub(crate) fn new_from_str(s: Option<&str>) -> io::Result<Self> {
|
||||
let crypto = s
|
||||
.map(chacha20_poly1305::PacketCrypto::new_from_str)
|
||||
.transpose()?
|
||||
.map(Arc::new);
|
||||
Ok(Self { crypto })
|
||||
}
|
||||
pub(crate) fn encrypt_reserve(&self) -> usize {
|
||||
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)
|
||||
.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();
|
||||
endpoint_config.max_udp_payload_size(1300)?;
|
||||
let mut endpoint = quinn::Endpoint::new_with_abstract_socket(
|
||||
@@ -159,14 +159,18 @@ async fn create_quic_endpoint(
|
||||
Ok((inbound, endpoint))
|
||||
}
|
||||
|
||||
fn build_transport_config() -> Arc<TransportConfig> {
|
||||
fn build_transport_config() -> anyhow::Result<Arc<TransportConfig>> {
|
||||
let mut transport = TransportConfig::default();
|
||||
transport.congestion_controller_factory(Arc::new(BbrConfig::default()));
|
||||
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(
|
||||
|
||||
@@ -85,9 +85,9 @@ async fn handle_outbound(
|
||||
} else {
|
||||
// 创建真实 UDP socket
|
||||
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 {
|
||||
"[::]:0".parse().expect("valid IPv6 bind address")
|
||||
SocketAddr::from(([0; 8], 0))
|
||||
};
|
||||
let interface = if dst.ip().is_loopback() {
|
||||
None
|
||||
@@ -223,9 +223,9 @@ where
|
||||
.next()
|
||||
.context("UDP NAT destination resolved to no address")?;
|
||||
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 {
|
||||
"[::]:0".parse().expect("valid IPv6 bind address")
|
||||
SocketAddr::from(([0; 8], 0))
|
||||
};
|
||||
let interface = if destination.ip().is_loopback() {
|
||||
None
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::protocol::transmission::TransmissionBytes;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use std::io;
|
||||
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)]
|
||||
#[repr(C)]
|
||||
@@ -167,11 +167,6 @@ impl<B: AsRef<[u8]>> NetPacket<B> {
|
||||
}
|
||||
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] {
|
||||
self.buffer.as_ref()
|
||||
}
|
||||
@@ -182,37 +177,40 @@ impl<B: AsRef<[u8]>> NetPacket<B> {
|
||||
&self.buffer
|
||||
}
|
||||
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 {
|
||||
self.header().max_ttl()
|
||||
self.buffer.as_ref()[1] >> 4
|
||||
}
|
||||
pub fn ttl(&self) -> u8 {
|
||||
self.header().curr_ttl()
|
||||
self.buffer.as_ref()[1] & 0x0F
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
(self.header().flags_byte & COMPRESSED) != 0
|
||||
(self.buffer.as_ref()[2] & COMPRESSED) != 0
|
||||
}
|
||||
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 {
|
||||
(self.header().flags_byte & FEC) != 0
|
||||
(self.buffer.as_ref()[2] & FEC) != 0
|
||||
}
|
||||
pub fn is_ethernet(&self) -> bool {
|
||||
(self.header().flags_byte & ETHERNET) != 0
|
||||
(self.buffer.as_ref()[2] & ETHERNET) != 0
|
||||
}
|
||||
pub fn head(&self) -> &[u8] {
|
||||
&self.buffer.as_ref()[..HEAD_LENGTH]
|
||||
@@ -223,47 +221,54 @@ impl<B: AsRef<[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) {
|
||||
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) {
|
||||
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) {
|
||||
self.header_mut().set_ttl(ttl, ttl);
|
||||
self.buffer.as_mut()[1] = (ttl << 4) | (ttl & 0x0F);
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
self.header_mut().set_flag(COMPRESSED, compressed);
|
||||
self.set_flag(COMPRESSED, compressed);
|
||||
}
|
||||
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) {
|
||||
self.header_mut().set_flag(FEC, fec);
|
||||
self.set_flag(FEC, fec);
|
||||
}
|
||||
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<()> {
|
||||
|
||||
@@ -99,12 +99,17 @@ impl DeviceIOManager {
|
||||
// 保证失败时调用方状态完整、可以重试
|
||||
let device_mode = device_config.device_mode;
|
||||
let device = Arc::new(create_device(device_config)?);
|
||||
let receiver = receiver.take().unwrap();
|
||||
let enhanced_outbound = enhanced_outbound.take().unwrap();
|
||||
let Some(receiver_value) = receiver.take() else {
|
||||
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(
|
||||
&self.task_group,
|
||||
device,
|
||||
receiver.receiver,
|
||||
receiver_value.receiver,
|
||||
enhanced_outbound,
|
||||
device_mode,
|
||||
);
|
||||
|
||||
@@ -357,7 +357,9 @@ pub(crate) async fn query_tcp_public_addr_loop(
|
||||
let addrs: Vec<SocketAddr> = active_connections.keys().cloned().collect();
|
||||
|
||||
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];
|
||||
|
||||
match tcp_stream.try_read(&mut buf) {
|
||||
|
||||
@@ -149,8 +149,11 @@ pub async fn ping_all(
|
||||
ping.set_ttl(1);
|
||||
ping.set_src_id(src.into());
|
||||
ping.set_dest_id(id.into());
|
||||
ping.set_payload(&crate::utils::time::now_ts_ms().to_be_bytes())
|
||||
.unwrap();
|
||||
if let Err(error) = ping.set_payload(&crate::utils::time::now_ts_ms().to_be_bytes())
|
||||
{
|
||||
log::warn!("failed to build route probe: {error}");
|
||||
continue;
|
||||
}
|
||||
let route_key = route.route_key();
|
||||
if socket_manager.send_to(ping, &route_key).await.is_ok() {
|
||||
packet_loss_stats.record_sent(id, route_key);
|
||||
|
||||
@@ -188,13 +188,15 @@ impl ServerTurnManager {
|
||||
|| reg.prefix_len != initial_response.prefix_len
|
||||
|| reg.gateway != initial_response.gateway
|
||||
{
|
||||
// 该服务器分配的虚拟网络与当前不一致,无法重连,
|
||||
// 只结束本服务器的任务,不影响其他服务器
|
||||
// 该服务器分配的虚拟网络与当前不一致,
|
||||
// 断开本次连接并降低重试频率,不影响其他服务器。
|
||||
log::error!(
|
||||
"服务器{}虚拟网络发生变化,放弃重连",
|
||||
"服务器{}虚拟网络发生变化,1分钟后重试",
|
||||
self.config.server_addr
|
||||
);
|
||||
break;
|
||||
self.disconnect();
|
||||
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
|
||||
continue;
|
||||
}
|
||||
// 保存服务器版本
|
||||
if !reg.server_version.is_empty() {
|
||||
@@ -224,8 +226,6 @@ impl ServerTurnManager {
|
||||
already_connected = false;
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
self.disconnect();
|
||||
data_handler.handle_disconnected();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,10 @@ impl ServerOutbound {
|
||||
|
||||
// 只有一个服务器,直接发送
|
||||
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() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -228,11 +228,9 @@ fn bind_udp(
|
||||
default_interface: &Option<LocalInterface>,
|
||||
) -> io::Result<UdpSocket> {
|
||||
let addr: SocketAddr = if name_server.is_ipv4() {
|
||||
"0.0.0.0:0"
|
||||
.parse()
|
||||
.expect("valid IPv4 socket address literal")
|
||||
SocketAddr::from(([0, 0, 0, 0], 0))
|
||||
} 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())?;
|
||||
UdpSocket::from_std(socket.into())
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "vnt-desktop",
|
||||
"private": true,
|
||||
"version": "2.0.1",
|
||||
"license": "Apache-2.0",
|
||||
"version": "2.0.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
[package]
|
||||
name = "vnt-desktop"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
description = "VNT virtual network desktop client"
|
||||
edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lib]
|
||||
name = "vnt_desktop_lib"
|
||||
|
||||
@@ -235,7 +235,7 @@ fn toggle_main_window(app: &AppHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
pub fn run() -> Result<(), tauri::Error> {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
api_request,
|
||||
@@ -320,5 +320,4 @@ pub fn run() {
|
||||
}
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running VNT Desktop");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fn main() {
|
||||
vnt_desktop_lib::run();
|
||||
fn main() -> Result<(), tauri::Error> {
|
||||
vnt_desktop_lib::run()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "VNT Desktop",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.2",
|
||||
"identifier": "com.vnt.desktop",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
[package]
|
||||
name = "vnt-ipc"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
|
||||
|
||||
+3
-4
@@ -1,4 +1,4 @@
|
||||
fn main() {
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut config = prost_build::Config::new();
|
||||
|
||||
match protoc_bin_vendored::protoc_bin_path() {
|
||||
@@ -13,7 +13,6 @@ fn main() {
|
||||
}
|
||||
|
||||
config.protoc_arg("--experimental_allow_proto3_optional");
|
||||
config
|
||||
.compile_protos(&["proto/local_ipc.proto"], &["proto"])
|
||||
.unwrap();
|
||||
config.compile_protos(&["proto/local_ipc.proto"], &["proto"])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -285,5 +285,5 @@ pub fn ts_to_string(ts_secs: i64) -> String {
|
||||
};
|
||||
let dt_local = dt.to_offset(local_offset);
|
||||
let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
|
||||
dt_local.format(&format).unwrap()
|
||||
dt_local.format(&format).unwrap_or_default()
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
[package]
|
||||
name = "vnt-jni"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
+98
-12
@@ -68,6 +68,16 @@ where
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).map_err(panic_message)
|
||||
}
|
||||
|
||||
fn encryption_state(local_key: Option<&str>, peer_key: Option<&str>) -> i32 {
|
||||
match (local_key, peer_key) {
|
||||
(Some(local), Some(peer)) if local == peer => 1,
|
||||
(None, None) => 2,
|
||||
(Some(_), None) => 3,
|
||||
(None, Some(_)) => 4,
|
||||
(Some(_), Some(_)) => 5,
|
||||
}
|
||||
}
|
||||
|
||||
/// JNI 导出函数的 panic 防护:panic 时向 JVM 抛出异常并返回默认值
|
||||
macro_rules! jni_guard {
|
||||
($env:ident, $default_ret:expr, { $($body:tt)* }) => {{
|
||||
@@ -479,21 +489,88 @@ pub extern "system" fn Java_com_vnt_VntApi_nativeGetClientList<'local>(
|
||||
) -> jstring {
|
||||
jni_guard!(env, std::ptr::null_mut(), {
|
||||
let result: anyhow::Result<String> = (|| {
|
||||
let global_state = GLOBAL_STATE.lock();
|
||||
let state = global_state.as_ref().context("VNT not initialized")?;
|
||||
let (api, runtime) = {
|
||||
let global_state = GLOBAL_STATE.lock();
|
||||
let state = global_state.as_ref().context("VNT not initialized")?;
|
||||
(
|
||||
state
|
||||
.vnt_apis
|
||||
.get(&api_handle)
|
||||
.context("Invalid API handle")?
|
||||
.clone(),
|
||||
state.runtime.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
let api = state
|
||||
.vnt_apis
|
||||
.get(&api_handle)
|
||||
.context("Invalid API handle")?;
|
||||
|
||||
let client_ips = api.client_ips();
|
||||
let json_array: Vec<_> = client_ips
|
||||
let local_clients: HashMap<_, _> = api
|
||||
.client_ips()
|
||||
.into_iter()
|
||||
.map(|client| {
|
||||
.map(|client| (client.ip, client.online))
|
||||
.collect();
|
||||
let server_clients: HashMap<_, _> = runtime
|
||||
.block_on(api.server_rpc().client_list())
|
||||
.map(|response| {
|
||||
response
|
||||
.list
|
||||
.into_iter()
|
||||
.map(|client| (Ipv4Addr::from(client.ip), client))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let local_key = api.get_config().and_then(|config| config.key_sign());
|
||||
let mut ips: Vec<_> = local_clients
|
||||
.keys()
|
||||
.chain(server_clients.keys())
|
||||
.copied()
|
||||
.collect();
|
||||
ips.sort_unstable();
|
||||
ips.dedup();
|
||||
|
||||
let json_array: Vec<_> = ips
|
||||
.into_iter()
|
||||
.map(|ip| {
|
||||
let server_client = server_clients.get(&ip);
|
||||
let route = api.find_route(&ip);
|
||||
let has_route = route.is_some();
|
||||
let direct = route
|
||||
.as_ref()
|
||||
.map(|route| route.metric() == 1)
|
||||
.unwrap_or(false);
|
||||
let route_protocol = route
|
||||
.as_ref()
|
||||
.map(|route| route.route_key().protocol().to_string());
|
||||
let route_metric = route.as_ref().map(|route| route.metric());
|
||||
let rtt = route.as_ref().map(|route| route.rtt());
|
||||
let online = local_clients.get(&ip).copied().unwrap_or(false)
|
||||
|| server_client.map(|client| client.online).unwrap_or(false)
|
||||
|| has_route;
|
||||
let packet_loss = api.packet_loss_info(&ip).map(|info| {
|
||||
serde_json::json!({
|
||||
"sent": info.sent,
|
||||
"received": info.received,
|
||||
"loss_rate": info.loss_rate,
|
||||
})
|
||||
});
|
||||
let traffic = api.traffic_info(&ip).map(|info| {
|
||||
serde_json::json!({
|
||||
"tx_bytes": info.tx_bytes,
|
||||
"rx_bytes": info.rx_bytes,
|
||||
})
|
||||
});
|
||||
serde_json::json!({
|
||||
"ip": client.ip.to_string(),
|
||||
"online": client.online,
|
||||
"ip": ip.to_string(),
|
||||
"name": server_client.map(|client| client.name.as_str()).unwrap_or(""),
|
||||
"version": server_client.map(|client| client.version.as_str()).unwrap_or(""),
|
||||
"online": online,
|
||||
"direct": direct,
|
||||
"route_protocol": route_protocol,
|
||||
"route_metric": route_metric,
|
||||
"rtt": rtt,
|
||||
"key_equal": server_client
|
||||
.map(|client| encryption_state(local_key.as_deref(), client.key_sign.as_deref()))
|
||||
.unwrap_or(0),
|
||||
"packet_loss": packet_loss,
|
||||
"traffic": traffic,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -1034,4 +1111,13 @@ mod tests {
|
||||
let result: Result<(), String> = catch_jni_panic(|| panic!("{}", "kaboom"));
|
||||
assert_eq!(result.unwrap_err(), "kaboom");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_peer_encryption_states() {
|
||||
assert_eq!(encryption_state(Some("same"), Some("same")), 1);
|
||||
assert_eq!(encryption_state(None, None), 2);
|
||||
assert_eq!(encryption_state(Some("local"), None), 3);
|
||||
assert_eq!(encryption_state(None, Some("peer")), 4);
|
||||
assert_eq!(encryption_state(Some("local"), Some("peer")), 5);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
[package]
|
||||
name = "vnt-web"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
vnt-core.workspace = true
|
||||
|
||||
+33
-21
@@ -5,16 +5,20 @@
|
||||
//! - 找不到 pnpm 时:已有产物则告警并沿用;没有产物则报错并给出指引
|
||||
//! - 设置环境变量 VNT_WEB_SKIP_UI_BUILD=1 可完全跳过前端构建
|
||||
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::time::SystemTime;
|
||||
|
||||
fn main() {
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")?;
|
||||
let manifest_dir = Path::new(&manifest_dir);
|
||||
let ui_dir = manifest_dir.join("ui");
|
||||
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 源码变化时重新运行本脚本
|
||||
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");
|
||||
|
||||
if std::env::var("VNT_WEB_SKIP_UI_BUILD").is_ok() {
|
||||
ensure_static_placeholder(&static_dir);
|
||||
return;
|
||||
ensure_static_placeholder(&static_dir)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if static_is_fresh(&ui_dir, &static_dir) {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(pnpm) = find_pnpm() else {
|
||||
@@ -51,20 +55,22 @@ fn main() {
|
||||
println!(
|
||||
"cargo:warning=未找到 pnpm,沿用 vnt-web/static 中已有的前端产物(可能不是最新)"
|
||||
);
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
panic!(
|
||||
return Err(io::Error::other(
|
||||
"未找到 pnpm 且 vnt-web/static 没有前端产物。\n\
|
||||
请安装 Node.js 与 pnpm 后重新构建(cargo 会自动完成前端构建),\n\
|
||||
或从发布包中获取 static 目录放入 vnt-web/。"
|
||||
);
|
||||
或从发布包中获取 static 目录放入 vnt-web/。",
|
||||
)
|
||||
.into());
|
||||
};
|
||||
|
||||
if !ui_dir.join("node_modules").is_dir() {
|
||||
// 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 执行)
|
||||
@@ -80,7 +86,7 @@ fn find_pnpm() -> Option<&'static str> {
|
||||
.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!(
|
||||
"cargo:warning=执行前端构建: {} {} ({})",
|
||||
program,
|
||||
@@ -91,15 +97,21 @@ fn run_or_panic(program: &str, args: &[&str], dir: &Path) {
|
||||
.args(args)
|
||||
.current_dir(dir)
|
||||
.status()
|
||||
.unwrap_or_else(|e| panic!("执行 {} 失败: {}", program, e));
|
||||
.map_err(|error| {
|
||||
io::Error::new(
|
||||
error.kind(),
|
||||
format!("执行 {program} 失败(目录 {}):{error}", dir.display()),
|
||||
)
|
||||
})?;
|
||||
if !status.success() {
|
||||
panic!(
|
||||
return Err(io::Error::other(format!(
|
||||
"前端构建失败: {} {} (exit: {:?})",
|
||||
program,
|
||||
args.join(" "),
|
||||
status.code()
|
||||
);
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// static 产物是否比 UI 源码新
|
||||
@@ -128,16 +140,16 @@ fn newest_mtime(dir: &Path) -> Option<SystemTime> {
|
||||
}
|
||||
|
||||
/// 跳过构建时保证 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() {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
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(
|
||||
static_dir.join("index.html"),
|
||||
"<!doctype html><html><body><p>VNT Web UI 未构建。请安装 pnpm 后重新执行 cargo build,\
|
||||
或取消 VNT_WEB_SKIP_UI_BUILD。</p></body></html>",
|
||||
)
|
||||
.expect("写入占位页面失败");
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+22
-15
@@ -21,7 +21,7 @@ use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
||||
use time::{OffsetDateTime, format_description};
|
||||
use time::{OffsetDateTime, macros::format_description};
|
||||
use tokio::fs;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -205,7 +205,7 @@ impl HttpAppState {
|
||||
|
||||
fn timestamp() -> String {
|
||||
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)
|
||||
.unwrap_or_else(|_| "00:00:00".to_string())
|
||||
}
|
||||
@@ -288,7 +288,7 @@ pub struct StartConfig {
|
||||
|
||||
impl StartConfig {
|
||||
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\"")
|
||||
}
|
||||
Ok(())
|
||||
@@ -649,9 +649,10 @@ pub async fn run_http_server(
|
||||
let handle = service
|
||||
.start_http(addr, token, cancellation.clone())
|
||||
.await?;
|
||||
shutdown_signal().await;
|
||||
let shutdown_result = shutdown_signal().await;
|
||||
cancellation.cancel();
|
||||
handle.await??;
|
||||
shutdown_result?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -751,7 +752,8 @@ fn build_headers_for_path(path: &str) -> HeaderMap {
|
||||
};
|
||||
headers.insert(
|
||||
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 {
|
||||
@@ -1309,7 +1311,7 @@ async fn save_config(Json(req): Json<SaveConfigReq>) -> Json<ApiResponse<()>> {
|
||||
.unwrap_or_else(|| {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.unwrap_or_default()
|
||||
.as_millis();
|
||||
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 {
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("failed to install Ctrl+C handler");
|
||||
.context("failed to install Ctrl+C handler")
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
let terminate = async {
|
||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.expect("failed to install signal handler")
|
||||
.recv()
|
||||
.await;
|
||||
let mut signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.context("failed to install terminate signal handler")?;
|
||||
signal.recv().await;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let terminate = std::future::pending::<()>();
|
||||
let terminate = std::future::pending::<anyhow::Result<()>>();
|
||||
|
||||
tokio::select! {
|
||||
_ = ctrl_c => {},
|
||||
_ = terminate => {},
|
||||
result = ctrl_c => result?,
|
||||
result = terminate => result?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_peers(
|
||||
@@ -1797,6 +1800,10 @@ network_code = "test"
|
||||
|
||||
let legacy: StartConfig = toml::from_str(&format!("{base}no_tun = true\n")).unwrap();
|
||||
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 互不影响
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "vnt-web-ui",
|
||||
"private": true,
|
||||
"version": "2.0.1",
|
||||
"license": "Apache-2.0",
|
||||
"version": "2.0.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -9,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "catalog:",
|
||||
"qrcode": "1.5.4",
|
||||
"vue": "catalog:",
|
||||
"vue-router": "catalog:"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
export const NETWORK_QR_TYPE = "vnt-network";
|
||||
export const NETWORK_QR_VERSION = 1;
|
||||
|
||||
export const buildNetworkQrPayload = (form) => {
|
||||
const networkCode = String(form.network_code || "").trim();
|
||||
const servers = (form.server || []).map((server) => String(server).trim()).filter(Boolean);
|
||||
const mtu = Number(form.mtu || 1380);
|
||||
|
||||
if (!networkCode) throw new Error("组网编号不能为空");
|
||||
if (servers.length === 0) throw new Error("服务器地址不能为空");
|
||||
if (!Number.isInteger(mtu) || mtu < 576 || mtu > 9000) {
|
||||
throw new Error("MTU 必须在 576-9000 之间");
|
||||
}
|
||||
|
||||
return {
|
||||
type: NETWORK_QR_TYPE,
|
||||
version: NETWORK_QR_VERSION,
|
||||
network_code: networkCode,
|
||||
server: servers,
|
||||
mtu,
|
||||
password: String(form.password || ""),
|
||||
};
|
||||
};
|
||||
@@ -19,7 +19,7 @@ const message = ref("");
|
||||
const downloaded = ref(0);
|
||||
const contentLength = ref(0);
|
||||
|
||||
const currentVersion = computed(() => app.version || "2.0.1");
|
||||
const currentVersion = computed(() => app.version || "2.0.2");
|
||||
const progress = computed(() => {
|
||||
if (!contentLength.value) return 0;
|
||||
return Math.min(100, Math.round((downloaded.value / contentLength.value) * 100));
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import QRCode from "qrcode";
|
||||
import { useAppStore } from "../stores/app";
|
||||
import { useUiStore } from "../stores/ui";
|
||||
import { deleteConfig } from "../api";
|
||||
import { deleteConfig, getConfig } from "../api";
|
||||
import EmptyState from "../components/EmptyState.vue";
|
||||
import AppModal from "../components/AppModal.vue";
|
||||
import ConfigEditor from "./ConfigEditor.vue";
|
||||
import { parseTomlToForm } from "../utils/toml";
|
||||
import { buildNetworkQrPayload } from "../utils/networkQr";
|
||||
|
||||
const app = useAppStore();
|
||||
const ui = useUiStore();
|
||||
|
||||
const showEditor = ref(false);
|
||||
const editorFileName = ref(null);
|
||||
const showQr = ref(false);
|
||||
const qrLoading = ref(false);
|
||||
const qrImage = ref("");
|
||||
const qrConfig = ref(null);
|
||||
const qrError = ref("");
|
||||
|
||||
// 配置对应的实例运行状态(无实例返回 null)
|
||||
const instStatus = (fileName) => {
|
||||
@@ -34,6 +43,29 @@ const onSaved = () => {
|
||||
app.fetchConfigList();
|
||||
};
|
||||
|
||||
const openQr = async (fileName) => {
|
||||
showQr.value = true;
|
||||
qrLoading.value = true;
|
||||
qrImage.value = "";
|
||||
qrConfig.value = null;
|
||||
qrError.value = "";
|
||||
try {
|
||||
const form = parseTomlToForm(await getConfig(fileName));
|
||||
const payload = buildNetworkQrPayload(form);
|
||||
qrConfig.value = payload;
|
||||
qrImage.value = await QRCode.toDataURL(JSON.stringify(payload), {
|
||||
errorCorrectionLevel: "M",
|
||||
width: 360,
|
||||
margin: 2,
|
||||
color: { dark: "#020617", light: "#ffffff" },
|
||||
});
|
||||
} catch (e) {
|
||||
qrError.value = e.message || "二维码生成失败";
|
||||
} finally {
|
||||
qrLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (fileName) => {
|
||||
const ok = await ui.confirm({
|
||||
title: "删除配置",
|
||||
@@ -79,19 +111,30 @@ onMounted(() => app.fetchConfigList());
|
||||
class="card group relative flex min-h-[140px] cursor-pointer flex-col justify-between overflow-hidden"
|
||||
@click="openEditor(cfg.file_name)"
|
||||
>
|
||||
<button
|
||||
class="absolute right-3 top-3 z-10 rounded-lg p-1.5 text-slate-400 transition hover:bg-indigo-50 hover:text-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:hover:bg-indigo-500/10 dark:hover:text-indigo-400"
|
||||
type="button"
|
||||
title="显示加入网络二维码"
|
||||
aria-label="显示加入网络二维码"
|
||||
@click.stop="openQr(cfg.file_name)"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h7v7H3V3zm11 0h7v7h-7V3zM3 14h7v7H3v-7zm12 0h2m4 0v2m-6 2h2v3m4-3v3h-2" />
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
v-if="instStatus(cfg.file_name) === 'running'"
|
||||
class="absolute right-0 top-0 rounded-bl bg-green-500 px-2 py-1 text-xs text-white"
|
||||
class="absolute right-12 top-3 rounded bg-green-500 px-2 py-1 text-xs text-white"
|
||||
>
|
||||
运行中
|
||||
</div>
|
||||
<div
|
||||
v-else-if="instStatus(cfg.file_name) === 'starting'"
|
||||
class="absolute right-0 top-0 rounded-bl bg-indigo-500 px-2 py-1 text-xs text-white"
|
||||
class="absolute right-12 top-3 rounded bg-indigo-500 px-2 py-1 text-xs text-white"
|
||||
>
|
||||
启动中
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-start pr-28">
|
||||
<div class="mr-3 mt-1 rounded-lg bg-indigo-50 p-2 text-indigo-600 dark:bg-indigo-500/10 dark:text-indigo-400">
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -131,6 +174,36 @@ onMounted(() => app.fetchConfigList());
|
||||
@close="showEditor = false"
|
||||
@saved="onSaved"
|
||||
/>
|
||||
|
||||
<AppModal :show="showQr" panel-class="w-full max-w-md" @close="showQr = false">
|
||||
<template #header>
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-slate-900 dark:text-white">扫码加入网络</h2>
|
||||
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">使用 VNT 安卓客户端扫描</p>
|
||||
</div>
|
||||
<button class="btn-ghost btn-sm" type="button" @click="showQr = false">关闭</button>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="p-6">
|
||||
<div v-if="qrLoading" class="py-20 text-center text-sm muted">正在生成二维码…</div>
|
||||
<div v-else-if="qrError" class="rounded-lg bg-red-50 p-4 text-sm text-red-600 dark:bg-red-500/10 dark:text-red-400">
|
||||
{{ qrError }}
|
||||
</div>
|
||||
<div v-else-if="qrConfig" class="space-y-5">
|
||||
<div class="mx-auto w-fit rounded-xl border border-slate-200 bg-white p-3 shadow-sm">
|
||||
<img :src="qrImage" class="h-auto w-full max-w-[320px]" alt="VNT 加入网络二维码" />
|
||||
</div>
|
||||
<dl class="space-y-2 rounded-lg bg-slate-50 p-4 text-sm dark:bg-slate-800/60">
|
||||
<div class="flex gap-3"><dt class="w-20 shrink-0 muted">组网编号</dt><dd class="min-w-0 break-all font-mono text-slate-900 dark:text-white">{{ qrConfig.network_code }}</dd></div>
|
||||
<div class="flex gap-3"><dt class="w-20 shrink-0 muted">服务器</dt><dd class="min-w-0 break-all font-mono text-slate-900 dark:text-white">{{ qrConfig.server.join(", ") }}</dd></div>
|
||||
<div class="flex gap-3"><dt class="w-20 shrink-0 muted">MTU</dt><dd class="font-mono text-slate-900 dark:text-white">{{ qrConfig.mtu }}</dd></div>
|
||||
<div class="flex gap-3"><dt class="w-20 shrink-0 muted">加密密码</dt><dd class="text-slate-900 dark:text-white">{{ qrConfig.password ? "已包含" : "未设置" }}</dd></div>
|
||||
</dl>
|
||||
<p class="text-xs leading-5 text-amber-600 dark:text-amber-400">二维码包含组网凭据,请仅分享给可信设备。</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user