mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-16 19:05:38 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd2e3488fe | |||
| 67366aba55 | |||
| 463228f1de | |||
| 7908f9c146 |
Generated
+22
-39
@@ -2273,7 +2273,6 @@ dependencies = [
|
||||
"gethostname 0.5.0",
|
||||
"git-version",
|
||||
"globwalk",
|
||||
"guarden",
|
||||
"hickory-client",
|
||||
"hickory-proto",
|
||||
"hickory-resolver",
|
||||
@@ -2457,7 +2456,6 @@ dependencies = [
|
||||
"dashmap",
|
||||
"easytier",
|
||||
"futures",
|
||||
"guarden",
|
||||
"jsonwebtoken",
|
||||
"mimalloc",
|
||||
"mockall",
|
||||
@@ -3592,28 +3590,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "guarden"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca87812d87fa82896df1adfb5c111cdeaae3edb6da028f5df002dcbd7df71454"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"guarden-macros",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "guarden-macros"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b42f4b8de91cbd793ce8e6cf8d4821ef02d2d5b4468e0a55a36c65c5581de53"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.7"
|
||||
@@ -3729,6 +3705,12 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
@@ -4044,7 +4026,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.1",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -4713,9 +4695,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
version = "0.2.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
@@ -5061,13 +5043,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.0"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.9",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6568,7 +6551,7 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"hermit-abi 0.5.2",
|
||||
"pin-project-lite",
|
||||
"rustix 1.0.7",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -8667,12 +8650,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.3"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9791,9 +9774,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.1"
|
||||
version = "1.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
|
||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -9801,7 +9784,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.6.3",
|
||||
"socket2 0.6.1",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -9809,9 +9792,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -12,7 +12,6 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||
guarden = "0.1"
|
||||
|
||||
# Axum web framework
|
||||
axum = { version = "0.8.4", features = ["macros"] }
|
||||
|
||||
@@ -10,9 +10,9 @@ use easytier::{
|
||||
common::config::{
|
||||
ConfigFileControl, ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader,
|
||||
},
|
||||
defer,
|
||||
instance_manager::NetworkInstanceManager,
|
||||
};
|
||||
use guarden::defer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::any;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
"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",
|
||||
|
||||
@@ -18,7 +18,6 @@ export interface ServiceMode extends WebClientConfig {
|
||||
rpc_portal: string
|
||||
file_log_level: 'off' | 'warn' | 'info' | 'debug' | 'trace'
|
||||
file_log_dir: string
|
||||
installed_core_version?: string
|
||||
}
|
||||
|
||||
export interface RemoteMode {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useToast, useConfirm } from 'primevue'
|
||||
import { loadMode, saveMode, WebClientConfig, type Mode } from '~/composables/mode'
|
||||
import { saveLastNetworkInstanceId, loadLastNetworkInstanceId } from '~/composables/config'
|
||||
import ModeSwitcher from '~/components/ModeSwitcher.vue'
|
||||
import { getEasytierVersion, getServiceStatus } from '~/composables/backend'
|
||||
import { getServiceStatus } from '~/composables/backend'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const confirm = useConfirm()
|
||||
@@ -85,20 +85,6 @@ async function onUninstallService() {
|
||||
});
|
||||
}
|
||||
|
||||
function stripModeMetadata(mode: Mode) {
|
||||
if (mode.mode !== 'service') {
|
||||
return mode
|
||||
}
|
||||
|
||||
const serviceConfig = { ...mode }
|
||||
delete serviceConfig.installed_core_version
|
||||
return serviceConfig
|
||||
}
|
||||
|
||||
function modeConfigChanged(next: Mode) {
|
||||
return JSON.stringify(stripModeMetadata(next)) !== JSON.stringify(stripModeMetadata(currentMode.value))
|
||||
}
|
||||
|
||||
async function onStopService() {
|
||||
isModeSaving.value = true
|
||||
manualDisconnect.value = true
|
||||
@@ -148,14 +134,13 @@ async function initWithMode(mode: Mode) {
|
||||
}
|
||||
url = mode.remote_rpc_address
|
||||
break;
|
||||
case 'service': {
|
||||
case 'service':
|
||||
if (!mode.config_dir || !mode.file_log_dir || !mode.file_log_level || !mode.rpc_portal) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: t('mode.service_config_empty'), life: 10000 })
|
||||
return initWithMode({ ...mode, mode: 'normal' });
|
||||
}
|
||||
let serviceStatus = await getServiceStatus()
|
||||
const coreVersion = await getEasytierVersion()
|
||||
if (serviceStatus === "NotInstalled" || modeConfigChanged(mode) || mode.installed_core_version !== coreVersion) {
|
||||
if (serviceStatus === "NotInstalled" || JSON.stringify(mode) !== JSON.stringify(currentMode.value)) {
|
||||
mode.config_server_url = mode.config_server_url || undefined
|
||||
await initService({
|
||||
config_dir: mode.config_dir,
|
||||
@@ -164,7 +149,6 @@ async function initWithMode(mode: Mode) {
|
||||
rpc_portal: mode.rpc_portal,
|
||||
config_server: mode.config_server_url,
|
||||
})
|
||||
mode.installed_core_version = coreVersion
|
||||
serviceStatus = await getServiceStatus()
|
||||
}
|
||||
if (serviceStatus === "Stopped") {
|
||||
@@ -173,7 +157,6 @@ async function initWithMode(mode: Mode) {
|
||||
url = "tcp://" + mode.rpc_portal.replace("0.0.0.0", "127.0.0.1")
|
||||
retrys = 5
|
||||
break;
|
||||
}
|
||||
case 'normal':
|
||||
url = mode.rpc_portal;
|
||||
break;
|
||||
|
||||
@@ -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')}/`,
|
||||
|
||||
@@ -5,7 +5,7 @@ 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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import InputGroup from 'primevue/inputgroup'
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { buildUrlInputValue, getHostInputValue, parseHostInputOnBlur, parseUrlInput } from '../modules/url-input'
|
||||
|
||||
const props = defineProps<{
|
||||
placeholder?: string
|
||||
@@ -33,11 +32,73 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const internalValue = ref(parseUrlInput(url.value, props.protos))
|
||||
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 }
|
||||
}
|
||||
|
||||
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 +107,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 +123,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 +140,6 @@ watch(() => url.value, (newVal) => {
|
||||
|
||||
// Sync to external
|
||||
watch(internalValue, () => {
|
||||
if (hostFocused.value) {
|
||||
return
|
||||
}
|
||||
syncUrlFromInternal(false)
|
||||
}, { deep: true })
|
||||
|
||||
@@ -119,8 +165,6 @@ const onProtoChange = (newProto: string) => {
|
||||
internalValue.value.port = newDefault
|
||||
}
|
||||
internalValue.value.proto = newProto
|
||||
internalValue.value.suffix = undefined
|
||||
internalValue.value.hasExplicitPort = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -130,7 +174,7 @@ const onProtoChange = (newProto: string) => {
|
||||
<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"
|
||||
@focus="onHostFocus" @blur="onHostBlur" />
|
||||
<template v-if="!isNoPortProto">
|
||||
<InputGroupAddon>
|
||||
@@ -160,7 +204,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">
|
||||
|
||||
@@ -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 ?? ''}`
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,8 +50,6 @@ time = "0.3"
|
||||
toml = "0.8.12"
|
||||
chrono = { version = "0.4.37", features = ["serde"] }
|
||||
|
||||
guarden = "0.1"
|
||||
|
||||
delegate = "0.13.5"
|
||||
|
||||
itertools = "0.14.0"
|
||||
|
||||
@@ -121,8 +121,9 @@ pub async fn socket_addrs(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::defer;
|
||||
|
||||
use super::*;
|
||||
use guarden::defer;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_socket_addrs() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
collections::{BTreeSet, HashMap, hash_map::DefaultHasher},
|
||||
collections::{HashMap, hash_map::DefaultHasher},
|
||||
hash::Hasher,
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::{Arc, Mutex},
|
||||
@@ -202,8 +202,6 @@ pub struct GlobalCtx {
|
||||
|
||||
cached_ipv4: AtomicCell<Option<cidr::Ipv4Inet>>,
|
||||
cached_ipv6: AtomicCell<Option<cidr::Ipv6Inet>>,
|
||||
public_ipv6_lease: AtomicCell<Option<cidr::Ipv6Inet>>,
|
||||
public_ipv6_routes: Mutex<BTreeSet<std::net::Ipv6Addr>>,
|
||||
cached_proxy_cidrs: AtomicCell<Option<Vec<ProxyNetworkConfig>>>,
|
||||
|
||||
ip_collector: Mutex<Option<Arc<IPCollector>>>,
|
||||
@@ -300,8 +298,6 @@ impl GlobalCtx {
|
||||
event_bus,
|
||||
cached_ipv4: AtomicCell::new(None),
|
||||
cached_ipv6: AtomicCell::new(None),
|
||||
public_ipv6_lease: AtomicCell::new(None),
|
||||
public_ipv6_routes: Mutex::new(BTreeSet::new()),
|
||||
cached_proxy_cidrs: AtomicCell::new(None),
|
||||
|
||||
ip_collector: Mutex::new(Some(Arc::new(IPCollector::new(
|
||||
@@ -389,31 +385,6 @@ impl GlobalCtx {
|
||||
self.cached_ipv6.store(None);
|
||||
}
|
||||
|
||||
pub fn get_public_ipv6_lease(&self) -> Option<cidr::Ipv6Inet> {
|
||||
self.public_ipv6_lease.load()
|
||||
}
|
||||
|
||||
pub fn set_public_ipv6_lease(&self, addr: Option<cidr::Ipv6Inet>) {
|
||||
self.public_ipv6_lease.store(addr);
|
||||
}
|
||||
|
||||
pub fn set_public_ipv6_routes(&self, routes: BTreeSet<cidr::Ipv6Inet>) {
|
||||
*self.public_ipv6_routes.lock().unwrap() =
|
||||
routes.into_iter().map(|route| route.address()).collect();
|
||||
}
|
||||
|
||||
pub fn is_ip_local_ipv6(&self, ip: &std::net::Ipv6Addr) -> bool {
|
||||
self.get_ipv6().map(|x| x.address() == *ip).unwrap_or(false)
|
||||
|| self
|
||||
.get_public_ipv6_lease()
|
||||
.map(|x| x.address() == *ip)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn is_ip_easytier_managed_ipv6(&self, ip: &std::net::Ipv6Addr) -> bool {
|
||||
self.is_ip_local_ipv6(ip) || self.public_ipv6_routes.lock().unwrap().contains(ip)
|
||||
}
|
||||
|
||||
pub fn get_advertised_ipv6_public_addr_prefix(&self) -> Option<cidr::Ipv6Cidr> {
|
||||
*self.advertised_ipv6_public_addr_prefix.lock().unwrap()
|
||||
}
|
||||
@@ -442,7 +413,7 @@ impl GlobalCtx {
|
||||
pub fn is_ip_local_virtual_ip(&self, ip: &IpAddr) -> bool {
|
||||
match ip {
|
||||
IpAddr::V4(v4) => self.get_ipv4().map(|x| x.address() == *v4).unwrap_or(false),
|
||||
IpAddr::V6(v6) => self.is_ip_local_ipv6(v6),
|
||||
IpAddr::V6(v6) => self.get_ipv6().map(|x| x.address() == *v6).unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,23 +663,23 @@ impl GlobalCtx {
|
||||
pub fn should_deny_proxy(&self, dst_addr: &SocketAddr, is_udp: bool) -> bool {
|
||||
let _g = self.net_ns.guard();
|
||||
let ip = dst_addr.ip();
|
||||
// first check if ip is an EasyTier-managed local address
|
||||
// first check if ip is virtual ip
|
||||
// then try bind this ip, if succ means it is local ip
|
||||
let dst_is_local_et_ip = self.is_ip_local_virtual_ip(&ip);
|
||||
let dst_is_local_virtual_ip = self.is_ip_local_virtual_ip(&ip);
|
||||
// this is an expensive operation, should be called sparingly
|
||||
// 1. tcp/kcp/quic call this only after proxy conn is established
|
||||
// 2. udp cache the result in nat entry
|
||||
let dst_is_local_phy_ip = std::net::UdpSocket::bind(format!("{}:0", ip)).is_ok();
|
||||
|
||||
tracing::trace!(
|
||||
"check should_deny_proxy: dst_addr={}, dst_is_local_et_ip={}, dst_is_local_phy_ip={}, is_udp={}",
|
||||
"check should_deny_proxy: dst_addr={}, dst_is_local_virtual_ip={}, dst_is_local_phy_ip={}, is_udp={}",
|
||||
dst_addr,
|
||||
dst_is_local_et_ip,
|
||||
dst_is_local_virtual_ip,
|
||||
dst_is_local_phy_ip,
|
||||
is_udp
|
||||
);
|
||||
|
||||
if dst_is_local_et_ip || dst_is_local_phy_ip {
|
||||
if dst_is_local_virtual_ip || dst_is_local_phy_ip {
|
||||
// if is local ip, make sure the port is not one of the listening ports
|
||||
self.is_port_in_running_listeners(dst_addr.port(), is_udp)
|
||||
|| (!is_udp && protected_port::is_protected_tcp_port(dst_addr.port()))
|
||||
@@ -837,40 +808,6 @@ pub mod tests {
|
||||
protected_port::clear_protected_tcp_ports_for_test();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn virtual_ipv6_and_public_ipv6_lease_are_stored_separately() {
|
||||
let config = TomlConfigLoader::default();
|
||||
let global_ctx = GlobalCtx::new(config);
|
||||
let virtual_ipv6 = "fd00::1/64".parse().unwrap();
|
||||
let public_ipv6 = "2001:db8::2/64".parse().unwrap();
|
||||
|
||||
global_ctx.set_ipv6(Some(virtual_ipv6));
|
||||
global_ctx.set_public_ipv6_lease(Some(public_ipv6));
|
||||
|
||||
assert_eq!(global_ctx.get_ipv6(), Some(virtual_ipv6));
|
||||
assert_eq!(global_ctx.get_public_ipv6_lease(), Some(public_ipv6));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn public_ipv6_lease_is_treated_as_local_ip() {
|
||||
protected_port::clear_protected_tcp_ports_for_test();
|
||||
|
||||
let config = TomlConfigLoader::default();
|
||||
let global_ctx = GlobalCtx::new(config);
|
||||
let public_ipv6 = "2001:db8::2/64".parse().unwrap();
|
||||
let listener: url::Url = "tcp://[2001:db8::2]:11010".parse().unwrap();
|
||||
global_ctx.set_public_ipv6_lease(Some(public_ipv6));
|
||||
global_ctx.add_running_listener(listener);
|
||||
|
||||
let ip = std::net::IpAddr::V6(public_ipv6.address());
|
||||
let socket = SocketAddr::from((public_ipv6.address(), 11010));
|
||||
|
||||
assert!(global_ctx.is_ip_local_virtual_ip(&ip));
|
||||
assert!(global_ctx.should_deny_proxy(&socket, false));
|
||||
|
||||
protected_port::clear_protected_tcp_ports_for_test();
|
||||
}
|
||||
|
||||
pub fn get_mock_global_ctx_with_network(
|
||||
network_identy: Option<NetworkIdentity>,
|
||||
) -> ArcGlobalCtx {
|
||||
|
||||
@@ -64,24 +64,6 @@ async fn resolve_mapped_listener_addrs(listener: &url::Url) -> Result<Vec<Socket
|
||||
socket_addrs(listener, || mapped_listener_port(listener)).await
|
||||
}
|
||||
|
||||
fn is_usable_public_ipv6_candidate(ip: &Ipv6Addr, global_ctx: &ArcGlobalCtx) -> bool {
|
||||
is_usable_public_ipv6_candidate_with_mode(ip, global_ctx, TESTING.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
fn is_usable_public_ipv6_candidate_with_mode(
|
||||
ip: &Ipv6Addr,
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
testing: bool,
|
||||
) -> bool {
|
||||
!global_ctx.is_ip_easytier_managed_ipv6(ip)
|
||||
&& (testing
|
||||
|| (!ip.is_loopback()
|
||||
&& !ip.is_unspecified()
|
||||
&& !ip.is_unique_local()
|
||||
&& !ip.is_unicast_link_local()
|
||||
&& !ip.is_multicast()))
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait PeerManagerForDirectConnector {
|
||||
async fn list_peers(&self) -> Vec<PeerId>;
|
||||
@@ -208,28 +190,34 @@ impl DirectConnectorManagerData {
|
||||
.with_context(|| format!("failed to bind local socket for {}", remote_url))?,
|
||||
);
|
||||
let connector_ip = self
|
||||
.global_ctx
|
||||
.peer_manager
|
||||
.get_global_ctx()
|
||||
.get_stun_info_collector()
|
||||
.get_stun_info()
|
||||
.public_ip
|
||||
.iter()
|
||||
.filter_map(|ip| ip.parse::<Ipv6Addr>().ok())
|
||||
.find(|ip| !self.global_ctx.is_ip_easytier_managed_ipv6(ip));
|
||||
.find(|x| x.contains(':'))
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"failed to get public ipv6 address from stun info"
|
||||
))?
|
||||
.parse::<Ipv6Addr>()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to parse public ipv6 address from stun info: {:?}",
|
||||
self.peer_manager
|
||||
.get_global_ctx()
|
||||
.get_stun_info_collector()
|
||||
.get_stun_info()
|
||||
)
|
||||
})?;
|
||||
let connector_addr =
|
||||
SocketAddr::new(IpAddr::V6(connector_ip), local_socket.local_addr()?.port());
|
||||
|
||||
// ask remote to send v6 hole punch packet
|
||||
// and no matter what the result is, continue to connect
|
||||
if let Some(connector_ip) = connector_ip {
|
||||
let connector_addr =
|
||||
SocketAddr::new(IpAddr::V6(connector_ip), local_socket.local_addr()?.port());
|
||||
let _ = self
|
||||
.remote_send_udp_hole_punch_packet(dst_peer_id, connector_addr, remote_url)
|
||||
.await;
|
||||
} else {
|
||||
tracing::debug!(
|
||||
?remote_url,
|
||||
"skip remote IPv6 hole-punch packet; no non-EasyTier public IPv6 in STUN info"
|
||||
);
|
||||
}
|
||||
let _ = self
|
||||
.remote_send_udp_hole_punch_packet(dst_peer_id, connector_addr, remote_url)
|
||||
.await;
|
||||
|
||||
let udp_connector = UdpTunnelConnector::new(remote_url.clone());
|
||||
let remote_addr = SocketAddr::from_url(remote_url.clone(), IpVersion::V6).await?;
|
||||
@@ -491,7 +479,14 @@ impl DirectConnectorManagerData {
|
||||
.iter()
|
||||
.chain(ip_list.public_ipv6.iter())
|
||||
.filter_map(|x| Ipv6Addr::from_str(&x.to_string()).ok())
|
||||
.filter(|x| is_usable_public_ipv6_candidate(x, &self.global_ctx))
|
||||
.filter(|x| {
|
||||
TESTING.load(Ordering::Relaxed)
|
||||
|| (!x.is_loopback()
|
||||
&& !x.is_unspecified()
|
||||
&& !x.is_unique_local()
|
||||
&& !x.is_unicast_link_local()
|
||||
&& !x.is_multicast())
|
||||
})
|
||||
.collect::<HashSet<_>>()
|
||||
.iter()
|
||||
.for_each(|ip| {
|
||||
@@ -520,11 +515,6 @@ impl DirectConnectorManagerData {
|
||||
);
|
||||
}
|
||||
});
|
||||
} else if self.global_ctx.is_ip_easytier_managed_ipv6(s_addr.ip()) {
|
||||
tracing::debug!(
|
||||
?listener,
|
||||
"skip EasyTier-managed IPv6 as direct-connect target"
|
||||
);
|
||||
} else if !s_addr.ip().is_loopback() || TESTING.load(Ordering::Relaxed) {
|
||||
if self
|
||||
.global_ctx
|
||||
@@ -800,10 +790,9 @@ impl DirectConnectorManager {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::BTreeSet, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
common::global_ctx::tests::get_mock_global_ctx,
|
||||
connector::direct::{
|
||||
DirectConnectorManager, DirectConnectorManagerData, DstListenerUrlBlackListItem,
|
||||
},
|
||||
@@ -813,41 +802,12 @@ mod tests {
|
||||
wait_route_appear_with_cost,
|
||||
},
|
||||
proto::peer_rpc::GetIpListResponse,
|
||||
tunnel::{IpScheme, TunnelScheme, matches_scheme},
|
||||
};
|
||||
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
use super::{TESTING, mapped_listener_port, resolve_mapped_listener_addrs};
|
||||
|
||||
#[tokio::test]
|
||||
async fn public_ipv6_candidate_rejects_easytier_managed_addr_even_in_tests() {
|
||||
let global_ctx = get_mock_global_ctx();
|
||||
let managed_ipv6: cidr::Ipv6Inet = "2001:db8::2/128".parse().unwrap();
|
||||
global_ctx.set_public_ipv6_routes(BTreeSet::from([managed_ipv6]));
|
||||
|
||||
assert!(!super::is_usable_public_ipv6_candidate_with_mode(
|
||||
&"2001:db8::2".parse().unwrap(),
|
||||
&global_ctx,
|
||||
true,
|
||||
));
|
||||
assert!(super::is_usable_public_ipv6_candidate_with_mode(
|
||||
&"::1".parse().unwrap(),
|
||||
&global_ctx,
|
||||
true,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udp_ipv6_url_matches_hole_punch_branch_condition() {
|
||||
let remote_url: url::Url = "udp://[2001:db8::1]:11010".parse().unwrap();
|
||||
let takes_udp_ipv6_hole_punch_branch =
|
||||
matches_scheme!(remote_url, TunnelScheme::Ip(IpScheme::Udp))
|
||||
&& matches!(remote_url.host(), Some(url::Host::Ipv6(_)));
|
||||
|
||||
assert!(takes_udp_ipv6_hole_punch_branch);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapped_listener_port_uses_ip_scheme_defaults() {
|
||||
assert_eq!(
|
||||
|
||||
+15
-180
@@ -1,17 +1,19 @@
|
||||
use std::net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
|
||||
use std::{
|
||||
net::{SocketAddr, SocketAddrV4, SocketAddrV6},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
common::{dns::socket_addrs, error::Error, global_ctx::ArcGlobalCtx, idn},
|
||||
common::{error::Error, global_ctx::ArcGlobalCtx, idn, network::IPCollector},
|
||||
connector::dns_connector::DnsTunnelConnector,
|
||||
proto::common::PeerFeatureFlag,
|
||||
tunnel::{
|
||||
self, IpScheme, IpVersion, TunnelConnector, TunnelError, TunnelScheme,
|
||||
self, FromUrl, IpScheme, IpVersion, TunnelConnector, TunnelError, TunnelScheme,
|
||||
ring::RingTunnelConnector, tcp::TcpTunnelConnector, udp::UdpTunnelConnector,
|
||||
},
|
||||
utils::BoxExt,
|
||||
};
|
||||
use http_connector::HttpTunnelConnector;
|
||||
use rand::seq::SliceRandom;
|
||||
|
||||
pub mod direct;
|
||||
pub mod manual;
|
||||
@@ -54,7 +56,7 @@ pub(crate) fn should_background_p2p_with_peer(
|
||||
async fn set_bind_addr_for_peer_connector(
|
||||
connector: &mut (impl TunnelConnector + ?Sized),
|
||||
is_ipv4: bool,
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
ip_collector: &Arc<IPCollector>,
|
||||
) {
|
||||
if cfg!(any(
|
||||
target_os = "android",
|
||||
@@ -67,7 +69,7 @@ async fn set_bind_addr_for_peer_connector(
|
||||
return;
|
||||
}
|
||||
|
||||
let ips = global_ctx.get_ip_collector().collect_ip_addrs().await;
|
||||
let ips = ip_collector.collect_ip_addrs().await;
|
||||
if is_ipv4 {
|
||||
let mut bind_addrs = vec![];
|
||||
for ipv4 in ips.interface_ipv4s {
|
||||
@@ -78,11 +80,7 @@ async fn set_bind_addr_for_peer_connector(
|
||||
} else {
|
||||
let mut bind_addrs = vec![];
|
||||
for ipv6 in ips.interface_ipv6s.iter().chain(ips.public_ipv6.iter()) {
|
||||
let ipv6 = std::net::Ipv6Addr::from(*ipv6);
|
||||
if global_ctx.is_ip_easytier_managed_ipv6(&ipv6) {
|
||||
continue;
|
||||
}
|
||||
let socket_addr = SocketAddrV6::new(ipv6, 0, 0, 0).into();
|
||||
let socket_addr = SocketAddrV6::new(std::net::Ipv6Addr::from(*ipv6), 0, 0, 0).into();
|
||||
bind_addrs.push(socket_addr);
|
||||
}
|
||||
connector.set_bind_addrs(bind_addrs);
|
||||
@@ -90,144 +88,6 @@ async fn set_bind_addr_for_peer_connector(
|
||||
let _ = connector;
|
||||
}
|
||||
|
||||
struct ResolvedConnectorAddr {
|
||||
addr: SocketAddr,
|
||||
ip_version: IpVersion,
|
||||
}
|
||||
|
||||
fn connector_default_port(url: &url::Url) -> Option<u16> {
|
||||
url.try_into()
|
||||
.ok()
|
||||
.and_then(|s: TunnelScheme| s.try_into().ok())
|
||||
.map(IpScheme::default_port)
|
||||
}
|
||||
|
||||
fn addr_matches_ip_version(addr: &SocketAddr, ip_version: IpVersion) -> bool {
|
||||
match ip_version {
|
||||
IpVersion::V4 => addr.is_ipv4(),
|
||||
IpVersion::V6 => addr.is_ipv6(),
|
||||
IpVersion::Both => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_effective_ip_version(addrs: &[SocketAddr], requested_ip_version: IpVersion) -> IpVersion {
|
||||
match requested_ip_version {
|
||||
IpVersion::Both if addrs.iter().all(SocketAddr::is_ipv4) => IpVersion::V4,
|
||||
IpVersion::Both if addrs.iter().all(SocketAddr::is_ipv6) => IpVersion::V6,
|
||||
_ => requested_ip_version,
|
||||
}
|
||||
}
|
||||
|
||||
async fn easytier_managed_ipv6_source_for_dst(
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
dst_addr: SocketAddrV6,
|
||||
) -> Result<Option<Ipv6Addr>, Error> {
|
||||
let socket = {
|
||||
let _g = global_ctx.net_ns.guard();
|
||||
tokio::net::UdpSocket::bind("[::]:0").await?
|
||||
};
|
||||
socket.connect(SocketAddr::V6(dst_addr)).await?;
|
||||
|
||||
let IpAddr::V6(local_ip) = socket.local_addr()?.ip() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(global_ctx
|
||||
.is_ip_easytier_managed_ipv6(&local_ip)
|
||||
.then_some(local_ip))
|
||||
}
|
||||
|
||||
async fn ipv6_connector_reject_reason(
|
||||
url: &url::Url,
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
v6_addr: SocketAddrV6,
|
||||
skip_source_validation_errors: bool,
|
||||
) -> Result<Option<String>, Error> {
|
||||
if global_ctx.is_ip_easytier_managed_ipv6(v6_addr.ip()) {
|
||||
return Ok(Some(format!(
|
||||
"{} resolves to EasyTier-managed IPv6 {}",
|
||||
url,
|
||||
v6_addr.ip()
|
||||
)));
|
||||
}
|
||||
|
||||
match easytier_managed_ipv6_source_for_dst(global_ctx, v6_addr).await {
|
||||
Ok(Some(local_ip)) => Ok(Some(format!(
|
||||
"{} would use EasyTier-managed IPv6 {} as local source for {}",
|
||||
url, local_ip, v6_addr
|
||||
))),
|
||||
Ok(None) => Ok(None),
|
||||
Err(err) if skip_source_validation_errors => Ok(Some(format!(
|
||||
"{} IPv6 candidate {} could not be validated: {}",
|
||||
url, v6_addr, err
|
||||
))),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_connector_socket_addr(
|
||||
url: &url::Url,
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
ip_version: IpVersion,
|
||||
) -> Result<ResolvedConnectorAddr, Error> {
|
||||
let addrs = socket_addrs(url, || connector_default_port(url))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
TunnelError::InvalidAddr(format!(
|
||||
"failed to resolve socket addr, url: {}, error: {}",
|
||||
url, e
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut usable_addrs = Vec::new();
|
||||
let mut rejected_ipv6_reason = None;
|
||||
let skip_source_validation_errors = ip_version == IpVersion::Both;
|
||||
for addr in addrs
|
||||
.into_iter()
|
||||
.filter(|addr| addr_matches_ip_version(addr, ip_version))
|
||||
{
|
||||
if let SocketAddr::V6(v6_addr) = addr
|
||||
&& let Some(reason) = ipv6_connector_reject_reason(
|
||||
url,
|
||||
global_ctx,
|
||||
v6_addr,
|
||||
skip_source_validation_errors,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
rejected_ipv6_reason = Some(reason);
|
||||
continue;
|
||||
}
|
||||
|
||||
usable_addrs.push(addr);
|
||||
}
|
||||
|
||||
if usable_addrs.is_empty() {
|
||||
if let Some(reason) = rejected_ipv6_reason {
|
||||
return Err(Error::InvalidUrl(format!(
|
||||
"{}, refusing overlay-backed underlay connection",
|
||||
reason
|
||||
)));
|
||||
}
|
||||
|
||||
return Err(Error::TunnelError(TunnelError::NoDnsRecordFound(
|
||||
ip_version,
|
||||
)));
|
||||
}
|
||||
|
||||
let effective_ip_version = infer_effective_ip_version(&usable_addrs, ip_version);
|
||||
|
||||
let addr = usable_addrs
|
||||
.choose(&mut rand::thread_rng())
|
||||
.copied()
|
||||
.ok_or_else(|| Error::TunnelError(TunnelError::NoDnsRecordFound(ip_version)))?;
|
||||
|
||||
Ok(ResolvedConnectorAddr {
|
||||
addr,
|
||||
ip_version: effective_ip_version,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_connector_by_url(
|
||||
url: &str,
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
@@ -238,11 +98,9 @@ pub async fn create_connector_by_url(
|
||||
let scheme = (&url)
|
||||
.try_into()
|
||||
.map_err(|_| TunnelError::InvalidProtocol(url.scheme().to_owned()))?;
|
||||
let mut effective_connector_ip_version = ip_version;
|
||||
let mut connector: Box<dyn TunnelConnector + 'static> = match scheme {
|
||||
TunnelScheme::Ip(scheme) => {
|
||||
let resolved_addr = resolve_connector_socket_addr(&url, global_ctx, ip_version).await?;
|
||||
effective_connector_ip_version = resolved_addr.ip_version;
|
||||
let dst_addr = SocketAddr::from_url(url.clone(), ip_version).await?;
|
||||
let mut connector: Box<dyn TunnelConnector> = match scheme {
|
||||
IpScheme::Tcp => TcpTunnelConnector::new(url).boxed(),
|
||||
IpScheme::Udp => UdpTunnelConnector::new(url).boxed(),
|
||||
@@ -267,12 +125,11 @@ pub async fn create_connector_by_url(
|
||||
#[cfg(feature = "faketcp")]
|
||||
IpScheme::FakeTcp => tunnel::fake_tcp::FakeTcpTunnelConnector::new(url).boxed(),
|
||||
};
|
||||
connector.set_resolved_addr(resolved_addr.addr);
|
||||
if global_ctx.config.get_flags().bind_device {
|
||||
set_bind_addr_for_peer_connector(
|
||||
&mut connector,
|
||||
resolved_addr.addr.is_ipv4(),
|
||||
global_ctx,
|
||||
dst_addr.is_ipv4(),
|
||||
&global_ctx.get_ip_collector(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -294,38 +151,16 @@ pub async fn create_connector_by_url(
|
||||
DnsTunnelConnector::new(url, global_ctx.clone()).boxed()
|
||||
}
|
||||
};
|
||||
connector.set_ip_version(effective_connector_ip_version);
|
||||
connector.set_ip_version(ip_version);
|
||||
|
||||
Ok(connector)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeSet;
|
||||
use crate::proto::common::PeerFeatureFlag;
|
||||
|
||||
use crate::{
|
||||
common::global_ctx::tests::get_mock_global_ctx, proto::common::PeerFeatureFlag,
|
||||
tunnel::IpVersion,
|
||||
};
|
||||
|
||||
use super::{
|
||||
create_connector_by_url, should_background_p2p_with_peer, should_try_p2p_with_peer,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn connector_rejects_easytier_managed_ipv6_destination() {
|
||||
let global_ctx = get_mock_global_ctx();
|
||||
let public_route: cidr::Ipv6Inet = "2001:db8::2/128".parse().unwrap();
|
||||
global_ctx.set_public_ipv6_routes(BTreeSet::from([public_route]));
|
||||
|
||||
let ret =
|
||||
create_connector_by_url("tcp://[2001:db8::2]:11010", &global_ctx, IpVersion::V6).await;
|
||||
|
||||
assert!(matches!(
|
||||
ret,
|
||||
Err(crate::common::error::Error::InvalidUrl(_))
|
||||
));
|
||||
}
|
||||
use super::{should_background_p2p_with_peer, should_try_p2p_with_peer};
|
||||
|
||||
#[test]
|
||||
fn lazy_background_p2p_requires_need_p2p() {
|
||||
|
||||
@@ -6,7 +6,6 @@ use std::{
|
||||
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use guarden::defer;
|
||||
use rand::seq::SliceRandom as _;
|
||||
use tokio::{net::UdpSocket, sync::Mutex, task::JoinSet};
|
||||
use tracing::{Instrument, Level, instrument};
|
||||
@@ -16,6 +15,7 @@ use crate::{
|
||||
common::{
|
||||
PeerId, error::Error, global_ctx::ArcGlobalCtx, join_joinset_background, netns::NetNS, upnp,
|
||||
},
|
||||
defer,
|
||||
peers::peer_manager::PeerManager,
|
||||
proto::common::NatType,
|
||||
tunnel::{
|
||||
@@ -719,31 +719,25 @@ async fn check_udp_socket_local_addr(
|
||||
) -> Result<(), Error> {
|
||||
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||
socket.connect(remote_mapped_addr).await?;
|
||||
if let Ok(local_addr) = socket.local_addr()
|
||||
&& let Some(err) = easytier_managed_local_addr_error(&global_ctx, local_addr)
|
||||
{
|
||||
return Err(anyhow::anyhow!(err).into());
|
||||
if let Ok(local_addr) = socket.local_addr() {
|
||||
// local_addr should not be equal to virtual ipv4 or virtual ipv6
|
||||
match local_addr.ip() {
|
||||
IpAddr::V4(ip) => {
|
||||
if global_ctx.get_ipv4().map(|ip| ip.address()) == Some(ip) {
|
||||
return Err(anyhow::anyhow!("local address is virtual ipv4").into());
|
||||
}
|
||||
}
|
||||
IpAddr::V6(ip) => {
|
||||
if global_ctx.get_ipv6().map(|ip| ip.address()) == Some(ip) {
|
||||
return Err(anyhow::anyhow!("local address is virtual ipv6").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn easytier_managed_local_addr_error(
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
local_addr: SocketAddr,
|
||||
) -> Option<&'static str> {
|
||||
// local_addr should not be equal to an EasyTier-managed virtual/public address.
|
||||
match local_addr.ip() {
|
||||
IpAddr::V4(ip) if global_ctx.get_ipv4().map(|ip| ip.address()) == Some(ip) => {
|
||||
Some("local address is virtual ipv4")
|
||||
}
|
||||
IpAddr::V6(ip) if global_ctx.is_ip_easytier_managed_ipv6(&ip) => {
|
||||
Some("local address is easytier-managed ipv6")
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn try_connect_with_socket(
|
||||
global_ctx: ArcGlobalCtx,
|
||||
socket: Arc<UdpSocket>,
|
||||
@@ -769,29 +763,11 @@ pub(crate) async fn try_connect_with_socket(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::BTreeSet, net::SocketAddr};
|
||||
|
||||
use crate::common::global_ctx::tests::get_mock_global_ctx;
|
||||
|
||||
use super::{
|
||||
MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS, easytier_managed_local_addr_error,
|
||||
should_create_public_listener, should_retry_public_listener_selection,
|
||||
MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS, should_create_public_listener,
|
||||
should_retry_public_listener_selection,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_addr_check_rejects_easytier_public_ipv6_route() {
|
||||
let global_ctx = get_mock_global_ctx();
|
||||
let public_route: cidr::Ipv6Inet = "2001:db8::4/128".parse().unwrap();
|
||||
global_ctx.set_public_ipv6_routes(BTreeSet::from([public_route]));
|
||||
|
||||
let local_addr: SocketAddr = "[2001:db8::4]:1234".parse().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
easytier_managed_local_addr_error(&global_ctx, local_addr),
|
||||
Some("local address is easytier-managed ipv6")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listener_selection_prefers_reuse_before_cap() {
|
||||
assert!(!should_create_public_listener(1, true, true, false, false));
|
||||
|
||||
@@ -9,7 +9,6 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use guarden::defer;
|
||||
use rand::{Rng, seq::SliceRandom};
|
||||
use tokio::{net::UdpSocket, sync::RwLock};
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
@@ -23,6 +22,7 @@ use crate::{
|
||||
},
|
||||
handle_rpc_result,
|
||||
},
|
||||
defer,
|
||||
peers::peer_manager::PeerManager,
|
||||
proto::{
|
||||
peer_rpc::{
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::{
|
||||
constants::EASYTIER_VERSION,
|
||||
log,
|
||||
},
|
||||
defer,
|
||||
instance_manager::NetworkInstanceManager,
|
||||
launcher::add_proxy_network_to_config,
|
||||
proto::common::{CompressionAlgoPb, SecureModeConfig},
|
||||
@@ -22,7 +23,6 @@ use crate::{
|
||||
use anyhow::Context;
|
||||
use cidr::IpCidr;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use guarden::defer;
|
||||
use rust_i18n::t;
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
|
||||
@@ -7,7 +7,6 @@ use std::{
|
||||
use anyhow::Context;
|
||||
use bytes::Bytes;
|
||||
use dashmap::DashMap;
|
||||
use guarden::defer;
|
||||
use kcp_sys::{
|
||||
endpoint::{ConnId, KcpEndpoint, KcpPacketReceiver},
|
||||
ffi_safe::KcpConfig,
|
||||
@@ -360,7 +359,7 @@ impl KcpProxyDst {
|
||||
transport_type: TcpProxyEntryTransportType::Kcp.into(),
|
||||
},
|
||||
);
|
||||
defer! {
|
||||
crate::defer! {
|
||||
proxy_entries.remove(&conn_id);
|
||||
if proxy_entries.capacity() - proxy_entries.len() > 16 {
|
||||
proxy_entries.shrink_to_fit();
|
||||
|
||||
@@ -24,7 +24,6 @@ use bytes::{BufMut, Bytes, BytesMut};
|
||||
use dashmap::DashMap;
|
||||
use derivative::Derivative;
|
||||
use derive_more::{Constructor, Deref, DerefMut, From, Into};
|
||||
use guarden::defer;
|
||||
use prost::Message;
|
||||
use quinn::udp::{EcnCodepoint, RecvMeta, Transmit};
|
||||
use quinn::{
|
||||
@@ -663,7 +662,7 @@ impl QuicStreamReceiver {
|
||||
transport_type: TcpProxyEntryTransportType::Quic.into(),
|
||||
},
|
||||
);
|
||||
defer! {
|
||||
crate::defer! {
|
||||
proxy_entries.remove(&handle);
|
||||
if proxy_entries.capacity() - proxy_entries.len() > 16 {
|
||||
proxy_entries.shrink_to_fit();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// translated from tailscale #32ce1bdb48078ec4cedaeeb5b1b2ff9c0ef61a49
|
||||
|
||||
use crate::defer;
|
||||
use anyhow::{Context, Result};
|
||||
use dbus::blocking::stdintf::org_freedesktop_dbus::Properties as _;
|
||||
use std::fs;
|
||||
@@ -166,7 +167,6 @@ fn new_os_configurator(_interface_name: String) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
use guarden::defer;
|
||||
use std::io::{self, BufRead, Cursor};
|
||||
|
||||
/// 返回 `resolv.conf` 内容的拥有者("systemd-resolved"、"NetworkManager"、"resolvconf" 或空字符串)
|
||||
|
||||
@@ -66,8 +66,7 @@ use super::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner};
|
||||
use super::listeners::ListenerManager;
|
||||
use super::public_ipv6_provider::{
|
||||
reconcile_public_ipv6_provider_runtime, run_public_ipv6_provider_reconcile_task,
|
||||
should_run_public_ipv6_provider_reconcile, validate_public_ipv6_config,
|
||||
validate_public_ipv6_config_values,
|
||||
validate_public_ipv6_config,
|
||||
};
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
@@ -257,64 +256,11 @@ pub struct InstanceConfigPatcher {
|
||||
}
|
||||
|
||||
impl InstanceConfigPatcher {
|
||||
fn parse_ipv6_public_addr_prefix_patch(
|
||||
prefix: Option<&str>,
|
||||
) -> Result<Option<Option<cidr::Ipv6Cidr>>, anyhow::Error> {
|
||||
let Some(prefix) = prefix else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let prefix = prefix.trim();
|
||||
if prefix.is_empty() {
|
||||
return Ok(Some(None));
|
||||
}
|
||||
|
||||
let parsed = prefix
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse ipv6 public address prefix: {prefix}"))?;
|
||||
Ok(Some(Some(parsed)))
|
||||
}
|
||||
|
||||
fn effective_ipv6_for_public_ipv6_validation(
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
patch: &crate::proto::api::config::InstanceConfigPatch,
|
||||
_auto_enabled: bool,
|
||||
) -> Option<cidr::Ipv6Inet> {
|
||||
if let Some(ipv6) = patch.ipv6 {
|
||||
return Some(ipv6.into());
|
||||
}
|
||||
|
||||
global_ctx.get_ipv6()
|
||||
}
|
||||
|
||||
fn validate_public_ipv6_patch(
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
patch: &crate::proto::api::config::InstanceConfigPatch,
|
||||
) -> Result<Option<Option<cidr::Ipv6Cidr>>, anyhow::Error> {
|
||||
let parsed_prefix =
|
||||
Self::parse_ipv6_public_addr_prefix_patch(patch.ipv6_public_addr_prefix.as_deref())?;
|
||||
|
||||
let auto_enabled = patch
|
||||
.ipv6_public_addr_auto
|
||||
.unwrap_or(global_ctx.config.get_ipv6_public_addr_auto());
|
||||
let provider_enabled = patch
|
||||
.ipv6_public_addr_provider
|
||||
.unwrap_or(global_ctx.config.get_ipv6_public_addr_provider());
|
||||
let prefix =
|
||||
parsed_prefix.unwrap_or_else(|| global_ctx.config.get_ipv6_public_addr_prefix());
|
||||
let ipv6 = Self::effective_ipv6_for_public_ipv6_validation(global_ctx, patch, auto_enabled);
|
||||
|
||||
validate_public_ipv6_config_values(ipv6, provider_enabled, auto_enabled, prefix)?;
|
||||
Ok(parsed_prefix)
|
||||
}
|
||||
|
||||
pub async fn apply_patch(
|
||||
&self,
|
||||
patch: crate::proto::api::config::InstanceConfigPatch,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let patch_for_event = patch.clone();
|
||||
let global_ctx = weak_upgrade(&self.global_ctx)?;
|
||||
let parsed_ipv6_public_addr_prefix = Self::validate_public_ipv6_patch(&global_ctx, &patch)?;
|
||||
|
||||
self.patch_port_forwards(patch.port_forwards).await?;
|
||||
self.patch_acl(patch.acl).await?;
|
||||
@@ -324,7 +270,9 @@ impl InstanceConfigPatcher {
|
||||
self.patch_mapped_listeners(patch.mapped_listeners).await?;
|
||||
self.patch_connector(patch.connectors).await?;
|
||||
|
||||
let provider_reconcile_was_running = should_run_public_ipv6_provider_reconcile(&global_ctx);
|
||||
let global_ctx = weak_upgrade(&self.global_ctx)?;
|
||||
let provider_reconcile_was_running = global_ctx.config.get_ipv6_public_addr_provider()
|
||||
&& global_ctx.config.get_ipv6_public_addr_prefix().is_none();
|
||||
let mut provider_config_changed = false;
|
||||
if let Some(hostname) = patch.hostname {
|
||||
global_ctx.set_hostname(hostname.clone());
|
||||
@@ -347,8 +295,16 @@ impl InstanceConfigPatcher {
|
||||
if let Some(enabled) = patch.ipv6_public_addr_auto {
|
||||
global_ctx.config.set_ipv6_public_addr_auto(enabled);
|
||||
}
|
||||
if let Some(prefix) = parsed_ipv6_public_addr_prefix {
|
||||
global_ctx.config.set_ipv6_public_addr_prefix(prefix);
|
||||
if let Some(prefix) = patch.ipv6_public_addr_prefix {
|
||||
let prefix = prefix.trim();
|
||||
let parsed = if prefix.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(prefix.parse().with_context(|| {
|
||||
format!("failed to parse ipv6 public address prefix: {prefix}")
|
||||
})?)
|
||||
};
|
||||
global_ctx.config.set_ipv6_public_addr_prefix(parsed);
|
||||
provider_config_changed = true;
|
||||
}
|
||||
|
||||
@@ -357,8 +313,8 @@ impl InstanceConfigPatcher {
|
||||
if provider_config_changed {
|
||||
reconcile_public_ipv6_provider_runtime(&global_ctx).await;
|
||||
|
||||
let provider_reconcile_should_run =
|
||||
should_run_public_ipv6_provider_reconcile(&global_ctx);
|
||||
let provider_reconcile_should_run = global_ctx.config.get_ipv6_public_addr_provider()
|
||||
&& global_ctx.config.get_ipv6_public_addr_prefix().is_none();
|
||||
if !provider_reconcile_was_running && provider_reconcile_should_run {
|
||||
run_public_ipv6_provider_reconcile_task(&global_ctx);
|
||||
}
|
||||
@@ -1631,9 +1587,7 @@ impl Drop for Instance {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
common::global_ctx::tests::get_mock_global_ctx,
|
||||
instance::instance::{InstanceConfigPatcher, InstanceRpcServerHook},
|
||||
proto::{api::config::InstanceConfigPatch, rpc_impl::standalone::RpcServerHook},
|
||||
instance::instance::InstanceRpcServerHook, proto::rpc_impl::standalone::RpcServerHook,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1754,50 +1708,4 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn validate_public_ipv6_patch_rejects_non_global_prefix() {
|
||||
let global_ctx = get_mock_global_ctx();
|
||||
let patch = InstanceConfigPatch {
|
||||
ipv6_public_addr_provider: Some(true),
|
||||
ipv6_public_addr_prefix: Some("fd00::/64".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let err =
|
||||
InstanceConfigPatcher::validate_public_ipv6_patch(&global_ctx, &patch).unwrap_err();
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("not a valid global unicast IPv6 prefix")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn validate_public_ipv6_patch_allows_enabling_auto_with_manual_ipv6() {
|
||||
let global_ctx = get_mock_global_ctx();
|
||||
global_ctx.set_ipv6(Some("fd00::1/64".parse().unwrap()));
|
||||
|
||||
let patch = InstanceConfigPatch {
|
||||
ipv6_public_addr_auto: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert!(InstanceConfigPatcher::validate_public_ipv6_patch(&global_ctx, &patch).is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn validate_public_ipv6_patch_ignores_runtime_auto_ipv6_cache() {
|
||||
let global_ctx = get_mock_global_ctx();
|
||||
global_ctx.config.set_ipv6_public_addr_auto(true);
|
||||
global_ctx.set_ipv6(Some("2001:db8::10/64".parse().unwrap()));
|
||||
|
||||
let patch = InstanceConfigPatch {
|
||||
ipv6_public_addr_provider: Some(true),
|
||||
ipv6_public_addr_prefix: Some("2001:db8:100::/64".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert!(InstanceConfigPatcher::validate_public_ipv6_patch(&global_ctx, &patch).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use anyhow::Context;
|
||||
use cidr::{Ipv6Cidr, Ipv6Inet};
|
||||
use cidr::Ipv6Cidr;
|
||||
#[cfg(target_os = "linux")]
|
||||
use netlink_packet_route::route::{RouteAddress, RouteAttribute, RouteMessage, RouteType};
|
||||
|
||||
@@ -44,12 +44,6 @@ fn should_run_public_ipv6_provider_reconcile_task(
|
||||
config.provider_enabled && config.configured_prefix.is_none()
|
||||
}
|
||||
|
||||
pub(super) fn should_run_public_ipv6_provider_reconcile(global_ctx: &ArcGlobalCtx) -> bool {
|
||||
should_run_public_ipv6_provider_reconcile_task(read_public_ipv6_provider_config_snapshot(
|
||||
global_ctx,
|
||||
))
|
||||
}
|
||||
|
||||
fn is_global_routable_public_ipv6_prefix(prefix: Ipv6Cidr) -> bool {
|
||||
let addr = prefix.first_address();
|
||||
!addr.is_loopback()
|
||||
@@ -59,19 +53,21 @@ fn is_global_routable_public_ipv6_prefix(prefix: Ipv6Cidr) -> bool {
|
||||
&& !addr.is_unspecified()
|
||||
}
|
||||
|
||||
pub(super) fn validate_public_ipv6_config_values(
|
||||
_ipv6: Option<Ipv6Inet>,
|
||||
provider_enabled: bool,
|
||||
_auto_enabled: bool,
|
||||
prefix: Option<Ipv6Cidr>,
|
||||
) -> Result<(), Error> {
|
||||
if !provider_enabled {
|
||||
pub(super) fn validate_public_ipv6_config(global_ctx: &ArcGlobalCtx) -> Result<(), Error> {
|
||||
if global_ctx.config.get_ipv6_public_addr_auto() && global_ctx.get_ipv6().is_some() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"cannot use --ipv6-public-addr-auto together with a manually set --ipv6; pick one or the other"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if !global_ctx.config.get_ipv6_public_addr_provider() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
ensure_public_ipv6_provider_supported()?;
|
||||
|
||||
if let Some(prefix) = prefix
|
||||
if let Some(prefix) = global_ctx.config.get_ipv6_public_addr_prefix()
|
||||
&& !is_global_routable_public_ipv6_prefix(prefix)
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -84,15 +80,6 @@ pub(super) fn validate_public_ipv6_config_values(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn validate_public_ipv6_config(global_ctx: &ArcGlobalCtx) -> Result<(), Error> {
|
||||
validate_public_ipv6_config_values(
|
||||
global_ctx.get_ipv6(),
|
||||
global_ctx.config.get_ipv6_public_addr_provider(),
|
||||
global_ctx.config.get_ipv6_public_addr_auto(),
|
||||
global_ctx.config.get_ipv6_public_addr_prefix(),
|
||||
)
|
||||
}
|
||||
|
||||
fn ensure_public_ipv6_provider_supported() -> Result<(), Error> {
|
||||
if cfg!(target_os = "linux") {
|
||||
return Ok(());
|
||||
|
||||
@@ -920,7 +920,7 @@ impl NicCtx {
|
||||
}
|
||||
let src_ipv6 = ipv6.get_source();
|
||||
let dst_ipv6 = ipv6.get_destination();
|
||||
let is_local_src = mgr.get_global_ctx().is_ip_local_ipv6(&src_ipv6);
|
||||
let my_ipv6 = mgr.get_global_ctx().get_ipv6().map(|x| x.address());
|
||||
tracing::trace!(
|
||||
?ret,
|
||||
?src_ipv6,
|
||||
@@ -928,14 +928,14 @@ impl NicCtx {
|
||||
"[USER_PACKET] recv new packet from tun device and forward to peers."
|
||||
);
|
||||
|
||||
if src_ipv6.is_unicast_link_local() && !is_local_src {
|
||||
if src_ipv6.is_unicast_link_local() && Some(src_ipv6) != my_ipv6 {
|
||||
// do not route link local packet to other nodes unless the address is assigned by user
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: use zero-copy
|
||||
let send_ret = mgr
|
||||
.send_msg_by_ip(ret, IpAddr::V6(dst_ipv6), is_local_src)
|
||||
.send_msg_by_ip(ret, IpAddr::V6(dst_ipv6), Some(src_ipv6) == my_ipv6)
|
||||
.await;
|
||||
if send_ret.is_err() {
|
||||
tracing::trace!(?send_ret, "[USER_PACKET] send_msg failed")
|
||||
@@ -1356,9 +1356,9 @@ impl NicCtx {
|
||||
|
||||
self.run_proxy_cidrs_route_updater().await?;
|
||||
self.run_public_ipv6_route_updater().await?;
|
||||
// Keep the updater running so runtime config patches can enable auto mode
|
||||
// without recreating the NIC.
|
||||
self.run_public_ipv6_addr_updater().await?;
|
||||
if self.global_ctx.config.get_ipv6_public_addr_auto() {
|
||||
self.run_public_ipv6_addr_updater().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -94,8 +94,6 @@ impl AclFilter {
|
||||
/// Preserves connection tracking and rate limiting state across reloads
|
||||
/// Now lock-free and doesn't require &mut self!
|
||||
pub fn reload_rules(&self, acl_config: Option<&Acl>) {
|
||||
self.outbound_allow_records.clear();
|
||||
|
||||
let Some(acl_config) = acl_config else {
|
||||
self.acl_enabled.store(false, Ordering::Relaxed);
|
||||
return;
|
||||
@@ -294,33 +292,13 @@ impl AclFilter {
|
||||
processor.increment_stat(AclStatKey::PacketsTotal);
|
||||
}
|
||||
|
||||
fn classify_chain_type(
|
||||
is_in: bool,
|
||||
packet_info: &PacketInfo,
|
||||
my_ipv4: Option<Ipv4Addr>,
|
||||
is_local_ipv6: impl Fn(Ipv6Addr) -> bool,
|
||||
) -> ChainType {
|
||||
if !is_in {
|
||||
return ChainType::Outbound;
|
||||
}
|
||||
|
||||
let is_local_dst = packet_info.dst_ip == my_ipv4.unwrap_or(Ipv4Addr::UNSPECIFIED)
|
||||
|| matches!(packet_info.dst_ip, IpAddr::V6(dst) if is_local_ipv6(dst));
|
||||
|
||||
if is_local_dst {
|
||||
ChainType::Inbound
|
||||
} else {
|
||||
ChainType::Forward
|
||||
}
|
||||
}
|
||||
|
||||
/// Common ACL processing logic
|
||||
pub fn process_packet_with_acl(
|
||||
&self,
|
||||
packet: &ZCPacket,
|
||||
is_in: bool,
|
||||
my_ipv4: Option<Ipv4Addr>,
|
||||
is_local_ipv6: impl Fn(Ipv6Addr) -> bool,
|
||||
my_ipv6: Option<Ipv6Addr>,
|
||||
route: &(dyn super::route_trait::Route + Send + Sync + 'static),
|
||||
) -> bool {
|
||||
if !self.acl_enabled.load(Ordering::Relaxed) {
|
||||
@@ -345,7 +323,17 @@ impl AclFilter {
|
||||
}
|
||||
};
|
||||
|
||||
let chain_type = Self::classify_chain_type(is_in, &packet_info, my_ipv4, is_local_ipv6);
|
||||
let chain_type = if is_in {
|
||||
if packet_info.dst_ip == my_ipv4.unwrap_or(Ipv4Addr::UNSPECIFIED)
|
||||
|| packet_info.dst_ip == my_ipv6.unwrap_or(Ipv6Addr::UNSPECIFIED)
|
||||
{
|
||||
ChainType::Inbound
|
||||
} else {
|
||||
ChainType::Forward
|
||||
}
|
||||
} else {
|
||||
ChainType::Outbound
|
||||
};
|
||||
|
||||
// Get current processor atomically
|
||||
let processor = self.get_processor();
|
||||
@@ -396,92 +384,3 @@ impl AclFilter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
sync::Arc,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
common::acl_processor::PacketInfo,
|
||||
proto::acl::{Acl, ChainType, Protocol},
|
||||
};
|
||||
|
||||
use super::{AclFilter, OutboundAllowRecord};
|
||||
|
||||
fn packet_info(dst_ip: IpAddr) -> PacketInfo {
|
||||
PacketInfo {
|
||||
src_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
|
||||
dst_ip,
|
||||
src_port: Some(1234),
|
||||
dst_port: Some(80),
|
||||
protocol: Protocol::Tcp,
|
||||
packet_size: 64,
|
||||
src_groups: Arc::new(Vec::new()),
|
||||
dst_groups: Arc::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_chain_type_treats_public_ipv6_lease_as_inbound() {
|
||||
let leased_ipv6 = Ipv6Addr::new(0x2001, 0xdb8, 0x100, 0, 0, 0, 0, 0x123);
|
||||
let packet_info = packet_info(IpAddr::V6(leased_ipv6));
|
||||
|
||||
let chain =
|
||||
AclFilter::classify_chain_type(true, &packet_info, None, |ip| ip == leased_ipv6);
|
||||
|
||||
assert_eq!(chain, ChainType::Inbound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_chain_type_keeps_non_local_ipv6_as_forward() {
|
||||
let leased_ipv6 = Ipv6Addr::new(0x2001, 0xdb8, 0x100, 0, 0, 0, 0, 0x123);
|
||||
let packet_info = packet_info(IpAddr::V6(Ipv6Addr::new(
|
||||
0x2001, 0xdb8, 0xffff, 2, 0, 0, 0, 0x100,
|
||||
)));
|
||||
|
||||
let chain =
|
||||
AclFilter::classify_chain_type(true, &packet_info, None, |ip| ip == leased_ipv6);
|
||||
|
||||
assert_eq!(chain, ChainType::Forward);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reload_rules_clears_outbound_allow_records() {
|
||||
let filter = AclFilter::new();
|
||||
filter.outbound_allow_records.insert(
|
||||
OutboundAllowRecord {
|
||||
src_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
|
||||
dst_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)),
|
||||
src_port: Some(1234),
|
||||
dst_port: Some(80),
|
||||
protocol: Protocol::Tcp,
|
||||
},
|
||||
Instant::now(),
|
||||
);
|
||||
assert_eq!(filter.outbound_allow_records.len(), 1);
|
||||
|
||||
filter.reload_rules(Some(&Acl::default()));
|
||||
|
||||
assert_eq!(filter.outbound_allow_records.len(), 0);
|
||||
|
||||
filter.outbound_allow_records.insert(
|
||||
OutboundAllowRecord {
|
||||
src_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)),
|
||||
dst_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
|
||||
src_port: Some(4321),
|
||||
dst_port: Some(443),
|
||||
protocol: Protocol::Tcp,
|
||||
},
|
||||
Instant::now(),
|
||||
);
|
||||
assert_eq!(filter.outbound_allow_records.len(), 1);
|
||||
|
||||
filter.reload_rules(None);
|
||||
|
||||
assert_eq!(filter.outbound_allow_records.len(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ use std::{
|
||||
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use guarden::guard;
|
||||
use hmac::Mac;
|
||||
use prost::Message;
|
||||
|
||||
@@ -41,6 +40,7 @@ use crate::{
|
||||
error::Error,
|
||||
global_ctx::ArcGlobalCtx,
|
||||
},
|
||||
guard,
|
||||
peers::peer_session::{PeerSessionStore, SessionKey, UpsertResponderSessionReturn},
|
||||
proto::{
|
||||
api::instance::{PeerConnInfo, PeerConnStats},
|
||||
|
||||
@@ -1062,7 +1062,7 @@ impl PeerManager {
|
||||
&ret,
|
||||
true,
|
||||
global_ctx.get_ipv4().map(|x| x.address()),
|
||||
|dst| global_ctx.is_ip_local_ipv6(&dst),
|
||||
global_ctx.get_ipv6().map(|x| x.address()),
|
||||
&route,
|
||||
) {
|
||||
continue;
|
||||
@@ -1342,7 +1342,7 @@ impl PeerManager {
|
||||
data,
|
||||
false,
|
||||
None,
|
||||
|_| false,
|
||||
None,
|
||||
&self.get_route(),
|
||||
) {
|
||||
return false;
|
||||
@@ -1544,10 +1544,6 @@ impl PeerManager {
|
||||
dst_peers.extend(self.peers.list_routes().await.iter().map(|x| *x.key()));
|
||||
} else if let Some(peer_id) = self.peers.get_peer_id_by_ipv6(ipv6_addr).await {
|
||||
dst_peers.push(peer_id);
|
||||
} else if !ipv6_addr.is_unicast_link_local()
|
||||
&& let Some(peer_id) = self.get_route().get_public_ipv6_gateway_peer_id().await
|
||||
{
|
||||
dst_peers.push(peer_id);
|
||||
} else if !ipv6_addr.is_unicast_link_local() {
|
||||
// NOTE: never route link local address to exit node.
|
||||
for exit_node in self.exit_nodes.read().await.iter() {
|
||||
@@ -1678,7 +1674,7 @@ impl PeerManager {
|
||||
&& !self.global_ctx.is_ip_local_virtual_ip(&ip_addr)
|
||||
{
|
||||
// Keep the loop-prevention flags for proxy-induced self-delivery where
|
||||
// the destination is not this node's own EasyTier-managed IP.
|
||||
// the destination is not this node's own virtual IP.
|
||||
hdr.set_not_send_to_tun(true);
|
||||
hdr.set_no_proxy(true);
|
||||
}
|
||||
|
||||
@@ -3954,10 +3954,6 @@ impl Route for PeerRoute {
|
||||
self.public_ipv6_service.my_addr()
|
||||
}
|
||||
|
||||
async fn get_public_ipv6_gateway_peer_id(&self) -> Option<PeerId> {
|
||||
self.public_ipv6_service.provider_peer_id_for_client()
|
||||
}
|
||||
|
||||
async fn get_local_public_ipv6_info(
|
||||
&self,
|
||||
) -> crate::proto::api::instance::ListPublicIpv6InfoResponse {
|
||||
|
||||
@@ -12,22 +12,6 @@ use crate::{
|
||||
tunnel::udp,
|
||||
};
|
||||
|
||||
fn remove_easytier_managed_ipv6s(ret: &mut GetIpListResponse, global_ctx: &ArcGlobalCtx) {
|
||||
ret.interface_ipv6s.retain(|ip| {
|
||||
let ip = std::net::Ipv6Addr::from(*ip);
|
||||
!global_ctx.is_ip_easytier_managed_ipv6(&ip)
|
||||
});
|
||||
|
||||
if ret
|
||||
.public_ipv6
|
||||
.as_ref()
|
||||
.map(|ip| std::net::Ipv6Addr::from(*ip))
|
||||
.is_some_and(|ip| global_ctx.is_ip_easytier_managed_ipv6(&ip))
|
||||
{
|
||||
ret.public_ipv6 = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DirectConnectorManagerRpcServer {
|
||||
// TODO: this only cache for one src peer, should make it global
|
||||
@@ -52,7 +36,11 @@ impl DirectConnectorRpc for DirectConnectorManagerRpcServer {
|
||||
.chain(self.global_ctx.get_running_listeners())
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
remove_easytier_managed_ipv6s(&mut ret, &self.global_ctx);
|
||||
// remove et ipv6 from the interface ipv6 list
|
||||
if let Some(et_ipv6) = self.global_ctx.get_ipv6() {
|
||||
let et_ipv6: crate::proto::common::Ipv6Addr = et_ipv6.address().into();
|
||||
ret.interface_ipv6s.retain(|x| *x != et_ipv6);
|
||||
}
|
||||
tracing::trace!(
|
||||
"get_ip_list: public_ipv4: {:?}, public_ipv6: {:?}, listeners: {:?}",
|
||||
ret.public_ipv4,
|
||||
@@ -96,41 +84,3 @@ impl DirectConnectorManagerRpcServer {
|
||||
Self { global_ctx }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::{
|
||||
common::global_ctx::tests::get_mock_global_ctx,
|
||||
peers::peer_rpc_service::remove_easytier_managed_ipv6s, proto::peer_rpc::GetIpListResponse,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_ip_list_sanitizer_removes_managed_ipv6_from_all_sources() {
|
||||
let global_ctx = get_mock_global_ctx();
|
||||
let virtual_ipv6 = "fd00::1/64".parse().unwrap();
|
||||
let public_ipv6 = "2001:db8::2/128".parse().unwrap();
|
||||
let physical_ipv6: std::net::Ipv6Addr = "2001:db8::3".parse().unwrap();
|
||||
let routed_ipv6: cidr::Ipv6Inet = "2001:db8::4/128".parse().unwrap();
|
||||
global_ctx.set_ipv6(Some(virtual_ipv6));
|
||||
global_ctx.set_public_ipv6_lease(Some(public_ipv6));
|
||||
global_ctx.set_public_ipv6_routes(BTreeSet::from([routed_ipv6]));
|
||||
|
||||
let mut ip_list = GetIpListResponse {
|
||||
public_ipv6: Some(public_ipv6.address().into()),
|
||||
interface_ipv6s: vec![
|
||||
virtual_ipv6.address().into(),
|
||||
public_ipv6.address().into(),
|
||||
routed_ipv6.address().into(),
|
||||
physical_ipv6.into(),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
remove_easytier_managed_ipv6s(&mut ip_list, &global_ctx);
|
||||
|
||||
assert_eq!(ip_list.public_ipv6, None);
|
||||
assert_eq!(ip_list.interface_ipv6s, vec![physical_ipv6.into()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +226,9 @@ impl PublicIpv6Service {
|
||||
if *cached_my_addr != my_addr {
|
||||
let old = *cached_my_addr;
|
||||
*cached_my_addr = my_addr;
|
||||
self.global_ctx.set_public_ipv6_lease(my_addr);
|
||||
if self.global_ctx.config.get_ipv6_public_addr_auto() {
|
||||
self.global_ctx.set_ipv6(my_addr);
|
||||
}
|
||||
self.global_ctx
|
||||
.issue_event(GlobalCtxEvent::PublicIpv6Changed(old, my_addr));
|
||||
}
|
||||
@@ -243,8 +245,6 @@ impl PublicIpv6Service {
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
*cached_routes = routes;
|
||||
self.global_ctx
|
||||
.set_public_ipv6_routes(cached_routes.clone());
|
||||
self.global_ctx
|
||||
.issue_event(GlobalCtxEvent::PublicIpv6RoutesUpdated(added, removed));
|
||||
}
|
||||
@@ -642,11 +642,6 @@ impl PublicIpv6Service {
|
||||
*self.my_addr_cache.lock().unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn provider_peer_id_for_client(&self) -> Option<PeerId> {
|
||||
self.current_client_state()
|
||||
.map(|state| state.provider.peer_id)
|
||||
}
|
||||
|
||||
pub(crate) fn local_provider_state(
|
||||
&self,
|
||||
) -> Option<(PublicIpv6Provider, Vec<PublicIpv6ProviderLease>)> {
|
||||
@@ -849,48 +844,12 @@ fn allocate_public_ipv6_leases(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::net::Ipv6Addr;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use cidr::{Ipv6Cidr, Ipv6Inet};
|
||||
use cidr::Ipv6Cidr;
|
||||
|
||||
use crate::{
|
||||
common::{PeerId, global_ctx::tests::get_mock_global_ctx},
|
||||
peers::peer_rpc::PeerRpcManager,
|
||||
};
|
||||
|
||||
use super::{
|
||||
PublicIpv6PeerRouteInfo, PublicIpv6RouteControl, PublicIpv6Service, PublicIpv6SyncTrigger,
|
||||
allocate_public_ipv6_leases,
|
||||
};
|
||||
|
||||
struct TestRouteControl {
|
||||
my_peer_id: PeerId,
|
||||
peers: Mutex<Vec<PublicIpv6PeerRouteInfo>>,
|
||||
}
|
||||
|
||||
impl PublicIpv6RouteControl for TestRouteControl {
|
||||
fn my_peer_id(&self) -> PeerId {
|
||||
self.my_peer_id
|
||||
}
|
||||
|
||||
fn peer_route_snapshot(&self) -> Vec<PublicIpv6PeerRouteInfo> {
|
||||
self.peers.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
fn publish_self_public_ipv6_lease(&self, _lease: Option<Ipv6Inet>) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
struct TestSyncTrigger;
|
||||
|
||||
impl PublicIpv6SyncTrigger for TestSyncTrigger {
|
||||
fn sync_now(&self, _reason: &str) {}
|
||||
}
|
||||
use super::{PublicIpv6PeerRouteInfo, PublicIpv6Service, allocate_public_ipv6_leases};
|
||||
|
||||
#[test]
|
||||
fn public_ipv6_lease_allocator_keeps_stable_addresses() {
|
||||
@@ -980,64 +939,4 @@ mod tests {
|
||||
|
||||
assert!(leases.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconcile_runtime_clears_public_ipv6_lease_when_auto_is_disabled() {
|
||||
let global_ctx = get_mock_global_ctx();
|
||||
global_ctx.config.set_ipv6_public_addr_auto(false);
|
||||
|
||||
let virtual_addr = "fd00::1/64".parse().unwrap();
|
||||
let stale_addr = "2001:db8::123/64".parse().unwrap();
|
||||
global_ctx.set_ipv6(Some(virtual_addr));
|
||||
global_ctx.set_public_ipv6_lease(Some(stale_addr));
|
||||
|
||||
let service = Arc::new(PublicIpv6Service::new(
|
||||
global_ctx.clone(),
|
||||
std::sync::Weak::<PeerRpcManager>::new(),
|
||||
Arc::new(TestRouteControl {
|
||||
my_peer_id: 1,
|
||||
peers: Mutex::new(Vec::new()),
|
||||
}),
|
||||
Arc::new(TestSyncTrigger),
|
||||
));
|
||||
*service.my_addr_cache.lock().unwrap() = Some(stale_addr);
|
||||
|
||||
service.reconcile_runtime_from_snapshot(&[]);
|
||||
|
||||
assert_eq!(*service.my_addr_cache.lock().unwrap(), None);
|
||||
assert_eq!(global_ctx.get_ipv6(), Some(virtual_addr));
|
||||
assert_eq!(global_ctx.get_public_ipv6_lease(), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconcile_runtime_keeps_virtual_ipv6_when_public_lease_changes() {
|
||||
let global_ctx = get_mock_global_ctx();
|
||||
global_ctx.config.set_ipv6_public_addr_auto(true);
|
||||
|
||||
let virtual_addr = "fd00::1/64".parse().unwrap();
|
||||
let public_addr = "2001:db8::123/64".parse().unwrap();
|
||||
global_ctx.set_ipv6(Some(virtual_addr));
|
||||
|
||||
let service = Arc::new(PublicIpv6Service::new(
|
||||
global_ctx.clone(),
|
||||
std::sync::Weak::<PeerRpcManager>::new(),
|
||||
Arc::new(TestRouteControl {
|
||||
my_peer_id: 1,
|
||||
peers: Mutex::new(vec![PublicIpv6PeerRouteInfo {
|
||||
peer_id: 1,
|
||||
inst_id: Some(uuid::Uuid::from_u128(1)),
|
||||
is_provider: false,
|
||||
prefix: None,
|
||||
lease: Some(public_addr),
|
||||
reachable: true,
|
||||
}]),
|
||||
}),
|
||||
Arc::new(TestSyncTrigger),
|
||||
));
|
||||
|
||||
service.reconcile_runtime();
|
||||
|
||||
assert_eq!(global_ctx.get_ipv6(), Some(virtual_addr));
|
||||
assert_eq!(global_ctx.get_public_ipv6_lease(), Some(public_addr));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,10 +105,6 @@ pub trait Route {
|
||||
None
|
||||
}
|
||||
|
||||
async fn get_public_ipv6_gateway_peer_id(&self) -> Option<PeerId> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn get_local_public_ipv6_info(&self) -> ListPublicIpv6InfoResponse {
|
||||
ListPublicIpv6InfoResponse::default()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::sync::{Arc, Mutex, atomic::AtomicBool};
|
||||
|
||||
use futures::{SinkExt as _, StreamExt};
|
||||
use guarden::defer;
|
||||
use tokio::{task::JoinSet, time::timeout};
|
||||
|
||||
use crate::{
|
||||
defer,
|
||||
proto::rpc_types::error::Error,
|
||||
tunnel::{Tunnel, packet_def::PacketType, ring::create_ring_tunnel_pair},
|
||||
};
|
||||
|
||||
@@ -4,17 +4,18 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use bytes::Bytes;
|
||||
use dashmap::DashMap;
|
||||
use guarden::defer;
|
||||
use prost::Message;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinSet;
|
||||
use tokio::time::timeout;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::common::shrink_dashmap;
|
||||
use crate::common::{
|
||||
PeerId, shrink_dashmap,
|
||||
PeerId,
|
||||
stats_manager::{LabelSet, LabelType, MetricName, StatsManager},
|
||||
};
|
||||
use crate::defer;
|
||||
use crate::proto::common::{
|
||||
CompressionAlgoPb, RpcCompressionInfo, RpcDescriptor, RpcPacket, RpcRequest, RpcResponse,
|
||||
};
|
||||
|
||||
@@ -3599,15 +3599,7 @@ pub async fn config_patch_test() {
|
||||
};
|
||||
use crate::tunnel::common::tests::_tunnel_pingpong_netns_with_timeout;
|
||||
|
||||
let insts = init_three_node_ex(
|
||||
"udp",
|
||||
|cfg| {
|
||||
cfg.set_ipv6(None);
|
||||
cfg
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
let insts = init_three_node("udp").await;
|
||||
|
||||
check_route(
|
||||
"10.144.144.2/24",
|
||||
|
||||
@@ -281,7 +281,6 @@ impl TunnelListener for FakeTcpTunnelListener {
|
||||
pub struct FakeTcpTunnelConnector {
|
||||
addr: url::Url,
|
||||
ip_to_if_name: IpToIfNameCache,
|
||||
resolved_addr: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
impl FakeTcpTunnelConnector {
|
||||
@@ -289,7 +288,6 @@ impl FakeTcpTunnelConnector {
|
||||
FakeTcpTunnelConnector {
|
||||
addr,
|
||||
ip_to_if_name: IpToIfNameCache::new(),
|
||||
resolved_addr: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,10 +314,7 @@ fn get_local_ip_for_destination(destination: IpAddr) -> Option<IpAddr> {
|
||||
#[async_trait::async_trait]
|
||||
impl TunnelConnector for FakeTcpTunnelConnector {
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let remote_addr = match self.resolved_addr {
|
||||
Some(addr) => addr,
|
||||
None => SocketAddr::from_url(self.addr.clone(), IpVersion::Both).await?,
|
||||
};
|
||||
let remote_addr = SocketAddr::from_url(self.addr.clone(), IpVersion::Both).await?;
|
||||
let local_ip = get_local_ip_for_destination(remote_addr.ip())
|
||||
.ok_or(TunnelError::InternalError("Failed to get local ip".into()))?;
|
||||
|
||||
@@ -395,10 +390,6 @@ impl TunnelConnector for FakeTcpTunnelConnector {
|
||||
fn remote_url(&self) -> url::Url {
|
||||
self.addr.clone()
|
||||
}
|
||||
|
||||
fn set_resolved_addr(&mut self, addr: SocketAddr) {
|
||||
self.resolved_addr = Some(addr);
|
||||
}
|
||||
}
|
||||
|
||||
type RecvFut = Pin<Box<dyn Future<Output = Option<(BytesMut, usize)>> + Send + Sync>>;
|
||||
|
||||
@@ -141,7 +141,6 @@ pub trait TunnelConnector: Send {
|
||||
fn remote_url(&self) -> url::Url;
|
||||
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) {}
|
||||
}
|
||||
|
||||
pub fn build_url_from_socket_addr(addr: &String, scheme: &str) -> url::Url {
|
||||
@@ -372,13 +371,9 @@ impl TryFrom<&url::Url> for TunnelScheme {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_scheme_by_url(l: &url::Url) -> Result<TunnelScheme, Error> {
|
||||
l.try_into()
|
||||
}
|
||||
|
||||
macro_rules! __matches_scheme__ {
|
||||
($url:expr, $( $pattern:pat_param )|+ ) => {
|
||||
matches!($crate::tunnel::get_scheme_by_url(&$url), Ok($( $pattern )|+))
|
||||
matches!($crate::tunnel::TunnelScheme::try_from(($url).as_ref()), Ok($( $pattern )|+))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -398,22 +393,3 @@ macro_rules! __matches_protocol__ {
|
||||
}
|
||||
|
||||
pub(crate) use __matches_protocol__ as matches_protocol;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{IpScheme, TunnelScheme, matches_scheme};
|
||||
|
||||
#[test]
|
||||
fn matches_scheme_accepts_owned_url() {
|
||||
let url: url::Url = "udp://[2001:db8::1]:11010".parse().unwrap();
|
||||
|
||||
assert!(matches_scheme!(url, TunnelScheme::Ip(IpScheme::Udp)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_scheme_accepts_borrowed_url() {
|
||||
let url: url::Url = "udp://[2001:db8::1]:11010".parse().unwrap();
|
||||
|
||||
assert!(matches_scheme!(&url, TunnelScheme::Ip(IpScheme::Udp)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,7 +432,6 @@ pub struct QuicTunnelConnector {
|
||||
addr: url::Url,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
ip_version: IpVersion,
|
||||
resolved_addr: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
impl QuicTunnelConnector {
|
||||
@@ -441,7 +440,6 @@ impl QuicTunnelConnector {
|
||||
addr,
|
||||
global_ctx,
|
||||
ip_version: IpVersion::Both,
|
||||
resolved_addr: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -449,10 +447,7 @@ impl QuicTunnelConnector {
|
||||
#[async_trait::async_trait]
|
||||
impl TunnelConnector for QuicTunnelConnector {
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let addr = match self.resolved_addr {
|
||||
Some(addr) => addr,
|
||||
None => SocketAddr::from_url(self.addr.clone(), self.ip_version).await?,
|
||||
};
|
||||
let addr = SocketAddr::from_url(self.addr.clone(), self.ip_version).await?;
|
||||
let (endpoint, connection) = QuicEndpointManager::connect(&self.global_ctx, addr).await?;
|
||||
|
||||
let local_addr = endpoint.local_addr()?;
|
||||
@@ -489,10 +484,6 @@ impl TunnelConnector for QuicTunnelConnector {
|
||||
fn set_ip_version(&mut self, ip_version: IpVersion) {
|
||||
self.ip_version = ip_version;
|
||||
}
|
||||
|
||||
fn set_resolved_addr(&mut self, addr: SocketAddr) {
|
||||
self.resolved_addr = Some(addr);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -129,7 +129,6 @@ pub struct TcpTunnelConnector {
|
||||
|
||||
bind_addrs: Vec<SocketAddr>,
|
||||
ip_version: IpVersion,
|
||||
resolved_addr: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
impl TcpTunnelConnector {
|
||||
@@ -138,7 +137,6 @@ impl TcpTunnelConnector {
|
||||
addr,
|
||||
bind_addrs: vec![],
|
||||
ip_version: IpVersion::Both,
|
||||
resolved_addr: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,10 +175,7 @@ impl TcpTunnelConnector {
|
||||
#[async_trait]
|
||||
impl super::TunnelConnector for TcpTunnelConnector {
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let addr = match self.resolved_addr {
|
||||
Some(addr) => addr,
|
||||
None => SocketAddr::from_url(self.addr.clone(), self.ip_version).await?,
|
||||
};
|
||||
let addr = SocketAddr::from_url(self.addr.clone(), self.ip_version).await?;
|
||||
if self.bind_addrs.is_empty() {
|
||||
self.connect_with_default_bind(addr).await
|
||||
} else {
|
||||
@@ -199,10 +194,6 @@ impl super::TunnelConnector for TcpTunnelConnector {
|
||||
fn set_ip_version(&mut self, ip_version: IpVersion) {
|
||||
self.ip_version = ip_version;
|
||||
}
|
||||
|
||||
fn set_resolved_addr(&mut self, addr: SocketAddr) {
|
||||
self.resolved_addr = Some(addr);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -303,31 +294,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn connector_uses_pre_resolved_addr_without_resolving_url() {
|
||||
let mut listener = TcpTunnelListener::new("tcp://127.0.0.1:0".parse().unwrap());
|
||||
listener.listen().await.unwrap();
|
||||
|
||||
let port = listener.local_url().port().unwrap();
|
||||
let source_url: url::Url = format!("tcp://unresolvable.invalid:{port}")
|
||||
.parse()
|
||||
.unwrap();
|
||||
let resolved_addr: SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
|
||||
let mut connector = TcpTunnelConnector::new(source_url.clone());
|
||||
connector.set_resolved_addr(resolved_addr);
|
||||
|
||||
let accept_task = tokio::spawn(async move { listener.accept().await.unwrap() });
|
||||
let tunnel = connector.connect().await.unwrap();
|
||||
let _accepted_tunnel = accept_task.await.unwrap();
|
||||
|
||||
let info = tunnel.info().unwrap();
|
||||
assert_eq!(info.remote_addr.unwrap().url, source_url.to_string());
|
||||
|
||||
let resolved_remote_addr: url::Url = info.resolved_remote_addr.unwrap().into();
|
||||
assert_eq!(resolved_remote_addr.host_str(), Some("127.0.0.1"));
|
||||
assert_eq!(resolved_remote_addr.port(), Some(port));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_alloc_port() {
|
||||
// v4
|
||||
|
||||
@@ -682,7 +682,6 @@ pub struct UdpTunnelConnector {
|
||||
addr: url::Url,
|
||||
bind_addrs: Vec<SocketAddr>,
|
||||
ip_version: IpVersion,
|
||||
resolved_addr: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
impl UdpTunnelConnector {
|
||||
@@ -691,7 +690,6 @@ impl UdpTunnelConnector {
|
||||
addr,
|
||||
bind_addrs: vec![],
|
||||
ip_version: IpVersion::Both,
|
||||
resolved_addr: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -908,10 +906,7 @@ impl UdpTunnelConnector {
|
||||
#[async_trait]
|
||||
impl super::TunnelConnector for UdpTunnelConnector {
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let addr = match self.resolved_addr {
|
||||
Some(addr) => addr,
|
||||
None => SocketAddr::from_url(self.addr.clone(), self.ip_version).await?,
|
||||
};
|
||||
let addr = SocketAddr::from_url(self.addr.clone(), self.ip_version).await?;
|
||||
if self.bind_addrs.is_empty() || addr.is_ipv6() {
|
||||
self.connect_with_default_bind(addr).await
|
||||
} else {
|
||||
@@ -930,10 +925,6 @@ impl super::TunnelConnector for UdpTunnelConnector {
|
||||
fn set_ip_version(&mut self, ip_version: IpVersion) {
|
||||
self.ip_version = ip_version;
|
||||
}
|
||||
|
||||
fn set_resolved_addr(&mut self, addr: SocketAddr) {
|
||||
self.resolved_addr = Some(addr);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -198,7 +198,6 @@ impl TunnelListener for WsTunnelListener {
|
||||
pub struct WsTunnelConnector {
|
||||
addr: url::Url,
|
||||
ip_version: IpVersion,
|
||||
resolved_addr: Option<SocketAddr>,
|
||||
|
||||
bind_addrs: Vec<SocketAddr>,
|
||||
}
|
||||
@@ -208,7 +207,6 @@ impl WsTunnelConnector {
|
||||
WsTunnelConnector {
|
||||
addr,
|
||||
ip_version: IpVersion::Both,
|
||||
resolved_addr: None,
|
||||
|
||||
bind_addrs: vec![],
|
||||
}
|
||||
@@ -216,10 +214,11 @@ impl WsTunnelConnector {
|
||||
|
||||
async fn connect_with(
|
||||
addr: url::Url,
|
||||
socket_addr: SocketAddr,
|
||||
ip_version: IpVersion,
|
||||
tcp_socket: TcpSocket,
|
||||
) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let is_wss = is_wss(&addr)?;
|
||||
let socket_addr = SocketAddr::from_url(addr.clone(), ip_version).await?;
|
||||
let stream = tcp_socket.connect(socket_addr).await?;
|
||||
if let Err(error) = stream.set_nodelay(true) {
|
||||
tracing::warn!(?error, "set_nodelay fail in ws connect");
|
||||
@@ -274,7 +273,7 @@ impl WsTunnelConnector {
|
||||
} else {
|
||||
TcpSocket::new_v6()?
|
||||
};
|
||||
Self::connect_with(self.addr.clone(), addr, socket).await
|
||||
Self::connect_with(self.addr.clone(), self.ip_version, socket).await
|
||||
}
|
||||
|
||||
async fn connect_with_custom_bind(
|
||||
@@ -286,7 +285,11 @@ impl WsTunnelConnector {
|
||||
for bind_addr in self.bind_addrs.iter() {
|
||||
tracing::info!(?bind_addr, ?addr, "bind addr");
|
||||
match bind().addr(*bind_addr).only_v6(true).call() {
|
||||
Ok(socket) => futures.push(Self::connect_with(self.addr.clone(), addr, socket)),
|
||||
Ok(socket) => futures.push(Self::connect_with(
|
||||
self.addr.clone(),
|
||||
self.ip_version,
|
||||
socket,
|
||||
)),
|
||||
Err(error) => {
|
||||
tracing::error!(?bind_addr, ?addr, ?error, "bind addr fail");
|
||||
continue;
|
||||
@@ -301,10 +304,7 @@ impl WsTunnelConnector {
|
||||
#[async_trait::async_trait]
|
||||
impl TunnelConnector for WsTunnelConnector {
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let addr = match self.resolved_addr {
|
||||
Some(addr) => addr,
|
||||
None => SocketAddr::from_url(self.addr.clone(), self.ip_version).await?,
|
||||
};
|
||||
let addr = SocketAddr::from_url(self.addr.clone(), self.ip_version).await?;
|
||||
if self.bind_addrs.is_empty() || addr.is_ipv6() {
|
||||
self.connect_with_default_bind(addr).await
|
||||
} else {
|
||||
@@ -323,10 +323,6 @@ impl TunnelConnector for WsTunnelConnector {
|
||||
fn set_bind_addrs(&mut self, addrs: Vec<SocketAddr>) {
|
||||
self.bind_addrs = addrs;
|
||||
}
|
||||
|
||||
fn set_resolved_addr(&mut self, addr: SocketAddr) {
|
||||
self.resolved_addr = Some(addr);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -598,7 +598,6 @@ pub struct WgTunnelConnector {
|
||||
|
||||
bind_addrs: Vec<SocketAddr>,
|
||||
ip_version: IpVersion,
|
||||
resolved_addr: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
impl Debug for WgTunnelConnector {
|
||||
@@ -618,7 +617,6 @@ impl WgTunnelConnector {
|
||||
udp: None,
|
||||
bind_addrs: vec![],
|
||||
ip_version: IpVersion::Both,
|
||||
resolved_addr: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,10 +702,7 @@ impl WgTunnelConnector {
|
||||
impl super::TunnelConnector for WgTunnelConnector {
|
||||
#[tracing::instrument]
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let addr = match self.resolved_addr {
|
||||
Some(addr) => addr,
|
||||
None => SocketAddr::from_url(self.addr.clone(), self.ip_version).await?,
|
||||
};
|
||||
let addr = SocketAddr::from_url(self.addr.clone(), self.ip_version).await?;
|
||||
|
||||
if addr.is_ipv6() {
|
||||
return self.connect_with_ipv6(addr).await;
|
||||
@@ -749,10 +744,6 @@ impl super::TunnelConnector for WgTunnelConnector {
|
||||
fn set_ip_version(&mut self, ip_version: IpVersion) {
|
||||
self.ip_version = ip_version;
|
||||
}
|
||||
|
||||
fn set_resolved_addr(&mut self, addr: SocketAddr) {
|
||||
self.resolved_addr = Some(addr);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,638 @@
|
||||
//! # Guard Module Utilities
|
||||
//!
|
||||
//! This module provides mechanisms for scope-based resource management and deferred execution.
|
||||
//!
|
||||
//! ### ⚠️ Critical Usage Note: Diverging Expressions
|
||||
//!
|
||||
//! Do not use "naked" diverging expressions—such as `panic!`, `todo!`, or `loop {}`—as
|
||||
//! the sole content of sync guard closure. This prevents the compiler from
|
||||
//! distinguishing between synchronous (`ASYNC = false`) and asynchronous
|
||||
//! (`ASYNC = true`) implementations, leading to a type inference error (E0277).
|
||||
//!
|
||||
//! ### Technical Context
|
||||
//!
|
||||
//! The `!` (Never Type) is a bottom type that can be coerced into any other type.
|
||||
//! Because it satisfies both the `()` requirement for sync guards and the `Future`
|
||||
//! requirement for async guards, the compiler encounters an inference deadlock.
|
||||
//!
|
||||
//! ### Workaround
|
||||
//!
|
||||
//! For macros like `guard!` or `guarded!`, force the closure to resolve to `()`
|
||||
//! by explicitly setting the guard to `sync`:
|
||||
//!
|
||||
//! ```rust
|
||||
//! let _g = guard!([val] sync {
|
||||
//! panic!("critical failure");
|
||||
//! });
|
||||
//! ```
|
||||
|
||||
use crate::utils::task::{DetachableTask, TaskSpawner};
|
||||
use std::fmt::Debug;
|
||||
use std::mem::ManuallyDrop;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
pub trait CallableGuard<const ASYNC: bool, Context> {
|
||||
type Output;
|
||||
fn call(self, context: Context) -> Self::Output;
|
||||
}
|
||||
|
||||
impl<Context, Guard> CallableGuard<false, Context> for Guard
|
||||
where
|
||||
Guard: FnOnce(Context),
|
||||
{
|
||||
type Output = ();
|
||||
|
||||
fn call(self, context: Context) -> Self::Output {
|
||||
self(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Context, Guard, Task, _R> CallableGuard<true, Context> for Guard
|
||||
where
|
||||
Guard: FnOnce(Context) -> Task + Send + 'static,
|
||||
Task: Future<Output = _R> + Send + 'static,
|
||||
_R: Send + 'static,
|
||||
{
|
||||
type Output = DetachableTask<TaskSpawner<Task>, Task>;
|
||||
|
||||
fn call(self, context: Context) -> Self::Output {
|
||||
DetachableTask::new(self(context))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContextGuard<const ASYNC: bool, Context, Guard: CallableGuard<ASYNC, Context>> {
|
||||
context: ManuallyDrop<Context>,
|
||||
guard: ManuallyDrop<Guard>,
|
||||
}
|
||||
|
||||
impl<const ASYNC: bool, Context, Guard: CallableGuard<ASYNC, Context>> Deref
|
||||
for ContextGuard<ASYNC, Context, Guard>
|
||||
{
|
||||
type Target = Context;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.context
|
||||
}
|
||||
}
|
||||
|
||||
impl<const ASYNC: bool, Context, Guard: CallableGuard<ASYNC, Context>> DerefMut
|
||||
for ContextGuard<ASYNC, Context, Guard>
|
||||
{
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.context
|
||||
}
|
||||
}
|
||||
|
||||
impl<const ASYNC: bool, Context: Debug, Guard: CallableGuard<ASYNC, Context>> Debug
|
||||
for ContextGuard<ASYNC, Context, Guard>
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let name = if ASYNC {
|
||||
"ContextGuard::Async"
|
||||
} else {
|
||||
"ContextGuard::Sync"
|
||||
};
|
||||
f.debug_struct(name)
|
||||
.field("context", &self.context)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const ASYNC: bool, Context, Guard: CallableGuard<ASYNC, Context>>
|
||||
ContextGuard<ASYNC, Context, Guard>
|
||||
{
|
||||
/// Creates a new `ContextGuard`.
|
||||
///
|
||||
/// **Note on generics:** The seemingly unused `_R` generic parameter and the
|
||||
/// `Guard: FnOnce(Context) -> _R` trait bound are intentionally included.
|
||||
/// They act as a hint to help the compiler infer closure types.
|
||||
pub fn new<_R>(context: Context, guard: Guard) -> Self
|
||||
where
|
||||
Guard: FnOnce(Context) -> _R,
|
||||
{
|
||||
ContextGuard {
|
||||
context: ManuallyDrop::new(context),
|
||||
guard: ManuallyDrop::new(guard),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const ASYNC: bool, Context, Guard: CallableGuard<ASYNC, Context>>
|
||||
ContextGuard<ASYNC, Context, Guard>
|
||||
{
|
||||
unsafe fn call(&mut self) -> Guard::Output {
|
||||
unsafe {
|
||||
let context = ManuallyDrop::take(&mut self.context);
|
||||
let guard = ManuallyDrop::take(&mut self.guard);
|
||||
|
||||
guard.call(context)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trigger(self) -> Guard::Output {
|
||||
let mut this = ManuallyDrop::new(self);
|
||||
unsafe { this.call() }
|
||||
}
|
||||
|
||||
pub fn defuse(self) -> Context {
|
||||
let mut this = ManuallyDrop::new(self);
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut this.guard);
|
||||
ManuallyDrop::take(&mut this.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const ASYNC: bool, Context, Guard: CallableGuard<ASYNC, Context>> Drop
|
||||
for ContextGuard<ASYNC, Context, Guard>
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
let _: Guard::Output = unsafe { self.call() };
|
||||
}
|
||||
}
|
||||
|
||||
// region macro
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! __guarded {
|
||||
(@parse@action $guard:ident => $($tt:tt)*) => {
|
||||
$crate::__guarded! { @parse@async action: [ @stmt $guard ] ; $($tt)* }
|
||||
};
|
||||
|
||||
(@parse@action $($tt:tt)*) => {
|
||||
$crate::__guarded! { @parse@async action: [ @stmt __guard ] ; $($tt)* }
|
||||
};
|
||||
|
||||
(@parse@async action: [ $($action:tt)* ] ; sync $($tt:tt)*) => {
|
||||
$crate::__guarded! { @parse@move action: [ $($action)* ] ; async: [ false ] ; $($tt)* }
|
||||
};
|
||||
|
||||
(@parse@async action: [ $($action:tt)* ] ; $($tt:tt)*) => {
|
||||
$crate::__guarded! { @parse@move action: [ $($action)* ] ; async: [ _ ] ; $($tt)* }
|
||||
};
|
||||
|
||||
(@parse@move action: [ $($action:tt)* ] ; async: [ $async:tt ] ; move $($tt:tt)*) => {
|
||||
$crate::__guarded! { @parse action: [ $($action)* ] ; async: [ $async ] ; move: [ move ] ; $($tt)* }
|
||||
};
|
||||
|
||||
(@parse@move action: [ $($action:tt)* ] ; async: [ $async:tt ] ; $($tt:tt)*) => {
|
||||
$crate::__guarded! { @parse action: [ $($action)* ] ; async: [ $async ] ; move: [] ; $($tt)* }
|
||||
};
|
||||
|
||||
(
|
||||
@parse action: [ $($action:tt)* ] ; async: [ $async:tt ] ; move: [ $($move:tt)? ] ;
|
||||
[ $($args:tt)* ] $body:block
|
||||
) => {
|
||||
$crate::__guarded! {
|
||||
action: [ $($action)* ]
|
||||
async: [ $async ]
|
||||
move: [ $($move)? ]
|
||||
mut: []
|
||||
rest: [ $($args)* , ]
|
||||
args: []
|
||||
vars: []
|
||||
body: [ $body ]
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
@parse action: [ $($action:tt)* ] ; async: [ $async:tt ] ; move: [ $($move:tt)? ] ;
|
||||
$body:block
|
||||
) => {
|
||||
$crate::__guarded! {
|
||||
@parse action: [ $($action)* ] ; async: [ $async ] ; move: [ $($move)? ] ;
|
||||
[] $body
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
@parse action: [ $($action:tt)* ] ; async: [ $async:tt ] ; move: [ $($move:tt)? ] ;
|
||||
[ $($args:tt)* ] $($body:tt)*
|
||||
) => {
|
||||
$crate::__guarded! {
|
||||
@parse action: [ $($action)* ] ; async: [ $async ] ; move: [ $($move)? ] ;
|
||||
[ $($args)* ] { $($body)* }
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
@parse action: [ $($action:tt)* ] ; async: [ $async:tt ] ; move: [ $($move:tt)? ] ;
|
||||
$($body:tt)*
|
||||
) => {
|
||||
$crate::__guarded! {
|
||||
@parse action: [ $($action)* ] ; async: [ $async ] ; move: [ $($move)? ] ;
|
||||
[] { $($body)* }
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
action: [ $($action:tt)* ]
|
||||
async: [ $async:tt ]
|
||||
move: [ $($move:tt)? ]
|
||||
mut: [ $($mut:tt)? ]
|
||||
rest: [ mut $arg:ident , $($rest:tt)* ]
|
||||
args: [ $($args:ident)* ]
|
||||
vars: [ $($vars:tt)* ]
|
||||
body: [ $body:expr ]
|
||||
) => {
|
||||
$crate::__guarded! {
|
||||
action: [ $($action)* ]
|
||||
async: [ $async ]
|
||||
move: [ $($move)? ]
|
||||
mut: [ mut ]
|
||||
rest: [ $($rest)* ]
|
||||
args: [ $($args)* $arg ]
|
||||
vars: [ $($vars)* [mut $arg] ]
|
||||
body: [ $body ]
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
action: [ $($action:tt)* ]
|
||||
async: [ $async:tt ]
|
||||
move: [ $($move:tt)? ]
|
||||
mut: [ $($mut:tt)? ]
|
||||
rest: [ $arg:ident , $($rest:tt)* ]
|
||||
args: [ $($args:ident)* ]
|
||||
vars: [ $($vars:tt)* ]
|
||||
body: [ $body:expr ]
|
||||
) => {
|
||||
$crate::__guarded! {
|
||||
action: [ $($action)* ]
|
||||
async: [ $async ]
|
||||
move: [ $($move)? ]
|
||||
mut: [ $($mut)? ]
|
||||
rest: [ $($rest)* ]
|
||||
args: [ $($args)* $arg ]
|
||||
vars: [ $($vars)* [$arg] ]
|
||||
body: [ $body ]
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
action: [ @stmt $guard:ident ]
|
||||
async: [ $async:tt ]
|
||||
move: [ $($move:tt)? ]
|
||||
mut: [ $($mut:tt)? ]
|
||||
rest: [ $(,)* ]
|
||||
args: [ $($args:ident)* ]
|
||||
vars: [ $([$($vars:tt)*])* ]
|
||||
body: [ $body:expr ]
|
||||
) => {
|
||||
let $($mut)? $guard = $crate::utils::guard::ContextGuard::<$async, _, _>::new(
|
||||
( $($args),* ),
|
||||
$($move)? |#[allow(unused_parens, unused_mut)] ( $($($vars)*),* )| $body
|
||||
);
|
||||
|
||||
#[allow(unused_parens, unused_variables, clippy::toplevel_ref_arg)]
|
||||
let ( $(ref $($vars)*),* ) = *$guard;
|
||||
};
|
||||
|
||||
(
|
||||
action: [ @expr ]
|
||||
async: [ $async:tt ]
|
||||
move: [ $($move:tt)? ]
|
||||
mut: [ $($mut:tt)? ]
|
||||
rest: [ $(,)* ]
|
||||
args: [ $($args:ident)* ]
|
||||
vars: [ $([$($vars:tt)*])* ]
|
||||
body: [ $body:expr ]
|
||||
) => {
|
||||
$crate::utils::guard::ContextGuard::<$async, _, _>::new(
|
||||
( $($args),* ),
|
||||
$($move)? |#[allow(unused_parens)] ( $($($vars)*),* )| $body
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates a [`ContextGuard`] object, binding it to a variable with the specified name (e.g., `_guard`).
|
||||
/// Context variables specified in the macro invocation are available within and after the guard body.
|
||||
///
|
||||
/// **Note:** For usage with `panic!` or `loop`, see the [module-level documentation](self)
|
||||
/// regarding type inference deadlocks.
|
||||
#[macro_export]
|
||||
macro_rules! guarded {
|
||||
( $($tt:tt)* ) => {
|
||||
$crate::__guarded! { @parse@action $($tt)* }
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates a [`ContextGuard`] object, without binding it to a variable.
|
||||
/// Context variables specified in the macro invocation are available within the guard body.
|
||||
///
|
||||
/// **Note:** For usage with `panic!` or `loop`, see the [module-level documentation](self)
|
||||
/// regarding type inference deadlocks.
|
||||
#[macro_export]
|
||||
macro_rules! guard {
|
||||
( $($tt:tt)* ) => {
|
||||
$crate::__guarded! { @parse@async action: [ @expr ] ; $($tt)* }
|
||||
};
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
/// Alias for [`guarded!`].
|
||||
///
|
||||
/// **Note:** For usage with `panic!` or `loop`, see the [module-level documentation](self)
|
||||
/// regarding type inference deadlocks.
|
||||
#[macro_export]
|
||||
macro_rules! defer {
|
||||
( $($tt:tt)* ) => {
|
||||
$crate::guarded! { $($tt)* }
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::panic::catch_unwind;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
#[test]
|
||||
fn trigger_sync_executes_once() {
|
||||
let called = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let value = 7usize;
|
||||
let guard = {
|
||||
let called = called.clone();
|
||||
let observed = observed.clone();
|
||||
crate::guard!(move [value] {
|
||||
called.fetch_add(1, Ordering::SeqCst);
|
||||
observed.store(value, Ordering::SeqCst);
|
||||
})
|
||||
};
|
||||
|
||||
guard.trigger();
|
||||
|
||||
assert_eq!(called.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(observed.load(Ordering::SeqCst), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defuse_sync_returns_context_without_running_guard() {
|
||||
let called = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let value = String::from("hello");
|
||||
let guard = {
|
||||
let called = called.clone();
|
||||
crate::guard!(move [mut value] {
|
||||
value.push_str(" world");
|
||||
called.fetch_add(1, Ordering::SeqCst);
|
||||
})
|
||||
};
|
||||
|
||||
let context = guard.defuse();
|
||||
assert_eq!(context, "hello");
|
||||
assert_eq!(called.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_sync_triggers_guard() {
|
||||
let called = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
{
|
||||
let called = called.clone();
|
||||
crate::guarded!([called] {
|
||||
called.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
|
||||
assert_eq!(called.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_propagates_guard_panic() {
|
||||
let dropped = catch_unwind(|| {
|
||||
guarded! {
|
||||
sync {
|
||||
panic!("boom");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert!(dropped.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn trigger_async_returns_runnable_task() {
|
||||
let called = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let value = 5usize;
|
||||
let guard = {
|
||||
let called = called.clone();
|
||||
crate::guard!(move [value] async move {
|
||||
called.fetch_add(value, Ordering::SeqCst);
|
||||
})
|
||||
};
|
||||
let task = guard.trigger();
|
||||
task.await;
|
||||
|
||||
assert_eq!(called.load(Ordering::SeqCst), 5);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn drop_async_detaches_task() {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
{
|
||||
let mut tx = Some(tx);
|
||||
let value = 9usize;
|
||||
let _guard = crate::guard!(move [value] {
|
||||
let tx = tx.take();
|
||||
async move {
|
||||
if let Some(tx) = tx {
|
||||
let _ = tx.send(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let value = tokio::time::timeout(Duration::from_secs(1), rx)
|
||||
.await
|
||||
.expect("detached task should run")
|
||||
.expect("detached task should send value");
|
||||
assert_eq!(value, 9);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn defuse_async_does_not_execute() {
|
||||
let called = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let value = 11usize;
|
||||
let guard = {
|
||||
let called = called.clone();
|
||||
crate::guard!(move [value] async move {
|
||||
called.fetch_add(value, Ordering::SeqCst);
|
||||
})
|
||||
};
|
||||
|
||||
let context = guard.defuse();
|
||||
assert_eq!(context, 11);
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
assert_eq!(called.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guarded_named_mut_binding_updates_context_before_drop() {
|
||||
let committed = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
{
|
||||
let value = 1usize;
|
||||
let step = 2usize;
|
||||
let committed = committed.clone();
|
||||
|
||||
crate::guarded!(scope_guard => [mut value, step] {
|
||||
committed.store(value + step, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
*value += 10;
|
||||
assert_eq!(*value, 11);
|
||||
assert_eq!(*step, 2);
|
||||
|
||||
drop(scope_guard);
|
||||
}
|
||||
|
||||
assert_eq!(committed.load(Ordering::SeqCst), 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guard_expression_parses_without_braces() {
|
||||
let observed = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let value = 3usize;
|
||||
let observed_clone = observed.clone();
|
||||
let guard = crate::guard!([value] observed_clone.store(value, Ordering::SeqCst));
|
||||
guard.trigger();
|
||||
|
||||
assert_eq!(observed.load(Ordering::SeqCst), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defer_alias_behaves_like_guarded_statement() {
|
||||
let called = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
{
|
||||
let n = 42usize;
|
||||
let called = called.clone();
|
||||
crate::defer!([n] {
|
||||
called.store(n, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
|
||||
assert_eq!(called.load(Ordering::SeqCst), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn guard_and_guarded_macro_usage_matrix() {
|
||||
// 1) guard!: block body + trailing comma args + trigger()
|
||||
let sink = Arc::new(AtomicUsize::new(0));
|
||||
let v = 1usize;
|
||||
let sink_clone = sink.clone();
|
||||
let g1 = crate::guard!([v,] {
|
||||
sink_clone.store(v, Ordering::SeqCst);
|
||||
});
|
||||
g1.trigger();
|
||||
assert_eq!(sink.load(Ordering::SeqCst), 1);
|
||||
|
||||
// 2) guard!: expression body (no braces)
|
||||
let sink = Arc::new(AtomicUsize::new(0));
|
||||
let sink_clone = sink.clone();
|
||||
let v = 2usize;
|
||||
let g2 = crate::guard!([v] sink_clone.store(v, Ordering::SeqCst));
|
||||
g2.trigger();
|
||||
assert_eq!(sink.load(Ordering::SeqCst), 2);
|
||||
|
||||
// 3) guard!: explicit sync + no args form
|
||||
let sink = Arc::new(AtomicUsize::new(0));
|
||||
let sink_clone = sink.clone();
|
||||
let g3 = crate::guard!(sync {
|
||||
sink_clone.store(3, Ordering::SeqCst);
|
||||
});
|
||||
g3.trigger();
|
||||
assert_eq!(sink.load(Ordering::SeqCst), 3);
|
||||
|
||||
// 4) guard!: move capture + defuse() prevents execution
|
||||
let sink = Arc::new(AtomicUsize::new(0));
|
||||
let owned = String::from("owned");
|
||||
let sink_clone = sink.clone();
|
||||
let g4 = crate::guard!(move [owned] {
|
||||
if owned == "owned" {
|
||||
sink_clone.store(4, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
let context = g4.defuse();
|
||||
assert_eq!(context, "owned");
|
||||
assert_eq!(sink.load(Ordering::SeqCst), 0);
|
||||
|
||||
// 5) guard!: async block inference + trigger() returns task
|
||||
let sink = Arc::new(AtomicUsize::new(0));
|
||||
let sink_clone = sink.clone();
|
||||
let n = 5usize;
|
||||
let g5 = crate::guard!([n] async move {
|
||||
sink_clone.fetch_add(n, Ordering::SeqCst);
|
||||
});
|
||||
g5.trigger().await;
|
||||
assert_eq!(sink.load(Ordering::SeqCst), 5);
|
||||
|
||||
// 6) guarded!: named binding + mut arg visible outside + explicit drop
|
||||
let sink = Arc::new(AtomicUsize::new(0));
|
||||
{
|
||||
let value = 6usize;
|
||||
let delta = 1usize;
|
||||
let sink_clone = sink.clone();
|
||||
|
||||
crate::guarded!(named => [mut value, delta] {
|
||||
sink_clone.store(value + delta, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
*value += 10;
|
||||
assert_eq!(*value, 16);
|
||||
assert_eq!(*delta, 1);
|
||||
drop(named);
|
||||
}
|
||||
assert_eq!(sink.load(Ordering::SeqCst), 17);
|
||||
|
||||
// 7) guarded!: unnamed statement + expression body + implicit drop at scope end
|
||||
let sink = Arc::new(AtomicUsize::new(0));
|
||||
{
|
||||
let n = 7usize;
|
||||
let sink_clone = sink.clone();
|
||||
crate::guarded!([n] sink_clone.store(n, Ordering::SeqCst));
|
||||
}
|
||||
assert_eq!(sink.load(Ordering::SeqCst), 7);
|
||||
|
||||
// 8) guarded!: explicit sync + panic path propagates on drop
|
||||
let dropped = catch_unwind(|| {
|
||||
guarded! {
|
||||
sync {
|
||||
panic!("matrix-boom");
|
||||
}
|
||||
}
|
||||
});
|
||||
assert!(dropped.is_err());
|
||||
|
||||
// 9) guarded!: async inference on drop detaches and executes
|
||||
let (tx, rx) = oneshot::channel();
|
||||
{
|
||||
let tx = Some(tx);
|
||||
crate::guarded!([mut tx] {
|
||||
let tx = tx.take();
|
||||
async move {
|
||||
if let Some(tx) = tx {
|
||||
let _ = tx.send(9usize);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
let detached = tokio::time::timeout(Duration::from_secs(1), rx)
|
||||
.await
|
||||
.expect("detached task should complete")
|
||||
.expect("detached task should send value");
|
||||
assert_eq!(detached, 9);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod guard;
|
||||
pub mod panic;
|
||||
pub mod string;
|
||||
pub mod task;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::utils::guard::ContextGuard;
|
||||
use std::future::Future;
|
||||
use std::io;
|
||||
use std::ops::DerefMut;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
@@ -78,3 +80,284 @@ impl<Output> Future for CancellableTask<Output> {
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region DetachableTask
|
||||
|
||||
/// A pinned, heap-allocated task.
|
||||
///
|
||||
/// **Why Box?** Heap allocation is required because if the task detaches,
|
||||
/// it outlives the current stack frame. `Pin<Box<_>>` ensures its memory address
|
||||
/// remains completely stable during and after the transfer.
|
||||
type BoxTask<Task> = Pin<Box<Task>>;
|
||||
|
||||
struct DetachableTaskContext<Spawner, Task> {
|
||||
spawner: Spawner,
|
||||
task: Option<BoxTask<Task>>,
|
||||
}
|
||||
type DetachableTaskGuardHelper<Context> = ContextGuard<false, Context, fn(Context)>;
|
||||
type DetachableTaskGuard<Spawner, Task> =
|
||||
DetachableTaskGuardHelper<DetachableTaskContext<Spawner, Task>>;
|
||||
|
||||
/// A task wrapper that executes inline but automatically detaches to a background spawner
|
||||
/// if the current execution context is interrupted or dropped.
|
||||
///
|
||||
/// `DetachableTask` ensures anti-cancellation. If the outer future is dropped (e.g., due to
|
||||
/// a timeout or a `select!` branch failing), the underlying unfinished task is seamlessly
|
||||
/// transferred to a background executor via an RAII guard.
|
||||
///
|
||||
/// # Advantages over `tokio::spawn` + `.await JoinHandle`
|
||||
///
|
||||
/// 1. **Zero Initial Scheduling Overhead**: Prioritizes inline execution. If the task
|
||||
/// completes before being interrupted, it entirely bypasses the runtime's scheduling queue,
|
||||
/// eliminating queuing latency and context-switching CPU costs. Spawning is strictly a fallback.
|
||||
///
|
||||
/// 2. **Context Locality**: Before detachment, the task is polled directly by the caller's thread.
|
||||
/// This implicitly preserves the current execution context, including thread-local storage (TLS),
|
||||
/// Tokio `task_local!` variables, and `tracing` spans, which would otherwise be immediately
|
||||
/// lost or require explicit propagation across task boundaries.
|
||||
pub struct DetachableTask<Spawner, Task> {
|
||||
guard: DetachableTaskGuard<Spawner, Task>,
|
||||
}
|
||||
|
||||
impl<Spawner, Task> DetachableTask<Spawner, Task> {
|
||||
pub fn detach(self) {
|
||||
self.guard.trigger()
|
||||
}
|
||||
|
||||
pub fn reclaim(self) -> BoxTask<Task> {
|
||||
self.guard.defuse().task.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub type TaskSpawner<Task, R = JoinHandle<<Task as Future>::Output>> = fn(BoxTask<Task>) -> R;
|
||||
|
||||
impl DetachableTask<fn(()), ()> {
|
||||
pub fn with_spawner<Spawner, _R, Task>(
|
||||
spawner: Spawner,
|
||||
task: Task,
|
||||
) -> DetachableTask<Spawner, Task>
|
||||
where
|
||||
Spawner: FnOnce(BoxTask<Task>) -> _R,
|
||||
{
|
||||
let context = DetachableTaskContext {
|
||||
spawner,
|
||||
task: Some(Box::pin(task)),
|
||||
};
|
||||
DetachableTask {
|
||||
guard: crate::guard!([context] if let Some(task) = context.task {
|
||||
(context.spawner)(task);
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new<Task>(task: Task) -> DetachableTask<TaskSpawner<Task>, Task>
|
||||
where
|
||||
Task: Future + Send + 'static,
|
||||
<Task as Future>::Output: Send + 'static,
|
||||
{
|
||||
Self::with_spawner(|task| tokio::runtime::Handle::current().spawn(task), task)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Spawner: FnOnce(BoxTask<Task>) -> _R, _R, Task> IntoFuture for DetachableTask<Spawner, Task>
|
||||
where
|
||||
Task: Future,
|
||||
{
|
||||
type Output = Task::Output;
|
||||
type IntoFuture = DetachableTaskFuture<Spawner, Task>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
DetachableTaskFuture { guard: self.guard }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DetachableTaskFuture<Spawner, Task> {
|
||||
guard: DetachableTaskGuard<Spawner, Task>,
|
||||
}
|
||||
|
||||
impl<Spawner: FnOnce(BoxTask<Task>) -> _R, _R, Task> Future for DetachableTaskFuture<Spawner, Task>
|
||||
where
|
||||
Task: Future,
|
||||
{
|
||||
type Output = Task::Output;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
// SAFETY:
|
||||
// 1. We only access the outer struct's unpinned fields.
|
||||
// 2. The inner task remains securely pinned on the heap via `BoxTask<Task>`.
|
||||
// 3. We never expose a mutable, unpinned reference to the underlying task.
|
||||
let this = unsafe { self.get_unchecked_mut() };
|
||||
let context = this.guard.deref_mut();
|
||||
let mut task = context.task.take().expect("polled after completion");
|
||||
let poll = task.as_mut().poll(cx);
|
||||
if poll.is_pending() {
|
||||
context.task = Some(task);
|
||||
}
|
||||
poll
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
#[tokio::test]
|
||||
async fn spawn_when_dropped() {
|
||||
let spawned = Arc::new(AtomicBool::new(false));
|
||||
{
|
||||
let spawned = spawned.clone();
|
||||
let _task = DetachableTask::new(async move {
|
||||
spawned.store(true, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
|
||||
tokio::time::timeout(Duration::from_secs(1), async {
|
||||
while !spawned.load(Ordering::SeqCst) {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("task should be spawned on drop");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn await_completed_task_does_not_detach() {
|
||||
let spawn_count = Arc::new(AtomicUsize::new(0));
|
||||
let result = {
|
||||
let spawn_count = spawn_count.clone();
|
||||
DetachableTask::with_spawner(
|
||||
move |_| {
|
||||
spawn_count.fetch_add(1, Ordering::SeqCst);
|
||||
},
|
||||
async { 7usize },
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
assert_eq!(result, 7);
|
||||
assert_eq!(spawn_count.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn drop_without_await_and_runs_once() {
|
||||
let spawn_count = Arc::new(AtomicUsize::new(0));
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
|
||||
{
|
||||
let spawn_count = spawn_count.clone();
|
||||
let _task = DetachableTask::with_spawner(
|
||||
move |f| {
|
||||
spawn_count.fetch_add(1, Ordering::SeqCst);
|
||||
tokio::spawn(async move {
|
||||
let result = f.await;
|
||||
let _ = done_tx.send(result);
|
||||
});
|
||||
},
|
||||
async { 42usize },
|
||||
);
|
||||
}
|
||||
|
||||
let detached_result = tokio::time::timeout(Duration::from_secs(1), done_rx)
|
||||
.await
|
||||
.expect("detached task should finish")
|
||||
.expect("detached task should send result");
|
||||
|
||||
assert_eq!(detached_result, 42);
|
||||
assert_eq!(spawn_count.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn drop_after_await_still_detaches() {
|
||||
let spawn_count = Arc::new(AtomicUsize::new(0));
|
||||
let (value_tx, mut value_rx) = mpsc::channel(4);
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
|
||||
let handle = {
|
||||
let future = async move {
|
||||
let mut sum = 0;
|
||||
while let Some(value) = value_rx.recv().await {
|
||||
sum += value;
|
||||
}
|
||||
sum
|
||||
};
|
||||
|
||||
let spawn_count = spawn_count.clone();
|
||||
let task = DetachableTask::with_spawner(
|
||||
move |f| {
|
||||
spawn_count.fetch_add(1, Ordering::SeqCst);
|
||||
tokio::spawn(async move {
|
||||
let result = f.await;
|
||||
let _ = done_tx.send(result);
|
||||
});
|
||||
},
|
||||
future,
|
||||
);
|
||||
|
||||
tokio::spawn(task.into_future())
|
||||
};
|
||||
|
||||
value_tx
|
||||
.send(10)
|
||||
.await
|
||||
.expect("value receiver should still exist");
|
||||
handle.abort();
|
||||
value_tx
|
||||
.send(11)
|
||||
.await
|
||||
.expect("value receiver should still exist");
|
||||
drop(value_tx);
|
||||
|
||||
let detached_result = tokio::time::timeout(Duration::from_secs(1), done_rx)
|
||||
.await
|
||||
.expect("detached polled task should finish")
|
||||
.expect("detached polled task should send result");
|
||||
|
||||
assert_eq!(detached_result, 21);
|
||||
assert_eq!(spawn_count.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn panic_during_inline_poll_does_not_detach_on_drop() {
|
||||
struct PanicOnPollFuture {
|
||||
poll_count: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl Future for PanicOnPollFuture {
|
||||
type Output = ();
|
||||
|
||||
fn poll(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
self.poll_count.fetch_add(1, Ordering::SeqCst);
|
||||
panic!("panic during inline poll")
|
||||
}
|
||||
}
|
||||
|
||||
let poll_count = Arc::new(AtomicUsize::new(0));
|
||||
let detach_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let task = {
|
||||
let detach_count = detach_count.clone();
|
||||
DetachableTask::with_spawner(
|
||||
move |_| {
|
||||
detach_count.fetch_add(1, Ordering::SeqCst);
|
||||
},
|
||||
PanicOnPollFuture {
|
||||
poll_count: poll_count.clone(),
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let err = tokio::spawn(task.into_future())
|
||||
.await
|
||||
.expect_err("inline poll panic should propagate");
|
||||
|
||||
assert!(err.is_panic());
|
||||
assert_eq!(poll_count.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(detach_count.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"private": true,
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"caniuse-lite": "1.0.30001791",
|
||||
"minimatch": "10.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+9
-277
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user