Compare commits

..

9 Commits

Author SHA1 Message Date
sijie.sun c0d2045e52 bump version to v2.4.5 2025-09-26 00:48:10 +08:00
ThermalEng 835cd407bf Update hotspot_iprule.sh, Support subnet forward for usb shared network (#1411) 2025-09-25 16:25:53 +08:00
Sijie.Sun f5ba5bb146 show traffic stats chart in web/gui (#1410) 2025-09-25 13:43:11 +08:00
Sijie.Sun 7a694257d9 add test for ipv6 wireguard vpn portal (#1408) 2025-09-25 08:24:56 +08:00
Sijie.Sun 67abf4446d fix socks5 panic (#1409) 2025-09-25 08:24:50 +08:00
Sijie.Sun 7035a3fef4 fix firewall rule not specify interface (#1407) 2025-09-25 00:11:26 +08:00
Sijie.Sun 4445916ba7 fix open log dir not work on gui (#1403) 2025-09-21 23:17:31 +08:00
Sijie.Sun a102a8bfc7 fix macos bind failed when addr is v6 (#1398) 2025-09-21 21:47:03 +08:00
Sijie.Sun c9e8c35e77 fix log dir not work; fix stun config from file not work; (#1393) 2025-09-20 00:20:08 +08:00
35 changed files with 3311 additions and 10902 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ on:
image_tag: image_tag:
description: 'Tag for this image build' description: 'Tag for this image build'
type: string type: string
default: 'v2.4.4' default: 'v2.4.5'
required: true required: true
mark_latest: mark_latest:
description: 'Mark this image as latest' description: 'Mark this image as latest'
+1 -1
View File
@@ -21,7 +21,7 @@ on:
version: version:
description: 'Version for this release' description: 'Version for this release'
type: string type: string
default: 'v2.4.4' default: 'v2.4.5'
required: true required: true
make_latest: make_latest:
description: 'Mark this release as latest' description: 'Mark this release as latest'
Generated
+3 -3
View File
@@ -2108,7 +2108,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]] [[package]]
name = "easytier" name = "easytier"
version = "2.4.4" version = "2.4.5"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"anyhow", "anyhow",
@@ -2258,7 +2258,7 @@ dependencies = [
[[package]] [[package]]
name = "easytier-gui" name = "easytier-gui"
version = "2.4.4" version = "2.4.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -2345,7 +2345,7 @@ dependencies = [
[[package]] [[package]]
name = "easytier-web" name = "easytier-web"
version = "2.4.4" version = "2.4.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
+3 -3
View File
@@ -105,9 +105,9 @@ After successful execution, you can check the network status using `easytier-cli
```text ```text
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version | | ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- | | ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.4.4-70e69a38~ | | 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.4.5-70e69a38~ |
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.4-70e69a38~ | | 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.5-70e69a38~ |
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.4-70e69a38~ | | | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.5-70e69a38~ |
``` ```
You can test connectivity between nodes: You can test connectivity between nodes:
+3 -3
View File
@@ -106,9 +106,9 @@ sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.ea
```text ```text
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version | | ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- | | ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.4.4-70e69a38~ | | 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.4.5-70e69a38~ |
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.4-70e69a38~ | | 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.5-70e69a38~ |
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.4-70e69a38~ | | | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.5-70e69a38~ |
``` ```
您可以测试节点之间的连通性: 您可以测试节点之间的连通性:
@@ -22,7 +22,10 @@ get_tun_iface() {
ip link | awk -F': ' '/ tun[[:alnum:]]+/ {print $2; exit}' ip link | awk -F': ' '/ tun[[:alnum:]]+/ {print $2; exit}'
} }
get_hot_iface() { get_hot_iface() {
ip link | awk -F': ' '/(^| )(swlan[[:alnum:]_]*|softap[[:alnum:]_]*|ap[[:alnum:]_]*)\:/ {print $2; exit}' | cut -d'@' -f1 | head -n1 ip link | awk -F': ' '/(^| )(swlan[[:alnum:]_]*|softap[[:alnum:]_]*|p2p-wlan[[:alnum:]_]*|ap[[:alnum:]_]*)\:/ {print $2; exit}' | cut -d'@' -f1 | head -n1
}
get_usb_iface() {
ip link | awk -F': ' '/(^| )(usb[[:alnum:]_]*|rndis[[:alnum:]_]*|eth[[:alnum:]_]*)\:/ {print $2; exit}' | cut -d'@' -f1 | head -n1
} }
get_hot_cidr() { get_hot_cidr() {
ip -4 addr show dev "$1" | awk '/inet /{print $2; exit}' ip -4 addr show dev "$1" | awk '/inet /{print $2; exit}'
@@ -33,10 +36,12 @@ set_nat_rules() {
ET_IFACE=$(get_et_iface) ET_IFACE=$(get_et_iface)
[ -z "$ET_IFACE" ] && ET_IFACE="$(get_tun_iface)" [ -z "$ET_IFACE" ] && ET_IFACE="$(get_tun_iface)"
HOT_IFACE=$(get_hot_iface) HOT_IFACE=$(get_hot_iface)
USB_IFACE=$(get_usb_iface)
HOT_CIDR=$(get_hot_cidr "$HOT_IFACE") HOT_CIDR=$(get_hot_cidr "$HOT_IFACE")
USB_CIDR=$(get_hot_cidr "$USB_IFACE")
# 如果热点关闭就删除自定义链 # 如果热点关闭就删除自定义链
[ -n "$ET_IFACE" ] && [ -n "$HOT_CIDR" ] || return 1 [ -n "$ET_IFACE" ] && { [ -n "$HOT_CIDR" ] || [ -n "$USB_CIDR" ]; } || return 1
# 创建自定义链(如不存在) # 创建自定义链(如不存在)
iptables -t nat -N ET_NAT 2>/dev/null iptables -t nat -N ET_NAT 2>/dev/null
@@ -49,13 +54,22 @@ set_nat_rules() {
iptables -I FORWARD 1 -j ET_FWD iptables -I FORWARD 1 -j ET_FWD
# 添加规则 # 添加规则
iptables -t nat -A ET_NAT -s "$HOT_CIDR" -o "$ET_IFACE" -j MASQUERADE if [ -n "$HOT_CIDR" ]; then
iptables -A ET_FWD -i "$HOT_IFACE" -o "$ET_IFACE" \ iptables -t nat -A ET_NAT -s "$HOT_CIDR" -o "$ET_IFACE" -j MASQUERADE
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT iptables -A ET_FWD -i "$HOT_IFACE" -o "$ET_IFACE" \
iptables -A ET_FWD -i "$ET_IFACE" -o "$HOT_IFACE" \ -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
-m state --state ESTABLISHED,RELATED -j ACCEPT iptables -A ET_FWD -i "$ET_IFACE" -o "$HOT_IFACE" \
-m state --state ESTABLISHED,RELATED -j ACCEPT
echo "[ET-NAT] Rules applied: $HOT_IFACE $HOT_CIDR$ET_IFACE" >> "$LOG_FILE" echo "[ET-NAT] Rules applied: $HOT_IFACE $HOT_CIDR$ET_IFACE" >> "$LOG_FILE"
fi
if [ -n "$USB_CIDR" ]; then
iptables -t nat -A ET_NAT -s "$USB_CIDR" -o "$ET_IFACE" -j MASQUERADE
iptables -A ET_FWD -i "$USB_IFACE" -o "$ET_IFACE" \
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
iptables -A ET_FWD -i "$ET_IFACE" -o "$USB_IFACE" \
-m state --state ESTABLISHED,RELATED -j ACCEPT
echo "[ET-NAT] Rules applied: $USB_IFACE $USB_CIDR$ET_IFACE" >> "$LOG_FILE"
fi
} }
flush_rules() { flush_rules() {
+1 -1
View File
@@ -1,6 +1,6 @@
id=easytier_magisk id=easytier_magisk
name=EasyTier_Magisk name=EasyTier_Magisk
version=v2.4.4 version=v2.4.5
versionCode=1 versionCode=1
author=EasyTier author=EasyTier
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier) description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
+4 -5
View File
@@ -1,7 +1,7 @@
{ {
"name": "easytier-gui", "name": "easytier-gui",
"type": "module", "type": "module",
"version": "2.4.4", "version": "2.4.5",
"private": true, "private": true,
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4", "packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
"scripts": { "scripts": {
@@ -13,18 +13,17 @@
"lint:fix": "eslint . --ignore-pattern src-tauri --fix" "lint:fix": "eslint . --ignore-pattern src-tauri --fix"
}, },
"dependencies": { "dependencies": {
"@primevue/themes": "4.3.3", "@primeuix/themes": "^1.2.3",
"@tauri-apps/plugin-autostart": "2.0.0", "@tauri-apps/plugin-autostart": "2.0.0",
"@tauri-apps/plugin-clipboard-manager": "2.3.0", "@tauri-apps/plugin-clipboard-manager": "2.3.0",
"@tauri-apps/plugin-os": "2.3.0", "@tauri-apps/plugin-os": "2.3.0",
"@tauri-apps/plugin-process": "2.3.0", "@tauri-apps/plugin-process": "2.3.0",
"@tauri-apps/plugin-shell": "2.3.0", "@tauri-apps/plugin-shell": "2.3.0",
"@vueuse/core": "^11.2.0", "@vueuse/core": "^11.2.0",
"aura": "link:@primevue\\themes\\aura",
"easytier-frontend-lib": "workspace:*", "easytier-frontend-lib": "workspace:*",
"ip-num": "1.5.1", "ip-num": "1.5.1",
"pinia": "^2.2.4", "pinia": "^2.2.4",
"primevue": "4.3.3", "primevue": "^4.3.9",
"tauri-plugin-vpnservice-api": "workspace:*", "tauri-plugin-vpnservice-api": "workspace:*",
"vue": "^3.5.12", "vue": "^3.5.12",
"vue-router": "^4.4.5" "vue-router": "^4.4.5"
@@ -32,7 +31,7 @@
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^3.7.3", "@antfu/eslint-config": "^3.7.3",
"@intlify/unplugin-vue-i18n": "^5.2.0", "@intlify/unplugin-vue-i18n": "^5.2.0",
"@primevue/auto-import-resolver": "4.3.3", "@primevue/auto-import-resolver": "4.3.9",
"@tauri-apps/api": "2.7.0", "@tauri-apps/api": "2.7.0",
"@tauri-apps/cli": "2.7.1", "@tauri-apps/cli": "2.7.1",
"@types/default-gateway": "^7.2.2", "@types/default-gateway": "^7.2.2",
-7220
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "easytier-gui" name = "easytier-gui"
version = "2.4.4" version = "2.4.5"
description = "EasyTier GUI" description = "EasyTier GUI"
authors = ["you"] authors = ["you"]
edition = "2021" edition = "2021"
+6 -2
View File
@@ -17,9 +17,13 @@
"createUpdaterArtifacts": false "createUpdaterArtifacts": false
}, },
"productName": "easytier-gui", "productName": "easytier-gui",
"version": "2.4.4", "version": "2.4.5",
"identifier": "com.kkrainbow.easytier", "identifier": "com.kkrainbow.easytier",
"plugins": {}, "plugins": {
"shell": {
"open": "^.+"
}
},
"app": { "app": {
"windows": [ "windows": [
{ {
+2 -10
View File
@@ -9,7 +9,6 @@ declare global {
const EffectScope: typeof import('vue')['EffectScope'] const EffectScope: typeof import('vue')['EffectScope']
const MenuItemExit: typeof import('./composables/tray')['MenuItemExit'] const MenuItemExit: typeof import('./composables/tray')['MenuItemExit']
const MenuItemShow: typeof import('./composables/tray')['MenuItemShow'] const MenuItemShow: typeof import('./composables/tray')['MenuItemShow']
const ReinitTray: typeof import('./composables/tray')['ReinitTray']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const collectNetworkInfos: typeof import('./composables/network')['collectNetworkInfos'] const collectNetworkInfos: typeof import('./composables/network')['collectNetworkInfos']
const computed: typeof import('vue')['computed'] const computed: typeof import('vue')['computed']
@@ -18,10 +17,8 @@ declare global {
const customRef: typeof import('vue')['customRef'] const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent'] const defineComponent: typeof import('vue')['defineComponent']
const definePage: typeof import('unplugin-vue-router/runtime')['definePage']
const defineStore: typeof import('pinia')['defineStore'] const defineStore: typeof import('pinia')['defineStore']
const effectScope: typeof import('vue')['effectScope'] const effectScope: typeof import('vue')['effectScope']
const event2human: typeof import('./composables/utils')['event2human']
const generateMenuItem: typeof import('./composables/tray')['generateMenuItem'] const generateMenuItem: typeof import('./composables/tray')['generateMenuItem']
const generateNetworkConfig: typeof import('./composables/network')['generateNetworkConfig'] const generateNetworkConfig: typeof import('./composables/network')['generateNetworkConfig']
const getActivePinia: typeof import('pinia')['getActivePinia'] const getActivePinia: typeof import('pinia')['getActivePinia']
@@ -30,7 +27,6 @@ declare global {
const getEasytierVersion: typeof import('./composables/network')['getEasytierVersion'] const getEasytierVersion: typeof import('./composables/network')['getEasytierVersion']
const getOsHostname: typeof import('./composables/network')['getOsHostname'] const getOsHostname: typeof import('./composables/network')['getOsHostname']
const h: typeof import('vue')['h'] const h: typeof import('vue')['h']
const initMobileService: typeof import('./composables/mobile_vpn')['initMobileService']
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService'] const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
const inject: typeof import('vue')['inject'] const inject: typeof import('vue')['inject']
const isAutostart: typeof import('./composables/network')['isAutostart'] const isAutostart: typeof import('./composables/network')['isAutostart']
@@ -38,7 +34,6 @@ declare global {
const isReactive: typeof import('vue')['isReactive'] const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly'] const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef'] const isRef: typeof import('vue')['isRef']
const loadRunningInstanceIdsFromLocalStorage: typeof import('./stores/network')['loadRunningInstanceIdsFromLocalStorage']
const mapActions: typeof import('pinia')['mapActions'] const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters'] const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState'] const mapState: typeof import('pinia')['mapState']
@@ -46,8 +41,6 @@ declare global {
const mapWritableState: typeof import('pinia')['mapWritableState'] const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw'] const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick'] const nextTick: typeof import('vue')['nextTick']
const num2ipv4: typeof import('./composables/utils')['num2ipv4']
const num2ipv6: typeof import('./composables/utils')['num2ipv6']
const onActivated: typeof import('vue')['onActivated'] const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount'] const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
@@ -74,7 +67,6 @@ declare global {
const retainNetworkInstance: typeof import('./composables/network')['retainNetworkInstance'] const retainNetworkInstance: typeof import('./composables/network')['retainNetworkInstance']
const runNetworkInstance: typeof import('./composables/network')['runNetworkInstance'] const runNetworkInstance: typeof import('./composables/network')['runNetworkInstance']
const setActivePinia: typeof import('pinia')['setActivePinia'] const setActivePinia: typeof import('pinia')['setActivePinia']
const setAutoLaunchStatus: typeof import('./composables/network')['setAutoLaunchStatus']
const setLoggingLevel: typeof import('./composables/network')['setLoggingLevel'] const setLoggingLevel: typeof import('./composables/network')['setLoggingLevel']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const setTrayMenu: typeof import('./composables/tray')['setTrayMenu'] const setTrayMenu: typeof import('./composables/tray')['setTrayMenu']
@@ -85,7 +77,6 @@ declare global {
const shallowReadonly: typeof import('vue')['shallowReadonly'] const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef'] const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs'] const storeToRefs: typeof import('pinia')['storeToRefs']
const timeAgoCn: typeof import('./composables/utils')['timeAgoCn']
const toRaw: typeof import('vue')['toRaw'] const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef'] const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs'] const toRefs: typeof import('vue')['toRefs']
@@ -116,6 +107,7 @@ declare global {
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue') import('vue')
} }
// for vue template auto import // for vue template auto import
import { UnwrapRef } from 'vue' import { UnwrapRef } from 'vue'
declare module 'vue' { declare module 'vue' {
@@ -216,4 +208,4 @@ declare module 'vue' {
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']> readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']> readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
} }
} }
+6 -2
View File
@@ -93,7 +93,7 @@ async function registerVpnServiceListener() {
) )
} }
function getRoutesForVpn(routes: Route[]): string[] { function getRoutesForVpn(routes: Route[], node_config: NetworkTypes.NetworkConfig): string[] {
if (!routes) { if (!routes) {
return [] return []
} }
@@ -108,6 +108,10 @@ function getRoutesForVpn(routes: Route[]): string[] {
} }
} }
node_config.routes.forEach(r => {
ret.push(r)
})
// sort and dedup // sort and dedup
return Array.from(new Set(ret)).sort() return Array.from(new Set(ret)).sort()
} }
@@ -142,7 +146,7 @@ async function onNetworkInstanceChange() {
network_length = 24 network_length = 24
} }
const routes = getRoutesForVpn(curNetworkInfo?.routes) const routes = getRoutesForVpn(curNetworkInfo?.routes, networkStore.curNetwork)
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes) const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
+1 -1
View File
@@ -1,4 +1,4 @@
import Aura from '@primevue/themes/aura' import Aura from '@primeuix/themes/aura';
import PrimeVue from 'primevue/config' import PrimeVue from 'primevue/config'
import ToastService from 'primevue/toastservice' import ToastService from 'primevue/toastservice'
+51 -49
View File
@@ -292,61 +292,63 @@ async function saveTomlConfig(tomlConfig: string) {
<About /> <About />
</Dialog> </Dialog>
<div> <div class="w-full">
<Toolbar> <div class="flex items-center gap-4 p-4 h-20">
<template #start> <!-- 网络按钮 -->
<div class="flex items-center"> <div class="flex shrink-0 items-center">
<Button icon="pi pi-plus" severity="primary" :label="t('add_new_network')" @click="addNewNetwork" /> <Button icon="pi pi-plus" severity="primary" :label="t('add_new_network')" class="hidden md:inline-flex"
</div> @click="addNewNetwork" />
</template> <Button icon="pi pi-plus" severity="primary" class="md:hidden px-6" @click="addNewNetwork" />
</div>
<template #center> <!-- 网络选择 - 占据中间剩余空间 -->
<div class="min-w-40"> <Select v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
<Select v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false" :placeholder="t('select_network')" class="flex-1 h-full min-w-0">
:placeholder="t('select_network')" class="w-full"> <template #value="slotProps">
<template #value="slotProps"> <div class="flex items-center content-center min-w-0">
<div class="flex items-start content-center"> <div class="mr-4 flex-col min-w-0 flex-1">
<div class="mr-4 flex-col"> <span class="truncate block"> &nbsp; {{ slotProps.value.network_name }}</span>
<span>{{ slotProps.value.network_name }}</span> </div>
</div> <Tag class="my-auto leading-3 shrink-0"
<Tag class="my-auto leading-3" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
:value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')" /> :value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')" />
</div>
</template>
<template #option="slotProps">
<div class="flex flex-col items-start content-center max-w-full">
<div class="flex items-center min-w-0 w-full">
<div class="mr-4 min-w-0 flex-1">
<span class="truncate block">{{ t('network_name') }}: {{ slotProps.option.network_name }}</span>
</div> </div>
</template> <Tag class="my-auto leading-3 shrink-0"
<template #option="slotProps"> :severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'"
<div class="flex flex-col items-start content-center max-w-full"> :value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')" />
<div class="flex"> </div>
<div class="mr-4"> <div v-if="slotProps.option.networking_method !== NetworkTypes.NetworkingMethod.Standalone"
{{ t('network_name') }}: {{ slotProps.option.network_name }} class="max-w-full overflow-hidden text-ellipsis">
</div> {{ slotProps.option.networking_method === NetworkTypes.NetworkingMethod.Manual
<Tag class="my-auto leading-3" ? slotProps.option.peer_urls.join(', ')
:severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'" : slotProps.option.public_server_url }}
:value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')" /> </div>
</div> <div
<div v-if="slotProps.option.networking_method !== NetworkTypes.NetworkingMethod.Standalone" v-if="isRunning(slotProps.option.instance_id) && networkStore.instances[slotProps.option.instance_id].detail && (!!networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)">
class="max-w-full overflow-hidden text-ellipsis"> {{
{{ slotProps.option.networking_method === NetworkTypes.NetworkingMethod.Manual Utils.ipv4InetToString(networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)
? slotProps.option.peer_urls.join(', ') }}
: slotProps.option.public_server_url }} </div>
</div> </div>
<div </template>
v-if="isRunning(slotProps.option.instance_id) && networkStore.instances[slotProps.option.instance_id].detail && (!!networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)"> </Select>
{{
Utils.ipv4InetToString(networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)
}}
</div>
</div>
</template>
</Select>
</div>
</template>
<template #end> <!-- 设置按钮 -->
<div class="flex items-center shrink-0">
<Button icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')" <Button icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')"
class="hidden md:inline-flex" aria-controls="overlay_setting_menu" @click="toggle_setting_menu" />
<Button icon="pi pi-cog" severity="secondary" aria-haspopup="true" class="md:hidden px-6"
aria-controls="overlay_setting_menu" @click="toggle_setting_menu" /> aria-controls="overlay_setting_menu" @click="toggle_setting_menu" />
<TieredMenu id="overlay_setting_menu" ref="setting_menu" :model="setting_menu_items" :popup="true" /> <TieredMenu id="overlay_setting_menu" ref="setting_menu" :model="setting_menu_items" :popup="true" />
</template> </div>
</Toolbar> </div>
</div> </div>
<Panel class="h-full overflow-y-auto"> <Panel class="h-full overflow-y-auto">
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "easytier-web" name = "easytier-web"
version = "2.4.4" version = "2.4.5"
edition = "2021" edition = "2021"
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server." description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
+4 -3
View File
@@ -18,18 +18,19 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@primevue/themes": "4.3.3", "@primeuix/themes": "^1.2.3",
"@vueuse/core": "^11.1.0", "@vueuse/core": "^11.1.0",
"aura": "link:@primevue\\themes\\aura",
"axios": "^1.7.7", "axios": "^1.7.7",
"chart.js": "^4.5.0",
"floating-vue": "^5.2", "floating-vue": "^5.2",
"ip-num": "1.5.1", "ip-num": "1.5.1",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primevue": "4.3.3", "primevue": "^4.3.9",
"tailwindcss-primeui": "^0.3.4", "tailwindcss-primeui": "^0.3.4",
"ts-md5": "^1.3.1", "ts-md5": "^1.3.1",
"uuid": "^11.0.2", "uuid": "^11.0.2",
"vue": "^3.5.12", "vue": "^3.5.12",
"vue-chartjs": "^5.3.2",
"vue-i18n": "^10.0.4" "vue-i18n": "^10.0.4"
}, },
"devDependencies": { "devDependencies": {
@@ -170,7 +170,7 @@ const bool_flags: BoolFlag[] = [
{ field: 'enable_private_mode', help: 'enable_private_mode_help' }, { field: 'enable_private_mode', help: 'enable_private_mode_help' },
] ]
const portForwardProtocolOptions = ref(["tcp","udp"]); const portForwardProtocolOptions = ref(["tcp", "udp"]);
</script> </script>
@@ -178,7 +178,7 @@ const portForwardProtocolOptions = ref(["tcp","udp"]);
<div class="frontend-lib"> <div class="frontend-lib">
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="w-11/12 self-center "> <div class="w-full self-center ">
<Panel :header="t('basic_settings')"> <Panel :header="t('basic_settings')">
<div class="flex flex-col gap-y-2"> <div class="flex flex-col gap-y-2">
<div class="flex flex-row gap-x-9 flex-wrap"> <div class="flex flex-row gap-x-9 flex-wrap">
@@ -227,9 +227,8 @@ const portForwardProtocolOptions = ref(["tcp","udp"]);
class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions" /> class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.PublicServer" <AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.PublicServer"
v-model="curNetwork.public_server_url" :suggestions="publicServerSuggestions" v-model="curNetwork.public_server_url" :suggestions="publicServerSuggestions" class="grow"
class="grow" dropdown :complete-on-focus="false" dropdown :complete-on-focus="false" @complete="searchPresetPublicServers" />
@complete="searchPresetPublicServers" />
</div> </div>
</div> </div>
</div> </div>
@@ -436,56 +435,36 @@ const portForwardProtocolOptions = ref(["tcp","udp"]);
</div> </div>
<div v-for="(row, index) in curNetwork.port_forwards" class="form-row"> <div v-for="(row, index) in curNetwork.port_forwards" class="form-row">
<div style="display: flex; gap: 0.5rem; align-items: flex-end;"> <div style="display: flex; gap: 0.5rem; align-items: flex-end;">
<SelectButton v-model="row.proto" :options="portForwardProtocolOptions" :allow-empty="false"/> <SelectButton v-model="row.proto" :options="portForwardProtocolOptions" :allow-empty="false" />
<div style="flex-grow: 4;"> <div style="flex-grow: 4;">
<InputGroup> <InputGroup>
<InputText <InputText v-model="row.bind_ip" :placeholder="t('port_forwards_bind_addr')" />
v-model="row.bind_ip"
:placeholder="t('port_forwards_bind_addr')"
/>
<InputGroupAddon> <InputGroupAddon>
<span style="font-weight: bold">:</span> <span style="font-weight: bold">:</span>
</InputGroupAddon> </InputGroupAddon>
<InputNumber v-model="row.bind_port" :format="false" <InputNumber v-model="row.bind_port" :format="false" inputId="horizontal-buttons" :step="1"
inputId="horizontal-buttons" :step="1" mode="decimal" :min="1" mode="decimal" :min="1" :max="65535" fluid class="max-w-20" />
:max="65535" fluid
class="max-w-20"/>
</InputGroup> </InputGroup>
</div> </div>
<div style="flex-grow: 4;"> <div style="flex-grow: 4;">
<InputGroup> <InputGroup>
<InputText <InputText v-model="row.dst_ip" :placeholder="t('port_forwards_dst_addr')" />
v-model="row.dst_ip"
:placeholder="t('port_forwards_dst_addr')"
/>
<InputGroupAddon> <InputGroupAddon>
<span style="font-weight: bold">:</span> <span style="font-weight: bold">:</span>
</InputGroupAddon> </InputGroupAddon>
<InputNumber v-model="row.dst_port" :format="false" <InputNumber v-model="row.dst_port" :format="false" inputId="horizontal-buttons" :step="1"
inputId="horizontal-buttons" :step="1" mode="decimal" :min="1" mode="decimal" :min="1" :max="65535" fluid class="max-w-20" />
:max="65535" fluid
class="max-w-20"/>
</InputGroup> </InputGroup>
</div> </div>
<div style="flex-grow: 1;"> <div style="flex-grow: 1;">
<Button <Button v-if="curNetwork.port_forwards.length > 0" icon="pi pi-trash" severity="danger" text
v-if="curNetwork.port_forwards.length > 0" rounded @click="removeRow(index, curNetwork.port_forwards)" />
icon="pi pi-trash"
severity="danger"
text
rounded
@click="removeRow(index,curNetwork.port_forwards)"
/>
</div> </div>
</div> </div>
</div> </div>
<div class="flex justify-content-end mt-4"> <div class="flex justify-content-end mt-4">
<Button <Button icon="pi pi-plus" :label="t('port_forwards_add_btn')" severity="success"
icon="pi pi-plus" @click="addRow(curNetwork.port_forwards)" />
:label="t('port_forwards_add_btn')"
severity="success"
@click="addRow(curNetwork.port_forwards)"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -0,0 +1,279 @@
<template>
<div
class="bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-blue-900/20 dark:to-indigo-800/20 rounded-xl p-4 border border-blue-200 dark:border-blue-700 shadow-md hover:shadow-lg transition-all duration-300">
<div class="flex items-center justify-center mb-3">
<div class="flex gap-2 text-sm">
<span class="flex items-center gap-1 w-32">
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
<span class="text-green-600 dark:text-green-400 truncate">{{ t('upload') }}: {{ currentUpload }}/s</span>
</span>
<span class="flex items-center gap-1 w-32">
<div class="w-2 h-2 bg-blue-500 rounded-full"></div>
<span class="text-blue-600 dark:text-blue-400 truncate">{{ t('download') }}: {{ currentDownload }}/s</span>
</span>
</div>
</div>
<div class="h-32">
<canvas ref="chartCanvas"></canvas>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
LineController,
Title,
Tooltip,
Legend,
Filler
} from 'chart.js'
import { useI18n } from 'vue-i18n';
const { t } = useI18n()
// 注册Chart.js组件
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
LineController,
Title,
Tooltip,
Legend,
Filler
)
interface Props {
uploadRate: string
downloadRate: string
}
const props = defineProps<Props>()
const chartCanvas = ref<HTMLCanvasElement>()
let chart: ChartJS | null = null
let updateTimer: number | null = null
// 存储历史数据,最多保存30个数据点(1分钟历史)
const maxDataPoints = 120
const uploadHistory: number[] = []
const downloadHistory: number[] = []
const timeLabels: string[] = []
const currentUpload = ref('0')
const currentDownload = ref('0')
// 将带单位的速率字符串转换为字节数
function parseRateToBytes(rateStr: string): number {
if (!rateStr || rateStr === '0') return 0
const match = rateStr.match(/([0-9.]+)\s*([KMGT]?i?B)/i)
if (!match) return 0
const value = parseFloat(match[1])
const unit = match[2].toUpperCase()
const multipliers: { [key: string]: number } = {
'B': 1,
'KB': 1000,
'KIB': 1024,
'MB': 1000000,
'MIB': 1024 * 1024,
'GB': 1000000000,
'GIB': 1024 * 1024 * 1024,
'TB': 1000000000000,
'TIB': 1024 * 1024 * 1024 * 1024
}
return value * (multipliers[unit] || 1)
}
// 格式化字节为可读格式
function formatBytes(bytes: number): string {
if (bytes < 1) return bytes.toFixed(1) + ' B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
// 更新数据
function updateData() {
const uploadBytes = parseRateToBytes(props.uploadRate)
const downloadBytes = parseRateToBytes(props.downloadRate)
currentUpload.value = formatBytes(uploadBytes)
currentDownload.value = formatBytes(downloadBytes)
// 添加新数据点
uploadHistory.push(uploadBytes)
downloadHistory.push(downloadBytes)
// 生成时间标签
const now = new Date()
const timeStr = now.toLocaleTimeString('zh-CN', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
timeLabels.push(timeStr)
// 保持数据点数量不超过最大值
if (uploadHistory.length > maxDataPoints) {
uploadHistory.shift()
downloadHistory.shift()
timeLabels.shift()
}
// 更新图表
if (chart) {
chart.data.labels = timeLabels
chart.data.datasets[0].data = uploadHistory
chart.data.datasets[1].data = downloadHistory
chart.update('none')
}
}
// 初始化图表
function initChart() {
if (!chartCanvas.value) return
const ctx = chartCanvas.value.getContext('2d')
if (!ctx) return
chart = new ChartJS(ctx, {
type: 'line',
data: {
labels: timeLabels,
datasets: [
{
label: t('upload'),
data: uploadHistory,
borderColor: 'rgb(34, 197, 94)',
backgroundColor: 'rgba(34, 197, 94, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 4
},
{
label: t('download'),
data: downloadHistory,
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function (context: any) {
const value = context.parsed.y
return `${context.dataset.label}: ${formatBytes(value)}/s`
}
}
}
},
scales: {
x: {
display: true,
grid: {
display: false
},
ticks: {
maxTicksLimit: 3,
font: {
size: 8
}
}
},
y: {
display: true,
beginAtZero: true,
min: 0,
grid: {
color: 'rgba(0, 0, 0, 0.1)'
},
ticks: {
callback: function (value: any) {
return formatBytes(value as number)
},
font: {
size: 8
},
},
}
},
animation: {
duration: 10
}
}
})
}
// 监听props变化
watch([() => props.uploadRate, () => props.downloadRate], () => {
updateData()
}, { immediate: true })
onMounted(async () => {
// add initial point
const now = new Date();
for (let i = 0; i < maxDataPoints; i++) {
let date = new Date(now.getTime() - (maxDataPoints - i) * 2000)
const timeStr = date.toLocaleTimeString(navigator.language, {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
uploadHistory.push(0)
downloadHistory.push(0)
timeLabels.push(timeStr)
}
await nextTick()
initChart()
updateData()
// 启动定时器,每2秒更新一次图表
updateTimer = window.setInterval(() => {
updateData()
}, 2000)
})
onUnmounted(() => {
if (chart) {
chart.destroy()
}
if (updateTimer) {
clearInterval(updateTimer)
}
})
</script>
@@ -5,7 +5,8 @@ import { NetworkInstance, type TunnelInfo, type NodeInfo, type PeerRoutePair } f
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { computed, onMounted, onUnmounted, ref } from 'vue'; import { computed, onMounted, onUnmounted, ref } from 'vue';
import { ipv4InetToString, ipv4ToString, ipv6ToString } from '../modules/utils'; import { ipv4InetToString, ipv4ToString, ipv6ToString } from '../modules/utils';
import { DataTable, Column, Tag, Chip, Button, Dialog, ScrollPanel, Timeline, Divider, Card, } from 'primevue'; import { Badge, DataTable, Column, Tag, Chip, Button, Dialog, ScrollPanel, Timeline, Divider, Card, } from 'primevue';
import NetworkChart from './NetworkChart.vue';
const props = defineProps<{ const props = defineProps<{
curNetworkInst: NetworkInstance | null, curNetworkInst: NetworkInstance | null,
@@ -285,6 +286,10 @@ let prevTxSum = 0
let prevRxSum = 0 let prevRxSum = 0
const txRate = ref('0') const txRate = ref('0')
const rxRate = ref('0') const rxRate = ref('0')
// 控制节点详细信息chips的显示/隐藏
const showNodeDetails = ref(false)
onMounted(() => { onMounted(() => {
rateIntervalId = window.setInterval(() => { rateIntervalId = window.setInterval(() => {
const curTxSum = txGlobalSum() const curTxSum = txGlobalSum()
@@ -365,36 +370,23 @@ function showEventLogs() {
</template> </template>
<template #content> <template #content>
<div class="flex w-full flex-col gap-y-5"> <div class="flex w-full flex-col gap-y-5">
<div class="m-0 flex flex-row justify-center gap-x-5"> <div class="gap-4">
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid green"> <!-- 网络流量图表 -->
<div class="font-bold"> <div class="w-full">
{{ t('peer_count') }} <NetworkChart :upload-rate="txRate" :download-rate="rxRate" />
</div>
<div class="text-5xl mt-1">
{{ peerCount }}
</div>
</div>
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid purple">
<div class="font-bold">
{{ t('upload') }}
</div>
<div class="text-xl mt-2">
{{ txRate }}/s
</div>
</div>
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid fuchsia">
<div class="font-bold">
{{ t('download') }}
</div>
<div class="text-xl mt-2">
{{ rxRate }}/s
</div>
</div> </div>
</div> </div>
<div class="flex flex-row items-center flex-wrap w-full max-h-40 overflow-scroll"> <!-- 展开/收起节点详细信息的divider按钮 -->
<div class="w-full">
<Button @click="showNodeDetails = !showNodeDetails"
:icon="showNodeDetails ? 'pi pi-chevron-up' : 'pi pi-chevron-down'"
:label="showNodeDetails ? t('hide_node_details') : t('show_node_details')" severity="secondary" outlined
class="w-full justify-center" size="small" />
</div>
<!-- 节点详细信息chips根据showNodeDetails状态显示/隐藏 -->
<div v-show="showNodeDetails" class="flex flex-row items-center flex-wrap w-full max-h-40 overflow-scroll">
<Chip v-for="(chip, i) in myNodeInfoChips" :key="i" :label="chip.label" :icon="chip.icon" <Chip v-for="(chip, i) in myNodeInfoChips" :key="i" :label="chip.label" :icon="chip.icon"
class="mr-2 mt-2 text-sm" /> class="mr-2 mt-2 text-sm" />
</div> </div>
@@ -411,7 +403,15 @@ function showEventLogs() {
<Card> <Card>
<template #title> <template #title>
{{ t('peer_info') }} <div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<span>{{ t('peer_info') }}</span>
</div>
<div class="flex items-center gap-1">
<Badge :value="peerCount" severity="info"
class="text-lg font-semibold px-2 py-1 rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200" />
</div>
</div>
</template> </template>
<template #content> <template #content>
<DataTable :value="peerRouteInfos" column-resize-mode="fit" table-class="w-full"> <DataTable :value="peerRouteInfos" column-resize-mode="fit" table-class="w-full">
@@ -2,7 +2,7 @@ import './style.css'
import type { App } from 'vue'; import type { App } from 'vue';
import { Config, Status, ConfigEditDialog } from "./components"; import { Config, Status, ConfigEditDialog } from "./components";
import Aura from '@primevue/themes/aura' import Aura from '@primeuix/themes/aura';
import PrimeVue from 'primevue/config' import PrimeVue from 'primevue/config'
import I18nUtils from './modules/i18n' import I18nUtils from './modules/i18n'
@@ -48,6 +48,8 @@ hide_dock_icon: 隐藏 Dock 图标
show_dock_icon: 显示 Dock 图标 show_dock_icon: 显示 Dock 图标
exit: 退出 exit: 退出
chips_placeholder: 例如: {0}, 输入后在下拉框中选择生效 chips_placeholder: 例如: {0}, 输入后在下拉框中选择生效
show_node_details: 显示节点详细信息
hide_node_details: 隐藏节点详细信息
hostname_placeholder: '留空默认为主机名: {0}' hostname_placeholder: '留空默认为主机名: {0}'
dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名称时,将会在设置TUN的IP时产生冲突,留空以自动生成随机名称 dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名称时,将会在设置TUN的IP时产生冲突,留空以自动生成随机名称
off_text: 点击关闭 off_text: 点击关闭
@@ -48,6 +48,8 @@ hide_dock_icon: Hide Dock Icon
show_dock_icon: Show Dock Icon show_dock_icon: Show Dock Icon
exit: Exit exit: Exit
use_latency_first: Latency First Mode use_latency_first: Latency First Mode
show_node_details: Show Node Details
hide_node_details: Hide Node Details
chips_placeholder: 'e.g: {0}, select from the dropdown after input' chips_placeholder: 'e.g: {0}, select from the dropdown after input'
hostname_placeholder: 'Leave blank and default to host name: {0}' hostname_placeholder: 'Leave blank and default to host name: {0}'
dev_name_placeholder: 'Note: When multiple networks use the same TUN interface name at the same time, there will be a conflict when setting the TUN''s IP. Leave blank to automatically generate a random name.' dev_name_placeholder: 'Note: When multiple networks use the same TUN interface name at the same time, there will be a conflict when setting the TUN''s IP. Leave blank to automatically generate a random name.'
+5 -5
View File
@@ -9,18 +9,18 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@primevue/themes": "4.3.3", "@modyfi/vite-plugin-yaml": "^1.1.0",
"aura": "link:@primevue/themes/aura", "@primeuix/themes": "^1.2.3",
"axios": "^1.7.7", "axios": "^1.7.7",
"easytier-frontend-lib": "workspace:*", "easytier-frontend-lib": "workspace:*",
"primevue": "4.3.3", "primevue": "^4.3.9",
"tailwindcss-primeui": "^0.3.4", "tailwindcss-primeui": "^0.3.4",
"vue": "^3.5.12", "vue": "^3.5.12",
"vue-router": "4",
"vue-i18n": "^9.9.1", "vue-i18n": "^9.9.1",
"@modyfi/vite-plugin-yaml": "^1.1.0" "vue-router": "4"
}, },
"devDependencies": { "devDependencies": {
"@primevue/auto-import-resolver": "4.3.9",
"@types/node": "^22.8.6", "@types/node": "^22.8.6",
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue": "^5.1.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { NetworkTypes } from 'easytier-frontend-lib'; import { NetworkTypes } from 'easytier-frontend-lib';
import {computed, ref} from 'vue'; import { computed, ref } from 'vue';
import { Api } from 'easytier-frontend-lib' import { Api } from 'easytier-frontend-lib'
import {AutoComplete, Divider, Button, Textarea} from "primevue"; import { AutoComplete, Divider, Button, Textarea } from "primevue";
import {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost} from "../modules/api-host" import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value)); const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
@@ -28,18 +28,18 @@ const generateConfig = (config: NetworkTypes.NetworkConfig) => {
saveApiHost(apiHost.value) saveApiHost(apiHost.value)
errorMessage.value = ""; errorMessage.value = "";
api.value?.generate_config({ api.value?.generate_config({
config: config config: config
}).then((res) => { }).then((res) => {
if (res.error) { if (res.error) {
errorMessage.value = "Generation failed: " + res.error; errorMessage.value = "Generation failed: " + res.error;
} else if (res.toml_config) { } else if (res.toml_config) {
toml_config.value = res.toml_config; toml_config.value = res.toml_config;
} else { } else {
errorMessage.value = "Api server returned an unexpected response"; errorMessage.value = "Api server returned an unexpected response";
} }
}).catch(err => { }).catch(err => {
errorMessage.value = "Generate request failed: " + (err instanceof Error ? err.message : String(err)); errorMessage.value = "Generate request failed: " + (err instanceof Error ? err.message : String(err));
}); });
}; };
const parseConfig = async () => { const parseConfig = async () => {
@@ -48,7 +48,7 @@ const parseConfig = async () => {
const res = await api.value?.parse_config({ const res = await api.value?.parse_config({
toml_config: toml_config.value toml_config: toml_config.value
}); });
if (res.error) { if (res.error) {
errorMessage.value = "Parse failed: " + res.error; errorMessage.value = "Parse failed: " + res.error;
} else if (res.config) { } else if (res.config) {
@@ -64,31 +64,29 @@ const parseConfig = async () => {
</script> </script>
<template> <template>
<div class="flex items-center justify-center m-5"> <div class="flex items-center justify-center m-5">
<div class="sm:block md:flex w-full"> <div class="sm:block md:flex w-full">
<div class="sm:w-full md:w-1/2 p-4"> <div class="sm:w-full md:w-1/2 p-4">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="w-11/12 self-center "> <div class="w-full self-center ">
<label>ApiHost</label> <label>ApiHost</label>
<AutoComplete id="api-host" v-model="apiHost" dropdown :suggestions="apiHostSuggestions" <AutoComplete id="api-host" v-model="apiHost" dropdown :suggestions="apiHostSuggestions"
@complete="apiHostSearch" class="w-full" /> @complete="apiHostSearch" class="w-full" />
<Divider /> <Divider />
</div> </div>
</div>
<Config :cur-network="newNetworkConfig" @run-network="generateConfig" />
</div>
<div class="sm:w-full md:w-1/2 p-4 flex flex-col h-[calc(100vh-80px)]">
<pre v-if="errorMessage" class="mb-2 p-2 rounded text-sm overflow-auto bg-red-100 text-red-700 max-h-40">{{ errorMessage }}</pre>
<Textarea
v-model="toml_config"
spellcheck="false"
class="w-full flex-grow p-2 whitespace-pre-wrap font-mono resize-none"
placeholder="Press 'Run Network' to generate TOML configuration, or paste your TOML configuration here to parse it"
></Textarea>
<div class="mt-3 flex justify-center">
<Button label="Parse Config" icon="pi pi-arrow-left" icon-pos="left" @click="parseConfig" />
</div>
</div>
</div> </div>
<Config :cur-network="newNetworkConfig" @run-network="generateConfig" />
</div>
<div class="sm:w-full md:w-1/2 p-4 flex flex-col h-[calc(100vh-80px)]">
<pre v-if="errorMessage"
class="mb-2 p-2 rounded text-sm overflow-auto bg-red-100 text-red-700 max-h-40">{{ errorMessage }}</pre>
<Textarea v-model="toml_config" spellcheck="false"
class="w-full flex-grow p-2 whitespace-pre-wrap font-mono resize-none"
placeholder="Press 'Run Network' to generate TOML configuration, or paste your TOML configuration here to parse it"></Textarea>
<div class="mt-3 flex justify-center">
<Button label="Parse Config" icon="pi pi-arrow-left" icon-pos="left" @click="parseConfig" />
</div>
</div>
</div> </div>
</div>
</template> </template>
+1 -1
View File
@@ -4,7 +4,7 @@ import './style.css'
import App from './App.vue' import App from './App.vue'
import EasytierFrontendLib from 'easytier-frontend-lib' import EasytierFrontendLib from 'easytier-frontend-lib'
import PrimeVue from 'primevue/config' import PrimeVue from 'primevue/config'
import Aura from '@primevue/themes/aura' import Aura from '@primeuix/themes/aura';
import ConfirmationService from 'primevue/confirmationservice'; import ConfirmationService from 'primevue/confirmationservice';
import { I18nUtils } from 'easytier-frontend-lib' import { I18nUtils } from 'easytier-frontend-lib'
+2 -1
View File
@@ -3,7 +3,7 @@ name = "easytier"
description = "A full meshed p2p VPN, connecting all your devices in one network with one command." description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
homepage = "https://github.com/EasyTier/EasyTier" homepage = "https://github.com/EasyTier/EasyTier"
repository = "https://github.com/EasyTier/EasyTier" repository = "https://github.com/EasyTier/EasyTier"
version = "2.4.4" version = "2.4.5"
edition = "2021" edition = "2021"
authors = ["kkrainbow"] authors = ["kkrainbow"]
keywords = ["vpn", "p2p", "network", "easytier"] keywords = ["vpn", "p2p", "network", "easytier"]
@@ -237,6 +237,7 @@ windows = { version = "0.52.0", features = [
"Win32_System_Com", "Win32_System_Com",
"Win32_Networking", "Win32_Networking",
"Win32_System_Ole", "Win32_System_Ole",
"Win32_System_Variant",
"Win32_Networking_WinSock", "Win32_Networking_WinSock",
"Win32_System_IO", "Win32_System_IO",
] } ] }
+37 -72
View File
@@ -1,4 +1,4 @@
use std::{io, net::SocketAddr, os::windows::io::AsRawSocket}; use std::{io, mem::ManuallyDrop, net::SocketAddr, os::windows::io::AsRawSocket};
use anyhow::Context; use anyhow::Context;
use network_interface::NetworkInterfaceConfig; use network_interface::NetworkInterfaceConfig;
@@ -18,6 +18,8 @@ use windows::{
System::Com::{ System::Com::{
CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, COINIT_MULTITHREADED,
}, },
System::Ole::{SafeArrayCreateVector, SafeArrayPutElement},
System::Variant::{VARENUM, VARIANT, VT_ARRAY, VT_BSTR, VT_VARIANT},
}, },
}; };
@@ -247,20 +249,20 @@ pub fn add_interface_to_firewall_allowlist(interface_name: &str) -> anyhow::Resu
); );
// Create rules for each protocol type // Create rules for each protocol type
add_protocol_firewall_rules(&policy, interface_name, "TCP", 6)?; // TCP protocol number 6 add_protocol_firewall_rules(&policy, interface_name, "TCP", Some(6))?; // TCP protocol number 6
tracing::debug!("Added TCP firewall rules for interface: {}", interface_name); tracing::debug!("Added TCP firewall rules for interface: {}", interface_name);
add_protocol_firewall_rules(&policy, interface_name, "UDP", 17)?; // UDP protocol number 17 add_protocol_firewall_rules(&policy, interface_name, "UDP", Some(17))?; // UDP protocol number 17
tracing::debug!("Added UDP firewall rules for interface: {}", interface_name); tracing::debug!("Added UDP firewall rules for interface: {}", interface_name);
add_protocol_firewall_rules(&policy, interface_name, "ICMP", 1)?; // ICMP protocol number 1 add_protocol_firewall_rules(&policy, interface_name, "ICMP", Some(1))?; // ICMP protocol number 1
tracing::debug!( tracing::debug!(
"Added ICMP firewall rules for interface: {}", "Added ICMP firewall rules for interface: {}",
interface_name interface_name
); );
// Add fallback rules for all protocols // Add fallback rules for all protocols
add_all_protocols_firewall_rules(&policy, interface_name)?; add_protocol_firewall_rules(&policy, interface_name, "ALL", None)?;
tracing::debug!( tracing::debug!(
"Added fallback all-protocols rules for interface: {}", "Added fallback all-protocols rules for interface: {}",
interface_name interface_name
@@ -279,7 +281,7 @@ fn add_protocol_firewall_rules(
policy: &INetFwPolicy2, policy: &INetFwPolicy2,
interface_name: &str, interface_name: &str,
protocol_name: &str, protocol_name: &str,
protocol_number: i32, protocol_number: Option<i32>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
// Create rules for both inbound and outbound traffic // Create rules for both inbound and outbound traffic
for (is_inbound, direction_name) in [(true, "Inbound"), (false, "Outbound")] { for (is_inbound, direction_name) in [(true, "Inbound"), (false, "Outbound")] {
@@ -307,7 +309,9 @@ fn add_protocol_firewall_rules(
unsafe { unsafe {
rule.SetName(&name_bstr)?; rule.SetName(&name_bstr)?;
rule.SetDescription(&desc_bstr)?; rule.SetDescription(&desc_bstr)?;
rule.SetProtocol(protocol_number)?; if let Some(protocol_number) = protocol_number {
rule.SetProtocol(protocol_number)?;
}
rule.SetAction(NET_FW_ACTION_ALLOW)?; rule.SetAction(NET_FW_ACTION_ALLOW)?;
if is_inbound { if is_inbound {
@@ -322,61 +326,35 @@ fn add_protocol_firewall_rules(
)?; )?;
rule.SetGrouping(&BSTR::from("EasyTier"))?; rule.SetGrouping(&BSTR::from("EasyTier"))?;
// Get rule collection and add new rule // Set the interface for this rule to apply to the specific network interface
let rules = policy.Rules()?; // According to Microsoft docs, interfaces should be represented by their friendly name
rules.Remove(&name_bstr)?; // Remove existing rule with same name first // We need to create a SAFEARRAY of VARIANT strings containing the interface name
rules.Add(&rule)?; let interface_bstr = BSTR::from(interface_name);
}
}
Ok(()) // Create a SAFEARRAY containing one interface name
} let interface_array = SafeArrayCreateVector(VT_VARIANT, 0, 1);
if interface_array.is_null() {
/// Add fallback rules for all protocols return Err(anyhow::anyhow!("Failed to create SAFEARRAY"));
fn add_all_protocols_firewall_rules(
policy: &INetFwPolicy2,
interface_name: &str,
) -> anyhow::Result<()> {
// Create rules for both inbound and outbound traffic
for (is_inbound, direction_name) in [(true, "Inbound"), (false, "Outbound")] {
// Create firewall rule instance
let rule: INetFwRule = unsafe {
CoCreateInstance(
&windows::Win32::NetworkManagement::WindowsFirewall::NetFwRule,
None,
CLSCTX_ALL,
)
}?;
let rule_name = format!(
"EasyTier {} - All Protocols ({})",
interface_name, direction_name
);
let description = format!(
"Allow all protocol traffic on EasyTier interface {}",
interface_name
);
let name_bstr = BSTR::from(&rule_name);
let desc_bstr = BSTR::from(&description);
unsafe {
rule.SetName(&name_bstr)?;
rule.SetDescription(&desc_bstr)?;
// Don't set protocol - allows all protocols by default
rule.SetAction(NET_FW_ACTION_ALLOW)?;
if is_inbound {
rule.SetDirection(NET_FW_RULE_DIR_IN)?;
} else {
rule.SetDirection(NET_FW_RULE_DIR_OUT)?;
} }
rule.SetEnabled(windows::Win32::Foundation::VARIANT_TRUE)?; let index = 0i32;
rule.SetProfiles( let mut variant_interface = VARIANT::default();
NET_FW_PROFILE2_PRIVATE.0 | NET_FW_PROFILE2_PUBLIC.0 | NET_FW_PROFILE2_DOMAIN.0, (*variant_interface.Anonymous.Anonymous).vt = VT_BSTR;
(*variant_interface.Anonymous.Anonymous).Anonymous.bstrVal =
ManuallyDrop::new(interface_bstr);
SafeArrayPutElement(
interface_array,
&index as *const _ as *const i32,
&variant_interface as *const _ as *const std::ffi::c_void,
)?; )?;
rule.SetGrouping(&BSTR::from("EasyTier"))?;
// Create the VARIANT that contains the SAFEARRAY
let mut interface_variant = VARIANT::default();
(*interface_variant.Anonymous.Anonymous).vt = VARENUM(VT_ARRAY.0 | VT_VARIANT.0);
(*interface_variant.Anonymous.Anonymous).Anonymous.parray = interface_array;
rule.SetInterfaces(interface_variant)?;
// Get rule collection and add new rule // Get rule collection and add new rule
let rules = policy.Rules()?; let rules = policy.Rules()?;
@@ -402,8 +380,7 @@ pub fn remove_interface_firewall_rules(interface_name: &str) -> anyhow::Result<(
let rules = unsafe { policy.Rules()? }; let rules = unsafe { policy.Rules()? };
// Remove protocol-specific rules for protocol_name in ["TCP", "UDP", "ICMP", "ALL"] {
for protocol_name in ["TCP", "UDP", "ICMP"] {
for direction in ["Inbound", "Outbound"] { for direction in ["Inbound", "Outbound"] {
let rule_name = format!( let rule_name = format!(
"EasyTier {} - {} Protocol ({})", "EasyTier {} - {} Protocol ({})",
@@ -416,18 +393,6 @@ pub fn remove_interface_firewall_rules(interface_name: &str) -> anyhow::Result<(
} }
} }
// Remove fallback protocol rules
for direction in ["Inbound", "Outbound"] {
let rule_name = format!(
"EasyTier {} - All Protocols ({})",
interface_name, direction
);
let name_bstr = BSTR::from(&rule_name);
unsafe {
let _ = rules.Remove(&name_bstr); // Ignore errors, rule might not exist
}
}
Ok(()) Ok(())
} }
+11 -2
View File
@@ -968,8 +968,17 @@ impl NetworkOptions {
old_udp_whitelist.extend(self.udp_whitelist.clone()); old_udp_whitelist.extend(self.udp_whitelist.clone());
cfg.set_udp_whitelist(old_udp_whitelist); cfg.set_udp_whitelist(old_udp_whitelist);
cfg.set_stun_servers(self.stun_servers.clone()); if let Some(stun_servers) = &self.stun_servers {
cfg.set_stun_servers_v6(self.stun_servers_v6.clone()); let mut old_stun_servers = cfg.get_stun_servers().unwrap_or_default();
old_stun_servers.extend(stun_servers.iter().cloned());
cfg.set_stun_servers(Some(old_stun_servers));
}
if let Some(stun_servers_v6) = &self.stun_servers_v6 {
let mut old_stun_servers_v6 = cfg.get_stun_servers_v6().unwrap_or_default();
old_stun_servers_v6.extend(stun_servers_v6.iter().cloned());
cfg.set_stun_servers_v6(Some(old_stun_servers_v6));
}
Ok(()) Ok(())
} }
} }
+6 -2
View File
@@ -450,7 +450,9 @@ impl PeerPacketFilter for Socks5Server {
let entry_key = match ipv4.get_next_level_protocol() { let entry_key = match ipv4.get_next_level_protocol() {
IpNextHeaderProtocols::Tcp => { IpNextHeaderProtocols::Tcp => {
let tcp_packet = TcpPacket::new(ipv4.payload()).unwrap(); let Some(tcp_packet) = TcpPacket::new(ipv4.payload()) else {
return Some(packet);
};
Socks5Entry { Socks5Entry {
dst: SocketAddr::new(ipv4.get_source().into(), tcp_packet.get_source()), dst: SocketAddr::new(ipv4.get_source().into(), tcp_packet.get_source()),
src: SocketAddr::new( src: SocketAddr::new(
@@ -479,7 +481,9 @@ impl PeerPacketFilter for Socks5Server {
return Some(packet); return Some(packet);
} }
let udp_packet = UdpPacket::new(ipv4.payload()).unwrap(); let Some(udp_packet) = UdpPacket::new(ipv4.payload()) else {
return Some(packet);
};
Socks5Entry { Socks5Entry {
dst: SocketAddr::new(ipv4.get_source().into(), udp_packet.get_source()), dst: SocketAddr::new(ipv4.get_source().into(), udp_packet.get_source()),
src: SocketAddr::new( src: SocketAddr::new(
+17 -15
View File
@@ -25,7 +25,7 @@ pub fn del_netns(name: &str) {
.output(); .output();
} }
pub fn create_netns(name: &str, ipv4: &str) { pub fn create_netns(name: &str, ipv4: &str, ipv6: &str) {
// create netns // create netns
let _ = std::process::Command::new("ip") let _ = std::process::Command::new("ip")
.args(["netns", "add", name]) .args(["netns", "add", name])
@@ -76,20 +76,22 @@ pub fn create_netns(name: &str, ipv4: &str) {
.output() .output()
.unwrap(); .unwrap();
let _ = std::process::Command::new("ip") for ip in [ipv4, ipv6] {
.args([ let _ = std::process::Command::new("ip")
"netns", .args([
"exec", "netns",
name, "exec",
"ip", name,
"addr", "ip",
"add", "addr",
ipv4, "add",
"dev", ip,
get_guest_veth_name(name), "dev",
]) get_guest_veth_name(name),
.output() ])
.unwrap(); .output()
.unwrap();
}
} }
pub fn prepare_bridge(name: &str) { pub fn prepare_bridge(name: &str) {
+20 -6
View File
@@ -40,10 +40,10 @@ pub fn prepare_linux_namespaces() {
del_netns("net_c"); del_netns("net_c");
del_netns("net_d"); del_netns("net_d");
create_netns("net_a", "10.1.1.1/24"); create_netns("net_a", "10.1.1.1/24", "fd11::1/64");
create_netns("net_b", "10.1.1.2/24"); create_netns("net_b", "10.1.1.2/24", "fd11::2/64");
create_netns("net_c", "10.1.2.3/24"); create_netns("net_c", "10.1.2.3/24", "fd12::3/64");
create_netns("net_d", "10.1.2.4/24"); create_netns("net_d", "10.1.2.4/24", "fd12::4/64");
prepare_bridge("br_a"); prepare_bridge("br_a");
prepare_bridge("br_b"); prepare_bridge("br_b");
@@ -931,10 +931,18 @@ fn run_wireguard_client(
} }
#[cfg(feature = "wireguard")] #[cfg(feature = "wireguard")]
#[rstest::rstest]
#[tokio::test] #[tokio::test]
#[serial_test::serial] #[serial_test::serial]
pub async fn wireguard_vpn_portal() { pub async fn wireguard_vpn_portal(#[values(true, false)] test_v6: bool) {
let mut insts = init_three_node("tcp").await; let mut insts = init_three_node("tcp").await;
if test_v6 {
ping6_test("net_d", "fd12::3", None).await;
} else {
ping_test("net_d", "10.1.2.3", None).await;
}
let net_ns = NetNS::new(Some("net_d".into())); let net_ns = NetNS::new(Some("net_d".into()));
let _g = net_ns.guard(); let _g = net_ns.guard();
insts[2] insts[2]
@@ -946,11 +954,17 @@ pub async fn wireguard_vpn_portal() {
}); });
insts[2].run_vpn_portal().await.unwrap(); insts[2].run_vpn_portal().await.unwrap();
let dst_socket_addr = if test_v6 {
"[fd12::3]:22121".parse().unwrap()
} else {
"10.1.2.3:22121".parse().unwrap()
};
let net_ns = NetNS::new(Some("net_d".into())); let net_ns = NetNS::new(Some("net_d".into()));
let _g = net_ns.guard(); let _g = net_ns.guard();
let wg_cfg = get_wg_config_for_portal(&insts[2].get_global_ctx().get_network_identity()); let wg_cfg = get_wg_config_for_portal(&insts[2].get_global_ctx().get_network_identity());
run_wireguard_client( run_wireguard_client(
"10.1.2.3:22121".parse().unwrap(), dst_socket_addr,
Key::try_from(wg_cfg.my_public_key()).unwrap(), Key::try_from(wg_cfg.my_public_key()).unwrap(),
Key::try_from(wg_cfg.peer_secret_key()).unwrap(), Key::try_from(wg_cfg.peer_secret_key()).unwrap(),
vec!["10.14.14.0/24".to_string(), "10.144.144.0/24".to_string()], vec!["10.14.14.0/24".to_string(), "10.144.144.0/24".to_string()],
+5 -1
View File
@@ -384,7 +384,11 @@ pub(crate) fn setup_sokcet2_ext(
unsafe { unsafe {
let dev_idx = nix::libc::if_nametoindex(dev_name.as_str().as_ptr() as *const i8); let dev_idx = nix::libc::if_nametoindex(dev_name.as_str().as_ptr() as *const i8);
tracing::warn!(?dev_idx, ?dev_name, "bind device"); tracing::warn!(?dev_idx, ?dev_name, "bind device");
socket2_socket.bind_device_by_index_v4(std::num::NonZeroU32::new(dev_idx))?; if bind_addr.is_ipv4() {
socket2_socket.bind_device_by_index_v4(std::num::NonZeroU32::new(dev_idx))?;
} else {
socket2_socket.bind_device_by_index_v6(std::num::NonZeroU32::new(dev_idx))?;
}
tracing::warn!(?dev_idx, ?dev_name, "bind device doen"); tracing::warn!(?dev_idx, ?dev_name, "bind device doen");
} }
} }
+6 -1
View File
@@ -81,9 +81,14 @@ pub fn init_logger(
}); });
} }
let dir = file_config.dir.as_deref().unwrap_or(".");
let file = file_config.file.as_deref().unwrap_or("easytier.log");
let path = std::path::Path::new(dir).join(file);
let path_str = path.to_string_lossy().into_owned();
let builder = RollingFileAppenderBase::builder(); let builder = RollingFileAppenderBase::builder();
let file_appender = builder let file_appender = builder
.filename(file_config.file.unwrap_or("easytier.log".to_string())) .filename(path_str)
.condition_daily() .condition_daily()
.max_filecount(file_config.count.unwrap_or(10)) .max_filecount(file_config.count.unwrap_or(10))
.condition_max_file_size(file_config.size_mb.unwrap_or(100) * 1024 * 1024) .condition_max_file_size(file_config.size_mb.unwrap_or(100) * 1024 * 1024)
+2723 -3374
View File
File diff suppressed because it is too large Load Diff