feat: 支持 IPv6 双栈地址派发
- model: VpnSetting 新增 Subnet6 字段,VpnReservation 新增 IPAddress6 - alloc: 处理 /64 等大子网 cidr.AddressCount 溢出,回退 65536 扫描上限 - protocol: initMessage 新增 ip6/prefix6/server_ip6(omitempty 向后兼容) - service: 双分配器 alloc4+alloc6,ApplySettings 配置双 IP 与双路由, Allocate 返回 v4+v6 一对地址 - switch: SwitchConn 新增 AssignedIP6(),Register/Unregister 注册双 key, 反欺骗按 IP 版本分别校验 v4/v6 源地址 - tunnel: tunnelConn 新增 assignedIP6,init 消息填充 v6 字段 - handler: 新增 validateSubnet6(/64~/126),settings/预留 API 全面支持 v6 - diag: 新增 IPv6 forwarding 与 NAT66 masquerade 检测 - install_linux.sh: 新增 VPN_SUBNET6、ipv6 forwarding、nft/ip6tables NAT66 - 前端: v6 子网输入框、v6 预留、v6 诊断卡片、客户端列表 v6 列 - 文档: init 消息、IP 分配、子网约束、TUN 配置、NAT、反欺骗全部更新
This commit is contained in:
@@ -8,6 +8,7 @@ const authHeader = () => ({ Authorization: `Bearer ${authStore.token}` })
|
||||
interface Settings {
|
||||
enabled: boolean
|
||||
subnet: string
|
||||
subnet6: string
|
||||
mtu: number
|
||||
interface_name: string
|
||||
allow_client_to_client: boolean
|
||||
@@ -17,6 +18,7 @@ interface Settings {
|
||||
interface ClientInfo {
|
||||
username: string
|
||||
ip: string
|
||||
ip6?: string
|
||||
connected_at: string
|
||||
}
|
||||
interface Status {
|
||||
@@ -24,6 +26,8 @@ interface Status {
|
||||
online: number
|
||||
used_ips: number
|
||||
capacity: number
|
||||
used_ips6?: number
|
||||
capacity6?: number
|
||||
clients: ClientInfo[]
|
||||
}
|
||||
interface Reservation {
|
||||
@@ -31,6 +35,7 @@ interface Reservation {
|
||||
user_id: number
|
||||
username: string
|
||||
ip_address: string
|
||||
ip_address6?: string
|
||||
created_at: string
|
||||
}
|
||||
interface User {
|
||||
@@ -46,6 +51,10 @@ interface Diag {
|
||||
ip_forward_note?: string
|
||||
masquerade: boolean | null
|
||||
masquerade_note?: string
|
||||
ip6_forward: boolean | null
|
||||
ip6_forward_note?: string
|
||||
masquerade6: boolean | null
|
||||
masquerade6_note?: string
|
||||
tun_create: string
|
||||
tun_running: boolean
|
||||
tun_name?: string
|
||||
@@ -64,6 +73,7 @@ const saveMsg = ref('')
|
||||
const form = ref<Settings>({
|
||||
enabled: false,
|
||||
subnet: '192.168.77.0/24',
|
||||
subnet6: '',
|
||||
mtu: 1420,
|
||||
interface_name: '',
|
||||
allow_client_to_client: false,
|
||||
@@ -146,13 +156,13 @@ async function handleSave() {
|
||||
}
|
||||
|
||||
const showAddResv = ref(false)
|
||||
const resvForm = ref({ user_id: 0, ip_address: '' })
|
||||
const resvForm = ref({ user_id: 0, ip_address: '', ip_address6: '' })
|
||||
const resvError = ref('')
|
||||
|
||||
async function handleAddResv() {
|
||||
resvError.value = ''
|
||||
if (!resvForm.value.user_id || !resvForm.value.ip_address) {
|
||||
resvError.value = '请选择用户并填写 IP'
|
||||
if (!resvForm.value.user_id || (!resvForm.value.ip_address && !resvForm.value.ip_address6)) {
|
||||
resvError.value = '请选择用户并至少填写一个 IP 地址'
|
||||
return
|
||||
}
|
||||
try {
|
||||
@@ -164,7 +174,7 @@ async function handleAddResv() {
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw new Error(data.error || '创建失败')
|
||||
showAddResv.value = false
|
||||
resvForm.value = { user_id: 0, ip_address: '' }
|
||||
resvForm.value = { user_id: 0, ip_address: '', ip_address6: '' }
|
||||
await fetchReservations()
|
||||
} catch (e: any) {
|
||||
resvError.value = e.message
|
||||
@@ -296,7 +306,7 @@ onMounted(() => {
|
||||
<span v-else-if="diag?.masquerade" class="text-green-500 mt-0.5">✓</span>
|
||||
<span v-else class="text-red-500 mt-0.5">✗</span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">NAT MASQUERADE</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">NAT MASQUERADE (IPv4)</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
<template v-if="diag?.masquerade === null">{{ diag?.masquerade_note }}</template>
|
||||
<template v-else>{{ diag?.masquerade ? '已配置' : diag?.masquerade_note || '未配置' }}</template>
|
||||
@@ -304,6 +314,34 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IPv6 Forward -->
|
||||
<div class="flex items-start gap-3 p-3 rounded-lg bg-gray-50 dark:bg-gray-700/30">
|
||||
<span v-if="diag?.ip6_forward === null" class="text-gray-400 mt-0.5">—</span>
|
||||
<span v-else-if="diag?.ip6_forward" class="text-green-500 mt-0.5">✓</span>
|
||||
<span v-else class="text-red-500 mt-0.5">✗</span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">IPv6 转发 (forwarding)</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
<template v-if="diag?.ip6_forward === null">{{ diag?.ip6_forward_note }}</template>
|
||||
<template v-else>{{ diag?.ip6_forward ? '已开启' : diag?.ip6_forward_note || '未开启' }}</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IPv6 MASQUERADE -->
|
||||
<div class="flex items-start gap-3 p-3 rounded-lg bg-gray-50 dark:bg-gray-700/30">
|
||||
<span v-if="diag?.masquerade6 === null" class="text-gray-400 mt-0.5">—</span>
|
||||
<span v-else-if="diag?.masquerade6" class="text-green-500 mt-0.5">✓</span>
|
||||
<span v-else class="text-red-500 mt-0.5">✗</span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">NAT MASQUERADE (IPv6)</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
<template v-if="diag?.masquerade6 === null">{{ diag?.masquerade6_note }}</template>
|
||||
<template v-else>{{ diag?.masquerade6 ? '已配置' : diag?.masquerade6_note || '未配置' }}</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 平台 -->
|
||||
<div class="flex items-start gap-3 p-3 rounded-lg bg-gray-50 dark:bg-gray-700/30">
|
||||
<span class="text-gray-400 mt-0.5">ℹ</span>
|
||||
@@ -333,6 +371,10 @@ onMounted(() => {
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">子网 (CIDR)</label>
|
||||
<input v-model="form.subnet" type="text" placeholder="192.168.77.0/24" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">IPv6 子网 (CIDR, 可选)</label>
|
||||
<input v-model="form.subnet6" type="text" placeholder="fd00:dead:beef::/112" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">MTU</label>
|
||||
<input v-model.number="form.mtu" type="number" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white" />
|
||||
@@ -375,17 +417,19 @@ onMounted(() => {
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
|
||||
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">用户</th>
|
||||
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">分配 IP</th>
|
||||
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">IPv4</th>
|
||||
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">IPv6</th>
|
||||
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">连接时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="!status?.clients?.length">
|
||||
<td colspan="3" class="px-6 py-6 text-center text-gray-400">暂无在线客户端</td>
|
||||
<td colspan="4" class="px-6 py-6 text-center text-gray-400">暂无在线客户端</td>
|
||||
</tr>
|
||||
<tr v-for="(c, i) in status?.clients" :key="i" class="border-b border-gray-100 dark:border-gray-700/50">
|
||||
<td class="px-6 py-3 text-gray-900 dark:text-white font-medium">{{ c.username }}</td>
|
||||
<td class="px-6 py-3 text-gray-700 dark:text-gray-300">{{ c.ip }}</td>
|
||||
<td class="px-6 py-3 text-gray-700 dark:text-gray-300">{{ c.ip6 || '—' }}</td>
|
||||
<td class="px-6 py-3 text-gray-500 dark:text-gray-400">{{ c.connected_at }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -402,18 +446,20 @@ onMounted(() => {
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
|
||||
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">用户</th>
|
||||
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">预留 IP</th>
|
||||
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">IPv4</th>
|
||||
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">IPv6</th>
|
||||
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">创建时间</th>
|
||||
<th class="px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="!reservations.length">
|
||||
<td colspan="4" class="px-6 py-6 text-center text-gray-400">暂无预留</td>
|
||||
<td colspan="5" class="px-6 py-6 text-center text-gray-400">暂无预留</td>
|
||||
</tr>
|
||||
<tr v-for="r in reservations" :key="r.id" class="border-b border-gray-100 dark:border-gray-700/50">
|
||||
<td class="px-6 py-3 text-gray-900 dark:text-white font-medium">{{ r.username }}</td>
|
||||
<td class="px-6 py-3 text-gray-700 dark:text-gray-300">{{ r.ip_address }}</td>
|
||||
<td class="px-6 py-3 text-gray-700 dark:text-gray-300">{{ r.ip_address || '—' }}</td>
|
||||
<td class="px-6 py-3 text-gray-700 dark:text-gray-300">{{ r.ip_address6 || '—' }}</td>
|
||||
<td class="px-6 py-3 text-gray-500 dark:text-gray-400">{{ r.created_at }}</td>
|
||||
<td class="px-6 py-3">
|
||||
<button class="px-3 py-1 text-xs rounded-md font-medium text-red-700 bg-red-50 hover:bg-red-100 dark:text-red-400 dark:bg-red-900/20 transition-colors" @click="handleDeleteResv(r.id)">删除</button>
|
||||
@@ -436,9 +482,13 @@ onMounted(() => {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">IP 地址</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">IPv4 地址 (可选)</label>
|
||||
<input v-model="resvForm.ip_address" type="text" placeholder="192.168.77.10" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">IPv6 地址 (可选)</label>
|
||||
<input v-model="resvForm.ip_address6" type="text" placeholder="fd00:dead:beef::10" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white" />
|
||||
</div>
|
||||
<p v-if="resvError" class="text-sm text-red-500">{{ resvError }}</p>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
|
||||
Reference in New Issue
Block a user