feat: 新增 IPv6 双栈支持

- 协议: InitMessage 增加 ip6/prefix6/server_ip6 字段(omitempty,向后兼容)
- TUN: Device 接口新增 ConfigureIPv6, macOS 用 ifconfig inet6, Linux 用 ip -6 addr add
- 路由: full 模式自动添加 ::/1+8000::/1, v6 服务器旁路 /128, resolveHosts 双栈解析
- 会话: setupTUN 解析 v6 字段并配置 TUN+路由, connectOnce 传 v6 给 stats/日志
- 统计: SetConnected(ip,ip6), Snapshot.AssignedIP6
- DB: connection_logs 增加 assigned_ip6 列 + migrateV3 幂等迁移
- UI: 状态卡片新增独立 IPv6 行, 始终可见(未获取显示 IPv6: —)
- 文档: 同步 client-development.md 至服务端 IPv6 版本
This commit is contained in:
2026-07-07 12:24:20 +08:00
parent e867c8e248
commit 8f6daf9f49
18 changed files with 478 additions and 148 deletions
+85 -28
View File
@@ -294,7 +294,7 @@ sequenceDiagram
### 5.2 IP 分配规则
服务端从 VPN 子网中分配客户端内网 IP(`internal/vpn/alloc.go:39-66``internal/vpn/service.go:47-57`):
服务端从 VPN 子网中分配客户端内网 IP(`internal/vpn/alloc.go:39-66``internal/vpn/service.go:47-58`):
| 地址 | 分配规则 | 说明 |
|------|----------|------|
@@ -308,9 +308,19 @@ sequenceDiagram
- 若该用户有预留 IP,优先使用预留;预留被占用时报错(`alloc.go:43-48`
- 地址耗尽时报错 "可用 IP 地址已耗尽"`alloc.go:65`
#### IPv6 双栈
当服务端配置了 IPv6 子网(`Subnet6`)时,客户端同时获得 IPv4 和 IPv6 地址:
- IPv4 地址始终分配(`Subnet` 必填)
- IPv6 地址仅当 `Subnet6` 非空时分配(可选)
- IPv6 预留独立于 IPv4 预留,可单独配置
- IPv6 子网前缀限制:`/64` ~ `/126`
- 对于 `/64` 等大子网,`cidr.AddressCount` 会溢出,实际扫描上限为 65536 个地址
### 5.3 init 消息
前置检查通过后,服务端发送 `init` 文本消息(`internal/vpn/tunnel.go:126-137`):
前置检查通过后,服务端发送 `init` 文本消息(`internal/vpn/tunnel.go:132-148`):
```json
{
@@ -318,17 +328,25 @@ sequenceDiagram
"ip": "10.0.0.5",
"prefix": 24,
"mtu": 1420,
"server_ip": "10.0.0.1"
"server_ip": "10.0.0.1",
"ip6": "fd00:dead:beef::5",
"prefix6": 112,
"server_ip6": "fd00:dead:beef::1"
}
```
| 字段 | 类型 | 说明 | 源码 |
|------|------|------|------|
| `type` | string | 固定值 `"init"` | `internal/vpn/protocol.go:3-9` |
| `ip` | string | 分配给客户端的内网 IP(点分十进制) | `tunnel.go:130` |
| `prefix` | int | 子网前缀长度(如 `24` | `tunnel.go:131` |
| `mtu` | int | TUN 网卡 MTU | `tunnel.go:132` |
| `server_ip` | string | 服务器内网 IP(对端地址 | `tunnel.go:133` |
| 字段 | 类型 | 必填 | 说明 | 源码 |
|------|------|------|------|------|
| `type` | string | 是 | 固定值 `"init"` | `internal/vpn/protocol.go:3-10` |
| `ip` | string | 是 | 分配给客户端的 IPv4 地址 | `tunnel.go:136` |
| `prefix` | int | 是 | IPv4 子网前缀长度(如 `24` | `tunnel.go:137` |
| `mtu` | int | 是 | TUN 网卡 MTU | `tunnel.go:138` |
| `server_ip` | string | 是 | 服务器 IPv4 地址 | `tunnel.go:139` |
| `ip6` | string | 否 | 分配给客户端的 IPv6 地址(仅当服务端配置了 IPv6 子网时存在) | `tunnel.go:141-144` |
| `prefix6` | int | 否 | IPv6 子网前缀长度 | `tunnel.go:142` |
| `server_ip6` | string | 否 | 服务器 IPv6 地址 | `tunnel.go:143` |
> ️ `ip6`/`prefix6`/`server_ip6` 字段使用 `omitempty`,旧客户端可安全忽略。若服务端未配置 IPv6 子网,这三个字段不会出现。
### 5.4 ready 消息与超时
@@ -412,14 +430,14 @@ sequenceDiagram
### 6.4 反欺骗(Anti-Spoofing
服务端**强制校验**每个来自客户端的 IP 包的源地址(`internal/vpn/switch.go:113-116`):
服务端**强制校验**每个来自客户端的 IP 包的源地址(`internal/vpn/switch.go:113-126`):
```
if 源IP != 该客户端被分配的IP:
丢弃该包(不转发,不写入TUN,不断开连接)
```
- **IPv4 包**:源地址必须等于客户端被分配的 IPv4 地址(`init.ip`
- **IPv6 包**:源地址必须等于客户端被分配的 IPv6 地址(`init.ip6`
> ⚠️ 客户端必须确保 TUN 网卡只发送源地址为 `init.ip` 的 IP 包。若客户端配置错误导致源 IP 不匹配,所有上行包将被静默丢弃
不匹配的包将被静默丢弃(不转发、不写入 TUN、不断开连接)
> ⚠️ 客户端必须确保 TUN 网卡只发送源地址与 `init.ip` / `init.ip6` 匹配的 IP 包。若客户端配置错误导致源 IP 不匹配,所有上行包将被静默丢弃。
### 6.5 非 IP 二进制帧
@@ -488,9 +506,11 @@ conn.WriteControl(websocket.PingMessage, nil, ...)
| 约束 | 值 | 源码 |
|------|----|----|
| IP 版本 | 仅 IPv4 | `internal/handler/vpn.go:66-73` |
| 前缀长度 | ≤ /30 | `internal/handler/vpn.go:74-78` |
| IPv4 版本 | 仅 IPv4 | `internal/handler/vpn.go:66-73` |
| IPv4 前缀长度 | ≤ /30 | `internal/handler/vpn.go:74-78` |
| IPv6 前缀长度 | /64 ~ /126 | `internal/handler/vpn.go:81-92` |
| 可用容量 | `AddressCount - 3` | `internal/vpn/alloc.go:106-112` |
| IPv6 大子网容量 | ~65533/64 等大子网 AddressCount 溢出时) | `internal/vpn/alloc.go:108-110` |
### 8.4 MTU 约束
@@ -516,15 +536,18 @@ type controlMessage struct {
}
```
`init` 消息结构较为特殊(`internal/vpn/protocol.go:3-9`):
`init` 消息结构较为特殊(`internal/vpn/protocol.go:3-10`):
```go
type initMessage struct {
Type string `json:"type"`
IP string `json:"ip"`
Prefix int `json:"prefix"`
MTU int `json:"mtu"`
ServerIP string `json:"server_ip"`
Type string `json:"type"`
IP string `json:"ip"`
Prefix int `json:"prefix"`
MTU int `json:"mtu"`
ServerIP string `json:"server_ip"`
IP6 string `json:"ip6,omitempty"`
Prefix6 int `json:"prefix6,omitempty"`
ServerIP6 string `json:"server_ip6,omitempty"`
}
```
@@ -578,7 +601,7 @@ type initMessage struct {
|------|----------------|------|------|------|
| `auth_ok` | Text | 密码认证成功(仅方式 B | JSON | `{"type":"auth_ok"}` |
| `auth_err` | Text | 认证失败 | JSON | `{"type":"auth_err","message":"..."}` |
| `init` | Text | 认证成功且前置检查通过 | JSON | `{"type":"init","ip":"...","prefix":24,"mtu":1420,"server_ip":"..."}` |
| `init` | Text | 认证成功且前置检查通过 | JSON | `{"type":"init","ip":"...","prefix":24,"mtu":1420,"server_ip":"...","ip6":"...","prefix6":112,"server_ip6":"..."}` |
| `error` | Text | 握手阶段失败 | JSON | `{"type":"error","message":"..."}` |
| 数据帧 | Binary | `ready` 之后,下行 IP 包 | 原始 IP 包 | (二进制) |
| Ping | WebSocket Ping | 每 30 秒 | 空 | WebSocket 协议层) |
@@ -593,9 +616,11 @@ type initMessage struct {
| 配置项 | 取值来源 | 说明 |
|--------|----------|------|
| TUN 网卡地址 | `init.ip` / `init.prefix` | 如 `10.0.0.5/24` |
| TUN 网卡地址 (IPv4) | `init.ip` / `init.prefix` | 如 `10.0.0.5/24` |
| TUN 网卡地址 (IPv6) | `init.ip6` / `init.prefix6` | 如 `fd00:dead:beef::5/112`(可选,仅当 init 含 ip6 时) |
| MTU | `init.mtu` | 如 `1420` |
| 对端地址 / 默认路由网关 | `init.server_ip` | 如 `10.0.0.1` |
| 对端地址 / 默认路由网关 (IPv4) | `init.server_ip` | 如 `10.0.0.1` |
| 对端地址 (IPv6) | `init.server_ip6` | 如 `fd00:dead:beef::1`(可选) |
### 11.2 Linux
@@ -608,10 +633,17 @@ ip addr add dev <tun> <ip>/<prefix> peer <server_ip>
ip link set dev <tun> mtu <mtu>
```
IPv6(仅当 init 含 `ip6` 时):
```
ip addr add dev <tun> <ip6>/<prefix6>
```
路由:若需将所有流量经 VPN,添加默认路由:
```
ip route add 0.0.0.0/0 dev <tun>
ip route add ::/0 dev <tun> # IPv6 默认路由(可选)
```
### 11.3 macOS
@@ -623,10 +655,17 @@ ifconfig <utun> inet <ip>/<prefix> <server_ip> up
ifconfig <utun> mtu <mtu>
```
IPv6(仅当 init 含 `ip6` 时):
```
ifconfig <utun> inet6 <ip6>/<prefix6> up
```
路由:
```
route add -inet -net 0.0.0.0/0 -interface <utun>
route add -inet6 -net ::/0 -interface <utun> # IPv6 默认路由(可选)
```
> macOS 的 utun 接口由系统分配编号(如 `utun4`),客户端通常无法指定名称。
@@ -670,6 +709,12 @@ Linux 服务端须开启 IP 转发(`internal/vpn/diag_linux.go:53-60`):
sysctl -w net.ipv4.ip_forward=1
```
IPv6 双栈场景还须开启 IPv6 转发(`internal/vpn/diag_linux.go:116-122`):
```
sysctl -w net.ipv6.conf.all.forwarding=1
```
### 12.2 NAT 伪装
服务端须配置 NAT masquerade,将客户端源 IP 转换为服务器物理网卡 IP(`internal/vpn/diag_linux.go:80-113`)。
@@ -688,7 +733,19 @@ nft add rule ip nat postrouting oifname <物理网卡> masquerade
iptables -t nat -A POSTROUTING -o <物理网卡> -j MASQUERADE
```
> 服务端诊断接口 `GET /api/admin/vpn/diag``internal/handler/vpn.go:190-192`)会检测上述配置,客户端无法上网时可请管理员查看诊断结果。
IPv6 NAT66(仅双栈场景需要):
**nft**
```
nft add rule inet lmvpn_nat postrouting oifname <物理网卡> ip6 saddr <VPN_V6_SUBNET> masquerade
```
**ip6tables(回退)**
```
ip6tables -t nat -A POSTROUTING -s <VPN_V6_SUBNET> -o <物理网卡> -j MASQUERADE
```
> 服务端诊断接口 `GET /api/admin/vpn/diag``internal/handler/vpn.go:190-192`)会检测上述配置(含 IPv6),客户端无法上网时可请管理员查看诊断结果。
### 12.3 子网与 MTU