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:
2026-07-07 11:38:21 +08:00
parent 808b991483
commit a770862c7c
13 changed files with 561 additions and 145 deletions
+143 -51
View File
@@ -16,6 +16,7 @@ import (
type vpnSettingsResponse struct {
Enabled bool `json:"enabled"`
Subnet string `json:"subnet"`
Subnet6 string `json:"subnet6"`
MTU int `json:"mtu"`
InterfaceName string `json:"interface_name"`
AllowClientToClient bool `json:"allow_client_to_client"`
@@ -26,6 +27,7 @@ type vpnSettingsResponse struct {
type updateVpnSettingsRequest struct {
Enabled *bool `json:"enabled"`
Subnet *string `json:"subnet"`
Subnet6 *string `json:"subnet6"`
MTU *int `json:"mtu"`
InterfaceName *string `json:"interface_name"`
AllowClientToClient *bool `json:"allow_client_to_client"`
@@ -39,16 +41,22 @@ func loadVpnSettings() (model.VpnSetting, error) {
return s, err
}
func loadReservationsMap() (map[uint]string, error) {
func loadReservationsMap() (map[uint]string, map[uint]string, error) {
var rows []model.VpnReservation
if err := db.DB.Find(&rows).Error; err != nil {
return nil, err
return nil, nil, err
}
out := make(map[uint]string, len(rows))
out4 := make(map[uint]string, len(rows))
out6 := make(map[uint]string, len(rows))
for _, r := range rows {
out[r.UserID] = r.IPAddress
if r.IPAddress != "" {
out4[r.UserID] = r.IPAddress
}
if r.IPAddress6 != "" {
out6[r.UserID] = r.IPAddress6
}
}
return out, nil
return out4, out6, nil
}
func ApplyVpnFromDB(svc *vpn.VpnService) error {
@@ -56,11 +64,11 @@ func ApplyVpnFromDB(svc *vpn.VpnService) error {
if err != nil {
return err
}
reservations, err := loadReservationsMap()
resv4, resv6, err := loadReservationsMap()
if err != nil {
return err
}
return svc.ApplySettings(s, reservations)
return svc.ApplySettings(s, resv4, resv6)
}
func validateSubnet(subnet string) error {
@@ -78,12 +86,30 @@ func validateSubnet(subnet string) error {
return nil
}
func validateSubnet6(subnet string) error {
ip, ipNet, err := net.ParseCIDR(subnet)
if err != nil {
return err
}
if ip.To4() != nil {
return errIPv6Only
}
ones, _ := ipNet.Mask.Size()
if ones < 64 || ones > 126 {
return errSubnet6Range
}
return nil
}
var (
errIPv4Only = errStr("仅支持 IPv4 子网")
errIPv6Only = errStr("IPv6 子网不能使用 IPv4 地址")
errSubnetTooSmall = errStr("子网前缀长度不能大于 /30")
errSubnet6Range = errStr("IPv6 子网前缀长度应在 /64 到 /126 之间")
errIPNotInSubnet = errStr("IP 不在子网范围内")
errIPReserved = errStr("该 IP 已被预留")
errIPIsServer = errStr("该 IP 为服务器 IP,不可预留")
errNeedAtLeastOne = errStr("至少需要填写 IPv4 或 IPv6 地址之一")
)
type errStr string
@@ -99,6 +125,7 @@ func GetVpnSettings(c *gin.Context) {
c.JSON(http.StatusOK, vpnSettingsResponse{
Enabled: s.Enabled,
Subnet: s.Subnet,
Subnet6: s.Subnet6,
MTU: s.MTU,
InterfaceName: s.InterfaceName,
AllowClientToClient: s.AllowClientToClient,
@@ -127,6 +154,15 @@ func UpdateVpnSettings(c *gin.Context) {
}
s.Subnet = *req.Subnet
}
if req.Subnet6 != nil && *req.Subnet6 != s.Subnet6 {
if *req.Subnet6 != "" {
if err := validateSubnet6(*req.Subnet6); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
}
s.Subnet6 = *req.Subnet6
}
if req.MTU != nil {
if *req.MTU < 500 || *req.MTU > 65535 {
c.JSON(http.StatusBadRequest, gin.H{"error": "MTU 范围 500-65535"})
@@ -163,11 +199,13 @@ func UpdateVpnSettings(c *gin.Context) {
}
type vpnStatusResponse struct {
Enabled bool `json:"enabled"`
Online int `json:"online"`
UsedIPs int `json:"used_ips"`
Capacity uint64 `json:"capacity"`
Clients []vpn.ClientInfo `json:"clients"`
Enabled bool `json:"enabled"`
Online int `json:"online"`
UsedIPs int `json:"used_ips"`
Capacity uint64 `json:"capacity"`
UsedIPs6 int `json:"used_ips6,omitempty"`
Capacity6 uint64 `json:"capacity6,omitempty"`
Clients []vpn.ClientInfo `json:"clients"`
}
func GetVpnStatus(c *gin.Context) {
@@ -176,14 +214,16 @@ func GetVpnStatus(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载设置失败"})
return
}
used, cap := vpn.VPN.AllocStats()
used4, cap4, used6, cap6 := vpn.VPN.AllocStats()
clients := vpn.VPN.ClientList()
c.JSON(http.StatusOK, vpnStatusResponse{
Enabled: s.Enabled,
Online: len(clients),
UsedIPs: used,
Capacity: cap,
Clients: clients,
Enabled: s.Enabled,
Online: len(clients),
UsedIPs: used4,
Capacity: cap4,
UsedIPs6: used6,
Capacity6: cap6,
Clients: clients,
})
}
@@ -192,11 +232,12 @@ func GetVpnDiag(c *gin.Context) {
}
type reservationResponse struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
Username string `json:"username"`
IPAddress string `json:"ip_address"`
CreatedAt string `json:"created_at"`
ID uint `json:"id"`
UserID uint `json:"user_id"`
Username string `json:"username"`
IPAddress string `json:"ip_address"`
IPAddress6 string `json:"ip_address6,omitempty"`
CreatedAt string `json:"created_at"`
}
func ListVpnReservations(c *gin.Context) {
@@ -220,19 +261,21 @@ func ListVpnReservations(c *gin.Context) {
out := make([]reservationResponse, len(rows))
for i, r := range rows {
out[i] = reservationResponse{
ID: r.ID,
UserID: r.UserID,
Username: nameMap[r.UserID],
IPAddress: r.IPAddress,
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
ID: r.ID,
UserID: r.UserID,
Username: nameMap[r.UserID],
IPAddress: r.IPAddress,
IPAddress6: r.IPAddress6,
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
}
}
c.JSON(http.StatusOK, gin.H{"reservations": out})
}
type createReservationRequest struct {
UserID uint `json:"user_id" binding:"required"`
IPAddress string `json:"ip_address" binding:"required"`
UserID uint `json:"user_id" binding:"required"`
IPAddress string `json:"ip_address"`
IPAddress6 string `json:"ip_address6"`
}
func CreateVpnReservation(c *gin.Context) {
@@ -242,6 +285,11 @@ func CreateVpnReservation(c *gin.Context) {
return
}
if req.IPAddress == "" && req.IPAddress6 == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": errNeedAtLeastOne.Error()})
return
}
var user model.User
if err := db.DB.First(&user, req.UserID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "用户不存在"})
@@ -253,42 +301,86 @@ func CreateVpnReservation(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载设置失败"})
return
}
_, ipNet, err := net.ParseCIDR(s.Subnet)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "子网配置错误"})
return
}
ip := net.ParseIP(req.IPAddress)
if ip == nil || !ipNet.Contains(ip) {
c.JSON(http.StatusBadRequest, gin.H{"error": errIPNotInSubnet.Error()})
return
}
serverIP, _ := cidr.Host(ipNet, 1)
if ip.Equal(serverIP) {
c.JSON(http.StatusBadRequest, gin.H{"error": errIPIsServer.Error()})
return
// validate v4
if req.IPAddress != "" {
_, ipNet, err := net.ParseCIDR(s.Subnet)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "子网配置错误"})
return
}
ip := net.ParseIP(req.IPAddress)
if ip == nil || !ipNet.Contains(ip) {
c.JSON(http.StatusBadRequest, gin.H{"error": errIPNotInSubnet.Error()})
return
}
serverIP, _ := cidr.Host(ipNet, 1)
if ip.Equal(serverIP) {
c.JSON(http.StatusBadRequest, gin.H{"error": errIPIsServer.Error()})
return
}
}
var count int64
db.DB.Model(&model.VpnReservation{}).Where("ip_address = ?", req.IPAddress).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": errIPReserved.Error()})
return
// validate v6
if req.IPAddress6 != "" {
if s.Subnet6 == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "服务端未配置 IPv6 子网"})
return
}
_, ipNet6, err := net.ParseCIDR(s.Subnet6)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "IPv6 子网配置错误"})
return
}
ip6 := net.ParseIP(req.IPAddress6)
if ip6 == nil || ip6.To4() != nil || !ipNet6.Contains(ip6) {
c.JSON(http.StatusBadRequest, gin.H{"error": "IPv6 " + errIPNotInSubnet.Error()})
return
}
serverIP6, _ := cidr.Host(ipNet6, 1)
if ip6.Equal(serverIP6) {
c.JSON(http.StatusBadRequest, gin.H{"error": "IPv6 " + errIPIsServer.Error()})
return
}
}
// check uniqueness
if req.IPAddress != "" {
var count int64
db.DB.Model(&model.VpnReservation{}).Where("ip_address = ?", req.IPAddress).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": errIPReserved.Error()})
return
}
}
if req.IPAddress6 != "" {
var count int64
db.DB.Model(&model.VpnReservation{}).Where("ip_address6 = ?", req.IPAddress6).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "IPv6 " + errIPReserved.Error()})
return
}
}
var existUser model.VpnReservation
if err := db.DB.Where("user_id = ?", req.UserID).First(&existUser).Error; err == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "该用户已有预留 IP"})
return
}
r := model.VpnReservation{UserID: req.UserID, IPAddress: req.IPAddress}
r := model.VpnReservation{UserID: req.UserID, IPAddress: req.IPAddress, IPAddress6: req.IPAddress6}
if err := db.DB.Create(&r).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建预留失败"})
return
}
if vpn.VPN.Running() {
vpn.VPN.AddReservation(req.UserID, req.IPAddress)
if req.IPAddress != "" {
vpn.VPN.AddReservation(req.UserID, req.IPAddress)
}
if req.IPAddress6 != "" {
vpn.VPN.AddReservation6(req.UserID, req.IPAddress6)
}
}
c.JSON(http.StatusOK, gin.H{"message": "预留已创建"})
}
+6 -4
View File
@@ -8,6 +8,7 @@ type VpnSetting struct {
ID uint `gorm:"primaryKey"`
Enabled bool `gorm:"default:false"`
Subnet string `gorm:"size:64;not null"`
Subnet6 string `gorm:"size:64"`
MTU int `gorm:"default:1420"`
InterfaceName string `gorm:"size:16"`
AllowClientToClient bool `gorm:"default:false"`
@@ -21,10 +22,11 @@ func (VpnSetting) TableName() string {
}
type VpnReservation struct {
ID uint `gorm:"primaryKey;autoIncrement"`
UserID uint `gorm:"uniqueIndex;not null"`
IPAddress string `gorm:"size:64;uniqueIndex;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
ID uint `gorm:"primaryKey;autoIncrement"`
UserID uint `gorm:"uniqueIndex;not null"`
IPAddress string `gorm:"size:64;uniqueIndex"`
IPAddress6 string `gorm:"size:64"`
CreatedAt time.Time `gorm:"autoCreateTime"`
}
func (VpnReservation) TableName() string {
+6
View File
@@ -49,6 +49,9 @@ func (m *AllocationManager) Allocate(userID uint) (net.IP, error) {
}
count := cidr.AddressCount(m.net)
if count == 0 && m.net.IP.To4() == nil {
count = 65536
}
maxIndex := int(count - 1)
for i := 2; i < maxIndex; i++ {
ip, err := cidr.Host(m.net, i)
@@ -105,6 +108,9 @@ func (m *AllocationManager) UsedCount() int {
func (m *AllocationManager) Capacity() uint64 {
count := cidr.AddressCount(m.net)
if count == 0 && m.net.IP.To4() == nil {
return 65533
}
if count < 3 {
return 0
}
+15 -11
View File
@@ -6,17 +6,21 @@ import (
)
type DiagResult struct {
Platform string `json:"platform"`
IsRoot bool `json:"is_root"`
HasCapNetAdmin *bool `json:"has_cap_net_admin"`
CapNetAdminNote string `json:"cap_net_admin_note,omitempty"`
IPForward *bool `json:"ip_forward"`
IPForwardNote string `json:"ip_forward_note,omitempty"`
Masquerade *bool `json:"masquerade"`
MasqueradeNote string `json:"masquerade_note,omitempty"`
TUNCreate string `json:"tun_create"`
TUNRunning bool `json:"tun_running"`
TUNName string `json:"tun_name,omitempty"`
Platform string `json:"platform"`
IsRoot bool `json:"is_root"`
HasCapNetAdmin *bool `json:"has_cap_net_admin"`
CapNetAdminNote string `json:"cap_net_admin_note,omitempty"`
IPForward *bool `json:"ip_forward"`
IPForwardNote string `json:"ip_forward_note,omitempty"`
Masquerade *bool `json:"masquerade"`
MasqueradeNote string `json:"masquerade_note,omitempty"`
IP6Forward *bool `json:"ip6_forward"`
IP6ForwardNote string `json:"ip6_forward_note,omitempty"`
Masquerade6 *bool `json:"masquerade6"`
Masquerade6Note string `json:"masquerade6_note,omitempty"`
TUNCreate string `json:"tun_create"`
TUNRunning bool `json:"tun_running"`
TUNName string `json:"tun_name,omitempty"`
}
func Diag(svc *VpnService) DiagResult {
+51
View File
@@ -24,6 +24,16 @@ func fillPlatformDiag(r *DiagResult) {
m, note := checkMasquerade()
r.Masquerade = m
r.MasqueradeNote = note
v6 := readIP6Forward()
r.IP6Forward = v6
if v6 == nil || !*v6 {
r.IP6ForwardNote = "未开启,执行: sysctl -w net.ipv6.conf.all.forwarding=1"
}
m6, note6 := checkMasquerade6()
r.Masquerade6 = m6
r.Masquerade6Note = note6
}
func ptrBool(b bool) *bool { return &b }
@@ -112,3 +122,44 @@ func checkMasquerade() (*bool, string) {
return nil, "iptables 与 nft 均未安装,无法检测 NAT 规则。Debian/Ubuntu 安装: apt install nftables"
}
func readIP6Forward() *bool {
data, err := os.ReadFile("/proc/sys/net/ipv6/conf/all/forwarding")
if err != nil {
return nil
}
v := strings.TrimSpace(string(data)) == "1"
return &v
}
func checkMasquerade6() (*bool, string) {
nftPath := findExecutable("nft")
if nftPath != "" {
out, err := exec.Command(nftPath, "list", "ruleset").Output()
if err == nil {
s := string(out)
if strings.Contains(s, "ip6 saddr") && strings.Contains(s, "masquerade") {
return ptrBool(true), ""
}
return ptrBool(false), "未检测到 IPv6 masquerade 规则,IPv6 客户端无法出网"
}
}
ip6tPath := findExecutable("ip6tables")
if ip6tPath != "" {
out, err := exec.Command(ip6tPath, "-t", "nat", "-L", "POSTROUTING", "-n").Output()
if err != nil {
if nftPath != "" {
return nil, "ip6tables 与原生 nft 表不兼容且 nft 不可执行,无法检测 IPv6 MASQUERADE"
}
return nil, "ip6tables 不可执行(权限不足?),无法检测 IPv6 MASQUERADE"
}
has := strings.Contains(string(out), "MASQUERADE")
if has {
return &has, ""
}
return &has, "未检测到 IPv6 MASQUERADE 规则,IPv6 客户端无法出网"
}
return nil, "ip6tables 与 nft 均未安装,无法检测 IPv6 NAT 规则"
}
+4
View File
@@ -9,4 +9,8 @@ func fillPlatformDiag(r *DiagResult) {
r.IPForwardNote = "仅 Linux 适用"
r.Masquerade = nil
r.MasqueradeNote = "仅 Linux 适用"
r.IP6Forward = nil
r.IP6ForwardNote = "仅 Linux 适用"
r.Masquerade6 = nil
r.Masquerade6Note = "仅 Linux 适用"
}
+8 -5
View File
@@ -1,11 +1,14 @@
package vpn
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"`
}
type controlMessage struct {
+97 -12
View File
@@ -19,6 +19,10 @@ type VpnService struct {
serverIP net.IP
prefix int
alloc *AllocationManager
net6 *net.IPNet
serverIP6 net.IP
prefix6 int
alloc6 *AllocationManager
switchx *PacketSwitch
tun *TUNInterface
tunDone chan struct{}
@@ -57,7 +61,7 @@ func (s *VpnService) parseNet(subnet string) (*net.IPNet, net.IP, int, error) {
return ipNet, serverIP, ones, nil
}
func (s *VpnService) ApplySettings(settings model.VpnSetting, reservations map[uint]string) error {
func (s *VpnService) ApplySettings(settings model.VpnSetting, reservations4, reservations6 map[uint]string) error {
s.mu.Lock()
if s.running {
s.mu.Unlock()
@@ -76,6 +80,17 @@ func (s *VpnService) ApplySettings(settings model.VpnSetting, reservations map[u
return err
}
var ipNet6 *net.IPNet
var serverIP6 net.IP
var prefix6 int
var alloc6 *AllocationManager
if settings.Subnet6 != "" {
ipNet6, serverIP6, prefix6, err = s.parseNet(settings.Subnet6)
if err != nil {
return fmt.Errorf("IPv6 子网错误: %w", err)
}
}
tun, err := CreateTUN(settings.InterfaceName)
if err != nil {
return err
@@ -84,11 +99,22 @@ func (s *VpnService) ApplySettings(settings model.VpnSetting, reservations map[u
if settings.DoLocalIPConfig {
if err := tun.Configure(serverIP, prefix, nil); err != nil {
_ = tun.Close()
return fmt.Errorf("配置 TUN 失败: %w", err)
return fmt.Errorf("配置 TUN IPv4 失败: %w", err)
}
}
if err := tun.AddSubnetRoute(ipNet); err != nil {
log.Printf("警告: 添加子网路由失败: %v", err)
log.Printf("警告: 添加 IPv4 子网路由失败: %v", err)
}
if ipNet6 != nil {
if settings.DoLocalIPConfig {
if err := tun.Configure(serverIP6, prefix6, nil); err != nil {
log.Printf("警告: 配置 TUN IPv6 失败: %v", err)
}
}
if err := tun.AddSubnetRoute(ipNet6); err != nil {
log.Printf("警告: 添加 IPv6 子网路由失败: %v", err)
}
alloc6 = NewAllocationManager(ipNet6, serverIP6, reservations6)
}
if err := tun.SetMTU(settings.MTU); err != nil {
log.Printf("警告: 设置 MTU 失败: %v", err)
@@ -98,7 +124,11 @@ func (s *VpnService) ApplySettings(settings model.VpnSetting, reservations map[u
s.net = ipNet
s.serverIP = serverIP
s.prefix = prefix
s.alloc = NewAllocationManager(ipNet, serverIP, reservations)
s.alloc = NewAllocationManager(ipNet, serverIP, reservations4)
s.net6 = ipNet6
s.serverIP6 = serverIP6
s.prefix6 = prefix6
s.alloc6 = alloc6
s.switchx = NewPacketSwitch(settings.AllowClientToClient)
s.tun = tun
s.tunDone = make(chan struct{})
@@ -106,7 +136,10 @@ func (s *VpnService) ApplySettings(settings model.VpnSetting, reservations map[u
s.mu.Unlock()
go s.serveTUN()
log.Printf("VPN 服务已启动: tun=%s subnet=%s server=%s mtu=%d", tun.Name(), ipNet.String(), serverIP.String(), settings.MTU)
log.Printf("VPN 服务已启动: tun=%s subnet=%s server=%s", tun.Name(), ipNet.String(), serverIP.String())
if ipNet6 != nil {
log.Printf(" IPv6: subnet=%s server=%s", ipNet6.String(), serverIP6.String())
}
return nil
}
@@ -162,14 +195,27 @@ func (s *VpnService) Stop() error {
return nil
}
func (s *VpnService) Allocate(user *model.User) (net.IP, error) {
func (s *VpnService) Allocate(user *model.User) (net.IP, net.IP, error) {
s.mu.RLock()
alloc := s.alloc
alloc6 := s.alloc6
s.mu.RUnlock()
if alloc == nil {
return nil, errors.New("VPN 服务未运行")
return nil, nil, errors.New("VPN 服务未运行")
}
return alloc.Allocate(user.ID)
ip4, err := alloc.Allocate(user.ID)
if err != nil {
return nil, nil, err
}
var ip6 net.IP
if alloc6 != nil {
ip6, err = alloc6.Allocate(user.ID)
if err != nil {
alloc.Release(ip4)
return ip4, nil, fmt.Errorf("IPv6 分配失败: %w", err)
}
}
return ip4, ip6, nil
}
func (s *VpnService) WriteToTUN(packet []byte) error {
@@ -209,6 +255,9 @@ func (s *VpnService) unregisterClient(c *tunnelConn) {
if s.alloc != nil {
s.alloc.Release(c.assignedIP)
}
if s.alloc6 != nil && c.assignedIP6 != nil {
s.alloc6.Release(c.assignedIP6)
}
s.mu.Unlock()
}
@@ -224,14 +273,36 @@ func (s *VpnService) Prefix() int {
return s.prefix
}
func (s *VpnService) AllocStats() (used int, capacity uint64) {
func (s *VpnService) ServerIP6() net.IP {
s.mu.RLock()
defer s.mu.RUnlock()
return s.serverIP6
}
func (s *VpnService) Prefix6() int {
s.mu.RLock()
defer s.mu.RUnlock()
return s.prefix6
}
func (s *VpnService) HasIPv6() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.alloc6 != nil
}
func (s *VpnService) AllocStats() (used4 int, cap4 uint64, used6 int, cap6 uint64) {
s.mu.RLock()
alloc := s.alloc
alloc6 := s.alloc6
s.mu.RUnlock()
if alloc == nil {
return 0, 0
if alloc != nil {
used4, cap4 = alloc.UsedCount(), alloc.Capacity()
}
return alloc.UsedCount(), alloc.Capacity()
if alloc6 != nil {
used6, cap6 = alloc6.UsedCount(), alloc6.Capacity()
}
return
}
func (s *VpnService) AddReservation(userID uint, ipStr string) {
@@ -243,13 +314,26 @@ func (s *VpnService) AddReservation(userID uint, ipStr string) {
}
}
func (s *VpnService) AddReservation6(userID uint, ipStr string) {
s.mu.RLock()
alloc6 := s.alloc6
s.mu.RUnlock()
if alloc6 != nil {
alloc6.AddReservation(userID, ipStr)
}
}
func (s *VpnService) RemoveReservation(userID uint) {
s.mu.RLock()
alloc := s.alloc
alloc6 := s.alloc6
s.mu.RUnlock()
if alloc != nil {
alloc.RemoveReservation(userID)
}
if alloc6 != nil {
alloc6.RemoveReservation(userID)
}
}
func (s *VpnService) ClientList() []ClientInfo {
@@ -265,6 +349,7 @@ func (s *VpnService) ClientList() []ClientInfo {
type ClientInfo struct {
Username string `json:"username"`
IP string `json:"ip"`
IP6 string `json:"ip6,omitempty"`
ConnectedAt string `json:"connected_at"`
}
+25 -8
View File
@@ -10,6 +10,7 @@ import (
type SwitchConn interface {
WritePacket(data []byte) error
AssignedIP() net.IP
AssignedIP6() net.IP
}
type ipKey [16]byte
@@ -40,17 +41,24 @@ func (s *PacketSwitch) SetAllowClientToClient(v bool) {
}
func (s *PacketSwitch) Register(c SwitchConn) {
k := ipToKey(c.AssignedIP())
s.mu.Lock()
s.table[k] = c
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) {
k := ipToKey(c.AssignedIP())
s.mu.Lock()
if cur, ok := s.table[k]; ok && cur == c {
delete(s.table, k)
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()
}
@@ -110,9 +118,18 @@ func (s *PacketSwitch) RouteFromClient(src SwitchConn, packet []byte) []SwitchCo
if !ok {
return nil
}
// anti-spoof: enforce assigned source IP
if srcIP != nil && !srcIP.Equal(src.AssignedIP()) {
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() {
+30 -15
View File
@@ -28,18 +28,20 @@ var (
)
type tunnelConn struct {
conn *websocket.Conn
user *model.User
svc *VpnService
assignedIP net.IP
connectedAt time.Time
writeMu sync.Mutex
ready atomic.Bool
rxBytes atomic.Int64
txBytes atomic.Int64
conn *websocket.Conn
user *model.User
svc *VpnService
assignedIP net.IP
assignedIP6 net.IP
connectedAt time.Time
writeMu sync.Mutex
ready atomic.Bool
rxBytes atomic.Int64
txBytes atomic.Int64
}
func (c *tunnelConn) AssignedIP() net.IP { return c.assignedIP }
func (c *tunnelConn) AssignedIP6() net.IP { return c.assignedIP6 }
func (c *tunnelConn) WritePacket(data []byte) error {
if !c.ready.Load() || len(data) == 0 {
@@ -73,11 +75,15 @@ func (c *tunnelConn) close() {
}
func (c *tunnelConn) info() ClientInfo {
return ClientInfo{
ci := ClientInfo{
Username: c.user.Username,
IP: c.assignedIP.String(),
ConnectedAt: c.connectedAt.Format("2006-01-02 15:04:05"),
}
if c.assignedIP6 != nil {
ci.IP6 = c.assignedIP6.String()
}
return ci
}
func runTunnel(conn *websocket.Conn, user *model.User) {
@@ -106,7 +112,7 @@ func runTunnel(conn *websocket.Conn, user *model.User) {
activeConnsMu.Unlock()
}()
ip, err := VPN.Allocate(user)
ip4, ip6, err := VPN.Allocate(user)
if err != nil {
_ = sendJSON(conn, controlMessage{Type: "error", Message: "分配 IP 失败: " + err.Error()})
return
@@ -116,7 +122,8 @@ func runTunnel(conn *websocket.Conn, user *model.User) {
conn: conn,
user: user,
svc: VPN,
assignedIP: ip,
assignedIP: ip4,
assignedIP6: ip6,
connectedAt: time.Now(),
}
@@ -126,17 +133,25 @@ func runTunnel(conn *websocket.Conn, user *model.User) {
settings := VPN.Settings()
initMsg := initMessage{
Type: "init",
IP: ip.String(),
IP: ip4.String(),
Prefix: VPN.Prefix(),
MTU: settings.MTU,
ServerIP: VPN.ServerIP().String(),
}
if ip6 != nil {
initMsg.IP6 = ip6.String()
initMsg.Prefix6 = VPN.Prefix6()
initMsg.ServerIP6 = VPN.ServerIP6().String()
}
if err := tc.writeControl(initMsg); err != nil {
log.Printf("用户 %s 发送 init 失败: %v", user.Username, err)
return
}
log.Printf("用户 %s 已连接,分配 IP %s", user.Username, ip.String())
log.Printf("用户 %s 已连接,分配 IP %s", user.Username, ip4.String())
if ip6 != nil {
log.Printf(" IPv6: %s", ip6.String())
}
conn.SetReadLimit(maxMessageSize)
conn.SetReadDeadline(time.Now().Add(readyTimeout))
@@ -175,7 +190,7 @@ func runTunnel(conn *websocket.Conn, user *model.User) {
if msg.Type == "ready" && !tc.ready.Load() {
tc.ready.Store(true)
conn.SetReadDeadline(time.Now().Add(readTimeout))
log.Printf("用户 %s 就绪 (IP %s)", user.Username, ip.String())
log.Printf("用户 %s 就绪 (IP %s)", user.Username, ip4.String())
}
continue
}