Compare commits

..

15 Commits

Author SHA1 Message Date
fanyang b80801cfdb fix: avoid selecting unknown-latency peer conns 2026-05-04 19:53:10 +08:00
fanyang e072587721 feat: support listener connection priorities 2026-05-04 10:52:15 +08:00
fanyang 362aa7a9cd fix: allow omitted ACL config fields (#2206) 2026-05-04 00:47:24 +08:00
KKRainbow 12a7b5a5c5 fix: scope peer center server data to instance (#2198)
Stop sharing PeerCenterServer state through a process-global map so local and foreign-network services cannot mix peer-center data when peer ids overlap.
2026-05-02 01:43:01 +08:00
fanyang 4eba9b07b6 fix(web-client): keep retrying unreachable config server (#2140)
Defer config-server connector creation into the web client retry loop so
service startup does not fail when network or DNS is unavailable.
2026-05-02 00:09:48 +08:00
KKRainbow 1b48029bdc fix: clean stale foreign network state (#2197)
- clear foreign-network traffic metric peer caches on peer removal and network cleanup
- release reserved foreign-network peer IDs on handshake/add-peer error paths
- avoid creating no-op foreign-network token buckets when limits are unlimited
- shrink relay/session maps after cleanup and remove unused peer-center global data entries
2026-05-01 23:30:51 +08:00
KKRainbow 3542e944cb fix(quic): prune stopped endpoints from pool (#2195)
* remove wss port 0 compatibility code
* fix(quic): prune stopped endpoints from pool
2026-05-01 18:51:39 +08:00
KKRainbow 852d1c9e14 feat(gui): add UPnP and public IPv6 advanced options (#2194)
Expose disable-upnp and ipv6_public_addr_auto in the shared web/GUI config editor
bump release metadata to 2.6.3.
2026-05-01 13:45:19 +08:00
KKRainbow 4958394469 fix: protect self peer during credential refresh and allow need-p2p peers through public server (#2192)
* fix: protect self peer during credential refresh

* fix: allow need-p2p peers through public server
2026-05-01 06:59:30 +08:00
KKRainbow 41b6d65604 fix faketcp filter on windows (#2190) 2026-04-30 23:55:56 +08:00
KKRainbow aae30894dd fix: keep file logger disabled by default (#2189) 2026-04-30 21:42:30 +08:00
fanyang 81d169abfc fix: fall back when CLI manage service is unavailable (#2185) 2026-04-30 19:50:50 +08:00
Luna Yao 9c6c210e89 fix: disable SO_EXCLUSIVEADDRUSE on Windows (#2180) 2026-04-30 19:48:54 +08:00
Mg Pig d1c6dcf754 fix: prevent URL input layout flicker with container queries (#2186) 2026-04-30 19:45:01 +08:00
KKRainbow 97c8c4f55a feat: support disabling relay data forwarding (#2188)
- add a disable_relay_data runtime/config patch option
- reuse the existing avoid_relay_data feature flag when relay data forwarding is disabled
2026-04-30 19:44:40 +08:00
67 changed files with 3076 additions and 970 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ on:
image_tag:
description: 'Tag for this image build'
type: string
default: 'v2.6.2'
default: 'v2.6.3'
required: true
mark_latest:
description: 'Mark this image as latest'
+1 -1
View File
@@ -18,7 +18,7 @@ on:
version:
description: 'Version for this release'
type: string
default: 'v2.6.2'
default: 'v2.6.3'
required: true
make_latest:
description: 'Mark this release as latest'
Generated
+3 -3
View File
@@ -2229,7 +2229,7 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "easytier"
version = "2.6.2"
version = "2.6.3"
dependencies = [
"aes-gcm",
"anyhow",
@@ -2405,7 +2405,7 @@ dependencies = [
[[package]]
name = "easytier-gui"
version = "2.6.2"
version = "2.6.3"
dependencies = [
"anyhow",
"async-trait",
@@ -2486,7 +2486,7 @@ dependencies = [
[[package]]
name = "easytier-web"
version = "2.6.2"
version = "2.6.3"
dependencies = [
"anyhow",
"async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
id=easytier_magisk
name=EasyTier_Magisk
version=v2.6.2
version=v2.6.3
versionCode=1
author=EasyTier
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
@@ -8,7 +8,8 @@ use anyhow::Context as _;
use dashmap::DashMap;
use easytier::{
common::config::{
ConfigFileControl, ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader,
ConfigFileControl, ConfigLoader, DEFAULT_CONNECTION_PRIORITY, NetworkIdentity, PeerConfig,
TomlConfigLoader,
},
instance_manager::NetworkInstanceManager,
};
@@ -360,6 +361,7 @@ impl HealthChecker {
.parse()
.with_context(|| "failed to parse peer uri")?,
peer_public_key: None,
priority: DEFAULT_CONNECTION_PRIORITY,
}]);
let inst_id = inst_id.unwrap_or(uuid::Uuid::new_v4());
+1
View File
@@ -2,6 +2,7 @@
<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>
+3 -4
View File
@@ -1,13 +1,12 @@
{
"name": "easytier-gui",
"type": "module",
"version": "2.6.2",
"version": "2.6.3",
"private": true,
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
"scripts": {
"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",
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"tauri": "tauri",
"lint": "eslint . --ignore-pattern src-tauri",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "easytier-gui"
version = "2.6.2"
version = "2.6.3"
description = "EasyTier GUI"
authors = ["you"]
edition.workspace = true
+1 -1
View File
@@ -17,7 +17,7 @@
"createUpdaterArtifacts": false
},
"productName": "easytier-gui",
"version": "2.6.2",
"version": "2.6.3",
"identifier": "com.kkrainbow.easytier",
"plugins": {
"shell": {
-1
View File
@@ -33,7 +33,6 @@ const host = process.env.TAURI_DEV_HOST
// https://vitejs.dev/config/
export default defineConfig(async () => ({
base: './',
resolve: {
alias: {
'~/': `${path.resolve(__dirname, 'src')}/`,
+2 -2
View File
@@ -1,11 +1,11 @@
[package]
name = "easytier-web"
version = "2.6.2"
version = "2.6.3"
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", default-features = false, features = ["websocket"] }
easytier = { path = "../easytier" }
tracing = { version = "0.1", features = ["log"] }
anyhow = { version = "1.0" }
thiserror = "1.0"
+1 -3
View File
@@ -15,7 +15,6 @@
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"test": "vitest run",
"preview": "vite preview"
},
"dependencies": {
@@ -44,11 +43,10 @@
"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,6 +81,7 @@ 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' },
@@ -98,6 +99,7 @@ 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,9 +2,8 @@
import { AutoComplete, Button, Dialog, InputNumber, InputText } from 'primevue'
import InputGroup from 'primevue/inputgroup'
import InputGroupAddon from 'primevue/inputgroupaddon'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { buildUrlInputValue, getHostInputValue, parseHostInputOnBlur, parseUrlInput } from '../modules/url-input'
const props = defineProps<{
placeholder?: string
@@ -14,30 +13,75 @@ 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)
onMounted(() => {
if (container.value) {
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
internalCompact.value = entry.contentRect.width < 400
}
})
observer.observe(container.value)
onUnmounted(() => {
observer.disconnect()
})
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 }
}
}
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 }
}
})
const internalValue = ref(parseUrlInput(url.value, props.protos))
if (!val) {
return { proto: 'tcp', host: '', port: props.protos['tcp'] ?? 11010 }
}
const parsedByPattern = parseByPattern(val)
if (parsedByPattern) {
return parsedByPattern
}
return { proto: 'tcp', host: '', port: null }
}
const internalValue = ref(parseUrl(url.value))
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 = buildUrlInputValue(internalValue.value, props.protos, forceDefaultHost)
const nextUrl = buildUrlValue(internalValue.value, forceDefaultHost)
if (!nextUrl || nextUrl === url.value) {
return
}
@@ -46,10 +90,6 @@ 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)
}
@@ -66,20 +106,12 @@ 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 = parseUrlInput(newVal, props.protos)
const parsed = parseUrl(newVal)
const internalHost = internalValue.value.host ?? ''
const sameHost = parsed.host === internalHost || (!internalHost.trim() && parsed.host === defaultHost)
if (parsed.proto !== internalValue.value.proto ||
@@ -91,9 +123,6 @@ watch(() => url.value, (newVal) => {
// Sync to external
watch(internalValue, () => {
if (hostFocused.value) {
return
}
syncUrlFromInternal(false)
}, { deep: true })
@@ -119,34 +148,34 @@ const onProtoChange = (newProto: string) => {
internalValue.value.port = newDefault
}
internalValue.value.proto = newProto
internalValue.value.suffix = undefined
internalValue.value.hasExplicitPort = true
}
</script>
<template>
<div ref="container" class="w-full">
<InputGroup v-if="!internalCompact" class="w-full">
<div class="url-input-container w-full min-w-0 overflow-hidden">
<InputGroup class="url-input-full w-full min-w-0">
<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="hostInputValue" :placeholder="placeholder || '0.0.0.0'" class="grow"
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="grow min-w-0"
@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 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" />
<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" />
<slot name="actions"></slot>
</div>
</div>
@@ -160,7 +189,7 @@ const onProtoChange = (newProto: string) => {
</div>
<div class="flex flex-col gap-2">
<label>{{ t('web.common.address') || 'Address' }}</label>
<InputText v-model="hostInputValue" :placeholder="placeholder || '0.0.0.0'" class="w-full"
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="w-full"
@focus="onHostFocus" @blur="onHostBlur" />
</div>
<div v-if="!isNoPortProto" class="flex flex-col gap-2">
@@ -178,6 +207,28 @@ 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,6 +104,9 @@ 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 流量,降低传输延迟,提升传输速度。
@@ -157,6 +160,9 @@ 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,6 +103,9 @@ 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.
@@ -156,6 +159,9 @@ 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
@@ -1,184 +0,0 @@
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)
})
})
@@ -1,105 +0,0 @@
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,6 +115,7 @@ 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
@@ -132,6 +133,7 @@ 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
@@ -190,6 +192,7 @@ 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,
@@ -207,6 +210,7 @@ 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: [],
+3 -4
View File
@@ -4,9 +4,8 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"build:deps": "pnpm --filter easytier-frontend-lib build",
"dev": "pnpm run build:deps && vite",
"build": "pnpm run build:deps && vue-tsc -b && vite build",
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
@@ -33,4 +32,4 @@
"vite-plugin-singlefile": "^2.0.3",
"vue-tsc": "^2.1.10"
}
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ name = "easytier"
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
homepage = "https://github.com/EasyTier/EasyTier"
repository = "https://github.com/EasyTier/EasyTier"
version = "2.6.2"
version = "2.6.3"
edition.workspace = true
rust-version.workspace = true
authors = ["kkrainbow"]
+5
View File
@@ -191,6 +191,11 @@ 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(["."])
+7 -6
View File
@@ -137,12 +137,13 @@ 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)?;
+39
View File
@@ -1339,6 +1339,45 @@ 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();
+198 -5
View File
@@ -28,6 +28,55 @@ use super::env_parser;
pub type Flags = crate::proto::common::FlagsInConfig;
pub const DEFAULT_CONNECTION_PRIORITY: u32 = 0;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(untagged)]
enum ListenerConfigDef {
Url(url::Url),
Config {
url: url::Url,
#[serde(default)]
priority: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(from = "ListenerConfigDef")]
pub struct ListenerConfig {
pub url: url::Url,
pub priority: u32,
}
impl ListenerConfig {
pub fn new(url: url::Url, priority: u32) -> Self {
Self { url, priority }
}
pub fn with_default_priority(url: url::Url) -> Self {
Self::new(url, DEFAULT_CONNECTION_PRIORITY)
}
}
impl From<url::Url> for ListenerConfig {
fn from(url: url::Url) -> Self {
Self::with_default_priority(url)
}
}
impl From<ListenerConfigDef> for ListenerConfig {
fn from(def: ListenerConfigDef) -> Self {
match def {
ListenerConfigDef::Url(url) => Self::with_default_priority(url),
ListenerConfigDef::Config { url, priority } => Self::new(url, priority),
}
}
}
fn listener_config_urls(listeners: Vec<ListenerConfig>) -> Vec<url::Url> {
listeners.into_iter().map(|listener| listener.url).collect()
}
pub fn gen_default_flags() -> Flags {
#[allow(deprecated)]
Flags {
@@ -71,6 +120,7 @@ pub fn gen_default_flags() -> Flags {
need_p2p: false,
instance_recv_bps_limit: u64::MAX,
disable_upnp: false,
disable_relay_data: false,
}
}
@@ -195,6 +245,7 @@ pub trait ConfigLoader: Send + Sync {
fn set_network_identity(&self, identity: NetworkIdentity);
fn get_listener_uris(&self) -> Vec<url::Url>;
fn get_listener_configs(&self) -> Vec<ListenerConfig>;
fn get_peers(&self) -> Vec<PeerConfig>;
fn set_peers(&self, peers: Vec<PeerConfig>);
@@ -204,6 +255,8 @@ pub trait ConfigLoader: Send + Sync {
fn get_mapped_listeners(&self) -> Vec<url::Url>;
fn set_mapped_listeners(&self, listeners: Option<Vec<url::Url>>);
fn get_mapped_listener_configs(&self) -> Vec<ListenerConfig>;
fn set_mapped_listener_configs(&self, listeners: Option<Vec<ListenerConfig>>);
fn get_vpn_portal_config(&self) -> Option<VpnPortalConfig>;
fn set_vpn_portal_config(&self, config: VpnPortalConfig);
@@ -402,6 +455,8 @@ impl Default for NetworkIdentity {
pub struct PeerConfig {
pub uri: url::Url,
pub peer_public_key: Option<String>,
#[serde(default)]
pub priority: u32,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
@@ -533,8 +588,8 @@ struct Config {
ipv6_public_addr_prefix: Option<String>,
dhcp: Option<bool>,
network_identity: Option<NetworkIdentity>,
listeners: Option<Vec<url::Url>>,
mapped_listeners: Option<Vec<url::Url>>,
listeners: Option<Vec<ListenerConfig>>,
mapped_listeners: Option<Vec<ListenerConfig>>,
exit_nodes: Option<Vec<IpAddr>>,
peer: Option<Vec<PeerConfig>>,
@@ -848,6 +903,10 @@ impl ConfigLoader for TomlConfigLoader {
}
fn get_listener_uris(&self) -> Vec<url::Url> {
listener_config_urls(self.get_listener_configs())
}
fn get_listener_configs(&self) -> Vec<ListenerConfig> {
self.config
.lock()
.unwrap()
@@ -865,14 +924,29 @@ impl ConfigLoader for TomlConfigLoader {
}
fn get_listeners(&self) -> Option<Vec<url::Url>> {
self.config.lock().unwrap().listeners.clone()
self.config
.lock()
.unwrap()
.listeners
.clone()
.map(listener_config_urls)
}
fn set_listeners(&self, listeners: Vec<url::Url>) {
self.config.lock().unwrap().listeners = Some(listeners);
self.config.lock().unwrap().listeners =
Some(listeners.into_iter().map(Into::into).collect());
}
fn get_mapped_listeners(&self) -> Vec<url::Url> {
listener_config_urls(self.get_mapped_listener_configs())
}
fn set_mapped_listeners(&self, listeners: Option<Vec<url::Url>>) {
self.config.lock().unwrap().mapped_listeners =
listeners.map(|listeners| listeners.into_iter().map(Into::into).collect());
}
fn get_mapped_listener_configs(&self) -> Vec<ListenerConfig> {
self.config
.lock()
.unwrap()
@@ -881,7 +955,7 @@ impl ConfigLoader for TomlConfigLoader {
.unwrap_or_default()
}
fn set_mapped_listeners(&self, listeners: Option<Vec<url::Url>>) {
fn set_mapped_listener_configs(&self, listeners: Option<Vec<ListenerConfig>>) {
self.config.lock().unwrap().mapped_listeners = listeners;
}
@@ -1336,6 +1410,125 @@ 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_listener_priority_config_supports_old_and_structured_values() {
let config = TomlConfigLoader::new_from_str(
r#"
listeners = [
"tcp://0.0.0.0:11010",
{ url = "udp://0.0.0.0:11010", priority = 80 },
]
mapped_listeners = [
"tcp://example.com:11010",
{ url = "tcp://frps.example.com:30001", priority = 100 },
]
[[peer]]
uri = "tcp://proxy.example.com:443"
priority = 100
[[peer]]
uri = "tcp://normal.example.com:11010"
"#,
)
.unwrap();
let listeners = config.get_listener_configs();
assert_eq!(listeners[0].url.to_string(), "tcp://0.0.0.0:11010");
assert_eq!(listeners[0].priority, DEFAULT_CONNECTION_PRIORITY);
assert_eq!(listeners[1].url.to_string(), "udp://0.0.0.0:11010");
assert_eq!(listeners[1].priority, 80);
let mapped_listeners = config.get_mapped_listener_configs();
assert_eq!(
mapped_listeners[0].url.to_string(),
"tcp://example.com:11010"
);
assert_eq!(mapped_listeners[0].priority, DEFAULT_CONNECTION_PRIORITY);
assert_eq!(
mapped_listeners[1].url.to_string(),
"tcp://frps.example.com:30001"
);
assert_eq!(mapped_listeners[1].priority, 100);
let peers = config.get_peers();
assert_eq!(peers[0].uri.to_string(), "tcp://proxy.example.com:443");
assert_eq!(peers[0].priority, 100);
assert_eq!(peers[1].uri.to_string(), "tcp://normal.example.com:11010");
assert_eq!(peers[1].priority, DEFAULT_CONNECTION_PRIORITY);
let dumped = config.dump();
let reloaded = TomlConfigLoader::new_from_str(&dumped).unwrap();
assert_eq!(reloaded.get_listener_configs(), listeners);
assert_eq!(reloaded.get_mapped_listener_configs(), mapped_listeners);
assert_eq!(reloaded.get_peers(), peers);
}
#[test]
fn test_network_config_source_user_is_implicit() {
let config = TomlConfigLoader::default();
+19 -10
View File
@@ -73,16 +73,6 @@ 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 {
@@ -139,4 +129,23 @@ 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))]
);
}
}
}
+170 -17
View File
@@ -11,7 +11,7 @@ use dashmap::DashMap;
use super::{
PeerId,
config::{ConfigLoader, Flags},
config::{ConfigLoader, DEFAULT_CONNECTION_PRIORITY, Flags, ListenerConfig},
netns::NetNS,
network::IPCollector,
stun::{StunInfoCollector, StunInfoCollectorTrait},
@@ -212,11 +212,17 @@ pub struct GlobalCtx {
stun_info_collection: Mutex<Arc<dyn StunInfoCollectorTrait>>,
running_listeners: Mutex<Vec<url::Url>>,
running_listeners: Mutex<Vec<ListenerConfig>>,
advertised_ipv6_public_addr_prefix: Mutex<Option<cidr::Ipv6Cidr>>,
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,
@@ -247,8 +253,17 @@ impl std::fmt::Debug for GlobalCtx {
pub type ArcGlobalCtx = std::sync::Arc<GlobalCtx>;
impl GlobalCtx {
fn derive_feature_flags(flags: &Flags, current: Option<PeerFeatureFlag>) -> PeerFeatureFlag {
let mut feature_flags = current.unwrap_or_default();
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 {
feature_flags.kcp_input = !flags.disable_kcp_input;
feature_flags.no_relay_kcp = flags.disable_relay_kcp;
feature_flags.support_conn_list_sync = true;
@@ -256,7 +271,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;
feature_flags
Self::apply_disable_relay_data_flag(flags, feature_flags)
}
pub fn new(config_fs: impl ConfigLoader + 'static) -> Self {
@@ -285,7 +300,8 @@ impl GlobalCtx {
let flags = config_fs.get_flags();
let feature_flags = Self::derive_feature_flags(&flags, None);
let base_feature_flags = PeerFeatureFlag::default();
let feature_flags = Self::derive_feature_flags(&flags, base_feature_flags);
let credential_storage_path = config_fs.get_credential_file();
let credential_manager = Arc::new(CredentialManager::new(credential_storage_path));
@@ -318,6 +334,8 @@ 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(),
@@ -491,13 +509,28 @@ impl GlobalCtx {
}
pub fn get_running_listeners(&self) -> Vec<url::Url> {
self.running_listeners
.lock()
.unwrap()
.iter()
.map(|listener| listener.url.clone())
.collect()
}
pub fn get_running_listener_configs(&self) -> Vec<ListenerConfig> {
self.running_listeners.lock().unwrap().clone()
}
pub fn add_running_listener(&self, url: url::Url) {
self.add_running_listener_with_priority(url, DEFAULT_CONNECTION_PRIORITY);
}
pub fn add_running_listener_with_priority(&self, url: url::Url, priority: u32) {
let mut l = self.running_listeners.lock().unwrap();
if !l.contains(&url) {
l.push(url);
if let Some(listener) = l.iter_mut().find(|listener| listener.url == url) {
listener.priority = priority;
} else {
l.push(ListenerConfig::new(url, priority));
}
}
@@ -513,7 +546,7 @@ impl GlobalCtx {
self.config.set_flags(flags.clone());
self.feature_flags.store(Self::derive_feature_flags(
&flags,
Some(self.feature_flags.load()),
self.base_feature_flags.load(),
));
self.flags.store(Arc::new(flags));
}
@@ -578,8 +611,53 @@ impl GlobalCtx {
self.feature_flags.load()
}
pub fn set_feature_flags(&self, flags: PeerFeatureFlag) {
self.feature_flags.store(flags);
/// 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 token_bucket_manager(&self) -> &TokenBucketManager {
@@ -681,11 +759,9 @@ impl GlobalCtx {
}
fn is_port_in_running_listeners(&self, port: u16, is_udp: bool) -> bool {
self.running_listeners
.lock()
.unwrap()
.iter()
.any(|x| x.port() == Some(port) && matches_protocol!(x, Protocol::UDP) == is_udp)
self.running_listeners.lock().unwrap().iter().any(|x| {
x.url.port() == Some(port) && matches_protocol!(&x.url, Protocol::UDP) == is_udp
})
}
#[tracing::instrument(ret, skip(self))]
@@ -796,7 +872,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_feature_flags(feature_flags);
global_ctx.set_base_advertised_feature_flags(feature_flags);
let mut flags = global_ctx.get_flags().clone();
flags.disable_kcp_input = true;
@@ -820,6 +896,83 @@ 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();
+193 -11
View File
@@ -58,6 +58,21 @@ 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}::"))
}
@@ -165,14 +180,17 @@ fn file_layers(
) -> anyhow::Result<(Vec<BoxLayer>, Option<NewFilterSender>)> {
let mut layers = Vec::new();
let level = config.level.map(|s| s.parse().unwrap());
let level = config
.level
.map(|s| s.parse().unwrap())
.unwrap_or(LevelFilter::OFF);
if matches!(level, Some(LevelFilter::OFF)) && !reload {
if matches!(level, LevelFilter::OFF) && !reload {
return Ok((layers, None));
}
let (file_filter, file_filter_reloader) =
tracing_subscriber::reload::Layer::<_, Registry>::new(parse_env_filter(level)?);
tracing_subscriber::reload::Layer::<_, Registry>::new(parse_file_filter(level)?);
let layer = |wrapper| {
layer()
@@ -218,9 +236,7 @@ fn file_layers(
// 初始化全局状态
let _ = LOGGER_LEVEL_SENDER.set(std::sync::Mutex::new(tx.clone()));
if let Some(level) = level {
let _ = CURRENT_LOG_LEVEL.set(std::sync::Mutex::new(level.to_string()));
}
let _ = CURRENT_LOG_LEVEL.set(std::sync::Mutex::new(level.to_string()));
std::thread::spawn(move || {
while let Ok(lf) = rx.recv() {
@@ -232,11 +248,7 @@ fn file_layers(
}
};
let mut new_filter = match EnvFilter::builder()
.with_default_directive(parsed_level.into())
.from_env()
.with_context(|| "failed to create file filter")
{
let mut new_filter = match parse_file_filter(parsed_level) {
Ok(filter) => Some(filter),
Err(e) => {
error!("Failed to build new log filter for {:?}: {:?}", lf, e);
@@ -268,6 +280,36 @@ 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()
@@ -276,7 +318,147 @@ 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);
+233 -54
View File
@@ -13,8 +13,8 @@ use std::{
use crate::{
common::{
PeerId, dns::socket_addrs, error::Error, global_ctx::ArcGlobalCtx,
stun::StunInfoCollectorTrait,
PeerId, config::DEFAULT_CONNECTION_PRIORITY, dns::socket_addrs, error::Error,
global_ctx::ArcGlobalCtx, stun::StunInfoCollectorTrait,
},
connector::udp_hole_punch::handle_rpc_result,
peers::{
@@ -31,7 +31,7 @@ use crate::{
},
rpc_types::controller::BaseController,
},
tunnel::{IpVersion, matches_protocol, udp::UdpTunnelConnector},
tunnel::{IpVersion, PrioritizedConnector, matches_protocol, udp::UdpTunnelConnector},
use_global_var,
};
@@ -48,6 +48,7 @@ use url::Host;
pub const DIRECT_CONNECTOR_SERVICE_ID: u32 = 1;
pub const DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC: u64 = 300;
const DIRECT_CONNECTOR_LOW_PRIORITY_RETRY_TIMEOUT_SEC: u64 = 300;
static TESTING: AtomicBool = AtomicBool::new(false);
@@ -131,11 +132,70 @@ struct DstBlackListItem(PeerId, String);
#[derive(Hash, Eq, PartialEq, Clone)]
struct DstListenerUrlBlackListItem(PeerId, String);
#[derive(Clone, Debug)]
struct AvailableListener {
url: url::Url,
priority: u32,
}
fn available_listeners_from_ip_list(
ip_list: &GetIpListResponse,
enable_ipv6: bool,
) -> Vec<AvailableListener> {
let candidate_listeners: Vec<AvailableListener> = if ip_list.listener_infos.is_empty() {
ip_list
.listeners
.iter()
.map(|url| AvailableListener {
url: url.clone().into(),
priority: DEFAULT_CONNECTION_PRIORITY,
})
.collect()
} else {
ip_list
.listener_infos
.iter()
.filter_map(|info| {
info.url.as_ref().map(|url| AvailableListener {
url: url.clone().into(),
priority: info.priority,
})
})
.collect()
};
candidate_listeners
.into_iter()
.filter(|l| l.url.scheme() != "ring")
.filter(|l| {
mapped_listener_port(&l.url).is_some()
&& l.url
.host()
.is_some_and(|host| enable_ipv6 || !matches!(host, Host::Ipv6(_)))
})
.collect()
}
fn sort_available_listeners(available_listeners: &mut [AvailableListener], default_protocol: &str) {
available_listeners.sort_by_key(|l| {
let scheme = l.url.scheme();
let protocol_priority = if scheme == default_protocol {
3
} else if scheme == "udp" {
2
} else {
1
};
(std::cmp::Reverse(l.priority), protocol_priority)
});
}
struct DirectConnectorManagerData {
global_ctx: ArcGlobalCtx,
peer_manager: Arc<PeerManager>,
dst_listener_blacklist: timedmap::TimedMap<DstListenerUrlBlackListItem, ()>,
peer_black_list: timedmap::TimedMap<PeerId, ()>,
low_priority_direct_retry_backoff: timedmap::TimedMap<PeerId, ()>,
}
impl DirectConnectorManagerData {
@@ -145,6 +205,7 @@ impl DirectConnectorManagerData {
peer_manager,
dst_listener_blacklist: timedmap::TimedMap::new(),
peer_black_list: timedmap::TimedMap::new(),
low_priority_direct_retry_backoff: timedmap::TimedMap::new(),
}
}
@@ -201,6 +262,7 @@ impl DirectConnectorManagerData {
&self,
dst_peer_id: PeerId,
remote_url: &url::Url,
priority: u32,
) -> Result<(PeerId, PeerConnId), Error> {
let local_socket = Arc::new(
UdpSocket::bind("[::]:0")
@@ -239,7 +301,12 @@ impl DirectConnectorManagerData {
// NOTICE: must add as directly connected tunnel
self.peer_manager
.add_client_tunnel_with_peer_id_hint(ret, true, Some(dst_peer_id))
.add_client_tunnel_with_peer_id_hint_and_priority(
ret,
true,
Some(dst_peer_id),
priority,
)
.await
}
@@ -247,6 +314,7 @@ impl DirectConnectorManagerData {
&self,
dst_peer_id: PeerId,
remote_url: &url::Url,
priority: u32,
) -> Result<(PeerId, PeerConnId), Error> {
let local_socket = {
let _g = self.global_ctx.net_ns.guard();
@@ -275,21 +343,34 @@ impl DirectConnectorManagerData {
.await?;
self.peer_manager
.add_client_tunnel_with_peer_id_hint(ret, true, Some(dst_peer_id))
.add_client_tunnel_with_peer_id_hint_and_priority(
ret,
true,
Some(dst_peer_id),
priority,
)
.await
}
async fn do_try_connect_to_ip(&self, dst_peer_id: PeerId, addr: String) -> Result<(), Error> {
async fn do_try_connect_to_ip(
&self,
dst_peer_id: PeerId,
addr: String,
priority: u32,
) -> Result<(), Error> {
let connector = create_connector_by_url(&addr, &self.global_ctx, IpVersion::Both).await?;
let remote_url = connector.remote_url();
let (peer_id, conn_id) = if matches_scheme!(remote_url, TunnelScheme::Ip(IpScheme::Udp)) {
match remote_url.host() {
Some(Host::Ipv6(_)) => {
self.connect_to_public_ipv6(dst_peer_id, &remote_url)
self.connect_to_public_ipv6(dst_peer_id, &remote_url, priority)
.await?
}
Some(Host::Ipv4(ip)) if is_public_ipv4(ip) => {
match self.connect_to_public_ipv4(dst_peer_id, &remote_url).await {
match self
.connect_to_public_ipv4(dst_peer_id, &remote_url, priority)
.await
{
Ok(ret) => ret,
Err(err) => {
tracing::debug!(
@@ -300,7 +381,7 @@ impl DirectConnectorManagerData {
timeout(
std::time::Duration::from_secs(3),
self.peer_manager.try_direct_connect_with_peer_id_hint(
connector,
PrioritizedConnector::new(connector, priority),
Some(dst_peer_id),
),
)
@@ -311,8 +392,10 @@ impl DirectConnectorManagerData {
_ => {
timeout(
std::time::Duration::from_secs(3),
self.peer_manager
.try_direct_connect_with_peer_id_hint(connector, Some(dst_peer_id)),
self.peer_manager.try_direct_connect_with_peer_id_hint(
PrioritizedConnector::new(connector, priority),
Some(dst_peer_id),
),
)
.await??
}
@@ -320,8 +403,10 @@ impl DirectConnectorManagerData {
} else {
timeout(
std::time::Duration::from_secs(3),
self.peer_manager
.try_direct_connect_with_peer_id_hint(connector, Some(dst_peer_id)),
self.peer_manager.try_direct_connect_with_peer_id_hint(
PrioritizedConnector::new(connector, priority),
Some(dst_peer_id),
),
)
.await??
};
@@ -345,6 +430,7 @@ impl DirectConnectorManagerData {
self: Arc<DirectConnectorManagerData>,
dst_peer_id: PeerId,
addr: String,
priority: u32,
) -> Result<(), Error> {
let mut rand_gen = rand::rngs::OsRng;
let backoff_ms = [1000, 2000, 4000];
@@ -361,19 +447,26 @@ impl DirectConnectorManagerData {
return Err(Error::UrlInBlacklist);
}
let has_good_direct_conn = || {
self.peer_manager
.has_directly_connected_conn_with_priority_at_most(dst_peer_id, priority)
};
loop {
if self.peer_manager.has_directly_connected_conn(dst_peer_id) {
if has_good_direct_conn() {
return Ok(());
}
tracing::debug!(?dst_peer_id, ?addr, "try_connect_to_ip start one round");
let ret = self.do_try_connect_to_ip(dst_peer_id, addr.clone()).await;
let ret = self
.do_try_connect_to_ip(dst_peer_id, addr.clone(), priority)
.await;
tracing::debug!(?ret, ?dst_peer_id, ?addr, "try_connect_to_ip return");
if ret.is_ok() {
return Ok(());
}
if self.peer_manager.has_directly_connected_conn(dst_peer_id) {
if has_good_direct_conn() {
return Ok(());
}
@@ -404,17 +497,19 @@ impl DirectConnectorManagerData {
self: &Arc<DirectConnectorManagerData>,
dst_peer_id: PeerId,
ip_list: &GetIpListResponse,
listener: &url::Url,
listener: &AvailableListener,
tasks: &mut JoinSet<Result<(), Error>>,
) {
let Ok(mut addrs) = resolve_mapped_listener_addrs(listener).await else {
let Ok(mut addrs) = resolve_mapped_listener_addrs(&listener.url).await else {
tracing::error!(?listener, "failed to parse socket address from listener");
return;
};
let listener_host = addrs.pop();
tracing::info!(?listener_host, ?listener, "try direct connect to peer");
let is_udp = matches_protocol!(listener, Protocol::UDP);
let is_udp = matches_protocol!(&listener.url, Protocol::UDP);
let listener_url = &listener.url;
let priority = listener.priority;
// Snapshot running listeners once; used for cheap port pre-checks before the
// expensive should_deny_proxy call (which binds a socket per IP) in the
// unspecified-address expansion loops below.
@@ -449,12 +544,13 @@ impl DirectConnectorManagerData {
);
return;
}
let mut addr = (*listener).clone();
let mut addr = listener_url.clone();
if addr.set_host(Some(ip.to_string().as_str())).is_ok() {
tasks.spawn(Self::try_connect_to_ip(
self.clone(),
dst_peer_id,
addr.to_string(),
priority,
));
} else {
tracing::error!(
@@ -475,7 +571,8 @@ impl DirectConnectorManagerData {
tasks.spawn(Self::try_connect_to_ip(
self.clone(),
dst_peer_id,
listener.to_string(),
listener_url.to_string(),
priority,
));
}
}
@@ -504,12 +601,13 @@ impl DirectConnectorManagerData {
);
return;
}
let mut addr = (*listener).clone();
let mut addr = listener_url.clone();
if addr.set_host(Some(format!("[{}]", ip).as_str())).is_ok() {
tasks.spawn(Self::try_connect_to_ip(
self.clone(),
dst_peer_id,
addr.to_string(),
priority,
));
} else {
tracing::error!(
@@ -535,7 +633,8 @@ impl DirectConnectorManagerData {
tasks.spawn(Self::try_connect_to_ip(
self.clone(),
dst_peer_id,
listener.to_string(),
listener_url.to_string(),
priority,
));
}
}
@@ -553,15 +652,7 @@ impl DirectConnectorManagerData {
ip_list: GetIpListResponse,
) -> Result<(), Error> {
let enable_ipv6 = self.global_ctx.get_flags().enable_ipv6;
let available_listeners = ip_list
.listeners
.clone()
.into_iter()
.map(Into::<url::Url>::into)
.filter_map(|l| if l.scheme() != "ring" { Some(l) } else { None })
.filter(|l| mapped_listener_port(l).is_some() && l.host().is_some())
.filter(|l| enable_ipv6 || !matches!(l.host().unwrap().to_owned(), Host::Ipv6(_)))
.collect::<Vec<_>>();
let mut available_listeners = available_listeners_from_ip_list(&ip_list, enable_ipv6);
tracing::debug!(?available_listeners, "got available listeners");
@@ -570,35 +661,30 @@ impl DirectConnectorManagerData {
}
let default_protocol = self.global_ctx.get_flags().default_protocol;
// sort available listeners, default protocol has the highest priority, udp is second, others just random
// highest priority is in the last
let mut available_listeners = available_listeners;
available_listeners.sort_by_key(|l| {
let scheme = l.scheme();
if scheme == default_protocol {
3
} else if scheme == "udp" {
2
} else {
1
}
});
// Sort by configured priority first (lower is better), then prefer the
// default protocol and UDP. The best candidate group is in the last slot.
sort_available_listeners(&mut available_listeners, &default_protocol);
while !available_listeners.is_empty() {
while let Some(cur_listener) = available_listeners.last() {
let mut tasks = JoinSet::new();
let mut listener_list = vec![];
let cur_scheme = available_listeners.last().unwrap().scheme().to_owned();
let cur_priority = cur_listener.priority;
let cur_scheme = cur_listener.url.scheme().to_owned();
while let Some(listener) = available_listeners.last() {
if listener.scheme() != cur_scheme {
if listener.priority != cur_priority || listener.url.scheme() != cur_scheme {
break;
}
tracing::debug!("try direct connect to peer with listener: {}", listener);
tracing::debug!(
%cur_priority,
"try direct connect to peer with listener: {}",
listener.url
);
self.spawn_direct_connect_task(dst_peer_id, &ip_list, listener, &mut tasks)
.await;
listener_list.push(listener.clone().to_string());
listener_list.push(listener.url.to_string());
available_listeners.pop();
}
@@ -606,12 +692,16 @@ impl DirectConnectorManagerData {
tracing::debug!(
?ret,
?dst_peer_id,
?cur_priority,
?cur_scheme,
?listener_list,
"all tasks finished for current scheme"
);
if self.peer_manager.has_directly_connected_conn(dst_peer_id) {
if self
.peer_manager
.has_directly_connected_conn_with_priority_at_most(dst_peer_id, cur_priority)
{
tracing::info!(
"direct connect to peer {} success, has direct conn",
dst_peer_id
@@ -666,13 +756,29 @@ impl DirectConnectorManagerData {
.await;
tracing::info!(?ret, ?dst_peer_id, "do_try_direct_connect return");
if peer_manager.has_directly_connected_conn(dst_peer_id) {
if peer_manager.has_directly_connected_conn_with_priority_at_most(
dst_peer_id,
DEFAULT_CONNECTION_PRIORITY,
) {
tracing::info!(
"direct connect to peer {} success, has direct conn",
dst_peer_id
);
return Ok(());
}
if peer_manager.has_directly_connected_conn(dst_peer_id) {
self.low_priority_direct_retry_backoff.insert(
dst_peer_id,
(),
Duration::from_secs(DIRECT_CONNECTOR_LOW_PRIORITY_RETRY_TIMEOUT_SEC),
);
tracing::info!(
"direct connect to peer {} skipped temporarily, only low-priority direct conn exists",
dst_peer_id
);
return Ok(());
}
}
}
}
@@ -715,6 +821,7 @@ impl PeerTaskLauncher for DirectConnectorLauncher {
async fn collect_peers_need_task(&self, data: &Self::Data) -> Vec<Self::CollectPeerItem> {
data.peer_black_list.cleanup();
data.low_priority_direct_retry_backoff.cleanup();
let my_peer_id = data.peer_manager.my_peer_id();
data.peer_manager
.list_peers()
@@ -722,7 +829,13 @@ impl PeerTaskLauncher for DirectConnectorLauncher {
.into_iter()
.filter(|peer_id| {
*peer_id != my_peer_id
&& !data.peer_manager.has_directly_connected_conn(*peer_id)
&& !data
.peer_manager
.has_directly_connected_conn_with_priority_at_most(
*peer_id,
DEFAULT_CONNECTION_PRIORITY,
)
&& !data.low_priority_direct_retry_backoff.contains(peer_id)
&& !data.peer_black_list.contains(peer_id)
})
.collect()
@@ -813,12 +926,16 @@ mod tests {
wait_route_appear_with_cost,
},
proto::peer_rpc::GetIpListResponse,
proto::peer_rpc::ListenerInfo,
tunnel::{IpScheme, TunnelScheme, matches_scheme},
};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use super::{TESTING, mapped_listener_port, resolve_mapped_listener_addrs};
use super::{
DEFAULT_CONNECTION_PRIORITY, TESTING, available_listeners_from_ip_list,
mapped_listener_port, resolve_mapped_listener_addrs, sort_available_listeners,
};
#[tokio::test]
async fn public_ipv6_candidate_rejects_easytier_managed_addr_even_in_tests() {
@@ -868,6 +985,68 @@ mod tests {
);
}
#[test]
fn available_listener_order_uses_priority_before_protocol() {
let ip_list = GetIpListResponse {
listener_infos: vec![
ListenerInfo {
url: Some("tcp://127.0.0.1:11010".parse().unwrap()),
priority: 100,
},
ListenerInfo {
url: Some("udp://127.0.0.1:11011".parse().unwrap()),
priority: 0,
},
ListenerInfo {
url: Some("tcp://127.0.0.1:11012".parse().unwrap()),
priority: 0,
},
],
..Default::default()
};
let mut listeners = available_listeners_from_ip_list(&ip_list, true);
sort_available_listeners(&mut listeners, "tcp");
let ordered_urls = listeners
.iter()
.rev()
.map(|listener| listener.url.to_string())
.collect::<Vec<_>>();
assert_eq!(
ordered_urls,
vec![
"tcp://127.0.0.1:11012",
"udp://127.0.0.1:11011",
"tcp://127.0.0.1:11010",
]
);
}
#[test]
fn available_listener_order_keeps_legacy_listeners_at_default_priority() {
let mut ip_list = GetIpListResponse::default();
ip_list
.listeners
.push("udp://127.0.0.1:11010".parse().unwrap());
ip_list
.listeners
.push("tcp://127.0.0.1:11011".parse().unwrap());
let mut listeners = available_listeners_from_ip_list(&ip_list, true);
sort_available_listeners(&mut listeners, "tcp");
assert!(
listeners
.iter()
.all(|listener| listener.priority == DEFAULT_CONNECTION_PRIORITY)
);
assert_eq!(
listeners.last().unwrap().url.to_string(),
"tcp://127.0.0.1:11011"
);
}
#[tokio::test]
async fn resolve_mapped_listener_addrs_uses_default_ports() {
let wss_addrs = resolve_mapped_listener_addrs(&"wss://127.0.0.1".parse().unwrap())
+49 -20
View File
@@ -3,11 +3,13 @@ use std::{
sync::{Arc, Weak},
};
use dashmap::DashSet;
use dashmap::{DashMap, DashSet};
use tokio::{sync::mpsc, task::JoinSet, time::timeout};
use crate::{
common::{PeerId, dns::socket_addrs, join_joinset_background},
common::{
PeerId, config::DEFAULT_CONNECTION_PRIORITY, dns::socket_addrs, join_joinset_background,
},
peers::peer_conn::PeerConnId,
proto::{
api::instance::{
@@ -16,7 +18,7 @@ use crate::{
},
rpc_types::{self, controller::BaseController},
},
tunnel::{IpVersion, TunnelConnector},
tunnel::{IpVersion, PrioritizedConnector, TunnelConnector},
utils::weak_upgrade,
};
@@ -32,7 +34,7 @@ use crate::{
use super::create_connector_by_url;
type ConnectorMap = Arc<DashSet<url::Url>>;
type ConnectorMap = Arc<DashMap<url::Url, u32>>;
#[derive(Debug, Clone)]
struct ReconnResult {
@@ -43,7 +45,7 @@ struct ReconnResult {
struct ConnectorManagerData {
connectors: ConnectorMap,
reconnecting: DashSet<url::Url>,
reconnecting: DashMap<url::Url, u32>,
peer_manager: Weak<PeerManager>,
alive_conn_urls: Arc<DashSet<url::Url>>,
// user removed connector urls
@@ -60,14 +62,14 @@ pub struct ManualConnectorManager {
impl ManualConnectorManager {
pub fn new(global_ctx: ArcGlobalCtx, peer_manager: Arc<PeerManager>) -> Self {
let connectors = Arc::new(DashSet::new());
let connectors = Arc::new(DashMap::new());
let tasks = JoinSet::new();
let mut ret = Self {
global_ctx: global_ctx.clone(),
data: Arc::new(ConnectorManagerData {
connectors,
reconnecting: DashSet::new(),
reconnecting: DashMap::new(),
peer_manager: Arc::downgrade(&peer_manager),
alive_conn_urls: Arc::new(DashSet::new()),
removed_conn_urls: Arc::new(DashSet::new()),
@@ -85,14 +87,26 @@ impl ManualConnectorManager {
pub fn add_connector<T>(&self, connector: T)
where
T: TunnelConnector + 'static,
T: TunnelConnector,
{
tracing::info!("add_connector: {}", connector.remote_url());
self.data.connectors.insert(connector.remote_url());
let priority = connector.priority();
self.data
.connectors
.insert(connector.remote_url(), priority);
}
pub async fn add_connector_by_url(&self, url: url::Url) -> Result<(), Error> {
self.data.connectors.insert(url);
self.add_connector_by_url_with_priority(url, DEFAULT_CONNECTION_PRIORITY)
.await
}
pub async fn add_connector_by_url_with_priority(
&self,
url: url::Url,
priority: u32,
) -> Result<(), Error> {
self.data.connectors.insert(url, priority);
Ok(())
}
@@ -138,19 +152,25 @@ impl ManualConnectorManager {
Connector {
url: Some(conn_url.into()),
status: status.into(),
priority: *item.value(),
},
);
}
let reconnecting_urls: BTreeSet<url::Url> =
self.data.reconnecting.iter().map(|x| x.clone()).collect();
let reconnecting_urls: BTreeSet<_> = self
.data
.reconnecting
.iter()
.map(|item| (item.key().clone(), *item.value()))
.collect();
for conn_url in reconnecting_urls {
for (conn_url, priority) in reconnecting_urls {
ret.insert(
0,
Connector {
url: Some(conn_url.into()),
status: ConnectorStatus::Connecting.into(),
priority,
},
);
}
@@ -177,16 +197,20 @@ impl ManualConnectorManager {
for dead_url in dead_urls {
let data_clone = data.clone();
let sender = reconn_result_send.clone();
data.connectors.remove(&dead_url).unwrap();
let insert_succ = data.reconnecting.insert(dead_url.clone());
assert!(insert_succ);
let priority = data
.connectors
.remove(&dead_url)
.map(|(_, priority)| priority)
.unwrap_or(DEFAULT_CONNECTION_PRIORITY);
let previous = data.reconnecting.insert(dead_url.clone(), priority);
assert!(previous.is_none());
tasks.lock().unwrap().spawn(async move {
let reconn_ret = Self::conn_reconnect(data_clone.clone(), dead_url.clone() ).await;
let reconn_ret = Self::conn_reconnect(data_clone.clone(), dead_url.clone(), priority).await;
let _ = sender.send(reconn_ret).await;
data_clone.reconnecting.remove(&dead_url).unwrap();
data_clone.connectors.insert(dead_url.clone());
data_clone.connectors.insert(dead_url.clone(), priority);
});
}
tracing::info!("reconn_interval tick, done");
@@ -206,7 +230,7 @@ impl ManualConnectorManager {
if data.connectors.remove(url).is_some() {
tracing::warn!("connector: {}, removed", url);
continue;
} else if data.reconnecting.contains(url) {
} else if data.reconnecting.contains_key(url) {
tracing::warn!("connector: {}, reconnecting, remove later.", url);
remove_later.insert(url.clone());
continue;
@@ -244,6 +268,7 @@ impl ManualConnectorManager {
data: Arc<ConnectorManagerData>,
dead_url: String,
ip_version: IpVersion,
priority: u32,
) -> Result<ReconnResult, Error> {
let connector =
create_connector_by_url(&dead_url, &data.global_ctx.clone(), ip_version).await?;
@@ -257,7 +282,9 @@ impl ManualConnectorManager {
)));
};
let (peer_id, conn_id) = pm.try_direct_connect(connector).await?;
let (peer_id, conn_id) = pm
.try_direct_connect(PrioritizedConnector::new(connector, priority))
.await?;
tracing::info!("reconnect succ: {} {} {}", peer_id, conn_id, dead_url);
Ok(ReconnResult {
dead_url,
@@ -269,6 +296,7 @@ impl ManualConnectorManager {
async fn conn_reconnect(
data: Arc<ConnectorManagerData>,
dead_url: url::Url,
priority: u32,
) -> Result<ReconnResult, Error> {
tracing::info!("reconnect: {}", dead_url);
@@ -326,6 +354,7 @@ impl ManualConnectorManager {
data.clone(),
dead_url.to_string(),
ip_version,
priority,
),
)
.await;
+4 -1
View File
@@ -472,7 +472,10 @@ impl PeerTaskLauncher for TcpHolePunchPeerTaskLauncher {
continue;
}
if data.peer_mgr.get_peer_map().has_peer(peer_id) {
if data.peer_mgr.has_conn_with_priority_at_most(
peer_id,
crate::common::config::DEFAULT_CONNECTION_PRIORITY,
) {
tracing::trace!(peer_id, "tcp hole punch task collect skip already has peer");
continue;
}
+4 -1
View File
@@ -474,7 +474,10 @@ impl PeerTaskLauncher for UdpHolePunchPeerTaskLauncher {
continue;
}
if data.peer_mgr.get_peer_map().has_peer(peer_id) {
if data.peer_mgr.has_conn_with_priority_at_most(
peer_id,
crate::common::config::DEFAULT_CONNECTION_PRIORITY,
) {
continue;
}
+2
View File
@@ -923,6 +923,7 @@ impl NetworkOptions {
.parse()
.with_context(|| format!("failed to parse peer uri: {}", p))?,
peer_public_key: None,
priority: crate::common::config::DEFAULT_CONNECTION_PRIORITY,
});
}
cfg.set_peers(peers);
@@ -960,6 +961,7 @@ impl NetworkOptions {
format!("failed to parse external node uri: {}", external_nodes)
})?,
peer_public_key: None,
priority: crate::common::config::DEFAULT_CONNECTION_PRIORITY,
});
cfg.set_peers(old_peers);
}
+110 -17
View File
@@ -43,7 +43,7 @@ use easytier::{
},
instance::{
AclManageRpc, AclManageRpcClientFactory, Connector, ConnectorManageRpc,
ConnectorManageRpcClientFactory, CredentialManageRpc,
ConnectorManageRpcClientFactory, ConnectorStatus, CredentialManageRpc,
CredentialManageRpcClientFactory, DumpRouteRequest, ForeignNetworkEntryPb,
GenerateCredentialRequest, GetAclStatsRequest, GetPrometheusStatsRequest,
GetStatsRequest, GetVpnPortalInfoRequest, GetWhitelistRequest,
@@ -74,7 +74,7 @@ use easytier::{
common::{NatType, PortForwardConfigPb, SocketType},
peer_rpc::{GetGlobalPeerMapRequest, PeerCenterRpc, PeerCenterRpcClientFactory},
rpc_impl::standalone::StandAloneClient,
rpc_types::controller::BaseController,
rpc_types::{controller::BaseController, error::Error as RpcError},
},
tunnel::{TunnelScheme, tcp::TcpTunnelConnector},
utils::{PeerRoutePair, string::cost_to_str},
@@ -236,6 +236,8 @@ enum ConnectorSubCommand {
Add {
#[arg(help = "connector url, e.g., tcp://1.2.3.4:11010")]
url: String,
#[arg(short = 'p', long = "priority", default_value_t = easytier::common::config::DEFAULT_CONNECTION_PRIORITY, help = "connection priority; lower values are preferred")]
priority: u32,
},
/// Remove a connector
Remove {
@@ -254,7 +256,11 @@ struct MappedListenerArgs {
#[derive(Subcommand, Debug)]
enum MappedListenerSubCommand {
/// Add Mapped Listerner
Add { url: String },
Add {
url: String,
#[arg(short = 'p', long = "priority", default_value_t = easytier::common::config::DEFAULT_CONNECTION_PRIORITY, help = "listener priority; lower values are preferred")]
priority: u32,
},
/// Remove Mapped Listener
Remove { url: String },
/// List Existing Mapped Listener
@@ -526,6 +532,40 @@ 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,
@@ -599,9 +639,15 @@ impl<'a> CommandHandler<'a> {
}
let client = self.get_manage_client().await?;
let inst_ids = client
let list_response = match client
.list_network_instance(BaseController::default(), ListNetworkInstanceRequest {})
.await?
.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
.inst_ids
.into_iter()
.map(uuid::Uuid::from)
@@ -1207,6 +1253,7 @@ impl<'a> CommandHandler<'a> {
&self,
url: &str,
action: ConfigPatchAction,
priority: Option<u32>,
) -> Result<(), Error> {
let url = match action {
ConfigPatchAction::Add => Self::connector_validate_url(url)?,
@@ -1227,6 +1274,7 @@ impl<'a> CommandHandler<'a> {
connectors: vec![UrlPatch {
action: action.into(),
url: Some(url.into()),
priority,
}],
..Default::default()
}),
@@ -1241,11 +1289,12 @@ impl<'a> CommandHandler<'a> {
&self,
url: &str,
action: ConfigPatchAction,
priority: Option<u32>,
) -> Result<(), Error> {
let url = url.to_string();
self.apply_to_instances(|handler| {
let url = url.clone();
Box::pin(async move { handler.apply_connector_modify(&url, action).await })
Box::pin(async move { handler.apply_connector_modify(&url, action, priority).await })
})
.await
}
@@ -1269,6 +1318,8 @@ impl<'a> CommandHandler<'a> {
tx_bytes: String,
#[tabled(rename = "tunnel")]
tunnel_proto: String,
#[tabled(rename = "prio")]
priority: String,
#[tabled(rename = "NAT")]
nat_type: String,
#[tabled(skip)]
@@ -1298,6 +1349,10 @@ impl<'a> CommandHandler<'a> {
rx_bytes: format_size(p.get_rx_bytes().unwrap_or(0), humansize::DECIMAL),
tx_bytes: format_size(p.get_tx_bytes().unwrap_or(0), humansize::DECIMAL),
tunnel_proto: p.get_conn_protos().unwrap_or_default().join(","),
priority: p
.get_conn_priority()
.map(|priority| priority.to_string())
.unwrap_or_else(|| "-".to_string()),
nat_type: p.get_udp_nat_type(),
id: route.peer_id.to_string(),
version: if route.version.is_empty() {
@@ -1323,6 +1378,7 @@ impl<'a> CommandHandler<'a> {
rx_bytes: "-".to_string(),
tx_bytes: "-".to_string(),
tunnel_proto: "-".to_string(),
priority: "-".to_string(),
nat_type: if let Some(info) = p.stun_info {
info.udp_nat_type().as_str_name().to_string()
} else {
@@ -1786,6 +1842,29 @@ impl<'a> CommandHandler<'a> {
}
async fn handle_connector_list(&self) -> Result<(), Error> {
#[derive(tabled::Tabled, serde::Serialize)]
struct ConnectorTableItem {
url: String,
status: String,
priority: String,
}
impl From<Connector> for ConnectorTableItem {
fn from(connector: Connector) -> Self {
Self {
url: connector
.url
.map(Into::<url::Url>::into)
.map(|url| url.to_string())
.unwrap_or_default(),
status: ConnectorStatus::try_from(connector.status)
.map(|status| format!("{:?}", status))
.unwrap_or_else(|_| connector.status.to_string()),
priority: connector.priority.to_string(),
}
}
}
let results = self
.collect_instance_results(|handler| Box::pin(handler.fetch_connector_list()))
.await?;
@@ -1793,8 +1872,13 @@ impl<'a> CommandHandler<'a> {
return self.print_json_results(results);
}
self.print_results(&results, |connectors| {
println!("response: {:#?}", connectors);
Ok(())
let mut items = connectors
.iter()
.cloned()
.map(ConnectorTableItem::from)
.collect::<Vec<_>>();
items.sort_by(|a, b| a.url.cmp(&b.url));
print_output(&items, self.output_format, &[], &[], self.no_trunc)
})
}
@@ -1833,6 +1917,7 @@ impl<'a> CommandHandler<'a> {
&self,
url: &str,
action: ConfigPatchAction,
priority: Option<u32>,
) -> Result<(), Error> {
let url = Self::mapped_listener_validate_url(url)?;
let client = self.get_config_client().await?;
@@ -1842,6 +1927,7 @@ impl<'a> CommandHandler<'a> {
mapped_listeners: vec![UrlPatch {
action: action.into(),
url: Some(url.into()),
priority,
}],
..Default::default()
}),
@@ -1856,11 +1942,16 @@ impl<'a> CommandHandler<'a> {
&self,
url: &str,
action: ConfigPatchAction,
priority: Option<u32>,
) -> Result<(), Error> {
let url = url.to_string();
self.apply_to_instances(|handler| {
let url = url.clone();
Box::pin(async move { handler.apply_mapped_listener_modify(&url, action).await })
Box::pin(async move {
handler
.apply_mapped_listener_modify(&url, action, priority)
.await
})
})
.await
}
@@ -2843,15 +2934,17 @@ async fn main() -> Result<(), Error> {
}
},
SubCommand::Connector(conn_args) => match conn_args.sub_command {
Some(ConnectorSubCommand::Add { url }) => {
Some(ConnectorSubCommand::Add { url, priority }) => {
handler
.handle_connector_modify(&url, ConfigPatchAction::Add)
.handle_connector_modify(&url, ConfigPatchAction::Add, Some(priority))
.await?;
println!("connector add applied to selected instance(s): {url}");
println!(
"connector add applied to selected instance(s): {url}, priority: {priority}"
);
}
Some(ConnectorSubCommand::Remove { url }) => {
handler
.handle_connector_modify(&url, ConfigPatchAction::Remove)
.handle_connector_modify(&url, ConfigPatchAction::Remove, None)
.await?;
println!("connector remove applied to selected instance(s): {url}");
}
@@ -2864,15 +2957,15 @@ async fn main() -> Result<(), Error> {
},
SubCommand::MappedListener(mapped_listener_args) => {
match mapped_listener_args.sub_command {
Some(MappedListenerSubCommand::Add { url }) => {
Some(MappedListenerSubCommand::Add { url, priority }) => {
handler
.handle_mapped_listener_modify(&url, ConfigPatchAction::Add)
.handle_mapped_listener_modify(&url, ConfigPatchAction::Add, Some(priority))
.await?;
println!("add mapped listener: {url}");
println!("add mapped listener: {url}, priority: {priority}");
}
Some(MappedListenerSubCommand::Remove { url }) => {
handler
.handle_mapped_listener_modify(&url, ConfigPatchAction::Remove)
.handle_mapped_listener_modify(&url, ConfigPatchAction::Remove, None)
.await?;
println!("remove mapped listener: {url}");
}
+45 -9
View File
@@ -340,6 +340,11 @@ 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;
@@ -555,16 +560,39 @@ impl InstanceConfigPatcher {
return Ok(());
}
let global_ctx = weak_upgrade(&self.global_ctx)?;
let mut current_mapped_listeners = global_ctx.config.get_mapped_listeners();
let current_mapped_listener_configs = global_ctx.config.get_mapped_listener_configs();
let mut priority_by_url = current_mapped_listener_configs
.iter()
.map(|listener| (listener.url.clone(), listener.priority))
.collect::<std::collections::HashMap<_, _>>();
let mut current_mapped_listeners = current_mapped_listener_configs
.into_iter()
.map(|listener| listener.url)
.collect();
for patch in &mapped_listeners {
if let (Some(url), Some(priority)) = (&patch.url, patch.priority) {
priority_by_url.insert(url.clone().into(), priority);
}
}
let patches = mapped_listeners.into_iter().map(Into::into).collect();
InstanceConfigPatcher::trace_patchables(&patches);
crate::proto::api::config::patch_vec(&mut current_mapped_listeners, patches);
if current_mapped_listeners.is_empty() {
global_ctx.config.set_mapped_listeners(None);
global_ctx.config.set_mapped_listener_configs(None);
} else {
let mapped_listener_configs = current_mapped_listeners
.into_iter()
.map(|url| {
let priority = priority_by_url
.get(&url)
.copied()
.unwrap_or(crate::common::config::DEFAULT_CONNECTION_PRIORITY);
crate::common::config::ListenerConfig::new(url, priority)
})
.collect();
global_ctx
.config
.set_mapped_listeners(Some(current_mapped_listeners));
.set_mapped_listener_configs(Some(mapped_listener_configs));
}
Ok(())
}
@@ -585,7 +613,14 @@ impl InstanceConfigPatcher {
match ConfigPatchAction::try_from(connector.action) {
Ok(ConfigPatchAction::Add) => {
tracing::info!("Connector added: {}", url);
conn_manager.add_connector_by_url(url).await?;
conn_manager
.add_connector_by_url_with_priority(
url,
connector
.priority
.unwrap_or(crate::common::config::DEFAULT_CONNECTION_PRIORITY),
)
.await?;
}
Ok(ConfigPatchAction::Remove) => {
tracing::info!("Connector removed: {}", url);
@@ -737,7 +772,7 @@ impl Instance {
async fn add_initial_peers(&self) -> Result<(), Error> {
for peer in self.global_ctx.config.get_peers().iter() {
self.get_conn_manager()
.add_connector_by_url(peer.uri.clone())
.add_connector_by_url_with_priority(peer.uri.clone(), peer.priority)
.await?;
}
Ok(())
@@ -1223,11 +1258,12 @@ impl Instance {
_request: ListMappedListenerRequest,
) -> Result<ListMappedListenerResponse, rpc_types::error::Error> {
let mut ret = ListMappedListenerResponse::default();
let urls = weak_upgrade(&self.0)?.config.get_mapped_listeners();
let mapped_listeners: Vec<MappedListener> = urls
let listener_configs = weak_upgrade(&self.0)?.config.get_mapped_listener_configs();
let mapped_listeners: Vec<MappedListener> = listener_configs
.into_iter()
.map(|u| MappedListener {
url: Some(u.into()),
.map(|listener| MappedListener {
url: Some(listener.url.into()),
priority: listener.priority,
})
.collect();
ret.mappedlisteners = mapped_listeners;
+26 -5
View File
@@ -91,6 +91,7 @@ pub type ListenerCreator = Box<dyn ListenerCreatorTrait>;
struct ListenerFactory {
creator_fn: Arc<ListenerCreator>,
must_succ: bool,
priority: u32,
}
pub struct ListenerManager<H> {
@@ -125,8 +126,9 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
)
.await?;
for l in self.global_ctx.config.get_listener_uris().iter() {
let l = l.clone();
for listener_cfg in self.global_ctx.config.get_listener_configs().iter() {
let l = listener_cfg.url.clone();
let priority = listener_cfg.priority;
let Ok(_) = create_listener_by_url(&l, self.global_ctx.clone()) else {
let msg = format!("failed to get listener by url: {}, maybe not supported", l);
self.global_ctx
@@ -136,9 +138,10 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
let ctx = self.global_ctx.clone();
let listener = l.clone();
self.add_listener(
self.add_listener_with_priority(
move || create_listener_by_url(&listener, ctx.clone()).unwrap(),
true,
priority,
)
.await?;
@@ -153,9 +156,10 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
.set_host(Some("[::]".to_string().as_str()))
.with_context(|| format!("failed to set ipv6 host for listener: {}", l))?;
let ctx = self.global_ctx.clone();
self.add_listener(
self.add_listener_with_priority(
move || create_listener_by_url(&ipv6_listener, ctx.clone()).unwrap(),
false,
priority,
)
.await?;
}
@@ -168,10 +172,25 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
&mut self,
creator: C,
must_succ: bool,
) -> Result<(), Error> {
self.add_listener_with_priority(
creator,
must_succ,
crate::common::config::DEFAULT_CONNECTION_PRIORITY,
)
.await
}
pub async fn add_listener_with_priority<C: ListenerCreatorTrait + 'static>(
&mut self,
creator: C,
must_succ: bool,
priority: u32,
) -> Result<(), Error> {
self.listeners.push(ListenerFactory {
creator_fn: Arc::new(Box::new(creator)),
must_succ,
priority,
});
Ok(())
}
@@ -181,6 +200,7 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
creator: Arc<ListenerCreator>,
peer_manager: Weak<H>,
global_ctx: ArcGlobalCtx,
priority: u32,
) {
let mut err_count = 0;
loop {
@@ -189,7 +209,7 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
match l.listen().await {
Ok(_) => {
err_count = 0;
global_ctx.add_running_listener(l.local_url());
global_ctx.add_running_listener_with_priority(l.local_url(), priority);
global_ctx.issue_event(GlobalCtxEvent::ListenerAdded(l.local_url()));
}
Err(e) => {
@@ -270,6 +290,7 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
listener.creator_fn.clone(),
self.peer_manager.clone(),
self.global_ctx.clone(),
listener.priority,
));
}
+2 -10
View File
@@ -361,16 +361,8 @@ 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 = {
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
}
};
let feature_changed =
global_ctx.set_ipv6_public_addr_provider_feature_flag(next_provider_enabled);
prefix_changed || feature_changed
}
+8
View File
@@ -551,6 +551,7 @@ impl NetworkConfig {
format!("failed to parse public server uri: {}", public_server_url)
})?,
peer_public_key: None,
priority: crate::common::config::DEFAULT_CONNECTION_PRIORITY,
}]);
}
NetworkingMethod::Manual => {
@@ -564,6 +565,7 @@ impl NetworkConfig {
.parse()
.with_context(|| format!("failed to parse peer uri: {}", peer_url))?,
peer_public_key: None,
priority: crate::common::config::DEFAULT_CONNECTION_PRIORITY,
});
}
if !peers.is_empty() {
@@ -816,6 +818,10 @@ 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;
}
@@ -990,6 +996,7 @@ 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);
@@ -1104,6 +1111,7 @@ mod tests {
peers.push(crate::common::config::PeerConfig {
uri,
peer_public_key: None,
priority: crate::common::config::DEFAULT_CONNECTION_PRIORITY,
});
}
config.set_peers(peers);
+1 -24
View File
@@ -65,7 +65,7 @@ impl PeerCenterBase {
return Err(Error::Shutdown);
};
rpc_mgr.rpc_server().registry().register(
PeerCenterRpcServer::new(PeerCenterServer::new(self.peer_mgr.my_peer_id())),
PeerCenterRpcServer::new(PeerCenterServer::new()),
&self.peer_mgr.get_global_ctx().get_network_name(),
);
Ok(())
@@ -486,7 +486,6 @@ 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,
};
@@ -515,25 +514,6 @@ 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();
@@ -578,8 +558,5 @@ 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);
}
}
+96 -30
View File
@@ -6,7 +6,6 @@ use std::{
use crossbeam::atomic::AtomicCell;
use dashmap::DashMap;
use once_cell::sync::Lazy;
use tokio::task::JoinSet;
use crate::{
@@ -35,50 +34,41 @@ pub(crate) struct PeerCenterInfoEntry {
update_time: std::time::Instant,
}
#[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(Debug, Default)]
struct PeerCenterServerData {
global_peer_map: DashMap<SrcDstPeerPair, PeerCenterInfoEntry>,
peer_report_time: DashMap<PeerId, std::time::Instant>,
digest: AtomicCell<Digest>,
}
#[derive(Clone, Debug)]
pub struct PeerCenterServer {
// every peer has its own server, so use per-struct dash map is ok.
my_node_id: PeerId,
data: Arc<PeerCenterServerData>,
tasks: Arc<JoinSet<()>>,
}
impl PeerCenterServer {
pub fn new(my_node_id: PeerId) -> Self {
pub fn new() -> Self {
let data = Arc::new(PeerCenterServerData::default());
let weak_data = Arc::downgrade(&data);
let mut tasks = JoinSet::new();
tasks.spawn(async move {
loop {
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
PeerCenterServer::clean_outdated_peer(my_node_id).await;
let Some(data) = weak_data.upgrade() else {
break;
};
PeerCenterServer::clean_outdated_peer_data(&data).await;
}
});
PeerCenterServer {
my_node_id,
data,
tasks: Arc::new(tasks),
}
}
async fn clean_outdated_peer(my_node_id: PeerId) {
let data = get_global_data(my_node_id);
async fn clean_outdated_peer_data(data: &PeerCenterServerData) {
data.peer_report_time.retain(|_, v| {
std::time::Instant::now().duration_since(*v) < std::time::Duration::from_secs(180)
});
@@ -88,8 +78,7 @@ impl PeerCenterServer {
});
}
fn calc_global_digest(my_node_id: PeerId) -> Digest {
let data = get_global_data(my_node_id);
fn calc_global_digest_data(data: &PeerCenterServerData) -> Digest {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
data.global_peer_map
.iter()
@@ -117,7 +106,7 @@ impl PeerCenterRpc for PeerCenterServer {
tracing::debug!("receive report_peers");
let data = get_global_data(self.my_node_id);
let data = &self.data;
data.peer_report_time
.insert(my_peer_id, std::time::Instant::now());
@@ -134,7 +123,7 @@ impl PeerCenterRpc for PeerCenterServer {
}
data.digest
.store(PeerCenterServer::calc_global_digest(self.my_node_id));
.store(PeerCenterServer::calc_global_digest_data(data));
Ok(ReportPeersResponse::default())
}
@@ -147,7 +136,7 @@ impl PeerCenterRpc for PeerCenterServer {
) -> Result<GetGlobalPeerMapResponse, rpc_types::error::Error> {
let digest = req.digest;
let data = get_global_data(self.my_node_id);
let data = &self.data;
if digest == data.digest.load() && digest != 0 {
return Ok(GetGlobalPeerMapResponse::default());
}
@@ -171,3 +160,80 @@ 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());
}
}
+304 -23
View File
@@ -56,7 +56,7 @@ use super::{
route_trait::NextHopPolicy,
traffic_metrics::{
InstanceLabelKind, LogicalTrafficMetrics, TrafficKind, TrafficMetricRecorder,
route_peer_info_instance_id, traffic_kind,
is_relay_data_packet_type, route_peer_info_instance_id, traffic_kind,
},
};
@@ -69,11 +69,16 @@ 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>>,
@@ -82,7 +87,7 @@ struct ForeignNetworkEntry {
packet_recv: Mutex<Option<PacketRecvChanReceiver>>,
bps_limiter: Arc<TokenBucket>,
bps_limiter: Option<Arc<TokenBucket>>,
peer_center: Arc<PeerCenterInstance>,
@@ -186,14 +191,16 @@ impl ForeignNetworkEntry {
);
let relay_bps_limit = global_ctx.config.get_flags().foreign_relay_bps_limit;
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 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 peer_center = Arc::new(PeerCenterInstance::new(Arc::new(
PeerMapWithPeerRpcManager {
@@ -205,6 +212,7 @@ impl ForeignNetworkEntry {
Self {
my_peer_id,
parent_global_ctx: global_ctx.clone(),
global_ctx: foreign_global_ctx,
network,
peer_map,
@@ -231,6 +239,27 @@ 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,
@@ -250,7 +279,7 @@ impl ForeignNetworkEntry {
flags.disable_relay_quic = !global_ctx.get_flags().enable_relay_foreign_network_quic;
config.set_flags(flags);
config.set_mapped_listeners(Some(global_ctx.config.get_mapped_listeners()));
config.set_mapped_listener_configs(Some(global_ctx.config.get_mapped_listener_configs()));
let foreign_global_ctx = Arc::new(GlobalCtx::new(config));
foreign_global_ctx
@@ -258,13 +287,12 @@ impl ForeignNetworkEntry {
let mut feature_flag = global_ctx.get_feature_flags();
feature_flag.is_public_server = true;
if !relay_data {
feature_flag.avoid_relay_data = true;
}
foreign_global_ctx.set_feature_flags(feature_flag);
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);
for u in global_ctx.get_running_listeners().into_iter() {
foreign_global_ctx.add_running_listener(u);
for listener in global_ctx.get_running_listener_configs().into_iter() {
foreign_global_ctx.add_running_listener_with_priority(listener.url, listener.priority);
}
foreign_global_ctx
@@ -412,6 +440,7 @@ 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();
@@ -497,14 +526,21 @@ impl ForeignNetworkEntry {
"ignore packet in foreign network"
);
} else {
if packet_type == PacketType::Data as u8
|| packet_type == PacketType::KcpSrc as u8
|| packet_type == PacketType::KcpDst as u8
{
if !relay_data {
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"
);
continue;
}
if !bps_limiter.try_consume(len.into()) {
if let Some(bps_limiter) = bps_limiter.as_ref()
&& !bps_limiter.try_consume(len.into())
{
continue;
}
}
@@ -589,10 +625,31 @@ 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;
}
@@ -660,6 +717,7 @@ 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| {
@@ -669,6 +727,9 @@ 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)]
@@ -941,12 +1002,14 @@ 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());
@@ -965,6 +1028,7 @@ 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);
});
}
@@ -1397,6 +1461,92 @@ 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;
@@ -1409,6 +1559,10 @@ 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")
@@ -1461,6 +1615,58 @@ 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);
@@ -1657,6 +1863,81 @@ 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(
+168 -15
View File
@@ -188,23 +188,45 @@ impl Peer {
async fn select_conn(&self) -> Option<ArcPeerConn> {
let default_conn_id = self.default_conn_id.load();
if let Some(conn) = self.conns.get(&default_conn_id) {
return Some(conn.clone());
}
// find a conn with the smallest latency
let mut min_latency = u64::MAX;
for conn in self.conns.iter() {
let latency = conn.value().get_stats().latency_us;
if latency < min_latency {
min_latency = latency;
self.default_conn_id.store(conn.get_conn_id());
let latency_score = |latency_us| {
if latency_us == 0 {
u64::MAX
} else {
latency_us
}
};
let selected_conn = self
.conns
.iter()
.filter(|conn| !conn.value().is_closed())
.min_by_key(|conn| {
(
conn.value().priority(),
latency_score(conn.value().get_stats().latency_us),
)
})
.map(|conn| {
(
conn.get_conn_id(),
conn.value().priority(),
latency_score(conn.value().get_stats().latency_us),
)
});
let (selected_conn_id, selected_priority, selected_latency) = selected_conn?;
if let Some(default_conn) = self.conns.get(&default_conn_id)
&& !default_conn.is_closed()
&& (
default_conn.priority(),
latency_score(default_conn.get_stats().latency_us),
) == (selected_priority, selected_latency)
{
return Some(default_conn.clone());
}
self.conns
.get(&self.default_conn_id.load())
.map(|conn| conn.clone())
self.default_conn_id.store(selected_conn_id);
self.conns.get(&selected_conn_id).map(|conn| conn.clone())
}
pub async fn send_msg(&self, msg: ZCPacket) -> Result<(), Error> {
@@ -249,12 +271,26 @@ impl Peer {
self.conns.iter().any(|entry| !entry.value().is_closed())
}
pub fn has_conn_with_priority_at_most(&self, priority: u32) -> bool {
self.conns
.iter()
.any(|entry| !entry.value().is_closed() && entry.value().priority() <= priority)
}
pub fn has_directly_connected_conn(&self) -> bool {
self.conns
.iter()
.any(|entry| !entry.value().is_closed() && !entry.value().is_hole_punched())
}
pub fn has_directly_connected_conn_with_priority_at_most(&self, priority: u32) -> bool {
self.conns.iter().any(|entry| {
!entry.value().is_closed()
&& !entry.value().is_hole_punched()
&& entry.value().priority() <= priority
})
}
pub fn get_directly_connections(&self) -> DashSet<uuid::Uuid> {
self.conns
.iter()
@@ -298,7 +334,7 @@ mod tests {
use crate::{
common::{
config::{NetworkIdentity, PeerConfig},
config::{DEFAULT_CONNECTION_PRIORITY, NetworkIdentity, PeerConfig},
global_ctx::{GlobalCtx, tests::get_mock_global_ctx},
new_peer_id,
},
@@ -380,6 +416,122 @@ mod tests {
close_handler.await.unwrap().unwrap();
}
#[tokio::test]
async fn select_conn_prefers_priority_then_latency() {
let (packet_send, _packet_recv) = create_packet_recv_chan();
let global_ctx = get_mock_global_ctx();
let local_peer_id = new_peer_id();
let remote_peer_id = new_peer_id();
let peer = Peer::new(remote_peer_id, packet_send, global_ctx.clone());
let ps = Arc::new(PeerSessionStore::new());
let (low_client_tunnel, low_server_tunnel) = create_ring_tunnel_pair();
let mut low_client_conn = PeerConn::new(
local_peer_id,
global_ctx.clone(),
low_client_tunnel,
ps.clone(),
);
low_client_conn.set_priority(100);
low_client_conn.record_latency_for_test(1000);
let low_conn_id = low_client_conn.get_conn_id();
let mut low_server_conn = PeerConn::new(
remote_peer_id,
global_ctx.clone(),
low_server_tunnel,
ps.clone(),
);
let (client_ret, server_ret) = tokio::join!(
low_client_conn.do_handshake_as_client(),
low_server_conn.do_handshake_as_server()
);
client_ret.unwrap();
server_ret.unwrap();
peer.add_peer_conn(low_client_conn).await.unwrap();
assert_eq!(peer.select_conn().await.unwrap().get_conn_id(), low_conn_id);
let (same_priority_client_tunnel, same_priority_server_tunnel) = create_ring_tunnel_pair();
let mut same_priority_client_conn = PeerConn::new(
local_peer_id,
global_ctx.clone(),
same_priority_client_tunnel,
ps.clone(),
);
same_priority_client_conn.set_priority(100);
same_priority_client_conn.record_latency_for_test(10);
let same_priority_conn_id = same_priority_client_conn.get_conn_id();
let mut same_priority_server_conn = PeerConn::new(
remote_peer_id,
global_ctx.clone(),
same_priority_server_tunnel,
ps.clone(),
);
let (client_ret, server_ret) = tokio::join!(
same_priority_client_conn.do_handshake_as_client(),
same_priority_server_conn.do_handshake_as_server()
);
client_ret.unwrap();
server_ret.unwrap();
peer.add_peer_conn(same_priority_client_conn).await.unwrap();
assert_eq!(
peer.select_conn().await.unwrap().get_conn_id(),
same_priority_conn_id
);
let (unknown_latency_client_tunnel, unknown_latency_server_tunnel) =
create_ring_tunnel_pair();
let mut unknown_latency_client_conn = PeerConn::new(
local_peer_id,
global_ctx.clone(),
unknown_latency_client_tunnel,
ps.clone(),
);
unknown_latency_client_conn.set_priority(100);
let mut unknown_latency_server_conn = PeerConn::new(
remote_peer_id,
global_ctx.clone(),
unknown_latency_server_tunnel,
ps.clone(),
);
let (client_ret, server_ret) = tokio::join!(
unknown_latency_client_conn.do_handshake_as_client(),
unknown_latency_server_conn.do_handshake_as_server()
);
client_ret.unwrap();
server_ret.unwrap();
peer.add_peer_conn(unknown_latency_client_conn)
.await
.unwrap();
assert_eq!(
peer.select_conn().await.unwrap().get_conn_id(),
same_priority_conn_id
);
let (high_client_tunnel, high_server_tunnel) = create_ring_tunnel_pair();
let mut high_client_conn = PeerConn::new(
local_peer_id,
global_ctx.clone(),
high_client_tunnel,
ps.clone(),
);
high_client_conn.set_priority(DEFAULT_CONNECTION_PRIORITY);
let high_conn_id = high_client_conn.get_conn_id();
let mut high_server_conn =
PeerConn::new(remote_peer_id, global_ctx, high_server_tunnel, ps);
let (client_ret, server_ret) = tokio::join!(
high_client_conn.do_handshake_as_client(),
high_server_conn.do_handshake_as_server()
);
client_ret.unwrap();
server_ret.unwrap();
peer.add_peer_conn(high_client_conn).await.unwrap();
assert_eq!(
peer.select_conn().await.unwrap().get_conn_id(),
high_conn_id
);
}
#[tokio::test]
async fn reject_peer_conn_with_mismatched_identity_type() {
let (packet_send, _packet_recv) = create_packet_recv_chan();
@@ -423,6 +575,7 @@ mod tests {
.local_public_key
.unwrap(),
),
priority: crate::common::config::DEFAULT_CONNECTION_PRIORITY,
}]);
let mut shared_client_conn = PeerConn::new(
local_peer_id,
+26 -2
View File
@@ -37,7 +37,7 @@ use crate::utils::BoxExt;
use crate::{
common::{
PeerId,
config::{NetworkIdentity, NetworkSecretDigest},
config::{DEFAULT_CONNECTION_PRIORITY, NetworkIdentity, NetworkSecretDigest},
error::Error,
global_ctx::ArcGlobalCtx,
},
@@ -305,6 +305,7 @@ pub struct PeerConn {
// remote or local
is_hole_punched: bool,
priority: u32,
close_event_notifier: Arc<PeerConnCloseNotify>,
@@ -393,6 +394,7 @@ impl PeerConn {
is_client: None,
is_hole_punched: true,
priority: DEFAULT_CONNECTION_PRIORITY,
close_event_notifier: Arc::new(PeerConnCloseNotify::new(conn_id)),
@@ -442,6 +444,19 @@ impl PeerConn {
self.is_hole_punched
}
pub fn set_priority(&mut self, priority: u32) {
self.priority = priority;
}
pub fn priority(&self) -> u32 {
self.priority
}
#[cfg(test)]
pub(crate) fn record_latency_for_test(&self, latency_us: u32) {
self.latency_stats.record_latency(latency_us);
}
pub fn is_closed(&self) -> bool {
self.close_event_notifier.is_closed()
}
@@ -529,6 +544,7 @@ impl PeerConn {
version: VERSION,
features: Vec::new(),
network_name: network.network_name.clone(),
connection_priority: self.priority,
..Default::default()
};
@@ -821,6 +837,7 @@ impl PeerConn {
a_session_generation,
a_conn_id: Some(a_conn_id.into()),
client_encryption_algorithm: self.my_encrypt_algo.clone(),
connection_priority: self.priority,
};
let mut hs = builder
@@ -1072,6 +1089,7 @@ impl PeerConn {
Some(&mut hs),
first_msg1,
)?;
self.priority = msg1_pb.connection_priority;
let remote_network_name = msg1_pb.a_network_name.clone();
self.record_control_rx(&remote_network_name, first_msg1_len);
@@ -1227,6 +1245,7 @@ impl PeerConn {
features: Vec::new(),
network_secret_digest: noise.secret_digest.clone(),
connection_priority: self.priority,
}
}
@@ -1264,6 +1283,7 @@ impl PeerConn {
self.is_client = Some(false);
} else if hdr.packet_type == PacketType::HandShake as u8 {
let rsp = Self::decode_handshake_packet(&first_pkt)?;
self.priority = rsp.connection_priority;
handshake_recved(self, &rsp.network_name)?;
tracing::info!("handshake request: {:?}", rsp);
self.record_control_rx(&rsp.network_name, first_pkt.buf_len() as u64);
@@ -1352,7 +1372,9 @@ 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 {
let recv_limiter = if is_foreign_network
&& self.global_ctx.get_flags().foreign_relay_bps_limit != u64::MAX
{
let relay_network_bps_limit = self.global_ctx.get_flags().foreign_relay_bps_limit;
let limiter_config = LimiterConfig {
burst_rate: None,
@@ -1564,6 +1586,7 @@ impl PeerConn {
.as_ref()
.map(|x| x.peer_identity_type as i32)
.unwrap_or(PeerIdentityType::Admin as i32),
priority: self.priority,
}
}
@@ -2086,6 +2109,7 @@ pub mod tests {
.local_public_key
.unwrap(),
),
priority: crate::common::config::DEFAULT_CONNECTION_PRIORITY,
}]);
let ps = Arc::new(PeerSessionStore::new());
+206 -20
View File
@@ -22,6 +22,7 @@ use crate::{
common::{
PeerId,
compressor::{Compressor as _, DefaultCompressor},
config::DEFAULT_CONNECTION_PRIORITY,
constants::EASYTIER_VERSION,
error::Error,
global_ctx::{ArcGlobalCtx, GlobalCtxEvent, NetworkIdentity},
@@ -31,6 +32,7 @@ use crate::{
},
peers::{
PeerPacketFilter,
peer::Peer,
peer_conn::PeerConn,
peer_rpc::PeerRpcManagerTransport,
peer_session::PeerSessionStore,
@@ -38,7 +40,7 @@ use crate::{
route_trait::{ForeignNetworkRouteInfoMap, MockRoute, NextHopPolicy, RouteInterface},
traffic_metrics::{
InstanceLabelKind, LogicalTrafficMetrics, TrafficKind, TrafficMetricRecorder,
route_peer_info_instance_id, traffic_kind,
is_relay_data_packet_type, route_peer_info_instance_id, traffic_kind,
},
},
proto::{
@@ -263,9 +265,7 @@ impl PeerManager {
.is_err()
{
// if local network is not in whitelist, avoid relay data when exist any other route path
let mut f = global_ctx.get_feature_flags();
f.avoid_relay_data = true;
global_ctx.set_feature_flags(f);
global_ctx.set_avoid_relay_data_preference(true);
}
let is_secure_mode_enabled = global_ctx
@@ -596,6 +596,22 @@ impl PeerManager {
tunnel: Box<dyn Tunnel>,
is_directly_connected: bool,
peer_id_hint: Option<PeerId>,
) -> Result<(PeerId, PeerConnId), Error> {
self.add_client_tunnel_with_peer_id_hint_and_priority(
tunnel,
is_directly_connected,
peer_id_hint,
DEFAULT_CONNECTION_PRIORITY,
)
.await
}
pub(crate) async fn add_client_tunnel_with_peer_id_hint_and_priority(
&self,
tunnel: Box<dyn Tunnel>,
is_directly_connected: bool,
peer_id_hint: Option<PeerId>,
priority: u32,
) -> Result<(PeerId, PeerConnId), Error> {
let mut peer = PeerConn::new_with_peer_id_hint(
self.my_peer_id,
@@ -604,6 +620,7 @@ impl PeerManager {
peer_id_hint,
self.peer_session_store.clone(),
);
peer.set_priority(priority);
peer.set_is_hole_punched(!is_directly_connected);
peer.do_handshake_as_client().await?;
let conn_id = peer.get_conn_id();
@@ -618,6 +635,14 @@ impl PeerManager {
Ok((peer_id, conn_id))
}
fn get_peer_by_id(&self, peer_id: PeerId) -> Option<Arc<Peer>> {
self.peers.get_peer_by_id(peer_id).or_else(|| {
self.foreign_network_client
.get_peer_map()
.get_peer_by_id(peer_id)
})
}
pub fn has_directly_connected_conn(&self, peer_id: PeerId) -> bool {
if let Some(peer) = self.peers.get_peer_by_id(peer_id) {
peer.has_directly_connected_conn()
@@ -626,6 +651,20 @@ impl PeerManager {
}
}
pub(crate) fn has_directly_connected_conn_with_priority_at_most(
&self,
peer_id: PeerId,
priority: u32,
) -> bool {
self.get_peer_by_id(peer_id)
.is_some_and(|peer| peer.has_directly_connected_conn_with_priority_at_most(priority))
}
pub(crate) fn has_conn_with_priority_at_most(&self, peer_id: PeerId, priority: u32) -> bool {
self.get_peer_by_id(peer_id)
.is_some_and(|peer| peer.has_conn_with_priority_at_most(priority))
}
#[tracing::instrument]
pub async fn try_direct_connect<C>(&self, connector: C) -> Result<(PeerId, PeerConnId), Error>
where
@@ -644,11 +683,12 @@ impl PeerManager {
where
C: TunnelConnector + Debug,
{
let priority = connector.priority();
let ns = self.global_ctx.net_ns.clone();
let t = ns
.run_async(|| async move { connector.connect().await })
.await?;
self.add_client_tunnel_with_peer_id_hint(t, true, peer_id_hint)
self.add_client_tunnel_with_peer_id_hint_and_priority(t, true, peer_id_hint, priority)
.await
}
@@ -689,6 +729,11 @@ 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,
@@ -704,7 +749,8 @@ impl PeerManager {
tunnel,
self.peer_session_store.clone(),
);
conn.do_handshake_as_server_ext(|peer, network_name:&str| {
let mut reserved_peer_id_network_name = None;
let handshake_ret = conn.do_handshake_as_server_ext(|peer, network_name:&str| {
if network_name
== self.global_ctx.get_network_identity().network_name
{
@@ -715,6 +761,7 @@ 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());
@@ -730,7 +777,14 @@ impl PeerManager {
Ok(())
})
.await?;
.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);
}
let peer_identity = conn.get_network_identity();
let peer_network_name = peer_identity.network_name.clone();
@@ -749,6 +803,7 @@ 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(),
));
@@ -756,14 +811,18 @@ impl PeerManager {
conn.set_is_hole_punched(!is_directly_connected);
if is_local_network {
self.add_new_peer_conn(conn).await?;
let add_peer_ret = if is_local_network {
self.add_new_peer_conn(conn).await
} else {
self.foreign_network_manager.add_peer_conn(conn).await?;
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.reserved_my_peer_id_map.remove(&peer_network_name);
shrink_dashmap(&self.reserved_my_peer_id_map, None);
self.release_reserved_peer_id(&peer_network_name);
tracing::info!("add tunnel as server done");
Ok(())
@@ -774,6 +833,7 @@ 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 {
@@ -783,6 +843,16 @@ 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();
@@ -872,6 +942,29 @@ 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;
@@ -925,14 +1018,21 @@ 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 Err(mut ret) =
Self::try_handle_foreign_network_packet(ret, my_peer_id, &peers, &foreign_mgr)
.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
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;
@@ -944,6 +1044,16 @@ 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;
@@ -2080,7 +2190,7 @@ mod tests {
},
},
proto::{
common::{CompressionAlgoPb, NatType, PeerFeatureFlag},
common::{CompressionAlgoPb, NatType},
peer_rpc::SecureAuthLevel,
},
tunnel::{
@@ -2224,6 +2334,84 @@ 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;
@@ -2889,6 +3077,7 @@ mod tests {
crate::common::config::PeerConfig {
uri: server_remote_url,
peer_public_key: Some(server_pub_b64.clone()),
priority: crate::common::config::DEFAULT_CONNECTION_PRIORITY,
},
]);
@@ -3121,10 +3310,7 @@ 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_feature_flags(PeerFeatureFlag {
avoid_relay_data: true,
..Default::default()
});
.set_avoid_relay_data_preference(true);
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
+81 -2
View File
@@ -1228,6 +1228,25 @@ 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,
{
@@ -1248,6 +1267,9 @@ 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() {
@@ -2735,7 +2757,11 @@ 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(network_identity.network_secret.as_deref());
.verify_and_update_credential_trusts_with_active_peers_protecting(
network_identity.network_secret.as_deref(),
|_| true,
Some(self.my_peer_id),
);
self.global_ctx
.update_trusted_keys(global_trusted_keys, &network_identity.network_name);
@@ -2751,9 +2777,10 @@ impl PeerRouteServiceImpl {
let (untrusted, global_trusted_keys) = self
.synced_route_info
.verify_and_update_credential_trusts_with_active_peers(
.verify_and_update_credential_trusts_with_active_peers_protecting(
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);
@@ -5047,6 +5074,58 @@ 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";
+16 -5
View File
@@ -5,7 +5,8 @@ use crate::{
proto::{
common::Void,
peer_rpc::{
DirectConnectorRpc, GetIpListRequest, GetIpListResponse, SendUdpHolePunchPacketRequest,
DirectConnectorRpc, GetIpListRequest, GetIpListResponse, ListenerInfo,
SendUdpHolePunchPacketRequest,
},
rpc_types::{self, controller::BaseController},
},
@@ -44,13 +45,23 @@ impl DirectConnectorRpc for DirectConnectorManagerRpcServer {
_: GetIpListRequest,
) -> rpc_types::error::Result<GetIpListResponse> {
let mut ret = self.global_ctx.get_ip_collector().collect_ip_addrs().await;
ret.listeners = self
let listener_configs = self
.global_ctx
.config
.get_mapped_listeners()
.get_mapped_listener_configs()
.into_iter()
.chain(self.global_ctx.get_running_listeners())
.map(Into::into)
.chain(self.global_ctx.get_running_listener_configs())
.collect::<Vec<_>>();
ret.listeners = listener_configs
.iter()
.map(|listener| listener.url.clone().into())
.collect();
ret.listener_infos = listener_configs
.into_iter()
.map(|listener| ListenerInfo {
url: Some(listener.url.into()),
priority: listener.priority,
})
.collect();
remove_easytier_managed_ipv6s(&mut ret, &self.global_ctx);
tracing::trace!(
+5 -1
View File
@@ -7,7 +7,10 @@ use anyhow::anyhow;
use dashmap::DashMap;
use super::secure_datagram::{SecureDatagramDirection, SecureDatagramSession};
use crate::{common::PeerId, tunnel::packet_def::ZCPacket};
use crate::{
common::{PeerId, shrink_dashmap},
tunnel::packet_def::ZCPacket,
};
pub struct UpsertResponderSessionReturn {
pub session: Arc<PeerSession>,
@@ -78,6 +81,7 @@ 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 -1
View File
@@ -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},
common::{PeerId, global_ctx::ArcGlobalCtx, shrink_dashmap},
peers::peer_map::PeerMap,
peers::peer_session::{PeerSession, PeerSessionAction, PeerSessionStore, SessionKey},
peers::route_trait::NextHopPolicy,
@@ -652,6 +652,10 @@ 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 {
@@ -679,6 +683,10 @@ 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");
}
+20
View File
@@ -201,6 +201,11 @@ 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()),
@@ -241,6 +246,13 @@ 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>,
@@ -326,6 +338,14 @@ 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)
}
+15
View File
@@ -141,6 +141,21 @@ pub mod instance {
ret
}
pub fn get_conn_priority(&self) -> Option<u32> {
let p = self.peer.as_ref()?;
let default_conn_id = p.default_conn_id.map(|id| id.to_string());
let mut ret = None;
for conn in p.conns.iter() {
if default_conn_id == Some(conn.conn_id.to_string()) {
return Some(conn.priority);
}
ret.get_or_insert(conn.priority);
}
ret
}
fn get_tunnel_proto_str(tunnel_info: &super::super::common::TunnelInfo) -> String {
tunnel_info.display_tunnel_type()
}
+2
View File
@@ -27,6 +27,7 @@ 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 {
@@ -42,6 +43,7 @@ message StringPatch {
message UrlPatch {
ConfigPatchAction action = 1;
common.Url url = 2;
optional uint32 priority = 3;
}
message AclPatch {
+6 -1
View File
@@ -45,6 +45,7 @@ message PeerConnInfo {
bytes noise_remote_static_pubkey = 12;
peer_rpc.SecureAuthLevel secure_auth_level = 13;
peer_rpc.PeerIdentityType peer_identity_type = 14;
uint32 priority = 15;
}
message PeerInfo {
@@ -208,6 +209,7 @@ enum ConnectorStatus {
message Connector {
common.Url url = 1;
ConnectorStatus status = 2;
uint32 priority = 3;
}
message ListConnectorRequest { InstanceIdentifier instance = 1; }
@@ -218,7 +220,10 @@ service ConnectorManageRpc {
rpc ListConnector(ListConnectorRequest) returns (ListConnectorResponse);
}
message MappedListener { common.Url url = 1; }
message MappedListener {
common.Url url = 1;
uint32 priority = 2;
}
message ListMappedListenerRequest { InstanceIdentifier instance = 1; }
+1
View File
@@ -99,6 +99,7 @@ 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 {
+1
View File
@@ -75,6 +75,7 @@ message FlagsInConfig {
bool need_p2p = 38;
uint64 instance_recv_bps_limit = 39;
bool disable_upnp = 40;
bool disable_relay_data = 41;
}
message RpcDescriptor {
+8
View File
@@ -184,6 +184,12 @@ message GetIpListResponse {
common.Ipv6Addr public_ipv6 = 3;
repeated common.Ipv6Addr interface_ipv6s = 4;
repeated common.Url listeners = 5;
repeated ListenerInfo listener_infos = 6;
}
message ListenerInfo {
common.Url url = 1;
uint32 priority = 2;
}
message SendUdpHolePunchPacketRequest {
@@ -314,6 +320,7 @@ message HandshakeRequest {
repeated string features = 4;
string network_name = 5;
bytes network_secret_digest = 6;
uint32 connection_priority = 7;
}
message KcpConnData {
@@ -346,6 +353,7 @@ message PeerConnNoiseMsg1Pb {
optional uint32 a_session_generation = 3;
common.UUID a_conn_id = 4;
string client_encryption_algorithm = 5;
uint32 connection_priority = 6;
}
message PeerConnNoiseMsg2Pb {
+330 -4
View File
@@ -14,13 +14,17 @@ 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},
tunnel::{common::tests::wait_for_condition, tcp::TcpTunnelConnector, udp::UdpTunnelConnector},
};
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)
@@ -221,6 +225,328 @@ 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,
@@ -501,10 +827,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)
ff.avoid_relay_data = allow_relay;
admin_inst.get_global_ctx().set_feature_flags(ff);
admin_inst
.get_global_ctx()
.set_avoid_relay_data_preference(allow_relay);
admin_inst.run().await.unwrap();
let admin_peer_id = admin_inst.peer_id();
+147
View File
@@ -3730,6 +3730,153 @@ 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>,
local_addr: SocketAddr,
interface_name: &str,
src_addr: Option<SocketAddr>,
dst_addr: SocketAddr,
) -> io::Result<Arc<dyn super::stack::Tun>> {
match windivert::WinDivertTun::new(local_addr) {
match windivert::WinDivertTun::new(src_addr, dst_addr) {
Ok(tun) => Ok(Arc::new(tun)),
Err(e) => {
tracing::warn!(
?e,
?local_addr,
?dst_addr,
"WinDivertTun init failed, falling back to PnetTun"
);
Ok(Arc::new(pnet::PnetTun::new(
local_addr.to_string().as_str(),
pnet::create_packet_filter(None, local_addr),
interface_name,
pnet::create_packet_filter(src_addr, dst_addr),
)?))
}
}
@@ -80,15 +80,11 @@ impl Drop for WinDivertTun {
}
impl WinDivertTun {
pub fn new(local_addr: SocketAddr) -> io::Result<Self> {
pub fn new(src_addr: Option<SocketAddr>, dst_addr: SocketAddr) -> io::Result<Self> {
let (tx, rx) = tokio::sync::mpsc::channel(1024);
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);
let filter = build_filter(src_addr, dst_addr)?;
tracing::debug!(%filter, "WinDivertTun created with filter");
// Sniff mode: 1 (WINDIVERT_FLAG_SNIFF)
// Layer: Network (0)
@@ -143,6 +139,46 @@ 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> {
-1
View File
@@ -128,7 +128,6 @@ pub fn build_tcp_packet(
eth_buf.freeze()
}
#[tracing::instrument(ret)]
pub fn parse_ip_packet(
buf: &Bytes,
) -> Option<(MacAddr, MacAddr, IPPacket<'_>, tcp::TcpPacket<'_>)> {
+4 -1
View File
@@ -517,9 +517,12 @@ 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 => {
+43 -1
View File
@@ -3,7 +3,7 @@ use std::{
};
use crate::{
common::{dns::socket_addrs, error::Error},
common::{config::DEFAULT_CONNECTION_PRIORITY, dns::socket_addrs, error::Error},
proto::common::TunnelInfo,
};
use async_trait::async_trait;
@@ -139,11 +139,53 @@ pub trait TunnelListener: Send {
pub trait TunnelConnector: Send {
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError>;
fn remote_url(&self) -> url::Url;
fn priority(&self) -> u32 {
DEFAULT_CONNECTION_PRIORITY
}
fn set_bind_addrs(&mut self, _addrs: Vec<SocketAddr>) {}
fn set_ip_version(&mut self, _ip_version: IpVersion) {}
fn set_resolved_addr(&mut self, _addr: SocketAddr) {}
}
#[derive(Debug)]
pub struct PrioritizedConnector<C> {
inner: C,
priority: u32,
}
impl<C> PrioritizedConnector<C> {
pub fn new(inner: C, priority: u32) -> Self {
Self { inner, priority }
}
}
#[async_trait]
impl<C: TunnelConnector> TunnelConnector for PrioritizedConnector<C> {
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
self.inner.connect().await
}
fn remote_url(&self) -> url::Url {
self.inner.remote_url()
}
fn priority(&self) -> u32 {
self.priority
}
fn set_bind_addrs(&mut self, addrs: Vec<SocketAddr>) {
self.inner.set_bind_addrs(addrs);
}
fn set_ip_version(&mut self, ip_version: IpVersion) {
self.inner.set_ip_version(ip_version);
}
fn set_resolved_addr(&mut self, addr: SocketAddr) {
self.inner.set_resolved_addr(addr);
}
}
pub fn build_url_from_socket_addr(addr: &String, scheme: &str) -> url::Url {
if let Ok(sock_addr) = addr.parse::<SocketAddr>() {
let url_str = format!("{}://0.0.0.0", scheme);
+11
View File
@@ -730,6 +730,17 @@ 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();
+202 -16
View File
@@ -14,8 +14,8 @@ use derivative::Derivative;
use derive_more::{Deref, DerefMut};
use parking_lot::RwLock;
use quinn::{
ClientConfig, Connection, Endpoint, EndpointConfig, ServerConfig, TransportConfig,
congestion::BbrConfig, default_runtime,
ClientConfig, ConnectError, Connection, Endpoint, EndpointConfig, ServerConfig,
TransportConfig, congestion::BbrConfig, default_runtime,
};
use std::net::{Ipv4Addr, Ipv6Addr};
use std::sync::OnceLock;
@@ -135,6 +135,12 @@ 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();
@@ -168,6 +174,49 @@ 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
@@ -262,6 +311,20 @@ 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
@@ -288,14 +351,8 @@ impl QuicEndpointManager {
Ok(endpoint)
}
/// 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| {
fn client_endpoint(&self, ip_version: IpVersion) -> Result<Endpoint, TunnelError> {
let (pool, endpoint) = self.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()),
@@ -318,6 +375,26 @@ 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,
@@ -327,14 +404,52 @@ impl QuicEndpointManager {
} else {
IpVersion::V6
};
let endpoint = Self::client(global_ctx, ip_version)?;
let connection = endpoint
.connect(addr, "localhost")
.with_context(|| format!("failed to create connection to {}", addr))?
Self::load(global_ctx)
.connect_with_ip_version(addr, ip_version)
.await
.with_context(|| format!("failed to connect to {}", addr))?;
}
Ok((endpoint, connection))
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));
}
}
}
//endregion
@@ -398,6 +513,18 @@ 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> {
@@ -516,6 +643,20 @@ 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())
@@ -591,6 +732,51 @@ 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())
+63 -5
View File
@@ -2,13 +2,17 @@ use std::sync::Arc;
use crate::{
common::{
config::TomlConfigLoader, global_ctx::GlobalCtx, log, os_info::collect_device_os_info,
set_default_machine_id, stun::MockStunInfoCollector,
config::TomlConfigLoader,
global_ctx::{ArcGlobalCtx, 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, TunnelConnector},
tunnel::{IpVersion, Tunnel, TunnelConnector, TunnelError, TunnelScheme},
};
use anyhow::{Context as _, Result};
use async_trait::async_trait;
@@ -49,6 +53,30 @@ 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,
@@ -218,6 +246,13 @@ 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("");
@@ -243,16 +278,20 @@ 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(
create_connector_by_url(c_url.as_str(), &global_ctx, IpVersion::Both).await?,
ConfigServerConnector {
url: c_url,
global_ctx,
},
token.to_string(),
hostname,
secure_mode,
manager.clone(),
manager,
hooks,
))
}
@@ -292,4 +331,23 @@ 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);
}
}
-1
View File
@@ -3,7 +3,6 @@
"private": true,
"pnpm": {
"overrides": {
"caniuse-lite": "1.0.30001791",
"minimatch": "10.2.4"
}
}
+9 -277
View File
@@ -5,7 +5,6 @@ settings:
excludeLinksFromLockfile: false
overrides:
caniuse-lite: 1.0.30001791
minimatch: 10.2.4
importers:
@@ -59,7 +58,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)(vitest@2.1.9(@types/node@22.18.1))
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)
'@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))
@@ -287,9 +286,6 @@ 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)
@@ -1677,35 +1673,6 @@ 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==}
@@ -2105,10 +2072,6 @@ 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'}
@@ -2171,10 +2134,6 @@ 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'}
@@ -2187,16 +2146,12 @@ packages:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
caniuse-lite@1.0.30001791:
resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==}
caniuse-lite@1.0.30001741:
resolution: {integrity: sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==}
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'}
@@ -2208,10 +2163,6 @@ 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'}
@@ -2300,10 +2251,6 @@ 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==}
@@ -2381,9 +2328,6 @@ 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'}
@@ -2652,10 +2596,6 @@ 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==}
@@ -3077,9 +3017,6 @@ 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==}
@@ -3433,10 +3370,6 @@ 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==}
@@ -3679,9 +3612,6 @@ 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==}
@@ -3734,12 +3664,6 @@ 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'}
@@ -3829,27 +3753,9 @@ 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'}
@@ -4069,11 +3975,6 @@ 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:
@@ -4149,31 +4050,6 @@ 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==}
@@ -4246,11 +4122,6 @@ 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'}
@@ -4309,7 +4180,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)(vitest@2.1.9(@types/node@22.18.1))':
'@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)':
dependencies:
'@antfu/install-pkg': 1.1.0
'@clack/prompts': 0.9.1
@@ -4318,7 +4189,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@2.1.9(@types/node@22.18.1))
'@vitest/eslint-plugin': 1.3.9(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)
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
@@ -5515,57 +5386,16 @@ 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@2.1.9(@types/node@22.18.1))':
'@vitest/eslint-plugin@1.3.9(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)':
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
@@ -6140,8 +5970,6 @@ snapshots:
argparse@2.0.1: {}
assertion-error@2.0.1: {}
ast-kit@1.4.3:
dependencies:
'@babel/parser': 7.28.4
@@ -6157,7 +5985,7 @@ snapshots:
autoprefixer@10.4.21(postcss@8.5.6):
dependencies:
browserslist: 4.25.4
caniuse-lite: 1.0.30001791
caniuse-lite: 1.0.30001741
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.1.1
@@ -6190,7 +6018,7 @@ snapshots:
browserslist@4.25.4:
dependencies:
caniuse-lite: 1.0.30001791
caniuse-lite: 1.0.30001741
electron-to-chromium: 1.5.214
node-releases: 2.0.20
update-browserslist-db: 1.1.3(browserslist@4.25.4)
@@ -6206,8 +6034,6 @@ 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
@@ -6217,18 +6043,10 @@ snapshots:
camelcase-css@2.0.1: {}
caniuse-lite@1.0.30001791: {}
caniuse-lite@1.0.30001741: {}
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
@@ -6240,8 +6058,6 @@ snapshots:
dependencies:
'@kurkle/color': 0.3.4
check-error@2.1.3: {}
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
@@ -6322,8 +6138,6 @@ snapshots:
dependencies:
character-entities: 2.0.2
deep-eql@5.0.2: {}
deep-is@0.1.4: {}
default-browser-id@5.0.0: {}
@@ -6384,8 +6198,6 @@ snapshots:
es-errors@1.3.0: {}
es-module-lexer@1.7.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
@@ -6802,8 +6614,6 @@ 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:
@@ -7167,8 +6977,6 @@ snapshots:
longest-streak@3.1.0: {}
loupe@3.2.1: {}
lru-cache@10.4.3: {}
lru-cache@5.1.1:
@@ -7694,8 +7502,6 @@ snapshots:
pathe@2.0.3: {}
pathval@2.0.1: {}
perfect-debounce@1.0.0: {}
picocolors@1.1.1: {}
@@ -7932,8 +7738,6 @@ snapshots:
shebang-regex@3.0.0: {}
siginfo@2.0.0: {}
signal-exit@3.0.7: {}
signal-exit@4.1.0: {}
@@ -7981,10 +7785,6 @@ 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:
@@ -8095,18 +7895,8 @@ 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
@@ -8409,24 +8199,6 @@ 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)
@@ -8518,41 +8290,6 @@ 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)):
@@ -8622,11 +8359,6 @@ 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: