Files
lmvpn_server/internal/vpn/switch.go
T
kevin a770862c7c 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、反欺骗全部更新
2026-07-07 11:38:21 +08:00

159 lines
3.0 KiB
Go

package vpn
import (
"net"
"sync"
"github.com/songgao/water/waterutil"
)
type SwitchConn interface {
WritePacket(data []byte) error
AssignedIP() net.IP
AssignedIP6() net.IP
}
type ipKey [16]byte
func ipToKey(ip net.IP) ipKey {
var k ipKey
copy(k[:], ip.To16())
return k
}
type PacketSwitch struct {
allowClientToClient bool
mu sync.RWMutex
table map[ipKey]SwitchConn
}
func NewPacketSwitch(allowClientToClient bool) *PacketSwitch {
return &PacketSwitch{
allowClientToClient: allowClientToClient,
table: make(map[ipKey]SwitchConn),
}
}
func (s *PacketSwitch) SetAllowClientToClient(v bool) {
s.mu.Lock()
s.allowClientToClient = v
s.mu.Unlock()
}
func (s *PacketSwitch) Register(c SwitchConn) {
s.mu.Lock()
s.table[ipToKey(c.AssignedIP())] = c
if ip6 := c.AssignedIP6(); ip6 != nil {
s.table[ipToKey(ip6)] = c
}
s.mu.Unlock()
}
func (s *PacketSwitch) Unregister(c SwitchConn) {
s.mu.Lock()
if cur, ok := s.table[ipToKey(c.AssignedIP())]; ok && cur == c {
delete(s.table, ipToKey(c.AssignedIP()))
}
if ip6 := c.AssignedIP6(); ip6 != nil {
k := ipToKey(ip6)
if cur, ok := s.table[k]; ok && cur == c {
delete(s.table, k)
}
}
s.mu.Unlock()
}
func (s *PacketSwitch) findByIP(ip net.IP) SwitchConn {
s.mu.RLock()
c := s.table[ipToKey(ip)]
s.mu.RUnlock()
return c
}
func (s *PacketSwitch) allExcept(skip SwitchConn) []SwitchConn {
s.mu.RLock()
out := make([]SwitchConn, 0, len(s.table))
for _, c := range s.table {
if c == skip {
continue
}
out = append(out, c)
}
s.mu.RUnlock()
return out
}
func parseIPAddrs(packet []byte) (src, dest net.IP, ok bool) {
if len(packet) < 1 {
return nil, nil, false
}
switch {
case waterutil.IsIPv4(packet):
if len(packet) < 20 {
return nil, nil, false
}
return waterutil.IPv4Source(packet), waterutil.IPv4Destination(packet), true
case waterutil.IsIPv6(packet):
if len(packet) < 40 {
return nil, nil, false
}
src = make(net.IP, 16)
copy(src, packet[8:24])
dest = make(net.IP, 16)
copy(dest, packet[24:40])
return src, dest, true
}
return nil, nil, false
}
func (s *PacketSwitch) allowC2C() bool {
s.mu.RLock()
v := s.allowClientToClient
s.mu.RUnlock()
return v
}
func (s *PacketSwitch) RouteFromClient(src SwitchConn, packet []byte) []SwitchConn {
srcIP, dest, ok := parseIPAddrs(packet)
if !ok {
return nil
}
// anti-spoof: enforce assigned source IP by version
if srcIP != nil {
if srcIP.To4() != nil {
if !srcIP.Equal(src.AssignedIP()) {
return nil
}
} else {
assigned6 := src.AssignedIP6()
if assigned6 == nil || !srcIP.Equal(assigned6) {
return nil
}
}
}
if dest.IsGlobalUnicast() {
if c := s.findByIP(dest); c != nil && s.allowC2C() {
return []SwitchConn{c}
}
return nil
}
if s.allowC2C() {
return s.allExcept(src)
}
return nil
}
func (s *PacketSwitch) RouteFromTUN(packet []byte) []SwitchConn {
_, dest, ok := parseIPAddrs(packet)
if !ok {
return nil
}
if dest.IsGlobalUnicast() {
if c := s.findByIP(dest); c != nil {
return []SwitchConn{c}
}
return nil
}
return s.allExcept(nil)
}