Compare commits

...

14 Commits

Author SHA1 Message Date
Sijie.Sun
c23b544c34 tcp accept should retry when encoutering some kinds of error (#565)
* tcp accept should retry when encoutering some kinds of error

bump version to v2.1.2

* persistent temporary machine id
2025-01-14 08:55:48 +08:00
Sijie.Sun
9d76b86f49 fix bugs (#561)
1. if peers disconnected before stop session, may crash at the assert.
2. bind_device flag should take effect on manual connector.
2025-01-12 00:16:38 +08:00
Sijie.Sun
bb0ccca3e5 allow manually specify public address of listeners (#556) 2025-01-10 09:25:14 +08:00
Sijie.Sun
306817ae9a allow listener retry listen (#554) 2025-01-09 00:01:41 +08:00
Sijie.Sun
d2ec60e108 batch recv for udp proxy (#552) 2025-01-07 23:52:18 +08:00
Sijie.Sun
e016aeddeb optimize pingpong conn close condition (#549)
if received some packets from peer, only close conn after enough
rounds of pingpong
2025-01-07 22:42:57 +08:00
Sijie.Sun
a4419a31fd fix peer rpc compatibility issue (#548)
every rpc packet should contains descriptor if sent to old version et.
2025-01-06 23:30:56 +08:00
Sijie.Sun
34e4e907a9 bump version to v2.1.1 (#533) 2024-12-24 10:40:57 -05:00
Sijie.Sun
2f4a097787 fix android (#531) 2024-12-23 19:38:32 -05:00
Sijie.Sun
f3de00be37 support pause a network (#528) 2024-12-23 09:29:59 +08:00
Sijie.Sun
4cf61f0d4a fix web show dup entry for same machine (#526) 2024-12-21 11:51:01 -05:00
Sijie.Sun
4e5915f98e save api host in local storage (#523) 2024-12-21 01:29:54 +08:00
Sijie.Sun
870eca9e9f optimize easytier-web (#522)
1. use default compress level for tower_http. the best level consume
lots of memory
2. add more help message and command line arg.
2024-12-21 01:27:39 +08:00
Sijie.Sun
25ed41caf5 use correct config server url (#519) 2024-12-20 00:21:22 +08:00
52 changed files with 1334 additions and 570 deletions

View File

@@ -21,7 +21,7 @@ on:
version:
description: 'Version for this release'
type: string
default: 'v2.1.0'
default: 'v2.1.2'
required: true
make_latest:
description: 'Mark this release as latest'

29
Cargo.lock generated
View File

@@ -1830,7 +1830,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]]
name = "easytier"
version = "2.1.0"
version = "2.1.2"
dependencies = [
"aes-gcm",
"anyhow",
@@ -1926,7 +1926,7 @@ dependencies = [
[[package]]
name = "easytier-gui"
version = "2.1.0"
version = "2.1.2"
dependencies = [
"anyhow",
"chrono",
@@ -1979,6 +1979,7 @@ dependencies = [
"axum-login",
"axum-messages",
"base64 0.22.1",
"chrono",
"clap",
"dashmap",
"easytier",
@@ -1987,12 +1988,14 @@ dependencies = [
"password-auth",
"rand 0.8.5",
"rust-embed",
"rust-i18n",
"rusttype",
"sea-orm",
"sea-orm-migration",
"serde",
"serde_json",
"sqlx",
"sys-locale",
"thiserror 1.0.63",
"tokio",
"tower-http",
@@ -7312,9 +7315,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
version = "2.1.0"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c24f1ab82d336e09f5f1094a4d9227c99ac26cce263bfdf8136897cc6db6f1d0"
checksum = "d3889b392db6d32a105d3757230ea0220090b8f94c90d3e60b6c5eb91178ab1b"
dependencies = [
"anyhow",
"bytes",
@@ -7350,7 +7353,7 @@ dependencies = [
"tauri-runtime",
"tauri-runtime-wry",
"tauri-utils",
"thiserror 2.0.2",
"thiserror 1.0.63",
"tokio",
"tray-icon",
"url",
@@ -7562,9 +7565,9 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "2.2.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cce18d43f80d4aba3aa8a0c953bbe835f3d0f2370aca75e8dbb14bd4bab27958"
checksum = "a1ef7363e7229ac8d04e8a5d405670dbd43dde8fc4bc3bc56105c35452d03784"
dependencies = [
"dpi",
"gtk",
@@ -7574,16 +7577,16 @@ dependencies = [
"serde",
"serde_json",
"tauri-utils",
"thiserror 2.0.2",
"thiserror 1.0.63",
"url",
"windows 0.58.0",
]
[[package]]
name = "tauri-runtime-wry"
version = "2.2.0"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f442a38863e10129ffe2cec7bd09c2dcf8a098a3a27801a476a304d5bb991d2"
checksum = "62fa2068e8498ad007b54d5773d03d57c3ff6dd96f8c8ce58beff44d0d5e0d30"
dependencies = [
"gtk",
"http 1.1.0",
@@ -9259,13 +9262,12 @@ dependencies = [
[[package]]
name = "wry"
version = "0.47.0"
version = "0.46.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "553ca1ce149982123962fac2506aa75b8b76288779a77e72b12fa2fc34938647"
checksum = "2f8c948dc5f7c23bd93ba03b85b7f679852589bb78e150424d993171e4ef7b73"
dependencies = [
"base64 0.22.1",
"block2",
"cookie",
"crossbeam-channel",
"dpi",
"dunce",
@@ -9290,7 +9292,6 @@ dependencies = [
"soup3",
"tao-macros",
"thiserror 1.0.63",
"url",
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com",

View File

@@ -31,6 +31,7 @@ EasyTier is a simple, safe and decentralized VPN networking solution implemented
- **High Availability**: Supports multi-path and switches to healthy paths when high packet loss or network errors are detected.
- **IPv6 Support**: Supports networking using IPv6.
- **Multiple Protocol Types**: Supports communication between nodes using protocols such as WebSocket and QUIC.
- **Web Management Interface**: Provides a [web-based management](https://easytier.cn/web) interface for easy configuration and monitoring.
## Installation
@@ -52,7 +53,7 @@ EasyTier is a simple, safe and decentralized VPN networking solution implemented
4. **Install by Docker Compose**
Please visit the [EasyTier Official Website](https://www.easytier.top/en/) to view the full documentation.
Please visit the [EasyTier Official Website](https://www.easytier.cn/en/) to view the full documentation.
5. **Install by script (For Linux Only)**
@@ -200,20 +201,20 @@ Subnet proxy information will automatically sync to each node in the virtual net
### Networking without Public IP
EasyTier supports networking using shared public nodes. The currently deployed shared public node is ``tcp://public.easytier.top:11010``.
EasyTier supports networking using shared public nodes. The currently deployed shared public node is ``tcp://public.easytier.cn:11010``.
When using shared nodes, each node entering the network needs to provide the same ``--network-name`` and ``--network-secret`` parameters as the unique identifier of the network.
Taking two nodes as an example, Node A executes:
```sh
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
```
Node B executes
```sh
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
```
After the command is successfully executed, Node A can access Node B through the virtual IP 10.144.144.2.
@@ -286,7 +287,7 @@ Run you own public server cluster is exactly same as running an virtual network,
You can also join the official public server cluster with following command:
```
sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.top:11010
sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.cn:11010
```
@@ -296,10 +297,8 @@ You can use ``easytier-core --help`` to view all configuration items
## Roadmap
- [ ] Improve documentation and user guides.
- [ ] Support features such as encryption, TCP hole punching, etc.
- [ ] Support features such TCP hole punching, KCP, FEC etc.
- [ ] Support iOS.
- [ ] Support Web configuration management.
## Community and Contribution

View File

@@ -8,7 +8,7 @@
[简体中文](/README_CN.md) | [English](/README.md)
**请访问 [EasyTier 官网](https://www.easytier.top/) 以查看完整的文档。**
**请访问 [EasyTier 官网](https://www.easytier.cn/) 以查看完整的文档。**
一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
@@ -31,6 +31,7 @@
- **高可用性**:支持多路径和在检测到高丢包率或网络错误时切换到健康路径。
- **IPV6 支持**:支持利用 IPV6 组网。
- **多协议类型**: 支持使用 WebSocket、QUIC 等协议进行节点间通信。
- **Web 管理界面**:支持通过 [Web 界面](https://easytier.cn)管理节点。
## 安装
@@ -52,7 +53,7 @@
4. **通过Docker Compose安装**
请访问 [EasyTier 官网](https://www.easytier.top/) 以查看完整的文档。
请访问 [EasyTier 官网](https://www.easytier.cn/) 以查看完整的文档。
5. **使用一键脚本安装 (仅适用于 Linux)**
@@ -199,20 +200,20 @@ sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
### 无公网IP组网
EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://public.easytier.top:11010``。
EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://public.easytier.cn:11010``。
使用共享节点时,需要每个入网节点提供相同的 ``--network-name`` 和 ``--network-secret`` 参数,作为网络的唯一标识。
以双节点为例,节点 A 执行:
```sh
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
```
节点 B 执行
```sh
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
```
命令执行成功后,节点 A 即可通过虚拟 IP 10.144.144.2 访问节点 B。
@@ -289,7 +290,7 @@ connected_clients:
也可以使用以下命令加入官方公共服务器集群,后续将实现公共服务器集群的节点间负载均衡:
```
sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.top:11010
sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.cn:11010
```
### 其他配置
@@ -299,9 +300,8 @@ sudo easytier-core --network-name easytier --network-secret easytier -p tcp://pu
## 路线图
- [ ] 完善文档和用户指南。
- [ ] 支持 TCP 打洞等特性。
- [ ] 支持 TCP 打洞、KCP、FEC 等特性。
- [ ] 支持 iOS。
- [ ] 支持 Web 配置管理。
## 社区和贡献

View File

@@ -1,7 +1,7 @@
{
"name": "easytier-gui",
"type": "module",
"version": "2.1.0",
"version": "2.1.2",
"private": true,
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
"scripts": {
@@ -35,14 +35,16 @@
"@primevue/auto-import-resolver": "^4.1.0",
"@tauri-apps/api": "2.1.0",
"@tauri-apps/cli": "2.1.0",
"@types/default-gateway": "^7.2.2",
"@types/node": "^22.7.4",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-vue": "^5.1.4",
"@vue-macros/volar": "0.30.5",
"autoprefixer": "^10.4.20",
"cidr-tools": "^11.0.2",
"default-gateway": "^7.2.2",
"eslint": "^9.12.0",
"eslint-plugin-format": "^0.1.2",
"internal-ip": "^8.0.0",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.13",
"typescript": "^5.6.2",
@@ -58,4 +60,4 @@
"vue-i18n": "^10.0.0",
"vue-tsc": "^2.1.10"
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "easytier-gui"
version = "2.1.0"
version = "2.1.2"
description = "EasyTier GUI"
authors = ["you"]
edition = "2021"
@@ -15,7 +15,8 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2.0.0-rc", features = [] }
[dependencies]
tauri = { version = "2.1", features = [
# wry 0.47 may crash on android, see https://github.com/EasyTier/EasyTier/issues/527
tauri = { version = "=2.0.6", features = [
"tray-icon",
"image-png",
"image-ico",

View File

@@ -39,7 +39,7 @@
"vpnservice:allow-prepare-vpn",
"vpnservice:allow-start-vpn",
"vpnservice:allow-stop-vpn",
"vpnservice:allow-register-listener",
"vpnservice:allow-registerListener",
"os:default",
"os:allow-os-type",
"os:allow-arch",

View File

@@ -141,7 +141,6 @@ pub fn run() {
process::exit(0);
}
#[cfg(not(target_os = "android"))]
utils::setup_panic_handler();
let mut builder = tauri::Builder::default();

View File

@@ -17,7 +17,7 @@
"createUpdaterArtifacts": false
},
"productName": "easytier-gui",
"version": "2.1.0",
"version": "2.1.2",
"identifier": "com.kkrainbow.easytier",
"plugins": {},
"app": {

View File

@@ -49,9 +49,9 @@ async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[]) {
return
}
console.log('start vpn')
console.log('start vpn service', ipv4Addr, cidr, routes)
const start_ret = await start_vpn({
ipv4Addr: `${ipv4Addr}`,
ipv4Addr: `${ipv4Addr}/${cidr}`,
routes,
disallowedApplications: ['com.kkrainbow.easytier'],
mtu: 1300,
@@ -113,6 +113,7 @@ function getRoutesForVpn(routes: Route[]): string[] {
}
async function onNetworkInstanceChange() {
console.error('vpn service watch network instance change ids', JSON.stringify(networkStore.networkInstanceIds))
const insts = networkStore.networkInstanceIds
if (!insts) {
await doStopVpn()
@@ -142,7 +143,7 @@ async function onNetworkInstanceChange() {
const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
if (ipChanged || routesChanged) {
console.log('virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
console.info('vpn service virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
try {
await doStopVpn()
}
@@ -154,7 +155,7 @@ async function onNetworkInstanceChange() {
await doStartVpn(virtual_ip, 24, routes)
}
catch (e) {
console.error('start vpn failed, clear all network insts.', e)
console.error('start vpn service failed, clear all network insts.', e)
networkStore.clearNetworkInstances()
await retainNetworkInstance(networkStore.networkInstanceIds)
}
@@ -175,6 +176,7 @@ async function watchNetworkInstance() {
}
subscribe_running = false
})
console.error('vpn service watch network instance')
}
export async function initMobileVpnService() {

View File

@@ -250,7 +250,12 @@ onBeforeMount(async () => {
onMounted(async () => {
if (type() === 'android') {
await initMobileVpnService()
try {
await initMobileVpnService()
console.error("easytier init vpn service done")
} catch (e: any) {
console.error("easytier init vpn service failed", e)
}
}
})

View File

@@ -1,9 +1,11 @@
import { networkInterfaces } from 'node:os'
import path from 'node:path'
import process from 'node:process'
import VueI18n from '@intlify/unplugin-vue-i18n/vite'
import { PrimeVueResolver } from '@primevue/auto-import-resolver'
import Vue from '@vitejs/plugin-vue'
import { internalIpV4Sync } from 'internal-ip'
import { containsCidr, parseCidr } from 'cidr-tools'
import { gateway4sync } from 'default-gateway'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import VueMacros from 'unplugin-vue-macros/vite'
@@ -13,6 +15,20 @@ import { defineConfig } from 'vite'
import VueDevTools from 'vite-plugin-vue-devtools'
import Layouts from 'vite-plugin-vue-layouts'
function findIp(gateway: string) {
// Look for the matching interface in all local interfaces
console.log('gateway', gateway)
for (const addresses of Object.values(networkInterfaces())) {
if (!addresses)
continue
for (const { cidr } of addresses) {
if (cidr && containsCidr(cidr, gateway)) {
return parseCidr(cidr).ip
}
}
}
}
const host = process.env.TAURI_DEV_HOST
// https://vitejs.dev/config/
@@ -99,10 +115,10 @@ export default defineConfig(async () => ({
},
hmr: host
? {
protocol: 'ws',
host: internalIpV4Sync(),
port: 1430,
}
protocol: 'ws',
host: findIp(gateway4sync().gateway),
port: 1430,
}
: undefined,
},
}))

View File

@@ -2,6 +2,7 @@
name = "easytier-web"
version = "0.1.0"
edition = "2021"
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
[dependencies]
easytier = { path = "../easytier" }
@@ -36,6 +37,8 @@ rusttype = "0.9.3"
imageproc = "0.23.0"
rust-i18n = "3"
sys-locale = "0.3"
clap = { version = "4.4.8", features = [
"string",
"unicode",
@@ -50,3 +53,5 @@ uuid = { version = "1.5.0", features = [
"macro-diagnostics",
"serde",
] }
chrono = { version = "0.4.37", features = ["serde"] }

View File

@@ -303,7 +303,7 @@ function showEventLogs() {
<template>
<div class="frontend-lib">
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" class="w-2/3 h-auto">
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" class="w-2/3 h-auto max-w-full">
<ScrollPanel v-if="dialogHeader === 'vpn_portal_config'">
<pre>{{ dialogContent }}</pre>
</ScrollPanel>

View File

@@ -1,5 +1,6 @@
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { Md5 } from 'ts-md5'
import { UUID } from './utils';
export interface ValidateConfigResponse {
toml_config: string;
@@ -31,6 +32,11 @@ export interface Summary {
device_count: number;
}
export interface ListNetworkInstanceIdResponse {
running_inst_ids: Array<UUID>,
disabled_inst_ids: Array<UUID>,
}
export class ApiClient {
private client: AxiosInstance;
private authFailedCb: Function | undefined;
@@ -141,6 +147,17 @@ export class ApiClient {
return response.machines;
}
public async list_deivce_instance_ids(machine_id: string): Promise<ListNetworkInstanceIdResponse> {
const response = await this.client.get<any, ListNetworkInstanceIdResponse>('/machines/' + machine_id + '/networks');
return response;
}
public async update_device_instance_state(machine_id: string, inst_id: string, disabled: boolean): Promise<undefined> {
await this.client.put<string>('/machines/' + machine_id + '/networks/' + inst_id, {
disabled: disabled,
});
}
public async get_network_info(machine_id: string, inst_id: string): Promise<any> {
const response = await this.client.get<any, Record<string, any>>('/machines/' + machine_id + '/networks/info/' + inst_id);
return response.info.map;

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { Toolbar, IftaLabel, Select, Button, ConfirmPopup, Dialog, useConfirm, useToast } from 'primevue';
import { NetworkTypes, Status, Utils, Api, } from 'easytier-frontend-lib';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { watch, computed, onMounted, onUnmounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
const props = defineProps<{
@@ -33,9 +33,16 @@ const isEditing = ref(false);
const showCreateNetworkDialog = ref(false);
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
const listInstanceIdResponse = ref<Api.ListNetworkInstanceIdResponse | undefined>(undefined);
const instanceIdList = computed(() => {
let insts = deviceInfo.value?.running_network_instances || [];
let options = insts.map((instance: string) => {
let insts = new Set(deviceInfo.value?.running_network_instances || []);
let t = listInstanceIdResponse.value;
if (t) {
t.running_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u)));
t.disabled_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u)));
}
let options = Array.from(insts).map((instance: string) => {
return { uuid: instance };
});
return options;
@@ -51,6 +58,53 @@ const selectedInstanceId = computed({
}
});
const needShowNetworkStatus = computed(() => {
if (!selectedInstanceId.value) {
// nothing selected
return false;
}
if (networkIsDisabled.value) {
// network is disabled
return false;
}
return true;
})
const networkIsDisabled = computed(() => {
if (!selectedInstanceId.value) {
return false;
}
return listInstanceIdResponse.value?.disabled_inst_ids.map(Utils.UuidToStr).includes(selectedInstanceId.value?.uuid);
});
watch(selectedInstanceId, async (newVal, oldVal) => {
if (newVal?.uuid !== oldVal?.uuid && networkIsDisabled.value) {
await loadDisabledNetworkConfig();
}
});
const disabledNetworkConfig = ref<NetworkTypes.NetworkConfig | undefined>(undefined);
const loadDisabledNetworkConfig = async () => {
disabledNetworkConfig.value = undefined;
if (!deviceId.value || !selectedInstanceId.value) {
return;
}
let ret = await props.api?.get_network_config(deviceId.value, selectedInstanceId.value.uuid);
disabledNetworkConfig.value = ret;
}
const updateNetworkState = async (disabled: boolean) => {
if (!deviceId.value || !selectedInstanceId.value) {
return;
}
await props.api?.update_device_instance_state(deviceId.value, selectedInstanceId.value.uuid, disabled);
await loadNetworkInstanceIds();
}
const confirm = useConfirm();
const confirmDeleteNetwork = (event: any) => {
confirm.require({
@@ -128,6 +182,15 @@ const editNetwork = async () => {
}
}
const loadNetworkInstanceIds = async () => {
if (!deviceId.value) {
return;
}
listInstanceIdResponse.value = await props.api?.list_deivce_instance_ids(deviceId.value);
console.debug("loadNetworkInstanceIds", listInstanceIdResponse.value);
}
const loadDeviceInfo = async () => {
if (!deviceId.value || !instanceId.value) {
return;
@@ -146,7 +209,7 @@ const loadDeviceInfo = async () => {
let periodFunc = new Utils.PeriodicTask(async () => {
try {
await loadDeviceInfo();
await Promise.all([loadNetworkInstanceIds(), loadDeviceInfo()]);
} catch (e) {
console.debug(e);
}
@@ -188,8 +251,23 @@ onUnmounted(() => {
</template>
</Toolbar>
<Status v-bind:cur-network-inst="curNetworkInfo" v-if="!!selectedInstanceId">
</Status>
<!-- For running network, show the status -->
<div v-if="needShowNetworkStatus">
<Status v-bind:cur-network-inst="curNetworkInfo" v-if="needShowNetworkStatus">
</Status>
<center>
<Button @click="updateNetworkState(true)" label="Disable Network" severity="warn" />
</center>
</div>
<!-- For disabled network, show the config -->
<div v-if="networkIsDisabled">
<Config :cur-network="disabledNetworkConfig" @run-network="updateNetworkState(false)"
v-if="disabledNetworkConfig" />
<div v-else>
<div class="text-center text-xl"> Network is disabled, Loading config... </div>
</div>
</div>
<div class="grid grid-cols-1 gap-4 place-content-center h-full" v-if="!selectedInstanceId">
<div class="text-center text-xl"> Select or create a network instance to manage </div>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { Card, InputText, Password, Button, AutoComplete } from 'primevue';
import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast';
@@ -20,8 +20,60 @@ const registerPassword = ref('');
const captcha = ref('');
const captchaSrc = computed(() => api.value.captcha_url());
interface ApiHost {
value: string;
usedAt: number;
}
const isValidHttpUrl = (s: string): boolean => {
let url;
try {
url = new URL(s);
} catch (_) {
return false;
}
return url.protocol === "http:" || url.protocol === "https:";
}
const cleanAndLoadApiHosts = (): Array<ApiHost> => {
const maxHosts = 10;
const apiHosts = localStorage.getItem('apiHosts');
if (apiHosts) {
const hosts: Array<ApiHost> = JSON.parse(apiHosts);
// sort by usedAt
hosts.sort((a, b) => b.usedAt - a.usedAt);
// only keep the first 10
if (hosts.length > maxHosts) {
hosts.splice(maxHosts);
}
localStorage.setItem('apiHosts', JSON.stringify(hosts));
return hosts;
} else {
return [];
}
};
const saveApiHost = (host: string) => {
console.log('Save API Host:', host);
if (!isValidHttpUrl(host)) {
console.error('Invalid API Host:', host);
return;
}
let hosts = cleanAndLoadApiHosts();
const newHost: ApiHost = { value: host, usedAt: Date.now() };
hosts = hosts.filter((h) => h.value !== host);
hosts.push(newHost);
localStorage.setItem('apiHosts', JSON.stringify(hosts));
};
const onSubmit = async () => {
// Add your login logic here
saveApiHost(apiHost.value);
const credential: Api.Credential = { username: username.value, password: password.value, };
let ret = await api.value?.login(credential);
if (ret.success) {
@@ -36,6 +88,7 @@ const onSubmit = async () => {
};
const onRegister = async () => {
saveApiHost(apiHost.value);
const credential: Api.Credential = { username: registerUsername.value, password: registerPassword.value };
const registerReq: Api.RegisterData = { credentials: credential, captcha: captcha.value };
let ret = await api.value?.register(registerReq);
@@ -47,17 +100,36 @@ const onRegister = async () => {
}
};
const defaultApiHost = 'http://easytier.cn:11211'
const apiHost = ref<string>(defaultApiHost)
const getInitialApiHost = (): string => {
const hosts = cleanAndLoadApiHosts();
if (hosts.length > 0) {
return hosts[0].value;
} else {
return defaultApiHost;
}
};
const defaultApiHost = 'https://config-server.easytier.cn'
const apiHost = ref<string>(getInitialApiHost())
const apiHostSuggestions = ref<Array<string>>([])
const apiHostSearch = async (event: { query: string }) => {
apiHostSuggestions.value = [];
let hosts = cleanAndLoadApiHosts();
if (event.query) {
apiHostSuggestions.value.push(event.query);
}
apiHostSuggestions.value.push(defaultApiHost);
hosts.forEach((host) => {
apiHostSuggestions.value.push(host.value);
});
}
onMounted(() => {
let hosts = cleanAndLoadApiHosts();
if (hosts.length === 0) {
saveApiHost(defaultApiHost);
}
});
</script>
<template>
@@ -87,7 +159,7 @@ const apiHostSearch = async (event: { query: string }) => {
</div>
<div class="flex items-center justify-between">
<Button label="Register" type="button" class="w-full"
@click="$router.replace({ name: 'register' })" severity="secondary" />
@click="saveApiHost(apiHost); $router.replace({ name: 'register' })" severity="secondary" />
</div>
</form>
@@ -111,7 +183,7 @@ const apiHostSearch = async (event: { query: string }) => {
</div>
<div class="flex items-center justify-between">
<Button label="Back to Login" type="button" class="w-full"
@click="$router.replace({ name: 'login' })" severity="secondary" />
@click="saveApiHost(apiHost); $router.replace({ name: 'login' })" severity="secondary" />
</div>
</form>
</template>

View File

@@ -0,0 +1,24 @@
_version: 2
cli:
db:
en: "path to the sqlite3 database file, used to save all the data"
zh-CN: "sqlite3 数据库文件路径, 用于保存所有数据"
console_log_level:
en: "The log level for the console logger. Possible values: trace, debug, info, warn, error"
zh-CN: "控制台日志级别。可能的值trace, debug, info, warn, error"
file_log_level:
en: "The log level for the file logger. Possible values: trace, debug, info, warn, error"
zh-CN: "文件日志级别。可能的值trace, debug, info, warn, error"
file_log_dir:
en: "The directory to save the log files, default is the current directory"
zh-CN: "保存日志文件的目录,默认为当前目录"
config_server_port:
en: "The port to listen for the config server, used by the easytier-core to connect to"
zh-CN: "配置服务器的监听端口,用于被 easytier-core 连接"
config_server_protocol:
en: "The protocol to listen for the config server, used by the easytier-core to connect to"
zh-CN: "配置服务器的监听协议,用于被 easytier-core 连接, 可能的值udp, tcp"
api_server_port:
en: "The port to listen for the restful server, acting as ApiHost and used by the web frontend"
zh-CN: "restful 服务器的监听端口,作为 ApiHost 并被 web 前端使用"

View File

@@ -93,7 +93,7 @@ impl ClientManager {
.map(|item| item.value().clone())
}
pub fn list_machine_by_token(&self, token: String) -> Vec<url::Url> {
pub async fn list_machine_by_token(&self, token: String) -> Vec<url::Url> {
self.storage.list_token_clients(&token)
}

View File

@@ -1,4 +1,4 @@
use std::{fmt::Debug, sync::Arc};
use std::{fmt::Debug, str::FromStr as _, sync::Arc};
use easytier::{
common::scoped_task::ScopedTask,
@@ -15,6 +15,8 @@ use easytier::{
};
use tokio::sync::{broadcast, RwLock};
use crate::db::ListNetworkProps;
use super::storage::{Storage, StorageToken, WeakRefStorage};
#[derive(Debug)]
@@ -87,10 +89,20 @@ impl WebServerService for SessionRpcService {
.map(Into::into)
.unwrap_or(uuid::Uuid::new_v4()),
});
if let Ok(storage) = Storage::try_from(data.storage.clone()) {
storage.add_client(data.storage_token.as_ref().unwrap().clone());
}
}
if let Ok(storage) = Storage::try_from(data.storage.clone()) {
let Ok(report_time) = chrono::DateTime::<chrono::Local>::from_str(&req.report_time)
else {
tracing::error!("Failed to parse report time: {:?}", req.report_time);
return Ok(HeartbeatResponse {});
};
storage.update_client(
data.storage_token.as_ref().unwrap().clone(),
report_time.timestamp(),
);
}
let _ = data.notifier.send(req);
Ok(HeartbeatResponse {})
}
@@ -196,7 +208,11 @@ impl Session {
let local_configs = match storage
.db
.list_network_configs(user_id, Some(req.machine_id.unwrap().into()), true)
.list_network_configs(
user_id,
Some(req.machine_id.unwrap().into()),
ListNetworkProps::EnabledOnly,
)
.await
{
Ok(configs) => configs,

View File

@@ -1,6 +1,6 @@
use std::sync::{Arc, Weak};
use dashmap::{DashMap, DashSet};
use dashmap::DashMap;
use crate::db::Db;
@@ -12,11 +12,19 @@ pub struct StorageToken {
pub machine_id: uuid::Uuid,
}
#[derive(Debug, Clone)]
struct ClientInfo {
client_url: url::Url,
machine_id: uuid::Uuid,
token: String,
report_time: i64,
}
#[derive(Debug)]
pub struct StorageInner {
// some map for indexing
pub token_clients_map: DashMap<String, DashSet<url::Url>>,
pub machine_client_url_map: DashMap<uuid::Uuid, DashSet<url::Url>>,
token_clients_map: DashMap<String, DashMap<uuid::Uuid, ClientInfo>>,
machine_client_url_map: DashMap<uuid::Uuid, ClientInfo>,
pub db: Db,
}
@@ -41,33 +49,57 @@ impl Storage {
}))
}
pub fn add_client(&self, stoken: StorageToken) {
fn remove_mid_to_client_info_map(
map: &DashMap<uuid::Uuid, ClientInfo>,
machine_id: &uuid::Uuid,
client_url: &url::Url,
) {
map.remove_if(&machine_id, |_, v| v.client_url == *client_url);
}
fn update_mid_to_client_info_map(
map: &DashMap<uuid::Uuid, ClientInfo>,
client_info: &ClientInfo,
) {
map.entry(client_info.machine_id)
.and_modify(|e| {
if e.report_time < client_info.report_time {
assert_eq!(e.machine_id, client_info.machine_id);
*e = client_info.clone();
}
})
.or_insert(client_info.clone());
}
pub fn update_client(&self, stoken: StorageToken, report_time: i64) {
let inner = self
.0
.token_clients_map
.entry(stoken.token)
.or_insert_with(DashSet::new);
inner.insert(stoken.client_url.clone());
.entry(stoken.token.clone())
.or_insert_with(DashMap::new);
self.0
.machine_client_url_map
.entry(stoken.machine_id)
.or_insert_with(DashSet::new)
.insert(stoken.client_url.clone());
let client_info = ClientInfo {
client_url: stoken.client_url.clone(),
machine_id: stoken.machine_id,
token: stoken.token.clone(),
report_time,
};
Self::update_mid_to_client_info_map(&inner, &client_info);
Self::update_mid_to_client_info_map(&self.0.machine_client_url_map, &client_info);
}
pub fn remove_client(&self, stoken: &StorageToken) {
self.0.token_clients_map.remove_if(&stoken.token, |_, set| {
set.remove(&stoken.client_url);
Self::remove_mid_to_client_info_map(set, &stoken.machine_id, &stoken.client_url);
set.is_empty()
});
self.0
.machine_client_url_map
.remove_if(&stoken.machine_id, |_, set| {
set.remove(&stoken.client_url);
set.is_empty()
});
Self::remove_mid_to_client_info_map(
&self.0.machine_client_url_map,
&stoken.machine_id,
&stoken.client_url,
);
}
pub fn weak_ref(&self) -> WeakRefStorage {
@@ -78,15 +110,19 @@ impl Storage {
self.0
.machine_client_url_map
.get(&machine_id)
.map(|url| url.iter().next().map(|url| url.clone()))
.flatten()
.map(|info| info.client_url.clone())
}
pub fn list_token_clients(&self, token: &str) -> Vec<url::Url> {
self.0
.token_clients_map
.get(token)
.map(|set| set.iter().map(|url| url.clone()).collect())
.map(|info_map| {
info_map
.iter()
.map(|info| info.value().client_url.clone())
.collect()
})
.unwrap_or_default()
}

View File

@@ -4,7 +4,7 @@ pub mod entity;
use entity::user_running_network_configs;
use sea_orm::{
sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait as _,
prelude::Expr, sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait,
QueryFilter as _, SqlxSqliteConnector, TransactionTrait as _,
};
use sea_orm_migration::MigratorTrait as _;
@@ -14,6 +14,12 @@ use crate::migrator;
type UserIdInDb = i32;
pub enum ListNetworkProps {
All,
EnabledOnly,
DisabledOnly,
}
#[derive(Debug, Clone)]
pub struct Db {
db_path: String,
@@ -115,17 +121,51 @@ impl Db {
Ok(())
}
pub async fn update_network_config_state(
&self,
user_id: UserIdInDb,
network_inst_id: uuid::Uuid,
disabled: bool,
) -> Result<entity::user_running_network_configs::Model, DbErr> {
use entity::user_running_network_configs as urnc;
urnc::Entity::update_many()
.filter(urnc::Column::UserId.eq(user_id))
.filter(urnc::Column::NetworkInstanceId.eq(network_inst_id.to_string()))
.col_expr(urnc::Column::Disabled, Expr::value(disabled))
.col_expr(
urnc::Column::UpdateTime,
Expr::value(chrono::Local::now().fixed_offset()),
)
.exec(self.orm_db())
.await?;
urnc::Entity::find()
.filter(urnc::Column::UserId.eq(user_id))
.filter(urnc::Column::NetworkInstanceId.eq(network_inst_id.to_string()))
.one(self.orm_db())
.await?
.ok_or(DbErr::RecordNotFound(format!(
"Network config not found for user {} and network instance {}",
user_id, network_inst_id
)))
}
pub async fn list_network_configs(
&self,
user_id: UserIdInDb,
device_id: Option<uuid::Uuid>,
only_enabled: bool,
props: ListNetworkProps,
) -> Result<Vec<user_running_network_configs::Model>, DbErr> {
use entity::user_running_network_configs as urnc;
let configs = urnc::Entity::find().filter(urnc::Column::UserId.eq(user_id));
let configs = if only_enabled {
configs.filter(urnc::Column::Disabled.eq(false))
let configs = if matches!(
props,
ListNetworkProps::EnabledOnly | ListNetworkProps::DisabledOnly
) {
configs
.filter(urnc::Column::Disabled.eq(matches!(props, ListNetworkProps::DisabledOnly)))
} else {
configs
};
@@ -140,6 +180,24 @@ impl Db {
Ok(configs)
}
pub async fn get_network_config(
&self,
user_id: UserIdInDb,
device_id: &uuid::Uuid,
network_inst_id: &String,
) -> Result<Option<user_running_network_configs::Model>, DbErr> {
use entity::user_running_network_configs as urnc;
let config = urnc::Entity::find()
.filter(urnc::Column::UserId.eq(user_id))
.filter(urnc::Column::DeviceId.eq(device_id.to_string()))
.filter(urnc::Column::NetworkInstanceId.eq(network_inst_id))
.one(self.orm_db())
.await?;
Ok(config)
}
pub async fn get_user_id<T: ToString>(
&self,
user_name: T,
@@ -167,7 +225,7 @@ impl Db {
mod tests {
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _};
use crate::db::{entity::user_running_network_configs, Db};
use crate::db::{entity::user_running_network_configs, Db, ListNetworkProps};
#[tokio::test]
async fn test_user_network_config_management() {
@@ -209,7 +267,7 @@ mod tests {
assert_ne!(result.update_time, result2.update_time);
assert_eq!(
db.list_network_configs(user_id, Some(device_id), true)
db.list_network_configs(user_id, Some(device_id), ListNetworkProps::All)
.await
.unwrap()
.len(),

View File

@@ -1,11 +1,18 @@
#![allow(dead_code)]
#[macro_use]
extern crate rust_i18n;
use std::sync::Arc;
use clap::{command, Parser};
use easytier::{
common::config::{ConfigLoader, ConsoleLoggerConfig, TomlConfigLoader},
common::{
config::{ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, TomlConfigLoader},
constants::EASYTIER_VERSION,
},
tunnel::udp::UdpTunnelListener,
utils::init_logger,
utils::{init_logger, setup_panic_handler},
};
mod client_manager;
@@ -13,26 +20,97 @@ mod db;
mod migrator;
mod restful;
rust_i18n::i18n!("locales", fallback = "en");
#[derive(Parser, Debug)]
#[command(name = "easytier-core", author, version = EASYTIER_VERSION , about, long_about = None)]
struct Cli {
#[arg(short, long, default_value = "et.db", help = t!("cli.db").to_string())]
db: String,
#[arg(
long,
help = t!("cli.console_log_level").to_string(),
)]
console_log_level: Option<String>,
#[arg(
long,
help = t!("cli.file_log_level").to_string(),
)]
file_log_level: Option<String>,
#[arg(
long,
help = t!("cli.file_log_dir").to_string(),
)]
file_log_dir: Option<String>,
#[arg(
long,
short='c',
default_value = "22020",
help = t!("cli.config_server_port").to_string(),
)]
config_server_port: u16,
#[arg(
long,
short='p',
default_value = "udp",
help = t!("cli.config_server_protocol").to_string(),
)]
config_server_protocol: String,
#[arg(
long,
short='a',
default_value = "11211",
help = t!("cli.api_server_port").to_string(),
)]
api_server_port: u16,
}
#[tokio::main]
async fn main() {
let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
rust_i18n::set_locale(&locale);
setup_panic_handler();
let cli = Cli::parse();
let config = TomlConfigLoader::default();
config.set_console_logger_config(ConsoleLoggerConfig {
level: Some("trace".to_string()),
level: cli.console_log_level,
});
config.set_file_logger_config(FileLoggerConfig {
dir: cli.file_log_dir,
level: cli.file_log_level,
file: None,
});
init_logger(config, false).unwrap();
// let db = db::Db::new(":memory:").await.unwrap();
let db = db::Db::new("et.db").await.unwrap();
let db = db::Db::new(cli.db).await.unwrap();
let listener = UdpTunnelListener::new("udp://0.0.0.0:22020".parse().unwrap());
let listener = UdpTunnelListener::new(
format!(
"{}://0.0.0.0:{}",
cli.config_server_protocol, cli.config_server_port
)
.parse()
.unwrap(),
);
let mut mgr = client_manager::ClientManager::new(db.clone());
mgr.serve(listener).await.unwrap();
let mgr = Arc::new(mgr);
let mut restful_server =
restful::RestfulServer::new("0.0.0.0:11211".parse().unwrap(), mgr.clone(), db)
.await
.unwrap();
let mut restful_server = restful::RestfulServer::new(
format!("0.0.0.0:{}", cli.api_server_port).parse().unwrap(),
mgr.clone(),
db,
)
.await
.unwrap();
restful_server.start().await.unwrap();
tokio::signal::ctrl_c().await.unwrap();
}

View File

@@ -121,7 +121,9 @@ impl RestfulServer {
return Err((StatusCode::UNAUTHORIZED, other_error("No such user").into()));
};
let machines = client_mgr.list_machine_by_token(user.tokens[0].clone());
let machines = client_mgr
.list_machine_by_token(user.tokens[0].clone())
.await;
Ok(GetSummaryJsonResp {
device_count: machines.len() as u32,
@@ -167,7 +169,7 @@ impl RestfulServer {
.deflate(true)
.gzip(true)
.zstd(true)
.quality(tower_http::compression::CompressionLevel::Best);
.quality(tower_http::compression::CompressionLevel::Default);
let app = Router::new()
.route("/api/v1/summary", get(Self::handle_get_summary))

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use axum::extract::{Path, Query};
use axum::extract::Path;
use axum::http::StatusCode;
use axum::routing::{delete, post};
use axum::{extract::State, routing::get, Json, Router};
@@ -13,6 +13,7 @@ use easytier::proto::web::*;
use crate::client_manager::session::Session;
use crate::client_manager::ClientManager;
use crate::db::ListNetworkProps;
use super::users::AuthSession;
use super::{
@@ -46,13 +47,21 @@ struct ColletNetworkInfoJsonReq {
inst_ids: Option<Vec<uuid::Uuid>>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct UpdateNetworkStateJsonReq {
disabled: bool,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct RemoveNetworkJsonReq {
inst_ids: Vec<uuid::Uuid>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ListNetworkInstanceIdsJsonResp(Vec<uuid::Uuid>);
struct ListNetworkInstanceIdsJsonResp {
running_inst_ids: Vec<easytier::proto::common::Uuid>,
disabled_inst_ids: Vec<easytier::proto::common::Uuid>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ListMachineItem {
@@ -190,7 +199,7 @@ impl NetworkApi {
auth_session: AuthSession,
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
Query(payload): Query<ColletNetworkInfoJsonReq>,
Json(payload): Json<ColletNetworkInfoJsonReq>,
) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> {
let result =
Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
@@ -226,10 +235,28 @@ impl NetworkApi {
.list_network_instance(BaseController::default(), ListNetworkInstanceRequest {})
.await
.map_err(convert_rpc_error)?;
Ok(
ListNetworkInstanceIdsJsonResp(ret.inst_ids.into_iter().map(Into::into).collect())
.into(),
)
let running_inst_ids = ret.inst_ids.clone().into_iter().map(Into::into).collect();
// collect networks that are disabled
let disabled_inst_ids = client_mgr
.db()
.list_network_configs(
auth_session.user.unwrap().id(),
Some(machine_id),
ListNetworkProps::DisabledOnly,
)
.await
.map_err(convert_db_error)?
.iter()
.filter_map(|x| x.network_instance_id.clone().try_into().ok())
.collect::<Vec<_>>();
Ok(ListNetworkInstanceIdsJsonResp {
running_inst_ids,
disabled_inst_ids,
}
.into())
}
async fn handle_remove_network_instance(
@@ -270,7 +297,7 @@ impl NetworkApi {
let client_urls = DashSet::new();
for token in tokens {
let urls = client_mgr.list_machine_by_token(token);
let urls = client_mgr.list_machine_by_token(token).await;
for url in urls {
client_urls.insert(url);
}
@@ -289,6 +316,54 @@ impl NetworkApi {
Ok(Json(ListMachineJsonResp { machines }))
}
async fn handle_update_network_state(
auth_session: AuthSession,
State(client_mgr): AppState,
Path((machine_id, inst_id)): Path<(uuid::Uuid, Option<uuid::Uuid>)>,
Json(payload): Json<UpdateNetworkStateJsonReq>,
) -> Result<(), HttpHandleError> {
let Some(inst_id) = inst_id else {
// not implement disable all
return Err((
StatusCode::NOT_IMPLEMENTED,
other_error(format!("Not implemented")).into(),
))
.into();
};
let sess = Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
let cfg = client_mgr
.db()
.update_network_config_state(auth_session.user.unwrap().id(), inst_id, payload.disabled)
.await
.map_err(convert_db_error)?;
let c = sess.scoped_rpc_client();
if payload.disabled {
c.delete_network_instance(
BaseController::default(),
DeleteNetworkInstanceRequest {
inst_ids: vec![inst_id.into()],
},
)
.await
.map_err(convert_rpc_error)?;
} else {
c.run_network_instance(
BaseController::default(),
RunNetworkInstanceRequest {
inst_id: Some(inst_id.into()),
config: Some(serde_json::from_str(&cfg.network_config).unwrap()),
},
)
.await
.map_err(convert_rpc_error)?;
}
Ok(())
}
async fn handle_get_network_config(
auth_session: AuthSession,
State(client_mgr): AppState,
@@ -298,25 +373,24 @@ impl NetworkApi {
let db_row = client_mgr
.db()
.list_network_configs(auth_session.user.unwrap().id(), Some(machine_id), false)
.get_network_config(auth_session.user.unwrap().id(), &machine_id, &inst_id)
.await
.map_err(convert_db_error)?
.iter()
.find(|x| x.network_instance_id == inst_id)
.map(|x| x.network_config.clone())
.ok_or((
StatusCode::NOT_FOUND,
other_error(format!("No such network instance: {}", inst_id)).into(),
))?;
Ok(serde_json::from_str::<NetworkConfig>(&db_row)
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
other_error(format!("Failed to parse network config: {:?}", e)).into(),
)
})?
.into())
Ok(
serde_json::from_str::<NetworkConfig>(&db_row.network_config)
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
other_error(format!("Failed to parse network config: {:?}", e)).into(),
)
})?
.into(),
)
}
pub fn build_route(&mut self) -> Router<AppStateInner> {
@@ -332,7 +406,7 @@ impl NetworkApi {
)
.route(
"/api/v1/machines/:machine-id/networks/:inst-id",
delete(Self::handle_remove_network_instance),
delete(Self::handle_remove_network_instance).put(Self::handle_update_network_state),
)
.route(
"/api/v1/machines/:machine-id/networks/info",

View File

@@ -3,7 +3,7 @@ name = "easytier"
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
homepage = "https://github.com/EasyTier/EasyTier"
repository = "https://github.com/EasyTier/EasyTier"
version = "2.1.0"
version = "2.1.2"
edition = "2021"
authors = ["kkrainbow"]
keywords = ["vpn", "p2p", "network", "easytier"]

View File

@@ -5,11 +5,11 @@ core_clap:
en: |+
config server address, allow format:
full url: --config-server udp://127.0.0.1:22020/admin
only user name: --config-server admin
only user name: --config-server admin, will use official server
zh-CN: |+
配置服务器地址。允许格式:
完整URL--config-server udp://127.0.0.1:22020/admin
仅用户名:--config-server admin
仅用户名:--config-server admin,将使用官方的服务器
config_file:
en: "path to the config file, NOTE: if this is set, all other options will be ignored"
zh-CN: "配置文件路径,注意:如果设置了这个选项,其他所有选项都将被忽略"
@@ -134,6 +134,12 @@ core_clap:
compression:
en: "compression algorithm to use, support none, zstd. default is none"
zh-CN: "要使用的压缩算法,支持 none、zstd。默认为 none"
mapped_listeners:
en: "manually specify the public address of the listener, other nodes can use this address to connect to this node. e.g.: tcp://123.123.123.123:11223, can specify multiple."
zh-CN: "手动指定监听器的公网地址其他节点可以使用该地址连接到本节点。例如tcp://123.123.123.123:11223可以指定多个。"
bind_device:
en: "bind the connector socket to physical devices to avoid routing issues. e.g.: subnet proxy segment conflicts with a node's segment, after binding the physical device, it can communicate with the node normally."
zh-CN: "将连接器的套接字绑定到物理设备以避免路由问题。比如子网代理网段与某节点的网段冲突,绑定物理设备后可以与该节点正常通信。"
core_app:
panic_backtrace_save:

View File

@@ -27,8 +27,9 @@ pub fn gen_default_flags() -> Flags {
relay_all_peer_rpc: false,
disable_udp_hole_punching: false,
ipv6_listener: "udp://[::]:0".to_string(),
multi_thread: false,
multi_thread: true,
data_compress_algo: CompressionAlgoPb::None.into(),
bind_device: true,
}
}
@@ -72,6 +73,9 @@ pub trait ConfigLoader: Send + Sync {
fn get_listeners(&self) -> Vec<url::Url>;
fn set_listeners(&self, listeners: Vec<url::Url>);
fn get_mapped_listeners(&self) -> Vec<url::Url>;
fn set_mapped_listeners(&self, listeners: Option<Vec<url::Url>>);
fn get_rpc_portal(&self) -> Option<SocketAddr>;
fn set_rpc_portal(&self, addr: SocketAddr);
@@ -183,6 +187,7 @@ struct Config {
dhcp: Option<bool>,
network_identity: Option<NetworkIdentity>,
listeners: Option<Vec<url::Url>>,
mapped_listeners: Option<Vec<url::Url>>,
exit_nodes: Option<Vec<Ipv4Addr>>,
peer: Option<Vec<PeerConfig>>,
@@ -472,6 +477,19 @@ impl ConfigLoader for TomlConfigLoader {
self.config.lock().unwrap().listeners = Some(listeners);
}
fn get_mapped_listeners(&self) -> Vec<url::Url> {
self.config
.lock()
.unwrap()
.mapped_listeners
.clone()
.unwrap_or_default()
}
fn set_mapped_listeners(&self, listeners: Option<Vec<url::Url>>) {
self.config.lock().unwrap().mapped_listeners = listeners;
}
fn get_rpc_portal(&self) -> Option<SocketAddr> {
self.config.lock().unwrap().rpc_portal
}

View File

@@ -230,7 +230,10 @@ impl GlobalCtx {
}
pub fn add_running_listener(&self, url: url::Url) {
self.running_listeners.lock().unwrap().push(url);
let mut l = self.running_listeners.lock().unwrap();
if !l.contains(&url) {
l.push(url);
}
}
pub fn get_vpn_portal_cidr(&self) -> Option<cidr::Ipv4Cidr> {

View File

@@ -1,6 +1,7 @@
use std::{
fmt::Debug,
future,
io::Write as _,
sync::{Arc, Mutex},
};
use tokio::task::JoinSet;
@@ -81,7 +82,17 @@ pub fn join_joinset_background<T: Debug + Send + Sync + 'static>(
}
pub fn get_machine_id() -> uuid::Uuid {
// TODO: load from local file
// a path same as the binary
let machine_id_file = std::env::current_exe()
.map(|x| x.with_file_name("et_machine_id"))
.unwrap_or_else(|_| std::path::PathBuf::from("et_machine_id"));
// try load from local file
if let Ok(mid) = std::fs::read_to_string(&machine_id_file) {
if let Ok(mid) = uuid::Uuid::parse_str(mid.trim()) {
return mid;
}
}
#[cfg(any(
target_os = "linux",
@@ -95,7 +106,7 @@ pub fn get_machine_id() -> uuid::Uuid {
crate::tunnel::generate_digest_from_str("", x.as_str(), &mut b);
uuid::Uuid::from_bytes(b)
})
.unwrap_or(uuid::Uuid::new_v4());
.ok();
#[cfg(not(any(
target_os = "linux",
@@ -103,9 +114,18 @@ pub fn get_machine_id() -> uuid::Uuid {
target_os = "windows",
target_os = "freebsd"
)))]
let gen_mid = None;
if gen_mid.is_some() {
return gen_mid.unwrap();
}
let gen_mid = uuid::Uuid::new_v4();
// TODO: save to local file
// try save to local file
if let Ok(mut file) = std::fs::File::create(machine_id_file) {
let _ = file.write_all(gen_mid.to_string().as_bytes());
}
gen_mid
}

View File

@@ -1,6 +1,13 @@
// try connect peers directly, with either its public ip or lan ip
use std::{net::SocketAddr, sync::Arc, time::Duration};
use std::{
net::SocketAddr,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration,
};
use crate::{
common::{error::Error, global_ctx::ArcGlobalCtx, PeerId},
@@ -29,6 +36,8 @@ use super::create_connector_by_url;
pub const DIRECT_CONNECTOR_SERVICE_ID: u32 = 1;
pub const DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC: u64 = 300;
static TESTING: AtomicBool = AtomicBool::new(false);
#[async_trait::async_trait]
pub trait PeerManagerForDirectConnector {
async fn list_peers(&self) -> Vec<PeerId>;
@@ -182,7 +191,7 @@ impl DirectConnectorManager {
// let (peer_id, conn_id) = data.peer_manager.try_connect(connector).await?;
if peer_id != dst_peer_id {
if peer_id != dst_peer_id && !TESTING.load(Ordering::Relaxed) {
tracing::info!(
"connect to ip succ: {}, but peer id mismatch, expect: {}, actual: {}",
addr,
@@ -279,87 +288,103 @@ impl DirectConnectorManager {
let listener_host = listener.socket_addrs(|| None).unwrap().pop();
match listener_host {
Some(SocketAddr::V4(_)) => {
ip_list.interface_ipv4s.iter().for_each(|ip| {
let mut addr = (*listener).clone();
if addr.set_host(Some(ip.to_string().as_str())).is_ok() {
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr.to_string(),
));
} else {
tracing::error!(
?ip,
?listener,
?dst_peer_id,
"failed to set host for interface ipv4"
);
}
});
Some(SocketAddr::V4(s_addr)) => {
if s_addr.ip().is_unspecified() {
ip_list.interface_ipv4s.iter().for_each(|ip| {
let mut addr = (*listener).clone();
if addr.set_host(Some(ip.to_string().as_str())).is_ok() {
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr.to_string(),
));
} else {
tracing::error!(
?ip,
?listener,
?dst_peer_id,
"failed to set host for interface ipv4"
);
}
});
if let Some(public_ipv4) = ip_list.public_ipv4 {
let mut addr = (*listener).clone();
if addr
.set_host(Some(public_ipv4.to_string().as_str()))
.is_ok()
{
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr.to_string(),
));
} else {
tracing::error!(
?public_ipv4,
?listener,
?dst_peer_id,
"failed to set host for public ipv4"
);
if let Some(public_ipv4) = ip_list.public_ipv4 {
let mut addr = (*listener).clone();
if addr
.set_host(Some(public_ipv4.to_string().as_str()))
.is_ok()
{
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr.to_string(),
));
} else {
tracing::error!(
?public_ipv4,
?listener,
?dst_peer_id,
"failed to set host for public ipv4"
);
}
}
} else if !s_addr.ip().is_loopback() || TESTING.load(Ordering::Relaxed) {
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
listener.to_string(),
));
}
}
Some(SocketAddr::V6(_)) => {
ip_list.interface_ipv6s.iter().for_each(|ip| {
let mut addr = (*listener).clone();
if addr
.set_host(Some(format!("[{}]", ip.to_string()).as_str()))
.is_ok()
{
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr.to_string(),
));
} else {
tracing::error!(
?ip,
?listener,
?dst_peer_id,
"failed to set host for interface ipv6"
);
}
});
Some(SocketAddr::V6(s_addr)) => {
if s_addr.ip().is_unspecified() {
ip_list.interface_ipv6s.iter().for_each(|ip| {
let mut addr = (*listener).clone();
if addr
.set_host(Some(format!("[{}]", ip.to_string()).as_str()))
.is_ok()
{
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr.to_string(),
));
} else {
tracing::error!(
?ip,
?listener,
?dst_peer_id,
"failed to set host for interface ipv6"
);
}
});
if let Some(public_ipv6) = ip_list.public_ipv6 {
let mut addr = (*listener).clone();
if addr
.set_host(Some(format!("[{}]", public_ipv6.to_string()).as_str()))
.is_ok()
{
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr.to_string(),
));
} else {
tracing::error!(
?public_ipv6,
?listener,
?dst_peer_id,
"failed to set host for public ipv6"
);
if let Some(public_ipv6) = ip_list.public_ipv6 {
let mut addr = (*listener).clone();
if addr
.set_host(Some(format!("[{}]", public_ipv6.to_string()).as_str()))
.is_ok()
{
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr.to_string(),
));
} else {
tracing::error!(
?public_ipv6,
?listener,
?dst_peer_id,
"failed to set host for public ipv6"
);
}
}
} else if !s_addr.ip().is_loopback() || TESTING.load(Ordering::Relaxed) {
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
listener.to_string(),
));
}
}
p => {
@@ -452,6 +477,49 @@ mod tests {
proto::peer_rpc::GetIpListResponse,
};
use super::TESTING;
#[tokio::test]
async fn direct_connector_mapped_listener() {
TESTING.store(true, std::sync::atomic::Ordering::Relaxed);
let p_a = create_mock_peer_manager().await;
let p_b = create_mock_peer_manager().await;
let p_c = create_mock_peer_manager().await;
let p_x = create_mock_peer_manager().await;
connect_peer_manager(p_a.clone(), p_b.clone()).await;
connect_peer_manager(p_b.clone(), p_c.clone()).await;
connect_peer_manager(p_c.clone(), p_x.clone()).await;
wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap();
wait_route_appear(p_a.clone(), p_x.clone()).await.unwrap();
let mut f = p_a.get_global_ctx().get_flags();
f.bind_device = false;
p_a.get_global_ctx().config.set_flags(f);
p_c.get_global_ctx()
.config
.set_mapped_listeners(Some(vec!["tcp://127.0.0.1:11334".parse().unwrap()]));
p_x.get_global_ctx()
.config
.set_listeners(vec!["tcp://0.0.0.0:11334".parse().unwrap()]);
let mut lis_x = ListenerManager::new(p_x.get_global_ctx(), p_x.clone());
lis_x.prepare_listeners().await.unwrap();
lis_x.run().await.unwrap();
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let mut dm_a = DirectConnectorManager::new(p_a.get_global_ctx(), p_a.clone());
let mut dm_c = DirectConnectorManager::new(p_c.get_global_ctx(), p_c.clone());
dm_a.run_as_client();
dm_c.run_as_server();
// p_c's mapped listener is p_x's listener, so p_a should connect to p_x directly
wait_route_appear_with_cost(p_a.clone(), p_x.my_peer_id(), Some(1))
.await
.unwrap();
}
#[rstest::rstest]
#[tokio::test]
async fn direct_connector_basic_test(

View File

@@ -297,12 +297,14 @@ impl ManualConnectorManager {
connector.lock().await.set_ip_version(ip_version);
set_bind_addr_for_peer_connector(
connector.lock().await.as_mut(),
ip_version == IpVersion::V4,
&ip_collector,
)
.await;
if data.global_ctx.config.get_flags().bind_device {
set_bind_addr_for_peer_connector(
connector.lock().await.as_mut(),
ip_version == IpVersion::V4,
&ip_collector,
)
.await;
}
data.global_ctx.issue_event(GlobalCtxEvent::Connecting(
connector.lock().await.remote_url().clone(),

View File

@@ -56,23 +56,27 @@ pub async fn create_connector_by_url(
"tcp" => {
let dst_addr = check_scheme_and_get_socket_addr::<SocketAddr>(&url, "tcp")?;
let mut connector = TcpTunnelConnector::new(url);
set_bind_addr_for_peer_connector(
&mut connector,
dst_addr.is_ipv4(),
&global_ctx.get_ip_collector(),
)
.await;
if global_ctx.config.get_flags().bind_device {
set_bind_addr_for_peer_connector(
&mut connector,
dst_addr.is_ipv4(),
&global_ctx.get_ip_collector(),
)
.await;
}
return Ok(Box::new(connector));
}
"udp" => {
let dst_addr = check_scheme_and_get_socket_addr::<SocketAddr>(&url, "udp")?;
let mut connector = UdpTunnelConnector::new(url);
set_bind_addr_for_peer_connector(
&mut connector,
dst_addr.is_ipv4(),
&global_ctx.get_ip_collector(),
)
.await;
if global_ctx.config.get_flags().bind_device {
set_bind_addr_for_peer_connector(
&mut connector,
dst_addr.is_ipv4(),
&global_ctx.get_ip_collector(),
)
.await;
}
return Ok(Box::new(connector));
}
"ring" => {
@@ -84,12 +88,14 @@ pub async fn create_connector_by_url(
"quic" => {
let dst_addr = check_scheme_and_get_socket_addr::<SocketAddr>(&url, "quic")?;
let mut connector = QUICTunnelConnector::new(url);
set_bind_addr_for_peer_connector(
&mut connector,
dst_addr.is_ipv4(),
&global_ctx.get_ip_collector(),
)
.await;
if global_ctx.config.get_flags().bind_device {
set_bind_addr_for_peer_connector(
&mut connector,
dst_addr.is_ipv4(),
&global_ctx.get_ip_collector(),
)
.await;
}
return Ok(Box::new(connector));
}
#[cfg(feature = "wireguard")]
@@ -101,12 +107,14 @@ pub async fn create_connector_by_url(
&nid.network_secret.unwrap_or_default(),
);
let mut connector = WgTunnelConnector::new(url, wg_config);
set_bind_addr_for_peer_connector(
&mut connector,
dst_addr.is_ipv4(),
&global_ctx.get_ip_collector(),
)
.await;
if global_ctx.config.get_flags().bind_device {
set_bind_addr_for_peer_connector(
&mut connector,
dst_addr.is_ipv4(),
&global_ctx.get_ip_collector(),
)
.await;
}
return Ok(Box::new(connector));
}
#[cfg(feature = "websocket")]
@@ -114,12 +122,14 @@ pub async fn create_connector_by_url(
use crate::tunnel::{FromUrl, IpVersion};
let dst_addr = SocketAddr::from_url(url.clone(), IpVersion::Both)?;
let mut connector = crate::tunnel::websocket::WSTunnelConnector::new(url);
set_bind_addr_for_peer_connector(
&mut connector,
dst_addr.is_ipv4(),
&global_ctx.get_ip_collector(),
)
.await;
if global_ctx.config.get_flags().bind_device {
set_bind_addr_for_peer_connector(
&mut connector,
dst_addr.is_ipv4(),
&global_ctx.get_ip_collector(),
)
.await;
}
return Ok(Box::new(connector));
}
_ => {

View File

@@ -123,6 +123,13 @@ struct Cli {
)]
listeners: Vec<String>,
#[arg(
long,
help = t!("core_clap.mapped_listeners").to_string(),
num_args = 0..
)]
mapped_listeners: Vec<String>,
#[arg(
long,
help = t!("core_clap.no_listener").to_string(),
@@ -185,7 +192,7 @@ struct Cli {
#[arg(
long,
help = t!("core_clap.multi_thread").to_string(),
default_value = "false"
default_value = "true"
)]
multi_thread: bool,
@@ -300,6 +307,12 @@ struct Cli {
default_value = "none",
)]
compression: String,
#[arg(
long,
help = t!("core_clap.bind_device").to_string()
)]
bind_device: Option<bool>,
}
rust_i18n::i18n!("locales", fallback = "en");
@@ -422,6 +435,23 @@ impl TryFrom<&Cli> for TomlConfigLoader {
.collect(),
);
cfg.set_mapped_listeners(Some(
cli.mapped_listeners
.iter()
.map(|s| {
s.parse()
.with_context(|| format!("mapped listener is not a valid url: {}", s))
.unwrap()
})
.map(|s: url::Url| {
if s.port().is_none() {
panic!("mapped listener port is missing: {}", s);
}
s
})
.collect(),
));
for n in cli.proxy_networks.iter() {
cfg.add_proxy_cidr(
n.parse()
@@ -534,6 +564,9 @@ impl TryFrom<&Cli> for TomlConfigLoader {
),
}
.into();
if let Some(bind_device) = cli.bind_device {
f.bind_device = bind_device;
}
cfg.set_flags(f);
cfg.set_exit_nodes(cli.exit_nodes.clone());
@@ -783,9 +816,12 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
let config_server_url_s = cli.config_server.clone().unwrap();
let config_server_url = match url::Url::parse(&config_server_url_s) {
Ok(u) => u,
Err(_) => format!("udp://easytier.cn:22020/{}", config_server_url_s)
.parse()
.unwrap(),
Err(_) => format!(
"udp://config-server.easytier.cn:22020/{}",
config_server_url_s
)
.parse()
.unwrap(),
};
let mut c_url = config_server_url.clone();
@@ -801,6 +837,8 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
c_url, token,
);
println!("Official config website: https://easytier.cn/web");
if token.is_empty() {
panic!("empty token");
}
@@ -823,7 +861,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
Ok(())
}
#[tokio::main]
#[tokio::main(flavor = "current_thread")]
async fn main() {
let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
rust_i18n::set_locale(&locale);

View File

@@ -4,6 +4,7 @@ use std::{
time::Duration,
};
use bytes::{BufMut, BytesMut};
use cidr::Ipv4Inet;
use crossbeam::atomic::AtomicCell;
use dashmap::DashMap;
@@ -24,11 +25,11 @@ use tokio::{
use tracing::Level;
use crate::{
common::{error::Error, global_ctx::ArcGlobalCtx, PeerId},
common::{error::Error, global_ctx::ArcGlobalCtx, scoped_task::ScopedTask, PeerId},
gateway::ip_reassembler::compose_ipv4_packet,
peers::{peer_manager::PeerManager, PeerPacketFilter},
tunnel::{
common::setup_sokcet2,
common::{reserve_buf, setup_sokcet2},
packet_def::{PacketType, ZCPacket},
},
};
@@ -139,59 +140,81 @@ impl UdpNatEntry {
mut packet_sender: Sender<ZCPacket>,
virtual_ipv4: Ipv4Addr,
) {
let mut buf = [0u8; 65536];
let mut udp_body: &mut [u8] = unsafe { std::mem::transmute(&mut buf[20 + 8..]) };
let mut ip_id = 1;
let (s, mut r) = tachyonix::channel(128);
loop {
let (len, src_socket) = match timeout(
Duration::from_secs(120),
self.socket.recv_from(&mut udp_body),
)
.await
{
Ok(Ok(x)) => x,
Ok(Err(err)) => {
tracing::error!(?err, "udp nat recv failed");
let self_clone = self.clone();
let recv_task = ScopedTask::from(tokio::spawn(async move {
let mut cur_buf = BytesMut::new();
loop {
if self_clone
.stopped
.load(std::sync::atomic::Ordering::Relaxed)
{
break;
}
Err(err) => {
tracing::error!(?err, "udp nat recv timeout");
break;
reserve_buf(&mut cur_buf, 64 * 1024 + 28, 128 * 1024 + 28);
assert_eq!(cur_buf.len(), 0);
unsafe {
cur_buf.advance_mut(28);
}
};
tracing::trace!(?len, ?src_socket, "udp nat packet response received");
let (len, src_socket) = match timeout(
Duration::from_secs(120),
self_clone.socket.recv_buf_from(&mut cur_buf),
)
.await
{
Ok(Ok(x)) => x,
Ok(Err(err)) => {
tracing::error!(?err, "udp nat recv failed");
break;
}
Err(err) => {
tracing::error!(?err, "udp nat recv timeout");
break;
}
};
if self.stopped.load(std::sync::atomic::Ordering::Relaxed) {
break;
tracing::trace!(?len, ?src_socket, "udp nat packet response received");
let ret_buf = cur_buf.split();
s.send((ret_buf, len, src_socket)).await.unwrap();
}
}));
let SocketAddr::V4(mut src_v4) = src_socket else {
continue;
};
let self_clone = self.clone();
let send_task = ScopedTask::from(tokio::spawn(async move {
let mut ip_id = 1;
while let Ok((mut packet, len, src_socket)) = r.recv().await {
let SocketAddr::V4(mut src_v4) = src_socket else {
continue;
};
self.mark_active();
self_clone.mark_active();
if src_v4.ip().is_loopback() {
src_v4.set_ip(virtual_ipv4);
if src_v4.ip().is_loopback() {
src_v4.set_ip(virtual_ipv4);
}
let Ok(_) = Self::compose_ipv4_packet(
&self_clone,
&mut packet_sender,
&mut packet,
&src_v4,
len,
1280,
ip_id,
)
.await
else {
break;
};
ip_id = ip_id.wrapping_add(1);
}
}));
let Ok(_) = Self::compose_ipv4_packet(
&self,
&mut packet_sender,
&mut buf,
&src_v4,
len,
1200,
ip_id,
)
.await
else {
break;
};
ip_id = ip_id.wrapping_add(1);
}
let _ = tokio::join!(recv_task, send_task);
self.stop();
}

View File

@@ -1,8 +1,7 @@
use std::{fmt::Debug, sync::Arc};
use anyhow::Context;
use async_trait::async_trait;
use tokio::{sync::Mutex, task::JoinSet};
use tokio::task::JoinSet;
#[cfg(feature = "quic")]
use crate::tunnel::quic::QUICTunnelListener;
@@ -63,16 +62,20 @@ impl TunnelHandlerForListener for PeerManager {
}
}
#[derive(Debug, Clone)]
struct Listener {
inner: Arc<Mutex<dyn TunnelListener>>,
pub trait ListenerCreatorTrait: Fn() -> Box<dyn TunnelListener> + Send + Sync {}
impl<T: Send + Sync> ListenerCreatorTrait for T where T: Fn() -> Box<dyn TunnelListener> + Send {}
pub type ListenerCreator = Box<dyn ListenerCreatorTrait>;
#[derive(Clone)]
struct ListenerFactory {
creator_fn: Arc<ListenerCreator>,
must_succ: bool,
}
pub struct ListenerManager<H> {
global_ctx: ArcGlobalCtx,
net_ns: NetNS,
listeners: Vec<Listener>,
listeners: Vec<ListenerFactory>,
peer_manager: Arc<H>,
tasks: JoinSet<()>,
@@ -90,31 +93,39 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
}
pub async fn prepare_listeners(&mut self) -> Result<(), Error> {
let self_id = self.global_ctx.get_id();
self.add_listener(
RingTunnelListener::new(
format!("ring://{}", self.global_ctx.get_id())
.parse()
.unwrap(),
),
move || {
Box::new(RingTunnelListener::new(
format!("ring://{}", self_id).parse().unwrap(),
))
},
true,
)
.await?;
for l in self.global_ctx.config.get_listener_uris().iter() {
let Ok(lis) = get_listener_by_url(l, self.global_ctx.clone()) else {
let l = l.clone();
let Ok(_) = get_listener_by_url(&l, self.global_ctx.clone()) else {
let msg = format!("failed to get listener by url: {}, maybe not supported", l);
self.global_ctx
.issue_event(GlobalCtxEvent::ListenerAddFailed(l.clone(), msg));
continue;
};
self.add_listener(lis, true).await?;
let ctx = self.global_ctx.clone();
self.add_listener(move || get_listener_by_url(&l, ctx.clone()).unwrap(), true)
.await?;
}
if self.global_ctx.config.get_flags().enable_ipv6 {
let ipv6_listener = self.global_ctx.config.get_flags().ipv6_listener.clone();
let _ = self
.add_listener(
UdpTunnelListener::new(ipv6_listener.parse().unwrap()),
move || {
Box::new(UdpTunnelListener::new(
ipv6_listener.clone().parse().unwrap(),
))
},
false,
)
.await?;
@@ -123,85 +134,91 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
Ok(())
}
pub async fn add_listener<L>(&mut self, listener: L, must_succ: bool) -> Result<(), Error>
where
L: TunnelListener + 'static,
{
let listener = Arc::new(Mutex::new(listener));
self.listeners.push(Listener {
inner: listener,
pub async fn add_listener<C: ListenerCreatorTrait + 'static>(
&mut self,
creator: C,
must_succ: bool,
) -> Result<(), Error> {
self.listeners.push(ListenerFactory {
creator_fn: Arc::new(Box::new(creator)),
must_succ,
});
Ok(())
}
#[tracing::instrument]
#[tracing::instrument(skip(creator))]
async fn run_listener(
listener: Arc<Mutex<dyn TunnelListener>>,
creator: Arc<ListenerCreator>,
peer_manager: Arc<H>,
global_ctx: ArcGlobalCtx,
) {
let mut l = listener.lock().await;
global_ctx.add_running_listener(l.local_url());
global_ctx.issue_event(GlobalCtxEvent::ListenerAdded(l.local_url()));
loop {
let ret = match l.accept().await {
Ok(ret) => ret,
let mut l = (creator)();
let _g = global_ctx.net_ns.guard();
match l.listen().await {
Ok(_) => {
global_ctx.add_running_listener(l.local_url());
global_ctx.issue_event(GlobalCtxEvent::ListenerAdded(l.local_url()));
}
Err(e) => {
global_ctx.issue_event(GlobalCtxEvent::ListenerAcceptFailed(
global_ctx.issue_event(GlobalCtxEvent::ListenerAddFailed(
l.local_url(),
e.to_string(),
format!("error: {:?}, retry listen later...", e),
));
tracing::error!(?e, ?l, "listener accept error");
tracing::error!(?e, ?l, "listener listen error");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
continue;
}
};
}
loop {
let ret = match l.accept().await {
Ok(ret) => ret,
Err(e) => {
global_ctx.issue_event(GlobalCtxEvent::ListenerAcceptFailed(
l.local_url(),
format!("error: {:?}, retry listen later...", e),
));
tracing::error!(?e, ?l, "listener accept error");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
break;
}
};
let tunnel_info = ret.info().unwrap();
global_ctx.issue_event(GlobalCtxEvent::ConnectionAccepted(
tunnel_info
.local_addr
.clone()
.unwrap_or_default()
.to_string(),
tunnel_info
.remote_addr
.clone()
.unwrap_or_default()
.to_string(),
));
tracing::info!(ret = ?ret, "conn accepted");
let peer_manager = peer_manager.clone();
let global_ctx = global_ctx.clone();
tokio::spawn(async move {
let server_ret = peer_manager.handle_tunnel(ret).await;
if let Err(e) = &server_ret {
global_ctx.issue_event(GlobalCtxEvent::ConnectionError(
tunnel_info.local_addr.unwrap_or_default().to_string(),
tunnel_info.remote_addr.unwrap_or_default().to_string(),
e.to_string(),
));
tracing::error!(error = ?e, "handle conn error");
}
});
let tunnel_info = ret.info().unwrap();
global_ctx.issue_event(GlobalCtxEvent::ConnectionAccepted(
tunnel_info
.local_addr
.clone()
.unwrap_or_default()
.to_string(),
tunnel_info
.remote_addr
.clone()
.unwrap_or_default()
.to_string(),
));
tracing::info!(ret = ?ret, "conn accepted");
let peer_manager = peer_manager.clone();
let global_ctx = global_ctx.clone();
tokio::spawn(async move {
let server_ret = peer_manager.handle_tunnel(ret).await;
if let Err(e) = &server_ret {
global_ctx.issue_event(GlobalCtxEvent::ConnectionError(
tunnel_info.local_addr.unwrap_or_default().to_string(),
tunnel_info.remote_addr.unwrap_or_default().to_string(),
e.to_string(),
));
tracing::error!(error = ?e, "handle conn error");
}
});
}
}
}
pub async fn run(&mut self) -> Result<(), Error> {
for listener in &self.listeners {
let _guard = self.net_ns.guard();
let addr = listener.inner.lock().await.local_url();
tracing::warn!("run listener: {:?}", listener);
listener
.inner
.lock()
.await
.listen()
.await
.with_context(|| format!("failed to add listener {}", addr))?;
self.tasks.spawn(Self::run_listener(
listener.inner.clone(),
listener.creator_fn.clone(),
self.peer_manager.clone(),
self.global_ctx.clone(),
));
@@ -213,12 +230,14 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicI32, Ordering};
use futures::{SinkExt, StreamExt};
use tokio::time::timeout;
use crate::{
common::global_ctx::tests::get_mock_global_ctx,
tunnel::{packet_def::ZCPacket, ring::RingTunnelConnector, TunnelConnector},
tunnel::{packet_def::ZCPacket, ring::RingTunnelConnector, TunnelConnector, TunnelError},
};
use super::*;
@@ -245,12 +264,18 @@ mod tests {
let ring_id = format!("ring://{}", uuid::Uuid::new_v4());
let ring_id_clone = ring_id.clone();
listener_mgr
.add_listener(RingTunnelListener::new(ring_id.parse().unwrap()), true)
.add_listener(
move || Box::new(RingTunnelListener::new(ring_id_clone.parse().unwrap())),
true,
)
.await
.unwrap();
listener_mgr.run().await.unwrap();
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let connect_once = |ring_id| async move {
let tunnel = RingTunnelConnector::new(ring_id).connect().await.unwrap();
let (mut recv, _send) = tunnel.split();
@@ -269,4 +294,60 @@ mod tests {
.await
.unwrap();
}
#[tokio::test]
async fn retry_listen() {
let counter = Arc::new(AtomicI32::new(0));
let drop_counter = Arc::new(AtomicI32::new(0));
struct MockListener {
counter: Arc<AtomicI32>,
drop_counter: Arc<AtomicI32>,
}
#[async_trait::async_trait]
impl TunnelListener for MockListener {
fn local_url(&self) -> url::Url {
"mock://".parse().unwrap()
}
async fn listen(&mut self) -> Result<(), TunnelError> {
self.counter.fetch_add(1, Ordering::Relaxed);
Ok(())
}
async fn accept(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Err(TunnelError::BufferFull)
}
}
impl Drop for MockListener {
fn drop(&mut self) {
self.drop_counter.fetch_add(1, Ordering::Relaxed);
}
}
let handler = Arc::new(MockListenerHandler {});
let mut listener_mgr = ListenerManager::new(get_mock_global_ctx(), handler.clone());
let counter_clone = counter.clone();
let drop_counter_clone = drop_counter.clone();
listener_mgr
.add_listener(
move || {
Box::new(MockListener {
counter: counter_clone.clone(),
drop_counter: drop_counter_clone.clone(),
})
},
true,
)
.await
.unwrap();
listener_mgr.run().await.unwrap();
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
assert!(counter.load(Ordering::Relaxed) >= 2);
assert!(drop_counter.load(Ordering::Relaxed) >= 1);
}
}

View File

@@ -484,13 +484,6 @@ mod tests {
let c_recorder = Arc::new(DropSendTunnelFilter::new(drop_start, drop_end));
let c = TunnelWithFilter::new(c, c_recorder.clone());
let s = if drop_both {
let s_recorder = Arc::new(DropSendTunnelFilter::new(drop_start, drop_end));
Box::new(TunnelWithFilter::new(s, s_recorder.clone()))
} else {
s
};
let c_peer_id = new_peer_id();
let s_peer_id = new_peer_id();
@@ -506,6 +499,7 @@ mod tests {
s_peer
.start_recv_loop(tokio::sync::mpsc::channel(200).0)
.await;
// do not start ping for s, s only reponde to ping from c
assert!(c_ret.is_ok());
assert!(s_ret.is_ok());
@@ -547,7 +541,7 @@ mod tests {
#[tokio::test]
async fn peer_conn_pingpong_bothside_timeout() {
peer_conn_pingpong_test_common(4, 12, true, true).await;
peer_conn_pingpong_test_common(3, 14, true, true).await;
}
#[tokio::test]

View File

@@ -289,29 +289,29 @@ impl PeerConnPinger {
"pingpong task recv pingpong_once result"
);
if (counter > 5 && loss_rate_20 > 0.74) || (counter > 150 && loss_rate_1 > 0.20) {
let current_rx_packets = throughput.rx_packets();
let need_close = if last_rx_packets != current_rx_packets {
// if we receive some packet from peers, we should relax the condition
counter > 50 && loss_rate_1 > 0.5
let current_rx_packets = throughput.rx_packets();
if last_rx_packets != current_rx_packets {
// if we receive some packet from peers, reset the counter to avoid conn close.
// conn will close only if we have 5 continous round pingpong loss after no packet received.
counter = 0;
}
// TODO: wait more time to see if the loss rate is still high after no rx
} else {
true
};
tracing::debug!(
"counter: {}, loss_rate_1: {}, loss_rate_20: {}, cur_rx_packets: {}, last_rx: {}, node_id: {}",
counter, loss_rate_1, loss_rate_20, current_rx_packets, last_rx_packets, my_node_id
);
if need_close {
tracing::warn!(
?ret,
?self,
?loss_rate_1,
?loss_rate_20,
?last_rx_packets,
?current_rx_packets,
"pingpong loss rate too high, closing"
);
break;
}
if (counter > 5 && loss_rate_20 > 0.74) || (counter > 100 && loss_rate_1 > 0.35) {
tracing::warn!(
?ret,
?self,
?loss_rate_1,
?loss_rate_20,
?last_rx_packets,
?current_rx_packets,
"pingpong loss rate too high, closing"
);
break;
}
last_rx_packets = throughput.rx_packets();

View File

@@ -1739,7 +1739,6 @@ impl RouteSessionManager {
continue;
}
let _ = self.stop_session(*peer_id);
assert_ne!(Some(*peer_id), cur_dst_peer_id_to_initiate);
}
}

View File

@@ -24,8 +24,10 @@ impl DirectConnectorRpc for DirectConnectorManagerRpcServer {
let mut ret = self.global_ctx.get_ip_collector().collect_ip_addrs().await;
ret.listeners = self
.global_ctx
.get_running_listeners()
.config
.get_mapped_listeners()
.into_iter()
.chain(self.global_ctx.get_running_listeners().into_iter())
.map(Into::into)
.collect();
Ok(ret)

View File

@@ -21,6 +21,7 @@ message FlagsInConfig {
string ipv6_listener = 14;
bool multi_thread = 15;
CompressionAlgoPb data_compress_algo = 16;
bool bind_device = 17;
}
message RpcDescriptor {

View File

@@ -159,7 +159,10 @@ pub fn build_rpc_packet(
let cur_packet = RpcPacket {
from_peer,
to_peer,
descriptor: if cur_offset == 0 {
descriptor: if cur_offset == 0
|| compression_info.algo == CompressionAlgoPb::None as i32
{
// old version must have descriptor on every piece
Some(rpc_desc.clone())
} else {
None

View File

@@ -38,6 +38,33 @@ impl<L: TunnelListener + 'static> StandAloneServer<L> {
&self.registry
}
async fn serve_loop(
listener: &mut L,
inflight: Arc<AtomicU32>,
registry: Arc<ServiceRegistry>,
tasks: Arc<Mutex<JoinSet<()>>>,
) -> Result<(), Error> {
listener
.listen()
.await
.with_context(|| "failed to listen")?;
loop {
let tunnel = listener.accept().await?;
let registry = registry.clone();
let inflight_server = inflight.clone();
inflight_server.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
tasks.lock().unwrap().spawn(async move {
let server =
BidirectRpcManager::new().set_rx_timeout(Some(Duration::from_secs(60)));
server.rpc_server().registry().replace_registry(&registry);
server.run_with_tunnel(tunnel);
server.wait().await;
inflight_server.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
});
}
}
pub async fn serve(&mut self) -> Result<(), Error> {
let tasks = self.tasks.clone();
let mut listener = self.listener.take().unwrap();
@@ -45,28 +72,24 @@ impl<L: TunnelListener + 'static> StandAloneServer<L> {
join_joinset_background(tasks.clone(), "standalone server tasks".to_string());
listener
.listen()
.await
.with_context(|| "failed to listen")?;
let inflight_server = self.inflight_server.clone();
self.tasks.lock().unwrap().spawn(async move {
while let Ok(tunnel) = listener.accept().await {
let registry = registry.clone();
let inflight_server = inflight_server.clone();
inflight_server.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
tasks.lock().unwrap().spawn(async move {
let server =
BidirectRpcManager::new().set_rx_timeout(Some(Duration::from_secs(60)));
server.rpc_server().registry().replace_registry(&registry);
server.run_with_tunnel(tunnel);
server.wait().await;
inflight_server.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
});
loop {
let ret = Self::serve_loop(
&mut listener,
inflight_server.clone(),
registry.clone(),
tasks.clone(),
)
.await;
if let Err(e) = ret {
tracing::error!(?e, url = ?listener.local_url(), "serve_loop exit unexpectedly");
println!("standalone serve_loop exit unexpectedly: {:?}", e);
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
panic!("standalone server listener exit");
});
Ok(())

View File

@@ -287,6 +287,8 @@ async fn standalone_rpc_test() {
server.registry().register(service, "test");
server.serve().await.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let mut client = StandAloneClient::new(TcpTunnelConnector::new(
"tcp://127.0.0.1:33455".parse().unwrap(),
));

View File

@@ -28,11 +28,36 @@ impl TcpTunnelListener {
listener: None,
}
}
async fn do_accept(&mut self) -> Result<Box<dyn Tunnel>, std::io::Error> {
let listener = self.listener.as_ref().unwrap();
let (stream, _) = listener.accept().await?;
if let Err(e) = stream.set_nodelay(true) {
tracing::warn!(?e, "set_nodelay fail in accept");
}
let info = TunnelInfo {
tunnel_type: "tcp".to_owned(),
local_addr: Some(self.local_url().into()),
remote_addr: Some(
super::build_url_from_socket_addr(&stream.peer_addr()?.to_string(), "tcp").into(),
),
};
let (r, w) = stream.into_split();
Ok(Box::new(TunnelWrapper::new(
FramedReader::new(r, TCP_MTU_BYTES),
FramedWriter::new(w),
Some(info),
)))
}
}
#[async_trait]
impl TunnelListener for TcpTunnelListener {
async fn listen(&mut self) -> Result<(), TunnelError> {
self.listener = None;
let addr = check_scheme_and_get_socket_addr::<SocketAddr>(&self.addr, "tcp")?;
let socket2_socket = socket2::Socket::new(
@@ -56,27 +81,23 @@ impl TunnelListener for TcpTunnelListener {
}
async fn accept(&mut self) -> Result<Box<dyn Tunnel>, super::TunnelError> {
let listener = self.listener.as_ref().unwrap();
let (stream, _) = listener.accept().await?;
if let Err(e) = stream.set_nodelay(true) {
tracing::warn!(?e, "set_nodelay fail in accept");
loop {
match self.do_accept().await {
Ok(ret) => return Ok(ret),
Err(e) => {
use std::io::ErrorKind::*;
if matches!(
e.kind(),
NotConnected | ConnectionAborted | ConnectionRefused | ConnectionReset
) {
tracing::warn!(?e, "accept fail with retryable error: {:?}", e);
continue;
}
tracing::warn!(?e, "accept fail");
return Err(e.into());
}
}
}
let info = TunnelInfo {
tunnel_type: "tcp".to_owned(),
local_addr: Some(self.local_url().into()),
remote_addr: Some(
super::build_url_from_socket_addr(&stream.peer_addr()?.to_string(), "tcp").into(),
),
};
let (r, w) = stream.into_split();
Ok(Box::new(TunnelWrapper::new(
FramedReader::new(r, TCP_MTU_BYTES),
FramedWriter::new(w),
Some(info),
)))
}
fn local_url(&self) -> url::Url {

View File

@@ -139,12 +139,17 @@ pub fn setup_panic_handler() {
if let Some(payload_str) = payload_str {
println!(
"panic occurred: payload:{}, location: {:?}",
"panic occurred: payload:{}, location: {:?}, backtrace: {:#?}",
payload_str,
info.location()
info.location(),
backtrace
);
} else {
println!("panic occurred: location: {:?}", info.location());
println!(
"panic occurred: location: {:?}, backtrace: {:#?}",
info.location(),
backtrace
);
}
println!("{}", rust_i18n::t!("core_app.panic_backtrace_save"));
let _ = std::fs::File::create("easytier-panic.log")
@@ -168,4 +173,4 @@ mod tests {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
tracing::debug!("test display debug");
}
}
}

161
pnpm-lock.yaml generated
View File

@@ -69,6 +69,9 @@ importers:
'@tauri-apps/cli':
specifier: 2.1.0
version: 2.1.0
'@types/default-gateway':
specifier: ^7.2.2
version: 7.2.2
'@types/node':
specifier: ^22.7.4
version: 22.8.6
@@ -84,15 +87,18 @@ importers:
autoprefixer:
specifier: ^10.4.20
version: 10.4.20(postcss@8.4.47)
cidr-tools:
specifier: ^11.0.2
version: 11.0.2
default-gateway:
specifier: ^7.2.2
version: 7.2.2
eslint:
specifier: ^9.12.0
version: 9.14.0(jiti@2.4.0)
eslint-plugin-format:
specifier: ^0.1.2
version: 0.1.2(eslint@9.14.0(jiti@2.4.0))
internal-ip:
specifier: ^8.0.0
version: 8.0.0
postcss:
specifier: ^8.4.47
version: 8.4.47
@@ -905,16 +911,16 @@ packages:
resolution: {integrity: sha512-AFbhEo10DP095/45EauinQJ5hJ3rJUmuuqltGguvc3WsvezZN+g8qNHLGWKu60FHQVizMrQY7VJ+zVlBXlQQkQ==}
engines: {node: '>= 16'}
'@intlify/message-compiler@11.0.0-beta.1':
resolution: {integrity: sha512-yMXfN4hg/EeSdtWfmoMrwB9X4TXwkBoZlTIpNydQaW9y0tSJHGnUPRoahtkbsyACCm9leSJINLY4jQ0rK6BK0Q==}
'@intlify/message-compiler@11.0.0-rc.1':
resolution: {integrity: sha512-TGw2uBfuTFTegZf/BHtUQBEKxl7Q/dVGLoqRIdw8lFsp9g/53sYn5iD+0HxIzdYjbWL6BTJMXCPUHp9PxDTRPw==}
engines: {node: '>= 16'}
'@intlify/shared@10.0.4':
resolution: {integrity: sha512-ukFn0I01HsSgr3VYhYcvkTCLS7rGa0gw4A4AMpcy/A9xx/zRJy7PS2BElMXLwUazVFMAr5zuiTk3MQeoeGXaJg==}
engines: {node: '>= 16'}
'@intlify/shared@11.0.0-beta.1':
resolution: {integrity: sha512-Md/4T/QOx7wZ7zqVzSsMx2M/9Mx/1nsgsjXS5SFIowFKydqUhMz7K+y7pMFh781aNYz+rGXYwad8E9/+InK9SA==}
'@intlify/shared@11.0.0-rc.1':
resolution: {integrity: sha512-8tR1xe7ZEbkabTuE/tNhzpolygUn9OaYp9yuYAF4MgDNZg06C3Qny80bes2/e9/Wm3aVkPUlCw6WgU7mQd0yEg==}
engines: {node: '>= 16'}
'@intlify/unplugin-vue-i18n@5.2.0':
@@ -1353,6 +1359,9 @@ packages:
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
'@types/default-gateway@7.2.2':
resolution: {integrity: sha512-35C93fYQlnLKLASkMPoxRvok4fENwB3By9clRLd2I/08n/XRl0pCdf7EB17K5oMMwZu8NBYA8i66jH5r/LYBKA==}
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
@@ -1916,13 +1925,9 @@ packages:
resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==}
engines: {node: '>=8'}
cidr-regex@4.0.3:
resolution: {integrity: sha512-HOwDIy/rhKeMf6uOzxtv7FAbrz8zPjmVKfSpM+U7/bNBXC5rtOyr758jxcptiSx6ZZn5LOhPJT5WWxPAGDV8dw==}
engines: {node: '>=14'}
cidr-tools@6.4.2:
resolution: {integrity: sha512-KZC8t2ipCqU2M+ISmTxRDGu9bku5MRU3V1cWyGEFJTZEzRhGvBJvVsbpZO5UAu12fExRFihtYGXAlgFFpmK9jw==}
engines: {node: '>=16'}
cidr-tools@11.0.2:
resolution: {integrity: sha512-OLeM9EOXybbhMsGGBNRLCMjn8e+wFOXARIShF/sZwmJLsxWywqfE0By4BMftT6BFWpbcETWpW7TfM2KGCtrZDg==}
engines: {node: '>=18'}
clean-regexp@1.0.0:
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
@@ -1932,10 +1937,6 @@ packages:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
clone-regexp@3.0.0:
resolution: {integrity: sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==}
engines: {node: '>=12'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -1967,10 +1968,6 @@ packages:
confbox@0.1.8:
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
convert-hrtime@5.0.0:
resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==}
engines: {node: '>=12'}
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
@@ -2436,10 +2433,6 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
function-timeout@0.1.1:
resolution: {integrity: sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==}
engines: {node: '>=14.16'}
gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
@@ -2554,21 +2547,13 @@ packages:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
internal-ip@8.0.0:
resolution: {integrity: sha512-e6c3zxr9COnnc29PIz9LffmALOt0XhIJdR7f83DyHcQksL3B40KGmU3Sr1lrHja3i7Zyqo+AbwKZ+nZiMvg/OA==}
engines: {node: '>=16'}
ip-bigint@7.3.0:
resolution: {integrity: sha512-2qVAe0Q9+Y+5nGvmogwK9y4kefD5Ks5l/IG0Jo1lhU9gIF34jifhqrwXwzkIl+LC594Q6SyAlngs4p890xsXVw==}
engines: {node: '>=16'}
ip-bigint@8.2.0:
resolution: {integrity: sha512-46EAEKzGNxojH5JaGEeCix49tL4h1W8ia5mhogZ68HroVAfyLj1E+SFFid4GuyK0mdIKjwcAITLqwg1wlkx2iQ==}
engines: {node: '>=18'}
ip-num@1.5.1:
resolution: {integrity: sha512-QziFxgxq3mjIf5CuwlzXFYscHxgLqdEdJKRo2UJ5GurL5zrSRMzT/O+nK0ABimoFH8MWF8YwIiwECYsHc1LpUQ==}
ip-regex@5.0.0:
resolution: {integrity: sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
@@ -2610,18 +2595,10 @@ packages:
engines: {node: '>=14.16'}
hasBin: true
is-ip@5.0.1:
resolution: {integrity: sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw==}
engines: {node: '>=14.16'}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
is-regexp@3.1.0:
resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==}
engines: {node: '>=12'}
is-stream@3.0.0:
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -3023,10 +3000,6 @@ packages:
oxc-resolver@2.0.1:
resolution: {integrity: sha512-xEbYdEGwafn+Y2GTyW0BGC3iIjJZXl+fxrIkyheew5mZrDODmPXJf2qwsa1ocBeVUC51g9e835vNZ9tRR5fYCg==}
p-event@5.0.1:
resolution: {integrity: sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
@@ -3043,10 +3016,6 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
p-timeout@5.1.0:
resolution: {integrity: sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==}
engines: {node: '>=12'}
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
@@ -3402,9 +3371,6 @@ packages:
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
engines: {node: '>=0.6.19'}
string-natural-compare@3.0.1:
resolution: {integrity: sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@@ -3445,10 +3411,6 @@ packages:
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
super-regex@0.2.0:
resolution: {integrity: sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==}
engines: {node: '>=14.16'}
superjson@2.2.1:
resolution: {integrity: sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==}
engines: {node: '>=16'}
@@ -3500,10 +3462,6 @@ packages:
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
time-span@5.1.0:
resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==}
engines: {node: '>=12'}
tinyexec@0.3.1:
resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==}
@@ -4420,8 +4378,8 @@ snapshots:
'@intlify/bundle-utils@9.0.0(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))':
dependencies:
'@intlify/message-compiler': 11.0.0-beta.1
'@intlify/shared': 11.0.0-beta.1
'@intlify/message-compiler': 11.0.0-rc.1
'@intlify/shared': 11.0.0-rc.1
acorn: 8.14.0
escodegen: 2.1.0
estree-walker: 2.0.2
@@ -4442,21 +4400,21 @@ snapshots:
'@intlify/shared': 10.0.4
source-map-js: 1.2.1
'@intlify/message-compiler@11.0.0-beta.1':
'@intlify/message-compiler@11.0.0-rc.1':
dependencies:
'@intlify/shared': 11.0.0-beta.1
'@intlify/shared': 11.0.0-rc.1
source-map-js: 1.2.1
'@intlify/shared@10.0.4': {}
'@intlify/shared@11.0.0-beta.1': {}
'@intlify/shared@11.0.0-rc.1': {}
'@intlify/unplugin-vue-i18n@5.2.0(@vue/compiler-dom@3.5.12)(eslint@9.14.0(jiti@2.4.0))(rollup@4.24.3)(typescript@5.6.3)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))':
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0(jiti@2.4.0))
'@intlify/bundle-utils': 9.0.0(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))
'@intlify/shared': 11.0.0-beta.1
'@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@11.0.0-beta.1)(@vue/compiler-dom@3.5.12)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))
'@intlify/shared': 11.0.0-rc.1
'@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@11.0.0-rc.1)(@vue/compiler-dom@3.5.12)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))
'@rollup/pluginutils': 5.1.3(rollup@4.24.3)
'@typescript-eslint/scope-manager': 7.18.0
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3)
@@ -4479,11 +4437,11 @@ snapshots:
- typescript
- webpack-sources
'@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@11.0.0-beta.1)(@vue/compiler-dom@3.5.12)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))':
'@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@11.0.0-rc.1)(@vue/compiler-dom@3.5.12)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))':
dependencies:
'@babel/parser': 7.26.2
optionalDependencies:
'@intlify/shared': 11.0.0-beta.1
'@intlify/shared': 11.0.0-rc.1
'@vue/compiler-dom': 3.5.12
vue: 3.5.12(typescript@5.6.3)
vue-i18n: 10.0.4(vue@3.5.12(typescript@5.6.3))
@@ -4865,6 +4823,8 @@ snapshots:
dependencies:
'@types/ms': 0.7.34
'@types/default-gateway@7.2.2': {}
'@types/estree@1.0.6': {}
'@types/json-schema@7.0.15': {}
@@ -5649,16 +5609,9 @@ snapshots:
ci-info@4.0.0: {}
cidr-regex@4.0.3:
cidr-tools@11.0.2:
dependencies:
ip-regex: 5.0.0
cidr-tools@6.4.2:
dependencies:
cidr-regex: 4.0.3
ip-bigint: 7.3.0
ip-regex: 5.0.0
string-natural-compare: 3.0.1
ip-bigint: 8.2.0
clean-regexp@1.0.0:
dependencies:
@@ -5670,10 +5623,6 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
clone-regexp@3.0.0:
dependencies:
is-regexp: 3.1.0
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -5696,8 +5645,6 @@ snapshots:
confbox@0.1.8: {}
convert-hrtime@5.0.0: {}
convert-source-map@2.0.0: {}
copy-anything@3.0.5:
@@ -6278,8 +6225,6 @@ snapshots:
function-bind@1.1.2: {}
function-timeout@0.1.1: {}
gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {}
@@ -6381,19 +6326,10 @@ snapshots:
indent-string@4.0.0: {}
internal-ip@8.0.0:
dependencies:
cidr-tools: 6.4.2
default-gateway: 7.2.2
is-ip: 5.0.1
p-event: 5.0.1
ip-bigint@7.3.0: {}
ip-bigint@8.2.0: {}
ip-num@1.5.1: {}
ip-regex@5.0.0: {}
is-arrayish@0.2.1: {}
is-binary-path@2.1.0:
@@ -6424,15 +6360,8 @@ snapshots:
dependencies:
is-docker: 3.0.0
is-ip@5.0.1:
dependencies:
ip-regex: 5.0.0
super-regex: 0.2.0
is-number@7.0.0: {}
is-regexp@3.1.0: {}
is-stream@3.0.0: {}
is-what@4.1.16: {}
@@ -6992,10 +6921,6 @@ snapshots:
'@oxc-resolver/binding-win32-arm64-msvc': 2.0.1
'@oxc-resolver/binding-win32-x64-msvc': 2.0.1
p-event@5.0.1:
dependencies:
p-timeout: 5.1.0
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
@@ -7012,8 +6937,6 @@ snapshots:
dependencies:
p-limit: 3.1.0
p-timeout@5.1.0: {}
p-try@2.2.0: {}
package-json-from-dist@1.0.1: {}
@@ -7332,8 +7255,6 @@ snapshots:
string-argv@0.3.2: {}
string-natural-compare@3.0.1: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
@@ -7378,12 +7299,6 @@ snapshots:
pirates: 4.0.6
ts-interface-checker: 0.1.13
super-regex@0.2.0:
dependencies:
clone-regexp: 3.0.0
function-timeout: 0.1.1
time-span: 5.1.0
superjson@2.2.1:
dependencies:
copy-anything: 3.0.5
@@ -7452,10 +7367,6 @@ snapshots:
dependencies:
any-promise: 1.3.0
time-span@5.1.0:
dependencies:
convert-hrtime: 5.0.0
tinyexec@0.3.1: {}
to-regex-range@5.0.1:

View File

@@ -89,7 +89,7 @@ class TauriVpnService : VpnService() {
for (route in routes) {
val ipParts = route.split("/")
if (ipParts.size != 2) throw IllegalArgumentException("Invalid IP addr string")
if (ipParts.size != 2) throw IllegalArgumentException("Invalid route cidr string")
builder.addRoute(ipParts[0], ipParts[1].toInt())
}

View File

@@ -3,7 +3,7 @@ const COMMANDS: &[&str] = &[
"prepare_vpn",
"start_vpn",
"stop_vpn",
"register_listener",
"registerListener",
];
fn main() {

View File

@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-registerListener"
description = "Enables the registerListener command without any pre-configured scope."
commands.allow = ["registerListener"]
[[permission]]
identifier = "deny-registerListener"
description = "Denies the registerListener command without any pre-configured scope."
commands.deny = ["registerListener"]

View File

@@ -69,6 +69,32 @@ Denies the prepare_vpn command without any pre-configured scope.
<tr>
<td>
`vpnservice:allow-registerListener`
</td>
<td>
Enables the registerListener command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`vpnservice:deny-registerListener`
</td>
<td>
Denies the registerListener command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`vpnservice:allow-register-listener`
</td>

View File

@@ -314,6 +314,16 @@
"type": "string",
"const": "deny-prepare-vpn"
},
{
"description": "Enables the registerListener command without any pre-configured scope.",
"type": "string",
"const": "allow-registerListener"
},
{
"description": "Denies the registerListener command without any pre-configured scope.",
"type": "string",
"const": "deny-registerListener"
},
{
"description": "Enables the register_listener command without any pre-configured scope.",
"type": "string",