mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-15 02:15:36 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf0b2bcce8 | |||
| 3832020d50 | |||
| ee9b51ff8a |
@@ -11,7 +11,7 @@ on:
|
||||
image_tag:
|
||||
description: 'Tag for this image build'
|
||||
type: string
|
||||
default: 'v2.6.3'
|
||||
default: 'v2.6.2'
|
||||
required: true
|
||||
mark_latest:
|
||||
description: 'Mark this image as latest'
|
||||
|
||||
@@ -18,7 +18,7 @@ on:
|
||||
version:
|
||||
description: 'Version for this release'
|
||||
type: string
|
||||
default: 'v2.6.3'
|
||||
default: 'v2.6.2'
|
||||
required: true
|
||||
make_latest:
|
||||
description: 'Mark this release as latest'
|
||||
|
||||
Generated
+3
-14
@@ -2229,7 +2229,7 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "easytier"
|
||||
version = "2.6.3"
|
||||
version = "2.6.2"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -2288,7 +2288,6 @@ dependencies = [
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"kcp-sys",
|
||||
"lzokay-native",
|
||||
"machine-uid",
|
||||
"maplit",
|
||||
"mimalloc",
|
||||
@@ -2406,7 +2405,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "easytier-gui"
|
||||
version = "2.6.3"
|
||||
version = "2.6.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -2487,7 +2486,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "easytier-web"
|
||||
version = "2.6.3"
|
||||
version = "2.6.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -4875,16 +4874,6 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lzokay-native"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "792ba667add2798c6c3e988e630f4eb921b5cbc735044825b7111ef1582c8730"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"thiserror 1.0.63",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
id=easytier_magisk
|
||||
name=EasyTier_Magisk
|
||||
version=v2.6.3
|
||||
version=v2.6.2
|
||||
versionCode=1
|
||||
author=EasyTier
|
||||
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tauri + Vue + TS</title>
|
||||
</head>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "easytier-gui",
|
||||
"type": "module",
|
||||
"version": "2.6.3",
|
||||
"version": "2.6.2",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build:deps": "pnpm --filter tauri-plugin-vpnservice-api build && pnpm --filter easytier-frontend-lib build",
|
||||
"dev": "pnpm run build:deps && vite",
|
||||
"build": "pnpm run build:deps && vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"lint": "eslint . --ignore-pattern src-tauri",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "easytier-gui"
|
||||
version = "2.6.3"
|
||||
version = "2.6.2"
|
||||
description = "EasyTier GUI"
|
||||
authors = ["you"]
|
||||
edition.workspace = true
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"createUpdaterArtifacts": false
|
||||
},
|
||||
"productName": "easytier-gui",
|
||||
"version": "2.6.3",
|
||||
"version": "2.6.2",
|
||||
"identifier": "com.kkrainbow.easytier",
|
||||
"plugins": {
|
||||
"shell": {
|
||||
|
||||
@@ -33,6 +33,7 @@ const host = process.env.TAURI_DEV_HOST
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
base: './',
|
||||
resolve: {
|
||||
alias: {
|
||||
'~/': `${path.resolve(__dirname, 'src')}/`,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "easytier-web"
|
||||
version = "2.6.3"
|
||||
version = "2.6.2"
|
||||
edition.workspace = true
|
||||
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
||||
|
||||
[dependencies]
|
||||
easytier = { path = "../easytier" }
|
||||
easytier = { path = "../easytier", default-features = false, features = ["websocket"] }
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
anyhow = { version = "1.0" }
|
||||
thiserror = "1.0"
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"test": "vitest run",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -43,10 +44,11 @@
|
||||
"typescript": "~5.6.3",
|
||||
"vite": "^5.4.21",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vitest": "^2.1.9",
|
||||
"vue-tsc": "^2.1.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.12",
|
||||
"primevue": "^4.3.9"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,6 @@ const bool_flags: BoolFlag[] = [
|
||||
{ field: 'latency_first', help: 'latency_first_help' },
|
||||
{ field: 'use_smoltcp', help: 'use_smoltcp_help' },
|
||||
{ field: 'disable_ipv6', help: 'disable_ipv6_help' },
|
||||
{ field: 'ipv6_public_addr_auto', help: 'ipv6_public_addr_auto_help' },
|
||||
{ field: 'enable_kcp_proxy', help: 'enable_kcp_proxy_help' },
|
||||
{ field: 'disable_kcp_input', help: 'disable_kcp_input_help' },
|
||||
{ field: 'enable_quic_proxy', help: 'enable_quic_proxy_help' },
|
||||
@@ -99,7 +98,6 @@ const bool_flags: BoolFlag[] = [
|
||||
{ field: 'disable_encryption', help: 'disable_encryption_help' },
|
||||
{ field: 'disable_tcp_hole_punching', help: 'disable_tcp_hole_punching_help' },
|
||||
{ field: 'disable_udp_hole_punching', help: 'disable_udp_hole_punching_help' },
|
||||
{ field: 'disable_upnp', help: 'disable_upnp_help' },
|
||||
{ field: 'disable_sym_hole_punching', help: 'disable_sym_hole_punching_help' },
|
||||
{ field: 'enable_magic_dns', help: 'enable_magic_dns_help' },
|
||||
{ field: 'enable_private_mode', help: 'enable_private_mode_help' },
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import { AutoComplete, Button, Dialog, InputNumber, InputText } from 'primevue'
|
||||
import InputGroup from 'primevue/inputgroup'
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { buildUrlInputValue, getHostInputValue, parseHostInputOnBlur, parseUrlInput } from '../modules/url-input'
|
||||
|
||||
const props = defineProps<{
|
||||
placeholder?: string
|
||||
@@ -13,75 +14,30 @@ const props = defineProps<{
|
||||
const { t } = useI18n()
|
||||
const url = defineModel<string>({ required: true })
|
||||
const editing = ref(false)
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const internalCompact = ref(false)
|
||||
const hostFocused = ref(false)
|
||||
|
||||
const parseUrl = (val: string | null | undefined): { proto: string; host: string; port: number | null } => {
|
||||
const getValidPort = (portStr: string, proto: string) => {
|
||||
const p = parseInt(portStr)
|
||||
return isNaN(p) ? (props.protos[proto] ?? 11010) : p
|
||||
}
|
||||
const parseByPattern = (input: string) => {
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) {
|
||||
return null
|
||||
}
|
||||
const match = trimmed.match(/^(\w+):\/\/(.*)$/)
|
||||
const proto = match ? match[1] : 'tcp'
|
||||
const rest = match ? match[2] : trimmed
|
||||
const authority = rest.split(/[/?#]/)[0]
|
||||
if (!authority) {
|
||||
return null
|
||||
}
|
||||
const hostAndMaybePort = authority.includes('@') ? authority.slice(authority.lastIndexOf('@') + 1) : authority
|
||||
if (hostAndMaybePort.startsWith('[')) {
|
||||
const ipv6End = hostAndMaybePort.indexOf(']')
|
||||
if (ipv6End > 0) {
|
||||
const host = hostAndMaybePort.slice(0, ipv6End + 1)
|
||||
const remain = hostAndMaybePort.slice(ipv6End + 1)
|
||||
// null = no explicit port in URL; do not fabricate a default
|
||||
const port: number | null = remain.startsWith(':') ? getValidPort(remain.slice(1), proto) : null
|
||||
return { proto, host, port }
|
||||
onMounted(() => {
|
||||
if (container.value) {
|
||||
const observer = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
internalCompact.value = entry.contentRect.width < 400
|
||||
}
|
||||
}
|
||||
const portMatch = hostAndMaybePort.match(/^(.*):(\d+)$/)
|
||||
const host = portMatch ? portMatch[1] : hostAndMaybePort
|
||||
// null = no explicit port in URL; buildUrlValue will omit the port entirely,
|
||||
// preserving the protocol's implied standard port (e.g. 443 for wss://).
|
||||
const port: number | null = portMatch ? parseInt(portMatch[2]) : null
|
||||
return { proto, host, port }
|
||||
}
|
||||
})
|
||||
observer.observe(container.value)
|
||||
|
||||
if (!val) {
|
||||
return { proto: 'tcp', host: '', port: props.protos['tcp'] ?? 11010 }
|
||||
onUnmounted(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
}
|
||||
const parsedByPattern = parseByPattern(val)
|
||||
if (parsedByPattern) {
|
||||
return parsedByPattern
|
||||
}
|
||||
return { proto: 'tcp', host: '', port: null }
|
||||
}
|
||||
})
|
||||
|
||||
const internalValue = ref(parseUrl(url.value))
|
||||
const internalValue = ref(parseUrlInput(url.value, props.protos))
|
||||
const defaultHost = '0.0.0.0'
|
||||
|
||||
const buildUrlValue = (value: { proto: string, host: string, port: number | null }, forceDefaultHost = false) => {
|
||||
const proto = value.proto || 'tcp'
|
||||
const rawHost = (value.host ?? '').trim()
|
||||
const host = rawHost || (forceDefaultHost ? defaultHost : '')
|
||||
if (!host) {
|
||||
return null
|
||||
}
|
||||
// Omit port when the protocol uses no port (protos value = 0), or when the
|
||||
// original URL had no explicit port (port === null) – avoids overwriting an
|
||||
// implicit standard port (e.g. 443 for wss) with an EasyTier default (11012).
|
||||
if (props.protos[proto] === 0 || value.port === null) {
|
||||
return `${proto}://${host}`
|
||||
}
|
||||
return `${proto}://${host}:${value.port}`
|
||||
}
|
||||
|
||||
const syncUrlFromInternal = (forceDefaultHost = false) => {
|
||||
const nextUrl = buildUrlValue(internalValue.value, forceDefaultHost)
|
||||
const nextUrl = buildUrlInputValue(internalValue.value, props.protos, forceDefaultHost)
|
||||
if (!nextUrl || nextUrl === url.value) {
|
||||
return
|
||||
}
|
||||
@@ -90,6 +46,10 @@ const syncUrlFromInternal = (forceDefaultHost = false) => {
|
||||
|
||||
const onHostBlur = () => {
|
||||
hostFocused.value = false
|
||||
const parsedHost = parseHostInputOnBlur(internalValue.value.host ?? '', internalValue.value.proto, props.protos)
|
||||
if (parsedHost) {
|
||||
internalValue.value = parsedHost
|
||||
}
|
||||
syncUrlFromInternal(true)
|
||||
}
|
||||
|
||||
@@ -106,12 +66,20 @@ const isNoPortProto = computed(() => {
|
||||
return props.protos[internalValue.value.proto] === 0
|
||||
})
|
||||
|
||||
const hostInputValue = computed({
|
||||
get: () => getHostInputValue(internalValue.value),
|
||||
set: (value: string) => {
|
||||
internalValue.value.host = value
|
||||
internalValue.value.suffix = undefined
|
||||
},
|
||||
})
|
||||
|
||||
// Sync from external
|
||||
watch(() => url.value, (newVal) => {
|
||||
if (hostFocused.value) {
|
||||
return
|
||||
}
|
||||
const parsed = parseUrl(newVal)
|
||||
const parsed = parseUrlInput(newVal, props.protos)
|
||||
const internalHost = internalValue.value.host ?? ''
|
||||
const sameHost = parsed.host === internalHost || (!internalHost.trim() && parsed.host === defaultHost)
|
||||
if (parsed.proto !== internalValue.value.proto ||
|
||||
@@ -123,6 +91,9 @@ watch(() => url.value, (newVal) => {
|
||||
|
||||
// Sync to external
|
||||
watch(internalValue, () => {
|
||||
if (hostFocused.value) {
|
||||
return
|
||||
}
|
||||
syncUrlFromInternal(false)
|
||||
}, { deep: true })
|
||||
|
||||
@@ -148,34 +119,34 @@ const onProtoChange = (newProto: string) => {
|
||||
internalValue.value.port = newDefault
|
||||
}
|
||||
internalValue.value.proto = newProto
|
||||
internalValue.value.suffix = undefined
|
||||
internalValue.value.hasExplicitPort = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="url-input-container w-full min-w-0 overflow-hidden">
|
||||
<InputGroup class="url-input-full w-full min-w-0">
|
||||
<div ref="container" class="w-full">
|
||||
<InputGroup v-if="!internalCompact" class="w-full">
|
||||
<AutoComplete :model-value="internalValue.proto" :suggestions="filteredProtos" dropdown
|
||||
class="max-w-32 proto-autocomplete-in-group" @complete="searchProtos"
|
||||
@update:model-value="onProtoChange" />
|
||||
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="grow min-w-0"
|
||||
<InputText v-model="hostInputValue" :placeholder="placeholder || '0.0.0.0'" class="grow"
|
||||
@focus="onHostFocus" @blur="onHostBlur" />
|
||||
<template v-if="!isNoPortProto">
|
||||
<InputGroupAddon>
|
||||
<span style="font-weight: bold">:</span>
|
||||
</InputGroupAddon>
|
||||
<InputNumber v-model="internalValue.port" :format="false" :min="1" :max="65535" class="max-w-24"
|
||||
:placeholder="String(protos[internalValue.proto] ?? 11010)" fluid />
|
||||
:placeholder="String(protos[internalValue.proto] ?? 11010)"
|
||||
fluid />
|
||||
</template>
|
||||
<!-- Rendered in both responsive branches; keep action slot content free of side effects and duplicate IDs. -->
|
||||
<slot name="actions"></slot>
|
||||
</InputGroup>
|
||||
|
||||
<div
|
||||
class="url-input-compact flex justify-between items-center p-2 border rounded w-full min-w-0 overflow-hidden">
|
||||
<span class="truncate mr-2 min-w-0 flex-1 overflow-hidden">{{ url }}</span>
|
||||
<div class="flex items-center shrink-0">
|
||||
<Button icon="pi pi-pencil" class="p-button-sm p-button-text" :aria-label="t('web.common.edit')"
|
||||
@click="editing = true" />
|
||||
<div v-else class="flex justify-between items-center p-2 border rounded w-full">
|
||||
<span class="truncate mr-2">{{ url }}</span>
|
||||
<div class="flex items-center">
|
||||
<Button icon="pi pi-pencil" class="p-button-sm p-button-text" @click="editing = true" />
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,7 +160,7 @@ const onProtoChange = (newProto: string) => {
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label>{{ t('web.common.address') || 'Address' }}</label>
|
||||
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="w-full"
|
||||
<InputText v-model="hostInputValue" :placeholder="placeholder || '0.0.0.0'" class="w-full"
|
||||
@focus="onHostFocus" @blur="onHostBlur" />
|
||||
</div>
|
||||
<div v-if="!isNoPortProto" class="flex flex-col gap-2">
|
||||
@@ -207,28 +178,6 @@ const onProtoChange = (newProto: string) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.url-input-container {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.url-input-full {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.url-input-compact {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@container (min-width: 400px) {
|
||||
.url-input-full {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.url-input-compact {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.proto-autocomplete-in-group,
|
||||
.proto-autocomplete-in-group :deep(.p-autocomplete-input),
|
||||
.proto-autocomplete-in-group :deep(.p-autocomplete-dropdown) {
|
||||
|
||||
@@ -104,9 +104,6 @@ use_smoltcp_help: 使用用户态 TCP/IP 协议栈,避免操作系统防火墙
|
||||
disable_ipv6: 禁用IPv6
|
||||
disable_ipv6_help: 禁用此节点的IPv6功能,仅使用IPv4进行网络通信。
|
||||
|
||||
ipv6_public_addr_auto: 自动获取公网 IPv6
|
||||
ipv6_public_addr_auto_help: 自动从共享了 IPv6 子网的对等节点获取一个公网 IPv6 地址。
|
||||
|
||||
enable_kcp_proxy: 启用 KCP 代理
|
||||
enable_kcp_proxy_help: 将 TCP 流量转为 KCP 流量,降低传输延迟,提升传输速度。
|
||||
|
||||
@@ -160,9 +157,6 @@ disable_tcp_hole_punching_help: 禁用TCP打洞功能
|
||||
disable_udp_hole_punching: 禁用UDP打洞
|
||||
disable_udp_hole_punching_help: 禁用UDP打洞功能
|
||||
|
||||
disable_upnp: 禁用 UPnP
|
||||
disable_upnp_help: 禁用符合条件监听器的运行时 UPnP/NAT-PMP 端口映射;自动端口映射默认开启。
|
||||
|
||||
disable_sym_hole_punching: 禁用对称NAT打洞
|
||||
disable_sym_hole_punching_help: 禁用对称NAT的打洞(生日攻击),将对称NAT视为锥形NAT处理
|
||||
|
||||
|
||||
@@ -103,9 +103,6 @@ use_smoltcp_help: Use a user-space TCP/IP stack to avoid issues with operating s
|
||||
disable_ipv6: Disable IPv6
|
||||
disable_ipv6_help: Disable IPv6 functionality for this node, only use IPv4 for network communication.
|
||||
|
||||
ipv6_public_addr_auto: Auto Public IPv6
|
||||
ipv6_public_addr_auto_help: Auto-obtain a public IPv6 address from a peer that shares its IPv6 subnet.
|
||||
|
||||
enable_kcp_proxy: Enable KCP Proxy
|
||||
enable_kcp_proxy_help: Convert TCP traffic to KCP traffic to reduce latency and boost transmission speed.
|
||||
|
||||
@@ -159,9 +156,6 @@ disable_tcp_hole_punching_help: Disable tcp hole punching
|
||||
disable_udp_hole_punching: Disable UDP Hole Punching
|
||||
disable_udp_hole_punching_help: Disable udp hole punching
|
||||
|
||||
disable_upnp: Disable UPnP
|
||||
disable_upnp_help: Disable runtime UPnP/NAT-PMP port mapping for eligible listeners; automatic port mapping is enabled by default.
|
||||
|
||||
disable_sym_hole_punching: Disable Symmetric NAT Hole Punching
|
||||
disable_sym_hole_punching_help: Disable special hole punching handling for symmetric NAT (based on birthday attack), treat symmetric NAT as cone NAT
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildUrlInputValue, getHostInputValue, parseHostInputOnBlur, parseUrlInput, type ProtoPorts } from './url-input'
|
||||
|
||||
const protos: ProtoPorts = {
|
||||
tcp: 11010,
|
||||
udp: 11010,
|
||||
wg: 11011,
|
||||
ws: 11011,
|
||||
wss: 11012,
|
||||
quic: 11012,
|
||||
faketcp: 11013,
|
||||
http: 80,
|
||||
https: 443,
|
||||
txt: 0,
|
||||
srv: 0,
|
||||
}
|
||||
|
||||
function normalizeUrl(input: string, defaultProto = 'tcp') {
|
||||
return buildUrlInputValue(parseUrlInput(input, protos, defaultProto), protos, true)
|
||||
}
|
||||
|
||||
describe('parseUrlInput', () => {
|
||||
it.each([
|
||||
['https://raw.githubusercontent.com/aaa/bb/cc.txt', {
|
||||
proto: 'https',
|
||||
host: 'raw.githubusercontent.com',
|
||||
port: null,
|
||||
suffix: '/aaa/bb/cc.txt',
|
||||
hasExplicitPort: false,
|
||||
}],
|
||||
['https://host:4443/path?x=1#hash', {
|
||||
proto: 'https',
|
||||
host: 'host',
|
||||
port: 4443,
|
||||
suffix: '/path?x=1#hash',
|
||||
hasExplicitPort: true,
|
||||
}],
|
||||
['[::1]:11010/path', {
|
||||
proto: 'tcp',
|
||||
host: '[::1]',
|
||||
port: 11010,
|
||||
suffix: '/path',
|
||||
hasExplicitPort: true,
|
||||
}],
|
||||
[' http://host/path ', {
|
||||
proto: 'http',
|
||||
host: 'host',
|
||||
port: null,
|
||||
suffix: '/path',
|
||||
hasExplicitPort: false,
|
||||
}],
|
||||
])('parses %s', (input, expected) => {
|
||||
expect(parseUrlInput(input, protos)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('parses IPv6 host without an explicit port', () => {
|
||||
expect(parseUrlInput('[::1]', protos)).toEqual({
|
||||
proto: 'tcp',
|
||||
host: '[::1]',
|
||||
port: null,
|
||||
suffix: '',
|
||||
hasExplicitPort: false,
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
['host:', 'host'],
|
||||
['host:notaport', 'host'],
|
||||
])('falls back to the default port for invalid port input %s', (input, host) => {
|
||||
expect(parseUrlInput(input, protos)).toEqual({
|
||||
proto: 'tcp',
|
||||
host,
|
||||
port: 11010,
|
||||
suffix: '',
|
||||
hasExplicitPort: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the explicit proto for an input without authority', () => {
|
||||
expect(parseUrlInput('https://', protos)).toEqual({
|
||||
proto: 'https',
|
||||
host: '',
|
||||
port: null,
|
||||
suffix: '',
|
||||
hasExplicitPort: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildUrlInputValue', () => {
|
||||
it.each([
|
||||
['https://host', 'https://host'],
|
||||
['http://host', 'http://host'],
|
||||
['https://host:4443/path', 'https://host:4443/path'],
|
||||
['https://host:443/path', 'https://host:443/path'],
|
||||
['tcp://host', 'tcp://host'],
|
||||
['wss://host', 'wss://host'],
|
||||
['http://host/path?x=1#hash', 'http://host/path?x=1#hash'],
|
||||
['https://host?x=1', 'https://host?x=1'],
|
||||
['https://host#hash', 'https://host#hash'],
|
||||
['txt://example.com/path.txt', 'txt://example.com/path.txt'],
|
||||
['srv://_easytier._tcp.example.com', 'srv://_easytier._tcp.example.com'],
|
||||
['custom://host/path', 'custom://host/path'],
|
||||
])('normalizes %s to %s', (input, expected) => {
|
||||
expect(normalizeUrl(input)).toBe(expected)
|
||||
})
|
||||
|
||||
it('returns null for empty host unless default host is forced', () => {
|
||||
const parsed = parseUrlInput('', protos)
|
||||
|
||||
expect(buildUrlInputValue(parsed, protos, false)).toBeNull()
|
||||
expect(buildUrlInputValue(parsed, protos, true)).toBe('tcp://0.0.0.0:11010')
|
||||
})
|
||||
|
||||
it('does not build a broken URL for a protocol without authority', () => {
|
||||
const parsed = parseUrlInput('https://', protos)
|
||||
|
||||
expect(buildUrlInputValue(parsed, protos, false)).toBeNull()
|
||||
expect(buildUrlInputValue(parsed, protos, true)).toBe('https://0.0.0.0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseHostInputOnBlur', () => {
|
||||
it('infers https for a pasted host:port/path when the current proto is tcp', () => {
|
||||
const parsed = parseHostInputOnBlur('raw.githubusercontent.com:4443/aaa/bb/cc.txt', 'tcp', protos)
|
||||
|
||||
expect(parsed).toEqual({
|
||||
proto: 'https',
|
||||
host: 'raw.githubusercontent.com',
|
||||
port: 4443,
|
||||
suffix: '/aaa/bb/cc.txt',
|
||||
hasExplicitPort: true,
|
||||
})
|
||||
expect(buildUrlInputValue(parsed!, protos, true)).toBe('https://raw.githubusercontent.com:4443/aaa/bb/cc.txt')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['raw.githubusercontent.com/aaa/bb/cc.txt', 'tcp', 'https://raw.githubusercontent.com/aaa/bb/cc.txt'],
|
||||
['raw.githubusercontent.com:4443/aaa/bb/cc.txt', 'https', 'https://raw.githubusercontent.com:4443/aaa/bb/cc.txt'],
|
||||
['https://raw.githubusercontent.com:4443/aaa/bb/cc.txt', 'tcp', 'https://raw.githubusercontent.com:4443/aaa/bb/cc.txt'],
|
||||
[' https://raw.githubusercontent.com/aaa/bb/cc.txt ', 'tcp', 'https://raw.githubusercontent.com/aaa/bb/cc.txt'],
|
||||
])('normalizes pasted host input %s with current proto %s', (input, currentProto, expected) => {
|
||||
const parsed = parseHostInputOnBlur(input, currentProto, protos)
|
||||
|
||||
expect(buildUrlInputValue(parsed!, protos, true)).toBe(expected)
|
||||
})
|
||||
|
||||
it('keeps ordinary host:port input on the current tcp protocol', () => {
|
||||
const parsed = parseHostInputOnBlur('example.com:11010', 'tcp', protos)
|
||||
|
||||
expect(buildUrlInputValue(parsed!, protos, true)).toBe('tcp://example.com:11010')
|
||||
})
|
||||
|
||||
it('returns null for a simple host without port or suffix', () => {
|
||||
expect(parseHostInputOnBlur('example.com', 'tcp', protos)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getHostInputValue', () => {
|
||||
it('shows host and suffix while keeping the port in the port field', () => {
|
||||
const parsed = parseUrlInput('https://raw.githubusercontent.com:4443/aaa/bb/cc.txt', protos)
|
||||
|
||||
expect(getHostInputValue(parsed)).toBe('raw.githubusercontent.com/aaa/bb/cc.txt')
|
||||
})
|
||||
|
||||
it('shows query and hash in the host input suffix', () => {
|
||||
const parsed = parseUrlInput('https://host/path?x=1#hash', protos)
|
||||
|
||||
expect(getHostInputValue(parsed)).toBe('host/path?x=1#hash')
|
||||
})
|
||||
})
|
||||
|
||||
describe('round trip scenarios', () => {
|
||||
it.each([
|
||||
['https://raw.githubusercontent.com/aaa/bb/cc.txt'],
|
||||
['https://raw.githubusercontent.com:4443/aaa/bb/cc.txt'],
|
||||
['http://host/path?x=1#hash'],
|
||||
['tcp://example.com:11010'],
|
||||
['txt://example.com/path.txt'],
|
||||
['srv://_easytier._tcp.example.com'],
|
||||
])('keeps %s stable after parse and build', (input) => {
|
||||
expect(normalizeUrl(input)).toBe(input)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,105 @@
|
||||
export interface UrlInputParts {
|
||||
proto: string
|
||||
host: string
|
||||
port: number | null
|
||||
suffix?: string
|
||||
hasExplicitPort?: boolean
|
||||
}
|
||||
|
||||
export type ProtoPorts = Record<string, number>
|
||||
|
||||
const fallbackProto = 'tcp'
|
||||
const fallbackPort = 11010
|
||||
const defaultHost = '0.0.0.0'
|
||||
|
||||
function defaultPortFor(protos: ProtoPorts, proto: string) {
|
||||
return protos[proto] ?? fallbackPort
|
||||
}
|
||||
|
||||
function getValidPort(portStr: string, protos: ProtoPorts, proto: string) {
|
||||
const p = parseInt(portStr)
|
||||
return isNaN(p) ? defaultPortFor(protos, proto) : p
|
||||
}
|
||||
|
||||
export function parseUrlInput(val: string | null | undefined, protos: ProtoPorts, defaultProto = fallbackProto): UrlInputParts {
|
||||
const parseByPattern = (input: string) => {
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) {
|
||||
return null
|
||||
}
|
||||
|
||||
const match = trimmed.match(/^(\w+):\/\/(.*)$/)
|
||||
const proto = match ? match[1] : defaultProto
|
||||
const rest = match ? match[2] : trimmed
|
||||
const suffixStart = rest.search(/[/?#]/)
|
||||
const authority = suffixStart >= 0 ? rest.slice(0, suffixStart) : rest
|
||||
const suffix = suffixStart >= 0 ? rest.slice(suffixStart) : ''
|
||||
if (!authority) {
|
||||
return { proto, host: '', port: null, suffix, hasExplicitPort: false }
|
||||
}
|
||||
|
||||
const hostAndMaybePort = authority.includes('@') ? authority.slice(authority.lastIndexOf('@') + 1) : authority
|
||||
if (hostAndMaybePort.startsWith('[')) {
|
||||
const ipv6End = hostAndMaybePort.indexOf(']')
|
||||
if (ipv6End > 0) {
|
||||
const host = hostAndMaybePort.slice(0, ipv6End + 1)
|
||||
const remain = hostAndMaybePort.slice(ipv6End + 1)
|
||||
const hasExplicitPort = remain.startsWith(':')
|
||||
const port = hasExplicitPort ? getValidPort(remain.slice(1), protos, proto) : null
|
||||
return { proto, host, port, suffix, hasExplicitPort }
|
||||
}
|
||||
}
|
||||
|
||||
const portMatch = hostAndMaybePort.match(/^(.*):(\d+)$/)
|
||||
if (portMatch) {
|
||||
return { proto, host: portMatch[1], port: parseInt(portMatch[2]), suffix, hasExplicitPort: true }
|
||||
}
|
||||
|
||||
const invalidPortMatch = hostAndMaybePort.match(/^([^:]+):[^:]*$/)
|
||||
const host = invalidPortMatch ? invalidPortMatch[1] : hostAndMaybePort
|
||||
const port = invalidPortMatch ? defaultPortFor(protos, proto) : null
|
||||
return { proto, host, port, suffix, hasExplicitPort: false }
|
||||
}
|
||||
|
||||
if (!val) {
|
||||
return { proto: defaultProto, host: '', port: defaultPortFor(protos, defaultProto) }
|
||||
}
|
||||
const parsedByPattern = parseByPattern(val)
|
||||
if (parsedByPattern) {
|
||||
return parsedByPattern
|
||||
}
|
||||
return { proto: defaultProto, host: '', port: defaultPortFor(protos, defaultProto) }
|
||||
}
|
||||
|
||||
export function buildUrlInputValue(value: UrlInputParts, protos: ProtoPorts, forceDefaultHost = false) {
|
||||
const proto = value.proto || fallbackProto
|
||||
const rawHost = (value.host ?? '').trim()
|
||||
const host = rawHost || (forceDefaultHost ? defaultHost : '')
|
||||
if (!host) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (protos[proto] === 0 || value.port === null) {
|
||||
return `${proto}://${host}${value.suffix ?? ''}`
|
||||
}
|
||||
|
||||
let port = value.port
|
||||
if (isNaN(parseInt(port as any))) {
|
||||
port = defaultPortFor(protos, proto)
|
||||
}
|
||||
|
||||
return `${proto}://${host}:${port}${value.suffix ?? ''}`
|
||||
}
|
||||
|
||||
export function parseHostInputOnBlur(rawHost: string, currentProto: string, protos: ProtoPorts) {
|
||||
const inferredProto = rawHost.includes('/') && currentProto === fallbackProto ? 'https' : currentProto
|
||||
const parsedHost = parseUrlInput(rawHost, protos, inferredProto)
|
||||
if (parsedHost.host && (parsedHost.proto !== currentProto || parsedHost.hasExplicitPort || parsedHost.suffix)) {
|
||||
return parsedHost
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function getHostInputValue(value: UrlInputParts) {
|
||||
return `${value.host ?? ''}${value.suffix ?? ''}`
|
||||
}
|
||||
@@ -115,7 +115,6 @@ export interface NetworkConfig {
|
||||
|
||||
use_smoltcp?: boolean
|
||||
disable_ipv6?: boolean
|
||||
ipv6_public_addr_auto?: boolean
|
||||
enable_kcp_proxy?: boolean
|
||||
disable_kcp_input?: boolean
|
||||
enable_quic_proxy?: boolean
|
||||
@@ -133,7 +132,6 @@ export interface NetworkConfig {
|
||||
disable_encryption?: boolean
|
||||
disable_tcp_hole_punching?: boolean
|
||||
disable_udp_hole_punching?: boolean
|
||||
disable_upnp?: boolean
|
||||
disable_sym_hole_punching?: boolean
|
||||
|
||||
enable_relay_network_whitelist?: boolean
|
||||
@@ -192,7 +190,6 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
|
||||
use_smoltcp: false,
|
||||
disable_ipv6: false,
|
||||
ipv6_public_addr_auto: false,
|
||||
enable_kcp_proxy: false,
|
||||
disable_kcp_input: false,
|
||||
enable_quic_proxy: false,
|
||||
@@ -210,7 +207,6 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
disable_encryption: false,
|
||||
disable_tcp_hole_punching: false,
|
||||
disable_udp_hole_punching: false,
|
||||
disable_upnp: false,
|
||||
disable_sym_hole_punching: false,
|
||||
enable_relay_network_whitelist: false,
|
||||
relay_network_whitelist: [],
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"build:deps": "pnpm --filter easytier-frontend-lib build",
|
||||
"dev": "pnpm run build:deps && vite",
|
||||
"build": "pnpm run build:deps && vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -32,4 +33,4 @@
|
||||
"vite-plugin-singlefile": "^2.0.3",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-5
@@ -3,7 +3,7 @@ name = "easytier"
|
||||
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasyTier"
|
||||
version = "2.6.3"
|
||||
version = "2.6.2"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors = ["kkrainbow"]
|
||||
@@ -221,7 +221,6 @@ async-ringbuf = "0.3.1"
|
||||
service-manager = { git = "https://github.com/EasyTier/service-manager-rs.git", branch = "main" }
|
||||
|
||||
zstd = { version = "0.13", optional = true }
|
||||
lzokay-native = { version = "0.1", optional = true }
|
||||
|
||||
kcp-sys = { git = "https://github.com/EasyTier/kcp-sys", rev = "94964794caaed5d388463137da59b97499619e5f", optional = true }
|
||||
|
||||
@@ -359,7 +358,6 @@ default = [
|
||||
"faketcp",
|
||||
"magic-dns",
|
||||
"zstd",
|
||||
"lzo",
|
||||
]
|
||||
full = [
|
||||
"websocket",
|
||||
@@ -374,7 +372,6 @@ full = [
|
||||
"faketcp",
|
||||
"magic-dns",
|
||||
"zstd",
|
||||
"lzo",
|
||||
]
|
||||
wireguard = ["dep:boringtun", "dep:ring"]
|
||||
quic = ["dep:quinn", "dep:quinn-plaintext", "dep:rustls", "dep:rcgen"]
|
||||
@@ -405,6 +402,5 @@ tracing = ["tokio/tracing", "dep:console-subscriber"]
|
||||
magic-dns = ["dep:hickory-client", "dep:hickory-server"]
|
||||
faketcp = ["dep:flume"]
|
||||
zstd = ["dep:zstd"]
|
||||
lzo = ["dep:lzokay-native"]
|
||||
# For Network Extension on macOS
|
||||
macos-ne = []
|
||||
|
||||
@@ -191,11 +191,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
)
|
||||
.type_attribute("peer_rpc.RouteForeignNetworkSummary", "#[derive(Hash, Eq)]")
|
||||
.type_attribute("common.RpcDescriptor", "#[derive(Hash, Eq)]")
|
||||
.type_attribute("acl.Acl", "#[serde(default)]")
|
||||
.type_attribute("acl.AclV1", "#[serde(default)]")
|
||||
.type_attribute("acl.Chain", "#[serde(default)]")
|
||||
.type_attribute("acl.Rule", "#[serde(default)]")
|
||||
.type_attribute("acl.GroupInfo", "#[serde(default)]")
|
||||
.field_attribute(".api.manage.NetworkConfig", "#[serde(default)]")
|
||||
.service_generator(Box::new(easytier_rpc_build::ServiceGenerator::default()))
|
||||
.btree_map(["."])
|
||||
|
||||
@@ -194,8 +194,8 @@ core_clap:
|
||||
en: "the url of the ipv6 listener, e.g.: tcp://[::]:11010, if not set, will listen on random udp port"
|
||||
zh-CN: "IPv6 监听器的URL,例如:tcp://[::]:11010,如果未设置,将在随机UDP端口上监听"
|
||||
compression:
|
||||
en: "compression algorithm to use, supported: %{algorithms}. default is none"
|
||||
zh-CN: "要使用的压缩算法,支持:%{algorithms}。默认为 none"
|
||||
en: "compression algorithm to use, support none, zstd. default is none"
|
||||
zh-CN: "要使用的压缩算法,支持 none、zstd。默认为 none"
|
||||
mapped_listeners:
|
||||
en: "manually specify the public address of the listener, other nodes can use this address to connect to this node. e.g.: tcp://123.123.123.123:11223, can specify multiple."
|
||||
zh-CN: "手动指定监听器的公网地址,其他节点可以使用该地址连接到本节点。例如:tcp://123.123.123.123:11223,可以指定多个。"
|
||||
|
||||
@@ -137,13 +137,12 @@ pub fn setup_socket_for_win<S: AsRawSocket>(
|
||||
}
|
||||
|
||||
let socket = SOCKET(socket.as_raw_socket() as usize);
|
||||
|
||||
// let optval = 1_i32.to_ne_bytes();
|
||||
// unsafe {
|
||||
// if setsockopt(socket, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, Some(&optval)) == SOCKET_ERROR {
|
||||
// return Err(io::Error::last_os_error());
|
||||
// }
|
||||
// }
|
||||
let optval = 1_i32.to_ne_bytes();
|
||||
unsafe {
|
||||
if setsockopt(socket, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, Some(&optval)) == SOCKET_ERROR {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(iface) = bind_dev {
|
||||
set_ip_unicast_if(socket, bind_addr, &iface)?;
|
||||
|
||||
@@ -1339,45 +1339,6 @@ mod tests {
|
||||
assert_eq!(result.matched_rule, Some(RuleId::Priority(70)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_forward_acl_source_ip_whitelist() {
|
||||
let mut acl_config = Acl::default();
|
||||
let mut acl_v1 = AclV1::default();
|
||||
let mut chain = Chain {
|
||||
name: "subnet_proxy_protect".to_string(),
|
||||
chain_type: ChainType::Forward as i32,
|
||||
enabled: true,
|
||||
default_action: Action::Drop as i32,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
chain.rules.push(Rule {
|
||||
name: "allow_my_devices".to_string(),
|
||||
priority: 1000,
|
||||
enabled: true,
|
||||
action: Action::Allow as i32,
|
||||
protocol: Protocol::Any as i32,
|
||||
source_ips: vec!["10.172.192.2/32".to_string()],
|
||||
..Default::default()
|
||||
});
|
||||
acl_v1.chains.push(chain);
|
||||
acl_config.acl_v1 = Some(acl_v1);
|
||||
|
||||
let processor = AclProcessor::new(acl_config);
|
||||
let mut packet_info = create_test_packet_info();
|
||||
packet_info.dst_ip = "192.168.1.10".parse().unwrap();
|
||||
|
||||
packet_info.src_ip = "10.172.192.2".parse().unwrap();
|
||||
let result = processor.process_packet(&packet_info, ChainType::Forward);
|
||||
assert_eq!(result.action, Action::Allow);
|
||||
assert_eq!(result.matched_rule, Some(RuleId::Priority(1000)));
|
||||
|
||||
packet_info.src_ip = "10.172.192.3".parse().unwrap();
|
||||
let result = processor.process_packet(&packet_info, ChainType::Forward);
|
||||
assert_eq!(result.action, Action::Drop);
|
||||
assert_eq!(result.matched_rule, Some(RuleId::Default));
|
||||
}
|
||||
|
||||
fn create_test_acl_config() -> Acl {
|
||||
let mut acl_config = Acl::default();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[cfg(any(feature = "zstd", feature = "lzo"))]
|
||||
#[cfg(feature = "zstd")]
|
||||
use anyhow::Context;
|
||||
#[cfg(feature = "zstd")]
|
||||
use dashmap::DashMap;
|
||||
@@ -53,13 +53,6 @@ impl DefaultCompressor {
|
||||
)
|
||||
})
|
||||
}),
|
||||
#[cfg(feature = "lzo")]
|
||||
CompressorAlgo::Lzo => lzokay_native::compress(data).with_context(|| {
|
||||
format!(
|
||||
"Failed to compress data with algorithm: {:?}",
|
||||
compress_algo
|
||||
)
|
||||
}),
|
||||
CompressorAlgo::None => Ok(data.to_vec()),
|
||||
}
|
||||
}
|
||||
@@ -92,13 +85,6 @@ impl DefaultCompressor {
|
||||
compress_algo
|
||||
))
|
||||
}),
|
||||
#[cfg(feature = "lzo")]
|
||||
CompressorAlgo::Lzo => lzokay_native::decompress_all(data, None).with_context(|| {
|
||||
format!(
|
||||
"Failed to decompress data with algorithm: {:?}",
|
||||
compress_algo
|
||||
)
|
||||
}),
|
||||
CompressorAlgo::None => Ok(data.to_vec()),
|
||||
}
|
||||
}
|
||||
@@ -195,13 +181,14 @@ thread_local! {
|
||||
static DCTX_MAP: RefCell<DashMap<CompressorAlgo, bulk::Decompressor<'static>>> = RefCell::new(DashMap::new());
|
||||
}
|
||||
|
||||
#[cfg(all(test, any(feature = "zstd", feature = "lzo")))]
|
||||
#[cfg(all(test, feature = "zstd"))]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
async fn test_compress_algo(compress_algo: CompressorAlgo) {
|
||||
let text = vec![b'a'; 4096];
|
||||
let mut packet = ZCPacket::new_with_payload(&text);
|
||||
#[tokio::test]
|
||||
async fn test_compress() {
|
||||
let text = b"12345670000000000000000000";
|
||||
let mut packet = ZCPacket::new_with_payload(text);
|
||||
packet.fill_peer_manager_hdr(0, 0, 0);
|
||||
|
||||
let compressor = DefaultCompressor {};
|
||||
@@ -213,7 +200,7 @@ pub mod tests {
|
||||
);
|
||||
|
||||
compressor
|
||||
.compress(&mut packet, compress_algo)
|
||||
.compress(&mut packet, CompressorAlgo::ZstdDefault)
|
||||
.await
|
||||
.unwrap();
|
||||
println!(
|
||||
@@ -228,7 +215,8 @@ pub mod tests {
|
||||
assert!(!packet.peer_manager_header().unwrap().is_compressed());
|
||||
}
|
||||
|
||||
async fn test_short_text_compress_algo(compress_algo: CompressorAlgo) {
|
||||
#[tokio::test]
|
||||
async fn test_short_text_compress() {
|
||||
let text = b"1234";
|
||||
let mut packet = ZCPacket::new_with_payload(text);
|
||||
packet.fill_peer_manager_hdr(0, 0, 0);
|
||||
@@ -237,7 +225,7 @@ pub mod tests {
|
||||
|
||||
// short text can't be compressed
|
||||
compressor
|
||||
.compress(&mut packet, compress_algo)
|
||||
.compress(&mut packet, CompressorAlgo::ZstdDefault)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!packet.peer_manager_header().unwrap().is_compressed());
|
||||
@@ -246,28 +234,4 @@ pub mod tests {
|
||||
assert_eq!(packet.payload(), text);
|
||||
assert!(!packet.peer_manager_header().unwrap().is_compressed());
|
||||
}
|
||||
|
||||
#[cfg(feature = "zstd")]
|
||||
#[tokio::test]
|
||||
async fn test_zstd_compress() {
|
||||
test_compress_algo(CompressorAlgo::ZstdDefault).await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "zstd")]
|
||||
#[tokio::test]
|
||||
async fn test_zstd_short_text_compress() {
|
||||
test_short_text_compress_algo(CompressorAlgo::ZstdDefault).await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "lzo")]
|
||||
#[tokio::test]
|
||||
async fn test_lzo_compress() {
|
||||
test_compress_algo(CompressorAlgo::Lzo).await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "lzo")]
|
||||
#[tokio::test]
|
||||
async fn test_lzo_short_text_compress() {
|
||||
test_short_text_compress_algo(CompressorAlgo::Lzo).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,6 @@ pub fn gen_default_flags() -> Flags {
|
||||
need_p2p: false,
|
||||
instance_recv_bps_limit: u64::MAX,
|
||||
disable_upnp: false,
|
||||
disable_relay_data: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1337,71 +1336,6 @@ stun_servers = [
|
||||
assert!(err.to_string().contains("mapped listener port is missing"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acl_toml_rule_uses_defaults_for_omitted_fields() {
|
||||
use crate::proto::acl::{Action, ChainType, Protocol};
|
||||
|
||||
let config_str = r#"
|
||||
[[acl.acl_v1.chains]]
|
||||
name = "subnet_proxy_protect"
|
||||
chain_type = 3
|
||||
enabled = true
|
||||
default_action = 2
|
||||
|
||||
[[acl.acl_v1.chains.rules]]
|
||||
name = "allow_my_devices"
|
||||
priority = 1000
|
||||
action = 1
|
||||
source_ips = ["10.172.192.2/32"]
|
||||
protocol = 5
|
||||
enabled = true
|
||||
"#;
|
||||
|
||||
let config = TomlConfigLoader::new_from_str(config_str).unwrap();
|
||||
let acl = config.get_acl().unwrap();
|
||||
let acl_v1 = acl.acl_v1.unwrap();
|
||||
let chain = &acl_v1.chains[0];
|
||||
let rule = &chain.rules[0];
|
||||
|
||||
assert_eq!(chain.chain_type, ChainType::Forward as i32);
|
||||
assert_eq!(chain.default_action, Action::Drop as i32);
|
||||
assert_eq!(rule.action, Action::Allow as i32);
|
||||
assert_eq!(rule.protocol, Protocol::Any as i32);
|
||||
assert_eq!(rule.source_ips, vec!["10.172.192.2/32"]);
|
||||
assert!(rule.ports.is_empty());
|
||||
assert!(rule.source_ports.is_empty());
|
||||
assert!(rule.destination_ips.is_empty());
|
||||
assert!(rule.source_groups.is_empty());
|
||||
assert!(rule.destination_groups.is_empty());
|
||||
assert_eq!(rule.rate_limit, 0);
|
||||
assert_eq!(rule.burst_limit, 0);
|
||||
assert!(!rule.stateful);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acl_toml_group_can_omit_declares_or_members() {
|
||||
let declares_only = r#"
|
||||
[acl.acl_v1.group]
|
||||
|
||||
[[acl.acl_v1.group.declares]]
|
||||
group_name = "admin"
|
||||
group_secret = "admin-pw"
|
||||
"#;
|
||||
let config = TomlConfigLoader::new_from_str(declares_only).unwrap();
|
||||
let group = config.get_acl().unwrap().acl_v1.unwrap().group.unwrap();
|
||||
assert_eq!(group.declares.len(), 1);
|
||||
assert!(group.members.is_empty());
|
||||
|
||||
let members_only = r#"
|
||||
[acl.acl_v1.group]
|
||||
members = ["admin"]
|
||||
"#;
|
||||
let config = TomlConfigLoader::new_from_str(members_only).unwrap();
|
||||
let group = config.get_acl().unwrap().acl_v1.unwrap().group.unwrap();
|
||||
assert!(group.declares.is_empty());
|
||||
assert_eq!(group.members, vec!["admin"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_config_source_user_is_implicit() {
|
||||
let config = TomlConfigLoader::default();
|
||||
|
||||
+10
-19
@@ -73,6 +73,16 @@ pub async fn socket_addrs(
|
||||
.port()
|
||||
.or_else(default_port_number)
|
||||
.ok_or(Error::InvalidUrl(url.to_string()))?;
|
||||
// See https://github.com/EasyTier/EasyTier/pull/947
|
||||
// here is for compatibility with old version
|
||||
let port = match port {
|
||||
0 => match url.scheme() {
|
||||
"ws" => 80,
|
||||
"wss" => 443,
|
||||
_ => port,
|
||||
},
|
||||
_ => port,
|
||||
};
|
||||
|
||||
// if host is an ip address, return it directly
|
||||
match host {
|
||||
@@ -129,23 +139,4 @@ mod tests {
|
||||
assert_eq!(2, addrs.len(), "addrs: {:?}", addrs);
|
||||
println!("addrs2: {:?}", addrs);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn socket_addrs_preserves_explicit_zero_port() {
|
||||
let cases = [
|
||||
("ws://127.0.0.1:0", 80, 0),
|
||||
("wss://127.0.0.1:0", 443, 0),
|
||||
("ws://127.0.0.1", 80, 80),
|
||||
("wss://127.0.0.1", 443, 443),
|
||||
];
|
||||
|
||||
for (raw_url, default_port, expected_port) in cases {
|
||||
let url = url::Url::parse(raw_url).unwrap();
|
||||
let addrs = socket_addrs(&url, || Some(default_port)).await.unwrap();
|
||||
assert_eq!(
|
||||
addrs,
|
||||
vec![SocketAddr::from(([127, 0, 0, 1], expected_port))]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,12 +217,6 @@ pub struct GlobalCtx {
|
||||
|
||||
flags: ArcSwap<Flags>,
|
||||
|
||||
// Runtime/base advertised feature flags before config-owned fields are
|
||||
// overlaid by set_flags. Keep this separate so config patches do not erase
|
||||
// runtime state such as public-server role, IPv6 provider status, or the
|
||||
// non-whitelist avoid-relay preference.
|
||||
base_feature_flags: AtomicCell<PeerFeatureFlag>,
|
||||
|
||||
feature_flags: AtomicCell<PeerFeatureFlag>,
|
||||
|
||||
token_bucket_manager: TokenBucketManager,
|
||||
@@ -253,17 +247,8 @@ impl std::fmt::Debug for GlobalCtx {
|
||||
pub type ArcGlobalCtx = std::sync::Arc<GlobalCtx>;
|
||||
|
||||
impl GlobalCtx {
|
||||
fn apply_disable_relay_data_flag(
|
||||
flags: &Flags,
|
||||
mut feature_flags: PeerFeatureFlag,
|
||||
) -> PeerFeatureFlag {
|
||||
if flags.disable_relay_data {
|
||||
feature_flags.avoid_relay_data = true;
|
||||
}
|
||||
feature_flags
|
||||
}
|
||||
|
||||
fn derive_feature_flags(flags: &Flags, mut feature_flags: PeerFeatureFlag) -> PeerFeatureFlag {
|
||||
fn derive_feature_flags(flags: &Flags, current: Option<PeerFeatureFlag>) -> PeerFeatureFlag {
|
||||
let mut feature_flags = current.unwrap_or_default();
|
||||
feature_flags.kcp_input = !flags.disable_kcp_input;
|
||||
feature_flags.no_relay_kcp = flags.disable_relay_kcp;
|
||||
feature_flags.support_conn_list_sync = true;
|
||||
@@ -271,7 +256,7 @@ impl GlobalCtx {
|
||||
feature_flags.no_relay_quic = flags.disable_relay_quic;
|
||||
feature_flags.need_p2p = flags.need_p2p;
|
||||
feature_flags.disable_p2p = flags.disable_p2p;
|
||||
Self::apply_disable_relay_data_flag(flags, feature_flags)
|
||||
feature_flags
|
||||
}
|
||||
|
||||
pub fn new(config_fs: impl ConfigLoader + 'static) -> Self {
|
||||
@@ -300,8 +285,7 @@ impl GlobalCtx {
|
||||
|
||||
let flags = config_fs.get_flags();
|
||||
|
||||
let base_feature_flags = PeerFeatureFlag::default();
|
||||
let feature_flags = Self::derive_feature_flags(&flags, base_feature_flags);
|
||||
let feature_flags = Self::derive_feature_flags(&flags, None);
|
||||
|
||||
let credential_storage_path = config_fs.get_credential_file();
|
||||
let credential_manager = Arc::new(CredentialManager::new(credential_storage_path));
|
||||
@@ -334,8 +318,6 @@ impl GlobalCtx {
|
||||
|
||||
flags: ArcSwap::new(Arc::new(flags)),
|
||||
|
||||
base_feature_flags: AtomicCell::new(base_feature_flags),
|
||||
|
||||
feature_flags: AtomicCell::new(feature_flags),
|
||||
|
||||
token_bucket_manager: TokenBucketManager::new(),
|
||||
@@ -531,7 +513,7 @@ impl GlobalCtx {
|
||||
self.config.set_flags(flags.clone());
|
||||
self.feature_flags.store(Self::derive_feature_flags(
|
||||
&flags,
|
||||
self.base_feature_flags.load(),
|
||||
Some(self.feature_flags.load()),
|
||||
));
|
||||
self.flags.store(Arc::new(flags));
|
||||
}
|
||||
@@ -596,53 +578,8 @@ impl GlobalCtx {
|
||||
self.feature_flags.load()
|
||||
}
|
||||
|
||||
/// Replace the runtime/base advertised flags as a complete snapshot.
|
||||
///
|
||||
/// This is intended for foreign scoped contexts that inherit an already
|
||||
/// computed feature-flag snapshot from their parent. Most callers should use
|
||||
/// a narrower setter so they do not accidentally overwrite unrelated runtime
|
||||
/// state.
|
||||
pub fn set_base_advertised_feature_flags(&self, feature_flags: PeerFeatureFlag) {
|
||||
self.base_feature_flags.store(feature_flags);
|
||||
let flags = self.flags.load();
|
||||
self.feature_flags
|
||||
.store(Self::apply_disable_relay_data_flag(
|
||||
flags.as_ref(),
|
||||
feature_flags,
|
||||
));
|
||||
}
|
||||
|
||||
/// Set the avoid-relay preference that is independent of disable_relay_data.
|
||||
///
|
||||
/// disable_relay_data still forces the effective advertised flag to true,
|
||||
/// but this base preference is preserved when that config flag is toggled.
|
||||
pub fn set_avoid_relay_data_preference(&self, avoid_relay_data: bool) -> bool {
|
||||
let mut base_feature_flags = self.base_feature_flags.load();
|
||||
base_feature_flags.avoid_relay_data = avoid_relay_data;
|
||||
self.base_feature_flags.store(base_feature_flags);
|
||||
|
||||
let mut feature_flags = self.feature_flags.load();
|
||||
let previous = feature_flags.avoid_relay_data;
|
||||
feature_flags.avoid_relay_data = avoid_relay_data || self.flags.load().disable_relay_data;
|
||||
self.feature_flags.store(feature_flags);
|
||||
previous != feature_flags.avoid_relay_data
|
||||
}
|
||||
|
||||
/// Set the runtime IPv6-provider advertised bit without touching
|
||||
/// config-derived feature flags.
|
||||
pub fn set_ipv6_public_addr_provider_feature_flag(&self, enabled: bool) -> bool {
|
||||
let mut base_feature_flags = self.base_feature_flags.load();
|
||||
base_feature_flags.ipv6_public_addr_provider = enabled;
|
||||
self.base_feature_flags.store(base_feature_flags);
|
||||
|
||||
let mut feature_flags = self.feature_flags.load();
|
||||
if feature_flags.ipv6_public_addr_provider == enabled {
|
||||
return false;
|
||||
}
|
||||
|
||||
feature_flags.ipv6_public_addr_provider = enabled;
|
||||
self.feature_flags.store(feature_flags);
|
||||
true
|
||||
pub fn set_feature_flags(&self, flags: PeerFeatureFlag) {
|
||||
self.feature_flags.store(flags);
|
||||
}
|
||||
|
||||
pub fn token_bucket_manager(&self) -> &TokenBucketManager {
|
||||
@@ -859,7 +796,7 @@ pub mod tests {
|
||||
let mut feature_flags = global_ctx.get_feature_flags();
|
||||
feature_flags.avoid_relay_data = true;
|
||||
feature_flags.is_public_server = true;
|
||||
global_ctx.set_base_advertised_feature_flags(feature_flags);
|
||||
global_ctx.set_feature_flags(feature_flags);
|
||||
|
||||
let mut flags = global_ctx.get_flags().clone();
|
||||
flags.disable_kcp_input = true;
|
||||
@@ -883,83 +820,6 @@ pub mod tests {
|
||||
assert!(!feature_flags.ipv6_public_addr_provider);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_base_advertised_feature_flags_applies_current_values() {
|
||||
let config = TomlConfigLoader::default();
|
||||
let global_ctx = GlobalCtx::new(config);
|
||||
|
||||
let feature_flags = PeerFeatureFlag {
|
||||
kcp_input: false,
|
||||
no_relay_kcp: true,
|
||||
quic_input: false,
|
||||
no_relay_quic: true,
|
||||
is_public_server: true,
|
||||
..Default::default()
|
||||
};
|
||||
global_ctx.set_base_advertised_feature_flags(feature_flags);
|
||||
|
||||
assert_eq!(global_ctx.get_feature_flags(), feature_flags);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_base_advertised_feature_flags_keeps_disable_relay_data_effective() {
|
||||
let config = TomlConfigLoader::default();
|
||||
let global_ctx = GlobalCtx::new(config);
|
||||
|
||||
let mut flags = global_ctx.get_flags().clone();
|
||||
flags.disable_relay_data = true;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
let mut feature_flags = global_ctx.get_feature_flags();
|
||||
feature_flags.avoid_relay_data = false;
|
||||
feature_flags.is_public_server = true;
|
||||
global_ctx.set_base_advertised_feature_flags(feature_flags);
|
||||
|
||||
let advertised_feature_flags = global_ctx.get_feature_flags();
|
||||
assert!(advertised_feature_flags.avoid_relay_data);
|
||||
assert!(advertised_feature_flags.is_public_server);
|
||||
|
||||
let mut flags = global_ctx.get_flags().clone();
|
||||
flags.disable_relay_data = false;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
let advertised_feature_flags = global_ctx.get_feature_flags();
|
||||
assert!(!advertised_feature_flags.avoid_relay_data);
|
||||
assert!(advertised_feature_flags.is_public_server);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disable_relay_data_sets_avoid_relay_feature_flag() {
|
||||
let config = TomlConfigLoader::default();
|
||||
let global_ctx = GlobalCtx::new(config);
|
||||
|
||||
let mut flags = global_ctx.get_flags().clone();
|
||||
flags.disable_relay_data = true;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
assert!(global_ctx.get_feature_flags().avoid_relay_data);
|
||||
|
||||
let mut flags = global_ctx.get_flags().clone();
|
||||
flags.disable_relay_data = false;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
assert!(!global_ctx.get_feature_flags().avoid_relay_data);
|
||||
|
||||
global_ctx.set_avoid_relay_data_preference(true);
|
||||
|
||||
let mut flags = global_ctx.get_flags().clone();
|
||||
flags.disable_relay_data = true;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
assert!(global_ctx.get_feature_flags().avoid_relay_data);
|
||||
|
||||
let mut flags = global_ctx.get_flags().clone();
|
||||
flags.disable_relay_data = false;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
assert!(global_ctx.get_feature_flags().avoid_relay_data);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn should_deny_proxy_for_process_wide_rpc_port() {
|
||||
protected_port::clear_protected_tcp_ports_for_test();
|
||||
|
||||
+11
-193
@@ -58,21 +58,6 @@ fn parse_env_filter(default_level: Option<LevelFilter>) -> Result<EnvFilter, any
|
||||
.with_context(|| "failed to create env filter")
|
||||
}
|
||||
|
||||
fn parse_static_filter(level: LevelFilter) -> Result<EnvFilter, anyhow::Error> {
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(level.into())
|
||||
.parse("")
|
||||
.with_context(|| "failed to create static filter")
|
||||
}
|
||||
|
||||
fn parse_file_filter(level: LevelFilter) -> Result<EnvFilter, anyhow::Error> {
|
||||
if matches!(level, LevelFilter::OFF) {
|
||||
parse_static_filter(level)
|
||||
} else {
|
||||
parse_env_filter(Some(level))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_log(meta: &Metadata) -> bool {
|
||||
meta.target() == LOG_TARGET || meta.target().starts_with(&format!("{LOG_TARGET}::"))
|
||||
}
|
||||
@@ -180,17 +165,14 @@ fn file_layers(
|
||||
) -> anyhow::Result<(Vec<BoxLayer>, Option<NewFilterSender>)> {
|
||||
let mut layers = Vec::new();
|
||||
|
||||
let level = config
|
||||
.level
|
||||
.map(|s| s.parse().unwrap())
|
||||
.unwrap_or(LevelFilter::OFF);
|
||||
let level = config.level.map(|s| s.parse().unwrap());
|
||||
|
||||
if matches!(level, LevelFilter::OFF) && !reload {
|
||||
if matches!(level, Some(LevelFilter::OFF)) && !reload {
|
||||
return Ok((layers, None));
|
||||
}
|
||||
|
||||
let (file_filter, file_filter_reloader) =
|
||||
tracing_subscriber::reload::Layer::<_, Registry>::new(parse_file_filter(level)?);
|
||||
tracing_subscriber::reload::Layer::<_, Registry>::new(parse_env_filter(level)?);
|
||||
|
||||
let layer = |wrapper| {
|
||||
layer()
|
||||
@@ -236,7 +218,9 @@ fn file_layers(
|
||||
|
||||
// 初始化全局状态
|
||||
let _ = LOGGER_LEVEL_SENDER.set(std::sync::Mutex::new(tx.clone()));
|
||||
let _ = CURRENT_LOG_LEVEL.set(std::sync::Mutex::new(level.to_string()));
|
||||
if let Some(level) = level {
|
||||
let _ = CURRENT_LOG_LEVEL.set(std::sync::Mutex::new(level.to_string()));
|
||||
}
|
||||
|
||||
std::thread::spawn(move || {
|
||||
while let Ok(lf) = rx.recv() {
|
||||
@@ -248,7 +232,11 @@ fn file_layers(
|
||||
}
|
||||
};
|
||||
|
||||
let mut new_filter = match parse_file_filter(parsed_level) {
|
||||
let mut new_filter = match EnvFilter::builder()
|
||||
.with_default_directive(parsed_level.into())
|
||||
.from_env()
|
||||
.with_context(|| "failed to create file filter")
|
||||
{
|
||||
Ok(filter) => Some(filter),
|
||||
Err(e) => {
|
||||
error!("Failed to build new log filter for {:?}: {:?}", lf, e);
|
||||
@@ -280,36 +268,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::common::config::FileLoggerConfig;
|
||||
|
||||
const RUST_LOG: &str = "RUST_LOG";
|
||||
|
||||
struct EnvVarGuard {
|
||||
key: &'static str,
|
||||
previous: Option<std::ffi::OsString>,
|
||||
}
|
||||
|
||||
impl EnvVarGuard {
|
||||
fn set(key: &'static str, value: &str) -> Self {
|
||||
let previous = std::env::var_os(key);
|
||||
unsafe { std::env::set_var(key, value) };
|
||||
Self { key, previous }
|
||||
}
|
||||
|
||||
fn unset(key: &'static str) -> Self {
|
||||
let previous = std::env::var_os(key);
|
||||
unsafe { std::env::remove_var(key) };
|
||||
Self { key, previous }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvVarGuard {
|
||||
fn drop(&mut self) {
|
||||
match &self.previous {
|
||||
Some(value) => unsafe { std::env::set_var(self.key, value) },
|
||||
None => unsafe { std::env::remove_var(self.key) },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init() {
|
||||
let _ = Registry::default()
|
||||
@@ -318,147 +276,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_file_logger_level_is_off_without_reload() {
|
||||
let (layers, sender) = file_layers(FileLoggerConfig::default(), false).unwrap();
|
||||
|
||||
assert!(layers.is_empty());
|
||||
assert!(sender.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn default_file_logger_level_filters_info_with_reload() {
|
||||
let _guard = EnvVarGuard::set(RUST_LOG, "info");
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let log_file_name = "default-off-test.log".to_string();
|
||||
let log_path = temp_dir.path().join(&log_file_name);
|
||||
|
||||
let cfg = FileLoggerConfig {
|
||||
file: Some(log_file_name),
|
||||
dir: Some(temp_dir.path().to_string_lossy().to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (layers, _sender) = file_layers(cfg, true).unwrap();
|
||||
let marker = "default-file-logger-off-marker";
|
||||
let subscriber = Registry::default().with(layers);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
tracing::info!(target: LOG_TARGET, "{}", marker);
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
});
|
||||
|
||||
let content = std::fs::read_to_string(&log_path).unwrap_or_default();
|
||||
assert!(
|
||||
!content.contains(marker),
|
||||
"default file logger level should filter info logs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn file_logger_level_uses_env_filter_when_enabled() {
|
||||
let _guard = EnvVarGuard::set(RUST_LOG, "debug");
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let log_file_name = "env-filter-test.log".to_string();
|
||||
let log_path = temp_dir.path().join(&log_file_name);
|
||||
|
||||
let cfg = FileLoggerConfig {
|
||||
level: Some(LevelFilter::INFO.to_string()),
|
||||
file: Some(log_file_name),
|
||||
dir: Some(temp_dir.path().to_string_lossy().to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (layers, _sender) = file_layers(cfg, true).unwrap();
|
||||
let marker = "file-logger-env-filter-marker";
|
||||
let subscriber = Registry::default().with(layers);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
tracing::debug!(target: LOG_TARGET, "{}", marker);
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
});
|
||||
|
||||
let content = std::fs::read_to_string(&log_path).unwrap_or_default();
|
||||
assert!(
|
||||
content.contains(marker),
|
||||
"enabled file logger should use RUST_LOG directives"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn file_logger_reload_uses_env_filter_when_enabled() {
|
||||
let _guard = EnvVarGuard::set(RUST_LOG, "debug");
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let log_file_name = "reload-env-filter-test.log".to_string();
|
||||
let log_path = temp_dir.path().join(&log_file_name);
|
||||
|
||||
let cfg = FileLoggerConfig {
|
||||
file: Some(log_file_name),
|
||||
dir: Some(temp_dir.path().to_string_lossy().to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (layers, sender) = file_layers(cfg, true).unwrap();
|
||||
let sender = sender.expect("reload=true should return a sender");
|
||||
let marker = "file-logger-reload-env-filter-marker";
|
||||
let subscriber = Registry::default().with(layers);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
sender.send(LevelFilter::INFO.to_string()).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
|
||||
tracing::debug!(target: LOG_TARGET, "{}", marker);
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
});
|
||||
|
||||
let content = std::fs::read_to_string(&log_path).unwrap_or_default();
|
||||
assert!(
|
||||
content.contains(marker),
|
||||
"file logger enabled by reload should use RUST_LOG directives"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn file_logger_reload_off_ignores_env_filter() {
|
||||
let _guard = EnvVarGuard::set(RUST_LOG, "info");
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let log_file_name = "reload-off-test.log".to_string();
|
||||
let log_path = temp_dir.path().join(&log_file_name);
|
||||
|
||||
let cfg = FileLoggerConfig {
|
||||
level: Some(LevelFilter::INFO.to_string()),
|
||||
file: Some(log_file_name),
|
||||
dir: Some(temp_dir.path().to_string_lossy().to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (layers, sender) = file_layers(cfg, true).unwrap();
|
||||
let sender = sender.expect("reload=true should return a sender");
|
||||
let marker = "file-logger-reload-off-marker";
|
||||
let subscriber = Registry::default().with(layers);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
sender.send(LevelFilter::OFF.to_string()).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
|
||||
tracing::info!(target: LOG_TARGET, "{}", marker);
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
});
|
||||
|
||||
let content = std::fs::read_to_string(&log_path).unwrap_or_default();
|
||||
assert!(
|
||||
!content.contains(marker),
|
||||
"disabled file logger should ignore RUST_LOG directives"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn test_logger_reload() {
|
||||
let _guard = EnvVarGuard::unset(RUST_LOG);
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let log_file_name = "reload-test.log".to_string();
|
||||
let log_path = temp_dir.path().join(&log_file_name);
|
||||
|
||||
+10
-49
@@ -37,38 +37,6 @@ use crate::tunnel::IpScheme;
|
||||
#[cfg(feature = "jemalloc-prof")]
|
||||
use jemalloc_ctl::{Access as _, AsName as _, epoch, stats};
|
||||
|
||||
fn supported_compression_algorithms() -> &'static str {
|
||||
cfg_select! {
|
||||
all(feature = "zstd", feature = "lzo") => "none, zstd, lzo",
|
||||
feature = "zstd" => "none, zstd",
|
||||
feature = "lzo" => "none, lzo",
|
||||
_ => "none",
|
||||
}
|
||||
}
|
||||
|
||||
fn compression_help() -> String {
|
||||
t!(
|
||||
"core_clap.compression",
|
||||
algorithms = supported_compression_algorithms()
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn parse_compression_algorithm(compression: &str) -> anyhow::Result<CompressionAlgoPb> {
|
||||
match compression {
|
||||
"none" => Ok(CompressionAlgoPb::None),
|
||||
#[cfg(feature = "zstd")]
|
||||
"zstd" => Ok(CompressionAlgoPb::Zstd),
|
||||
#[cfg(feature = "lzo")]
|
||||
"lzo" => Ok(CompressionAlgoPb::Lzo),
|
||||
_ => anyhow::bail!(
|
||||
"unknown compression algorithm: {}, supported: {}",
|
||||
compression,
|
||||
supported_compression_algorithms()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
windows_service::define_windows_service!(ffi_service_main, win_service_main);
|
||||
|
||||
@@ -545,7 +513,7 @@ struct NetworkOptions {
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_COMPRESSION",
|
||||
help = compression_help(),
|
||||
help = t!("core_clap.compression").to_string(),
|
||||
)]
|
||||
compression: Option<String>,
|
||||
|
||||
@@ -1138,7 +1106,15 @@ impl NetworkOptions {
|
||||
f.need_p2p = self.need_p2p.unwrap_or(f.need_p2p);
|
||||
f.multi_thread = self.multi_thread.unwrap_or(f.multi_thread);
|
||||
if let Some(compression) = &self.compression {
|
||||
f.data_compress_algo = parse_compression_algorithm(compression)?.into();
|
||||
f.data_compress_algo = match compression.as_str() {
|
||||
"none" => CompressionAlgoPb::None,
|
||||
"zstd" => CompressionAlgoPb::Zstd,
|
||||
_ => panic!(
|
||||
"unknown compression algorithm: {}, supported: none, zstd",
|
||||
compression
|
||||
),
|
||||
}
|
||||
.into();
|
||||
}
|
||||
f.bind_device = self.bind_device.unwrap_or(f.bind_device);
|
||||
f.enable_kcp_proxy = self.enable_kcp_proxy.unwrap_or(f.enable_kcp_proxy);
|
||||
@@ -1651,21 +1627,6 @@ async fn validate_config(cli: &Cli) -> anyhow::Result<()> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_compression_help_uses_supported_algorithms() {
|
||||
assert!(compression_help().contains(supported_compression_algorithms()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_compression_algorithm_rejects_unknown() {
|
||||
let err = parse_compression_algorithm("snappy")
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
|
||||
assert!(err.contains("snappy"));
|
||||
assert!(err.contains(supported_compression_algorithms()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_listeners() {
|
||||
type IpSchemeMap = fn(&IpScheme) -> String;
|
||||
|
||||
@@ -74,7 +74,7 @@ use easytier::{
|
||||
common::{NatType, PortForwardConfigPb, SocketType},
|
||||
peer_rpc::{GetGlobalPeerMapRequest, PeerCenterRpc, PeerCenterRpcClientFactory},
|
||||
rpc_impl::standalone::StandAloneClient,
|
||||
rpc_types::{controller::BaseController, error::Error as RpcError},
|
||||
rpc_types::controller::BaseController,
|
||||
},
|
||||
tunnel::{TunnelScheme, tcp::TcpTunnelConnector},
|
||||
utils::{PeerRoutePair, string::cost_to_str},
|
||||
@@ -526,40 +526,6 @@ type LocalBoxFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, Error>> + 'a>
|
||||
type ForeignNetworkMap = BTreeMap<String, ForeignNetworkEntryPb>;
|
||||
type GlobalForeignNetworkMap = BTreeMap<u32, list_global_foreign_network_response::ForeignNetworks>;
|
||||
|
||||
fn is_missing_web_client_service(error: &RpcError) -> bool {
|
||||
matches!(
|
||||
error,
|
||||
RpcError::InvalidServiceKey(service_name, _)
|
||||
if service_name.trim_matches('"') == "WebClientService"
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn missing_web_client_service_matches_raw_service_name() {
|
||||
let error = RpcError::InvalidServiceKey("WebClientService".to_string(), "".to_string());
|
||||
|
||||
assert!(is_missing_web_client_service(&error));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_web_client_service_matches_serialized_service_name() {
|
||||
let error = RpcError::InvalidServiceKey("\"WebClientService\"".to_string(), "".to_string());
|
||||
|
||||
assert!(is_missing_web_client_service(&error));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_web_client_service_rejects_other_services() {
|
||||
let error = RpcError::InvalidServiceKey("PeerManageRpc".to_string(), "".to_string());
|
||||
|
||||
assert!(!is_missing_web_client_service(&error));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct PeerListData {
|
||||
node_info: NodeInfo,
|
||||
@@ -633,15 +599,9 @@ impl<'a> CommandHandler<'a> {
|
||||
}
|
||||
|
||||
let client = self.get_manage_client().await?;
|
||||
let list_response = match client
|
||||
let inst_ids = client
|
||||
.list_network_instance(BaseController::default(), ListNetworkInstanceRequest {})
|
||||
.await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(error) if is_missing_web_client_service(&error) => return Ok(None),
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
let inst_ids = list_response
|
||||
.await?
|
||||
.inst_ids
|
||||
.into_iter()
|
||||
.map(uuid::Uuid::from)
|
||||
|
||||
@@ -340,11 +340,6 @@ impl InstanceConfigPatcher {
|
||||
global_ctx.set_ipv6(Some(ipv6.into()));
|
||||
global_ctx.config.set_ipv6(Some(ipv6.into()));
|
||||
}
|
||||
if let Some(disable_relay_data) = patch.disable_relay_data {
|
||||
let mut flags = global_ctx.get_flags();
|
||||
flags.disable_relay_data = disable_relay_data;
|
||||
global_ctx.set_flags(flags);
|
||||
}
|
||||
if let Some(enabled) = patch.ipv6_public_addr_provider {
|
||||
global_ctx.config.set_ipv6_public_addr_provider(enabled);
|
||||
provider_config_changed = true;
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::{
|
||||
|
||||
pub fn create_listener_by_url(
|
||||
l: &url::Url,
|
||||
_global_ctx: ArcGlobalCtx,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
) -> Result<Box<dyn TunnelListener>, Error> {
|
||||
Ok(match l.try_into()? {
|
||||
TunnelScheme::Ip(scheme) => match scheme {
|
||||
@@ -34,7 +34,7 @@ pub fn create_listener_by_url(
|
||||
#[cfg(feature = "wireguard")]
|
||||
IpScheme::Wg => {
|
||||
use crate::tunnel::wireguard::{WgConfig, WgTunnelListener};
|
||||
let nid = _global_ctx.get_network_identity();
|
||||
let nid = global_ctx.get_network_identity();
|
||||
let wg_config = WgConfig::new_from_network_identity(
|
||||
&nid.network_name,
|
||||
&nid.network_secret.unwrap_or_default(),
|
||||
@@ -43,7 +43,7 @@ pub fn create_listener_by_url(
|
||||
}
|
||||
#[cfg(feature = "quic")]
|
||||
IpScheme::Quic => {
|
||||
tunnel::quic::QuicTunnelListener::new(l.clone(), _global_ctx.clone()).boxed()
|
||||
tunnel::quic::QuicTunnelListener::new(l.clone(), global_ctx.clone()).boxed()
|
||||
}
|
||||
#[cfg(feature = "websocket")]
|
||||
IpScheme::Ws | IpScheme::Wss => {
|
||||
|
||||
@@ -361,8 +361,16 @@ fn apply_public_ipv6_provider_runtime_state(
|
||||
let prefix_changed = global_ctx.set_advertised_ipv6_public_addr_prefix(next_prefix);
|
||||
|
||||
let next_provider_enabled = matches!(state, PublicIpv6ProviderRuntimeState::Active(_));
|
||||
let feature_changed =
|
||||
global_ctx.set_ipv6_public_addr_provider_feature_flag(next_provider_enabled);
|
||||
let feature_changed = {
|
||||
let mut feature_flags = global_ctx.get_feature_flags();
|
||||
if feature_flags.ipv6_public_addr_provider == next_provider_enabled {
|
||||
false
|
||||
} else {
|
||||
feature_flags.ipv6_public_addr_provider = next_provider_enabled;
|
||||
global_ctx.set_feature_flags(feature_flags);
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
prefix_changed || feature_changed
|
||||
}
|
||||
|
||||
@@ -816,10 +816,6 @@ impl NetworkConfig {
|
||||
flags.disable_upnp = disable_upnp;
|
||||
}
|
||||
|
||||
if let Some(disable_relay_data) = self.disable_relay_data {
|
||||
flags.disable_relay_data = disable_relay_data;
|
||||
}
|
||||
|
||||
if let Some(disable_sym_hole_punching) = self.disable_sym_hole_punching {
|
||||
flags.disable_sym_hole_punching = disable_sym_hole_punching;
|
||||
}
|
||||
@@ -994,7 +990,6 @@ impl NetworkConfig {
|
||||
result.disable_tcp_hole_punching = Some(flags.disable_tcp_hole_punching);
|
||||
result.disable_udp_hole_punching = Some(flags.disable_udp_hole_punching);
|
||||
result.disable_upnp = Some(flags.disable_upnp);
|
||||
result.disable_relay_data = Some(flags.disable_relay_data);
|
||||
result.disable_sym_hole_punching = Some(flags.disable_sym_hole_punching);
|
||||
result.enable_magic_dns = Some(flags.accept_dns);
|
||||
result.mtu = Some(flags.mtu as i32);
|
||||
|
||||
@@ -65,7 +65,7 @@ impl PeerCenterBase {
|
||||
return Err(Error::Shutdown);
|
||||
};
|
||||
rpc_mgr.rpc_server().registry().register(
|
||||
PeerCenterRpcServer::new(PeerCenterServer::new()),
|
||||
PeerCenterRpcServer::new(PeerCenterServer::new(self.peer_mgr.my_peer_id())),
|
||||
&self.peer_mgr.get_global_ctx().get_network_name(),
|
||||
);
|
||||
Ok(())
|
||||
@@ -486,6 +486,7 @@ impl PeerCenterPeerManagerTrait for PeerMapWithPeerRpcManager {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
peer_center::server::get_global_data,
|
||||
peers::tests::{connect_peer_manager, create_mock_peer_manager, wait_route_appear},
|
||||
tunnel::common::tests::wait_for_condition,
|
||||
};
|
||||
@@ -514,6 +515,25 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let center_peer = PeerCenterBase::select_center_peer(&peer_mgr_a)
|
||||
.await
|
||||
.unwrap();
|
||||
let center_data = get_global_data(center_peer);
|
||||
|
||||
// wait center_data has 3 records for 10 seconds
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
if center_data.global_peer_map.len() == 4 {
|
||||
println!("center data {:#?}", center_data.global_peer_map);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
Duration::from_secs(20),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut digest = None;
|
||||
for pc in peer_centers.iter() {
|
||||
let rpc_service = pc.get_rpc_service();
|
||||
@@ -558,5 +578,8 @@ mod tests {
|
||||
route_cost.end_update();
|
||||
assert!(!route_cost.need_update());
|
||||
}
|
||||
|
||||
let global_digest = get_global_data(center_peer).digest.load();
|
||||
assert_eq!(digest.as_ref().unwrap(), &global_digest);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::{
|
||||
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use dashmap::DashMap;
|
||||
use once_cell::sync::Lazy;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use crate::{
|
||||
@@ -34,41 +35,50 @@ pub(crate) struct PeerCenterInfoEntry {
|
||||
update_time: std::time::Instant,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct PeerCenterServerData {
|
||||
global_peer_map: DashMap<SrcDstPeerPair, PeerCenterInfoEntry>,
|
||||
peer_report_time: DashMap<PeerId, std::time::Instant>,
|
||||
digest: AtomicCell<Digest>,
|
||||
#[derive(Default)]
|
||||
pub(crate) struct PeerCenterServerGlobalData {
|
||||
pub(crate) global_peer_map: DashMap<SrcDstPeerPair, PeerCenterInfoEntry>,
|
||||
pub(crate) peer_report_time: DashMap<PeerId, std::time::Instant>,
|
||||
pub(crate) digest: AtomicCell<Digest>,
|
||||
}
|
||||
|
||||
// a global unique instance for PeerCenterServer
|
||||
pub(crate) static GLOBAL_DATA: Lazy<DashMap<PeerId, Arc<PeerCenterServerGlobalData>>> =
|
||||
Lazy::new(DashMap::new);
|
||||
|
||||
pub(crate) fn get_global_data(node_id: PeerId) -> Arc<PeerCenterServerGlobalData> {
|
||||
GLOBAL_DATA
|
||||
.entry(node_id)
|
||||
.or_insert_with(|| Arc::new(PeerCenterServerGlobalData::default()))
|
||||
.value()
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PeerCenterServer {
|
||||
data: Arc<PeerCenterServerData>,
|
||||
// every peer has its own server, so use per-struct dash map is ok.
|
||||
my_node_id: PeerId,
|
||||
tasks: Arc<JoinSet<()>>,
|
||||
}
|
||||
|
||||
impl PeerCenterServer {
|
||||
pub fn new() -> Self {
|
||||
let data = Arc::new(PeerCenterServerData::default());
|
||||
let weak_data = Arc::downgrade(&data);
|
||||
pub fn new(my_node_id: PeerId) -> Self {
|
||||
let mut tasks = JoinSet::new();
|
||||
tasks.spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
|
||||
let Some(data) = weak_data.upgrade() else {
|
||||
break;
|
||||
};
|
||||
PeerCenterServer::clean_outdated_peer_data(&data).await;
|
||||
PeerCenterServer::clean_outdated_peer(my_node_id).await;
|
||||
}
|
||||
});
|
||||
|
||||
PeerCenterServer {
|
||||
data,
|
||||
my_node_id,
|
||||
tasks: Arc::new(tasks),
|
||||
}
|
||||
}
|
||||
|
||||
async fn clean_outdated_peer_data(data: &PeerCenterServerData) {
|
||||
async fn clean_outdated_peer(my_node_id: PeerId) {
|
||||
let data = get_global_data(my_node_id);
|
||||
data.peer_report_time.retain(|_, v| {
|
||||
std::time::Instant::now().duration_since(*v) < std::time::Duration::from_secs(180)
|
||||
});
|
||||
@@ -78,7 +88,8 @@ impl PeerCenterServer {
|
||||
});
|
||||
}
|
||||
|
||||
fn calc_global_digest_data(data: &PeerCenterServerData) -> Digest {
|
||||
fn calc_global_digest(my_node_id: PeerId) -> Digest {
|
||||
let data = get_global_data(my_node_id);
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
data.global_peer_map
|
||||
.iter()
|
||||
@@ -106,7 +117,7 @@ impl PeerCenterRpc for PeerCenterServer {
|
||||
|
||||
tracing::debug!("receive report_peers");
|
||||
|
||||
let data = &self.data;
|
||||
let data = get_global_data(self.my_node_id);
|
||||
data.peer_report_time
|
||||
.insert(my_peer_id, std::time::Instant::now());
|
||||
|
||||
@@ -123,7 +134,7 @@ impl PeerCenterRpc for PeerCenterServer {
|
||||
}
|
||||
|
||||
data.digest
|
||||
.store(PeerCenterServer::calc_global_digest_data(data));
|
||||
.store(PeerCenterServer::calc_global_digest(self.my_node_id));
|
||||
|
||||
Ok(ReportPeersResponse::default())
|
||||
}
|
||||
@@ -136,7 +147,7 @@ impl PeerCenterRpc for PeerCenterServer {
|
||||
) -> Result<GetGlobalPeerMapResponse, rpc_types::error::Error> {
|
||||
let digest = req.digest;
|
||||
|
||||
let data = &self.data;
|
||||
let data = get_global_data(self.my_node_id);
|
||||
if digest == data.digest.load() && digest != 0 {
|
||||
return Ok(GetGlobalPeerMapResponse::default());
|
||||
}
|
||||
@@ -160,80 +171,3 @@ impl PeerCenterRpc for PeerCenterServer {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn server_clones_share_instance_data() {
|
||||
let server = PeerCenterServer::new();
|
||||
let server_clone = server.clone();
|
||||
|
||||
let mut peers = PeerInfoForGlobalMap::default();
|
||||
peers
|
||||
.direct_peers
|
||||
.insert(100, DirectConnectedPeerInfo { latency_ms: 3 });
|
||||
|
||||
server
|
||||
.report_peers(
|
||||
BaseController::default(),
|
||||
ReportPeersRequest {
|
||||
my_peer_id: 99,
|
||||
peer_infos: Some(peers),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let resp = server_clone
|
||||
.get_global_peer_map(
|
||||
BaseController::default(),
|
||||
GetGlobalPeerMapRequest { digest: 0 },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(1, resp.global_peer_map.len());
|
||||
assert!(resp.global_peer_map[&99].direct_peers.contains_key(&100));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn independent_server_instances_do_not_share_data() {
|
||||
let server_a = PeerCenterServer::new();
|
||||
let server_b = PeerCenterServer::new();
|
||||
|
||||
let mut peers = PeerInfoForGlobalMap::default();
|
||||
peers
|
||||
.direct_peers
|
||||
.insert(101, DirectConnectedPeerInfo { latency_ms: 5 });
|
||||
|
||||
server_a
|
||||
.report_peers(
|
||||
BaseController::default(),
|
||||
ReportPeersRequest {
|
||||
my_peer_id: 100,
|
||||
peer_infos: Some(peers),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let resp_a = server_a
|
||||
.get_global_peer_map(
|
||||
BaseController::default(),
|
||||
GetGlobalPeerMapRequest { digest: 0 },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(1, resp_a.global_peer_map.len());
|
||||
|
||||
let resp_b = server_b
|
||||
.get_global_peer_map(
|
||||
BaseController::default(),
|
||||
GetGlobalPeerMapRequest { digest: 0 },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(resp_b.global_peer_map.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ use super::{
|
||||
route_trait::NextHopPolicy,
|
||||
traffic_metrics::{
|
||||
InstanceLabelKind, LogicalTrafficMetrics, TrafficKind, TrafficMetricRecorder,
|
||||
is_relay_data_packet_type, route_peer_info_instance_id, traffic_kind,
|
||||
route_peer_info_instance_id, traffic_kind,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -69,16 +69,11 @@ pub trait GlobalForeignNetworkAccessor: Send + Sync + 'static {
|
||||
struct ForeignNetworkEntry {
|
||||
my_peer_id: PeerId,
|
||||
|
||||
// Node-global runtime flags, such as disable_relay_data, live on the parent
|
||||
// context. The foreign context is scoped to the foreign network's OSPF view.
|
||||
parent_global_ctx: ArcGlobalCtx,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
network: NetworkIdentity,
|
||||
peer_map: Arc<PeerMap>,
|
||||
relay_peer_map: Arc<RelayPeerMap>,
|
||||
peer_session_store: Arc<PeerSessionStore>,
|
||||
// Static per-network permission from the whitelist check. disable_relay_data
|
||||
// is the node-wide runtime override layered on top of this value.
|
||||
relay_data: bool,
|
||||
pm_packet_sender: Mutex<Option<PacketRecvChan>>,
|
||||
|
||||
@@ -87,7 +82,7 @@ struct ForeignNetworkEntry {
|
||||
|
||||
packet_recv: Mutex<Option<PacketRecvChanReceiver>>,
|
||||
|
||||
bps_limiter: Option<Arc<TokenBucket>>,
|
||||
bps_limiter: Arc<TokenBucket>,
|
||||
|
||||
peer_center: Arc<PeerCenterInstance>,
|
||||
|
||||
@@ -191,16 +186,14 @@ impl ForeignNetworkEntry {
|
||||
);
|
||||
|
||||
let relay_bps_limit = global_ctx.config.get_flags().foreign_relay_bps_limit;
|
||||
let bps_limiter = (relay_bps_limit != u64::MAX).then(|| {
|
||||
let limiter_config = LimiterConfig {
|
||||
burst_rate: None,
|
||||
bps: Some(relay_bps_limit),
|
||||
fill_duration_ms: None,
|
||||
};
|
||||
global_ctx
|
||||
.token_bucket_manager()
|
||||
.get_or_create(&network.network_name, limiter_config.into())
|
||||
});
|
||||
let limiter_config = LimiterConfig {
|
||||
burst_rate: None,
|
||||
bps: Some(relay_bps_limit),
|
||||
fill_duration_ms: None,
|
||||
};
|
||||
let bps_limiter = global_ctx
|
||||
.token_bucket_manager()
|
||||
.get_or_create(&network.network_name, limiter_config.into());
|
||||
|
||||
let peer_center = Arc::new(PeerCenterInstance::new(Arc::new(
|
||||
PeerMapWithPeerRpcManager {
|
||||
@@ -212,7 +205,6 @@ impl ForeignNetworkEntry {
|
||||
Self {
|
||||
my_peer_id,
|
||||
|
||||
parent_global_ctx: global_ctx.clone(),
|
||||
global_ctx: foreign_global_ctx,
|
||||
network,
|
||||
peer_map,
|
||||
@@ -239,27 +231,6 @@ impl ForeignNetworkEntry {
|
||||
}
|
||||
}
|
||||
|
||||
fn desired_avoid_relay_data_feature_flag(
|
||||
parent_global_ctx: &ArcGlobalCtx,
|
||||
relay_data: bool,
|
||||
) -> bool {
|
||||
!relay_data || parent_global_ctx.get_feature_flags().avoid_relay_data
|
||||
}
|
||||
|
||||
fn sync_parent_relay_data_feature_flag(
|
||||
parent_global_ctx: &ArcGlobalCtx,
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
relay_data: bool,
|
||||
) -> bool {
|
||||
let avoid_relay_data =
|
||||
Self::desired_avoid_relay_data_feature_flag(parent_global_ctx, relay_data);
|
||||
if global_ctx.get_feature_flags().avoid_relay_data == avoid_relay_data {
|
||||
return false;
|
||||
}
|
||||
|
||||
global_ctx.set_avoid_relay_data_preference(avoid_relay_data)
|
||||
}
|
||||
|
||||
fn build_foreign_global_ctx(
|
||||
network: &NetworkIdentity,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
@@ -287,9 +258,10 @@ impl ForeignNetworkEntry {
|
||||
|
||||
let mut feature_flag = global_ctx.get_feature_flags();
|
||||
feature_flag.is_public_server = true;
|
||||
feature_flag.avoid_relay_data =
|
||||
Self::desired_avoid_relay_data_feature_flag(&global_ctx, relay_data);
|
||||
foreign_global_ctx.set_base_advertised_feature_flags(feature_flag);
|
||||
if !relay_data {
|
||||
feature_flag.avoid_relay_data = true;
|
||||
}
|
||||
foreign_global_ctx.set_feature_flags(feature_flag);
|
||||
|
||||
for u in global_ctx.get_running_listeners().into_iter() {
|
||||
foreign_global_ctx.add_running_listener(u);
|
||||
@@ -440,7 +412,6 @@ impl ForeignNetworkEntry {
|
||||
let peer_map = self.peer_map.clone();
|
||||
let relay_peer_map = self.relay_peer_map.clone();
|
||||
let traffic_metrics = self.traffic_metrics.clone();
|
||||
let parent_global_ctx = self.parent_global_ctx.clone();
|
||||
let relay_data = self.relay_data;
|
||||
let pm_sender = self.pm_packet_sender.lock().await.take().unwrap();
|
||||
let network_name = self.network.network_name.clone();
|
||||
@@ -526,21 +497,14 @@ impl ForeignNetworkEntry {
|
||||
"ignore packet in foreign network"
|
||||
);
|
||||
} else {
|
||||
if is_relay_data_packet_type(packet_type) {
|
||||
let disable_relay_data = parent_global_ctx.flags_arc().disable_relay_data;
|
||||
if !relay_data || disable_relay_data {
|
||||
tracing::debug!(
|
||||
?from_peer_id,
|
||||
?to_peer_id,
|
||||
packet_type,
|
||||
disable_relay_data,
|
||||
"drop foreign network relay data"
|
||||
);
|
||||
if packet_type == PacketType::Data as u8
|
||||
|| packet_type == PacketType::KcpSrc as u8
|
||||
|| packet_type == PacketType::KcpDst as u8
|
||||
{
|
||||
if !relay_data {
|
||||
continue;
|
||||
}
|
||||
if let Some(bps_limiter) = bps_limiter.as_ref()
|
||||
&& !bps_limiter.try_consume(len.into())
|
||||
{
|
||||
if !bps_limiter.try_consume(len.into()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -625,31 +589,10 @@ impl ForeignNetworkEntry {
|
||||
});
|
||||
}
|
||||
|
||||
async fn run_parent_feature_flag_sync_routine(&self) {
|
||||
let parent_global_ctx = self.parent_global_ctx.clone();
|
||||
let global_ctx = self.global_ctx.clone();
|
||||
let relay_data = self.relay_data;
|
||||
self.tasks.lock().await.spawn(async move {
|
||||
let mut parent_events = parent_global_ctx.subscribe();
|
||||
loop {
|
||||
ForeignNetworkEntry::sync_parent_relay_data_feature_flag(
|
||||
&parent_global_ctx,
|
||||
&global_ctx,
|
||||
relay_data,
|
||||
);
|
||||
|
||||
if parent_events.recv().await.is_err() {
|
||||
parent_events = parent_global_ctx.subscribe();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn prepare(&self, accessor: Box<dyn GlobalForeignNetworkAccessor>) {
|
||||
self.prepare_route(accessor).await;
|
||||
self.start_packet_recv().await;
|
||||
self.run_relay_session_gc_routine().await;
|
||||
self.run_parent_feature_flag_sync_routine().await;
|
||||
self.peer_rpc.run();
|
||||
self.peer_center.init().await;
|
||||
}
|
||||
@@ -717,7 +660,6 @@ impl ForeignNetworkManagerData {
|
||||
fn remove_network(&self, network_name: &String) {
|
||||
let _l = self.lock.lock().unwrap();
|
||||
if let Some(old) = self.network_peer_maps.remove(network_name) {
|
||||
old.1.traffic_metrics.clear_peer_cache();
|
||||
let to_remove_peers = old.1.peer_map.list_peers();
|
||||
for p in to_remove_peers {
|
||||
self.peer_network_map.remove_if(&p, |_, v| {
|
||||
@@ -727,9 +669,6 @@ impl ForeignNetworkManagerData {
|
||||
}
|
||||
}
|
||||
self.network_peer_last_update.remove(network_name);
|
||||
shrink_dashmap(&self.peer_network_map, None);
|
||||
shrink_dashmap(&self.network_peer_maps, None);
|
||||
shrink_dashmap(&self.network_peer_last_update, None);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -1002,14 +941,12 @@ impl ForeignNetworkManager {
|
||||
async fn start_event_handler(&self, entry: &ForeignNetworkEntry) {
|
||||
let data = self.data.clone();
|
||||
let network_name = entry.network.network_name.clone();
|
||||
let traffic_metrics = entry.traffic_metrics.clone();
|
||||
let mut s = entry.global_ctx.subscribe();
|
||||
self.tasks.lock().unwrap().spawn(async move {
|
||||
while let Ok(e) = s.recv().await {
|
||||
match &e {
|
||||
GlobalCtxEvent::PeerRemoved(peer_id) => {
|
||||
tracing::info!(?e, "remove peer from foreign network manager");
|
||||
traffic_metrics.remove_peer(*peer_id);
|
||||
data.remove_peer(*peer_id, &network_name);
|
||||
data.network_peer_last_update
|
||||
.insert(network_name.clone(), SystemTime::now());
|
||||
@@ -1028,7 +965,6 @@ impl ForeignNetworkManager {
|
||||
}
|
||||
// if lagged or recv done just remove the network
|
||||
tracing::error!("global event handler at foreign network manager exit");
|
||||
traffic_metrics.clear_peer_cache();
|
||||
data.remove_network(&network_name);
|
||||
});
|
||||
}
|
||||
@@ -1461,92 +1397,6 @@ pub mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disable_relay_data_blocks_foreign_network_transit_data() {
|
||||
let pm_center = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
||||
let pma_net1 = create_mock_peer_manager_for_foreign_network("net1").await;
|
||||
let pmb_net1 = create_mock_peer_manager_for_foreign_network("net1").await;
|
||||
|
||||
connect_peer_manager(pma_net1.clone(), pm_center.clone()).await;
|
||||
connect_peer_manager(pmb_net1.clone(), pm_center.clone()).await;
|
||||
wait_route_appear(pma_net1.clone(), pmb_net1.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut flags = pm_center.get_global_ctx().get_flags();
|
||||
flags.disable_relay_data = true;
|
||||
pm_center.get_global_ctx().set_flags(flags);
|
||||
pm_center
|
||||
.get_global_ctx()
|
||||
.issue_event(GlobalCtxEvent::ConfigPatched(Default::default()));
|
||||
|
||||
let center_peer_id = pm_center
|
||||
.get_foreign_network_manager()
|
||||
.get_network_peer_id("net1")
|
||||
.unwrap();
|
||||
wait_for_condition(
|
||||
|| {
|
||||
let pma_net1 = pma_net1.clone();
|
||||
async move {
|
||||
pma_net1.list_routes().await.iter().any(|route| {
|
||||
route.peer_id == center_peer_id
|
||||
&& route
|
||||
.feature_flag
|
||||
.as_ref()
|
||||
.map(|flag| flag.avoid_relay_data)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
},
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await;
|
||||
|
||||
let network_labels =
|
||||
LabelSet::new().with_label_type(LabelType::NetworkName("net1".to_string()));
|
||||
let forwarded_bytes_before = metric_value(
|
||||
&pm_center,
|
||||
MetricName::TrafficBytesForwarded,
|
||||
network_labels.clone(),
|
||||
);
|
||||
let forwarded_packets_before = metric_value(
|
||||
&pm_center,
|
||||
MetricName::TrafficPacketsForwarded,
|
||||
network_labels.clone(),
|
||||
);
|
||||
|
||||
let mut transit_pkt = ZCPacket::new_with_payload(b"foreign-transit-disabled");
|
||||
transit_pkt.fill_peer_manager_hdr(
|
||||
pma_net1.my_peer_id(),
|
||||
pmb_net1.my_peer_id(),
|
||||
PacketType::Data as u8,
|
||||
);
|
||||
pma_net1
|
||||
.get_foreign_network_client()
|
||||
.send_msg(transit_pkt, center_peer_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
assert_eq!(
|
||||
metric_value(
|
||||
&pm_center,
|
||||
MetricName::TrafficBytesForwarded,
|
||||
network_labels.clone()
|
||||
),
|
||||
forwarded_bytes_before
|
||||
);
|
||||
assert_eq!(
|
||||
metric_value(
|
||||
&pm_center,
|
||||
MetricName::TrafficPacketsForwarded,
|
||||
network_labels
|
||||
),
|
||||
forwarded_packets_before
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn foreign_network_transit_control_forwarding_records_control_forwarded_metrics() {
|
||||
let pm_center = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
||||
@@ -1559,10 +1409,6 @@ pub mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut flags = pm_center.get_global_ctx().get_flags();
|
||||
flags.disable_relay_data = true;
|
||||
pm_center.get_global_ctx().set_flags(flags);
|
||||
|
||||
let center_peer_id = pm_center
|
||||
.get_foreign_network_manager()
|
||||
.get_network_peer_id("net1")
|
||||
@@ -1615,58 +1461,6 @@ pub mod tests {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn foreign_network_peer_removed_clears_traffic_metric_peer_cache() {
|
||||
let pm_center = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
||||
let pma_net1 = create_mock_peer_manager_for_foreign_network("net1").await;
|
||||
|
||||
connect_peer_manager(pma_net1.clone(), pm_center.clone()).await;
|
||||
wait_for_condition(
|
||||
|| {
|
||||
let pm_center = pm_center.clone();
|
||||
async move {
|
||||
pm_center
|
||||
.get_foreign_network_manager()
|
||||
.get_network_peer_id("net1")
|
||||
.is_some()
|
||||
}
|
||||
},
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await;
|
||||
|
||||
let entry = pm_center
|
||||
.get_foreign_network_manager()
|
||||
.data
|
||||
.get_network_entry("net1")
|
||||
.unwrap();
|
||||
|
||||
entry
|
||||
.traffic_metrics
|
||||
.record_rx(pma_net1.my_peer_id(), PacketType::Data as u8, 128)
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
entry
|
||||
.traffic_metrics
|
||||
.contains_peer_cache(pma_net1.my_peer_id())
|
||||
);
|
||||
|
||||
entry
|
||||
.global_ctx
|
||||
.issue_event(GlobalCtxEvent::PeerRemoved(pma_net1.my_peer_id()));
|
||||
|
||||
wait_for_condition(
|
||||
|| {
|
||||
let entry = entry.clone();
|
||||
let peer_id = pma_net1.my_peer_id();
|
||||
async move { !entry.traffic_metrics.contains_peer_cache(peer_id) }
|
||||
},
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn foreign_network_encapsulated_forwarding_records_tx_metrics() {
|
||||
set_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, 1);
|
||||
@@ -1863,81 +1657,6 @@ pub mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn foreign_entry_feature_flag_tracks_parent_disable_relay_data_toggle() {
|
||||
let global_ctx = get_mock_global_ctx_with_network(Some(NetworkIdentity::new(
|
||||
"__access__".to_string(),
|
||||
"access_secret".to_string(),
|
||||
)));
|
||||
let foreign_network = NetworkIdentity::new("net1".to_string(), "net1_secret".to_string());
|
||||
let (pm_packet_sender, _pm_packet_recv) = create_packet_recv_chan();
|
||||
let entry = ForeignNetworkEntry::new(
|
||||
foreign_network,
|
||||
1,
|
||||
global_ctx.clone(),
|
||||
true,
|
||||
Arc::new(PeerSessionStore::new()),
|
||||
pm_packet_sender,
|
||||
);
|
||||
assert!(!entry.global_ctx.get_feature_flags().avoid_relay_data);
|
||||
|
||||
entry.run_parent_feature_flag_sync_routine().await;
|
||||
|
||||
let mut flags = global_ctx.get_flags();
|
||||
flags.disable_relay_data = true;
|
||||
global_ctx.set_flags(flags);
|
||||
global_ctx.issue_event(GlobalCtxEvent::ConfigPatched(Default::default()));
|
||||
|
||||
wait_for_condition(
|
||||
|| async { entry.global_ctx.get_feature_flags().avoid_relay_data },
|
||||
Duration::from_secs(2),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut flags = global_ctx.get_flags();
|
||||
flags.disable_relay_data = false;
|
||||
global_ctx.set_flags(flags);
|
||||
global_ctx.issue_event(GlobalCtxEvent::ConfigPatched(Default::default()));
|
||||
|
||||
wait_for_condition(
|
||||
|| async { !entry.global_ctx.get_feature_flags().avoid_relay_data },
|
||||
Duration::from_secs(2),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn foreign_entry_without_relay_data_keeps_avoid_feature_flag() {
|
||||
let global_ctx = get_mock_global_ctx_with_network(Some(NetworkIdentity::new(
|
||||
"__access__".to_string(),
|
||||
"access_secret".to_string(),
|
||||
)));
|
||||
let foreign_network = NetworkIdentity::new("net1".to_string(), "net1_secret".to_string());
|
||||
let (pm_packet_sender, _pm_packet_recv) = create_packet_recv_chan();
|
||||
let entry = ForeignNetworkEntry::new(
|
||||
foreign_network,
|
||||
1,
|
||||
global_ctx.clone(),
|
||||
false,
|
||||
Arc::new(PeerSessionStore::new()),
|
||||
pm_packet_sender,
|
||||
);
|
||||
|
||||
assert!(entry.global_ctx.get_feature_flags().avoid_relay_data);
|
||||
|
||||
let mut flags = global_ctx.get_flags();
|
||||
flags.disable_relay_data = false;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
ForeignNetworkEntry::sync_parent_relay_data_feature_flag(
|
||||
&global_ctx,
|
||||
&entry.global_ctx,
|
||||
entry.relay_data,
|
||||
);
|
||||
|
||||
assert!(entry.global_ctx.get_feature_flags().avoid_relay_data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credential_trust_path_rejects_admin_identity() {
|
||||
assert!(ForeignNetworkManager::should_reject_credential_trust_path(
|
||||
|
||||
@@ -1352,9 +1352,7 @@ impl PeerConn {
|
||||
|
||||
let is_foreign_network = conn_info_for_instrument.network_name
|
||||
!= self.global_ctx.get_network_identity().network_name;
|
||||
let recv_limiter = if is_foreign_network
|
||||
&& self.global_ctx.get_flags().foreign_relay_bps_limit != u64::MAX
|
||||
{
|
||||
let recv_limiter = if is_foreign_network {
|
||||
let relay_network_bps_limit = self.global_ctx.get_flags().foreign_relay_bps_limit;
|
||||
let limiter_config = LimiterConfig {
|
||||
burst_rate: None,
|
||||
|
||||
@@ -38,7 +38,7 @@ use crate::{
|
||||
route_trait::{ForeignNetworkRouteInfoMap, MockRoute, NextHopPolicy, RouteInterface},
|
||||
traffic_metrics::{
|
||||
InstanceLabelKind, LogicalTrafficMetrics, TrafficKind, TrafficMetricRecorder,
|
||||
is_relay_data_packet_type, route_peer_info_instance_id, traffic_kind,
|
||||
route_peer_info_instance_id, traffic_kind,
|
||||
},
|
||||
},
|
||||
proto::{
|
||||
@@ -263,7 +263,9 @@ impl PeerManager {
|
||||
.is_err()
|
||||
{
|
||||
// if local network is not in whitelist, avoid relay data when exist any other route path
|
||||
global_ctx.set_avoid_relay_data_preference(true);
|
||||
let mut f = global_ctx.get_feature_flags();
|
||||
f.avoid_relay_data = true;
|
||||
global_ctx.set_feature_flags(f);
|
||||
}
|
||||
|
||||
let is_secure_mode_enabled = global_ctx
|
||||
@@ -687,11 +689,6 @@ impl PeerManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release_reserved_peer_id(&self, network_name: &str) {
|
||||
self.reserved_my_peer_id_map.remove(network_name);
|
||||
shrink_dashmap(&self.reserved_my_peer_id_map, None);
|
||||
}
|
||||
|
||||
#[tracing::instrument(ret)]
|
||||
pub async fn add_tunnel_as_server(
|
||||
&self,
|
||||
@@ -707,8 +704,7 @@ impl PeerManager {
|
||||
tunnel,
|
||||
self.peer_session_store.clone(),
|
||||
);
|
||||
let mut reserved_peer_id_network_name = None;
|
||||
let handshake_ret = conn.do_handshake_as_server_ext(|peer, network_name:&str| {
|
||||
conn.do_handshake_as_server_ext(|peer, network_name:&str| {
|
||||
if network_name
|
||||
== self.global_ctx.get_network_identity().network_name
|
||||
{
|
||||
@@ -719,7 +715,6 @@ impl PeerManager {
|
||||
.foreign_network_manager
|
||||
.get_network_peer_id(network_name);
|
||||
if peer_id.is_none() {
|
||||
reserved_peer_id_network_name = Some(network_name.to_string());
|
||||
peer_id = Some(*self.reserved_my_peer_id_map.entry(network_name.to_string()).or_insert_with(|| {
|
||||
rand::random::<PeerId>()
|
||||
}).value());
|
||||
@@ -735,14 +730,7 @@ impl PeerManager {
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Err(err) = handshake_ret {
|
||||
if let Some(network_name) = reserved_peer_id_network_name {
|
||||
self.release_reserved_peer_id(&network_name);
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
.await?;
|
||||
|
||||
let peer_identity = conn.get_network_identity();
|
||||
let peer_network_name = peer_identity.network_name.clone();
|
||||
@@ -761,7 +749,6 @@ impl PeerManager {
|
||||
|
||||
if !is_local_network && self.global_ctx.get_flags().private_mode && !foreign_network_allowed
|
||||
{
|
||||
self.release_reserved_peer_id(&peer_network_name);
|
||||
return Err(Error::SecretKeyError(
|
||||
"private mode is turned on, foreign network secret mismatch".to_string(),
|
||||
));
|
||||
@@ -769,18 +756,14 @@ impl PeerManager {
|
||||
|
||||
conn.set_is_hole_punched(!is_directly_connected);
|
||||
|
||||
let add_peer_ret = if is_local_network {
|
||||
self.add_new_peer_conn(conn).await
|
||||
if is_local_network {
|
||||
self.add_new_peer_conn(conn).await?;
|
||||
} else {
|
||||
self.foreign_network_manager.add_peer_conn(conn).await
|
||||
};
|
||||
|
||||
if let Err(err) = add_peer_ret {
|
||||
self.release_reserved_peer_id(&peer_network_name);
|
||||
return Err(err);
|
||||
self.foreign_network_manager.add_peer_conn(conn).await?;
|
||||
}
|
||||
|
||||
self.release_reserved_peer_id(&peer_network_name);
|
||||
self.reserved_my_peer_id_map.remove(&peer_network_name);
|
||||
shrink_dashmap(&self.reserved_my_peer_id_map, None);
|
||||
|
||||
tracing::info!("add tunnel as server done");
|
||||
Ok(())
|
||||
@@ -791,7 +774,6 @@ impl PeerManager {
|
||||
my_peer_id: PeerId,
|
||||
peer_map: &PeerMap,
|
||||
foreign_network_mgr: &ForeignNetworkManager,
|
||||
disable_relay_data: bool,
|
||||
) -> Result<(), ZCPacket> {
|
||||
let pm_header = packet.peer_manager_header().unwrap();
|
||||
if pm_header.packet_type != PacketType::ForeignNetworkPacket as u8 {
|
||||
@@ -801,16 +783,6 @@ impl PeerManager {
|
||||
let from_peer_id = pm_header.from_peer_id.get();
|
||||
let to_peer_id = pm_header.to_peer_id.get();
|
||||
|
||||
if disable_relay_data && Self::is_relay_data_zc_packet(&packet) {
|
||||
tracing::debug!(
|
||||
?from_peer_id,
|
||||
?to_peer_id,
|
||||
inner_packet_type = ?packet.foreign_network_inner_packet_type(),
|
||||
"drop foreign network relay data while relay data is disabled"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let foreign_hdr = packet.foreign_network_hdr().unwrap();
|
||||
let foreign_network_name = foreign_hdr.get_network_name(packet.payload());
|
||||
let foreign_peer_id = foreign_hdr.get_dst_peer_id();
|
||||
@@ -900,29 +872,6 @@ impl PeerManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_relay_data_packet(packet_type: u8) -> bool {
|
||||
is_relay_data_packet_type(packet_type)
|
||||
}
|
||||
|
||||
fn is_relay_data_zc_packet(packet: &ZCPacket) -> bool {
|
||||
let Some(hdr) = packet.peer_manager_header() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if hdr.packet_type == PacketType::ForeignNetworkPacket as u8 {
|
||||
let inner_packet_type = packet.foreign_network_inner_packet_type();
|
||||
if inner_packet_type.is_none() {
|
||||
tracing::warn!(
|
||||
?hdr,
|
||||
"foreign network packet has unparseable inner peer manager header"
|
||||
);
|
||||
}
|
||||
return inner_packet_type.is_none_or(Self::is_relay_data_packet);
|
||||
}
|
||||
|
||||
Self::is_relay_data_packet(hdr.packet_type)
|
||||
}
|
||||
|
||||
async fn start_peer_recv(&self) {
|
||||
let mut recv = self.packet_recv.lock().await.take().unwrap();
|
||||
let my_peer_id = self.my_peer_id;
|
||||
@@ -976,21 +925,14 @@ impl PeerManager {
|
||||
self.tasks.lock().await.spawn(async move {
|
||||
tracing::trace!("start_peer_recv");
|
||||
while let Ok(ret) = recv_packet_from_chan(&mut recv).await {
|
||||
let disable_relay_data = global_ctx.flags_arc().disable_relay_data;
|
||||
let Err(mut ret) = Self::try_handle_foreign_network_packet(
|
||||
ret,
|
||||
my_peer_id,
|
||||
&peers,
|
||||
&foreign_mgr,
|
||||
disable_relay_data,
|
||||
)
|
||||
.await
|
||||
let Err(mut ret) =
|
||||
Self::try_handle_foreign_network_packet(ret, my_peer_id, &peers, &foreign_mgr)
|
||||
.await
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let buf_len = ret.buf_len();
|
||||
let is_relay_data_packet = Self::is_relay_data_zc_packet(&ret);
|
||||
let Some(hdr) = ret.mut_peer_manager_header() else {
|
||||
tracing::warn!(?ret, "invalid packet, skip");
|
||||
continue;
|
||||
@@ -1002,16 +944,6 @@ impl PeerManager {
|
||||
let packet_type = hdr.packet_type;
|
||||
let is_encrypted = hdr.is_encrypted();
|
||||
if to_peer_id != my_peer_id {
|
||||
if disable_relay_data && is_relay_data_packet {
|
||||
tracing::debug!(
|
||||
?from_peer_id,
|
||||
?to_peer_id,
|
||||
packet_type,
|
||||
"drop forwarded relay data while relay data is disabled"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if hdr.forward_counter > 7 {
|
||||
tracing::warn!(?hdr, "forward counter exceed, drop packet");
|
||||
continue;
|
||||
@@ -2148,7 +2080,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
proto::{
|
||||
common::{CompressionAlgoPb, NatType},
|
||||
common::{CompressionAlgoPb, NatType, PeerFeatureFlag},
|
||||
peer_rpc::SecureAuthLevel,
|
||||
},
|
||||
tunnel::{
|
||||
@@ -2292,84 +2224,6 @@ mod tests {
|
||||
assert_eq!(signal.version(), initial_version + 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disable_relay_data_classifies_data_plane_packets_only() {
|
||||
for packet_type in [
|
||||
PacketType::Data,
|
||||
PacketType::KcpSrc,
|
||||
PacketType::KcpDst,
|
||||
PacketType::QuicSrc,
|
||||
PacketType::QuicDst,
|
||||
PacketType::DataWithKcpSrcModified,
|
||||
PacketType::DataWithQuicSrcModified,
|
||||
PacketType::ForeignNetworkPacket,
|
||||
] {
|
||||
assert!(PeerManager::is_relay_data_packet(packet_type as u8));
|
||||
}
|
||||
|
||||
for packet_type in [
|
||||
PacketType::RpcReq,
|
||||
PacketType::RpcResp,
|
||||
PacketType::Ping,
|
||||
PacketType::Pong,
|
||||
PacketType::HandShake,
|
||||
PacketType::NoiseHandshakeMsg1,
|
||||
PacketType::NoiseHandshakeMsg2,
|
||||
PacketType::NoiseHandshakeMsg3,
|
||||
PacketType::RelayHandshake,
|
||||
PacketType::RelayHandshakeAck,
|
||||
] {
|
||||
assert!(!PeerManager::is_relay_data_packet(packet_type as u8));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disable_relay_data_inspects_foreign_network_inner_packet_type() {
|
||||
let network_name = "net1".to_string();
|
||||
|
||||
let mut rpc_packet = ZCPacket::new_with_payload(b"rpc");
|
||||
rpc_packet.fill_peer_manager_hdr(1, 2, PacketType::RpcReq as u8);
|
||||
let mut foreign_rpc_packet =
|
||||
ZCPacket::new_for_foreign_network(&network_name, 2, &rpc_packet);
|
||||
foreign_rpc_packet.fill_peer_manager_hdr(10, 20, PacketType::ForeignNetworkPacket as u8);
|
||||
|
||||
assert_eq!(
|
||||
foreign_rpc_packet.foreign_network_inner_packet_type(),
|
||||
Some(PacketType::RpcReq as u8)
|
||||
);
|
||||
assert!(!PeerManager::is_relay_data_zc_packet(&foreign_rpc_packet));
|
||||
|
||||
let mut data_packet = ZCPacket::new_with_payload(b"data");
|
||||
data_packet.fill_peer_manager_hdr(1, 2, PacketType::Data as u8);
|
||||
let mut foreign_data_packet =
|
||||
ZCPacket::new_for_foreign_network(&network_name, 2, &data_packet);
|
||||
foreign_data_packet.fill_peer_manager_hdr(10, 20, PacketType::ForeignNetworkPacket as u8);
|
||||
|
||||
assert_eq!(
|
||||
foreign_data_packet.foreign_network_inner_packet_type(),
|
||||
Some(PacketType::Data as u8)
|
||||
);
|
||||
assert!(PeerManager::is_relay_data_zc_packet(&foreign_data_packet));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn non_whitelisted_network_avoid_relay_survives_disable_relay_data_toggle() {
|
||||
let global_ctx = get_mock_global_ctx();
|
||||
let mut flags = global_ctx.get_flags();
|
||||
flags.disable_relay_data = true;
|
||||
flags.relay_network_whitelist = "other-network".to_string();
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
let (packet_send, _packet_recv) = create_packet_recv_chan();
|
||||
let _peer_mgr = PeerManager::new(RouteAlgoType::Ospf, global_ctx.clone(), packet_send);
|
||||
|
||||
let mut flags = global_ctx.get_flags();
|
||||
flags.disable_relay_data = false;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
assert!(global_ctx.get_feature_flags().avoid_relay_data);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_msg_internal_does_not_record_tx_metrics_on_failed_delivery() {
|
||||
let peer_mgr = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
||||
@@ -3267,7 +3121,10 @@ mod tests {
|
||||
// when b's avoid_relay_data is true, a->c should route through d and e, cost is 3
|
||||
peer_mgr_b
|
||||
.get_global_ctx()
|
||||
.set_avoid_relay_data_preference(true);
|
||||
.set_feature_flags(PeerFeatureFlag {
|
||||
avoid_relay_data: true,
|
||||
..Default::default()
|
||||
});
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
if wait_route_appear_with_cost(peer_mgr_a.clone(), peer_mgr_c.my_peer_id, Some(3))
|
||||
.await
|
||||
|
||||
@@ -1228,25 +1228,6 @@ impl SyncedRouteInfo {
|
||||
Vec<PeerId>,
|
||||
HashMap<Vec<u8>, crate::common::global_ctx::TrustedKeyMetadata>,
|
||||
)
|
||||
where
|
||||
F: FnMut(PeerId) -> bool,
|
||||
{
|
||||
self.verify_and_update_credential_trusts_with_active_peers_protecting(
|
||||
network_secret,
|
||||
is_peer_active,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn verify_and_update_credential_trusts_with_active_peers_protecting<F>(
|
||||
&self,
|
||||
network_secret: Option<&str>,
|
||||
is_peer_active: F,
|
||||
protected_peer_id: Option<PeerId>,
|
||||
) -> (
|
||||
Vec<PeerId>,
|
||||
HashMap<Vec<u8>, crate::common::global_ctx::TrustedKeyMetadata>,
|
||||
)
|
||||
where
|
||||
F: FnMut(PeerId) -> bool,
|
||||
{
|
||||
@@ -1267,9 +1248,6 @@ impl SyncedRouteInfo {
|
||||
let mut untrusted_peers =
|
||||
Self::collect_revoked_credential_peers(&peer_infos, &prev_trusted, &all_trusted);
|
||||
untrusted_peers.extend(duplicate_untrusted_peers);
|
||||
if let Some(protected_peer_id) = protected_peer_id {
|
||||
untrusted_peers.remove(&protected_peer_id);
|
||||
}
|
||||
|
||||
// Remove untrusted peers from peer_infos so they won't appear in route graph
|
||||
if !untrusted_peers.is_empty() {
|
||||
@@ -2757,11 +2735,7 @@ impl PeerRouteServiceImpl {
|
||||
let network_identity = self.global_ctx.get_network_identity();
|
||||
let (untrusted, global_trusted_keys) = self
|
||||
.synced_route_info
|
||||
.verify_and_update_credential_trusts_with_active_peers_protecting(
|
||||
network_identity.network_secret.as_deref(),
|
||||
|_| true,
|
||||
Some(self.my_peer_id),
|
||||
);
|
||||
.verify_and_update_credential_trusts(network_identity.network_secret.as_deref());
|
||||
self.global_ctx
|
||||
.update_trusted_keys(global_trusted_keys, &network_identity.network_name);
|
||||
|
||||
@@ -2777,10 +2751,9 @@ impl PeerRouteServiceImpl {
|
||||
|
||||
let (untrusted, global_trusted_keys) = self
|
||||
.synced_route_info
|
||||
.verify_and_update_credential_trusts_with_active_peers_protecting(
|
||||
.verify_and_update_credential_trusts_with_active_peers(
|
||||
network_identity.network_secret.as_deref(),
|
||||
|peer_id| self.is_active_non_reusable_credential_peer(peer_id),
|
||||
Some(self.my_peer_id),
|
||||
);
|
||||
self.global_ctx
|
||||
.update_trusted_keys(global_trusted_keys, &network_identity.network_name);
|
||||
@@ -5074,58 +5047,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn credential_trust_refresh_does_not_remove_self_peer() {
|
||||
let my_peer_id = 11;
|
||||
let remote_peer_id = 12;
|
||||
let credential_key = vec![8; 32];
|
||||
let service_impl = PeerRouteServiceImpl::new(my_peer_id, get_mock_global_ctx());
|
||||
|
||||
let self_info = make_credential_route_peer_info(my_peer_id, &credential_key);
|
||||
let remote_info = make_credential_route_peer_info(remote_peer_id, &credential_key);
|
||||
|
||||
{
|
||||
let mut guard = service_impl.synced_route_info.peer_infos.write();
|
||||
guard.insert(self_info.peer_id, self_info);
|
||||
guard.insert(remote_info.peer_id, remote_info);
|
||||
}
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.trusted_credential_pubkeys
|
||||
.insert(
|
||||
credential_key.clone(),
|
||||
TrustedCredentialPubkey {
|
||||
pubkey: credential_key,
|
||||
expiry_unix: i64::MAX,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let (untrusted_peers, _) = service_impl
|
||||
.synced_route_info
|
||||
.verify_and_update_credential_trusts_with_active_peers_protecting(
|
||||
None,
|
||||
|_| true,
|
||||
Some(my_peer_id),
|
||||
);
|
||||
|
||||
assert_eq!(untrusted_peers, vec![remote_peer_id]);
|
||||
assert!(
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.peer_infos
|
||||
.read()
|
||||
.contains_key(&my_peer_id)
|
||||
);
|
||||
assert!(
|
||||
!service_impl
|
||||
.synced_route_info
|
||||
.peer_infos
|
||||
.read()
|
||||
.contains_key(&remote_peer_id)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn credential_refresh_rebuilds_reachability_before_owner_election() {
|
||||
const NETWORK_SECRET: &str = "sec1";
|
||||
|
||||
@@ -7,10 +7,7 @@ use anyhow::anyhow;
|
||||
use dashmap::DashMap;
|
||||
|
||||
use super::secure_datagram::{SecureDatagramDirection, SecureDatagramSession};
|
||||
use crate::{
|
||||
common::{PeerId, shrink_dashmap},
|
||||
tunnel::packet_def::ZCPacket,
|
||||
};
|
||||
use crate::{common::PeerId, tunnel::packet_def::ZCPacket};
|
||||
|
||||
pub struct UpsertResponderSessionReturn {
|
||||
pub session: Arc<PeerSession>,
|
||||
@@ -81,7 +78,6 @@ impl PeerSessionStore {
|
||||
pub fn evict_unused_sessions(&self) {
|
||||
self.sessions
|
||||
.retain(|_key, session| Arc::strong_count(session) > 1);
|
||||
shrink_dashmap(&self.sessions, None);
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
|
||||
@@ -9,7 +9,7 @@ use tokio::time::{Duration, timeout};
|
||||
use crate::peers::foreign_network_client::ForeignNetworkClient;
|
||||
use crate::{
|
||||
common::error::Error,
|
||||
common::{PeerId, global_ctx::ArcGlobalCtx, shrink_dashmap},
|
||||
common::{PeerId, global_ctx::ArcGlobalCtx},
|
||||
peers::peer_map::PeerMap,
|
||||
peers::peer_session::{PeerSession, PeerSessionAction, PeerSessionStore, SessionKey},
|
||||
peers::route_trait::NextHopPolicy,
|
||||
@@ -652,10 +652,6 @@ impl RelayPeerMap {
|
||||
self.handshake_locks.remove(&peer_id);
|
||||
self.pending_packets.remove(&peer_id);
|
||||
}
|
||||
shrink_dashmap(&self.states, None);
|
||||
shrink_dashmap(&self.pending_handshakes, None);
|
||||
shrink_dashmap(&self.handshake_locks, None);
|
||||
shrink_dashmap(&self.pending_packets, None);
|
||||
}
|
||||
|
||||
pub fn has_state(&self, peer_id: PeerId) -> bool {
|
||||
@@ -683,10 +679,6 @@ impl RelayPeerMap {
|
||||
self.pending_handshakes.remove(&peer_id);
|
||||
self.handshake_locks.remove(&peer_id);
|
||||
self.pending_packets.remove(&peer_id);
|
||||
shrink_dashmap(&self.states, None);
|
||||
shrink_dashmap(&self.pending_handshakes, None);
|
||||
shrink_dashmap(&self.handshake_locks, None);
|
||||
shrink_dashmap(&self.pending_packets, None);
|
||||
|
||||
tracing::debug!(?peer_id, "RelayPeerMap removed peer relay state");
|
||||
}
|
||||
|
||||
@@ -201,11 +201,6 @@ impl LogicalTrafficMetrics {
|
||||
self.per_peer.len()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn contains_peer_cache(&self, peer_id: PeerId) -> bool {
|
||||
self.per_peer.contains_key(&peer_id)
|
||||
}
|
||||
|
||||
fn build_peer_counters(&self, instance_id: &str) -> TrafficCounters {
|
||||
let instance_label = match self.label_kind {
|
||||
InstanceLabelKind::To => LabelType::ToInstanceId(instance_id.to_string()),
|
||||
@@ -246,13 +241,6 @@ pub(crate) fn traffic_kind(packet_type: u8) -> TrafficKind {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_relay_data_packet_type(packet_type: u8) -> bool {
|
||||
// Relay handshakes are control-plane setup; payload data is blocked by its
|
||||
// original packet type after the session exists.
|
||||
traffic_kind(packet_type) == TrafficKind::Data
|
||||
|| packet_type == PacketType::ForeignNetworkPacket as u8
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TrafficMetricGroup {
|
||||
data: Arc<LogicalTrafficMetrics>,
|
||||
@@ -338,14 +326,6 @@ impl TrafficMetricRecorder {
|
||||
self.rx_metrics.control.clear_peer_cache();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn contains_peer_cache(&self, peer_id: PeerId) -> bool {
|
||||
self.tx_metrics.data.contains_peer_cache(peer_id)
|
||||
|| self.tx_metrics.control.contains_peer_cache(peer_id)
|
||||
|| self.rx_metrics.data.contains_peer_cache(peer_id)
|
||||
|| self.rx_metrics.control.contains_peer_cache(peer_id)
|
||||
}
|
||||
|
||||
fn resolve_instance_id(&self, peer_id: PeerId) -> BoxFuture<'static, Option<String>> {
|
||||
(self.resolve_instance_id)(peer_id)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ message InstanceConfigPatch {
|
||||
optional bool ipv6_public_addr_provider = 11;
|
||||
optional bool ipv6_public_addr_auto = 12;
|
||||
optional string ipv6_public_addr_prefix = 13;
|
||||
optional bool disable_relay_data = 14;
|
||||
}
|
||||
|
||||
message PortForwardPatch {
|
||||
|
||||
@@ -99,7 +99,6 @@ message NetworkConfig {
|
||||
optional bool ipv6_public_addr_provider = 62;
|
||||
optional bool ipv6_public_addr_auto = 63;
|
||||
optional string ipv6_public_addr_prefix = 64;
|
||||
optional bool disable_relay_data = 65;
|
||||
}
|
||||
|
||||
message PortForwardConfig {
|
||||
|
||||
@@ -75,7 +75,6 @@ message FlagsInConfig {
|
||||
bool need_p2p = 38;
|
||||
uint64 instance_recv_bps_limit = 39;
|
||||
bool disable_upnp = 40;
|
||||
bool disable_relay_data = 41;
|
||||
}
|
||||
|
||||
message RpcDescriptor {
|
||||
@@ -105,7 +104,6 @@ enum CompressionAlgoPb {
|
||||
Invalid = 0;
|
||||
None = 1;
|
||||
Zstd = 2;
|
||||
Lzo = 3;
|
||||
}
|
||||
|
||||
message RpcCompressionInfo {
|
||||
|
||||
@@ -467,8 +467,6 @@ impl TryFrom<CompressionAlgoPb> for CompressorAlgo {
|
||||
match value {
|
||||
#[cfg(feature = "zstd")]
|
||||
CompressionAlgoPb::Zstd => Ok(CompressorAlgo::ZstdDefault),
|
||||
#[cfg(feature = "lzo")]
|
||||
CompressionAlgoPb::Lzo => Ok(CompressorAlgo::Lzo),
|
||||
CompressionAlgoPb::None => Ok(CompressorAlgo::None),
|
||||
_ => Err(anyhow::anyhow!("Invalid CompressionAlgoPb")),
|
||||
}
|
||||
@@ -482,8 +480,6 @@ impl TryFrom<CompressorAlgo> for CompressionAlgoPb {
|
||||
match value {
|
||||
#[cfg(feature = "zstd")]
|
||||
CompressorAlgo::ZstdDefault => Ok(CompressionAlgoPb::Zstd),
|
||||
#[cfg(feature = "lzo")]
|
||||
CompressorAlgo::Lzo => Ok(CompressionAlgoPb::Lzo),
|
||||
CompressorAlgo::None => Ok(CompressionAlgoPb::None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,17 +14,13 @@ use crate::{
|
||||
},
|
||||
instance::instance::Instance,
|
||||
tests::three_node::{generate_secure_mode_config, generate_secure_mode_config_with_key},
|
||||
tunnel::{common::tests::wait_for_condition, tcp::TcpTunnelConnector, udp::UdpTunnelConnector},
|
||||
tunnel::{common::tests::wait_for_condition, tcp::TcpTunnelConnector},
|
||||
};
|
||||
|
||||
use super::{add_ns_to_bridge, create_netns, del_netns, drop_insts, ping_test};
|
||||
|
||||
use rstest::rstest;
|
||||
|
||||
const PUBLIC_SERVER_NETWORK_NAME: &str = "__public_server__";
|
||||
const PUBLIC_SERVER_SHARED_SECRET: &str = "public-server-shared-secret";
|
||||
const NEED_P2P_ADMIN_NETWORK_NAME: &str = "need_p2p_credential_test_network";
|
||||
|
||||
/// Prepare network namespaces for credential tests
|
||||
/// Topology:
|
||||
/// br_a (10.1.1.0/24): ns_adm (10.1.1.1), ns_c1 (10.1.1.2), ns_c2 (10.1.1.3), ns_c3 (10.1.1.4), ns_c4 (10.1.1.5)
|
||||
@@ -225,328 +221,6 @@ fn create_shared_config(
|
||||
config
|
||||
}
|
||||
|
||||
fn create_public_server_config() -> TomlConfigLoader {
|
||||
let config = TomlConfigLoader::default();
|
||||
config.set_inst_name(PUBLIC_SERVER_NETWORK_NAME.to_string());
|
||||
config.set_hostname(Some("public-server".to_string()));
|
||||
config.set_netns(Some("ns_adm".to_string()));
|
||||
config.set_listeners(vec!["udp://0.0.0.0:11010".parse().unwrap()]);
|
||||
config.set_network_identity(NetworkIdentity::new(
|
||||
PUBLIC_SERVER_NETWORK_NAME.to_string(),
|
||||
PUBLIC_SERVER_SHARED_SECRET.to_string(),
|
||||
));
|
||||
config.set_secure_mode(Some(generate_secure_mode_config()));
|
||||
|
||||
let mut flags = config.get_flags();
|
||||
flags.no_tun = true;
|
||||
flags.private_mode = true;
|
||||
flags.relay_all_peer_rpc = true;
|
||||
flags.relay_network_whitelist = "".to_string();
|
||||
config.set_flags(flags);
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
fn create_need_p2p_admin_config(listener_scheme: &str) -> TomlConfigLoader {
|
||||
let config = TomlConfigLoader::default();
|
||||
config.set_inst_name(NEED_P2P_ADMIN_NETWORK_NAME.to_string());
|
||||
config.set_hostname(Some("need-p2p-admin".to_string()));
|
||||
config.set_netns(Some("ns_c3".to_string()));
|
||||
config.set_listeners(vec![
|
||||
format!("{listener_scheme}://0.0.0.0:0").parse().unwrap(),
|
||||
]);
|
||||
config.set_network_identity(NetworkIdentity::new(
|
||||
NEED_P2P_ADMIN_NETWORK_NAME.to_string(),
|
||||
PUBLIC_SERVER_SHARED_SECRET.to_string(),
|
||||
));
|
||||
config.set_secure_mode(Some(generate_secure_mode_config()));
|
||||
|
||||
let mut flags = config.get_flags();
|
||||
flags.no_tun = true;
|
||||
flags.relay_all_peer_rpc = true;
|
||||
flags.need_p2p = true;
|
||||
flags.disable_udp_hole_punching = true;
|
||||
flags.disable_tcp_hole_punching = true;
|
||||
flags.disable_sym_hole_punching = true;
|
||||
config.set_flags(flags);
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn create_public_server_credential_config(
|
||||
credential_secret: &str,
|
||||
inst_name: &str,
|
||||
hostname: &str,
|
||||
ns: &str,
|
||||
ipv4: &str,
|
||||
ipv6: &str,
|
||||
tcp_listener_port: u16,
|
||||
udp_listener_port: u16,
|
||||
proxy_cidrs: &[&str],
|
||||
) -> TomlConfigLoader {
|
||||
let config = create_credential_config_from_secret(
|
||||
NEED_P2P_ADMIN_NETWORK_NAME.to_string(),
|
||||
credential_secret,
|
||||
inst_name,
|
||||
Some(ns),
|
||||
ipv4,
|
||||
ipv6,
|
||||
);
|
||||
config.set_hostname(Some(hostname.to_string()));
|
||||
config.set_listeners(vec![
|
||||
format!("tcp://0.0.0.0:{tcp_listener_port}")
|
||||
.parse()
|
||||
.unwrap(),
|
||||
format!("udp://0.0.0.0:{udp_listener_port}")
|
||||
.parse()
|
||||
.unwrap(),
|
||||
]);
|
||||
for cidr in proxy_cidrs {
|
||||
config
|
||||
.add_proxy_cidr((*cidr).parse().unwrap(), None)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let mut flags = config.get_flags();
|
||||
flags.disable_p2p = true;
|
||||
config.set_flags(flags);
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
async fn wait_direct_peer(inst: &Instance, peer_id: u32, timeout: Duration, label: &str) {
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
let peers = inst.get_peer_manager().get_peer_map().list_peers();
|
||||
let connected = peers.contains(&peer_id);
|
||||
println!("{label}: direct peers={:?}, target={}", peers, peer_id);
|
||||
connected
|
||||
},
|
||||
timeout,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn wait_running_listener(inst: &Instance, scheme: &str, timeout: Duration, label: &str) {
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
let listeners = inst.get_global_ctx().get_running_listeners();
|
||||
let matched = listeners.iter().any(|listener| {
|
||||
listener.scheme() == scheme && listener.port().is_some_and(|p| p != 0)
|
||||
});
|
||||
println!("{label}: running listeners={:?}", listeners);
|
||||
matched
|
||||
},
|
||||
timeout,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn wait_route_cost(inst: &Instance, peer_id: u32, cost: i32, timeout: Duration, label: &str) {
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
let routes = inst.get_peer_manager().list_routes().await;
|
||||
let matched = routes
|
||||
.iter()
|
||||
.any(|route| route.peer_id == peer_id && route.cost == cost);
|
||||
println!(
|
||||
"{label}: routes={:?}, target={}, cost={}",
|
||||
routes
|
||||
.iter()
|
||||
.map(|route| (route.peer_id, route.cost))
|
||||
.collect::<Vec<_>>(),
|
||||
peer_id,
|
||||
cost
|
||||
);
|
||||
matched
|
||||
},
|
||||
timeout,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn wait_foreign_network_count(inst: &Instance, expected: usize, timeout: Duration) {
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
let foreign_networks = inst
|
||||
.get_peer_manager()
|
||||
.get_foreign_network_manager()
|
||||
.list_foreign_networks()
|
||||
.await
|
||||
.foreign_networks;
|
||||
println!("foreign networks: {:?}", foreign_networks);
|
||||
foreign_networks.len() == expected
|
||||
},
|
||||
timeout,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Regression coverage for a public-server-mediated credential topology:
|
||||
/// Public server <- admin peer (need_p2p) <- two credential peers.
|
||||
///
|
||||
/// Credential peers set `disable_p2p=true`, while the admin peer advertises `need_p2p=true`.
|
||||
/// The credential peers should still proactively build direct peers with the admin peer through
|
||||
/// peer RPC forwarded by the public server, even when the admin listener binds an ephemeral port.
|
||||
#[rstest]
|
||||
#[case("quic")]
|
||||
#[case("wss")]
|
||||
#[case("tcp")]
|
||||
#[case("udp")]
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn credential_peers_p2p_to_need_p2p_admin_through_public_server(
|
||||
#[case] admin_listener_scheme: &str,
|
||||
) {
|
||||
prepare_credential_network();
|
||||
|
||||
let mut public_server_inst = Instance::new(create_public_server_config());
|
||||
public_server_inst.run().await.unwrap();
|
||||
|
||||
let mut admin_inst = Instance::new(create_need_p2p_admin_config(admin_listener_scheme));
|
||||
admin_inst.run().await.unwrap();
|
||||
wait_running_listener(
|
||||
&admin_inst,
|
||||
admin_listener_scheme,
|
||||
Duration::from_secs(10),
|
||||
"admin ephemeral listener",
|
||||
)
|
||||
.await;
|
||||
admin_inst
|
||||
.get_conn_manager()
|
||||
.add_connector(UdpTunnelConnector::new(
|
||||
"udp://10.1.1.1:11010".parse().unwrap(),
|
||||
));
|
||||
|
||||
wait_foreign_network_count(&public_server_inst, 1, Duration::from_secs(10)).await;
|
||||
|
||||
let (_credential_a_id, credential_a_secret) = admin_inst
|
||||
.get_global_ctx()
|
||||
.get_credential_manager()
|
||||
.generate_credential_with_options(
|
||||
vec![],
|
||||
false,
|
||||
vec!["10.1.0.0/24".to_string()],
|
||||
Duration::from_secs(3600),
|
||||
Some("credential-peer-a".to_string()),
|
||||
false,
|
||||
);
|
||||
let (_credential_b_id, credential_b_secret) = admin_inst
|
||||
.get_global_ctx()
|
||||
.get_credential_manager()
|
||||
.generate_credential_with_options(
|
||||
vec![],
|
||||
false,
|
||||
vec![],
|
||||
Duration::from_secs(3600),
|
||||
Some("credential-peer-b".to_string()),
|
||||
false,
|
||||
);
|
||||
admin_inst
|
||||
.get_global_ctx()
|
||||
.issue_event(GlobalCtxEvent::CredentialChanged);
|
||||
|
||||
wait_foreign_network_count(&public_server_inst, 1, Duration::from_secs(10)).await;
|
||||
|
||||
let mut credential_a_inst = Instance::new(create_public_server_credential_config(
|
||||
&credential_a_secret,
|
||||
"credential-peer-a",
|
||||
"credential-a",
|
||||
"ns_c1",
|
||||
"10.154.0.1",
|
||||
"fd00::1/64",
|
||||
11030,
|
||||
11031,
|
||||
&["10.1.0.0/24"],
|
||||
));
|
||||
let mut credential_b_inst = Instance::new(create_public_server_credential_config(
|
||||
&credential_b_secret,
|
||||
"credential-peer-b",
|
||||
"credential-b",
|
||||
"ns_c2",
|
||||
"10.154.0.2",
|
||||
"fd00::2/64",
|
||||
11040,
|
||||
11041,
|
||||
&[],
|
||||
));
|
||||
credential_a_inst.run().await.unwrap();
|
||||
credential_b_inst.run().await.unwrap();
|
||||
|
||||
credential_a_inst
|
||||
.get_conn_manager()
|
||||
.add_connector(UdpTunnelConnector::new(
|
||||
"udp://10.1.1.1:11010".parse().unwrap(),
|
||||
));
|
||||
credential_b_inst
|
||||
.get_conn_manager()
|
||||
.add_connector(UdpTunnelConnector::new(
|
||||
"udp://10.1.1.1:11010".parse().unwrap(),
|
||||
));
|
||||
|
||||
let admin_peer_id = admin_inst.peer_id();
|
||||
let credential_a_peer_id = credential_a_inst.peer_id();
|
||||
let credential_b_peer_id = credential_b_inst.peer_id();
|
||||
println!(
|
||||
"admin={}, credential_a={}, credential_b={}, admin_listener_scheme={}",
|
||||
admin_peer_id, credential_a_peer_id, credential_b_peer_id, admin_listener_scheme
|
||||
);
|
||||
|
||||
wait_direct_peer(
|
||||
&credential_a_inst,
|
||||
admin_peer_id,
|
||||
Duration::from_secs(30),
|
||||
"credential_a -> admin",
|
||||
)
|
||||
.await;
|
||||
wait_direct_peer(
|
||||
&credential_b_inst,
|
||||
admin_peer_id,
|
||||
Duration::from_secs(30),
|
||||
"credential_b -> admin",
|
||||
)
|
||||
.await;
|
||||
wait_direct_peer(
|
||||
&admin_inst,
|
||||
credential_a_peer_id,
|
||||
Duration::from_secs(10),
|
||||
"admin -> credential_a",
|
||||
)
|
||||
.await;
|
||||
wait_direct_peer(
|
||||
&admin_inst,
|
||||
credential_b_peer_id,
|
||||
Duration::from_secs(10),
|
||||
"admin -> credential_b",
|
||||
)
|
||||
.await;
|
||||
wait_route_cost(
|
||||
&credential_a_inst,
|
||||
admin_peer_id,
|
||||
1,
|
||||
Duration::from_secs(10),
|
||||
"credential_a route to admin",
|
||||
)
|
||||
.await;
|
||||
wait_route_cost(
|
||||
&credential_b_inst,
|
||||
admin_peer_id,
|
||||
1,
|
||||
Duration::from_secs(10),
|
||||
"credential_b route to admin",
|
||||
)
|
||||
.await;
|
||||
|
||||
drop_insts(vec![
|
||||
public_server_inst,
|
||||
admin_inst,
|
||||
credential_a_inst,
|
||||
credential_b_inst,
|
||||
])
|
||||
.await;
|
||||
}
|
||||
|
||||
fn create_generated_credential_config(
|
||||
admin_inst: &Instance,
|
||||
inst_name: &str,
|
||||
@@ -827,10 +501,10 @@ async fn credential_relay_capability(#[case] allow_relay: bool) {
|
||||
// Create admin node
|
||||
let admin_config = create_admin_config("admin", Some("ns_adm"), "10.144.144.1", "fd00::1/64");
|
||||
let mut admin_inst = Instance::new(admin_config);
|
||||
let mut ff = admin_inst.get_global_ctx().get_feature_flags();
|
||||
// if cred c allow relay, we set admin inst avoid relay (if other same-cost path available, admin will not relay data)
|
||||
admin_inst
|
||||
.get_global_ctx()
|
||||
.set_avoid_relay_data_preference(allow_relay);
|
||||
ff.avoid_relay_data = allow_relay;
|
||||
admin_inst.get_global_ctx().set_feature_flags(ff);
|
||||
admin_inst.run().await.unwrap();
|
||||
|
||||
let admin_peer_id = admin_inst.peer_id();
|
||||
|
||||
@@ -3730,153 +3730,6 @@ pub async fn config_patch_test() {
|
||||
drop_insts(insts).await;
|
||||
}
|
||||
|
||||
#[rstest::rstest]
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
pub async fn config_patch_disable_relay_data_test() {
|
||||
use crate::proto::api::config::InstanceConfigPatch;
|
||||
|
||||
let insts = init_three_node_ex(
|
||||
"udp",
|
||||
|cfg| {
|
||||
cfg.set_ipv6(None);
|
||||
cfg
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
let relay_peer_id = insts[1].peer_id();
|
||||
let dst_peer_id = insts[2].peer_id();
|
||||
assert!(!insts[1].get_global_ctx().get_flags().disable_relay_data);
|
||||
assert!(
|
||||
!insts[1]
|
||||
.get_global_ctx()
|
||||
.get_feature_flags()
|
||||
.avoid_relay_data
|
||||
);
|
||||
|
||||
check_route_ex(
|
||||
insts[0].get_peer_manager().list_routes().await,
|
||||
dst_peer_id,
|
||||
|route| {
|
||||
assert_eq!(route.next_hop_peer_id, relay_peer_id);
|
||||
true
|
||||
},
|
||||
);
|
||||
|
||||
wait_for_condition(
|
||||
|| async { ping_test("net_a", "10.144.144.3", None).await },
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await;
|
||||
|
||||
insts[1]
|
||||
.get_config_patcher()
|
||||
.apply_patch(InstanceConfigPatch {
|
||||
disable_relay_data: Some(true),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(insts[1].get_global_ctx().get_flags().disable_relay_data);
|
||||
assert!(
|
||||
insts[1]
|
||||
.get_global_ctx()
|
||||
.config
|
||||
.get_flags()
|
||||
.disable_relay_data
|
||||
);
|
||||
assert!(
|
||||
insts[1]
|
||||
.get_global_ctx()
|
||||
.get_feature_flags()
|
||||
.avoid_relay_data
|
||||
);
|
||||
|
||||
wait_for_condition(
|
||||
|| {
|
||||
let peer_mgr = insts[0].get_peer_manager().clone();
|
||||
async move {
|
||||
peer_mgr.list_routes().await.iter().any(|route| {
|
||||
route.peer_id == relay_peer_id
|
||||
&& route
|
||||
.feature_flag
|
||||
.as_ref()
|
||||
.map(|flag| flag.avoid_relay_data)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
},
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await;
|
||||
|
||||
check_route_ex(
|
||||
insts[0].get_peer_manager().list_routes().await,
|
||||
dst_peer_id,
|
||||
|route| {
|
||||
assert_eq!(route.next_hop_peer_id, relay_peer_id);
|
||||
true
|
||||
},
|
||||
);
|
||||
assert!(
|
||||
!ping_test("net_a", "10.144.144.3", None).await,
|
||||
"traffic from inst1 to inst3 should be blocked while inst2 relay data is disabled"
|
||||
);
|
||||
|
||||
insts[1]
|
||||
.get_config_patcher()
|
||||
.apply_patch(InstanceConfigPatch {
|
||||
disable_relay_data: Some(false),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!insts[1].get_global_ctx().get_flags().disable_relay_data);
|
||||
assert!(
|
||||
!insts[1]
|
||||
.get_global_ctx()
|
||||
.config
|
||||
.get_flags()
|
||||
.disable_relay_data
|
||||
);
|
||||
assert!(
|
||||
!insts[1]
|
||||
.get_global_ctx()
|
||||
.get_feature_flags()
|
||||
.avoid_relay_data
|
||||
);
|
||||
|
||||
wait_for_condition(
|
||||
|| {
|
||||
let peer_mgr = insts[0].get_peer_manager().clone();
|
||||
async move {
|
||||
peer_mgr.list_routes().await.iter().any(|route| {
|
||||
route.peer_id == relay_peer_id
|
||||
&& route
|
||||
.feature_flag
|
||||
.as_ref()
|
||||
.map(|flag| !flag.avoid_relay_data)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
},
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await;
|
||||
|
||||
wait_for_condition(
|
||||
|| async { ping_test("net_a", "10.144.144.3", None).await },
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await;
|
||||
|
||||
drop_insts(insts).await;
|
||||
}
|
||||
|
||||
/// Generate SecureModeConfig with specified x25519 private key
|
||||
pub fn generate_secure_mode_config_with_key(
|
||||
private_key: &x25519_dalek::StaticSecret,
|
||||
|
||||
@@ -57,21 +57,21 @@ cfg_select! {
|
||||
pub mod windivert;
|
||||
|
||||
pub fn create_tun(
|
||||
interface_name: &str,
|
||||
src_addr: Option<SocketAddr>,
|
||||
dst_addr: SocketAddr,
|
||||
_interface_name: &str,
|
||||
_src_addr: Option<SocketAddr>,
|
||||
local_addr: SocketAddr,
|
||||
) -> io::Result<Arc<dyn super::stack::Tun>> {
|
||||
match windivert::WinDivertTun::new(src_addr, dst_addr) {
|
||||
match windivert::WinDivertTun::new(local_addr) {
|
||||
Ok(tun) => Ok(Arc::new(tun)),
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
?e,
|
||||
?dst_addr,
|
||||
?local_addr,
|
||||
"WinDivertTun init failed, falling back to PnetTun"
|
||||
);
|
||||
Ok(Arc::new(pnet::PnetTun::new(
|
||||
interface_name,
|
||||
pnet::create_packet_filter(src_addr, dst_addr),
|
||||
local_addr.to_string().as_str(),
|
||||
pnet::create_packet_filter(None, local_addr),
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,11 +80,15 @@ impl Drop for WinDivertTun {
|
||||
}
|
||||
|
||||
impl WinDivertTun {
|
||||
pub fn new(src_addr: Option<SocketAddr>, dst_addr: SocketAddr) -> io::Result<Self> {
|
||||
pub fn new(local_addr: SocketAddr) -> io::Result<Self> {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(1024);
|
||||
|
||||
let filter = build_filter(src_addr, dst_addr)?;
|
||||
tracing::debug!(%filter, "WinDivertTun created with filter");
|
||||
let ip_filter = match local_addr {
|
||||
SocketAddr::V4(addr) => format!("ip.DstAddr == {}", addr.ip()),
|
||||
SocketAddr::V6(addr) => format!("ipv6.DstAddr == {}", addr.ip()),
|
||||
};
|
||||
// Filter: DstIP == LocalIP AND TCP.
|
||||
let filter = format!("{} and tcp", ip_filter);
|
||||
|
||||
// Sniff mode: 1 (WINDIVERT_FLAG_SNIFF)
|
||||
// Layer: Network (0)
|
||||
@@ -139,46 +143,6 @@ impl WinDivertTun {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_filter(src_addr: Option<SocketAddr>, dst_addr: SocketAddr) -> io::Result<String> {
|
||||
if let Some(src_addr) = src_addr
|
||||
&& src_addr.is_ipv4() != dst_addr.is_ipv4()
|
||||
{
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"src/dst addr family mismatch",
|
||||
));
|
||||
}
|
||||
|
||||
let mut filters = Vec::with_capacity(5);
|
||||
filters.push("tcp".to_owned());
|
||||
|
||||
match dst_addr {
|
||||
SocketAddr::V4(addr) => {
|
||||
filters.push(format!("ip.DstAddr == {}", addr.ip()));
|
||||
filters.push(format!("tcp.DstPort == {}", addr.port()));
|
||||
}
|
||||
SocketAddr::V6(addr) => {
|
||||
filters.push(format!("ipv6.DstAddr == {}", addr.ip()));
|
||||
filters.push(format!("tcp.DstPort == {}", addr.port()));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(src_addr) = src_addr {
|
||||
match src_addr {
|
||||
SocketAddr::V4(addr) => {
|
||||
filters.push(format!("ip.SrcAddr == {}", addr.ip()));
|
||||
filters.push(format!("tcp.SrcPort == {}", addr.port()));
|
||||
}
|
||||
SocketAddr::V6(addr) => {
|
||||
filters.push(format!("ipv6.SrcAddr == {}", addr.ip()));
|
||||
filters.push(format!("tcp.SrcPort == {}", addr.port()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(filters.join(" and "))
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl stack::Tun for WinDivertTun {
|
||||
async fn recv(&self, packet: &mut BytesMut) -> Result<usize, std::io::Error> {
|
||||
|
||||
@@ -128,6 +128,7 @@ pub fn build_tcp_packet(
|
||||
eth_buf.freeze()
|
||||
}
|
||||
|
||||
#[tracing::instrument(ret)]
|
||||
pub fn parse_ip_packet(
|
||||
buf: &Bytes,
|
||||
) -> Option<(MacAddr, MacAddr, IPPacket<'_>, tcp::TcpPacket<'_>)> {
|
||||
|
||||
@@ -517,12 +517,9 @@ impl Stack {
|
||||
{
|
||||
trace!(?tcp_packet, "Received SYN packet for port {}, ignoring", tcp_packet.get_destination());
|
||||
continue;
|
||||
} else if (tcp_packet.get_flags() & tcp::TcpFlags::RST) != 0 {
|
||||
} else if (tcp_packet.get_flags() & tcp::TcpFlags::RST) == 0 {
|
||||
info!("Unknown RST TCP packet from {}, ignoring", remote_addr);
|
||||
continue;
|
||||
} else {
|
||||
trace!("Unknown TCP packet from {}, ignoring", remote_addr);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
||||
@@ -309,8 +309,6 @@ pub enum CompressorAlgo {
|
||||
None = 0,
|
||||
#[cfg(feature = "zstd")]
|
||||
ZstdDefault = 1,
|
||||
#[cfg(feature = "lzo")]
|
||||
Lzo = 2,
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
@@ -325,8 +323,6 @@ impl CompressorTail {
|
||||
match self.algo {
|
||||
#[cfg(feature = "zstd")]
|
||||
1 => Some(CompressorAlgo::ZstdDefault),
|
||||
#[cfg(feature = "lzo")]
|
||||
2 => Some(CompressorAlgo::Lzo),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -734,17 +730,6 @@ impl ZCPacket {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn foreign_network_inner_packet_type(&self) -> Option<u8> {
|
||||
if self.peer_manager_header()?.packet_type != PacketType::ForeignNetworkPacket as u8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let payload = self.payload();
|
||||
let hdr = ForeignNetworkPacketHeader::ref_from_prefix(payload)?;
|
||||
let inner_packet = payload.get(hdr.get_header_len()..)?;
|
||||
PeerManagerHeader::ref_from_prefix(inner_packet).map(|hdr| hdr.packet_type)
|
||||
}
|
||||
|
||||
pub fn foreign_network_packet(mut self) -> Self {
|
||||
let hdr = self.foreign_network_hdr().unwrap();
|
||||
let foreign_hdr_len = hdr.get_header_len();
|
||||
|
||||
+16
-202
@@ -14,8 +14,8 @@ use derivative::Derivative;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use parking_lot::RwLock;
|
||||
use quinn::{
|
||||
ClientConfig, ConnectError, Connection, Endpoint, EndpointConfig, ServerConfig,
|
||||
TransportConfig, congestion::BbrConfig, default_runtime,
|
||||
ClientConfig, Connection, Endpoint, EndpointConfig, ServerConfig, TransportConfig,
|
||||
congestion::BbrConfig, default_runtime,
|
||||
};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::sync::OnceLock;
|
||||
@@ -135,12 +135,6 @@ impl<Item> RwPool<Item> {
|
||||
self.resize();
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
let persistent_len = self.persistent.read().len();
|
||||
let ephemeral_len = self.ephemeral.read().len();
|
||||
persistent_len + ephemeral_len
|
||||
}
|
||||
|
||||
/// try to push an item to the ephemeral pool, return the item if full
|
||||
fn try_push(&self, item: Item) -> Option<Item> {
|
||||
let mut pool = self.ephemeral.write();
|
||||
@@ -174,49 +168,6 @@ impl<Item> RwPool<Item> {
|
||||
f(&mut persistent.iter().chain(ephemeral.iter()))
|
||||
}
|
||||
}
|
||||
|
||||
impl RwPool<Endpoint> {
|
||||
fn retain_endpoints<F>(&self, mut keep: F) -> usize
|
||||
where
|
||||
F: FnMut(&Endpoint) -> bool,
|
||||
{
|
||||
let persistent_removed = {
|
||||
let mut persistent = self.persistent.write();
|
||||
let before = persistent.len();
|
||||
persistent.retain(|endpoint| keep(endpoint));
|
||||
before - persistent.len()
|
||||
};
|
||||
|
||||
let ephemeral_removed = {
|
||||
let mut ephemeral = self.ephemeral.write();
|
||||
let before = ephemeral.len();
|
||||
ephemeral.retain(|endpoint| keep(endpoint));
|
||||
before - ephemeral.len()
|
||||
};
|
||||
|
||||
let removed = persistent_removed + ephemeral_removed;
|
||||
if removed > 0 {
|
||||
self.resize();
|
||||
}
|
||||
removed
|
||||
}
|
||||
|
||||
fn remove_by_local_addr(&self, local_addr: SocketAddr) -> usize {
|
||||
self.retain_endpoints(|endpoint| endpoint.local_addr().ok() != Some(local_addr))
|
||||
}
|
||||
|
||||
fn contains_local_addr(&self, local_addr: SocketAddr) -> bool {
|
||||
self.persistent
|
||||
.read()
|
||||
.iter()
|
||||
.any(|endpoint| endpoint.local_addr().ok() == Some(local_addr))
|
||||
|| self
|
||||
.ephemeral
|
||||
.read()
|
||||
.iter()
|
||||
.any(|endpoint| endpoint.local_addr().ok() == Some(local_addr))
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region endpoint manager
|
||||
@@ -311,20 +262,6 @@ impl QuicEndpointManager {
|
||||
QUIC_ENDPOINT_MANAGER.get().unwrap()
|
||||
}
|
||||
|
||||
fn client_pool(&self, ip_version: IpVersion) -> &RwPool<Endpoint> {
|
||||
let dual_stack = self.both.is_enabled();
|
||||
match ip_version {
|
||||
IpVersion::V4 if !dual_stack => &self.ipv4,
|
||||
_ => {
|
||||
if dual_stack {
|
||||
&self.both
|
||||
} else {
|
||||
&self.ipv6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a QUIC endpoint to be used as a server
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -351,8 +288,14 @@ impl QuicEndpointManager {
|
||||
Ok(endpoint)
|
||||
}
|
||||
|
||||
fn client_endpoint(&self, ip_version: IpVersion) -> Result<Endpoint, TunnelError> {
|
||||
let (pool, endpoint) = self.create(|mgr| {
|
||||
/// Get a quic endpoint to be used as a client
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `ip_version`: the IP version of the remote address
|
||||
fn client(global_ctx: &ArcGlobalCtx, ip_version: IpVersion) -> Result<Endpoint, TunnelError> {
|
||||
let mgr = Self::load(global_ctx);
|
||||
|
||||
let (pool, endpoint) = mgr.create(|mgr| {
|
||||
let dual_stack = mgr.both.is_enabled();
|
||||
let (pool, addr) = match ip_version {
|
||||
IpVersion::V4 if !dual_stack => (&mgr.ipv4, (Ipv4Addr::UNSPECIFIED, 0).into()),
|
||||
@@ -375,26 +318,6 @@ impl QuicEndpointManager {
|
||||
Ok(pool.with_iter(|iter| iter.min_by_key(|e| e.open_connections()).unwrap().clone()))
|
||||
}
|
||||
|
||||
fn remove_endpoint(&self, endpoint: &Endpoint) -> usize {
|
||||
let Ok(local_addr) = endpoint.local_addr() else {
|
||||
return 0;
|
||||
};
|
||||
self.remove_endpoint_by_local_addr(local_addr)
|
||||
}
|
||||
|
||||
fn remove_endpoint_by_local_addr(&self, local_addr: SocketAddr) -> usize {
|
||||
[&self.ipv4, &self.ipv6, &self.both]
|
||||
.into_iter()
|
||||
.map(|pool| pool.remove_by_local_addr(local_addr))
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn contains_local_addr(&self, local_addr: SocketAddr) -> bool {
|
||||
[&self.ipv4, &self.ipv6, &self.both]
|
||||
.into_iter()
|
||||
.any(|pool| pool.contains_local_addr(local_addr))
|
||||
}
|
||||
|
||||
async fn connect(
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
addr: SocketAddr,
|
||||
@@ -404,52 +327,14 @@ impl QuicEndpointManager {
|
||||
} else {
|
||||
IpVersion::V6
|
||||
};
|
||||
Self::load(global_ctx)
|
||||
.connect_with_ip_version(addr, ip_version)
|
||||
let endpoint = Self::client(global_ctx, ip_version)?;
|
||||
let connection = endpoint
|
||||
.connect(addr, "localhost")
|
||||
.with_context(|| format!("failed to create connection to {}", addr))?
|
||||
.await
|
||||
}
|
||||
.with_context(|| format!("failed to connect to {}", addr))?;
|
||||
|
||||
async fn connect_with_ip_version(
|
||||
&self,
|
||||
addr: SocketAddr,
|
||||
ip_version: IpVersion,
|
||||
) -> Result<(Endpoint, Connection), TunnelError> {
|
||||
let max_endpoint_stopping_retries = self.client_pool(ip_version).len().saturating_add(1);
|
||||
let mut endpoint_stopping_retries = 0;
|
||||
|
||||
loop {
|
||||
let endpoint = self.client_endpoint(ip_version)?;
|
||||
let connecting = match endpoint.connect(addr, "localhost") {
|
||||
Ok(connecting) => connecting,
|
||||
Err(ConnectError::EndpointStopping) => {
|
||||
let local_addr = endpoint.local_addr().ok();
|
||||
let removed = self.remove_endpoint(&endpoint);
|
||||
endpoint_stopping_retries += 1;
|
||||
tracing::warn!(
|
||||
?addr,
|
||||
?local_addr,
|
||||
removed,
|
||||
"removed stopped quic endpoint and retry connect"
|
||||
);
|
||||
if endpoint_stopping_retries > max_endpoint_stopping_retries {
|
||||
return Err(anyhow::Error::new(ConnectError::EndpointStopping)
|
||||
.context(format!("failed to create connection to {}", addr))
|
||||
.into());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(anyhow::Error::new(e)
|
||||
.context(format!("failed to create connection to {}", addr))
|
||||
.into());
|
||||
}
|
||||
};
|
||||
let connection = connecting
|
||||
.await
|
||||
.with_context(|| format!("failed to connect to {}", addr))?;
|
||||
|
||||
return Ok((endpoint, connection));
|
||||
}
|
||||
Ok((endpoint, connection))
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
@@ -513,18 +398,6 @@ impl QuicTunnelListener {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for QuicTunnelListener {
|
||||
fn drop(&mut self) {
|
||||
let Some(endpoint) = &self.endpoint else {
|
||||
return;
|
||||
};
|
||||
let Ok(local_addr) = endpoint.local_addr() else {
|
||||
return;
|
||||
};
|
||||
QuicEndpointManager::load(&self.global_ctx).remove_endpoint_by_local_addr(local_addr);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TunnelListener for QuicTunnelListener {
|
||||
async fn listen(&mut self) -> Result<(), TunnelError> {
|
||||
@@ -643,20 +516,6 @@ mod tests {
|
||||
get_mock_global_ctx_with_network(Some(identity))
|
||||
}
|
||||
|
||||
fn stopped_client_endpoint() -> (Endpoint, SocketAddr) {
|
||||
let rt = Builder::new_current_thread().enable_all().build().unwrap();
|
||||
let endpoint = rt.block_on(async {
|
||||
QuicEndpointManager::try_create((Ipv4Addr::UNSPECIFIED, 0).into(), false).unwrap()
|
||||
});
|
||||
let local_addr = endpoint.local_addr().unwrap();
|
||||
drop(rt);
|
||||
assert!(matches!(
|
||||
endpoint.connect("127.0.0.1:1".parse().unwrap(), "localhost"),
|
||||
Err(ConnectError::EndpointStopping)
|
||||
));
|
||||
(endpoint, local_addr)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quic_pingpong() {
|
||||
RUNTIME.block_on(quic_pingpong_impl())
|
||||
@@ -732,51 +591,6 @@ mod tests {
|
||||
assert!(port > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listener_drop_removes_persistent_endpoint() {
|
||||
RUNTIME.block_on(listener_drop_removes_persistent_endpoint_impl())
|
||||
}
|
||||
async fn listener_drop_removes_persistent_endpoint_impl() {
|
||||
let global_ctx = global_ctx();
|
||||
let endpoint_addr = {
|
||||
let mut listener =
|
||||
QuicTunnelListener::new("quic://127.0.0.1:0".parse().unwrap(), global_ctx.clone());
|
||||
listener.listen().await.unwrap();
|
||||
let endpoint_addr = listener.endpoint.as_ref().unwrap().local_addr().unwrap();
|
||||
assert!(QuicEndpointManager::load(&global_ctx).contains_local_addr(endpoint_addr));
|
||||
endpoint_addr
|
||||
};
|
||||
|
||||
assert!(!QuicEndpointManager::load(&global_ctx).contains_local_addr(endpoint_addr));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connect_removes_stopped_endpoints_and_retries() {
|
||||
let (stopped_endpoint_a, stopped_addr_a) = stopped_client_endpoint();
|
||||
let (stopped_endpoint_b, stopped_addr_b) = stopped_client_endpoint();
|
||||
|
||||
RUNTIME.block_on(async move {
|
||||
let mgr = QuicEndpointManager::new(2);
|
||||
mgr.both.push(stopped_endpoint_a);
|
||||
mgr.both.push(stopped_endpoint_b);
|
||||
assert!(mgr.contains_local_addr(stopped_addr_a));
|
||||
assert!(mgr.contains_local_addr(stopped_addr_b));
|
||||
|
||||
let err = mgr
|
||||
.connect_with_ip_version("127.0.0.1:0".parse().unwrap(), IpVersion::V4)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let err = format!("{:?}", err);
|
||||
assert!(
|
||||
err.contains("invalid remote address"),
|
||||
"unexpected error: {}",
|
||||
err
|
||||
);
|
||||
assert!(!mgr.contains_local_addr(stopped_addr_a));
|
||||
assert!(!mgr.contains_local_addr(stopped_addr_b));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_peer_addr() {
|
||||
RUNTIME.block_on(invalid_peer_addr_impl())
|
||||
|
||||
@@ -2,17 +2,13 @@ use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
common::{
|
||||
config::TomlConfigLoader,
|
||||
global_ctx::{ArcGlobalCtx, GlobalCtx},
|
||||
log,
|
||||
os_info::collect_device_os_info,
|
||||
set_default_machine_id,
|
||||
stun::MockStunInfoCollector,
|
||||
config::TomlConfigLoader, global_ctx::GlobalCtx, log, os_info::collect_device_os_info,
|
||||
set_default_machine_id, stun::MockStunInfoCollector,
|
||||
},
|
||||
connector::create_connector_by_url,
|
||||
instance_manager::{DaemonGuard, NetworkInstanceManager},
|
||||
proto::common::NatType,
|
||||
tunnel::{IpVersion, Tunnel, TunnelConnector, TunnelError, TunnelScheme},
|
||||
tunnel::{IpVersion, TunnelConnector},
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
@@ -53,30 +49,6 @@ pub struct WebClient {
|
||||
connected: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
struct ConfigServerConnector {
|
||||
url: Url,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TunnelConnector for ConfigServerConnector {
|
||||
async fn connect(&mut self) -> std::result::Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let mut connector =
|
||||
create_connector_by_url(self.url.as_str(), &self.global_ctx, IpVersion::Both)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
crate::common::error::Error::TunnelError(err) => err,
|
||||
err => TunnelError::Anyhow(err.into()),
|
||||
})?;
|
||||
|
||||
connector.connect().await
|
||||
}
|
||||
|
||||
fn remote_url(&self) -> Url {
|
||||
self.url.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl WebClient {
|
||||
pub fn new<T: TunnelConnector + 'static, S: ToString, H: ToString>(
|
||||
connector: T,
|
||||
@@ -246,13 +218,6 @@ pub async fn run_web_client(
|
||||
.with_context(|| "failed to parse config server URL")?,
|
||||
};
|
||||
|
||||
TunnelScheme::try_from(&config_server_url).map_err(|_| {
|
||||
anyhow::anyhow!(
|
||||
"unsupported config server scheme: {}",
|
||||
config_server_url.scheme()
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut c_url = config_server_url.clone();
|
||||
if !matches!(c_url.scheme(), "ws" | "wss") {
|
||||
c_url.set_path("");
|
||||
@@ -278,20 +243,16 @@ pub async fn run_web_client(
|
||||
let mut flags = global_ctx.get_flags();
|
||||
flags.bind_device = false;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
let hostname = match hostname {
|
||||
None => gethostname::gethostname().to_string_lossy().to_string(),
|
||||
Some(hostname) => hostname,
|
||||
};
|
||||
Ok(WebClient::new(
|
||||
ConfigServerConnector {
|
||||
url: c_url,
|
||||
global_ctx,
|
||||
},
|
||||
create_connector_by_url(c_url.as_str(), &global_ctx, IpVersion::Both).await?,
|
||||
token.to_string(),
|
||||
hostname,
|
||||
secure_mode,
|
||||
manager,
|
||||
manager.clone(),
|
||||
hooks,
|
||||
))
|
||||
}
|
||||
@@ -331,23 +292,4 @@ mod tests {
|
||||
assert!(sleep_finish.load(std::sync::atomic::Ordering::Relaxed));
|
||||
println!("Manager stopped.");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_run_web_client_with_unreachable_config_server() {
|
||||
let manager = Arc::new(NetworkInstanceManager::new());
|
||||
let client = super::run_web_client(
|
||||
"udp://config-server.invalid:22020/test",
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
manager,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
assert!(!client.is_connected());
|
||||
drop(client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"private": true,
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"caniuse-lite": "1.0.30001791",
|
||||
"minimatch": "10.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+277
-9
@@ -5,6 +5,7 @@ settings:
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
caniuse-lite: 1.0.30001791
|
||||
minimatch: 10.2.4
|
||||
|
||||
importers:
|
||||
@@ -58,7 +59,7 @@ importers:
|
||||
devDependencies:
|
||||
'@antfu/eslint-config':
|
||||
specifier: ^3.7.3
|
||||
version: 3.16.0(@typescript-eslint/utils@8.42.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3))(@vue/compiler-sfc@3.5.21)(eslint-plugin-format@0.1.3(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)
|
||||
version: 3.16.0(@typescript-eslint/utils@8.42.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3))(@vue/compiler-sfc@3.5.21)(eslint-plugin-format@0.1.3(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)(vitest@2.1.9(@types/node@22.18.1))
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
specifier: ^5.2.0
|
||||
version: 5.3.1(@vue/compiler-dom@3.5.21)(eslint@9.35.0(jiti@2.5.1))(rollup@4.50.1)(typescript@5.6.3)(vue-i18n@10.0.8(vue@3.5.21(typescript@5.6.3)))(vue@3.5.21(typescript@5.6.3))
|
||||
@@ -286,6 +287,9 @@ importers:
|
||||
vite-plugin-dts:
|
||||
specifier: ^4.3.0
|
||||
version: 4.5.4(@types/node@22.18.1)(rollup@4.50.1)(typescript@5.6.3)(vite@5.4.21(@types/node@22.18.1))
|
||||
vitest:
|
||||
specifier: ^2.1.9
|
||||
version: 2.1.9(@types/node@22.18.1)
|
||||
vue-tsc:
|
||||
specifier: ^2.1.10
|
||||
version: 2.2.12(typescript@5.6.3)
|
||||
@@ -1673,6 +1677,35 @@ packages:
|
||||
vitest:
|
||||
optional: true
|
||||
|
||||
'@vitest/expect@2.1.9':
|
||||
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
|
||||
|
||||
'@vitest/mocker@2.1.9':
|
||||
resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9
|
||||
vite: ^5.0.0
|
||||
peerDependenciesMeta:
|
||||
msw:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@2.1.9':
|
||||
resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==}
|
||||
|
||||
'@vitest/runner@2.1.9':
|
||||
resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==}
|
||||
|
||||
'@vitest/snapshot@2.1.9':
|
||||
resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==}
|
||||
|
||||
'@vitest/spy@2.1.9':
|
||||
resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
|
||||
|
||||
'@vitest/utils@2.1.9':
|
||||
resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
|
||||
|
||||
'@volar/language-core@2.4.15':
|
||||
resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==}
|
||||
|
||||
@@ -2072,6 +2105,10 @@ packages:
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ast-kit@1.4.3:
|
||||
resolution: {integrity: sha512-MdJqjpodkS5J149zN0Po+HPshkTdUyrvF7CKTafUgv69vBSPtncrj+3IiUgqdd7ElIEkbeXCsEouBUwLrw9Ilg==}
|
||||
engines: {node: '>=16.14.0'}
|
||||
@@ -2134,6 +2171,10 @@ packages:
|
||||
peerDependencies:
|
||||
esbuild: '>=0.18'
|
||||
|
||||
cac@6.7.14:
|
||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2146,12 +2187,16 @@ packages:
|
||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
caniuse-lite@1.0.30001741:
|
||||
resolution: {integrity: sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==}
|
||||
caniuse-lite@1.0.30001791:
|
||||
resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==}
|
||||
|
||||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
|
||||
chai@5.3.3:
|
||||
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2163,6 +2208,10 @@ packages:
|
||||
resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==}
|
||||
engines: {pnpm: '>=8'}
|
||||
|
||||
check-error@2.1.3:
|
||||
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
@@ -2251,6 +2300,10 @@ packages:
|
||||
decode-named-character-reference@1.2.0:
|
||||
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
|
||||
|
||||
deep-eql@5.0.2:
|
||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
||||
@@ -2328,6 +2381,9 @@ packages:
|
||||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-module-lexer@1.7.0:
|
||||
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2596,6 +2652,10 @@ packages:
|
||||
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
|
||||
engines: {node: '>=16.17'}
|
||||
|
||||
expect-type@1.3.0:
|
||||
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
exsolve@1.0.7:
|
||||
resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
|
||||
|
||||
@@ -3017,6 +3077,9 @@ packages:
|
||||
longest-streak@3.1.0:
|
||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||
|
||||
loupe@3.2.1:
|
||||
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
@@ -3370,6 +3433,10 @@ packages:
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
pathval@2.0.1:
|
||||
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
|
||||
engines: {node: '>= 14.16'}
|
||||
|
||||
perfect-debounce@1.0.0:
|
||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||
|
||||
@@ -3612,6 +3679,9 @@ packages:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
siginfo@2.0.0:
|
||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||
|
||||
signal-exit@3.0.7:
|
||||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||
|
||||
@@ -3664,6 +3734,12 @@ packages:
|
||||
resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
std-env@3.10.0:
|
||||
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
||||
|
||||
string-argv@0.3.2:
|
||||
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
|
||||
engines: {node: '>=0.6.19'}
|
||||
@@ -3753,9 +3829,27 @@ packages:
|
||||
thenify@3.3.1:
|
||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
tinyexec@0.3.2:
|
||||
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
||||
|
||||
tinyexec@1.0.1:
|
||||
resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==}
|
||||
|
||||
tinypool@1.1.1:
|
||||
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
|
||||
tinyrainbow@1.2.0:
|
||||
resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tinyspy@3.0.2:
|
||||
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
@@ -3975,6 +4069,11 @@ packages:
|
||||
peerDependencies:
|
||||
vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0
|
||||
|
||||
vite-node@2.1.9:
|
||||
resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
|
||||
vite-plugin-dts@4.5.4:
|
||||
resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==}
|
||||
peerDependencies:
|
||||
@@ -4050,6 +4149,31 @@ packages:
|
||||
terser:
|
||||
optional: true
|
||||
|
||||
vitest@2.1.9:
|
||||
resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@types/node': ^18.0.0 || >=20.0.0
|
||||
'@vitest/browser': 2.1.9
|
||||
'@vitest/ui': 2.1.9
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
peerDependenciesMeta:
|
||||
'@edge-runtime/vm':
|
||||
optional: true
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
'@vitest/ui':
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
vscode-uri@3.1.0:
|
||||
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
||||
|
||||
@@ -4122,6 +4246,11 @@ packages:
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
word-wrap@1.2.5:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -4180,7 +4309,7 @@ snapshots:
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@antfu/eslint-config@3.16.0(@typescript-eslint/utils@8.42.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3))(@vue/compiler-sfc@3.5.21)(eslint-plugin-format@0.1.3(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)':
|
||||
'@antfu/eslint-config@3.16.0(@typescript-eslint/utils@8.42.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3))(@vue/compiler-sfc@3.5.21)(eslint-plugin-format@0.1.3(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)(vitest@2.1.9(@types/node@22.18.1))':
|
||||
dependencies:
|
||||
'@antfu/install-pkg': 1.1.0
|
||||
'@clack/prompts': 0.9.1
|
||||
@@ -4189,7 +4318,7 @@ snapshots:
|
||||
'@stylistic/eslint-plugin': 2.13.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)
|
||||
'@typescript-eslint/eslint-plugin': 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3))(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)
|
||||
'@typescript-eslint/parser': 8.42.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)
|
||||
'@vitest/eslint-plugin': 1.3.9(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)
|
||||
'@vitest/eslint-plugin': 1.3.9(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)(vitest@2.1.9(@types/node@22.18.1))
|
||||
eslint: 9.35.0(jiti@2.5.1)
|
||||
eslint-config-flat-gitignore: 1.0.1(eslint@9.35.0(jiti@2.5.1))
|
||||
eslint-flat-config-utils: 1.1.0
|
||||
@@ -5386,16 +5515,57 @@ snapshots:
|
||||
vite: 5.4.21(@types/node@22.18.1)
|
||||
vue: 3.5.21(typescript@5.6.3)
|
||||
|
||||
'@vitest/eslint-plugin@1.3.9(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)':
|
||||
'@vitest/eslint-plugin@1.3.9(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)(vitest@2.1.9(@types/node@22.18.1))':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.42.0
|
||||
'@typescript-eslint/utils': 8.42.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)
|
||||
eslint: 9.35.0(jiti@2.5.1)
|
||||
optionalDependencies:
|
||||
typescript: 5.6.3
|
||||
vitest: 2.1.9(@types/node@22.18.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/expect@2.1.9':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.9
|
||||
'@vitest/utils': 2.1.9
|
||||
chai: 5.3.3
|
||||
tinyrainbow: 1.2.0
|
||||
|
||||
'@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.18.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.9
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.18
|
||||
optionalDependencies:
|
||||
vite: 5.4.21(@types/node@22.18.1)
|
||||
|
||||
'@vitest/pretty-format@2.1.9':
|
||||
dependencies:
|
||||
tinyrainbow: 1.2.0
|
||||
|
||||
'@vitest/runner@2.1.9':
|
||||
dependencies:
|
||||
'@vitest/utils': 2.1.9
|
||||
pathe: 1.1.2
|
||||
|
||||
'@vitest/snapshot@2.1.9':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 2.1.9
|
||||
magic-string: 0.30.18
|
||||
pathe: 1.1.2
|
||||
|
||||
'@vitest/spy@2.1.9':
|
||||
dependencies:
|
||||
tinyspy: 3.0.2
|
||||
|
||||
'@vitest/utils@2.1.9':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 2.1.9
|
||||
loupe: 3.2.1
|
||||
tinyrainbow: 1.2.0
|
||||
|
||||
'@volar/language-core@2.4.15':
|
||||
dependencies:
|
||||
'@volar/source-map': 2.4.15
|
||||
@@ -5970,6 +6140,8 @@ snapshots:
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
ast-kit@1.4.3:
|
||||
dependencies:
|
||||
'@babel/parser': 7.28.4
|
||||
@@ -5985,7 +6157,7 @@ snapshots:
|
||||
autoprefixer@10.4.21(postcss@8.5.6):
|
||||
dependencies:
|
||||
browserslist: 4.25.4
|
||||
caniuse-lite: 1.0.30001741
|
||||
caniuse-lite: 1.0.30001791
|
||||
fraction.js: 4.3.7
|
||||
normalize-range: 0.1.2
|
||||
picocolors: 1.1.1
|
||||
@@ -6018,7 +6190,7 @@ snapshots:
|
||||
|
||||
browserslist@4.25.4:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001741
|
||||
caniuse-lite: 1.0.30001791
|
||||
electron-to-chromium: 1.5.214
|
||||
node-releases: 2.0.20
|
||||
update-browserslist-db: 1.1.3(browserslist@4.25.4)
|
||||
@@ -6034,6 +6206,8 @@ snapshots:
|
||||
esbuild: 0.25.9
|
||||
load-tsconfig: 0.2.5
|
||||
|
||||
cac@6.7.14: {}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -6043,10 +6217,18 @@ snapshots:
|
||||
|
||||
camelcase-css@2.0.1: {}
|
||||
|
||||
caniuse-lite@1.0.30001741: {}
|
||||
caniuse-lite@1.0.30001791: {}
|
||||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
chai@5.3.3:
|
||||
dependencies:
|
||||
assertion-error: 2.0.1
|
||||
check-error: 2.1.3
|
||||
deep-eql: 5.0.2
|
||||
loupe: 3.2.1
|
||||
pathval: 2.0.1
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
@@ -6058,6 +6240,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@kurkle/color': 0.3.4
|
||||
|
||||
check-error@2.1.3: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
dependencies:
|
||||
anymatch: 3.1.3
|
||||
@@ -6138,6 +6322,8 @@ snapshots:
|
||||
dependencies:
|
||||
character-entities: 2.0.2
|
||||
|
||||
deep-eql@5.0.2: {}
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
|
||||
default-browser-id@5.0.0: {}
|
||||
@@ -6198,6 +6384,8 @@ snapshots:
|
||||
|
||||
es-errors@1.3.0: {}
|
||||
|
||||
es-module-lexer@1.7.0: {}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -6614,6 +6802,8 @@ snapshots:
|
||||
signal-exit: 4.1.0
|
||||
strip-final-newline: 3.0.0
|
||||
|
||||
expect-type@1.3.0: {}
|
||||
|
||||
exsolve@1.0.7: {}
|
||||
|
||||
extend-shallow@2.0.1:
|
||||
@@ -6977,6 +7167,8 @@ snapshots:
|
||||
|
||||
longest-streak@3.1.0: {}
|
||||
|
||||
loupe@3.2.1: {}
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
@@ -7502,6 +7694,8 @@ snapshots:
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
pathval@2.0.1: {}
|
||||
|
||||
perfect-debounce@1.0.0: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
@@ -7738,6 +7932,8 @@ snapshots:
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
siginfo@2.0.0: {}
|
||||
|
||||
signal-exit@3.0.7: {}
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
@@ -7785,6 +7981,10 @@ snapshots:
|
||||
|
||||
stable-hash-x@0.2.0: {}
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
std-env@3.10.0: {}
|
||||
|
||||
string-argv@0.3.2: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
@@ -7895,8 +8095,18 @@ snapshots:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
|
||||
tinyexec@1.0.1: {}
|
||||
|
||||
tinypool@1.1.1: {}
|
||||
|
||||
tinyrainbow@1.2.0: {}
|
||||
|
||||
tinyspy@3.0.2: {}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
@@ -8199,6 +8409,24 @@ snapshots:
|
||||
dependencies:
|
||||
vite: 5.4.21(@types/node@22.18.1)
|
||||
|
||||
vite-node@2.1.9(@types/node@22.18.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.1
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 1.1.2
|
||||
vite: 5.4.21(@types/node@22.18.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite-plugin-dts@4.5.4(@types/node@22.18.1)(rollup@4.50.1)(typescript@5.6.3)(vite@5.4.21(@types/node@22.18.1)):
|
||||
dependencies:
|
||||
'@microsoft/api-extractor': 7.52.11(@types/node@22.18.1)
|
||||
@@ -8290,6 +8518,41 @@ snapshots:
|
||||
'@types/node': 22.18.1
|
||||
fsevents: 2.3.3
|
||||
|
||||
vitest@2.1.9(@types/node@22.18.1):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.9
|
||||
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.18.1))
|
||||
'@vitest/pretty-format': 2.1.9
|
||||
'@vitest/runner': 2.1.9
|
||||
'@vitest/snapshot': 2.1.9
|
||||
'@vitest/spy': 2.1.9
|
||||
'@vitest/utils': 2.1.9
|
||||
chai: 5.3.3
|
||||
debug: 4.4.1
|
||||
expect-type: 1.3.0
|
||||
magic-string: 0.30.18
|
||||
pathe: 1.1.2
|
||||
std-env: 3.10.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 0.3.2
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 1.2.0
|
||||
vite: 5.4.21(@types/node@22.18.1)
|
||||
vite-node: 2.1.9(@types/node@22.18.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.18.1
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vscode-uri@3.1.0: {}
|
||||
|
||||
vue-chartjs@5.3.2(chart.js@4.5.0)(vue@3.5.21(typescript@5.6.3)):
|
||||
@@ -8359,6 +8622,11 @@ snapshots:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
dependencies:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
|
||||
Reference in New Issue
Block a user