gui use frontend-lib, fix memory leak (#467)
Some checks failed
EasyTier Core / pre_job (push) Has been cancelled
EasyTier GUI / pre_job (push) Has been cancelled
EasyTier Mobile / pre_job (push) Has been cancelled
EasyTier Test / pre_job (push) Has been cancelled
EasyTier Core / build (freebsd-13.2-x86_64, 13.2, ubuntu-22.04, x86_64-unknown-freebsd) (push) Has been cancelled
EasyTier Core / build (linux-aarch64, ubuntu-22.04, aarch64-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-arm, ubuntu-22.04, arm-unknown-linux-musleabi) (push) Has been cancelled
EasyTier Core / build (linux-armhf, ubuntu-22.04, arm-unknown-linux-musleabihf) (push) Has been cancelled
EasyTier Core / build (linux-armv7, ubuntu-22.04, armv7-unknown-linux-musleabi) (push) Has been cancelled
EasyTier Core / build (linux-armv7hf, ubuntu-22.04, armv7-unknown-linux-musleabihf) (push) Has been cancelled
EasyTier Core / build (linux-mips, ubuntu-22.04, mips-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-mipsel, ubuntu-22.04, mipsel-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-x86_64, ubuntu-22.04, x86_64-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (macos-aarch64, macos-latest, aarch64-apple-darwin) (push) Has been cancelled
EasyTier Core / build (macos-x86_64, macos-latest, x86_64-apple-darwin) (push) Has been cancelled
EasyTier Core / build (windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
EasyTier Core / core-result (push) Has been cancelled
EasyTier GUI / build-gui (linux-aarch64, aarch64-unknown-linux-gnu, ubuntu-22.04, aarch64-unknown-linux-musl) (push) Has been cancelled
EasyTier GUI / build-gui (linux-x86_64, x86_64-unknown-linux-gnu, ubuntu-22.04, x86_64-unknown-linux-musl) (push) Has been cancelled
EasyTier GUI / build-gui (macos-aarch64, aarch64-apple-darwin, macos-latest, aarch64-apple-darwin) (push) Has been cancelled
EasyTier GUI / build-gui (macos-x86_64, x86_64-apple-darwin, macos-latest, x86_64-apple-darwin) (push) Has been cancelled
EasyTier GUI / build-gui (windows-x86_64, x86_64-pc-windows-msvc, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
EasyTier GUI / gui-result (push) Has been cancelled
EasyTier Mobile / build-mobile (android, ubuntu-22.04, android) (push) Has been cancelled
EasyTier Mobile / mobile-result (push) Has been cancelled
EasyTier Test / test (push) Has been cancelled

This commit is contained in:
Sijie.Sun 2024-11-10 23:03:40 +08:00 committed by GitHub
parent 88e6de9d7e
commit 4fc3ff8ce8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 868 additions and 2181 deletions

765
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +0,0 @@
shamefully-hoist=true
strict-peer-dependencies=false

View File

@ -13,34 +13,32 @@
"lint:fix": "eslint . --ignore-pattern src-tauri --fix"
},
"dependencies": {
"@primevue/themes": "^4.1.0",
"@tauri-apps/plugin-autostart": "2.0.0-rc.1",
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.1",
"@tauri-apps/plugin-os": "2.0.0-rc.1",
"@tauri-apps/plugin-process": "2.0.0-rc.1",
"@tauri-apps/plugin-shell": "2.0.0-rc.1",
"@vueuse/core": "^11.1.0",
"@primevue/themes": "^4.2.1",
"@tauri-apps/plugin-autostart": "2.0.0",
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
"@tauri-apps/plugin-os": "2.0.0",
"@tauri-apps/plugin-process": "2.0.0",
"@tauri-apps/plugin-shell": "2.0.1",
"@vueuse/core": "^11.2.0",
"aura": "link:@primevue\\themes\\aura",
"easytier-frontend-lib": "workspace:*",
"ip-num": "1.5.1",
"pinia": "^2.2.4",
"primeflex": "^3.3.1",
"primeicons": "^7.0.0",
"primevue": "^4.1.0",
"tauri-plugin-vpnservice-api": "link:..\\tauri-plugin-vpnservice",
"vue": "=3.4.38",
"vue-i18n": "^10.0.4",
"primevue": "^4.2.1",
"tauri-plugin-vpnservice-api": "workspace:*",
"vue": "^3.5.12",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@antfu/eslint-config": "^3.7.3",
"@intlify/unplugin-vue-i18n": "^5.2.0",
"@primevue/auto-import-resolver": "^4.1.0",
"@tauri-apps/api": "2.0.0-rc.0",
"@tauri-apps/cli": "2.0.0-rc.3",
"@tauri-apps/api": "2.1.0",
"@tauri-apps/cli": "2.1.0",
"@types/node": "^22.7.4",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-vue": "^5.1.4",
"@vue-macros/volar": "0.30.3",
"@vue-macros/volar": "0.30.5",
"autoprefixer": "^10.4.20",
"eslint": "^9.12.0",
"eslint-plugin-format": "^0.1.2",
@ -50,7 +48,7 @@
"typescript": "^5.6.2",
"unplugin-auto-import": "^0.18.3",
"unplugin-vue-components": "^0.27.4",
"unplugin-vue-macros": "^2.12.3",
"unplugin-vue-macros": "^2.13.3",
"unplugin-vue-markdown": "^0.26.2",
"unplugin-vue-router": "^0.10.8",
"uuid": "^10.0.0",
@ -58,6 +56,6 @@
"vite-plugin-vue-devtools": "^7.4.6",
"vite-plugin-vue-layouts": "^0.11.0",
"vue-i18n": "^10.0.0",
"vue-tsc": "^2.1.6"
"vue-tsc": "^2.1.10"
}
}

View File

@ -15,10 +15,11 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2.0.0-rc", features = [] }
[dependencies]
tauri = { version = "2.0.0-rc", features = [
tauri = { version = "2.1", features = [
"tray-icon",
"image-png",
"image-ico",
"devtools",
] }
serde = { version = "1", features = ["derive"] }
@ -37,13 +38,13 @@ gethostname = "0.5"
dunce = "1.0.4"
tauri-plugin-shell = "2.0.0-rc"
tauri-plugin-process = "2.0.0-rc"
tauri-plugin-clipboard-manager = "2.0.0-rc"
tauri-plugin-positioner = { version = "2.0.0-rc", features = ["tray-icon"] }
tauri-plugin-shell = "2.0"
tauri-plugin-process = "2.0"
tauri-plugin-clipboard-manager = "2.0"
tauri-plugin-positioner = { version = "2.0", features = ["tray-icon"] }
tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" }
tauri-plugin-os = "2.0.0-rc"
tauri-plugin-autostart = "2.0.0-rc"
tauri-plugin-os = "2.0"
tauri-plugin-autostart = "2.0"
[features]

View File

@ -3,17 +3,12 @@
use std::collections::BTreeMap;
use anyhow::Context;
use dashmap::DashMap;
use easytier::{
common::config::{
ConfigLoader, FileLoggerConfig, Flags, NetworkIdentity, PeerConfig, TomlConfigLoader,
VpnPortalConfig,
},
common::config::{ConfigLoader, FileLoggerConfig, TomlConfigLoader},
launcher::{NetworkConfig, NetworkInstance, NetworkInstanceRunningInfo},
utils::{self, NewFilterSender},
};
use serde::{Deserialize, Serialize};
use tauri::Manager as _;

View File

@ -154,8 +154,6 @@ declare module 'vue' {
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly num2ipv4: UnwrapRef<typeof import('./composables/utils')['num2ipv4']>
readonly num2ipv6: UnwrapRef<typeof import('./composables/utils')['num2ipv6']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>

View File

@ -1,296 +0,0 @@
<script setup lang="ts">
import InputGroup from 'primevue/inputgroup'
import InputGroupAddon from 'primevue/inputgroupaddon'
import { getOsHostname } from '~/composables/network'
import { NetworkingMethod } from '~/types/network'
const props = defineProps<{
configInvalid?: boolean
instanceId?: string
}>()
defineEmits(['runNetwork'])
const { t } = useI18n()
const networking_methods = ref([
{ value: NetworkingMethod.PublicServer, label: () => t('public_server') },
{ value: NetworkingMethod.Manual, label: () => t('manual') },
{ value: NetworkingMethod.Standalone, label: () => t('standalone') },
])
const networkStore = useNetworkStore()
const curNetwork = computed(() => {
if (props.instanceId) {
// console.log('instanceId', props.instanceId)
const c = networkStore.networkList.find(n => n.instance_id === props.instanceId)
if (c !== undefined)
return c
}
return networkStore.curNetwork
})
const protos: { [proto: string]: number } = { tcp: 11010, udp: 11010, wg: 11011, ws: 11011, wss: 11012 }
function searchUrlSuggestions(e: { query: string }): string[] {
const query = e.query
const ret = []
// if query match "^\w+:.*", then no proto prefix
if (query.match(/^\w+:.*/)) {
// if query is a valid url, then add to suggestions
try {
// eslint-disable-next-line no-new
new URL(query)
ret.push(query)
}
catch { }
}
else {
for (const proto in protos) {
let item = `${proto}://${query}`
// if query match ":\d+$", then no port suffix
if (!query.match(/:\d+$/)) {
item += `:${protos[proto]}`
}
ret.push(item)
}
}
return ret
}
const publicServerSuggestions = ref([''])
function searchPresetPublicServers(e: { query: string }) {
const presetPublicServers = [
'tcp://public.easytier.top:11010',
]
const query = e.query
// if query is sub string of presetPublicServers, add to suggestions
let ret = presetPublicServers.filter(item => item.includes(query))
// add additional suggestions
if (query.length > 0) {
ret = ret.concat(searchUrlSuggestions(e))
}
publicServerSuggestions.value = ret
}
const peerSuggestions = ref([''])
function searchPeerSuggestions(e: { query: string }) {
peerSuggestions.value = searchUrlSuggestions(e)
}
const inetSuggestions = ref([''])
function searchInetSuggestions(e: { query: string }) {
if (e.query.search('/') >= 0) {
inetSuggestions.value = [e.query]
} else {
const ret = []
for (let i = 0; i < 32; i++) {
ret.push(`${e.query}/${i}`)
}
inetSuggestions.value = ret
}
}
const listenerSuggestions = ref([''])
function searchListenerSuggestiong(e: { query: string }) {
const ret = []
for (const proto in protos) {
let item = `${proto}://0.0.0.0:`
// if query is a number, use it as port
if (e.query.match(/^\d+$/)) {
item += e.query
}
else {
item += protos[proto]
}
if (item.includes(e.query)) {
ret.push(item)
}
}
if (ret.length === 0) {
ret.push(e.query)
}
listenerSuggestions.value = ret
}
function validateHostname() {
if (curNetwork.value.hostname) {
// eslint no-useless-escape
let name = curNetwork.value.hostname!.replaceAll(/[^\u4E00-\u9FA5a-z0-9\-]*/gi, '')
if (name.length > 32)
name = name.substring(0, 32)
if (curNetwork.value.hostname !== name)
curNetwork.value.hostname = name
}
}
const osHostname = ref<string>('')
onMounted(async () => {
osHostname.value = await getOsHostname()
})
</script>
<template>
<div class="flex flex-column h-full">
<div class="flex flex-column">
<div class="w-10/12 self-center ">
<Panel :header="t('basic_settings')">
<div class="flex flex-column gap-y-2">
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
<div class="flex align-items-center" for="virtual_ip">
<label class="mr-2"> {{ t('virtual_ipv4') }} </label>
<Checkbox v-model="curNetwork.dhcp" input-id="virtual_ip_auto" :binary="true" />
<label for="virtual_ip_auto" class="ml-2">
{{ t('virtual_ipv4_dhcp') }}
</label>
</div>
<InputGroup>
<InputText id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp"
aria-describedby="virtual_ipv4-help" />
<InputGroupAddon>
<span>/</span>
</InputGroupAddon>
<InputNumber v-model="curNetwork.network_length" :disabled="curNetwork.dhcp"
inputId="horizontal-buttons" showButtons :step="1" mode="decimal" :min="1" :max="32" fluid
class="max-w-20" />
</InputGroup>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
<label for="network_name">{{ t('network_name') }}</label>
<InputText id="network_name" v-model="curNetwork.network_name" aria-describedby="network_name-help" />
</div>
<div class="flex flex-column gap-2 basis-5/12 grow">
<label for="network_secret">{{ t('network_secret') }}</label>
<InputText id="network_secret" v-model="curNetwork.network_secret"
aria-describedby="network_secret-help" />
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
<label for="nm">{{ t('networking_method') }}</label>
<SelectButton v-model="curNetwork.networking_method" :options="networking_methods"
:option-label="(v: any) => v.label()" option-value="value" />
<div class="items-center flex flex-row p-fluid gap-x-1">
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.Manual" id="chips"
v-model="curNetwork.peer_urls" :placeholder="t('chips_placeholder', ['tcp://8.8.8.8:11010'])"
class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.PublicServer"
v-model="curNetwork.public_server_url" :suggestions="publicServerSuggestions"
:virtual-scroller-options="{ itemSize: 38 }" class="grow" dropdown :complete-on-focus="true"
@complete="searchPresetPublicServers" />
</div>
</div>
</div>
</div>
</Panel>
<Divider />
<Panel :header="t('advanced_settings')" toggleable collapsed>
<div class="flex flex-column gap-y-2">
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
<div class="flex align-items-center">
<Checkbox v-model="curNetwork.latency_first" input-id="use_latency_first" :binary="true" />
<label for="use_latency_first" class="ml-2"> {{ t('use_latency_first') }} </label>
</div>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
<label for="hostname">{{ t('hostname') }}</label>
<InputText id="hostname" v-model="curNetwork.hostname" aria-describedby="hostname-help" :format="true"
:placeholder="t('hostname_placeholder', [osHostname])" @blur="validateHostname" />
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap w-full">
<div class="flex flex-column gap-2 grow p-fluid">
<label for="username">{{ t('proxy_cidrs') }}</label>
<AutoComplete id="subnet-proxy" v-model="curNetwork.proxy_cidrs"
:placeholder="t('chips_placeholder', ['10.0.0.0/24'])" class="w-full" multiple fluid
:suggestions="inetSuggestions" @complete="searchInetSuggestions" />
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap ">
<div class="flex flex-column gap-2 grow">
<label for="username">VPN Portal</label>
<ToggleButton v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times"
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
<div v-if="curNetwork.enable_vpn_portal" class="items-center flex flex-row gap-x-4">
<div class="min-w-64">
<InputGroup>
<InputText v-model="curNetwork.vpn_portal_client_network_addr"
:placeholder="t('vpn_portal_client_network')" />
<InputGroupAddon>
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
</InputGroupAddon>
</InputGroup>
<InputNumber v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false" :format="false"
:min="0" :max="65535" class="w-8" fluid />
</div>
</div>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 grow p-fluid">
<label for="listener_urls">{{ t('listener_urls') }}</label>
<AutoComplete id="listener_urls" v-model="curNetwork.listener_urls" :suggestions="listenerSuggestions"
class="w-full" dropdown :complete-on-focus="true"
:placeholder="t('chips_placeholder', ['tcp://1.1.1.1:11010'])" multiple
@complete="searchListenerSuggestiong" />
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
<label for="rpc_port">{{ t('rpc_port') }}</label>
<InputNumber id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="rpc_port-help"
:format="false" :min="0" :max="65535" />
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
<label for="dev_name">{{ t('dev_name') }}</label>
<InputText id="dev_name" v-model="curNetwork.dev_name" aria-describedby="dev_name-help" :format="true"
:placeholder="t('dev_name_placeholder')" />
</div>
</div>
</div>
</Panel>
<div class="flex pt-4 justify-content-center">
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
@click="$emit('runNetwork', curNetwork)" />
</div>
</div>
</div>
</div>
</template>

View File

@ -1,32 +0,0 @@
<script setup lang="ts">
import { EventType } from '~/types/network'
const props = defineProps<{
event: {
[key: string]: any
}
}>()
const { t } = useI18n()
const eventKey = computed(() => {
const key = Object.keys(props.event)[0]
return Object.keys(EventType).includes(key) ? key : 'Unknown'
})
const eventValue = computed(() => {
const value = props.event[eventKey.value]
return typeof value === 'object' ? value : value
})
</script>
<template>
<Fieldset :legend="t(`event.${eventKey}`)">
<template v-if="eventKey !== 'Unknown'">
<div v-if="event.DhcpIpv4Changed">
{{ `${eventValue[0]} -> ${eventValue[1]}` }}
</div>
<pre v-else>{{ eventValue }}</pre>
</template>
<pre v-else>{{ eventValue }}</pre>
</Fieldset>
</template>

View File

@ -1,459 +0,0 @@
<script setup lang="ts">
import { useTimeAgo } from '@vueuse/core'
import { IPv4, IPv6 } from 'ip-num/IPNumber'
import type { NodeInfo, PeerRoutePair } from '~/types/network'
const props = defineProps<{
instanceId?: string
}>()
const { t } = useI18n()
const networkStore = useNetworkStore()
const curNetwork = computed(() => {
if (props.instanceId) {
// console.log('instanceId', props.instanceId)
const c = networkStore.networkList.find(n => n.instance_id === props.instanceId)
if (c !== undefined)
return c
}
return networkStore.curNetwork
})
const curNetworkInst = computed(() => {
return networkStore.networkInstances.find(n => n.instance_id === curNetwork.value.instance_id)
})
const peerRouteInfos = computed(() => {
if (curNetworkInst.value) {
const my_node_info = curNetworkInst.value.detail?.my_node_info
return [{
route: {
ipv4_addr: my_node_info?.virtual_ipv4,
hostname: my_node_info?.hostname,
version: my_node_info?.version,
},
}, ...(curNetworkInst.value.detail?.peer_route_pairs || [])]
}
return []
})
function routeCost(info: any) {
if (info.route) {
const cost = info.route.cost
return cost ? cost === 1 ? 'p2p' : `relay(${cost})` : t('status.local')
}
return '?'
}
function resolveObjPath(path: string, obj = globalThis, separator = '.') {
const properties = Array.isArray(path) ? path : path.split(separator)
return properties.reduce((prev, curr) => prev?.[curr], obj)
}
function statsCommon(info: any, field: string): number | undefined {
if (!info.peer)
return undefined
const conns = info.peer.conns
return conns.reduce((acc: number, conn: any) => {
return acc + resolveObjPath(field, conn)
}, 0)
}
function humanFileSize(bytes: number, si = false, dp = 1) {
const thresh = si ? 1000 : 1024
if (Math.abs(bytes) < thresh)
return `${bytes} B`
const units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
let u = -1
const r = 10 ** dp
do {
bytes /= thresh
++u
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1)
return `${bytes.toFixed(dp)} ${units[u]}`
}
function latencyMs(info: PeerRoutePair) {
let lat_us_sum = statsCommon(info, 'stats.latency_us')
if (lat_us_sum === undefined)
return ''
lat_us_sum = lat_us_sum / 1000 / info.peer!.conns.length
return `${lat_us_sum % 1 > 0 ? Math.round(lat_us_sum) + 1 : Math.round(lat_us_sum)}ms`
}
function txBytes(info: PeerRoutePair) {
const tx = statsCommon(info, 'stats.tx_bytes')
return tx ? humanFileSize(tx) : ''
}
function rxBytes(info: PeerRoutePair) {
const rx = statsCommon(info, 'stats.rx_bytes')
return rx ? humanFileSize(rx) : ''
}
function lossRate(info: PeerRoutePair) {
const lossRate = statsCommon(info, 'loss_rate')
return lossRate !== undefined ? `${Math.round(lossRate * 100)}%` : ''
}
function version(info: PeerRoutePair) {
return info.route.version === '' ? 'unknown' : info.route.version
}
function ipFormat(info: PeerRoutePair) {
const ip = info.route.ipv4_addr
if (typeof ip === 'string')
return ip
return ip ? `${num2ipv4(ip.address)}/${ip.network_length}` : ''
}
const myNodeInfo = computed(() => {
if (!curNetworkInst.value)
return {} as NodeInfo
return curNetworkInst.value.detail?.my_node_info
})
interface Chip {
label: string
icon: string
}
const myNodeInfoChips = computed(() => {
if (!curNetworkInst.value)
return []
const chips: Array<Chip> = []
const my_node_info = curNetworkInst.value.detail?.my_node_info
if (!my_node_info)
return chips
// TUN Device Name
const dev_name = curNetworkInst.value.detail?.dev_name
if (dev_name) {
chips.push({
label: `TUN Device Name: ${dev_name}`,
icon: '',
} as Chip)
}
// virtual ipv4
chips.push({
label: `Virtual IPv4: ${my_node_info.virtual_ipv4}`,
icon: '',
} as Chip)
// local ipv4s
const local_ipv4s = my_node_info.ips?.interface_ipv4s
for (const [idx, ip] of local_ipv4s?.entries()) {
chips.push({
label: `Local IPv4 ${idx}: ${num2ipv4(ip)}`,
icon: '',
} as Chip)
}
// local ipv6s
const local_ipv6s = my_node_info.ips?.interface_ipv6s
for (const [idx, ip] of local_ipv6s?.entries()) {
chips.push({
label: `Local IPv6 ${idx}: ${num2ipv6(ip)}`,
icon: '',
} as Chip)
}
// public ip
const public_ip = my_node_info.ips?.public_ipv4
if (public_ip) {
chips.push({
label: `Public IP: ${IPv4.fromNumber(public_ip.addr)}`,
icon: '',
} as Chip)
}
const public_ipv6 = my_node_info.ips?.public_ipv6
if (public_ipv6) {
chips.push({
label: `Public IPv6: ${IPv6.fromBigInt((BigInt(public_ipv6.part1) << BigInt(96))
+ (BigInt(public_ipv6.part2) << BigInt(64))
+ (BigInt(public_ipv6.part3) << BigInt(32))
+ BigInt(public_ipv6.part4),
)}`,
icon: '',
} as Chip)
}
// listeners:
const listeners = my_node_info.listeners
for (const [idx, listener] of listeners?.entries()) {
chips.push({
label: `Listener ${idx}: ${listener}`,
icon: '',
} as Chip)
}
// udp nat type
enum NatType {
// has NAT; but own a single public IP, port is not changed
Unknown = 0,
OpenInternet = 1,
NoPAT = 2,
FullCone = 3,
Restricted = 4,
PortRestricted = 5,
Symmetric = 6,
SymUdpFirewall = 7,
SymmetricEasyInc = 8,
SymmetricEasyDec = 9,
};
const udpNatType: NatType = my_node_info.stun_info?.udp_nat_type
if (udpNatType !== undefined) {
const udpNatTypeStrMap = {
[NatType.Unknown]: 'Unknown',
[NatType.OpenInternet]: 'Open Internet',
[NatType.NoPAT]: 'No PAT',
[NatType.FullCone]: 'Full Cone',
[NatType.Restricted]: 'Restricted',
[NatType.PortRestricted]: 'Port Restricted',
[NatType.Symmetric]: 'Symmetric',
[NatType.SymUdpFirewall]: 'Symmetric UDP Firewall',
[NatType.SymmetricEasyInc]: 'Symmetric Easy Inc',
[NatType.SymmetricEasyDec]: 'Symmetric Easy Dec',
}
chips.push({
label: `UDP NAT Type: ${udpNatTypeStrMap[udpNatType]}`,
icon: '',
} as Chip)
}
return chips
})
function globalSumCommon(field: string) {
let sum = 0
if (!peerRouteInfos.value)
return sum
for (const info of peerRouteInfos.value) {
const tx = statsCommon(info, field)
if (tx)
sum += tx
}
return sum
}
function txGlobalSum() {
return globalSumCommon('stats.tx_bytes')
}
function rxGlobalSum() {
return globalSumCommon('stats.rx_bytes')
}
const peerCount = computed(() => {
if (!peerRouteInfos.value)
return 0
return peerRouteInfos.value.length
})
// calculate tx/rx rate every 2 seconds
let rateIntervalId = 0
const rateInterval = 2000
let prevTxSum = 0
let prevRxSum = 0
const txRate = ref('0')
const rxRate = ref('0')
onMounted(() => {
rateIntervalId = window.setInterval(() => {
const curTxSum = txGlobalSum()
txRate.value = humanFileSize((curTxSum - prevTxSum) / (rateInterval / 1000))
prevTxSum = curTxSum
const curRxSum = rxGlobalSum()
rxRate.value = humanFileSize((curRxSum - prevRxSum) / (rateInterval / 1000))
prevRxSum = curRxSum
}, rateInterval)
})
onUnmounted(() => {
clearInterval(rateIntervalId)
})
const dialogVisible = ref(false)
const dialogContent = ref<any>('')
const dialogHeader = ref('event_log')
function showVpnPortalConfig() {
const my_node_info = myNodeInfo.value
if (!my_node_info)
return
const url = 'https://www.wireguardconfig.com/qrcode'
dialogContent.value = `${my_node_info.vpn_portal_cfg}\n\n # can generate QR code: ${url}`
dialogHeader.value = 'vpn_portal_config'
dialogVisible.value = true
}
function showEventLogs() {
const detail = curNetworkInst.value?.detail
if (!detail)
return
dialogContent.value = detail.events
dialogHeader.value = 'event_log'
dialogVisible.value = true
}
</script>
<template>
<div>
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" class="w-2/3 h-auto">
<ScrollPanel v-if="dialogHeader === 'vpn_portal_config'">
<pre>{{ dialogContent }}</pre>
</ScrollPanel>
<Timeline v-else :value="dialogContent">
<template #opposite="slotProps">
<small class="text-surface-500 dark:text-surface-400">{{ useTimeAgo(Date.parse(slotProps.item[0])) }}</small>
</template>
<template #content="slotProps">
<HumanEvent :event="slotProps.item[1]" />
</template>
</Timeline>
</Dialog>
<Card v-if="curNetworkInst?.error_msg">
<template #title>
Run Network Error
</template>
<template #content>
<div class="flex flex-column gap-y-5">
<div class="text-red-500">
{{ curNetworkInst.error_msg }}
</div>
</div>
</template>
</Card>
<template v-else>
<Card>
<template #title>
{{ t('my_node_info') }}
</template>
<template #content>
<div class="flex w-full flex-column gap-y-5">
<div class="m-0 flex flex-row justify-center gap-x-5">
<div
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
style="border: 1px solid green"
>
<div class="font-bold">
{{ t('peer_count') }}
</div>
<div class="text-5xl mt-1">
{{ peerCount }}
</div>
</div>
<div
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
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-column align-items-center pt-4"
style="border: 1px solid fuchsia"
>
<div class="font-bold">
{{ t('download') }}
</div>
<div class="text-xl mt-2">
{{ rxRate }}/s
</div>
</div>
</div>
<div class="flex flex-row align-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"
class="mr-2 mt-2 text-sm"
/>
</div>
<div v-if="myNodeInfo" class="m-0 flex flex-row justify-center gap-x-5 text-sm">
<Button severity="info" :label="t('show_vpn_portal_config')" @click="showVpnPortalConfig" />
<Button severity="info" :label="t('show_event_log')" @click="showEventLogs" />
</div>
</div>
</template>
</Card>
<Divider />
<Card>
<template #title>
{{ t('peer_info') }}
</template>
<template #content>
<DataTable :value="peerRouteInfos" column-resize-mode="fit" table-class="w-full">
<Column :field="ipFormat" :header="t('virtual_ipv4')" />
<Column :header="t('hostname')">
<template #body="slotProps">
<div
v-if="!slotProps.data.route.cost || !slotProps.data.route.feature_flag.is_public_server"
v-tooltip="slotProps.data.route.hostname"
>
{{
slotProps.data.route.hostname }}
</div>
<div v-else v-tooltip="slotProps.data.route.hostname" class="space-x-1">
<Tag v-if="slotProps.data.route.feature_flag.is_public_server" severity="info" value="Info">
{{ t('status.server') }}
</Tag>
<Tag v-if="slotProps.data.route.no_relay_data" severity="warn" value="Warn">
{{ t('status.relay') }}
</Tag>
</div>
</template>
</Column>
<Column :field="routeCost" :header="t('route_cost')" />
<Column :field="latencyMs" :header="t('latency')" />
<Column :field="txBytes" :header="t('upload_bytes')" />
<Column :field="rxBytes" :header="t('download_bytes')" />
<Column :field="lossRate" :header="t('loss_rate')" />
<Column :header="t('status.version')">
<template #body="slotProps">
<span>{{ version(slotProps.data) }}</span>
</template>
</Column>
</DataTable>
</template>
</Card>
</template>
</div>
</template>
<style lang="postcss" scoped>
.p-timeline :deep(.p-timeline-event-opposite) {
@apply flex-none;
}
</style>

View File

@ -1,6 +1,8 @@
import { addPluginListener } from '@tauri-apps/api/core'
import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
import type { Route } from '~/types/network'
import { NetworkTypes, Utils } from 'easytier-frontend-lib'
type Route = NetworkTypes.Route
const networkStore = useNetworkStore()
@ -122,12 +124,17 @@ async function onNetworkInstanceChange() {
return
}
const virtual_ip = curNetworkInfo?.node_info?.virtual_ipv4
const virtual_ip = Utils.ipv4ToString(curNetworkInfo?.my_node_info?.virtual_ipv4.address)
if (!virtual_ip || !virtual_ip.length) {
await doStopVpn()
return
}
let network_length = curNetworkInfo?.my_node_info?.virtual_ipv4.network_length
if (!network_length) {
network_length = 24
}
const routes = getRoutesForVpn(curNetworkInfo?.routes)
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr

View File

@ -1,6 +1,8 @@
import { invoke } from '@tauri-apps/api/core'
import { NetworkTypes } from 'easytier-frontend-lib'
import type { NetworkConfig, NetworkInstanceRunningInfo } from '~/types/network'
type NetworkConfig = NetworkTypes.NetworkConfig
type NetworkInstanceRunningInfo = NetworkTypes.NetworkInstanceRunningInfo
export async function parseNetworkConfig(cfg: NetworkConfig) {
return invoke<string>('parse_network_config', { cfg })

View File

@ -1,15 +0,0 @@
import { IPv4, IPv6 } from 'ip-num/IPNumber'
import type { Ipv4Addr, Ipv6Addr } from '~/types/network'
export function num2ipv4(ip: Ipv4Addr) {
return IPv4.fromNumber(ip.addr)
}
export function num2ipv6(ip: Ipv6Addr) {
return IPv6.fromBigInt(
(BigInt(ip.part1) << BigInt(96))
+ (BigInt(ip.part2) << BigInt(64))
+ (BigInt(ip.part3) << BigInt(32))
+ BigInt(ip.part4),
)
}

View File

@ -5,12 +5,11 @@ import ToastService from 'primevue/toastservice'
import { createRouter, createWebHistory } from 'vue-router/auto'
import { routes } from 'vue-router/auto-routes'
import App from '~/App.vue'
import { i18n, loadLanguageAsync } from '~/modules/i18n'
import EasyTierFrontendLib, { I18nUtils } from 'easytier-frontend-lib'
import { getAutoLaunchStatusAsync, loadAutoLaunchStatusAsync } from './modules/auto_launch'
import '~/styles.css'
import 'primeicons/primeicons.css'
import 'primeflex/primeflex.css'
import 'easytier-frontend-lib/style.css'
if (import.meta.env.PROD) {
document.addEventListener('keydown', (event) => {
@ -29,7 +28,7 @@ if (import.meta.env.PROD) {
}
async function main() {
await loadLanguageAsync(localStorage.getItem('lang') || 'en')
await I18nUtils.loadLanguageAsync(localStorage.getItem('lang') || 'en')
await loadAutoLaunchStatusAsync(getAutoLaunchStatusAsync())
const app = createApp(App)
@ -41,14 +40,18 @@ async function main() {
app.use(router)
app.use(createPinia())
app.use(i18n, { useScope: 'global' })
app.use(EasyTierFrontendLib)
// app.use(i18n, { useScope: 'global' })
app.use(PrimeVue, {
theme: {
preset: Aura,
options: {
prefix: 'p',
darkModeSelector: 'system',
cssLayer: false,
cssLayer: {
name: 'primevue',
order: 'tailwind-base, primevue, tailwind-utilities'
}
},
},
})

View File

@ -1,50 +0,0 @@
import { createI18n } from 'vue-i18n'
import type { Locale } from 'vue-i18n'
// Import i18n resources
// https://vitejs.dev/guide/features.html#glob-import
export const i18n = createI18n({
legacy: false,
locale: '',
fallbackLocale: '',
messages: {},
})
const localesMap = Object.fromEntries(
Object.entries(import.meta.glob('../../locales/*.yml'))
.map(([path, loadLocale]) => [path.match(/([\w-]*)\.yml$/)?.[1], loadLocale]),
) as Record<Locale, () => Promise<{ default: Record<string, string> }>>
export const availableLocales = Object.keys(localesMap)
const loadedLanguages: string[] = []
function setI18nLanguage(lang: Locale) {
i18n.global.locale.value = lang as any
localStorage.setItem('lang', lang)
return lang
}
export async function loadLanguageAsync(lang: string): Promise<Locale> {
// If the same language
if (i18n.global.locale.value === lang)
return setI18nLanguage(lang)
// If the language was already loaded
if (loadedLanguages.includes(lang))
return setI18nLanguage(lang)
// If the language hasn't been loaded yet
let messages
try {
messages = await localesMap[lang]()
}
catch {
messages = await localesMap.en()
}
i18n.global.setLocaleMessage(lang, messages.default)
loadedLanguages.push(lang)
return setI18nLanguage(lang)
}

View File

@ -8,14 +8,11 @@ import { exit } from '@tauri-apps/plugin-process'
import { open } from '@tauri-apps/plugin-shell'
import TieredMenu from 'primevue/tieredmenu'
import { useToast } from 'primevue/usetoast'
import Config from '~/components/Config.vue'
import { NetworkTypes, Config, Status, Utils, I18nUtils } from 'easytier-frontend-lib'
import Status from '~/components/Status.vue'
import { isAutostart, setLoggingLevel } from '~/composables/network'
import { useTray } from '~/composables/tray'
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
import { loadLanguageAsync } from '~/modules/i18n'
import { type NetworkConfig, NetworkingMethod } from '~/types/network'
const { t, locale } = useI18n()
const visible = ref(false)
@ -65,6 +62,27 @@ const toast = useToast()
const networkStore = useNetworkStore()
const curNetworkConfig = computed(() => {
if (networkStore.curNetworkId) {
// console.log('instanceId', props.instanceId)
const c = networkStore.networkList.find(n => n.instance_id === networkStore.curNetworkId)
if (c !== undefined)
return c
}
return networkStore.curNetwork
})
const curNetworkInst = computed<NetworkTypes.NetworkInstance | null>(() => {
let ret = networkStore.networkInstances.find(n => n.instance_id === curNetworkConfig.value.instance_id)
console.log('curNetworkInst', ret)
if (ret === undefined) {
return null;
} else {
return ret;
}
})
function addNewNetwork() {
networkStore.addNewNetwork()
networkStore.curNetwork = networkStore.lastNetwork
@ -82,7 +100,7 @@ networkStore.$subscribe(async () => {
}
})
async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
async function runNetworkCb(cfg: NetworkTypes.NetworkConfig, cb: () => void) {
if (type() === 'android') {
await prepareVpnService()
networkStore.clearNetworkInstances()
@ -106,7 +124,7 @@ async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
cb()
}
async function stopNetworkCb(cfg: NetworkConfig, cb: () => void) {
async function stopNetworkCb(cfg: NetworkTypes.NetworkConfig, cb: () => void) {
// console.log('stopNetworkCb', cfg, cb)
cb()
networkStore.removeNetworkInstance(cfg.instance_id)
@ -145,7 +163,7 @@ const setting_menu_items = ref([
label: () => t('exchange_language'),
icon: 'pi pi-language',
command: async () => {
await loadLanguageAsync((locale.value === 'en' ? 'cn' : 'en'))
await I18nUtils.loadLanguageAsync((locale.value === 'en' ? 'cn' : 'en'))
await setTrayMenu([
await MenuItemExit(t('tray.exit')),
await MenuItemShow(t('tray.show')),
@ -221,7 +239,7 @@ onBeforeMount(async () => {
getCurrentWindow().hide()
const autoStartIds = networkStore.autoStartInstIds
for (const id of autoStartIds) {
const cfg = networkStore.networkList.find(item => item.instance_id === id)
const cfg = networkStore.networkList.find((item: NetworkTypes.NetworkConfig) => item.instance_id === id)
if (cfg) {
networkStore.addNetworkInstance(cfg.instance_id)
await runNetworkInstance(cfg)
@ -245,7 +263,7 @@ function isRunning(id: string) {
</script>
<template>
<div id="root" class="flex flex-column">
<div id="root" class="flex flex-col">
<Dialog v-model:visible="visible" modal header="Config File" :style="{ width: '70%' }">
<Panel>
<ScrollPanel style="width: 100%; height: 300px">
@ -253,7 +271,7 @@ function isRunning(id: string) {
</ScrollPanel>
</Panel>
<Divider />
<div class="flex gap-2 justify-content-end">
<div class="flex gap-2 justify-end">
<Button type="button" :label="t('close')" @click="visible = false" />
</div>
</Dialog>
@ -265,65 +283,55 @@ function isRunning(id: string) {
<div>
<Toolbar>
<template #start>
<div class="flex align-items-center">
<div class="flex items-center">
<Button icon="pi pi-plus" severity="primary" :label="t('add_new_network')" @click="addNewNetwork" />
</div>
</template>
<template #center>
<div class="min-w-40">
<Dropdown
v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
:placeholder="t('select_network')" class="w-full"
>
<Select v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
:placeholder="t('select_network')" class="w-full">
<template #value="slotProps">
<div class="flex items-start content-center">
<div class="mr-3 flex-column">
<div class="mr-4 flex-col">
<span>{{ slotProps.value.network_name }}</span>
</div>
<Tag
class="my-auto leading-3" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
:value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')"
/>
<Tag class="my-auto leading-3" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
: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">
<div class="mr-3">
<div class="mr-4">
{{ t('network_name') }}: {{ slotProps.option.network_name }}
</div>
<Tag
class="my-auto leading-3"
<Tag class="my-auto leading-3"
:severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'"
:value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')"
/>
:value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')" />
</div>
<div
v-if="slotProps.option.networking_method !== NetworkingMethod.Standalone"
class="max-w-full overflow-hidden text-ellipsis"
>
{{ slotProps.option.networking_method === NetworkingMethod.Manual
<div v-if="slotProps.option.networking_method !== NetworkTypes.NetworkingMethod.Standalone"
class="max-w-full overflow-hidden text-ellipsis">
{{ slotProps.option.networking_method === NetworkTypes.NetworkingMethod.Manual
? slotProps.option.peer_urls.join(', ')
: slotProps.option.public_server_url }}
</div>
<div
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 !== '')"
>
{{ networkStore.instances[slotProps.option.instance_id].detail
? networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4 : '' }}
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)">
{{
Utils.ipv4InetToString(networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)
}}
</div>
</div>
</template>
</Dropdown>
</Select>
</div>
</template>
<template #end>
<Button
icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')"
aria-controls="overlay_setting_menu" @click="toggle_setting_menu"
/>
<Button icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')"
aria-controls="overlay_setting_menu" @click="toggle_setting_menu" />
<TieredMenu id="overlay_setting_menu" ref="setting_menu" :model="setting_menu_items" :popup="true" />
</template>
</Toolbar>
@ -341,20 +349,16 @@ function isRunning(id: string) {
</StepList>
<StepPanels value="1">
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="1">
<Config
:instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None"
@run-network="runNetworkCb($event, () => activateCallback('2'))"
/>
<Config :instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None"
:cur-network="curNetworkConfig" @run-network="runNetworkCb($event, () => activateCallback('2'))" />
</StepPanel>
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="2">
<div class="flex flex-column">
<Status :instance-id="networkStore.curNetworkId" />
<div class="flex flex-col">
<Status :cur-network-inst="curNetworkInst" />
</div>
<div class="flex pt-4 justify-content-center">
<Button
:label="t('stop_network')" severity="danger" icon="pi pi-arrow-left"
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))"
/>
<div class="flex pt-6 justify-center">
<Button :label="t('stop_network')" severity="danger" icon="pi pi-arrow-left"
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))" />
</div>
</StepPanel>
</StepPanels>

View File

@ -1,26 +1,25 @@
import type { NetworkConfig, NetworkInstance, NetworkInstanceRunningInfo } from '~/types/network'
import { DEFAULT_NETWORK_CONFIG } from '~/types/network'
import { NetworkTypes } from 'easytier-frontend-lib'
export const useNetworkStore = defineStore('networkStore', {
state: () => {
const networkList = [DEFAULT_NETWORK_CONFIG()]
const networkList = [NetworkTypes.DEFAULT_NETWORK_CONFIG()]
return {
// for initially empty lists
networkList: networkList as NetworkConfig[],
networkList: networkList as NetworkTypes.NetworkConfig[],
// for data that is not yet loaded
curNetwork: networkList[0],
// uuid -> instance
instances: {} as Record<string, NetworkInstance>,
instances: {} as Record<string, NetworkTypes.NetworkInstance>,
networkInfos: {} as Record<string, NetworkInstanceRunningInfo>,
networkInfos: {} as Record<string, NetworkTypes.NetworkInstanceRunningInfo>,
autoStartInstIds: [] as string[],
}
},
getters: {
lastNetwork(): NetworkConfig {
lastNetwork(): NetworkTypes.NetworkConfig {
return this.networkList[this.networkList.length - 1]
},
@ -28,7 +27,7 @@ export const useNetworkStore = defineStore('networkStore', {
return this.curNetwork.instance_id
},
networkInstances(): Array<NetworkInstance> {
networkInstances(): Array<NetworkTypes.NetworkInstance> {
return Object.values(this.instances)
},
@ -39,7 +38,7 @@ export const useNetworkStore = defineStore('networkStore', {
actions: {
addNewNetwork() {
this.networkList.push(DEFAULT_NETWORK_CONFIG())
this.networkList.push(NetworkTypes.DEFAULT_NETWORK_CONFIG())
},
delCurNetwork() {
@ -66,7 +65,7 @@ export const useNetworkStore = defineStore('networkStore', {
this.instances = {}
},
updateWithNetworkInfos(networkInfos: Record<string, NetworkInstanceRunningInfo>) {
updateWithNetworkInfos(networkInfos: Record<string, NetworkTypes.NetworkInstanceRunningInfo>) {
this.networkInfos = networkInfos
for (const [instanceId, info] of Object.entries(networkInfos)) {
if (this.instances[instanceId] === undefined)
@ -79,17 +78,17 @@ export const useNetworkStore = defineStore('networkStore', {
},
loadFromLocalStorage() {
let networkList: NetworkConfig[]
let networkList: NetworkTypes.NetworkConfig[]
// if localStorage default is [{}], instanceId will be undefined
networkList = JSON.parse(localStorage.getItem('networkList') || '[]')
networkList = networkList.map((cfg) => {
return { ...DEFAULT_NETWORK_CONFIG(), ...cfg } as NetworkConfig
return { ...NetworkTypes.DEFAULT_NETWORK_CONFIG(), ...cfg } as NetworkTypes.NetworkConfig
})
// prevent a empty list from localStorage, should not happen
if (networkList.length === 0)
networkList = [DEFAULT_NETWORK_CONFIG()]
networkList = [NetworkTypes.DEFAULT_NETWORK_CONFIG()]
this.networkList = networkList
this.curNetwork = this.networkList[0]

View File

@ -1,213 +0,0 @@
import { v4 as uuidv4 } from 'uuid'
export enum NetworkingMethod {
PublicServer = 'PublicServer',
Manual = 'Manual',
Standalone = 'Standalone',
}
export interface NetworkConfig {
instance_id: string
dhcp: boolean
virtual_ipv4: string
network_length: number
hostname?: string
network_name: string
network_secret: string
networking_method: NetworkingMethod
public_server_url: string
peer_urls: string[]
proxy_cidrs: string[]
enable_vpn_portal: boolean
vpn_portal_listen_port: number
vpn_portal_client_network_addr: string
vpn_portal_client_network_len: number
advanced_settings: boolean
listener_urls: string[]
rpc_port: number
latency_first: boolean
dev_name: string
}
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
return {
instance_id: uuidv4(),
dhcp: true,
virtual_ipv4: '',
network_length: 24,
network_name: 'easytier',
network_secret: '',
networking_method: NetworkingMethod.PublicServer,
public_server_url: 'tcp://public.easytier.top:11010',
peer_urls: [],
proxy_cidrs: [],
enable_vpn_portal: false,
vpn_portal_listen_port: 22022,
vpn_portal_client_network_addr: '',
vpn_portal_client_network_len: 24,
advanced_settings: false,
listener_urls: [
'tcp://0.0.0.0:11010',
'udp://0.0.0.0:11010',
'wg://0.0.0.0:11011',
],
rpc_port: 0,
latency_first: true,
dev_name: '',
}
}
export interface NetworkInstance {
instance_id: string
running: boolean
error_msg: string
detail?: NetworkInstanceRunningInfo
}
export interface NetworkInstanceRunningInfo {
dev_name: string
my_node_info: NodeInfo
events: Record<string, any>
node_info: NodeInfo
routes: Route[]
peers: PeerInfo[]
peer_route_pairs: PeerRoutePair[]
running: boolean
error_msg?: string
}
export interface Ipv4Addr {
addr: number
}
export interface Ipv6Addr {
part1: number
part2: number
part3: number
part4: number
}
export interface NodeInfo {
virtual_ipv4: string
hostname: string
version: string
ips: {
public_ipv4: Ipv4Addr
interface_ipv4s: Ipv4Addr[]
public_ipv6: Ipv6Addr
interface_ipv6s: Ipv6Addr[]
listeners: {
serialization: string
scheme_end: number
username_end: number
host_start: number
host_end: number
host: any
port?: number
path_start: number
query_start?: number
fragment_start?: number
}[]
}
stun_info: StunInfo
listeners: string[]
vpn_portal_cfg?: string
}
export interface StunInfo {
udp_nat_type: number
tcp_nat_type: number
last_update_time: number
}
export interface Route {
peer_id: number
ipv4_addr: {
address: Ipv4Addr
network_length: number
} | string | null
next_hop_peer_id: number
cost: number
proxy_cidrs: string[]
hostname: string
stun_info?: StunInfo
inst_id: string
version: string
}
export interface PeerInfo {
peer_id: number
conns: PeerConnInfo[]
}
export interface PeerConnInfo {
conn_id: string
my_peer_id: number
is_client: boolean
peer_id: number
features: string[]
tunnel?: TunnelInfo
stats?: PeerConnStats
loss_rate: number
}
export interface PeerRoutePair {
route: Route
peer?: PeerInfo
}
export interface TunnelInfo {
tunnel_type: string
local_addr: string
remote_addr: string
}
export interface PeerConnStats {
rx_bytes: number
tx_bytes: number
rx_packets: number
tx_packets: number
latency_us: number
}
export enum EventType {
TunDeviceReady = 'TunDeviceReady', // string
TunDeviceError = 'TunDeviceError', // string
PeerAdded = 'PeerAdded', // number
PeerRemoved = 'PeerRemoved', // number
PeerConnAdded = 'PeerConnAdded', // PeerConnInfo
PeerConnRemoved = 'PeerConnRemoved', // PeerConnInfo
ListenerAdded = 'ListenerAdded', // any
ListenerAddFailed = 'ListenerAddFailed', // any, string
ListenerAcceptFailed = 'ListenerAcceptFailed', // any, string
ConnectionAccepted = 'ConnectionAccepted', // string, string
ConnectionError = 'ConnectionError', // string, string, string
Connecting = 'Connecting', // any
ConnectError = 'ConnectError', // string, string, string
VpnPortalClientConnected = 'VpnPortalClientConnected', // string, string
VpnPortalClientDisconnected = 'VpnPortalClientDisconnected', // string, string, string
DhcpIpv4Changed = 'DhcpIpv4Changed', // ipv4 | null, ipv4 | null
DhcpIpv4Conflicted = 'DhcpIpv4Conflicted', // ipv4 | null
}

View File

@ -22,6 +22,7 @@
"@vueuse/core": "^11.1.0",
"aura": "link:@primevue\\themes\\aura",
"axios": "^1.7.7",
"floating-vue": "^5.2",
"ip-num": "1.5.1",
"primeicons": "^7.0.0",
"primevue": "^4.2.1",
@ -42,6 +43,6 @@
"typescript": "~5.6.3",
"vite": "^5.4.10",
"vite-plugin-dts": "^4.3.0",
"vue-tsc": "^2.1.8"
"vue-tsc": "^2.1.10"
}
}

View File

@ -8,7 +8,6 @@ import { useI18n } from 'vue-i18n'
const props = defineProps<{
configInvalid?: boolean
instanceId?: string
hostname?: string
}>()

View File

@ -4,7 +4,7 @@ import { IPv4 } from 'ip-num/IPNumber'
import { NetworkInstance, type NodeInfo, type PeerRoutePair } from '../types/network'
import { useI18n } from 'vue-i18n';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { num2ipv4, num2ipv6 } from '../modules/utils';
import { ipv4InetToString, ipv4ToString, ipv6ToString } from '../modules/utils';
import { DataTable, Column, Tag, Chip, Button, Dialog, ScrollPanel, Timeline, Divider, Card, } from 'primevue';
const props = defineProps<{
@ -138,7 +138,7 @@ const myNodeInfoChips = computed(() => {
// virtual ipv4
chips.push({
label: `Virtual IPv4: ${my_node_info.virtual_ipv4}`,
label: `Virtual IPv4: ${ipv4InetToString(my_node_info.virtual_ipv4)}`,
icon: '',
} as Chip)
@ -146,7 +146,7 @@ const myNodeInfoChips = computed(() => {
const local_ipv4s = my_node_info.ips?.interface_ipv4s
for (const [idx, ip] of local_ipv4s?.entries()) {
chips.push({
label: `Local IPv4 ${idx}: ${num2ipv4(ip)}`,
label: `Local IPv4 ${idx}: ${ipv4ToString(ip)}`,
icon: '',
} as Chip)
}
@ -155,7 +155,7 @@ const myNodeInfoChips = computed(() => {
const local_ipv6s = my_node_info.ips?.interface_ipv6s
for (const [idx, ip] of local_ipv6s?.entries()) {
chips.push({
label: `Local IPv6 ${idx}: ${num2ipv6(ip)}`,
label: `Local IPv6 ${idx}: ${ipv6ToString(ip)}`,
icon: '',
} as Chip)
}
@ -172,7 +172,7 @@ const myNodeInfoChips = computed(() => {
const public_ipv6 = my_node_info.ips?.public_ipv6
if (public_ipv6) {
chips.push({
label: `Public IPv6: ${num2ipv6(public_ipv6)}`,
label: `Public IPv6: ${ipv6ToString(public_ipv6)}`,
icon: '',
} as Chip)
}

View File

@ -8,12 +8,17 @@ import PrimeVue from 'primevue/config'
import I18nUtils from './modules/i18n'
import * as NetworkTypes from './types/network'
import HumanEvent from './components/HumanEvent.vue';
import Tooltip from 'primevue/tooltip';
// do not use primevue tooltip, it has serious memory leak issue
// https://github.com/primefaces/primevue/issues/5856
// import Tooltip from 'primevue/tooltip';
import { vTooltip } from 'floating-vue';
import * as Api from './modules/api';
import * as Utils from './modules/utils';
export default {
install: (app: App) => {
install: (app: App): void => {
app.use(I18nUtils.i18n, { useScope: 'global' })
app.use(PrimeVue, {
theme: {
@ -32,7 +37,7 @@ export default {
app.component('Config', Config);
app.component('Status', Status);
app.component('HumanEvent', HumanEvent);
app.directive('tooltip', Tooltip);
app.directive('tooltip', vTooltip as any);
}
};

View File

@ -1,11 +1,18 @@
import { IPv4, IPv6 } from 'ip-num/IPNumber'
import { Ipv4Addr, Ipv6Addr } from '../types/network'
import { Ipv4Addr, Ipv4Inet, Ipv6Addr } from '../types/network'
export function num2ipv4(ip: Ipv4Addr) {
return IPv4.fromNumber(ip.addr)
export function ipv4ToString(ip: Ipv4Addr) {
return IPv4.fromNumber(ip.addr).toString()
}
export function num2ipv6(ip: Ipv6Addr) {
export function ipv4InetToString(ip: Ipv4Inet | undefined) {
if (ip?.address === undefined) {
return 'undefined'
}
return `${ipv4ToString(ip.address)}/${ip.network_length}`
}
export function ipv6ToString(ip: Ipv6Addr) {
return IPv6.fromBigInt(
(BigInt(ip.part1) << BigInt(96))
+ (BigInt(ip.part2) << BigInt(64))

View File

@ -1,4 +1,5 @@
@import 'primeicons/primeicons.css';
@import 'floating-vue/dist/style.css';
.frontend-lib {

View File

@ -85,7 +85,6 @@ export interface NetworkInstanceRunningInfo {
dev_name: string
my_node_info: NodeInfo
events: Array<string>,
node_info: NodeInfo
routes: Route[]
peers: PeerInfo[]
peer_route_pairs: PeerRoutePair[]
@ -97,6 +96,11 @@ export interface Ipv4Addr {
addr: number
}
export interface Ipv4Inet {
address: Ipv4Addr
network_length: number
}
export interface Ipv6Addr {
part1: number
part2: number
@ -109,7 +113,7 @@ export interface Url {
}
export interface NodeInfo {
virtual_ipv4: string
virtual_ipv4: Ipv4Inet,
hostname: string
version: string
ips: {
@ -143,10 +147,7 @@ export interface StunInfo {
export interface Route {
peer_id: number
ipv4_addr: {
address: Ipv4Addr
network_length: number
} | string | null
ipv4_addr: Ipv4Inet | string | null
next_hop_peer_id: number
cost: number
proxy_cidrs: string[]

View File

@ -27,6 +27,6 @@
"typescript": "~5.6.2",
"vite": "^5.4.10",
"vite-plugin-singlefile": "^2.0.3",
"vue-tsc": "^2.1.8"
"vue-tsc": "^2.1.10"
}
}

View File

@ -31,7 +31,7 @@ pub struct Event {
struct EasyTierData {
events: RwLock<VecDeque<Event>>,
node_info: RwLock<MyNodeInfo>,
my_node_info: RwLock<MyNodeInfo>,
routes: RwLock<Vec<Route>>,
peers: RwLock<Vec<PeerInfo>>,
tun_fd: Arc<RwLock<Option<i32>>>,
@ -46,7 +46,7 @@ impl Default for EasyTierData {
Self {
event_subscriber: RwLock::new(tx),
events: RwLock::new(VecDeque::new()),
node_info: RwLock::new(MyNodeInfo::default()),
my_node_info: RwLock::new(MyNodeInfo::default()),
routes: RwLock::new(Vec::new()),
peers: RwLock::new(Vec::new()),
tun_fd: Arc::new(RwLock::new(None)),
@ -162,7 +162,7 @@ impl EasyTierLauncher {
global_ctx_c.get_flags().dev_name.clone();
let node_info = MyNodeInfo {
virtual_ipv4: global_ctx_c.get_ipv4().map(|x| x.address().into()),
virtual_ipv4: global_ctx_c.get_ipv4().map(|ip| ip.into()),
hostname: global_ctx_c.get_hostname(),
version: EASYTIER_VERSION.to_string(),
ips: Some(global_ctx_c.get_ip_collector().collect_ip_addrs().await),
@ -180,7 +180,7 @@ impl EasyTierLauncher {
.await,
),
};
*data_c.node_info.write().unwrap() = node_info.clone();
*data_c.my_node_info.write().unwrap() = node_info.clone();
*data_c.routes.write().unwrap() = peer_mgr_c.list_routes().await;
*data_c.peers.write().unwrap() = PeerManagerRpcService::new(peer_mgr_c.clone())
.list_peers()
@ -282,7 +282,7 @@ impl EasyTierLauncher {
}
pub fn get_node_info(&self) -> MyNodeInfo {
self.data.node_info.read().unwrap().clone()
self.data.my_node_info.read().unwrap().clone()
}
pub fn get_routes(&self) -> Vec<Route> {
@ -352,7 +352,6 @@ impl NetworkInstance {
.iter()
.map(|e| serde_json::to_string(e).unwrap())
.collect(),
node_info: Some(launcher.get_node_info()),
routes,
peers,
peer_route_pairs,

View File

@ -43,7 +43,7 @@ message NetworkConfig {
}
message MyNodeInfo {
common.Ipv4Addr virtual_ipv4 = 1;
common.Ipv4Inet virtual_ipv4 = 1;
string hostname = 2;
string version = 3;
peer_rpc.GetIpListResponse ips = 4;
@ -56,12 +56,11 @@ message NetworkInstanceRunningInfo {
string dev_name = 1;
MyNodeInfo my_node_info = 2;
repeated string events = 3;
MyNodeInfo node_info = 4;
repeated cli.Route routes = 5;
repeated cli.PeerInfo peers = 6;
repeated cli.PeerRoutePair peer_route_pairs = 7;
bool running = 8;
optional string error_msg = 9;
repeated cli.Route routes = 4;
repeated cli.PeerInfo peers = 5;
repeated cli.PeerRoutePair peer_route_pairs = 6;
bool running = 7;
optional string error_msg = 8;
}
message NetworkInstanceRunningInfoMap {

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@ Default permissions for the plugin
- `allow-ping`
- `allow-start-vpn`
### Permission Table
## Permission Table
<table>
<tr>

View File

@ -295,81 +295,59 @@
"type": "string",
"oneOf": [
{
"description": "allow-ping -> Enables the ping command without any pre-configured scope.",
"description": "Enables the ping command without any pre-configured scope.",
"type": "string",
"enum": [
"allow-ping"
]
"const": "allow-ping"
},
{
"description": "deny-ping -> Denies the ping command without any pre-configured scope.",
"description": "Denies the ping command without any pre-configured scope.",
"type": "string",
"enum": [
"deny-ping"
]
"const": "deny-ping"
},
{
"description": "allow-prepare-vpn -> Enables the prepare_vpn command without any pre-configured scope.",
"description": "Enables the prepare_vpn command without any pre-configured scope.",
"type": "string",
"enum": [
"allow-prepare-vpn"
]
"const": "allow-prepare-vpn"
},
{
"description": "deny-prepare-vpn -> Denies the prepare_vpn command without any pre-configured scope.",
"description": "Denies the prepare_vpn command without any pre-configured scope.",
"type": "string",
"enum": [
"deny-prepare-vpn"
]
"const": "deny-prepare-vpn"
},
{
"description": "allow-register-listener -> Enables the register_listener command without any pre-configured scope.",
"description": "Enables the register_listener command without any pre-configured scope.",
"type": "string",
"enum": [
"allow-register-listener"
]
"const": "allow-register-listener"
},
{
"description": "deny-register-listener -> Denies the register_listener command without any pre-configured scope.",
"description": "Denies the register_listener command without any pre-configured scope.",
"type": "string",
"enum": [
"deny-register-listener"
]
"const": "deny-register-listener"
},
{
"description": "allow-start-vpn -> Enables the start_vpn command without any pre-configured scope.",
"description": "Enables the start_vpn command without any pre-configured scope.",
"type": "string",
"enum": [
"allow-start-vpn"
]
"const": "allow-start-vpn"
},
{
"description": "deny-start-vpn -> Denies the start_vpn command without any pre-configured scope.",
"description": "Denies the start_vpn command without any pre-configured scope.",
"type": "string",
"enum": [
"deny-start-vpn"
]
"const": "deny-start-vpn"
},
{
"description": "allow-stop-vpn -> Enables the stop_vpn command without any pre-configured scope.",
"description": "Enables the stop_vpn command without any pre-configured scope.",
"type": "string",
"enum": [
"allow-stop-vpn"
]
"const": "allow-stop-vpn"
},
{
"description": "deny-stop-vpn -> Denies the stop_vpn command without any pre-configured scope.",
"description": "Denies the stop_vpn command without any pre-configured scope.",
"type": "string",
"enum": [
"deny-stop-vpn"
]
"const": "deny-stop-vpn"
},
{
"description": "default -> Default permissions for the plugin",
"description": "Default permissions for the plugin",
"type": "string",
"enum": [
"default"
]
"const": "default"
}
]
}