Files
lmvpn_server/internal/handler/vpn.go
T
kevin 8398067acb feat: 连接列表显示客户端真实 IP,支持反代和 CDN 场景
新增可配置的 real_ip_headers(默认 CF-Connecting-IP > X-Real-IP >
X-Forwarded-For 降级取值),trusted_proxies 用于 gin 代理信任链。
WebSocket 连接建立时捕获真实 IP 并在 /profile 和 /admin 连接列表展示,
登录会话 IP 同步改用真实 IP。
2026-07-10 16:58:14 +08:00

487 lines
13 KiB
Go

package handler
import (
"net"
"net/http"
"strconv"
"lmvpn/internal/db"
"lmvpn/internal/model"
"lmvpn/internal/vpn"
"github.com/apparentlymart/go-cidr/cidr"
"github.com/gin-gonic/gin"
)
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"`
DoLocalIPConfig bool `json:"do_local_ip_config"`
DoRemoteIPConfig bool `json:"do_remote_ip_config"`
MaxConnsPerUser int `json:"max_conns_per_user"`
}
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"`
DoLocalIPConfig *bool `json:"do_local_ip_config"`
DoRemoteIPConfig *bool `json:"do_remote_ip_config"`
MaxConnsPerUser *int `json:"max_conns_per_user"`
}
func loadVpnSettings() (model.VpnSetting, error) {
var s model.VpnSetting
err := db.DB.First(&s, model.VpnSettingSingletonID).Error
return s, err
}
func loadReservationsMap() (map[uint]string, map[uint]string, error) {
var rows []model.VpnReservation
if err := db.DB.Find(&rows).Error; err != nil {
return nil, nil, err
}
out4 := make(map[uint]string, len(rows))
out6 := make(map[uint]string, len(rows))
for _, r := range rows {
if r.IPAddress != "" {
out4[r.UserID] = r.IPAddress
}
if r.IPAddress6 != "" {
out6[r.UserID] = r.IPAddress6
}
}
return out4, out6, nil
}
func ApplyVpnFromDB(svc *vpn.VpnService) error {
s, err := loadVpnSettings()
if err != nil {
return err
}
resv4, resv6, err := loadReservationsMap()
if err != nil {
return err
}
return svc.ApplySettings(s, resv4, resv6)
}
func validateSubnet(subnet string) error {
ip, ipNet, err := net.ParseCIDR(subnet)
if err != nil {
return err
}
if ip.To4() == nil {
return errIPv4Only
}
ones, _ := ipNet.Mask.Size()
if ones > 30 {
return errSubnetTooSmall
}
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
func (e errStr) Error() string { return string(e) }
func GetVpnSettings(c *gin.Context) {
s, err := loadVpnSettings()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载设置失败"})
return
}
c.JSON(http.StatusOK, vpnSettingsResponse{
Enabled: s.Enabled,
Subnet: s.Subnet,
Subnet6: s.Subnet6,
MTU: s.MTU,
InterfaceName: s.InterfaceName,
AllowClientToClient: s.AllowClientToClient,
DoLocalIPConfig: s.DoLocalIPConfig,
DoRemoteIPConfig: s.DoRemoteIPConfig,
MaxConnsPerUser: s.MaxConnsPerUser,
})
}
func UpdateVpnSettings(c *gin.Context) {
var req updateVpnSettingsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
s, err := loadVpnSettings()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载设置失败"})
return
}
if req.Subnet != nil && *req.Subnet != s.Subnet {
if err := validateSubnet(*req.Subnet); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
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"})
return
}
s.MTU = *req.MTU
}
if req.InterfaceName != nil {
s.InterfaceName = *req.InterfaceName
}
if req.Enabled != nil {
s.Enabled = *req.Enabled
}
if req.AllowClientToClient != nil {
s.AllowClientToClient = *req.AllowClientToClient
}
if req.DoLocalIPConfig != nil {
s.DoLocalIPConfig = *req.DoLocalIPConfig
}
if req.DoRemoteIPConfig != nil {
s.DoRemoteIPConfig = *req.DoRemoteIPConfig
}
if req.MaxConnsPerUser != nil {
if *req.MaxConnsPerUser < 1 || *req.MaxConnsPerUser > 1000 {
c.JSON(http.StatusBadRequest, gin.H{"error": "每用户最大连接数范围 1-1000"})
return
}
s.MaxConnsPerUser = *req.MaxConnsPerUser
}
if err := db.DB.Save(&s).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存设置失败"})
return
}
if err := ApplyVpnFromDB(vpn.VPN); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "应用设置失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "设置已更新"})
}
type myVpnConnection struct {
IP string `json:"ip"`
IP6 string `json:"ip6,omitempty"`
RealIP string `json:"real_ip"`
ConnectedAt string `json:"connected_at"`
}
func GetMyVpnConnections(c *gin.Context) {
userID, _ := c.Get("user_id")
maxConns := 30
if vpn.VPN != nil {
s := vpn.VPN.Settings()
if s.MaxConnsPerUser > 0 {
maxConns = s.MaxConnsPerUser
}
}
connections := make([]myVpnConnection, 0)
if vpn.VPN != nil && vpn.VPN.Running() {
for _, ci := range vpn.VPN.ClientList() {
if ci.UserID == userID.(uint) {
connections = append(connections, myVpnConnection{
IP: ci.IP,
IP6: ci.IP6,
RealIP: ci.RealIP,
ConnectedAt: ci.ConnectedAt,
})
}
}
}
c.JSON(http.StatusOK, gin.H{
"max_conns_per_user": maxConns,
"connections": connections,
})
}
type vpnStatusResponse struct {
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) {
s, err := loadVpnSettings()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载设置失败"})
return
}
used4, cap4, used6, cap6 := vpn.VPN.AllocStats()
clients := vpn.VPN.ClientList()
c.JSON(http.StatusOK, vpnStatusResponse{
Enabled: s.Enabled,
Online: len(clients),
UsedIPs: used4,
Capacity: cap4,
UsedIPs6: used6,
Capacity6: cap6,
Clients: clients,
})
}
func GetVpnDiag(c *gin.Context) {
c.JSON(http.StatusOK, vpn.Diag(vpn.VPN))
}
type reservationResponse struct {
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) {
var rows []model.VpnReservation
if err := db.DB.Order("id asc").Find(&rows).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载预留失败"})
return
}
userIDs := make([]uint, 0, len(rows))
for _, r := range rows {
userIDs = append(userIDs, r.UserID)
}
nameMap := make(map[uint]string)
if len(userIDs) > 0 {
var users []model.User
db.DB.Where("id IN ?", userIDs).Find(&users)
for _, u := range users {
nameMap[u.ID] = u.Username
}
}
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,
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"`
IPAddress6 string `json:"ip_address6"`
}
func CreateVpnReservation(c *gin.Context) {
var req createReservationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
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": "用户不存在"})
return
}
s, err := loadVpnSettings()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"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
}
}
// 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, IPAddress6: req.IPAddress6}
if err := db.DB.Create(&r).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建预留失败"})
return
}
if vpn.VPN.Running() {
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": "预留已创建"})
}
func DeleteVpnReservation(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
var r model.VpnReservation
if err := db.DB.First(&r, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "预留不存在"})
return
}
if err := db.DB.Delete(&r).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"})
return
}
if vpn.VPN.Running() {
vpn.VPN.RemoveReservation(r.UserID)
}
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}
func KickUserClient(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
var user model.User
if err := db.DB.First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
currentUserID, _ := c.Get("user_id")
if uint(id) == currentUserID.(uint) {
c.JSON(http.StatusBadRequest, gin.H{"error": "不能踢自己下线,如需断开自己的设备请修改密码"})
return
}
n := 0
db.DB.Model(&user).Update("status", 0)
db.DB.Model(&model.Session{}).Where("user_id = ?", id).Update("invalid", true)
if vpn.VPN != nil && vpn.VPN.Running() {
n = vpn.VPN.KickUser(uint(id))
}
c.JSON(http.StatusOK, gin.H{"message": "已断开用户连接并禁用账号", "kicked": n})
}