- Add #/routing-table with one-second polling and route filters - Separate auto-connect targets from local external endpoints - Remove the obsolete interfaces navigation module BREAKING CHANGE: #/interfaces is removed.
1298 lines
49 KiB
TypeScript
1298 lines
49 KiB
TypeScript
import { useState } from 'react'
|
||
import {
|
||
Settings,
|
||
Server,
|
||
Network,
|
||
Cpu,
|
||
Save,
|
||
RotateCcw,
|
||
AlertCircle,
|
||
Dice5,
|
||
Plus,
|
||
Trash2,
|
||
Layers,
|
||
} from 'lucide-react'
|
||
import {
|
||
Card,
|
||
CardContent,
|
||
CardDescription,
|
||
CardHeader,
|
||
CardTitle,
|
||
} from '@/components/ui/card'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Input } from '@/components/ui/input'
|
||
import { Textarea } from '@/components/ui/textarea'
|
||
import { Switch } from '@/components/ui/switch'
|
||
import { Slider } from '@/components/ui/slider'
|
||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||
import {
|
||
Tabs,
|
||
TabsContent,
|
||
TabsList,
|
||
TabsTrigger,
|
||
} from '@/components/ui/tabs'
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from '@/components/ui/select'
|
||
import {
|
||
Field,
|
||
FieldContent,
|
||
FieldDescription,
|
||
FieldGroup,
|
||
FieldLabel,
|
||
} from '@/components/ui/field'
|
||
import { toast } from '@/components/ui/toast'
|
||
import { useKlalbConfig } from '@/hooks/use-klalb-config'
|
||
import type { KLALBControllerConfig } from '@/types/api'
|
||
|
||
const LANGUAGE_ITEMS = [
|
||
{ value: 'ZH_CN', label: '简体中文 (ZH_CN)' },
|
||
{ value: 'EN_US', label: 'English (EN_US)' },
|
||
]
|
||
|
||
const CONGESTION_ITEMS = [
|
||
{ value: 'BBR', label: 'BBR (高吞吐低延迟,推荐)' },
|
||
{ value: 'Vegas2', label: 'Vegas2 (基于延迟的平滑算法)' },
|
||
{ value: 'DCTCP', label: 'DCTCP (数据中心低时延拥塞控制)' },
|
||
]
|
||
|
||
const PERFORMANCE_ITEMS = [
|
||
{ value: 'multifill', label: '多核按顺序填充 (multifill - 推荐通用)' },
|
||
{ value: 'multiscatter', label: '多核均匀打散 (multiscatter - 适合多路NUMA)' },
|
||
{ value: 'singlecore', label: '单核节能优先 (singlecore - 低功耗/云机)' },
|
||
]
|
||
|
||
// Random IPv6 generator compatible with KLALB format
|
||
function generateRandomIPv6(): string {
|
||
const hex = () => Math.floor(Math.random() * 0x10000).toString(16)
|
||
return `2486:${hex()}:${hex()}:${hex()}:${hex()}:${hex()}:${hex()}:${hex()}`
|
||
}
|
||
|
||
// Helper to safely extract address string whether it's a string or an object with host/port
|
||
function toAddrString(item: unknown): string {
|
||
if (!item) return ''
|
||
if (typeof item === 'string') return item
|
||
if (typeof item === 'object' && item !== null) {
|
||
const obj = item as Record<string, unknown>
|
||
if (obj.host && obj.port) {
|
||
const proto = obj.protocol ? `${obj.protocol}://` : ''
|
||
return `${proto}${obj.host}:${obj.port}`
|
||
}
|
||
if (obj.name && typeof obj.name === 'string') return obj.name
|
||
if (obj.address && typeof obj.address === 'string') return obj.address
|
||
}
|
||
return String(item)
|
||
}
|
||
|
||
function SettingsForm({
|
||
initialConfig,
|
||
onSave,
|
||
onReset,
|
||
isSaving,
|
||
}: {
|
||
initialConfig: KLALBControllerConfig
|
||
onSave: (config: KLALBControllerConfig) => Promise<void>
|
||
onReset: () => void
|
||
isSaving: boolean
|
||
}) {
|
||
const [form, setForm] = useState<KLALBControllerConfig>(() => ({
|
||
...initialConfig,
|
||
TCPListen: toAddrString(initialConfig.TCPListen),
|
||
UDPListen: toAddrString(initialConfig.UDPListen),
|
||
webListen:
|
||
initialConfig.webListen ??
|
||
(initialConfig.webPort ? `http://0.0.0.0:${initialConfig.webPort}` : 'http://0.0.0.0:4665'),
|
||
}))
|
||
const [tunEnabled, setTunEnabled] = useState<boolean>(() =>
|
||
initialConfig.enableTUN ??
|
||
(initialConfig.TUNName !== null &&
|
||
initialConfig.TUNName !== undefined &&
|
||
initialConfig.TUNName.trim() !== '')
|
||
)
|
||
|
||
const [autoConnections, setAutoConnections] = useState<string[]>(() =>
|
||
(
|
||
Array.isArray(initialConfig.autoConnections)
|
||
? initialConfig.autoConnections
|
||
: Array.isArray(initialConfig.ConnectLineTable)
|
||
? initialConfig.ConnectLineTable
|
||
: []
|
||
).map(toAddrString)
|
||
)
|
||
|
||
const [externalEndpoints, setExternalEndpoints] = useState<string[]>(() =>
|
||
(
|
||
Array.isArray(initialConfig.externalEndpoints)
|
||
? initialConfig.externalEndpoints
|
||
: Array.isArray(initialConfig.openConnections)
|
||
? initialConfig.openConnections
|
||
: Array.isArray(initialConfig.LineTable)
|
||
? initialConfig.LineTable
|
||
: []
|
||
).map(toAddrString)
|
||
)
|
||
|
||
const [ntpServers, setNtpServers] = useState<string[]>(() =>
|
||
(
|
||
Array.isArray(initialConfig.ntpServers)
|
||
? initialConfig.ntpServers
|
||
: Array.isArray(initialConfig.ntpServerTable)
|
||
? initialConfig.ntpServerTable
|
||
: []
|
||
).map(toAddrString)
|
||
)
|
||
|
||
const [dnsList, setDnsList] = useState<string[]>(() =>
|
||
(Array.isArray(initialConfig.DNS) ? initialConfig.DNS : []).map(toAddrString)
|
||
)
|
||
|
||
const [extraRoutes, setExtraRoutes] = useState<string[]>(() =>
|
||
(Array.isArray(initialConfig.ExtraRoutes) ? initialConfig.ExtraRoutes : []).map(toAddrString)
|
||
)
|
||
|
||
const [interfaceExcepts, setInterfaceExcepts] = useState<string[]>(() =>
|
||
(
|
||
Array.isArray(initialConfig.NetworkInterfaceExcepts)
|
||
? initialConfig.NetworkInterfaceExcepts
|
||
: []
|
||
).map(toAddrString)
|
||
)
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
|
||
const updatedAutoConnections = autoConnections
|
||
.filter((address) => address.trim() !== '')
|
||
.map((address) => address.trim())
|
||
const updatedExternalEndpoints = externalEndpoints
|
||
.filter((address) => address.trim() !== '')
|
||
.map((address) => address.trim())
|
||
|
||
// 保存时同时携带新键名与历史别名,兼容新旧后端
|
||
const denyQuery =
|
||
form.denyExternalEndpointQuery ?? form.denyConnectionQuery ?? form.denyLineTableQuery ?? false
|
||
const denyBroadcast =
|
||
form.denyExternalEndpointBroadcast ??
|
||
form.denyConnectionBroadcast ??
|
||
form.denyLineTableBroadcast ??
|
||
false
|
||
const webListen = form.webListen?.trim() || 'http://0.0.0.0:4665'
|
||
let webPort = 4665
|
||
try {
|
||
const parsedPort = new URL(webListen).port
|
||
if (parsedPort) webPort = parseInt(parsedPort, 10)
|
||
} catch {
|
||
// Java backend validates the canonical listen address.
|
||
}
|
||
|
||
const updatedConfig: Record<string, unknown> = {
|
||
...form,
|
||
webListen,
|
||
webPort,
|
||
externalEndpoints: updatedExternalEndpoints,
|
||
openConnections: updatedExternalEndpoints,
|
||
autoConnections: updatedAutoConnections,
|
||
LineTable: updatedExternalEndpoints,
|
||
ConnectLineTable: updatedAutoConnections,
|
||
ntpServers: ntpServers.filter((s) => s.trim() !== ''),
|
||
ntpServerTable: ntpServers.filter((s) => s.trim() !== ''),
|
||
DNS: dnsList.filter((s) => s.trim() !== ''),
|
||
ExtraRoutes: extraRoutes.filter((s) => s.trim() !== ''),
|
||
NetworkInterfaceExcepts: interfaceExcepts.filter((s) => s.trim() !== ''),
|
||
enableTUN: tunEnabled,
|
||
TUNName: form.TUNName?.trim() || 'KLALB_SRv6',
|
||
denyExternalEndpointQuery: denyQuery,
|
||
denyExternalEndpointBroadcast: denyBroadcast,
|
||
denyConnectionQuery: denyQuery,
|
||
denyConnectionBroadcast: denyBroadcast,
|
||
Type: 'KLALBController',
|
||
}
|
||
|
||
await onSave(updatedConfig as KLALBControllerConfig)
|
||
}
|
||
|
||
// --- Generic Dynamic List Handlers ---
|
||
const addListItem = (setter: React.Dispatch<React.SetStateAction<string[]>>) => {
|
||
setter((prev) => [...prev, ''])
|
||
}
|
||
|
||
const updateListItem = (
|
||
index: number,
|
||
value: string,
|
||
setter: React.Dispatch<React.SetStateAction<string[]>>
|
||
) => {
|
||
setter((prev) => {
|
||
const next = [...prev]
|
||
next[index] = value
|
||
return next
|
||
})
|
||
}
|
||
|
||
const removeListItem = (
|
||
index: number,
|
||
setter: React.Dispatch<React.SetStateAction<string[]>>
|
||
) => {
|
||
setter((prev) => prev.filter((_, i) => i !== index))
|
||
}
|
||
|
||
return (
|
||
<form onSubmit={handleSubmit} className="flex flex-1 flex-col gap-6 p-6">
|
||
{/* Header Banner */}
|
||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||
<div>
|
||
<h1 className="text-xl font-bold tracking-tight">系统配置</h1>
|
||
<p className="text-xs text-muted-foreground">
|
||
管理 KLALB SRv6 路由控制核心、网络寻址、连接与性能调度参数
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={onReset}
|
||
disabled={isSaving}
|
||
>
|
||
<RotateCcw data-icon="inline-start" />
|
||
重置
|
||
</Button>
|
||
|
||
<Button type="submit" size="sm" disabled={isSaving}>
|
||
<Save data-icon="inline-start" />
|
||
{isSaving ? '正在保存...' : '保存配置'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Main Tabs Container */}
|
||
<Tabs defaultValue="general" className="flex flex-col gap-6">
|
||
<TabsList className="w-full justify-start border-b rounded-none bg-transparent p-0 h-auto">
|
||
<TabsTrigger
|
||
value="general"
|
||
className="rounded-none border-b-2 border-transparent py-2.5 px-4 font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
||
>
|
||
<Server className="size-4 mr-2" />
|
||
常规与设备
|
||
</TabsTrigger>
|
||
<TabsTrigger
|
||
value="network"
|
||
className="rounded-none border-b-2 border-transparent py-2.5 px-4 font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
||
>
|
||
<Network className="size-4 mr-2" />
|
||
网络与寻址
|
||
</TabsTrigger>
|
||
<TabsTrigger
|
||
value="lists"
|
||
className="rounded-none border-b-2 border-transparent py-2.5 px-4 font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
||
>
|
||
<Layers className="size-4 mr-2" />
|
||
连接与同步列表
|
||
</TabsTrigger>
|
||
<TabsTrigger
|
||
value="performance"
|
||
className="rounded-none border-b-2 border-transparent py-2.5 px-4 font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
|
||
>
|
||
<Cpu className="size-4 mr-2" />
|
||
性能与流控
|
||
</TabsTrigger>
|
||
</TabsList>
|
||
|
||
{/* Tab 1: 常规与设备 (General) */}
|
||
<TabsContent value="general" className="flex flex-col gap-6 m-0">
|
||
<Card className="shadow-2xs">
|
||
<CardHeader>
|
||
<CardTitle className="text-base">节点基本身份</CardTitle>
|
||
<CardDescription>
|
||
设置该节点在 KLALB 全网拓扑中呈现的设备标识与语言偏好
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<FieldGroup className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||
<Field>
|
||
<FieldLabel htmlFor="deviceName">设备名称</FieldLabel>
|
||
<Input
|
||
id="deviceName"
|
||
placeholder="如: Edge-Router-01"
|
||
value={form.DeviceName || ''}
|
||
onChange={(e) =>
|
||
setForm((prev) => ({ ...prev, DeviceName: e.target.value }))
|
||
}
|
||
/>
|
||
<FieldDescription>全网路由广播与拓扑图中显示的节点名称</FieldDescription>
|
||
</Field>
|
||
|
||
<Field>
|
||
<FieldLabel htmlFor="language">系统语言</FieldLabel>
|
||
<Select
|
||
items={LANGUAGE_ITEMS}
|
||
value={form.language || 'ZH_CN'}
|
||
onValueChange={(val) =>
|
||
setForm((prev) => ({ ...prev, language: val || undefined }))
|
||
}
|
||
>
|
||
<SelectTrigger id="language">
|
||
<SelectValue placeholder="选择语言" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{LANGUAGE_ITEMS.map((item) => (
|
||
<SelectItem key={item.value} value={item.value}>
|
||
{item.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<FieldDescription>本地界面与日志消息的默认语言</FieldDescription>
|
||
</Field>
|
||
|
||
<Field className="md:col-span-2">
|
||
<FieldLabel htmlFor="deviceDescription">设备描述</FieldLabel>
|
||
<Textarea
|
||
id="deviceDescription"
|
||
placeholder={'如: 机房 A 区多线聚合网关\n支持多行输入'}
|
||
className="min-h-20"
|
||
value={form.DeviceDescription || ''}
|
||
onChange={(e) =>
|
||
setForm((prev) => ({ ...prev, DeviceDescription: e.target.value }))
|
||
}
|
||
/>
|
||
<FieldDescription>本地备注说明信息,支持多行,不参与网络路由决策</FieldDescription>
|
||
</Field>
|
||
</FieldGroup>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="shadow-2xs">
|
||
<CardHeader>
|
||
<CardTitle className="text-base">服务与虚拟网卡</CardTitle>
|
||
<CardDescription>
|
||
配置 TUN 虚拟网络适配器及 Web API 服务的运行参数
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<FieldGroup className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||
<Field
|
||
orientation="horizontal"
|
||
className="flex items-center justify-between rounded-lg border p-4 md:col-span-2"
|
||
>
|
||
<FieldContent>
|
||
<FieldLabel htmlFor="switch-tun-enable">
|
||
启用 TUN 虚拟网卡
|
||
</FieldLabel>
|
||
<FieldDescription>
|
||
开启后创建系统级虚拟网卡,将虚拟 IPv6/SRv6 流量注入系统内核协议栈
|
||
</FieldDescription>
|
||
</FieldContent>
|
||
<Switch
|
||
id="switch-tun-enable"
|
||
checked={tunEnabled}
|
||
onCheckedChange={(checked) => {
|
||
setTunEnabled(checked)
|
||
if (
|
||
checked &&
|
||
(!form.TUNName || form.TUNName.trim() === '')
|
||
) {
|
||
setForm((prev) => ({ ...prev, TUNName: 'KLALB_SRv6' }))
|
||
}
|
||
}}
|
||
/>
|
||
</Field>
|
||
|
||
{!tunEnabled && (
|
||
<div className="md:col-span-2">
|
||
<Alert className="border-amber-200 bg-amber-50 text-amber-900 shadow-2xs dark:border-amber-900 dark:bg-amber-950 dark:text-amber-50">
|
||
<AlertCircle className="size-4 text-amber-600 dark:text-amber-400" />
|
||
<AlertTitle className="font-semibold">
|
||
TUN 虚拟网卡已禁用
|
||
</AlertTitle>
|
||
<AlertDescription className="text-amber-800 dark:text-amber-200">
|
||
系统将以无 TUN 适配器模式运行,仅支持链路转发与端口桥接,本端虚拟 IPv6 地址将无法被操作系统内核直接路由访问。
|
||
</AlertDescription>
|
||
</Alert>
|
||
</div>
|
||
)}
|
||
|
||
{tunEnabled && (
|
||
<Field className="md:col-span-2">
|
||
<FieldLabel htmlFor="tunName">
|
||
TUN 虚拟网卡设备名
|
||
</FieldLabel>
|
||
<Input
|
||
id="tunName"
|
||
placeholder="默认: KLALB_SRv6"
|
||
value={form.TUNName || ''}
|
||
onChange={(e) =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
TUNName: e.target.value,
|
||
}))
|
||
}
|
||
/>
|
||
<FieldDescription>
|
||
创建的虚拟网卡适配器名称(如 Windows 上的 Wintun 适配器名,默认 KLALB_SRv6)
|
||
</FieldDescription>
|
||
</Field>
|
||
)}
|
||
|
||
<Field>
|
||
<FieldLabel htmlFor="webListen">Web API 监听地址</FieldLabel>
|
||
<Input
|
||
id="webListen"
|
||
type="text"
|
||
placeholder="http://0.0.0.0:4665"
|
||
value={form.webListen ?? 'http://0.0.0.0:4665'}
|
||
onChange={(e) =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
webListen: e.target.value,
|
||
}))
|
||
}
|
||
/>
|
||
<FieldDescription>
|
||
Web API 与前端仪表盘托管监听地址(默认 http://0.0.0.0:4665)
|
||
</FieldDescription>
|
||
</Field>
|
||
|
||
<Field
|
||
orientation="horizontal"
|
||
className="flex items-center justify-between rounded-lg border p-4"
|
||
>
|
||
<FieldContent>
|
||
<FieldLabel htmlFor="switch-nogui">无 GUI 模式</FieldLabel>
|
||
<FieldDescription>
|
||
启动时跳过 Java Swing 图形窗口,仅通过控制台与 Web Dashboard 运行
|
||
</FieldDescription>
|
||
</FieldContent>
|
||
<Switch
|
||
id="switch-nogui"
|
||
checked={form.nogui ?? false}
|
||
onCheckedChange={(checked) =>
|
||
setForm((prev) => ({ ...prev, nogui: checked }))
|
||
}
|
||
/>
|
||
</Field>
|
||
</FieldGroup>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
|
||
{/* Tab 2: 网络与寻址 (Network) */}
|
||
<TabsContent value="network" className="m-0 flex flex-col gap-6">
|
||
<Card className="shadow-2xs">
|
||
<CardHeader>
|
||
<CardTitle className="text-base">SRv6 虚拟网络寻址</CardTitle>
|
||
<CardDescription>
|
||
配置本节点在虚拟 SRv6 网络中的专属 IPv6 虚拟地址 (SID) 及 AS 自治域号
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<FieldGroup className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||
<Field className="md:col-span-2">
|
||
<FieldLabel htmlFor="virtualAddress">
|
||
本节点虚拟 IPv6 地址
|
||
</FieldLabel>
|
||
<div className="flex items-center gap-2">
|
||
<Input
|
||
id="virtualAddress"
|
||
className="font-mono"
|
||
placeholder="如: 2486:1:0:0:0:0:0:8889"
|
||
value={form.VirtualAddress || ''}
|
||
onChange={(e) =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
VirtualAddress: e.target.value,
|
||
}))
|
||
}
|
||
/>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={() =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
VirtualAddress: generateRandomIPv6(),
|
||
}))
|
||
}
|
||
title="随机生成私有 SRv6 专用地址"
|
||
>
|
||
<Dice5 data-icon="inline-start" />
|
||
随机生成
|
||
</Button>
|
||
</div>
|
||
<FieldDescription>
|
||
本端 SRv6 报文路由目的地址(/128 主机路由)。
|
||
</FieldDescription>
|
||
</Field>
|
||
|
||
<Field>
|
||
<FieldLabel htmlFor="virtualASN">
|
||
虚拟自治系统编号
|
||
</FieldLabel>
|
||
<Input
|
||
id="virtualASN"
|
||
type="number"
|
||
placeholder="如: 2142606939373348329"
|
||
value={form.VirtualASN ?? ''}
|
||
onChange={(e) =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
VirtualASN: e.target.value
|
||
? parseInt(e.target.value)
|
||
: undefined,
|
||
}))
|
||
}
|
||
/>
|
||
<FieldDescription>
|
||
虚拟网络 BGP/SRv6 域 AS 编号
|
||
</FieldDescription>
|
||
</Field>
|
||
|
||
<Field>
|
||
<FieldLabel htmlFor="virtualSocketName">
|
||
虚拟套接字协议
|
||
</FieldLabel>
|
||
<Input
|
||
id="virtualSocketName"
|
||
placeholder="kltp"
|
||
value={form.VirtualSocketName || 'kltp'}
|
||
onChange={(e) =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
VirtualSocketName: e.target.value,
|
||
}))
|
||
}
|
||
/>
|
||
<FieldDescription>
|
||
用于虚拟数据包传输的可靠协议栈名称(默认 kltp)
|
||
</FieldDescription>
|
||
</Field>
|
||
</FieldGroup>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="shadow-2xs">
|
||
<CardHeader>
|
||
<CardTitle className="text-base">本地监听端点</CardTitle>
|
||
<CardDescription>
|
||
配置节点在物理网络上用于接收其他节点连接的 TCP / UDP 监听 URI
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<FieldGroup className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||
<Field>
|
||
<FieldLabel htmlFor="tcpListen">TCP 监听地址</FieldLabel>
|
||
<Input
|
||
id="tcpListen"
|
||
className="font-mono text-xs"
|
||
placeholder="tcp://0.0.0.0:4565"
|
||
value={form.TCPListen || ''}
|
||
onChange={(e) =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
TCPListen: e.target.value,
|
||
}))
|
||
}
|
||
/>
|
||
<FieldDescription>
|
||
接收外部 TCP 链路握手的监听 URI
|
||
</FieldDescription>
|
||
</Field>
|
||
|
||
<Field>
|
||
<FieldLabel htmlFor="udpListen">UDP 监听地址</FieldLabel>
|
||
<Input
|
||
id="udpListen"
|
||
className="font-mono text-xs"
|
||
placeholder="udp://0.0.0.0:4572"
|
||
value={form.UDPListen || ''}
|
||
onChange={(e) =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
UDPListen: e.target.value,
|
||
}))
|
||
}
|
||
/>
|
||
<FieldDescription>
|
||
接收外部 UDP/KLTP 链路报文的监听 URI
|
||
</FieldDescription>
|
||
</Field>
|
||
</FieldGroup>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
|
||
{/* Tab 3: 连接与同步列表 (Lists) */}
|
||
<TabsContent value="lists" className="m-0 flex flex-col gap-6">
|
||
{/* 1. Automatic Connection Endpoints */}
|
||
<Card className="shadow-2xs">
|
||
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
||
<div>
|
||
<CardTitle className="text-base">与以下端点自动建立连接</CardTitle>
|
||
<CardDescription>本节点启动时主动连接的远端端点</CardDescription>
|
||
</div>
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => addListItem(setAutoConnections)}
|
||
>
|
||
<Plus data-icon="inline-start" />
|
||
添加端点
|
||
</Button>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<FieldGroup className="gap-2">
|
||
{autoConnections.length === 0 ? (
|
||
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
|
||
未配置自动连接端点
|
||
</div>
|
||
) : (
|
||
autoConnections.map((endpoint, idx) => (
|
||
<div key={idx} className="flex items-center gap-2">
|
||
<Input
|
||
className="flex-1 font-mono text-xs"
|
||
placeholder="tcp://kne01.yoyo250.fun:4565 或 udp://..."
|
||
value={endpoint}
|
||
onChange={(e) =>
|
||
updateListItem(idx, e.target.value, setAutoConnections)
|
||
}
|
||
/>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon-xs"
|
||
className="text-destructive hover:text-destructive"
|
||
onClick={() => removeListItem(idx, setAutoConnections)}
|
||
title="删除该端点"
|
||
>
|
||
<Trash2 className="size-3.5" />
|
||
</Button>
|
||
</div>
|
||
))
|
||
)}
|
||
</FieldGroup>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 2. Published External Endpoints */}
|
||
<Card className="shadow-2xs">
|
||
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
||
<div>
|
||
<CardTitle className="text-base">本机外部端点</CardTitle>
|
||
<CardDescription>
|
||
本节点对外发布且可被其他节点访问的端点,不是远端连接目标
|
||
</CardDescription>
|
||
</div>
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => addListItem(setExternalEndpoints)}
|
||
>
|
||
<Plus data-icon="inline-start" />
|
||
添加端点
|
||
</Button>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<FieldGroup className="gap-2">
|
||
{externalEndpoints.length === 0 ? (
|
||
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
|
||
未配置本机外部端点
|
||
</div>
|
||
) : (
|
||
externalEndpoints.map((endpoint, idx) => (
|
||
<div key={idx} className="flex items-center gap-2">
|
||
<Input
|
||
className="flex-1 font-mono text-xs"
|
||
placeholder="tcp://本机公网地址:4565 或 udp://..."
|
||
value={endpoint}
|
||
onChange={(e) =>
|
||
updateListItem(idx, e.target.value, setExternalEndpoints)
|
||
}
|
||
/>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon-xs"
|
||
className="text-destructive hover:text-destructive"
|
||
onClick={() => removeListItem(idx, setExternalEndpoints)}
|
||
title="删除该端点"
|
||
>
|
||
<Trash2 className="size-3.5" />
|
||
</Button>
|
||
</div>
|
||
))
|
||
)}
|
||
</FieldGroup>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 3. NTP Server List */}
|
||
<Card className="shadow-2xs">
|
||
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
||
<div>
|
||
<CardTitle className="text-base">
|
||
NTP 授时服务器列表
|
||
</CardTitle>
|
||
<CardDescription>
|
||
高精度分布式时钟同步参考源列表 (如 ntp://ntp1.aliyun.com)
|
||
</CardDescription>
|
||
</div>
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => addListItem(setNtpServers)}
|
||
>
|
||
<Plus data-icon="inline-start" />
|
||
添加 NTP
|
||
</Button>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<FieldGroup className="gap-2">
|
||
{ntpServers.length === 0 ? (
|
||
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
|
||
未配置 NTP 服务器,系统将使用本机系统时间
|
||
</div>
|
||
) : (
|
||
ntpServers.map((server, idx) => (
|
||
<div key={idx} className="flex items-center gap-2">
|
||
<Input
|
||
className="flex-1 font-mono text-xs"
|
||
placeholder="ntp://ntp1.aliyun.com"
|
||
value={server}
|
||
onChange={(e) =>
|
||
updateListItem(idx, e.target.value, setNtpServers)
|
||
}
|
||
/>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon-xs"
|
||
className="text-destructive hover:text-destructive"
|
||
onClick={() => removeListItem(idx, setNtpServers)}
|
||
>
|
||
<Trash2 className="size-3.5" />
|
||
</Button>
|
||
</div>
|
||
))
|
||
)}
|
||
</FieldGroup>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 3. DNS, Extra Routes & Network Interface Exceptions in 3 Columns / Grid */}
|
||
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||
{/* DNS List */}
|
||
<Card className="shadow-2xs">
|
||
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
||
<div>
|
||
<CardTitle className="text-base">
|
||
DNS 服务器列表
|
||
</CardTitle>
|
||
<CardDescription>
|
||
虚拟网络递归 DNS 解析服务器
|
||
</CardDescription>
|
||
</div>
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => addListItem(setDnsList)}
|
||
>
|
||
<Plus data-icon="inline-start" />
|
||
添加
|
||
</Button>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<FieldGroup className="gap-2">
|
||
{dnsList.length === 0 ? (
|
||
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
|
||
未配置专用 DNS 服务器
|
||
</div>
|
||
) : (
|
||
dnsList.map((dns, idx) => (
|
||
<div key={idx} className="flex items-center gap-2">
|
||
<Input
|
||
className="flex-1 font-mono text-xs"
|
||
placeholder="2486:1::8888 或 8.8.8.8"
|
||
value={dns}
|
||
onChange={(e) =>
|
||
updateListItem(idx, e.target.value, setDnsList)
|
||
}
|
||
/>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon-xs"
|
||
className="text-destructive hover:text-destructive"
|
||
onClick={() => removeListItem(idx, setDnsList)}
|
||
>
|
||
<Trash2 className="size-3.5" />
|
||
</Button>
|
||
</div>
|
||
))
|
||
)}
|
||
</FieldGroup>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Extra Routes */}
|
||
<Card className="shadow-2xs">
|
||
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
||
<div>
|
||
<CardTitle className="text-base">
|
||
额外路由列表
|
||
</CardTitle>
|
||
<CardDescription>
|
||
本地控制器附加静态路由规则
|
||
</CardDescription>
|
||
</div>
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => addListItem(setExtraRoutes)}
|
||
>
|
||
<Plus data-icon="inline-start" />
|
||
添加
|
||
</Button>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<FieldGroup className="gap-2">
|
||
{extraRoutes.length === 0 ? (
|
||
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
|
||
未配置额外静态路由
|
||
</div>
|
||
) : (
|
||
extraRoutes.map((route, idx) => (
|
||
<div key={idx} className="flex items-center gap-2">
|
||
<Input
|
||
className="flex-1 font-mono text-xs"
|
||
placeholder="如: 2486:2::/64"
|
||
value={route}
|
||
onChange={(e) =>
|
||
updateListItem(idx, e.target.value, setExtraRoutes)
|
||
}
|
||
/>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon-xs"
|
||
className="text-destructive hover:text-destructive"
|
||
onClick={() => removeListItem(idx, setExtraRoutes)}
|
||
>
|
||
<Trash2 className="size-3.5" />
|
||
</Button>
|
||
</div>
|
||
))
|
||
)}
|
||
</FieldGroup>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Network Interface Exceptions */}
|
||
<Card className="shadow-2xs">
|
||
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
||
<div>
|
||
<CardTitle className="text-base">
|
||
网卡排除列表
|
||
</CardTitle>
|
||
<CardDescription>
|
||
指定不绑定的物理网卡设备名
|
||
</CardDescription>
|
||
</div>
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => addListItem(setInterfaceExcepts)}
|
||
>
|
||
<Plus data-icon="inline-start" />
|
||
添加
|
||
</Button>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<FieldGroup className="gap-2">
|
||
{interfaceExcepts.length === 0 ? (
|
||
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
|
||
未配置排除项,系统将使用全部可用物理接口
|
||
</div>
|
||
) : (
|
||
interfaceExcepts.map((iface, idx) => (
|
||
<div key={idx} className="flex items-center gap-2">
|
||
<Input
|
||
className="flex-1 font-mono text-xs"
|
||
placeholder="如: eth0 或 WLAN"
|
||
value={iface}
|
||
onChange={(e) =>
|
||
updateListItem(
|
||
idx,
|
||
e.target.value,
|
||
setInterfaceExcepts
|
||
)
|
||
}
|
||
/>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon-xs"
|
||
className="text-destructive hover:text-destructive"
|
||
onClick={() =>
|
||
removeListItem(idx, setInterfaceExcepts)
|
||
}
|
||
>
|
||
<Trash2 className="size-3.5" />
|
||
</Button>
|
||
</div>
|
||
))
|
||
)}
|
||
</FieldGroup>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* Security & Broadcast Flags */}
|
||
<Card className="shadow-2xs">
|
||
<CardHeader>
|
||
<CardTitle className="text-base">安全与广播开关</CardTitle>
|
||
<CardDescription>
|
||
控制连接信息的外部探测查询与全网广播行为
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<FieldGroup className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||
<Field
|
||
orientation="horizontal"
|
||
className="flex items-center justify-between rounded-lg border p-3"
|
||
>
|
||
<FieldContent>
|
||
<FieldLabel htmlFor="switch-deny-query">禁用地址查询</FieldLabel>
|
||
<FieldDescription>
|
||
禁止远端节点主动探测查询本机的连接信息
|
||
</FieldDescription>
|
||
</FieldContent>
|
||
<Switch
|
||
id="switch-deny-query"
|
||
checked={
|
||
form.denyExternalEndpointQuery ??
|
||
form.denyConnectionQuery ??
|
||
form.denyLineTableQuery ??
|
||
false
|
||
}
|
||
onCheckedChange={(checked) =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
denyExternalEndpointQuery: checked,
|
||
denyConnectionQuery: checked,
|
||
denyLineTableQuery: checked,
|
||
}))
|
||
}
|
||
/>
|
||
</Field>
|
||
|
||
<Field
|
||
orientation="horizontal"
|
||
className="flex items-center justify-between rounded-lg border p-3"
|
||
>
|
||
<FieldContent>
|
||
<FieldLabel htmlFor="switch-deny-broadcast">禁用地址广播</FieldLabel>
|
||
<FieldDescription>
|
||
禁止向全网周期性广播本机的连接信息
|
||
</FieldDescription>
|
||
</FieldContent>
|
||
<Switch
|
||
id="switch-deny-broadcast"
|
||
checked={
|
||
form.denyExternalEndpointBroadcast ??
|
||
form.denyConnectionBroadcast ??
|
||
form.denyLineTableBroadcast ??
|
||
false
|
||
}
|
||
onCheckedChange={(checked) =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
denyExternalEndpointBroadcast: checked,
|
||
denyConnectionBroadcast: checked,
|
||
denyLineTableBroadcast: checked,
|
||
}))
|
||
}
|
||
/>
|
||
</Field>
|
||
</FieldGroup>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
|
||
{/* Tab 4: 性能与流控 (Performance) */}
|
||
<TabsContent value="performance" className="m-0 flex flex-col gap-6">
|
||
<Card className="shadow-2xs">
|
||
<CardHeader>
|
||
<CardTitle className="text-base">算法与调度策略</CardTitle>
|
||
<CardDescription>
|
||
设置传输层拥塞控制算法及多核 CPU 负载均衡策略
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<FieldGroup className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||
<Field>
|
||
<FieldLabel htmlFor="congestion">拥塞控制算法</FieldLabel>
|
||
<Select
|
||
items={CONGESTION_ITEMS}
|
||
value={form.congestionAlgorithm || 'BBR'}
|
||
onValueChange={(val) =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
congestionAlgorithm: val || undefined,
|
||
}))
|
||
}
|
||
>
|
||
<SelectTrigger id="congestion">
|
||
<SelectValue placeholder="选择算法" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{CONGESTION_ITEMS.map((item) => (
|
||
<SelectItem key={item.value} value={item.value}>
|
||
{item.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<FieldDescription>
|
||
WAN 链路报文传输的流量与拥塞控制引擎
|
||
</FieldDescription>
|
||
</Field>
|
||
|
||
<Field>
|
||
<FieldLabel htmlFor="perfStrategy">
|
||
性能调度策略
|
||
</FieldLabel>
|
||
<Select
|
||
items={PERFORMANCE_ITEMS}
|
||
value={form.performanceStrategy || 'multifill'}
|
||
onValueChange={(val) =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
performanceStrategy: val || undefined,
|
||
}))
|
||
}
|
||
>
|
||
<SelectTrigger id="perfStrategy">
|
||
<SelectValue placeholder="选择策略" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{PERFORMANCE_ITEMS.map((item) => (
|
||
<SelectItem key={item.value} value={item.value}>
|
||
{item.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<FieldDescription>
|
||
多核 CPU 上的转发线程负载分配模型
|
||
</FieldDescription>
|
||
</Field>
|
||
|
||
<Field>
|
||
<FieldLabel htmlFor="linkConns">
|
||
单链路连接流数
|
||
</FieldLabel>
|
||
<Input
|
||
id="linkConns"
|
||
type="number"
|
||
min={1}
|
||
max={10}
|
||
value={form.linkConnectionsCount ?? 1}
|
||
onChange={(e) =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
linkConnectionsCount: parseInt(e.target.value) || 1,
|
||
}))
|
||
}
|
||
/>
|
||
<FieldDescription>
|
||
单个远端物理节点建立的底层并发 TCP 流数 (1~10)
|
||
</FieldDescription>
|
||
</Field>
|
||
</FieldGroup>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="shadow-2xs">
|
||
<CardHeader>
|
||
<CardTitle className="text-base">
|
||
网络时延门限与微调参数
|
||
</CardTitle>
|
||
<CardDescription>
|
||
调整动态突发流量阈值及 Nagle 延迟合并包参数
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<FieldGroup className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||
<Field>
|
||
<div className="flex items-center justify-between">
|
||
<FieldLabel htmlFor="burstLimit">
|
||
突发限制倍率
|
||
</FieldLabel>
|
||
<span className="font-mono text-xs font-semibold">
|
||
{(form.burstLimit ?? 1.5).toFixed(2)}x
|
||
</span>
|
||
</div>
|
||
<Slider
|
||
id="burstLimit"
|
||
min={1}
|
||
max={2}
|
||
step={0.05}
|
||
value={[form.burstLimit ?? 1.5]}
|
||
onValueChange={(vals) => {
|
||
const v = Array.isArray(vals) ? vals[0] : vals
|
||
setForm((prev) => ({
|
||
...prev,
|
||
burstLimit: typeof v === 'number' ? v : 1.5,
|
||
}))
|
||
}}
|
||
/>
|
||
<FieldDescription>
|
||
允许瞬时突发带宽相对于平稳速率的倍数上限 (1.0~2.0)
|
||
</FieldDescription>
|
||
</Field>
|
||
|
||
<Field>
|
||
<div className="flex items-center justify-between">
|
||
<FieldLabel htmlFor="delayUpper">
|
||
时延门限上限
|
||
</FieldLabel>
|
||
<span className="font-mono text-xs font-semibold">
|
||
{(form.delayUpperBound ?? 1.2).toFixed(2)}x
|
||
</span>
|
||
</div>
|
||
<Slider
|
||
id="delayUpper"
|
||
min={1}
|
||
max={3}
|
||
step={0.05}
|
||
value={[form.delayUpperBound ?? 1.2]}
|
||
onValueChange={(vals) => {
|
||
const v = Array.isArray(vals) ? vals[0] : vals
|
||
setForm((prev) => ({
|
||
...prev,
|
||
delayUpperBound: typeof v === 'number' ? v : 1.2,
|
||
}))
|
||
}}
|
||
/>
|
||
<FieldDescription>
|
||
判定链路发生拥塞的时延相对膨胀上限倍率
|
||
</FieldDescription>
|
||
</Field>
|
||
|
||
<Field>
|
||
<FieldLabel htmlFor="nagleDelay">
|
||
传输粘包等待时间
|
||
</FieldLabel>
|
||
<Input
|
||
id="nagleDelay"
|
||
type="number"
|
||
min={0}
|
||
placeholder="1000000"
|
||
value={form.nagleDelayTime ?? 1000000}
|
||
onChange={(e) =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
nagleDelayTime: parseInt(e.target.value) || 0,
|
||
}))
|
||
}
|
||
/>
|
||
<FieldDescription>
|
||
小包合并发送等待超时 (单位: 纳秒 ns,1000000ns = 1ms)
|
||
</FieldDescription>
|
||
</Field>
|
||
|
||
<Field>
|
||
<FieldLabel htmlFor="linkNagleDelay">
|
||
链路粘包等待时间
|
||
</FieldLabel>
|
||
<Input
|
||
id="linkNagleDelay"
|
||
type="number"
|
||
min={0}
|
||
placeholder="0"
|
||
value={form.linkNagleDelayTime ?? 0}
|
||
onChange={(e) =>
|
||
setForm((prev) => ({
|
||
...prev,
|
||
linkNagleDelayTime: parseInt(e.target.value) || 0,
|
||
}))
|
||
}
|
||
/>
|
||
<FieldDescription>
|
||
物理链路发送层的聚合超时 (单位: 纳秒 ns,0 为禁用)
|
||
</FieldDescription>
|
||
</Field>
|
||
</FieldGroup>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
</Tabs>
|
||
</form>
|
||
)
|
||
}
|
||
|
||
export function SettingsPage() {
|
||
const {
|
||
config,
|
||
isLoading,
|
||
isSaving,
|
||
fetchConfig,
|
||
saveConfig,
|
||
} = useKlalbConfig()
|
||
|
||
const [versionKey, setVersionKey] = useState(0)
|
||
|
||
const handleReset = () => {
|
||
fetchConfig()
|
||
setVersionKey((prev) => prev + 1)
|
||
}
|
||
|
||
const handleSave = async (updatedConfig: KLALBControllerConfig) => {
|
||
const res = await saveConfig(updatedConfig)
|
||
if (res.success) {
|
||
toast.add({
|
||
title: '配置已保存',
|
||
type: 'success',
|
||
})
|
||
setVersionKey((prev) => prev + 1)
|
||
} else {
|
||
toast.add({
|
||
title: '保存配置失败',
|
||
description: res.message || '请检查配置参数或后端服务状态',
|
||
type: 'error',
|
||
})
|
||
}
|
||
}
|
||
|
||
if (isLoading || !config) {
|
||
return (
|
||
<div className="flex flex-1 items-center justify-center p-12">
|
||
<div className="flex flex-col items-center gap-2 text-muted-foreground">
|
||
<Settings className="size-8 animate-spin text-primary" />
|
||
<span className="text-sm">正在载入系统配置...</span>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<SettingsForm
|
||
key={`${versionKey}-${config.VirtualAddress ?? ''}`}
|
||
initialConfig={config}
|
||
onSave={handleSave}
|
||
onReset={handleReset}
|
||
isSaving={isSaving}
|
||
/>
|
||
)
|
||
}
|