Initial commit

This commit is contained in:
2026-06-30 15:14:37 +08:00
commit 15dab96872
311 changed files with 95639 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
// Package api provides HTTP API handlers using Gin framework.
package api
+131
View File
@@ -0,0 +1,131 @@
package dto
import (
"fmt"
"git.zkcoi.com/zkcoi/meshray/internal/model"
)
// NetworkResponse 网络响应 DTOID 为字符串格式,避免 JavaScript 精度丢失)
type NetworkResponse struct {
ID string `json:"id"` // ❄️ 雪花算法 ID(字符串格式)
Name string `json:"name"` // 组网名称
SubnetIPv4 string `json:"subnet_ipv4"` // IPv4 子网(snake_case
MeshMode string `json:"mesh_mode"` // 组网模式(enhanced/native
WGMode string `json:"wg_mode"` // WG 运行模式(kernel/userspace
PolicyID string `json:"policy_id"` // ❄️ 关联策略 ID(字符串格式)
Status string `json:"status"` // 运行状态(running/stopped
DeviceCount int64 `json:"device_count"` // 设备数量(统计字段)
Description string `json:"description,omitempty"` // 描述
CreatedAt string `json:"created_at"` // 创建时间
UpdatedAt string `json:"updated_at"` // 更新时间
}
// DeviceResponse 设备响应 DTOID 为字符串格式,避免 JavaScript 精度丢失)
type DeviceResponse struct {
ID string `json:"id"` // ❄️ 雪花算法 ID(字符串格式)
NetworkID string `json:"networkId"` // ❄️ 所属网络 ID(字符串格式)
Name string `json:"name"` // 设备名称
VirtualIP string `json:"virtualIP"` // 虚拟 IP
PublicKey string `json:"publicKey"` // WireGuard 公钥
Endpoint string `json:"endpoint,omitempty"` // 公网 Endpoint
IsRelayCapable bool `json:"isRelayCapable"` // 是否中继节点
Status string `json:"status"` // 在线状态(online/offline
Description string `json:"description,omitempty"` // 描述
CreatedAt string `json:"createdAt"` // 创建时间
UpdatedAt string `json:"updatedAt"` // 更新时间
}
// PolicyResponse 策略响应 DTOID 为字符串格式)
type PolicyResponse struct {
ID string `json:"id"` // ❄️ 雪花算法 ID(字符串格式)
Name string `json:"name"` // 策略名称
Type string `json:"type"` // 类型(system/custom
LayerConfig string `json:"layerConfig"` // 链路配置 JSON
GlobalParams string `json:"globalParams"` // 全局参数 JSON
Enabled bool `json:"enabled"` // 是否启用
IsDefault bool `json:"isDefault"` // 是否默认策略
Description string `json:"description,omitempty"` // 描述
CreatedAt string `json:"createdAt"` // 创建时间
UpdatedAt string `json:"updatedAt"` // 更新时间
}
// ToNetworkResponse Network 转 NetworkResponse
func ToNetworkResponse(network *model.Network) NetworkResponse {
// 计算设备数量
var deviceCount int64 = 0
if len(network.Devices) > 0 {
deviceCount = int64(len(network.Devices))
}
return NetworkResponse{
ID: fmt.Sprintf("%d", network.ID),
Name: network.Name,
SubnetIPv4: network.SubnetIPv4,
MeshMode: network.Mode, // Mode → MeshMode
WGMode: network.WGMode,
PolicyID: fmt.Sprintf("%d", network.PolicyID),
Status: network.Status,
DeviceCount: deviceCount, // 新增:设备数量
CreatedAt: network.CreatedAt.Format("2006-01-02T15:04:05Z"),
UpdatedAt: network.UpdatedAt.Format("2006-01-02T15:04:05Z"),
}
}
// ToDeviceResponse Device 转 DeviceResponse
func ToDeviceResponse(device *model.Device) DeviceResponse {
return DeviceResponse{
ID: fmt.Sprintf("%d", device.ID),
NetworkID: fmt.Sprintf("%d", device.NetworkID),
Name: device.Name,
VirtualIP: device.VirtualIP,
PublicKey: device.PublicKey,
Endpoint: device.Endpoint,
IsRelayCapable: device.IsRelayCapable,
Status: device.Status,
CreatedAt: device.CreatedAt.Format("2006-01-02T15:04:05Z"),
}
}
// ToPolicyResponse Policy 转 PolicyResponse
func ToPolicyResponse(policy *model.Policy) PolicyResponse {
return PolicyResponse{
ID: fmt.Sprintf("%d", policy.ID),
Name: policy.Name,
Type: policy.Type,
LayerConfig: policy.LayerConfig,
GlobalParams: policy.GlobalParams,
Enabled: policy.Enabled,
IsDefault: policy.IsDefault,
Description: policy.Description,
CreatedAt: policy.CreatedAt.Format("2006-01-02T15:04:05Z"),
UpdatedAt: policy.UpdatedAt.Format("2006-01-02T15:04:05Z"),
}
}
// ToNetworkResponseList 批量转换 Network
func ToNetworkResponseList(networks []model.Network) []NetworkResponse {
result := make([]NetworkResponse, 0, len(networks))
for _, n := range networks {
result = append(result, ToNetworkResponse(&n))
}
return result
}
// ToDeviceResponseList 批量转换 Device
func ToDeviceResponseList(devices []model.Device) []DeviceResponse {
result := make([]DeviceResponse, 0, len(devices))
for _, d := range devices {
result = append(result, ToDeviceResponse(&d))
}
return result
}
// ToPolicyResponseList 批量转换 Policy
func ToPolicyResponseList(policies []model.Policy) []PolicyResponse {
result := make([]PolicyResponse, 0, len(policies))
for _, p := range policies {
result = append(result, ToPolicyResponse(&p))
}
return result
}
+195
View File
@@ -0,0 +1,195 @@
package handler
import (
"net/http"
"git.zkcoi.com/zkcoi/meshray/internal/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// AdminHandler 管理员 Handler
type AdminHandler struct {
userService *service.UserService
restartCoreSvc *service.RestartCoreService
logger *zap.Logger
}
// NewAdminHandler 创建管理员 Handler
func NewAdminHandler(userService *service.UserService, logger *zap.Logger) *AdminHandler {
return &AdminHandler{
userService: userService,
restartCoreSvc: service.NewRestartCoreService(logger),
logger: logger,
}
}
// GetProfile 获取管理员信息
func (h *AdminHandler) GetProfile(c *gin.Context) {
// 从上下文获取用户 ID(由 JWT 中间件注入)
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
return
}
// 获取用户信息
user, err := h.userService.GetUserByID(userID.(uint))
if err != nil {
h.logger.Error("获取管理员信息失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取信息失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"id": user.ID,
"username": user.Username,
"email": user.Email,
"role": user.Role,
"status": user.Status,
"created_at": user.CreatedAt,
},
})
}
// UpdateProfileRequest 更新资料请求
type UpdateProfileRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"` // 可选,留空则不修改
}
// UpdateProfile 更新管理员资料
func (h *AdminHandler) UpdateProfile(c *gin.Context) {
var req UpdateProfileRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
// 从上下文获取当前用户名(用于验证)
currentUsername, _ := c.Get("username")
// 如果修改了用户名,需要验证不为空
if req.Username != "" && req.Username != currentUsername {
// 用户名不能为空
if len(req.Username) < 3 {
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名至少 3 个字符"})
return
}
}
// 如果提供了密码,验证长度
if req.Password != "" && len(req.Password) < 6 {
c.JSON(http.StatusBadRequest, gin.H{"error": "密码至少 6 个字符"})
return
}
// 如果提供了邮箱,验证格式
if req.Email != "" {
if len(req.Email) < 5 || len(req.Email) > 255 {
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式无效"})
return
}
}
// 调用服务层更新
err := h.userService.UpdateAdminProfile(req.Username, req.Email, req.Password)
if err != nil {
h.logger.Error("更新管理员资料失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
h.logger.Info("管理员资料已更新",
zap.String("username", req.Username),
zap.Bool("password_changed", req.Password != ""))
c.JSON(http.StatusOK, gin.H{
"message": "资料已更新",
"data": gin.H{
"username": req.Username,
"email": req.Email,
"password_changed": req.Password != "",
},
})
}
// ChangePassword 修改密码
func (h *AdminHandler) ChangePassword(c *gin.Context) {
var req ChangePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
// 从上下文获取用户 ID
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
return
}
// 调用服务层修改密码
err := h.userService.ChangePassword(userID.(uint), &service.ChangePasswordRequest{
OldPassword: req.OldPassword,
NewPassword: req.NewPassword,
})
if err != nil {
h.logger.Error("修改密码失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
h.logger.Info("密码修改成功",
zap.Uint("user_id", userID.(uint)))
c.JSON(http.StatusOK, gin.H{
"message": "密码已修改",
})
}
// AdminRestartCoreRequest 重启核心服务请求(管理员接口)
type AdminRestartCoreRequest struct {
Force bool `json:"force"` // 是否强制重启
}
// RestartCore 重启核心服务
func (h *AdminHandler) RestartCore(c *gin.Context) {
var req AdminRestartCoreRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
// 从上下文获取用户 ID(验证管理员权限)
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
return
}
// 验证是否为管理员
user, err := h.userService.GetUserByID(userID.(uint))
if err != nil || user.Role != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
return
}
// 调用服务层重启核心
err = h.restartCoreSvc.RestartCore()
if err != nil {
h.logger.Error("重启核心服务失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
h.logger.Info("核心服务已重启",
zap.Uint("user_id", userID.(uint)),
zap.Bool("force", req.Force))
c.JSON(http.StatusOK, gin.H{
"message": "核心服务正在重启",
})
}
+376
View File
@@ -0,0 +1,376 @@
package handler
import (
"net"
"net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/model"
sqlite "git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
"github.com/gin-gonic/gin"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/host"
"github.com/shirou/gopsutil/v4/mem"
gopsutilNet "github.com/shirou/gopsutil/v4/net"
"go.uber.org/zap"
)
// DashboardHandler Dashboard Handler
type DashboardHandler struct {
logger *zap.Logger
store *sqlite.Store
}
// NewDashboardHandler 创建 Dashboard Handler
func NewDashboardHandler(store *sqlite.Store, logger *zap.Logger) *DashboardHandler {
return &DashboardHandler{
logger: logger,
store: store,
}
}
// GetStats 获取统计数据
func (h *DashboardHandler) GetStats(c *gin.Context) {
var deviceCount, networkCount, onlineCount int64
// 统计设备数量
h.store.DB().Model(&model.Device{}).Count(&deviceCount)
// 统计网络数量
h.store.DB().Model(&model.Network{}).Count(&networkCount)
// 统计在线设备数量
h.store.DB().Model(&model.Device{}).Where("status = ?", "online").Count(&onlineCount)
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"total_devices": deviceCount,
"total_networks": networkCount,
"online_devices": onlineCount,
},
})
}
// GetRecentLogs 获取最近日志
func (h *DashboardHandler) GetRecentLogs(c *gin.Context) {
limitStr := c.DefaultQuery("limit", "10")
limit := 10
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
limit = l
}
// 从数据库查询最近的 AuditLog
var logs []model.AuditLog
if err := h.store.DB().
Order("created_at DESC").
Limit(limit).
Find(&logs).Error; err != nil {
h.logger.Error("查询日志失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "查询日志失败",
})
return
}
// 转换为前端格式
type LogEntry struct {
ID uint64 `json:"id"`
Timestamp string `json:"timestamp"`
Message string `json:"message"`
Action string `json:"action"`
OperatorIP string `json:"operator_ip"`
Type string `json:"type"` // info, success, warning, error
}
logEntries := make([]LogEntry, 0, len(logs))
for _, log := range logs {
// 从 Detail 中提取信息构建 Message
message := log.Action
if log.Detail != "" {
message = log.Detail
}
logEntries = append(logEntries, LogEntry{
ID: uint64(log.ID),
Timestamp: log.CreatedAt.Format(time.RFC3339),
Message: message,
Action: log.Action,
OperatorIP: log.OperatorIP,
Type: "info", // AuditLog 默认类型
})
}
c.JSON(http.StatusOK, gin.H{
"data": logEntries,
})
}
// GetSystemInfo 获取系统信息
func (h *DashboardHandler) GetSystemInfo(c *gin.Context) {
// 获取主机信息
hostInfo, err := host.Info()
if err != nil {
h.logger.Warn("获取主机信息失败", zap.Error(err))
hostInfo = &host.InfoStat{} // 使用空对象避免 nil 指针
}
// 获取 CPU 信息
cpuPercent, _ := cpu.Percent(0, false)
cpuUsage := float64(0)
if len(cpuPercent) > 0 {
cpuUsage = cpuPercent[0]
}
// 获取内存信息
memInfo, _ := mem.VirtualMemory()
// 获取磁盘信息
diskInfo, _ := disk.Usage("/")
// Windows 下使用 C:\
if runtime.GOOS == "windows" {
diskInfo, _ = disk.Usage("C:/")
}
// 获取网络 IO
netIO, _ := gopsutilNet.IOCounters(false)
// 获取本机 IPIPv4 + IPv6
ipv4, ipv6 := getLocalIPs()
ip := ipv4 // 默认返回 IPv4
if ipv4 == "unknown" && ipv6 != "unknown" {
ip = ipv6 // 如果只有 IPv6,则返回 IPv6
}
// 计算运行时长(小时)
uptimeHours := float64(hostInfo.Uptime) / 3600.0
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
// 基础信息
"hostname": hostInfo.Hostname,
"os": hostInfo.OS,
"platform": hostInfo.Platform,
"os_version": hostInfo.PlatformVersion,
"kernel": hostInfo.KernelVersion,
"arch": runtime.GOARCH,
// CPU
"cpu_count": runtime.NumCPU(),
"cpu_usage": cpuUsage,
"cpu_model": getCPUModel(), // 从 cpu.Info() 获取
// 内存
"memory_total": int(memInfo.Total / 1024 / 1024), // MB
"memory_used": int(memInfo.Used / 1024 / 1024), // MB
"memory_percent": memInfo.UsedPercent,
"memory_alloc": int(runtime.MemStats{}.Alloc / 1024 / 1024), // Go 内存
// 磁盘
"disk_total": int(diskInfo.Total / 1024 / 1024 / 1024), // GB
"disk_used": int(diskInfo.Used / 1024 / 1024 / 1024), // GB
"disk_percent": diskInfo.UsedPercent,
// 网络
"net_sent": netIO[0].BytesSent,
"net_recv": netIO[0].BytesRecv,
// 运行时长
"boot_time": formatBootTime(hostInfo),
"uptime": uptimeHours,
// IP 地址(同时返回 IPv4 和 IPv6)
"ip": ip, // 保持向后兼容
"ipv4": ipv4, // IPv4 地址
"ipv6": ipv6, // IPv6 地址
},
})
}
// getLocalIPs 获取本机 IPv4 和 IPv6 地址
func getLocalIPs() (ipv4, ipv6 string) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "unknown", "unknown"
}
for _, addr := range addrs {
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
// 检查是否为公网地址(可选,目前保留私有地址)
if ipNet.IP.To4() != nil {
// 优先选择非链路本地地址
if ipv4 == "" || !ipNet.IP.IsLinkLocalUnicast() {
ipv4 = ipNet.IP.String()
}
} else if ipNet.IP.To16() != nil {
// IPv6
if ipv6 == "" || !ipNet.IP.IsLinkLocalUnicast() {
ipv6 = ipNet.IP.String()
}
}
}
}
if ipv4 == "" {
ipv4 = "unknown"
}
if ipv6 == "" {
ipv6 = "unknown"
}
return ipv4, ipv6
}
// formatBootTime 格式化启动时间
func formatBootTime(hostInfo *host.InfoStat) string {
if hostInfo == nil || hostInfo.BootTime == 0 {
return "unknown"
}
return time.Unix(int64(hostInfo.BootTime), 0).Format("2006-01-02 15:04:05")
}
// GetLinkDistribution 获取链路分布
func (h *DashboardHandler) GetLinkDistribution(c *gin.Context) {
// 查询所有网络
var networks []model.Network
if err := h.store.DB().Find(&networks).Error; err != nil {
h.logger.Error("查询网络失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "查询网络失败",
})
return
}
// 构建链路分布数据
distribution := make([]gin.H, 0, len(networks))
totalP2P := 0
totalRelay := 0
for _, network := range networks {
// 查询该网络下的所有设备
var devices []model.Device
if err := h.store.DB().Where("network_id = ?", network.ID).Find(&devices).Error; err != nil {
h.logger.Warn("查询设备失败", zap.Uint64("network_id", network.ID), zap.Error(err))
continue
}
// 统计 P2P 和 Relay 连接
p2pCount := 0
relayCount := 0
for _, device := range devices {
// 根据 Endpoint 判断是 P2P 还是 Relay
// 如果 Endpoint 为空或为内网地址,认为是 P2P
// 如果 Endpoint 包含中继端口(如 53493),认为是 Relay
if device.Endpoint == "" {
p2pCount++
} else if isRelayEndpoint(device.Endpoint) {
relayCount++
} else {
p2pCount++
}
}
totalP2P += p2pCount
totalRelay += relayCount
distribution = append(distribution, gin.H{
"network_id": network.ID,
"network_name": network.Name,
"p2p_count": p2pCount,
"relay_count": relayCount,
"total_peers": len(devices),
"mode": network.Mode, // native / userspace
})
}
// 返回总体统计和详细分布
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"summary": gin.H{
"total_p2p": totalP2P,
"total_relay": totalRelay,
"total": totalP2P + totalRelay,
"p2p_percent": calculatePercent(totalP2P, totalP2P+totalRelay),
},
"by_network": distribution,
},
})
}
// isRelayEndpoint 判断是否为中继端点
func isRelayEndpoint(endpoint string) bool {
// 简单的启发式判断:如果端口在常见 TURN 端口范围内
_, port, err := net.SplitHostPort(endpoint)
if err != nil {
return false
}
// 常见 TURN/Relay 端口
relayPorts := []string{"53493", "53494", "53495", "3478", "5349"}
for _, p := range relayPorts {
if port == p {
return true
}
}
return false
}
// calculatePercent 计算百分比
func calculatePercent(part, total int) float64 {
if total == 0 {
return 0
}
return float64(part) / float64(total) * 100
}
// ClearLogs 清理日志文件
func (h *DashboardHandler) ClearLogs(c *gin.Context) {
logPath := "./logs"
// 检查日志目录是否存在
if _, err := os.Stat(logPath); os.IsNotExist(err) {
c.JSON(http.StatusOK, gin.H{
"message": "日志目录不存在",
})
return
}
// 读取所有日志文件
files, err := filepath.Glob(filepath.Join(logPath, "*.log"))
if err != nil {
h.logger.Error("读取日志文件失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "读取日志文件失败",
})
return
}
// 删除所有日志文件
deletedCount := 0
for _, file := range files {
if err := os.Remove(file); err != nil {
h.logger.Warn("删除日志文件失败", zap.String("file", file), zap.Error(err))
continue
}
deletedCount++
}
h.logger.Info("清理日志完成", zap.Int("deleted", deletedCount))
c.JSON(http.StatusOK, gin.H{
"message": "日志清理完成",
"deleted": deletedCount,
})
}
// getCPUModel 获取 CPU 型号
func getCPUModel() string {
cpuInfos, err := cpu.Info()
if err != nil || len(cpuInfos) == 0 {
return "Unknown"
}
return cpuInfos[0].ModelName
}
+141
View File
@@ -0,0 +1,141 @@
package handler
import (
"net/http"
"git.zkcoi.com/zkcoi/meshray/internal/service"
"github.com/gin-gonic/gin"
)
// DDNSHandler DDNS 配置处理器
type DDNSHandler struct {
ddnsService *service.DDNSService
}
// NewDDNSHandler 创建 DDNS 处理器
func NewDDNSHandler(ddnsService *service.DDNSService) *DDNSHandler {
return &DDNSHandler{
ddnsService: ddnsService,
}
}
// DDNSConfigRequest DDNS 配置请求
type DDNSConfigRequest struct {
Provider string `json:"provider"`
AccessKeyID string `json:"access_key_id"`
AccessKeySecret string `json:"access_key_secret"`
Domain string `json:"domain"`
TxtRecordName string `json:"txt_record_name"`
SyncMode string `json:"sync_mode"`
RetryInterval int `json:"retry_interval"`
MaxRetries int `json:"max_retries"`
Enabled bool `json:"enabled"`
}
// GetDDNSConfig 获取 DDNS 配置
// GET /api/v1/ddns/config
func (h *DDNSHandler) GetDDNSConfig(c *gin.Context) {
config, err := h.ddnsService.GetConfig(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": config,
})
}
// UpdateDDNSConfig 更新 DDNS 配置
// PUT /api/v1/ddns/config
func (h *DDNSHandler) UpdateDDNSConfig(c *gin.Context) {
var req DDNSConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "参数错误:" + err.Error(),
})
return
}
if req.Provider == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请选择 DNS 提供商",
})
return
}
if req.AccessKeyID == "" || req.AccessKeySecret == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请输入完整的访问密钥",
})
return
}
if req.Domain == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请输入域名",
})
return
}
if req.TxtRecordName == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请输入 TXT 记录名称",
})
return
}
if err := h.ddnsService.UpdateConfig(c.Request.Context(), req); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "配置已保存",
})
}
// TestDDNSRequest 测试请求
type TestDDNSRequest struct {
Provider string `json:"provider"`
AccessKeyID string `json:"access_key_id"`
AccessKeySecret string `json:"access_key_secret"`
Domain string `json:"domain"`
TxtRecordName string `json:"txt_record_name"`
}
// TestDDNSConnectivity 测试 DDNS 连通性
// POST /api/v1/ddns/test
func (h *DDNSHandler) TestDDNSConnectivity(c *gin.Context) {
var req TestDDNSRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "参数错误:" + err.Error(),
})
return
}
results := h.ddnsService.TestConnectivity(c.Request.Context(), req)
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"results": results,
},
})
}
// SyncDDNS 手动同步 DDNS
// POST /api/v1/ddns/sync
func (h *DDNSHandler) SyncDDNS(c *gin.Context) {
if err := h.ddnsService.SyncNow(c.Request.Context()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "同步成功",
})
}
+133
View File
@@ -0,0 +1,133 @@
package handler
import (
"encoding/json"
"net/http"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// DDNSStatsHandler DDNS 统计处理器
type DDNSStatsHandler struct {
db *gorm.DB
}
// NewDDNSStatsHandler 创建 DDNS 统计处理器
func NewDDNSStatsHandler(db *gorm.DB) *DDNSStatsHandler {
return &DDNSStatsHandler{
db: db,
}
}
// DDNSServiceInfo DDNS 服务信息(返回给前端)
type DDNSServiceInfo struct {
ID string `json:"id"`
Name string `json:"name"`
FullDomain string `json:"full_domain"` // 完整域名:subdomain.domain
CurrentIP string `json:"current_ip"` // 当前 IP
RecordType string `json:"record_type"` // A/AAAA/TXT/CNAME
Enabled bool `json:"enabled"` // 是否启用
Status string `json:"status"` // active/disabled
LastUpdated string `json:"last_updated"` // 最后更新时间
}
// GetDDNSStats 获取 DDNS 统计数据
func (h *DDNSStatsHandler) GetDDNSStats(c *gin.Context) {
// 查询所有 DDNS 全功能模式服务
var services []model.Service
if err := h.db.Where("type = ? AND config_mode = ?", "DDNS", "fullservice").Find(&services).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "查询 DDNS 服务失败:" + err.Error(),
})
return
}
// 统计数据
total := len(services)
active := 0
// 构建服务列表
serviceList := make([]DDNSServiceInfo, 0, len(services))
for _, svc := range services {
status := "disabled"
if svc.Enabled {
status = "active"
active++
}
fullDomain := ""
rootDomain := h.getDDNSDomain(svc.DDNSConfigID)
switch svc.RecordType {
case "A", "AAAA":
if svc.Subdomain != "" && rootDomain != "" {
fullDomain = svc.Subdomain + "." + rootDomain
}
case "TXT":
if svc.TXTRecordName != "" && rootDomain != "" {
fullDomain = svc.TXTRecordName + "." + rootDomain
}
case "CNAME":
if svc.Subdomain != "" && rootDomain != "" {
fullDomain = svc.Subdomain + "." + rootDomain
}
}
currentIP := svc.TargetIP
if svc.RecordType == "TXT" {
currentIP = "-"
}
lastUpdated := ""
if !svc.UpdatedAt.IsZero() {
lastUpdated = svc.UpdatedAt.Format(time.RFC3339)
}
serviceList = append(serviceList, DDNSServiceInfo{
ID: svc.ID,
Name: svc.Name,
FullDomain: fullDomain,
CurrentIP: currentIP,
RecordType: svc.RecordType,
Enabled: svc.Enabled,
Status: status,
LastUpdated: lastUpdated,
})
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": gin.H{
"total": total,
"active": active,
"services": serviceList,
},
"message": "获取成功",
})
}
// getDDNSDomain 获取 DDNS 配置中的根域名
func (h *DDNSStatsHandler) getDDNSDomain(ddnsConfigID string) string {
if ddnsConfigID == "" {
return ""
}
var extService model.ExternalService
if err := h.db.Where("id = ?", ddnsConfigID).First(&extService).Error; err != nil {
return ""
}
var config map[string]interface{}
if err := json.Unmarshal([]byte(extService.Config), &config); err != nil {
return ""
}
if rootDomain, ok := config["root_domain"].(string); ok {
return rootDomain
}
return ""
}
+313
View File
@@ -0,0 +1,313 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/pkg/shortid"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
"gorm.io/gorm"
)
// DDNSUsageHandler DDNS Usage 管理 Handler
type DDNSUsageHandler struct {
db *gorm.DB
logger *zap.Logger
}
// NewDDNSUsageHandler 创建 DDNS Usage Handler
func NewDDNSUsageHandler(db *gorm.DB, logger *zap.Logger) *DDNSUsageHandler {
return &DDNSUsageHandler{
db: db,
logger: logger,
}
}
// CreateUsageRequest 创建 Usage 请求
type CreateUsageRequest struct {
ServiceID string `json:"service_id"` // DDNS 服务 ID
PrefixMode string `json:"prefix_mode"` // "auto" | "custom"
CustomPrefix string `json:"custom_prefix"` // 自定义前缀(仅 custom 模式需要)
NetworkID uint64 `json:"network_id"` // 绑定的网络 ID
NetworkName string `json:"network_name"` // 网络名称(用于显示)
}
// CreateUsage 创建 DDNS Usage
// POST /api/v1/ddns/usages
func (h *DDNSUsageHandler) CreateUsage(c *gin.Context) {
var req CreateUsageRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
// 验证必填字段
if req.ServiceID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择 DDNS 服务"})
return
}
if req.PrefixMode != "auto" && req.PrefixMode != "custom" {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的前缀模式"})
return
}
// 开启事务
tx := h.db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 1. 验证 DDNS 服务存在
var service model.ExternalService
if err := tx.First(&service, req.ServiceID).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusNotFound, gin.H{"error": "DDNS 服务不存在"})
return
}
// 2. 解析配置获取域名
var config map[string]interface{}
if err := json.Unmarshal([]byte(service.Config), &config); err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "解析 DDNS 配置失败"})
return
}
domain, _ := config["domain"].(string)
if domain == "" {
tx.Rollback()
c.JSON(http.StatusBadRequest, gin.H{"error": "DDNS 配置缺少域名"})
return
}
// 3. 根据模式生成前缀
var recordPrefix string
if req.PrefixMode == "auto" {
// ✅ 算法生成:Base64(NetworkID)
recordPrefix = shortid.EncodeID(req.NetworkID)
h.logger.Debug("自动生成前缀",
zap.Uint64("network_id", req.NetworkID),
zap.String("prefix", recordPrefix))
} else if req.PrefixMode == "custom" {
// ✅ 用户自定义:验证格式 + 检测占用
if err := validateCustomPrefix(req.CustomPrefix); err != nil {
tx.Rollback()
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 检测是否被占用
var count int64
tx.Model(&model.DDNSUsage{}).
Where("service_id = ? AND record_prefix = ?", req.ServiceID, req.CustomPrefix).
Count(&count)
if count > 0 {
tx.Rollback()
c.JSON(http.StatusBadRequest, gin.H{"error": "该前缀已被其他组网占用"})
return
}
recordPrefix = req.CustomPrefix
h.logger.Debug("使用自定义前缀",
zap.String("prefix", req.CustomPrefix))
}
// 4. 创建 Usage 记录
usage := &model.DDNSUsage{
ID: uuid.New().String(),
ProviderID: req.ServiceID, // ✅ 使用 ProviderID 字段
UsageType: "meshseed_sync", // MeshSeed 同步用途
PrefixMode: req.PrefixMode,
RecordPrefix: recordPrefix,
RecordType: "TXT", // MeshSeed 同步固定使用 TXT 记录
Description: fmt.Sprintf("MeshSeed 同步 - %s", req.NetworkName),
IsExclusive: true, // 独占性,一个前缀只能绑定一个网络
}
if err := tx.Create(usage).Error; err != nil {
tx.Rollback()
h.logger.Error("创建 Usage 失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建失败:" + err.Error()})
return
}
// 5. 创建绑定关系
binding := &model.NetworkDDNSBinding{
ID: uuid.New().String(),
NetworkID: req.NetworkID,
UsageID: usage.ID,
ProviderID: req.ServiceID,
Status: "active",
}
if err := tx.Create(binding).Error; err != nil {
tx.Rollback()
h.logger.Error("创建绑定关系失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建绑定失败:" + err.Error()})
return
}
// 提交事务
if err := tx.Commit().Error; err != nil {
h.logger.Error("事务提交失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "提交失败"})
return
}
// 构造完整域名
fullDomain := fmt.Sprintf("_meshray.%s.%s", recordPrefix, domain)
h.logger.Info("DDNS Usage 创建成功",
zap.String("usage_id", usage.ID),
zap.Uint64("network_id", req.NetworkID),
zap.String("full_domain", fullDomain))
c.JSON(http.StatusOK, gin.H{
"message": "创建成功",
"data": gin.H{
"id": usage.ID,
"provider_id": usage.ProviderID,
"prefix_mode": usage.PrefixMode,
"record_prefix": recordPrefix,
"full_domain": fullDomain,
"network_id": req.NetworkID,
},
})
}
// CheckPrefixOccupied 检查前缀是否被占用
// GET /api/v1/ddns/check-prefix?service_id=xxx&prefix=yyy
func (h *DDNSUsageHandler) CheckPrefixOccupied(c *gin.Context) {
serviceID := c.Query("service_id")
prefix := c.Query("prefix")
if serviceID == "" || prefix == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少必要参数"})
return
}
// 查询占用情况
var count int64
h.db.Model(&model.DDNSUsage{}).
Where("service_id = ? AND record_prefix = ?", serviceID, prefix).
Count(&count)
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"occupied": count > 0,
"count": count,
},
})
}
// GetAvailableUsages 获取可用的 DDNS Usage 列表
// GET /api/v1/ddns/usages/available
func (h *DDNSUsageHandler) GetAvailableUsages(c *gin.Context) {
serviceID := c.Query("service_id")
// 查询所有 TXT 类型的 Usage
query := h.db.Where("record_type = ?", "TXT").
Preload("Provider")
if serviceID != "" {
query = query.Where("service_id = ?", serviceID)
}
var usages []model.DDNSUsage
if err := query.Find(&usages).Error; err != nil {
h.logger.Error("查询 Usage 列表失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败"})
return
}
// 构造返回数据
type UsageVO struct {
ID string `json:"id"`
ProviderID string `json:"provider_id"`
PrefixMode string `json:"prefix_mode"`
RecordPrefix string `json:"record_prefix"`
RecordType string `json:"record_type"`
Description string `json:"description"`
IsOccupied bool `json:"is_occupied"`
NetworkID *uint64 `json:"network_id,omitempty"`
FullDomain string `json:"full_domain"`
Provider interface{} `json:"provider,omitempty"`
}
result := make([]UsageVO, 0, len(usages))
for _, usage := range usages {
// 获取完整域名
var provider model.ExternalService
h.db.First(&provider, usage.ProviderID)
var config map[string]interface{}
json.Unmarshal([]byte(provider.Config), &config)
domain, _ := config["domain"].(string)
fullDomain := fmt.Sprintf("_meshray.%s.%s", usage.RecordPrefix, domain)
// 从 NetworkDDNSBinding 表查询关联的 Network ID
var networkID *uint64
var binding model.NetworkDDNSBinding
if err := h.db.Where("ddns_usage_id = ?", usage.ID).First(&binding).Error; err == nil {
networkID = &binding.NetworkID
}
vo := UsageVO{
ID: usage.ID,
ProviderID: usage.ProviderID,
PrefixMode: usage.PrefixMode,
RecordPrefix: usage.RecordPrefix,
RecordType: usage.RecordType,
Description: usage.Description,
IsOccupied: usage.IsExclusive,
NetworkID: networkID,
FullDomain: fullDomain,
}
if usage.Provider != nil {
vo.Provider = gin.H{
"id": usage.Provider.ID,
"name": usage.Provider.Name,
}
}
result = append(result, vo)
}
c.JSON(http.StatusOK, gin.H{"data": result})
}
// validateCustomPrefix 验证自定义前缀
func validateCustomPrefix(prefix string) error {
if prefix == "" {
return fmt.Errorf("前缀不能为空")
}
if len(prefix) > 63 {
return fmt.Errorf("前缀不能超过 63 个字符")
}
// DNS 标签规范
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, prefix)
if !matched {
return fmt.Errorf("前缀只能包含字母、数字、下划线和连字符")
}
// 系统保留字检查
if strings.HasPrefix(prefix, "_meshray") {
return fmt.Errorf("_meshray.* 前缀为系统保留,请使用自动生成模式")
}
return nil
}
+263
View File
@@ -0,0 +1,263 @@
package handler
import (
"net/http"
"strconv"
"git.zkcoi.com/zkcoi/meshray/internal/api/dto"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// DeviceHandler 设备管理 Handler
type DeviceHandler struct {
deviceService *service.DeviceService
logger *zap.Logger
}
// NewDeviceHandler 创建设备处理器
func NewDeviceHandler(deviceService *service.DeviceService, logger *zap.Logger) *DeviceHandler {
return &DeviceHandler{
deviceService: deviceService,
logger: logger,
}
}
// CreateDevice 创建设备
// @Summary 在指定网络下创建设备
// @Tags devices
// @Accept json
// @Produce json
// @Param network_id path int true "网络 ID"
// @Param device body model.Device true "设备配置"
// @Success 200 {object} model.Device
// @Router /api/v1/networks/:network_id/devices [post]
func (h *DeviceHandler) CreateDevice(c *gin.Context) {
// 从 Query 参数获取 network_id
networkIDStr := c.Query("network_id")
networkID, err := strconv.ParseUint(networkIDStr, 10, 64)
if err != nil {
h.logger.Error("解析网络 ID 失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的网络 ID"})
return
}
var req service.CreateDeviceRequest
req.NetworkID = networkID
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
// 使用 Service 层创建设备(返回完整配置)
result, err := h.deviceService.CreateDevice(&req)
if err != nil {
h.logger.Error("创建设备失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Info("设备创建成功",
zap.Uint64("id", result.Device.ID),
zap.String("name", result.Device.Name),
zap.Uint64("network_id", result.Device.NetworkID))
// 返回完整配置(包含私钥和 WG 配置)
c.JSON(http.StatusOK, gin.H{
"message": "设备创建成功",
"data": gin.H{
"device": result.Device,
"private_key": result.PrivateKey,
"wireguard_config": result.ConfigText,
},
})
}
// GetDevice 获取设备详情
// @Summary 获取设备详细信息
// @Tags devices
// @Accept json
// @Produce json
// @Param id path int true "设备 ID"
// @Success 200 {object} model.Device
// @Router /api/v1/devices/:id [get]
// GetDevice 获取设备详情
func (h *DeviceHandler) GetDevice(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
h.logger.Error("解析设备 ID 失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的设备 ID"})
return
}
device, err := h.deviceService.GetDevice(id)
if err != nil {
h.logger.Error("获取设备失败", zap.Error(err))
c.JSON(http.StatusNotFound, gin.H{"error": "设备不存在"})
return
}
// 使用 DTO 转换
resp := dto.ToDeviceResponse(device)
c.JSON(http.StatusOK, gin.H{
"data": resp,
})
}
// ListDevices 获取设备列表
// @Summary 获取指定网络下的设备列表
// @Tags devices
// @Accept json
// @Produce json
// @Param network_id path int true "网络 ID"
// @Success 200 {array} model.Device
// @Router /api/v1/networks/:network_id/devices [get]
// ListDevices 获取设备列表
func (h *DeviceHandler) ListDevices(c *gin.Context) {
networkIDStr := c.Query("network_id")
var devices []model.Device
var err error
if networkIDStr != "" {
networkID, err := strconv.ParseUint(networkIDStr, 10, 64)
if err != nil {
h.logger.Error("解析网络 ID 失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的网络 ID"})
return
}
devices, err = h.deviceService.ListDevicesByNetwork(networkID)
} else {
devices, err = h.deviceService.ListAllDevices()
}
if err != nil {
h.logger.Error("查询设备列表失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败"})
return
}
// 使用 DTO 批量转换
respList := dto.ToDeviceResponseList(devices)
c.JSON(http.StatusOK, gin.H{
"data": respList,
})
}
// UpdateDevice 更新设备
// @Summary 更新设备信息
// @Tags devices
// @Accept json
// @Produce json
// @Param id path int true "设备 ID"
// @Param device body model.Device true "设备配置"
// @Success 200 {object} model.Device
// @Router /api/v1/devices/:id [put]
func (h *DeviceHandler) UpdateDevice(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
h.logger.Error("解析设备 ID 失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的设备 ID"})
return
}
var req struct {
Name string `json:"name"`
VirtualIP string `json:"virtual_ip"`
Status string `json:"status"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
// 构建更新字段
updates := make(map[string]interface{})
if req.Name != "" {
updates["name"] = req.Name
}
if req.VirtualIP != "" {
updates["virtual_ip"] = req.VirtualIP
}
if req.Status != "" {
updates["status"] = req.Status
}
// 使用 Service 层更新
device, err := h.deviceService.UpdateDevice(id, updates)
if err != nil {
h.logger.Error("更新设备失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Info("设备更新成功", zap.Uint64("id", device.ID))
c.JSON(http.StatusOK, gin.H{
"message": "更新成功",
"data": device,
})
}
// DeleteDevice 删除设备
// @Summary 删除指定设备
// @Tags devices
// @Accept json
// @Produce json
// @Param id path int true "设备 ID"
// @Success 200
// @Router /api/v1/devices/:id [delete]
func (h *DeviceHandler) DeleteDevice(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
h.logger.Error("解析设备 ID 失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的设备 ID"})
return
}
// 使用 Service 层删除
err = h.deviceService.DeleteDevice(id)
if err != nil {
h.logger.Error("删除设备失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Info("设备已删除", zap.Uint64("id", id))
c.JSON(http.StatusOK, gin.H{
"message": "删除成功",
})
}
// GenerateDeviceConfig 生成设备配置文件
// @Summary 生成 WireGuard 设备配置
// @Tags devices
// @Accept json
// @Produce json
// @Param id path int true "设备 ID"
// @Success 200 {string} string "配置文件内容"
// @Router /api/v1/devices/:id/config [get]
func (h *DeviceHandler) GenerateDeviceConfig(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
h.logger.Error("解析设备 ID 失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的设备 ID"})
return
}
// 调用 Service 层生成配置(包含密钥生成)
config, err := h.deviceService.GenerateDeviceConfig(id)
if err != nil {
h.logger.Error("生成配置失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Data(http.StatusOK, "text/plain", []byte(config))
}
+57
View File
@@ -0,0 +1,57 @@
package handler
import (
"net/http"
"git.zkcoi.com/zkcoi/meshray/internal/service"
"github.com/gin-gonic/gin"
)
// IPDetectHandler IP 检测处理器
type IPDetectHandler struct {
ipDetection *service.IPDetectionService
}
// NewIPDetectHandler 创建 IP 检测处理器
func NewIPDetectHandler() *IPDetectHandler {
return &IPDetectHandler{
ipDetection: service.NewIPDetectionService(),
}
}
// DetectIP 检测公网 IP 地址
// @Summary 检测公网 IP 地址
// @Tags DDNS
// @Accept json
// @Produce json
// @Param record_type query string false "记录类型 (A|AAAA)" default(A)
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/services/ddns/detect-ip [get]
func (h *IPDetectHandler) DetectIP(c *gin.Context) {
recordType := c.DefaultQuery("record_type", "A")
if recordType != "A" && recordType != "AAAA" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "不支持的记录类型,仅支持 A 或 AAAA",
})
return
}
ip, err := h.ipDetection.DetectIP(recordType)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "检测失败:" + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": gin.H{
"ip": ip,
},
"message": "检测成功",
})
}
+549
View File
@@ -0,0 +1,549 @@
package handler
import (
"net/http"
"strconv"
"strings"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/api/dto"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/service"
sqlite "git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// NetworkHandler 网络管理 Handler
type NetworkHandler struct {
networkService *service.NetworkService
meshSeedService *service.MeshSeedService // 新增:MeshSeed 服务
store *sqlite.Store // 新增:用于存储 PendingJoin
logger *zap.Logger
}
// NewNetworkHandler 创建 Network Handler
func NewNetworkHandler(networkService *service.NetworkService, meshSeedService *service.MeshSeedService, store *sqlite.Store, logger *zap.Logger) *NetworkHandler {
return &NetworkHandler{
networkService: networkService,
meshSeedService: meshSeedService,
store: store,
logger: logger,
}
}
// CreateNetworkResponse 创建网络响应(包含完整配置信息)
type CreateNetworkResponse struct {
*model.Network
STUNServers []model.Service `json:"stun_servers"` // STUN 服务器列表
TURNServers []model.Service `json:"turn_servers"` // TURN 服务器列表
DDNSConfig *DDNSConfigInfo `json:"ddns_config,omitempty"` // DDNS 配置信息(如果启用)
}
// DDNSConfigInfo DDNS 配置信息
type DDNSConfigInfo struct {
Provider string `json:"provider"` // 服务商
Domain string `json:"domain"` // 域名
RecordType string `json:"record_type"` // 记录类型
Prefix string `json:"prefix"` // 前缀
}
// CreateNetwork 创建网络
// @Summary 创建新的 WireGuard 网络
// @Tags networks
// @Accept json
// @Produce json
// @Param network body model.Network true "网络配置"
// @Success 200 {object} CreateNetworkResponse
// @Router /api/v1/networks [post]
func (h *NetworkHandler) CreateNetwork(c *gin.Context) {
var req model.Network
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
// 使用 Service 层创建网络
network, err := h.networkService.CreateNetwork(&req)
if err != nil {
h.logger.Error("创建网络失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 查询关联的 STUN/TURN 服务器
var stunServers []model.Service
h.store.DB().Where("type = 'STUN' AND enabled = true").Find(&stunServers)
var turnServers []model.Service
h.store.DB().Where("type = 'TURN' AND enabled = true").Find(&turnServers)
// 构建响应
resp := &CreateNetworkResponse{
Network: network,
STUNServers: stunServers,
TURNServers: turnServers,
}
// 如果启用了 DDNS,查询 DDNS 配置
if network.DDNSEnabled && network.DDNSServiceID != "" {
var ddnsService model.Service
if err := h.store.DB().First(&ddnsService, network.DDNSServiceID).Error; err == nil {
resp.DDNSConfig = &DDNSConfigInfo{
Provider: ddnsService.Provider,
Domain: ddnsService.Domain,
RecordType: ddnsService.RecordType,
Prefix: network.DDNSPrefix,
}
}
}
h.logger.Info("网络创建成功",
zap.String("name", network.Name),
zap.Uint64("id", network.ID),
zap.Int("stun_count", len(stunServers)),
zap.Int("turn_count", len(turnServers)))
c.JSON(http.StatusOK, gin.H{
"message": "网络创建成功",
"data": resp,
})
}
// GetNetwork 获取网络详情
// @Summary 获取网络详细信息
// @Tags networks
// @Accept json
// @Produce json
// @Param id path string true "网络 ID"
// @Success 200 {object} model.Network
// @Router /api/v1/networks/:id [get]
func (h *NetworkHandler) GetNetwork(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
h.logger.Error("解析网络 ID 失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的网络 ID"})
return
}
// 使用 Service 层获取网络
network, err := h.networkService.GetNetwork(id)
if err != nil {
h.logger.Error("获取网络失败", zap.Error(err))
c.JSON(http.StatusNotFound, gin.H{"error": "网络不存在"})
return
}
// 使用 DTO 转换
resp := dto.ToNetworkResponse(network)
c.JSON(http.StatusOK, gin.H{
"data": resp,
})
}
// ListNetworks 获取网络列表
// @Summary 获取所有网络列表
// @Tags networks
// @Accept json
// @Produce json
// @Success 200 {array} model.Network
// @Router /api/v1/networks [get]
// ListNetworks 获取网络列表
func (h *NetworkHandler) ListNetworks(c *gin.Context) {
networks, err := h.networkService.ListNetworks()
if err != nil {
h.logger.Error("查询网络列表失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败"})
return
}
// 使用 DTO 批量转换
respList := dto.ToNetworkResponseList(networks)
c.JSON(http.StatusOK, gin.H{
"data": respList,
})
}
// DeleteNetwork 删除网络
// @Summary 删除指定网络
// @Tags networks
// @Accept json
// @Produce json
// @Param id path string true "网络 ID"
// @Success 200
// @Router /api/v1/networks/:id [delete]
func (h *NetworkHandler) DeleteNetwork(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
h.logger.Error("解析网络 ID 失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的网络 ID"})
return
}
// 获取是否强制级联删除
force := c.Query("force") == "true"
// 使用 Service 层删除网络
err = h.networkService.DeleteNetwork(id, force)
if err != nil {
h.logger.Error("删除网络失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Info("网络已删除", zap.Uint64("id", id))
c.JSON(http.StatusOK, gin.H{
"message": "删除成功",
})
}
// UpdateNetwork 更新网络
// @Summary 更新网络信息
// @Tags networks
// @Accept json
// @Produce json
// @Param id path string true "网络 ID"
// @Param network body model.Network true "网络配置"
// @Success 200 {object} model.Network
// @Router /api/v1/networks/:id [put]
func (h *NetworkHandler) UpdateNetwork(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
h.logger.Error("解析网络 ID 失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的网络 ID"})
return
}
var req model.Network
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
// 构建更新字段
updates := make(map[string]interface{})
if req.Name != "" {
updates["name"] = req.Name
}
if req.SubnetIPv4 != "" {
updates["subnetIPv4"] = req.SubnetIPv4
}
if req.SubnetIPv6 != "" {
updates["subnetIPv6"] = req.SubnetIPv6
}
if req.Mode != "" {
updates["mode"] = req.Mode
}
updates["dhcpEnabled"] = req.DHCPEnabled
// 使用 Service 层更新
network, err := h.networkService.UpdateNetwork(id, updates)
if err != nil {
h.logger.Error("更新网络失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Info("网络更新成功", zap.Uint64("id", id))
c.JSON(http.StatusOK, gin.H{
"message": "更新成功",
"data": network,
})
}
// StartNetwork 启动网络(创建 WG 设备)
// @Summary 启动网络
// @Tags networks
// @Accept json
// @Produce json
// @Param id path string true "网络 ID"
// @Success 200
// @Router /api/v1/networks/:id/start [post]
func (h *NetworkHandler) StartNetwork(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
h.logger.Error("解析网络 ID 失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的网络 ID"})
return
}
// 使用 Service 层启动网络
err = h.networkService.StartNetwork(id)
if err != nil {
h.logger.Error("启动网络失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Info("网络启动成功", zap.Uint64("id", id))
c.JSON(http.StatusOK, gin.H{
"message": "启动成功",
})
}
// StopNetwork 停止网络
// @Summary 停止网络
// @Tags networks
// @Accept json
// @Produce json
// @Param id path string true "网络 ID"
// @Success 200
// @Router /api/v1/networks/:id/stop [post]
func (h *NetworkHandler) StopNetwork(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
h.logger.Error("解析网络 ID 失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的网络 ID"})
return
}
// 使用 Service 层停止网络
err = h.networkService.StopNetwork(id)
if err != nil {
h.logger.Error("停止网络失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Info("网络已停止", zap.Uint64("id", id))
c.JSON(http.StatusOK, gin.H{
"message": "停止成功",
})
}
// SwitchMode 切换组网模式
// @Summary 切换组网模式
// @Tags networks
// @Accept json
// @Produce json
// @Param id path string true "网络 ID"
// @Param mode body object{mesh_mode string} true "组网模式 (native|enhanced)"
// @Success 200
// @Router /api/v1/networks/:id/switch-mode [post]
func (h *NetworkHandler) SwitchMode(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
h.logger.Error("解析网络 ID 失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的网络 ID"})
return
}
var req struct {
MeshMode string `json:"mesh_mode"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
// 使用 Service 层切换模式
err = h.networkService.SwitchMode(id, req.MeshMode)
if err != nil {
h.logger.Error("切换模式失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Info("模式切换成功", zap.Uint64("id", id), zap.String("mesh_mode", req.MeshMode))
c.JSON(http.StatusOK, gin.H{
"message": "切换成功",
})
}
// GenerateMeshSeed 生成 MeshSeed
// @Summary 生成 MeshSeed
// @Tags networks
// @Accept json
// @Produce json
// @Param id path string true "网络 ID"
// @Param params body object true "生成参数"
// @Success 200
// @Router /api/v1/networks/:id/meshseed [post]
func (h *NetworkHandler) GenerateMeshSeed(c *gin.Context) {
idStr := c.Param("id")
networkID, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
h.logger.Error("解析网络 ID 失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的网络 ID"})
return
}
var req struct {
ExpiresInHours int `json:"expires_in_hours"`
MaxUses int `json:"max_uses"`
DDNSEnabled bool `json:"ddns_enabled"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
// 默认值
if req.ExpiresInHours <= 0 {
req.ExpiresInHours = 24 // 默认 24 小时
}
if req.MaxUses <= 0 {
req.MaxUses = 10 // 默认 10 次
}
// 计算过期时间
expiresAt := time.Now().Add(time.Duration(req.ExpiresInHours) * time.Hour)
// 调用 MeshSeedService 生成真实的 MeshSeed
meshSeed, err := h.meshSeedService.GenerateMeshSeed(networkID, req.MaxUses, expiresAt, req.DDNSEnabled)
if err != nil {
h.logger.Error("生成 MeshSeed 失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 返回完整的 MeshSeed URL(包含 JoinToken 和 Signature
seedString := "meshray://" + meshSeed.JoinToken + "." + meshSeed.Signature
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"seed_string": seedString,
"expires_at": meshSeed.ExpiresAt.Format(time.RFC3339),
"max_uses": meshSeed.MaxUses,
"ddns_enabled": meshSeed.DDNSEnabled,
"used_count": meshSeed.UsedCount,
"revoked": meshSeed.Revoked,
"issued_at": time.Now().Format(time.RFC3339),
"remaining_uses": meshSeed.MaxUses - meshSeed.UsedCount,
},
})
}
// PreviewMeshSeed 预览 MeshSeed 信息
// @Summary 预览 MeshSeed
// @Tags networks
// @Accept json
// @Produce json
// @Param req body object{seed string} true "MeshSeed 字符串"
// @Success 200
// @Router /api/v1/networks/preview [post]
func (h *NetworkHandler) PreviewMeshSeed(c *gin.Context) {
var req struct {
Seed string `json:"seed"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
// 解析 seed_string
// 格式: meshray://<joinToken>.<signature>
const prefix = "meshray://"
if len(req.Seed) <= len(prefix) || string(req.Seed[:len(prefix)]) != prefix {
c.JSON(http.StatusBadRequest, gin.H{"error": "MeshSeed 格式不正确"})
return
}
seedContent := req.Seed[len(prefix):]
parts := strings.SplitN(seedContent, ".", 2)
if len(parts) != 2 {
c.JSON(http.StatusBadRequest, gin.H{"error": "MeshSeed 内容不完整"})
return
}
joinToken, signature := parts[0], parts[1]
meshSeed, err := h.meshSeedService.VerifyMeshSeed(joinToken, signature)
if err != nil {
h.logger.Error("MeshSeed 验证失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 查询网络名称和信息
network, err := h.networkService.GetNetworkByID(meshSeed.NetworkID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取网络信息失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"name": network.Name,
"subnet_ipv4": network.SubnetIPv4,
"mesh_mode": network.Mode,
// 在该应用逻辑中,如果是基于 MeshSeed 加入的设备默认进入待审核状态
"require_approval": true,
},
})
}
// JoinNetwork 加入网络
// @Summary 加入网络
// @Tags networks
// @Accept json
// @Produce json
// @Param req body object{seed string, device_name string, message string} true "请求参数"
// @Success 200
// @Router /api/v1/networks/join [post]
func (h *NetworkHandler) JoinNetwork(c *gin.Context) {
var req struct {
Seed string `json:"seed"`
DeviceName string `json:"device_name"`
Message string `json:"message"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
const prefix = "meshray://"
if len(req.Seed) <= len(prefix) || string(req.Seed[:len(prefix)]) != prefix {
c.JSON(http.StatusBadRequest, gin.H{"error": "MeshSeed 格式不正确"})
return
}
seedContent := req.Seed[len(prefix):]
parts := strings.SplitN(seedContent, ".", 2)
if len(parts) != 2 {
c.JSON(http.StatusBadRequest, gin.H{"error": "MeshSeed 内容不完整"})
return
}
joinToken, signature := parts[0], parts[1]
meshSeed, err := h.meshSeedService.VerifyMeshSeed(joinToken, signature)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 记录一次使用次数
if err := h.meshSeedService.IncrementUseCount(meshSeed.SeedID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "处理失败"})
return
}
// 在目前的 handler 这里,直接通过 store 存储记录
pendingJoin := &model.PendingJoin{
SeedID: meshSeed.SeedID,
DeviceName: req.DeviceName,
RequestIP: c.ClientIP(),
Status: "pending",
ExpireAt: time.Now().Add(72 * time.Hour), // 72小时过期
}
// 存入数据库
db := h.store.DB()
if db != nil {
db.Create(pendingJoin)
}
c.JSON(http.StatusOK, gin.H{
"message": "申请已提交,等待管理员审核",
"data": gin.H{
"success": true,
"needApproval": true,
},
})
}
+205
View File
@@ -0,0 +1,205 @@
package handler
import (
"net/http"
"strconv"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/service"
sqlite "git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// PendingJoinHandler 待审核 Handler
type PendingJoinHandler struct {
service *service.PendingJoinService
store *sqlite.Store // ← 新增
logger *zap.Logger
}
// NewPendingJoinHandler 创建待审核 Handler
func NewPendingJoinHandler(service *service.PendingJoinService, store *sqlite.Store, logger *zap.Logger) *PendingJoinHandler {
return &PendingJoinHandler{
service: service,
store: store, // ← 新增
logger: logger,
}
}
// ListPendingJoins 获取待审核列表
func (h *PendingJoinHandler) ListPendingJoins(c *gin.Context) {
// 获取查询参数
networkIDStr := c.DefaultQuery("network_id", "0")
status := c.DefaultQuery("status", "pending")
pageStr := c.DefaultQuery("page", "1")
sizeStr := c.DefaultQuery("size", "20")
// 转换参数
networkID, _ := strconv.ParseUint(networkIDStr, 10, 32)
page, _ := strconv.Atoi(pageStr)
size, _ := strconv.Atoi(sizeStr)
if page < 1 {
page = 1
}
if size < 1 || size > 100 {
size = 20
}
// 调用 Service
list, total, err := h.service.ListPendingJoins(uint(networkID), status, page, size)
if err != nil {
h.logger.Error("获取待审核列表失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取失败"})
return
}
// 转换为响应格式
type ResponseItem struct {
ID uint `json:"id"`
SeedID string `json:"seed_id"`
DeviceName string `json:"device_name"`
RequestIP string `json:"request_ip"`
Status string `json:"status"`
ExpireAt string `json:"expire_at"`
ApprovedAt string `json:"approved_at"`
RejectedAt string `json:"rejected_at"`
Reason string `json:"reason"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
NetworkID uint `json:"network_id"`
NetworkName string `json:"network_name"`
ApplicantIP string `json:"applicant_ip"`
ApplyTime string `json:"apply_time"`
Message string `json:"message"`
}
var responseList []ResponseItem
for _, item := range list {
// 通过 SeedID 关联查询 MeshSeed 获取 NetworkID
var meshSeed model.MeshSeed
networkName := "未知网络"
networkID := uint(0)
if err := h.store.DB().Where("seed_id = ?", item.SeedID).First(&meshSeed).Error; err == nil {
// 查询网络信息
var network model.Network
if err := h.store.DB().Where("id = ?", meshSeed.NetworkID).First(&network).Error; err == nil {
networkName = network.Name
networkID = uint(network.ID)
}
}
responseList = append(responseList, ResponseItem{
ID: item.ID,
SeedID: item.SeedID,
DeviceName: item.DeviceName,
RequestIP: item.RequestIP,
Status: item.Status,
ExpireAt: item.ExpireAt.Format("2006-01-02 15:04:05"),
ApprovedAt: formatTime(item.ApprovedAt),
RejectedAt: formatTime(item.RejectedAt),
Reason: item.Reason,
CreatedAt: item.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: item.UpdatedAt.Format("2006-01-02 15:04:05"),
NetworkID: networkID,
NetworkName: networkName,
ApplicantIP: item.RequestIP,
ApplyTime: item.CreatedAt.Format("2006-01-02 15:04:05"),
Message: "",
})
}
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"list": responseList,
"total": total,
},
})
}
// ApproveJoin 审核通过
func (h *PendingJoinHandler) ApproveJoin(c *gin.Context) {
idStr := c.Param("id")
id, _ := strconv.ParseUint(idStr, 10, 32)
result, err := h.service.ApproveJoin(uint(id))
if err != nil {
h.logger.Error("审核通过失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 返回完整配置给前端
h.logger.Info("审核通过成功",
zap.Uint64("id", uint64(id)),
zap.String("device_name", result.Device.Name),
zap.String("device_ip", result.Device.VirtualIP))
c.JSON(http.StatusOK, gin.H{
"message": "审核通过",
"data": gin.H{
"device": result.Device,
"private_key": result.PrivateKey,
"network": result.Network,
"wireguard_config": result.ConfigText,
},
})
}
// RejectJoin 审核拒绝
func (h *PendingJoinHandler) RejectJoin(c *gin.Context) {
idStr := c.Param("id")
id, _ := strconv.ParseUint(idStr, 10, 32)
var req struct {
Reason string `json:"reason"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
if err := h.service.RejectJoin(uint(id), req.Reason); err != nil {
h.logger.Error("审核拒绝失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "已拒绝"})
}
// CountPending 统计待审核数量
func (h *PendingJoinHandler) CountPending(c *gin.Context) {
count, err := h.service.CountPending()
if err != nil {
h.logger.Error("统计待审核数量失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "统计失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"count": count,
},
})
}
// DeleteExpired 清理过期记录
func (h *PendingJoinHandler) DeleteExpired(c *gin.Context) {
if err := h.service.DeleteExpired(); err != nil {
h.logger.Error("清理过期记录失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "清理失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "清理完成"})
}
func formatTime(t *time.Time) string {
if t == nil {
return ""
}
return t.Format("2006-01-02 15:04:05")
}
+126
View File
@@ -0,0 +1,126 @@
package handler
import (
"net/http"
"strconv"
"git.zkcoi.com/zkcoi/meshray/internal/api/dto"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// PolicyHandler 策略管理 Handler
type PolicyHandler struct {
policyService *service.PolicyService
logger *zap.Logger
}
// NewPolicyHandler 创建 Policy Handler
func NewPolicyHandler(policyService *service.PolicyService, logger *zap.Logger) *PolicyHandler {
return &PolicyHandler{
policyService: policyService,
logger: logger,
}
}
// ListPolicies 获取策略列表
func (h *PolicyHandler) ListPolicies(c *gin.Context) {
policies, err := h.policyService.ListPolicies()
if err != nil {
h.logger.Error("获取策略列表失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取策略列表失败"})
return
}
// 使用 DTO 批量转换
respList := dto.ToPolicyResponseList(policies)
c.JSON(http.StatusOK, gin.H{"data": respList})
}
// GetPolicy 获取策略详情
func (h *PolicyHandler) GetPolicy(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
return
}
policy, err := h.policyService.GetPolicyByID(uint(id))
if err != nil {
h.logger.Error("获取策略失败", zap.Error(err))
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
// 使用 DTO 转换
resp := dto.ToPolicyResponse(policy)
c.JSON(http.StatusOK, gin.H{"data": resp})
}
// CreatePolicy 创建策略
func (h *PolicyHandler) CreatePolicy(c *gin.Context) {
var req model.Policy
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
policy, err := h.policyService.CreatePolicy(&req)
if err != nil {
h.logger.Error("创建策略失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Info("策略创建成功", zap.String("name", policy.Name))
// 使用 DTO 转换
resp := dto.ToPolicyResponse(policy)
c.JSON(http.StatusOK, gin.H{"data": resp})
}
// UpdatePolicy 更新策略
func (h *PolicyHandler) UpdatePolicy(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
return
}
var updates map[string]interface{}
if err := c.ShouldBindJSON(&updates); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
policy, err := h.policyService.UpdatePolicy(uint(id), updates)
if err != nil {
h.logger.Error("更新策略失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Info("策略更新成功", zap.Uint64("id", policy.ID))
// 使用 DTO 转换
resp := dto.ToPolicyResponse(policy)
c.JSON(http.StatusOK, gin.H{"data": resp})
}
// DeletePolicy 删除策略
func (h *PolicyHandler) DeletePolicy(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
return
}
if err := h.policyService.DeletePolicy(uint(id)); err != nil {
h.logger.Error("删除策略失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Info("策略删除成功", zap.Uint64("id", id))
c.JSON(http.StatusOK, gin.H{"message": "策略已删除"})
}
+190
View File
@@ -0,0 +1,190 @@
package handler
import (
"net/http"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// ServiceHandler 服务管理 Handler
type ServiceHandler struct {
serviceService *service.ServiceService
logger *zap.Logger
}
// NewServiceHandler 创建 Service Handler
func NewServiceHandler(serviceService *service.ServiceService, logger *zap.Logger) *ServiceHandler {
return &ServiceHandler{
serviceService: serviceService,
logger: logger,
}
}
// ListServices 获取服务列表
func (h *ServiceHandler) ListServices(c *gin.Context) {
serverType := c.Query("type")
services, err := h.serviceService.ListServices(serverType)
if err != nil {
h.logger.Error("获取服务列表失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取服务列表失败"})
return
}
c.JSON(http.StatusOK, gin.H{"data": services})
}
// GetService 获取服务详情
func (h *ServiceHandler) GetService(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 ID"})
return
}
service, err := h.serviceService.GetServiceByID(id)
if err != nil {
h.logger.Error("获取服务失败", zap.Error(err))
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": service})
}
// CreateService 创建服务
func (h *ServiceHandler) CreateService(c *gin.Context) {
var req model.Service
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
service, err := h.serviceService.CreateService(&req)
if err != nil {
h.logger.Error("创建服务失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Info("服务创建成功", zap.String("name", service.Name))
c.JSON(http.StatusOK, gin.H{"data": service})
}
// UpdateService 更新服务
func (h *ServiceHandler) UpdateService(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 ID"})
return
}
var updates map[string]interface{}
if err := c.ShouldBindJSON(&updates); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
service, err := h.serviceService.UpdateService(id, updates)
if err != nil {
h.logger.Error("更新服务失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Info("服务更新成功", zap.String("id", service.ID))
c.JSON(http.StatusOK, gin.H{"data": service})
}
// DeleteService 删除服务
func (h *ServiceHandler) DeleteService(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 ID"})
return
}
if err := h.serviceService.DeleteService(id); err != nil {
h.logger.Error("删除服务失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Info("服务删除成功", zap.String("id", id))
c.JSON(http.StatusOK, gin.H{"message": "服务已删除"})
}
// TestServiceConnectivity 测试服务连通性
func (h *ServiceHandler) TestServiceConnectivity(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 ID"})
return
}
status, err := h.serviceService.TestServiceConnectivity(id)
if err != nil {
h.logger.Error("测试连通性失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": gin.H{"status": status}})
}
// GetServiceSchema 获取服务 Schema(支持的协议类型)
func (h *ServiceHandler) GetServiceSchema(c *gin.Context) {
schema := gin.H{
"protocols": []gin.H{
{
"type": "http",
"label": "HTTP 服务",
"description": "超文本传输协议,用于 Web 服务",
"defaultPort": 80,
},
{
"type": "https",
"label": "HTTPS 服务",
"description": "安全的 HTTP 协议,用于加密 Web 服务",
"defaultPort": 443,
},
{
"type": "tcp",
"label": "TCP 服务",
"description": "传输控制协议,面向连接的可靠传输",
"defaultPort": 0,
},
{
"type": "udp",
"label": "UDP 服务",
"description": "用户数据报协议,无连接的快速传输",
"defaultPort": 0,
},
{
"type": "ssh",
"label": "SSH 服务",
"description": "安全外壳协议,用于远程登录",
"defaultPort": 22,
},
{
"type": "rdp",
"label": "RDP 服务",
"description": "远程桌面协议,用于 Windows 远程桌面",
"defaultPort": 3389,
},
{
"type": "vnc",
"label": "VNC 服务",
"description": "虚拟网络计算,用于图形化远程桌面",
"defaultPort": 5900,
},
{
"type": "custom",
"label": "自定义服务",
"description": "用户自定义的其他服务类型",
"defaultPort": 0,
},
},
}
c.JSON(http.StatusOK, gin.H{"data": schema})
}
+65
View File
@@ -0,0 +1,65 @@
package handler
import (
"net/http"
"git.zkcoi.com/zkcoi/meshray/internal/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// SettingsHandler 系统设置 Handler
type SettingsHandler struct {
logger *zap.Logger
settingsService *service.SettingsService
}
// NewSettingsHandler 创建系统设置 Handler
func NewSettingsHandler(settingsService *service.SettingsService, logger *zap.Logger) *SettingsHandler {
return &SettingsHandler{
logger: logger,
settingsService: settingsService,
}
}
// GetSettings 获取系统设置
func (h *SettingsHandler) GetSettings(c *gin.Context) {
setting, err := h.settingsService.GetSettings()
if err != nil {
h.logger.Error("获取系统设置失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "获取设置失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": setting,
})
}
// UpdateSettings 更新系统设置
func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
var updates map[string]interface{}
if err := c.ShouldBindJSON(&updates); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "参数错误",
})
return
}
updatedSetting, err := h.settingsService.UpdateSettings(updates)
if err != nil {
h.logger.Error("更新系统设置失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "更新失败",
})
return
}
h.logger.Info("系统设置已更新", zap.Any("updates", updates))
c.JSON(http.StatusOK, gin.H{
"message": "设置已保存",
"data": updatedSetting,
})
}
+177
View File
@@ -0,0 +1,177 @@
package handler
import (
"net/http"
"os"
"os/exec"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// SystemConfigHandler 系统配置处理器
type SystemConfigHandler struct {
configService *service.SystemConfigService
logger *zap.Logger
}
// NewSystemConfigHandler 创建系统配置处理器
func NewSystemConfigHandler(configService *service.SystemConfigService, logger *zap.Logger) *SystemConfigHandler {
return &SystemConfigHandler{
configService: configService,
logger: logger,
}
}
// GetWGMode 获取当前 WG 运行模式
// @Summary 获取当前 WG 运行模式
// @Tags system-config
// @Accept json
// @Produce json
// @Success 200 {object} object{wg_mode=string,wg_mode_display=string,actual_mode=string}
// @Router /api/v1/system-config/wg-mode [get]
func (h *SystemConfigHandler) GetWGMode(c *gin.Context) {
// 获取配置的模式(用户设置)
configMode, err := h.configService.GetWGMode()
if err != nil {
h.logger.Error("获取 WG 配置失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取失败"})
return
}
// ✅ 固定使用用户态模式
actualMode := "userspace"
h.logger.Debug("WG 模式:固定使用 wireguard-go 用户态")
c.JSON(http.StatusOK, gin.H{
"wg_mode": configMode, // 配置的模式(已废弃,保留兼容)
"wg_mode_display": getModeDisplay(actualMode), // 实际运行的模式显示
"actual_mode": actualMode, // 实际运行的模式代码
})
}
// getModeDisplay 获取模式的中文显示
func getModeDisplay(mode string) string {
switch mode {
case "kernel":
return "内核态"
case "userspace":
return "用户态"
default:
return mode
}
}
// SetWGMode 设置 WG 运行模式
// @Summary 设置 WG 运行模式(需要重启 MeshRay 才能生效)
// @Tags system-config
// @Accept json
// @Produce json
// @Param mode body object{mode string} true "WG 模式 (auto|kernel|userspace)"
// @Success 200 {object} object{message=string}
// @Router /api/v1/system-config/wg-mode [put]
func (h *SystemConfigHandler) SetWGMode(c *gin.Context) {
var req struct {
Mode string `json:"mode"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
if err := h.configService.SetWGMode(req.Mode); err != nil {
h.logger.Error("设置 WG 模式失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Info("WG 模式已更新,需要重启 MeshRay 才能生效", zap.String("mode", req.Mode))
c.JSON(http.StatusOK, gin.H{
"message": "设置成功,请重启 MeshRay 使新配置生效",
"wg_mode": req.Mode,
})
}
// ==================== 系统运维功能(P1/P2 ====================
// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
}
// ChangePassword 修改密码(P1
func (h *SystemConfigHandler) ChangePassword(c *gin.Context) {
var req ChangePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
// 验证密码长度
if len(req.NewPassword) < 6 {
c.JSON(http.StatusBadRequest, gin.H{"error": "密码至少 6 个字符"})
return
}
// 从上下文获取用户 ID
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
return
}
// 调用服务层修改密码
err := h.configService.ChangePassword(userID.(uint), req.OldPassword, req.NewPassword)
if err != nil {
h.logger.Error("修改密码失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
h.logger.Info("密码修改成功", zap.Uint("user_id", userID.(uint)))
c.JSON(http.StatusOK, gin.H{"message": "密码修改成功"})
}
// RestartCoreRequest 重启核心服务请求
type RestartCoreRequest struct {
Graceful bool `json:"graceful"` // 是否优雅重启
}
// RestartCore 重启核心服务(P1 - Windows 下通过重启进程实现)
func (h *SystemConfigHandler) RestartCore(c *gin.Context) {
var req RestartCoreRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
h.logger.Info("收到重启核心服务请求", zap.Bool("graceful", req.Graceful))
// 获取当前可执行文件路径
execPath, err := os.Executable()
if err != nil {
h.logger.Error("获取可执行文件路径失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "无法获取程序路径"})
return
}
// 在后台启动新进程
cmd := exec.Command(execPath)
if err := cmd.Start(); err != nil {
h.logger.Error("启动新进程失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "重启失败"})
return
}
// 延迟退出,让新进程有时间启动
go func() {
time.Sleep(2 * time.Second)
os.Exit(0)
}()
h.logger.Info("核心服务将在 2 秒后重启")
c.JSON(http.StatusOK, gin.H{"message": "核心服务正在重启..."})
}
+157
View File
@@ -0,0 +1,157 @@
package handler
import (
"encoding/json"
"net/http"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/ctr"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"go.uber.org/zap"
)
var upgrader = websocket.Upgrader{
// ✅ 修复:限制 WebSocket 跨域,只允许特定来源
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
allowedOrigins := map[string]bool{
"http://localhost:9531": true,
"http://127.0.0.1:9531": true,
}
return allowedOrigins[origin]
},
}
// WSMessage 与前端定义一致的结构
type WSMessage struct {
Channel string `json:"channel"`
Type string `json:"type"`
Payload interface{} `json:"payload"`
}
// WSHandler 处理 WebSocket 连接
type WSHandler struct {
ctrClient *ctr.Ctr
logger *zap.Logger
store *sqlite.Store
}
func NewWSHandler(ctrClient *ctr.Ctr, store *sqlite.Store, logger *zap.Logger) *WSHandler {
return &WSHandler{
ctrClient: ctrClient,
logger: logger,
store: store,
}
}
func (h *WSHandler) ServeWS(c *gin.Context) {
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
h.logger.Error("升级 WebSocket 失败", zap.Error(err))
return
}
defer ws.Close()
h.logger.Info("客户端已连接 WebSocket")
// 这里暂时用简单轮询的方案,每秒向终端推送 core metrics 和 peering 状态
// 在生产级应用中,可以使用 EventBus 订阅发布模式
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
// 启动一个 goroutine 读取客户端发来的控制指令 (ping/subscribe)
go func() {
for {
_, msg, err := ws.ReadMessage()
if err != nil {
h.logger.Debug("WebSocket 客户端断开", zap.Error(err))
break
}
// 简单响应 ping
var req map[string]interface{}
if err := json.Unmarshal(msg, &req); err == nil {
if req["type"] == "ping" {
_ = ws.WriteJSON(map[string]interface{}{"type": "pong", "timestamp": time.Now().Unix()})
}
}
}
}()
for {
select {
case <-ticker.C:
// 1. 获取所有组网
var networks []model.Network
if err := h.store.DB().Find(&networks).Error; err != nil {
continue
}
// 聚合数据
var totalBytesSent, totalBytesRecv uint64
var totalFallbacks int64
var totalActiveConnections int64
// 收集链路分布 (Layer -> Count)
layerDistribution := make(map[string]int)
// wg 状态聚合(设备是否在运行)
wgRunningCount := 0
// 2. 遍历所有组网聚合 Ctr Status
for _, net := range networks {
status, err := h.ctrClient.GetStatus(net.ID)
if err != nil || status == nil {
continue
}
if status.WGStatus != nil && status.WGStatus.Running {
wgRunningCount++
}
if status.CoreStatus != nil {
totalBytesSent += status.CoreStatus.BytesSent
totalBytesRecv += status.CoreStatus.BytesReceived
totalFallbacks += status.CoreStatus.StrategyFallbacks
totalActiveConnections += status.CoreStatus.ActiveConnections
for _, peerStatus := range status.CoreStatus.Peers {
if peerStatus.Connected && peerStatus.Layer != "" {
layerDistribution[peerStatus.Layer]++
} else if peerStatus.Connected {
layerDistribution["Native"]++
}
}
}
}
// 3. 构建 payload 推送
msg := WSMessage{
Channel: "metrics",
Type: "update",
Payload: map[string]interface{}{
"summary": map[string]interface{}{
"networks_count": len(networks),
"wg_running": wgRunningCount,
},
"core_status": map[string]interface{}{
"bytes_sent": totalBytesSent,
"bytes_received": totalBytesRecv,
"strategy_fallbacks": totalFallbacks,
"active_connections": totalActiveConnections,
"layer_distribution": layerDistribution,
},
},
}
if err := ws.WriteJSON(msg); err != nil {
return
}
case <-c.Request.Context().Done():
return
}
}
}
+327
View File
@@ -0,0 +1,327 @@
package middleware
import (
"errors"
"net/http"
"strings"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/service"
"git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"go.uber.org/zap"
)
// Claims JWT 声明
type Claims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
TokenType string `json:"token_type"` // access | refresh
jwt.RegisteredClaims
}
// Response 统一响应格式
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// Success 成功响应
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Response{
Code: 0,
Message: "success",
Data: data,
})
}
// Error 错误响应
func Error(c *gin.Context, code int, message string) {
c.JSON(http.StatusOK, Response{
Code: code,
Message: message,
})
}
// GenerateToken 生成 JWT Token
func GenerateToken(secret string, userID uint, username string, tokenType string, duration time.Duration) (string, error) {
claims := Claims{
UserID: userID,
Username: username,
TokenType: tokenType,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(duration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "meshray",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}
// JWTAuth JWT 鉴权中间件
func JWTAuth(secret string) gin.HandlerFunc {
return func(c *gin.Context) {
var tokenString string
// 1. 优先从 Authorization Header 获取
authHeader := c.GetHeader("Authorization")
if authHeader != "" {
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) == 2 && parts[0] == "Bearer" {
tokenString = parts[1]
}
}
// 2. 如果 Header 中没有,尝试从 query 参数获取(WebSocket 场景)
if tokenString == "" {
tokenString = c.Query("token")
}
// 3. 都没有则拒绝
if tokenString == "" {
Error(c, 401, "未提供认证令牌")
c.Abort()
return
}
// 解析和验证 Token
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil {
Error(c, 401, "无效的认证令牌")
c.Abort()
return
}
if !token.Valid {
Error(c, 401, "认证令牌已过期或无效")
c.Abort()
return
}
claims, ok := token.Claims.(*Claims)
if !ok {
Error(c, 401, "无法解析认证声明")
c.Abort()
return
}
// 检查 Token 类型
if claims.TokenType != "access" {
Error(c, 403, "Token 类型错误")
c.Abort()
return
}
// 将用户信息存入上下文
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Next()
}
}
// LoginRequest 登录请求
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// LoginResponse 登录响应
type LoginResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
}
// LoginHandler 登录 Handler
func LoginHandler(jwtSecret string, logger *zap.Logger, store *sqlite.Store) gin.HandlerFunc {
return func(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
Error(c, 400, "请求参数错误")
c.Abort()
return
}
logger.Info("Login request received",
zap.String("username", req.Username))
// 创建 UserService 并验证用户
userService := service.NewUserService(store)
user, err := userService.Authenticate(req.Username, req.Password)
if err != nil {
logger.Warn("Authentication failed",
zap.String("username", req.Username),
zap.Error(err))
Error(c, 401, err.Error())
c.Abort()
return
}
// 生成 Access Token
token, err := GenerateToken(
jwtSecret,
user.ID,
user.Username,
"access",
2*time.Hour,
)
if err != nil {
logger.Error("Failed to generate token", zap.Error(err))
Error(c, 500, "无法生成访问令牌")
c.Abort()
return
}
// 生成 Refresh Token
refreshToken, err := GenerateToken(
jwtSecret,
user.ID,
user.Username,
"refresh",
7*24*time.Hour,
)
if err != nil {
logger.Error("Failed to generate refresh token", zap.Error(err))
Error(c, 500, "无法生成刷新令牌")
c.Abort()
return
}
logger.Info("User login successful",
zap.String("username", user.Username),
zap.Uint("user_id", user.ID))
Success(c, LoginResponse{
AccessToken: token,
RefreshToken: refreshToken,
ExpiresIn: 7200,
})
}
}
// RefreshTokenRequest 刷新 Token 请求
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
// RefreshTokenHandler 刷新 Token Handler
func RefreshTokenHandler(jwtSecret string, logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
var req RefreshTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
Error(c, 400, "请求参数错误")
c.Abort()
return
}
logger.Info("Refresh token request received")
// 验证刷新 Token
token, err := jwt.ParseWithClaims(req.RefreshToken, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(jwtSecret), nil
})
if err != nil || !token.Valid {
Error(c, 401, "无效的刷新令牌")
c.Abort()
return
}
claims, ok := token.Claims.(*Claims)
if !ok || claims.TokenType != "refresh" {
Error(c, 403, "Token 类型错误")
c.Abort()
return
}
// 生成新的访问令牌
newAccessToken, err := GenerateToken(
jwtSecret,
claims.UserID,
claims.Username,
"access",
2*time.Hour,
)
if err != nil {
logger.Error("Failed to generate new access token", zap.Error(err))
Error(c, 500, "无法生成访问令牌")
c.Abort()
return
}
Success(c, gin.H{
"access_token": newAccessToken,
"expires_in": 7200,
})
}
}
// RequestLogger 请求日志中间件
func RequestLogger(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
latency := time.Since(start)
statusCode := c.Writer.Status()
logger.Info("HTTP request",
zap.Int("status", statusCode),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", c.ClientIP()),
zap.String("user_agent", c.Request.UserAgent()),
zap.Duration("latency", latency),
)
}
}
// CORS CORS 中间件
func CORS() gin.HandlerFunc {
// 允许的 Origin 白名单
allowedOrigins := map[string]bool{
"http://localhost:9531": true,
"http://127.0.0.1:9531": true,
// 生产环境可以添加域名
// "https://meshray.example.com": true,
}
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
// ✅ 修复:检查 Origin 是否在白名单内
if !allowedOrigins[origin] {
// 不在白名单,使用默认值(不设置 Access-Control-Allow-Origin
c.Next()
return
}
// 在白名单内,设置 CORS 头
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
// InternalError 内部错误
var InternalError = errors.New("内部错误")
+532
View File
@@ -0,0 +1,532 @@
package api
import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/api/handler"
"git.zkcoi.com/zkcoi/meshray/internal/api/middleware"
"git.zkcoi.com/zkcoi/meshray/internal/config"
"git.zkcoi.com/zkcoi/meshray/internal/ctr"
corehandler "git.zkcoi.com/zkcoi/meshray/internal/handler"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/service"
"git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
"git.zkcoi.com/zkcoi/meshray/web"
"github.com/gin-gonic/gin"
"github.com/shirou/gopsutil/v4/cpu"
"go.uber.org/zap"
"gorm.io/gorm"
)
// Server API 服务器
type Server struct {
engine *gin.Engine
config *config.Config
logger *zap.Logger
store *sqlite.Store
ctrClient *ctr.Ctr // 全局 Ctr 实例
}
// NewServer 创建 API 服务器
func NewServer(cfg *config.Config, logger *zap.Logger, dbStore *sqlite.Store) (*Server, error) {
// 设置 Gin 模式
switch cfg.Server.Mode {
case "debug":
gin.SetMode(gin.DebugMode)
case "release":
gin.SetMode(gin.ReleaseMode)
default:
gin.SetMode(gin.TestMode)
}
engine := gin.New()
server := &Server{
engine: engine,
config: cfg,
logger: logger,
store: dbStore,
}
// 注册中间件
server.registerMiddleware()
// 注册路由
if err := server.registerRoutes(); err != nil {
return nil, err
}
return server, nil
}
// registerMiddleware 注册全局中间件
func (s *Server) registerMiddleware() {
// 恢复中间件
s.engine.Use(gin.Recovery())
// 请求日志中间件
s.engine.Use(middleware.RequestLogger(s.logger))
// CORS 中间件(开发环境)
if s.config.Server.Mode != "release" {
s.engine.Use(middleware.CORS())
}
}
// registerRoutes 注册所有路由
func (s *Server) registerRoutes() error {
// 健康检查
s.engine.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// 静态文件服务(前端)
// 使用 embed 的静态文件
var staticFS fs.FS
var useEmbed bool
// 直接使用 web 包中的 WebAssets,剥离外层的 static 目录
if embedFS, err := fs.Sub(web.WebAssets, "static"); err == nil {
// 检查是否有 index.html
if _, statErr := fs.Stat(embedFS, "index.html"); statErr == nil {
staticFS = embedFS
useEmbed = true
s.logger.Info("使用内嵌的静态文件")
} else {
s.logger.Warn("embed 中找不到 index.html", zap.Error(statErr))
}
} else {
s.logger.Warn("embed 文件系统创建失败", zap.Error(err))
}
// 如果 embed 没有内容,使用外部目录
if !useEmbed && s.config.Server.StaticPath != "" {
staticFS = os.DirFS(s.config.Server.StaticPath)
s.logger.Info("使用外部静态文件目录", zap.String("path", s.config.Server.StaticPath))
}
if staticFS != nil {
// ✅ 注册根路径处理
s.engine.GET("/", func(c *gin.Context) {
file, err := staticFS.Open("index.html")
if err == nil {
defer file.Close()
content, _ := io.ReadAll(file)
c.Data(200, "text/html; charset=utf-8", content)
return
}
c.String(500, "Failed to load index.html")
})
// ✅ 简化:只注册 NoRoute 处理 SPA 路由
s.engine.NoRoute(func(c *gin.Context) {
path := c.Request.URL.Path
// 如果是 API 请求,返回 404
if strings.HasPrefix(path, "/api/") {
c.JSON(404, gin.H{"error": "API not found"})
return
}
// 尝试直接提供文件
filePath := strings.TrimPrefix(path, "/")
if filePath == "" {
filePath = "index.html"
}
// 读取并返回文件内容
file, err := staticFS.Open(filePath)
if err == nil {
defer file.Close()
content, _ := io.ReadAll(file)
c.Data(200, getContentType(filePath), content)
return
}
// 其他请求都返回 index.htmlSPA 路由支持)
file, _ = staticFS.Open("index.html")
if file != nil {
defer file.Close()
content, _ := io.ReadAll(file)
c.Data(200, "text/html; charset=utf-8", content)
}
})
} else {
s.logger.Warn("未配置静态文件路径,前端将不可用")
}
// API v1 路由组
v1 := s.engine.Group("/api/v1")
{
// 公开接口(无需鉴权)
public := v1.Group("")
{
public.POST("/auth/login", middleware.LoginHandler(s.config.JWT.Secret, s.logger, s.store))
public.POST("/auth/refresh", middleware.RefreshTokenHandler(s.config.JWT.Secret, s.logger))
}
// ✅ 提前初始化 ctrClient(在路由注册前)
var err error
s.ctrClient, err = ctr.NewCtr("default", 1, &ctr.CtrConfig{}, s.logger)
if err != nil {
s.logger.Error("初始化 meshray-ctr 失败", zap.Error(err))
return fmt.Errorf("初始化 ctr 失败:%w", err)
}
// 需要鉴权的接口
protected := v1.Group("")
protected.Use(middleware.JWTAuth(s.config.JWT.Secret))
{
// 初始化 Service 层(注入 ctr 客户端)
networkService := service.NewNetworkService(s.store, s.ctrClient, s.logger)
deviceService := service.NewDeviceService(s.store, s.ctrClient)
userService := service.NewUserService(s.store)
policyService := service.NewPolicyService(s.store)
settingsService := service.NewSettingsService(s.store, s.logger)
pendingJoinService := service.NewPendingJoinService(s.store)
// 初始化 MeshSeedService(需要 Ed25519 签名密钥)
signingKey, err := s.loadSigningKey()
if err != nil {
s.logger.Error("加载签名密钥失败", zap.Error(err))
return fmt.Errorf("加载签名密钥失败:%w", err)
}
meshSeedService := service.NewMeshSeedService(s.store, s.logger, signingKey, "node-1")
// 初始化 SystemConfigService(系统配置服务)
systemConfigService := service.NewSystemConfigService(s.store, s.logger)
// 初始化 ExternalService 相关 Handler
serviceService := service.NewServiceService(s.store)
serviceHandler := handler.NewServiceHandler(serviceService, s.logger)
// 初始化 SystemConfigHandler(系统配置处理器)
systemConfigHandler := handler.NewSystemConfigHandler(systemConfigService, s.logger)
// 初始化 PendingJoinHandler(待审核处理器)
pendingJoinHandler := handler.NewPendingJoinHandler(pendingJoinService, s.store, s.logger)
// 初始化 handlers(使用 Service 层)
networkHandler := handler.NewNetworkHandler(networkService, meshSeedService, s.store, s.logger)
deviceHandler := handler.NewDeviceHandler(deviceService, s.logger)
adminHandler := handler.NewAdminHandler(userService, s.logger)
dashboardHandler := handler.NewDashboardHandler(s.store, s.logger)
policyHandler := handler.NewPolicyHandler(policyService, s.logger)
settingsHandler := handler.NewSettingsHandler(settingsService, s.logger)
wsHandler := handler.NewWSHandler(s.ctrClient, s.store, s.logger)
ddnsService, err := service.NewDDNSService(s.store.DB())
if err != nil {
s.logger.Error("初始化 DDNSService 失败", zap.Error(err))
return fmt.Errorf("初始化 DDNSService 失败:%w", err)
}
// 启动后台自动同步协程
go ddnsService.StartAutoSync(context.Background())
ddnsHandler := handler.NewDDNSHandler(ddnsService)
// ✅ 新增:DDNS Usage Handler
ddnsUsageHandler := handler.NewDDNSUsageHandler(s.store.DB(), s.logger)
// ✅ 新增:IP 检测 API
ddnsDetectHandler := handler.NewIPDetectHandler()
// 管理员管理
protected.GET("/admin/profile", adminHandler.GetProfile)
protected.PUT("/admin/profile", adminHandler.UpdateProfile)
protected.POST("/admin/change-password", adminHandler.ChangePassword)
// ✅ 系统备份恢复
backupHandler := corehandler.NewBackupHandler(s.store.DB(), s.logger)
protected.POST("/system/backup", backupHandler.CreateBackup)
protected.GET("/system/backups", backupHandler.ListBackups)
protected.POST("/system/restore", backupHandler.RestoreBackup)
protected.DELETE("/system/backup", backupHandler.DeleteBackup)
protected.GET("/system/backup/download", backupHandler.DownloadBackup)
// ✅ 系统更新检查
updateHandler := corehandler.NewUpdateHandler(s.config.App.Version)
protected.GET("/system/update/check", func(c *gin.Context) {
resp, err := updateHandler.CheckUpdate()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "检查更新失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": resp,
})
})
// ✅ WebSocket 实时通知推送
notificationSvc := service.NewNotificationService(s.store.DB(), s.logger)
notificationHandler := corehandler.NewNotificationHandler(notificationSvc, s.logger, s.store.DB())
protected.GET("/notifications", notificationHandler.GetNotifications)
protected.GET("/notifications/unread-count", notificationHandler.GetUnreadCount)
protected.POST("/notifications/:id/read", notificationHandler.MarkAsRead)
protected.POST("/notifications/read-all", notificationHandler.MarkAllAsRead)
protected.DELETE("/notifications/:id", notificationHandler.DeleteNotification)
protected.POST("/notifications/test", notificationHandler.TestSendNotification)
// DDNS 配置
protected.GET("/ddns/config", ddnsHandler.GetDDNSConfig)
protected.PUT("/ddns/config", ddnsHandler.UpdateDDNSConfig)
protected.POST("/ddns/test", ddnsHandler.TestDDNSConnectivity)
protected.POST("/ddns/sync", ddnsHandler.SyncDDNS)
// ✅ IP 检测 API(用于前端自动填充)
protected.GET("/ddns/detect-ip", ddnsDetectHandler.DetectIP)
// ✅ DDNS 统计 API(用于 Dashboard 监控)
ddnsStatsHandler := handler.NewDDNSStatsHandler(s.store.DB())
protected.GET("/ddns/stats", ddnsStatsHandler.GetDDNSStats)
// ✅ DDNS Usage 管理
protected.POST("/ddns/usages", ddnsUsageHandler.CreateUsage)
protected.GET("/ddns/usages/available", ddnsUsageHandler.GetAvailableUsages)
protected.GET("/ddns/check-prefix", ddnsUsageHandler.CheckPrefixOccupied)
// Dashboard
protected.GET("/dashboard/stats", dashboardHandler.GetStats)
protected.GET("/dashboard/logs", dashboardHandler.GetRecentLogs)
protected.GET("/dashboard/system-info", dashboardHandler.GetSystemInfo)
protected.GET("/dashboard/link-distribution", dashboardHandler.GetLinkDistribution)
protected.POST("/dashboard/logs/clear", dashboardHandler.ClearLogs) // 新增:清理日志
// 系统设置
protected.GET("/settings", settingsHandler.GetSettings)
protected.PUT("/settings", settingsHandler.UpdateSettings)
// 系统配置(WG 模式等)
protected.GET("/system-config/wg-mode", systemConfigHandler.GetWGMode)
protected.PUT("/system-config/wg-mode", systemConfigHandler.SetWGMode)
// 系统运维(P1/P2 功能)- 使用 SystemConfigHandler 统一管理
protected.POST("/system/change-password", systemConfigHandler.ChangePassword) // P1: 修改密码
protected.POST("/system/restart-core", systemConfigHandler.RestartCore) // P1: 重启核心
// 组网管理
protected.GET("/networks", networkHandler.ListNetworks)
protected.POST("/networks", networkHandler.CreateNetwork)
protected.POST("/networks/preview", networkHandler.PreviewMeshSeed)
protected.POST("/networks/join", networkHandler.JoinNetwork)
protected.GET("/networks/:id", networkHandler.GetNetwork)
protected.PUT("/networks/:id", networkHandler.UpdateNetwork)
protected.DELETE("/networks/:id", networkHandler.DeleteNetwork)
protected.POST("/networks/:id/start", networkHandler.StartNetwork)
protected.POST("/networks/:id/stop", networkHandler.StopNetwork)
protected.POST("/networks/:id/switch-mode", networkHandler.SwitchMode)
protected.POST("/networks/:id/meshseed", networkHandler.GenerateMeshSeed)
// 设备管理(使用独立前缀避免路由冲突)
protected.GET("/devices", deviceHandler.ListDevices)
protected.POST("/devices", deviceHandler.CreateDevice)
protected.GET("/devices/:id", deviceHandler.GetDevice)
protected.PUT("/devices/:id", deviceHandler.UpdateDevice)
protected.DELETE("/devices/:id", deviceHandler.DeleteDevice)
protected.GET("/devices/:id/config", deviceHandler.GenerateDeviceConfig)
// 策略管理
protected.GET("/policies", policyHandler.ListPolicies)
protected.POST("/policies", policyHandler.CreatePolicy)
protected.GET("/policies/:id", policyHandler.GetPolicy)
protected.PUT("/policies/:id", policyHandler.UpdatePolicy)
protected.DELETE("/policies/:id", policyHandler.DeletePolicy)
// ExternalService 管理(用户视角)
protected.GET("/services", serviceHandler.ListServices)
protected.POST("/services", serviceHandler.CreateService)
protected.GET("/services/:id", serviceHandler.GetService)
protected.PUT("/services/:id", serviceHandler.UpdateService)
protected.DELETE("/services/:id", serviceHandler.DeleteService)
protected.POST("/services/:id/test", serviceHandler.TestServiceConnectivity)
protected.GET("/services/schema", serviceHandler.GetServiceSchema)
protected.GET("/monitor/metrics", s.handleMetrics)
// 注册 WebSocket 通道
protected.GET("/ws", wsHandler.ServeWS)
// 待审核管理
protected.GET("/pending-joins", pendingJoinHandler.ListPendingJoins)
protected.POST("/pending-joins/:id/approve", pendingJoinHandler.ApproveJoin)
protected.POST("/pending-joins/:id/reject", pendingJoinHandler.RejectJoin)
protected.GET("/pending-joins/count", pendingJoinHandler.CountPending)
protected.POST("/pending-joins/cleanup", pendingJoinHandler.DeleteExpired)
}
}
return nil
}
// handleMetrics 监控指标 API
func (s *Server) handleMetrics(c *gin.Context) {
// 获取系统信息
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
// 获取 CPU 使用率(简化版本)
cpuPercent := 0.0
if cpus, err := cpu.Percent(time.Second, false); err == nil && len(cpus) > 0 {
cpuPercent = cpus[0]
}
// 网络统计(从数据库)
var deviceCount, networkCount, onlineCount int64
s.store.DB().Model(&model.Device{}).Count(&deviceCount)
s.store.DB().Model(&model.Network{}).Count(&networkCount)
s.store.DB().Model(&model.Device{}).Where("status = ?", "online").Count(&onlineCount)
// 返回 Prometheus 格式或 JSON 格式
accept := c.GetHeader("Accept")
if strings.Contains(accept, "text/plain") {
// Prometheus 格式
metrics := fmt.Sprintf(`# HELP meshray_memory_alloc_bytes 当前内存使用量
# TYPE meshray_memory_alloc_bytes gauge
meshray_memory_alloc_bytes %d
# HELP meshray_cpu_usage_percent CPU 使用率
# TYPE meshray_cpu_usage_percent gauge
meshray_cpu_usage_percent %.2f
# HELP meshray_device_total 设备总数
# TYPE meshray_device_total gauge
meshray_device_total %d
# HELP meshray_device_online 在线设备数
# TYPE meshray_device_online gauge
meshray_device_online %d
# HELP meshray_network_total 网络总数
# TYPE meshray_network_total gauge
meshray_network_total %d
`,
memStats.Alloc,
cpuPercent,
deviceCount,
onlineCount,
networkCount)
c.Header("Content-Type", "text/plain; version=0.0.4")
c.String(200, metrics)
} else {
// JSON 格式(前端使用)
c.JSON(200, gin.H{
"data": gin.H{
"memory": gin.H{
"alloc_bytes": memStats.Alloc,
"alloc_mb": float64(memStats.Alloc) / 1024 / 1024,
"sys_bytes": memStats.Sys,
"num_gc": memStats.NumGC,
},
"cpu": gin.H{
"usage_percent": cpuPercent,
},
"devices": gin.H{
"total": deviceCount,
"online": onlineCount,
"offline": deviceCount - onlineCount,
},
"networks": gin.H{
"total": networkCount,
},
"timestamp": time.Now().Unix(),
},
})
}
}
// getContentType 根据文件扩展名返回 Content-Type
func getContentType(filePath string) string {
ext := strings.ToLower(filepath.Ext(filePath))
switch ext {
case ".html":
return "text/html; charset=utf-8"
case ".css":
return "text/css; charset=utf-8"
case ".js":
return "application/javascript; charset=utf-8"
case ".json":
return "application/json; charset=utf-8"
case ".png":
return "image/png"
case ".jpg", ".jpeg":
return "image/jpeg"
case ".gif":
return "image/gif"
case ".svg":
return "image/svg+xml"
case ".ico":
return "image/x-icon"
default:
return "application/octet-stream"
}
}
// Run 启动 API 服务器
func (s *Server) Run() error {
addr := fmt.Sprintf(":%d", s.config.Server.Port)
s.logger.Info("Starting MeshRay", zap.String("address", addr))
// 打印访问地址
fmt.Println("")
fmt.Printf("🌐 MeshRay 启动成功!\n")
fmt.Printf("📍 访问地址:http://localhost:%d\n", s.config.Server.Port)
fmt.Printf("💡 提示:请在浏览器中打开上述地址访问管理面板\n")
fmt.Println("")
return s.engine.Run(addr)
}
// loadSigningKey 加载或生成 Ed25519 签名密钥
func (s *Server) loadSigningKey() (ed25519.PrivateKey, error) {
var key model.SecurityKey
err := s.store.DB().Where("name = ?", "meshseed_signing").First(&key).Error
if err == nil {
// 从数据库加载已有密钥
keyBytes, decodeErr := base64.StdEncoding.DecodeString(key.Value)
if decodeErr != nil {
return nil, fmt.Errorf("解码密钥失败:%w", decodeErr)
}
return ed25519.PrivateKey(keyBytes), nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("查询密钥失败:%w", err)
}
// 密钥不存在,生成新密钥并保存
_, newKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("生成密钥失败:%w", err)
}
keyBytes := []byte(newKey)
err = s.store.DB().Create(&model.SecurityKey{
Name: "meshseed_signing",
Value: base64.StdEncoding.EncodeToString(keyBytes),
Algorithm: "ed25519",
Purpose: "MeshSeed 数字签名",
}).Error
if err != nil {
return nil, fmt.Errorf("保存密钥失败:%w", err)
}
s.logger.Info("已生成新的签名密钥")
return newKey, nil
}
+172
View File
@@ -0,0 +1,172 @@
package config
import (
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"github.com/google/uuid"
"github.com/spf13/viper"
)
// Config 全局配置结构
type Config struct {
App AppConfig `mapstructure:"app"`
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
JWT JWTConfig `mapstructure:"jwt"`
Log LogConfig `mapstructure:"log"`
Encryption EncryptionConfig `mapstructure:"encryption"`
WireGuard WireGuardConfig `mapstructure:"wireguard"`
STUN STUNConfig `mapstructure:"stun"`
TURN TURNConfig `mapstructure:"turn"`
}
// AppConfig 应用配置
type AppConfig struct {
Version string `mapstructure:"version"` // 版本号
BuildDate string `mapstructure:"build_date"` // 构建日期
}
// ServerConfig 服务器配置
type ServerConfig struct {
Port int `mapstructure:"port"`
Mode string `mapstructure:"mode"` // debug, release, test
StaticPath string `mapstructure:"static_path"` // 前端静态文件目录
}
// DatabaseConfig 数据库配置
type DatabaseConfig struct {
Type string `mapstructure:"type"`
Path string `mapstructure:"path"`
}
// JWTConfig JWT 配置
type JWTConfig struct {
Secret string `mapstructure:"secret"`
AccessTokenDur string `mapstructure:"access_token_duration"`
RefreshTokenDur string `mapstructure:"refresh_token_duration"`
}
// LogConfig 日志配置
type LogConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
Output string `mapstructure:"output"`
MaxSize int `mapstructure:"max_size"`
MaxBackups int `mapstructure:"max_backups"`
MaxAge int `mapstructure:"max_age"`
}
// EncryptionConfig 加密配置
type EncryptionConfig struct {
NetworkSecretKey string `mapstructure:"network_secret_key"`
}
// WireGuardConfig WireGuard 配置(固定使用用户态)
type WireGuardConfig struct {
// 已删除 PreferredMode 字段 - 统一使用 wireguard-go 用户态
}
// STUNConfig STUN 配置
type STUNConfig struct {
DefaultServers []string `mapstructure:"default_servers"` // 默认 STUN 服务器列表
SelectionStrategy string `mapstructure:"selection_strategy"` // 选择策略:auto, domestic, international, custom
AutoTest bool `mapstructure:"auto_test"` // 是否启用自动测试
TestInterval int `mapstructure:"test_interval"` // 测试间隔(秒)
Timeout int `mapstructure:"timeout"` // 超时时间(秒)
}
// TURNConfig TURN 配置
type TURNConfig struct {
DefaultServers []TURNServerConfig `mapstructure:"default_servers"` // 默认 TURN 服务器列表
}
// TURNServerConfig TURN 服务器配置
type TURNServerConfig struct {
URL string `mapstructure:"url"` // TURN 服务器 URL
Username string `mapstructure:"username"` // 用户名
Credential string `mapstructure:"credential"` // 凭证
AuthType string `mapstructure:"auth_type"` // 鉴权方式:credential, token, secret
}
// Load 加载配置文件
func Load(configPath string) (*Config, error) {
// 如果未指定配置文件路径,使用默认路径
if configPath == "" {
configPath = "config.yaml"
}
// 检查配置文件是否存在
if _, err := os.Stat(configPath); os.IsNotExist(err) {
// 尝试从示例文件复制
examplePath := "configs/config.example.yaml"
if _, err := os.Stat(examplePath); err == nil {
// 创建目录
dir := filepath.Dir(configPath)
if dir != "." {
os.MkdirAll(dir, 0755)
}
// 复制示例配置
data, _ := os.ReadFile(examplePath)
os.WriteFile(configPath, data, 0644)
}
}
// 读取配置文件
viper.SetConfigFile(configPath)
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("读取配置文件失败:%w", err)
}
var config Config
if err := viper.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("解析配置文件失败:%w", err)
}
// 自动填充默认值
if config.Server.Port == 0 {
config.Server.Port = 9531
}
if config.Server.Mode == "" {
config.Server.Mode = "release"
}
// 自动生成 JWT Secret
if config.JWT.Secret == "" {
config.JWT.Secret = generateJWTSecret()
}
// 自动生成加密密钥
if config.Encryption.NetworkSecretKey == "" {
config.Encryption.NetworkSecretKey = generateEncryptionKey()
}
return &config, nil
}
// generateJWTSecret 使用 crypto/rand 生成随机 JWT Secret
func generateJWTSecret() string {
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
// 极端情况回退(几乎不会发生)
return uuid.New().String()
}
return hex.EncodeToString(b)
}
// generateEncryptionKey 使用 crypto/rand 生成加密密钥
func generateEncryptionKey() string {
key := make([]byte, 32)
_, err := rand.Read(key)
if err != nil {
// 极端情况回退(几乎不会发生)
return uuid.New().String() + uuid.New().String()
}
return hex.EncodeToString(key)
}
+406
View File
@@ -0,0 +1,406 @@
package ctr
import (
"fmt"
"strconv"
"sync"
"git.zkcoi.com/zkcoi/meshray/core"
"git.zkcoi.com/zkcoi/meshray/core/connect"
"go.uber.org/zap"
)
// Ctr meshray-ctr 调度中心
type Ctr struct {
name string // 组网名称(用于日志)
networkID uint64 // 组网 ID(雪花算法 ID
config *CtrConfig
logger *zap.Logger
// WireGuard 管理器
wgManager *WGManager
// Core 实例(直接集成,无需 gRPC)
coreInst *core.Core
mu sync.RWMutex
}
// CtrConfig 配置
type CtrConfig struct {
// 空配置,保留结构体以备未来扩展
}
// NewCtr 创建调度中心
func NewCtr(name string, networkID uint64, config *CtrConfig, logger *zap.Logger) (*Ctr, error) {
ctr := &Ctr{
name: name,
networkID: networkID,
config: config,
logger: logger,
}
// 初始化 WireGuard 管理器
ctr.wgManager = NewWGManager(logger)
// 初始化 Core 实例(直接集成,无需 gRPC)
ctr.coreInst = core.NewCore(logger)
return ctr, nil
}
// Start 启动调度中心
func (c *Ctr) Start() error {
c.mu.Lock()
defer c.mu.Unlock()
c.logger.Info("启动 meshray-ctr",
zap.String("name", c.name),
zap.Uint64("network_id", c.networkID))
// ✅ Watchdog 监控暂不实现(P3 阶段)
// go c.watchdog.Start()
return nil
}
// Stop 停止调度中心
func (c *Ctr) Stop() error {
c.mu.Lock()
defer c.mu.Unlock()
c.logger.Info("停止 meshray-ctr",
zap.Uint64("network_id", c.networkID))
// 停止 Core 实例(直接调用方法)
if c.coreInst != nil {
// ✅ Close 方法已实现(会停止所有 Engine)
c.coreInst.Close()
}
// 停止 WireGuard 管理
if err := c.wgManager.Stop(); err != nil {
c.logger.Error("停止 WireGuard 管理失败", zap.Error(err))
}
return nil
}
// CreateNetwork 创建网络(根据组网模式决定行为)
// - 原生模式:仅创建 WG 设备
// - 增强模式:创建 WG 设备 + 启动 Core 实例
func (c *Ctr) CreateNetwork(networkID uint64, subnet string, listenPort int, meshMode string) error {
c.mu.Lock()
defer c.mu.Unlock()
c.logger.Info("开始创建网络",
zap.Uint64("network_id", networkID),
zap.String("subnet", subnet),
zap.Int("listen_port", listenPort),
zap.String("mesh_mode", meshMode))
// 1. 创建 WireGuard 设备(两种模式都需要)
networkIDStr := strconv.FormatUint(networkID, 10)
if err := c.wgManager.CreateDevice(networkIDStr, subnet, listenPort); err != nil {
return fmt.Errorf("创建 WireGuard 设备失败:%w", err)
}
c.logger.Debug("WireGuard 设备创建成功",
zap.Uint64("network_id", networkID))
// 2. 仅增强模式需要创建并启动 Core 实例
if meshMode == "enhanced" {
// 直接调用 Core 实例的方法(无需 gRPC)
metrics := core.NewMetrics()
engine, err := c.coreInst.CreateEngine(networkIDStr, metrics)
if err != nil {
c.wgManager.DeleteDevice(networkIDStr) // 回滚 WG设备
c.logger.Error("创建 Engine 失败",
zap.Uint64("network_id", networkID),
zap.Error(err))
return fmt.Errorf("创建 Engine 失败:%w", err)
}
if err := engine.Start(); err != nil {
// ✅ 调用 Stop 方法停止(会清理所有资源)
engine.Stop()
c.wgManager.DeleteDevice(networkIDStr) // 回滚 WG 设备
c.logger.Error("启动 Engine 失败",
zap.Uint64("network_id", networkID),
zap.Error(err))
return fmt.Errorf("启动 Engine 失败:%w", err)
}
c.logger.Info("增强模式:Engine 实例已启动",
zap.Uint64("network_id", networkID))
} else {
c.logger.Info("原生模式:仅创建 WG 设备,不启动 Core",
zap.Uint64("network_id", networkID))
}
c.logger.Info("网络创建成功",
zap.Uint64("network_id", networkID),
zap.String("mesh_mode", meshMode))
return nil
}
// DeleteNetwork 删除网络
func (c *Ctr) DeleteNetwork(networkID uint64) error {
c.mu.Lock()
defer c.mu.Unlock()
c.logger.Info("开始删除网络",
zap.Uint64("network_id", networkID))
// 1. Core 实例不需要显式删除(它是内存中的对象)
// 如果需要清理,可以在未来实现 Engine.Stop() 方法
networkIDStr := strconv.FormatUint(networkID, 10)
// 2. 删除 WireGuard 设备
if err := c.wgManager.DeleteDevice(networkIDStr); err != nil {
return fmt.Errorf("删除 WireGuard 设备失败:%w", err)
}
c.logger.Info("网络删除成功",
zap.Uint64("network_id", networkID))
return nil
}
// AddPeer 添加 Peer
func (c *Ctr) AddPeer(networkID uint64, publicKey, allowedIP string) error {
c.mu.RLock()
defer c.mu.RUnlock()
c.logger.Info("添加 Peer",
zap.Uint64("network_id", networkID),
zap.String("public_key", publicKey[:8]+"..."))
// 1. 添加到 WireGuard
networkIDStr := strconv.FormatUint(networkID, 10)
if err := c.wgManager.AddPeer(networkIDStr, publicKey, allowedIP); err != nil {
return fmt.Errorf("添加 Peer 失败:%w", err)
}
// 2. 通知 Core
// 如果已经在 Enhanced 模式,需要接管刚刚加入的这个 Peer
engine, err := c.coreInst.GetEngine(networkIDStr)
if err == nil && engine != nil {
// 引擎存在,说明是 enhanced 模式
// 由 Core 分配一个本地端口
localPort, err := engine.Bind(publicKey, 0)
if err != nil {
c.logger.Error("增强模式动态 AddPeer Bind 失败", zap.Error(err))
} else {
// 通知 WG 更新为本地代理 Endpoint
newEndpoint := fmt.Sprintf("127.0.0.1:%d", localPort)
if err := c.wgManager.UpdatePeerEndpoint(networkIDStr, publicKey, newEndpoint); err != nil {
c.logger.Warn("增强模式自动重写 Endpoint 失败", zap.Error(err))
engine.Unbind(publicKey)
} else {
c.logger.Info("增强模式:动态 Peer 添加并成功代理给 Core",
zap.String("public_key", publicKey[:8]+"..."),
zap.String("new_endpoint", newEndpoint))
}
}
} else {
c.logger.Debug("Core 未启动,普通模式无需代理 Peer", zap.Uint64("network_id", networkID))
}
return nil
}
// RemovePeer 移除 Peer
func (c *Ctr) RemovePeer(networkID uint64, publicKey string) error {
c.mu.RLock()
defer c.mu.RUnlock()
c.logger.Info("移除 Peer",
zap.Uint64("network_id", networkID),
zap.String("public_key", publicKey[:8]+"..."))
// 从 WireGuard 移除
networkIDStr := strconv.FormatUint(networkID, 10)
if err := c.wgManager.RemovePeer(networkIDStr, publicKey); err != nil {
return fmt.Errorf("移除 Peer 失败:%w", err)
}
// 通知 Core 卸载这台 Peer 的监听
engine, err := c.coreInst.GetEngine(networkIDStr)
if err == nil && engine != nil {
engine.Unbind(publicKey)
c.logger.Info("增强模式:Core 已解除对该 Peer 的挂载", zap.String("public_key", publicKey[:8]+"..."))
}
return nil
}
// SetSTUNTURNConfig 为指定网络设置 STUN/TURN 配置
func (c *Ctr) SetSTUNTURNConfig(networkID uint64, stunServers []string, turnServers []connect.TURNServerConfig) error {
c.mu.RLock()
defer c.mu.RUnlock()
networkIDStr := strconv.FormatUint(networkID, 10)
// 获取 Engine 实例
engine, err := c.coreInst.GetEngine(networkIDStr)
if err != nil {
c.logger.Debug("网络未启动增强模式,跳过 STUN/TURN 配置",
zap.Uint64("network_id", networkID))
return nil // 无需错误,因为原生模式不需要
}
// 更新 WebRTC 工厂的 ICE 配置
engine.SetICEConfig(connect.ICEConfig{
STUNServers: stunServers,
TURNServers: turnServers,
})
c.logger.Info("STUN/TURN 配置已设置",
zap.Uint64("network_id", networkID),
zap.Int("stun_count", len(stunServers)),
zap.Int("turn_count", len(turnServers)))
return nil
}
func (c *Ctr) GetStatus(networkID uint64) (*NetworkStatus, error) {
c.mu.RLock()
defer c.mu.RUnlock()
networkIDStr := strconv.FormatUint(networkID, 10)
status := &NetworkStatus{
NetworkID: networkIDStr,
}
// 获取 WireGuard 状态
wgStatus, err := c.wgManager.GetStatus(networkIDStr)
if err != nil {
return nil, err
}
status.WGStatus = wgStatus
// 获取 Core 状态(直接调用 Engine 的方法)
engine, err := c.coreInst.GetEngine(networkIDStr)
if err == nil && engine != nil {
coreStatus, err := engine.GetStatus()
if err == nil {
status.CoreStatus = coreStatus
}
}
return status, nil
}
// SwitchMode 切换传输模式(原生→增强)
// 仅支持 "native" → "enhanced" 单向切换
func (c *Ctr) SwitchMode(networkID uint64, mode string) error {
c.mu.Lock()
defer c.mu.Unlock()
c.logger.Info("开始切换传输模式",
zap.Uint64("network_id", networkID),
zap.String("target_mode", mode))
// 1. 验证目标模式
if mode != "enhanced" {
return fmt.Errorf("仅支持切换到 enhanced 模式,当前请求:%s", mode)
}
networkIDStr := strconv.FormatUint(networkID, 10)
// 2. 获取当前 WG 状态(获取 Peer 数量)
wgStatus, err := c.wgManager.GetStatus(networkIDStr)
if err != nil {
return fmt.Errorf("获取 WG 状态失败:%w", err)
}
c.logger.Info("获取到 WG 状态",
zap.Int("peer_count", wgStatus.PeerCount))
// 3. 创建并启动 Core Engine(增强模式核心)
metrics := core.NewMetrics()
engine, err := c.coreInst.CreateEngine(networkIDStr, metrics)
if err != nil {
return fmt.Errorf("创建 Core Engine 失败:%w", err)
}
if err := engine.Start(); err != nil {
return fmt.Errorf("启动 Core Engine 失败:%w", err)
}
c.logger.Info("Core Engine 已启动",
zap.Uint64("network_id", networkID))
// 4. 获取 Peer 列表
peers, err := c.wgManager.ListPeers(networkIDStr)
if err != nil {
return fmt.Errorf("获取 Peer 列表失败:%w", err)
}
// 5. 修改 Peer Endpoint → 127.0.0.1:{port},并且让 Core 绑定
// 增强模式下,所有 Peer 的 Endpoint 都指向本地回环地址
for _, peer := range peers {
// 1. Core 引擎动态接管,传 0 意为让操作系统挑个未占用的随意回环端口
localPort, err := engine.Bind(peer.PublicKey, 0)
if err != nil {
c.logger.Error("Engine Bind 失败",
zap.String("public_key", peer.PublicKey[:8]+"..."),
zap.Error(err))
continue
}
newEndpoint := fmt.Sprintf("127.0.0.1:%d", localPort)
// 2. 将此端口告知 WG 更新 Endpoint
if err := c.wgManager.UpdatePeerEndpoint(networkIDStr, peer.PublicKey, newEndpoint); err != nil {
c.logger.Warn("更新 Peer Endpoint 失败",
zap.String("public_key", peer.PublicKey[:8]+"..."),
zap.Error(err))
// 如果更新失败,解绑 Core
engine.Unbind(peer.PublicKey)
continue
}
c.logger.Info("Peer Endpoint 已成功切换并接入 Core",
zap.String("public_key", peer.PublicKey[:8]+"..."),
zap.String("original_endpoint", peer.Endpoint),
zap.String("new_endpoint", newEndpoint))
}
c.logger.Info("传输模式切换成功(基础功能已实现,P3 阶段待完善)",
zap.Uint64("network_id", networkID),
zap.String("mode", mode))
return nil
}
// UpdateCoreConfig 更新 Core 配置(策略修改后同步)
// ✅ P3-1 阶段实现
func (c *Ctr) UpdateCoreConfig(networkID uint64, config interface{}) error {
c.mu.RLock()
defer c.mu.RUnlock()
c.logger.Info("更新 Core 配置",
zap.Uint64("network_id", networkID))
// ✅ P3-1 阶段 - 暂不实现
// 未来实现:
// 1. 根据策略类型解析配置
// 2. 调用 Core 的 UpdateConfig 方法
// 3. 持久化到数据库
return fmt.Errorf("UpdateCoreConfig 尚未实现(P3-1 阶段)")
}
// GetWGMode 获取系统 WG 模式
func (c *Ctr) GetWGMode() string {
return c.wgManager.GetWGMode()
}
// NetworkStatus 网络状态
type NetworkStatus struct {
NetworkID string `json:"network_id"`
WGStatus *WGStatus `json:"wg_status"`
CoreStatus *core.EngineStatus `json:"core_status,omitempty"`
}
+64
View File
@@ -0,0 +1,64 @@
package ctr
// Client Ctr 客户端接口(Service 层调用)
//
// 职责:
// 1. 定义 Service 层可调用的所有方法
// 2. 解耦 Service 和 Ctr 实现
// 3. 便于单元测试 Mock
//
// 使用示例:
//
// type NetworkService struct {
// ctrClient ctr.Client
// }
//
// func (s *NetworkService) CreateNetwork(req *model.Network) error {
// return s.ctrClient.CreateNetwork(req.ID, req.Subnet, listenPort)
// }
type Client interface {
// ========== 网络生命周期管理 ==========
// CreateNetwork 创建网络
// networkID: 组网 ID(雪花算法 uint64
// subnet: 子网段(例如:"10.0.0.0/24"
// listenPort: WireGuard 监听端口(例如:51820 + hash(networkID)
// meshMode: 组网模式("native" | "enhanced"
CreateNetwork(networkID uint64, subnet string, listenPort int, meshMode string) error
// DeleteNetwork 删除网络
DeleteNetwork(networkID uint64) error
// ========== Peer 管理 ==========
// AddPeer 添加 Peer
// publicKey: Peer 的公钥
// allowedIP: Peer 的允许 IP(例如:"10.0.0.2/32"
AddPeer(networkID uint64, publicKey, allowedIP string) error
// RemovePeer 移除 Peer
RemovePeer(networkID uint64, publicKey string) error
// ========== 状态查询 ==========
// GetStatus 获取网络状态
// 返回 WGStatus 和 CoreStatus(如果 Core 存在)
GetStatus(networkID uint64) (*NetworkStatus, error)
// ========== 模式切换(P3 阶段) ==========
// SwitchMode 切换传输模式(原生→增强)
// mode: "native" | "enhanced"
// ✅ P3-1 阶段实现
SwitchMode(networkID uint64, mode string) error
// ========== 配置同步(P3 阶段) ==========
// UpdateCoreConfig 更新 Core 配置(策略修改后同步)
// config: 新的配置对象(根据策略类型决定)
// ✅ P3-1 阶段实现
UpdateCoreConfig(networkID uint64, config interface{}) error
}
// 确保 Ctr 实现了 Client 接口
var _ Client = (*Ctr)(nil)
+581
View File
@@ -0,0 +1,581 @@
package ctr
import (
"fmt"
"net"
"os/exec"
"runtime"
"strings"
"sync"
"go.uber.org/zap"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
// WGManager WireGuard 管理器(P2 阶段骨架)
type WGManager struct {
devices map[string]*WGDevice // network_id -> device
mu sync.RWMutex
logger *zap.Logger
wgMode string // "kernel" | "userspace"(当前运行模式)
}
// WGDevice WireGuard 设备
type WGDevice struct {
NetworkID string
Name string // wg0, wg1, ...
Config *DeviceConfig
Peers []PeerInfo
Running bool
// 用户态模式下使用的资源引用
tunDevice tun.Device // TUN 设备引用(用户态模式)
wgDevice *device.Device // WireGuard 设备引用(用户态模式)
}
// NewWGManager 创建 WireGuard 管理器(固定使用用户态)
func NewWGManager(logger *zap.Logger) *WGManager {
logger.Info("使用 wireguard-go 用户态模式(跨平台统一)")
return &WGManager{
devices: make(map[string]*WGDevice),
logger: logger,
wgMode: "userspace", // 固定为用户态
}
}
// CreateDevice 创建 WireGuard 设备
func (m *WGManager) CreateDevice(networkID string, subnet string, listenPort int) error {
m.mu.Lock()
defer m.mu.Unlock()
// 检查是否已存在
if _, ok := m.devices[networkID]; ok {
return fmt.Errorf("网络 %s 的设备已存在", networkID)
}
deviceName := fmt.Sprintf("wg%s", networkID)
m.logger.Info("开始创建 WireGuard 设备",
zap.String("network_id", networkID),
zap.String("device", deviceName),
zap.String("subnet", subnet),
zap.Int("listen_port", listenPort))
// 生成密钥对
privateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
return fmt.Errorf("生成私钥失败:%w", err)
}
publicKey := privateKey.PublicKey()
m.logger.Debug("生成 WG 密钥对",
zap.String("public_key", publicKey.String()))
// 使用用户态模式启动 wireguard-go 进程,并获取资源引用
tunDev, wgDev, err := m.startUserModeWGProcessWithRefs(deviceName, privateKey, listenPort)
if err != nil {
return err
}
// 4. ✅ Core 模块使用回环地址拦截,无需配置 TUN 设备 IP
// WireGuard 用户态设备会自动管理自己的网络接口
m.logger.Info("WireGuard 用户态设备已创建,使用回环地址拦截模式",
zap.String("device", deviceName))
// 5. 启动设备(带回滚)
if err := m.bringUpDevice(deviceName); err != nil {
m.cleanupDevice(deviceName) // 回滚
return fmt.Errorf("启动设备失败:%w", err)
}
// 7. 记录到内存
device := &WGDevice{
NetworkID: networkID,
Name: deviceName,
Config: &DeviceConfig{
PrivateKey: privateKey.String(),
PublicKey: publicKey.String(),
ListenPort: listenPort,
Subnet: subnet,
},
Running: true,
}
// ⚠️ 关键:保存用户态模式资源引用
device.tunDevice = tunDev
device.wgDevice = wgDev
m.logger.Debug("已保存用户态模式资源引用",
zap.String("device", deviceName))
m.devices[networkID] = device
m.logger.Info("WireGuard 设备创建成功(用户态模式)",
zap.String("network_id", networkID),
zap.String("public_key", publicKey.String()))
return nil
}
// AddPeer 添加 Peer
func (m *WGManager) AddPeer(networkID string, publicKey, allowedIP string) error {
m.mu.Lock()
defer m.mu.Unlock()
device, ok := m.devices[networkID]
if !ok {
return fmt.Errorf("网络 %s 的设备不存在", networkID)
}
m.logger.Info("添加 Peer",
zap.String("network_id", networkID),
zap.String("public_key", truncatePublicKey(publicKey)+"..."),
zap.String("allowed_ip", allowedIP))
// 1. 解析公钥
peerKey, err := wgtypes.ParseKey(publicKey)
if err != nil {
return fmt.Errorf("解析公钥失败:%w", err)
}
// 2. 解析 AllowedIP
_, ipNet, err := net.ParseCIDR(allowedIP)
if err != nil {
return fmt.Errorf("解析 AllowedIP 失败:%w", err)
}
// 3. 连接 wgctrl 并配置
client, err := wgctrl.New()
if err != nil {
return fmt.Errorf("wgctrl 连接失败:%w", err)
}
defer client.Close()
// 4. 配置 Peer
config := wgtypes.Config{
Peers: []wgtypes.PeerConfig{
{
PublicKey: peerKey,
ReplaceAllowedIPs: true,
AllowedIPs: []net.IPNet{*ipNet},
},
},
}
if err := client.ConfigureDevice(device.Name, config); err != nil {
return fmt.Errorf("配置 Peer 失败:%w", err)
}
m.logger.Info("Peer 配置成功",
zap.String("network_id", networkID),
zap.String("public_key", truncatePublicKey(publicKey)+"..."),
zap.String("allowed_ip", allowedIP))
// 5. 同时更新内存(保持向后兼容)
peer := PeerInfo{
PublicKey: publicKey,
AllowedIPs: []string{allowedIP},
}
device.Peers = append(device.Peers, peer)
return nil
}
// RemovePeer 移除 Peer
func (m *WGManager) RemovePeer(networkID string, publicKey string) error {
m.mu.Lock()
defer m.mu.Unlock()
device, ok := m.devices[networkID]
if !ok {
return fmt.Errorf("网络 %s 的设备不存在", networkID)
}
m.logger.Info("移除 Peer",
zap.String("network_id", networkID),
zap.String("public_key", truncatePublicKey(publicKey)+"..."))
// 1. 解析公钥
peerKey, err := wgtypes.ParseKey(publicKey)
if err != nil {
return fmt.Errorf("解析公钥失败:%w", err)
}
// 2. 连接 wgctrl 并移除
client, err := wgctrl.New()
if err != nil {
return fmt.Errorf("wgctrl 连接失败:%w", err)
}
defer client.Close()
// 3. 配置移除 Peer(使用 Remove: true
config := wgtypes.Config{
Peers: []wgtypes.PeerConfig{
{
PublicKey: peerKey,
Remove: true,
},
},
}
if err := client.ConfigureDevice(device.Name, config); err != nil {
return fmt.Errorf("移除 Peer 失败:%w", err)
}
m.logger.Info("Peer 移除成功",
zap.String("network_id", networkID),
zap.String("public_key", truncatePublicKey(publicKey)+"..."))
// 4. 同时从内存删除(保持向后兼容)
newPeers := []PeerInfo{}
for _, p := range device.Peers {
if p.PublicKey != publicKey {
newPeers = append(newPeers, p)
}
}
device.Peers = newPeers
return nil
}
// DeleteDevice 删除 WireGuard 设备
func (m *WGManager) DeleteDevice(networkID string) error {
m.mu.Lock()
defer m.mu.Unlock()
device, ok := m.devices[networkID]
if !ok {
return fmt.Errorf("网络 %s 的设备不存在", networkID)
}
m.logger.Info("删除 WireGuard 设备",
zap.String("network_id", networkID),
zap.String("device", device.Name))
// 1. 清理真实设备(内核态或用户态)
m.cleanupDevice(device.Name)
// 2. 从内存删除
delete(m.devices, networkID)
m.logger.Info("WireGuard 设备已删除",
zap.String("network_id", networkID))
return nil
}
// GetStatus 获取设备状态
func (m *WGManager) GetStatus(networkID string) (*WGStatus, error) {
m.mu.RLock()
defer m.mu.RUnlock()
device, ok := m.devices[networkID]
if !ok {
return nil, fmt.Errorf("网络 %s 的设备不存在", networkID)
}
status := &WGStatus{
DeviceName: device.Name,
Running: device.Running,
PeerCount: len(device.Peers),
Subnet: device.Config.Subnet,
}
return status, nil
}
// GetWGMode 获取当前 WG 模式
func (m *WGManager) GetWGMode() string {
m.mu.RLock()
defer m.mu.RUnlock()
return m.wgMode
}
// Stop 停止管理器并释放所有资源
func (m *WGManager) Stop() error {
m.mu.Lock()
defer m.mu.Unlock()
m.logger.Info("正在停止 WireGuard 管理器...")
// 关闭所有设备
for networkID, device := range m.devices {
m.logger.Info("正在关闭设备",
zap.String("network_id", networkID),
zap.String("device_name", device.Name))
// 如果是用户态模式,关闭相关资源
if device.wgDevice != nil {
m.logger.Debug("关闭用户态 WireGuard 设备",
zap.String("device", device.Name))
device.wgDevice.Close()
}
if device.tunDevice != nil {
m.logger.Debug("关闭 TUN 设备",
zap.String("device", device.Name))
device.tunDevice.Close()
}
// 清理内核态设备(忽略返回值)
m.cleanupDevice(device.Name)
delete(m.devices, networkID)
}
m.logger.Info("WireGuard 管理器已停止")
return nil
}
// WGDevice WireGuard 设备(结构已在上面定义)
// DeviceConfig 设备配置
type DeviceConfig struct {
PrivateKey string
PublicKey string
ListenPort int
Subnet string
}
// PeerInfo Peer 信息
type PeerInfo struct {
PublicKey string
Endpoint string
AllowedIPs []string
}
// WGStatus WireGuard 状态
type WGStatus struct {
DeviceName string `json:"device_name"`
Running bool `json:"running"`
PeerCount int `json:"peer_count"`
Subnet string `json:"subnet"`
}
// ListPeers 获取设备下所有 Peer 信息
func (m *WGManager) ListPeers(networkID string) ([]PeerInfo, error) {
m.mu.RLock()
defer m.mu.RUnlock()
device, ok := m.devices[networkID]
if !ok {
return nil, fmt.Errorf("网络 %s 的设备不存在", networkID)
}
return device.Peers, nil
}
// UpdatePeerEndpoint 更新 Peer 的 Endpoint
func (m *WGManager) UpdatePeerEndpoint(networkID string, publicKey string, newEndpoint string) error {
m.mu.Lock()
defer m.mu.Unlock()
device, ok := m.devices[networkID]
if !ok {
return fmt.Errorf("网络 %s 的设备不存在", networkID)
}
m.logger.Info("更新 Peer Endpoint",
zap.String("network_id", networkID),
zap.String("public_key", truncatePublicKey(publicKey)+"..."),
zap.String("new_endpoint", newEndpoint))
// 解析公钥
peerKey, err := wgtypes.ParseKey(publicKey)
if err != nil {
return fmt.Errorf("解析公钥失败:%w", err)
}
// 解析 Endpoint 地址
var udpAddr *net.UDPAddr
if newEndpoint != "" {
addr, err := net.ResolveUDPAddr("udp", newEndpoint)
if err != nil {
return fmt.Errorf("解析 Endpoint 地址 %s 失败:%w", newEndpoint, err)
}
udpAddr = addr
}
// 连接 wgctrl 并配置
client, err := wgctrl.New()
if err != nil {
return fmt.Errorf("wgctrl 连接失败:%w", err)
}
defer client.Close()
// 配置更新 Peer
config := wgtypes.Config{
Peers: []wgtypes.PeerConfig{
{
PublicKey: peerKey,
UpdateOnly: true,
Endpoint: udpAddr,
},
},
}
if err := client.ConfigureDevice(device.Name, config); err != nil {
return fmt.Errorf("更新 Peer 失败:%w", err)
}
// 同时更新内存
for i, p := range device.Peers {
if p.PublicKey == publicKey {
device.Peers[i].Endpoint = newEndpoint
break
}
}
return nil
}
// startUserModeWGProcess 用户态模式启动 wireguard-go(保存资源引用)
func (m *WGManager) startUserModeWGProcess(deviceName string, privateKey wgtypes.Key, listenPort int) error {
tunDev, wgDev, err := m.startUserModeWGProcessWithRefs(deviceName, privateKey, listenPort)
if err != nil {
return err
}
// 兼容旧接口,忽略返回值
_ = tunDev
_ = wgDev
return nil
}
// startUserModeWGProcessWithRefs 用户态模式启动 wireguard-go,返回资源引用
func (m *WGManager) startUserModeWGProcessWithRefs(deviceName string, privateKey wgtypes.Key, listenPort int) (tun.Device, *device.Device, error) {
m.logger.Info("使用用户态模式启动 WireGuard",
zap.String("device", deviceName),
zap.Int("port", listenPort))
// 1. 创建 TUN 设备并保存引用
tunDevice, err := tun.CreateTUN(deviceName, 1420)
if err != nil {
return nil, nil, fmt.Errorf("创建 TUN 设备失败:%w", err)
}
// 2. 创建 UDP bind
bind := conn.NewDefaultBind()
// 3. 创建 WireGuard device 并保存引用
logger := &device.Logger{
Verbosef: func(format string, args ...interface{}) {
m.logger.Debug(fmt.Sprintf(format, args...))
},
Errorf: func(format string, args ...interface{}) {
m.logger.Error(fmt.Sprintf(format, args...))
},
}
wgDevice := device.NewDevice(tunDevice, bind, logger)
// 4. 配置设备
config := fmt.Sprintf("private_key=%s\nlisten_port=%d\n",
privateKey.String(), listenPort)
if err := wgDevice.IpcSet(config); err != nil {
wgDevice.Close()
return nil, nil, fmt.Errorf("配置 WireGuard 设备失败:%w", err)
}
// 5. 启动设备
if err := wgDevice.Up(); err != nil {
wgDevice.Close()
return nil, nil, fmt.Errorf("启动 WireGuard 设备失败:%w", err)
}
m.logger.Info("用户态 WireGuard 启动成功",
zap.String("device", deviceName),
zap.Int("port", listenPort))
// 6. 返回资源引用
return tunDevice, wgDevice, nil
}
// truncatePublicKey 截断公钥用于日志显示(避免数组越界)
func truncatePublicKey(publicKey string) string {
if len(publicKey) <= 8 {
return publicKey
}
return publicKey[:8]
}
// bringUpDevice 启动设备(跨平台实现)
func (m *WGManager) bringUpDevice(deviceName string) error {
m.logger.Info("启动设备",
zap.String("device", deviceName))
var cmd *exec.Cmd
var output []byte
var err error
switch runtime.GOOS {
case "linux":
// Linux: ip link set up device
cmd = exec.Command("ip", "link", "set", "up", deviceName)
output, err = cmd.CombinedOutput()
case "windows":
// Windows: 用户态模式下 wireguard-go 会自动管理设备状态
// 这里不需要额外操作
m.logger.Debug("Windows 平台用户态模式无需手动启动设备")
return nil
case "darwin":
// macOS: ifconfig device up
cmd = exec.Command("ifconfig", deviceName, "up")
output, err = cmd.CombinedOutput()
default:
return fmt.Errorf("不支持的操作系统:%s", runtime.GOOS)
}
if err != nil {
return fmt.Errorf("启动设备失败 (%s): %w", string(output), err)
}
m.logger.Info("设备已启动",
zap.String("device", deviceName))
return nil
}
// cleanupDevice 清理残留设备(回滚用,跨平台实现)
func (m *WGManager) cleanupDevice(deviceName string) {
m.logger.Warn("清理残留设备",
zap.String("device", deviceName))
var cmd *exec.Cmd
var output []byte
switch runtime.GOOS {
case "linux":
// Linux: ip link delete device
cmd = exec.Command("ip", "link", "delete", deviceName)
output, _ = cmd.CombinedOutput()
case "windows":
// Windows: 无法通过命令行直接删除,需要用户手动操作
m.logger.Warn("Windows 平台需要通过 WireGuard 客户端删除设备",
zap.String("device", deviceName))
return
case "darwin":
// macOS: ifconfig device down delete
cmd = exec.Command("ifconfig", deviceName, "down")
_, _ = cmd.CombinedOutput()
// macOS 可能需要额外步骤,这里简化处理
default:
m.logger.Warn("不支持的操作系统,跳过清理",
zap.String("os", runtime.GOOS))
return
}
// 检查是否是"设备不存在"错误
if strings.Contains(string(output), "does not exist") ||
strings.Contains(string(output), "cannot find device") {
m.logger.Debug("设备不存在,无需清理",
zap.String("device", deviceName))
return
}
m.logger.Debug("设备清理完成",
zap.String("device", deviceName))
}
+52
View File
@@ -0,0 +1,52 @@
package dnsprovider
import (
"context"
"fmt"
"github.com/libdns/libdns"
)
// AliyunProvider 阿里云 DNS 服务商实现
// 注意:由于网络问题,github.com/libdns/aliyun 暂时无法下载
// 此实现为占位代码,待网络恢复后需要安装并适配真实的 libdns/aliyun 库
type AliyunProvider struct {
domain string
}
// NewAliyunProvider 创建阿里云 DNS 服务商实例
func NewAliyunProvider(config ProviderConfig) (*AliyunProvider, error) {
if config.AccessKeyID == "" || config.AccessKeySecret == "" {
return nil, fmt.Errorf("AccessKey ID 和 Secret 不能为空")
}
// TODO: 安装 github.com/libdns/aliyun 后,替换为真实实现
// provider := &aliyun.Provider{
// AccessKeyID: config.AccessKeyID,
// AccessKeySecret: config.AccessKeySecret,
// }
return &AliyunProvider{
domain: config.Domain,
}, fmt.Errorf("阿里云 DNS 服务商暂未支持,请稍后重试")
}
// AppendRecords 添加 DNS 记录
func (p *AliyunProvider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
return nil, fmt.Errorf("未实现")
}
// SetRecords 设置 DNS 记录(会覆盖现有记录)
func (p *AliyunProvider) SetRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
return nil, fmt.Errorf("未实现")
}
// GetRecords 获取 DNS 记录
func (p *AliyunProvider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
return nil, fmt.Errorf("未实现")
}
// DeleteRecords 删除 DNS 记录
func (p *AliyunProvider) DeleteRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
return nil, fmt.Errorf("未实现")
}
+51
View File
@@ -0,0 +1,51 @@
package dnsprovider
import (
"context"
"fmt"
"github.com/libdns/cloudflare"
"github.com/libdns/libdns"
)
// CloudflareProvider Cloudflare DNS 服务商实现
type CloudflareProvider struct {
client *cloudflare.Provider
domain string
}
// NewCloudflareProvider 创建 Cloudflare DNS 服务商实例
func NewCloudflareProvider(config ProviderConfig) (*CloudflareProvider, error) {
if config.APIToken == "" {
return nil, fmt.Errorf("API Token 不能为空")
}
provider := &cloudflare.Provider{
APIToken: config.APIToken,
}
return &CloudflareProvider{
client: provider,
domain: config.Domain,
}, nil
}
// AppendRecords 添加 DNS 记录
func (p *CloudflareProvider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
return p.client.AppendRecords(ctx, zone, recs)
}
// SetRecords 设置 DNS 记录(会覆盖现有记录)
func (p *CloudflareProvider) SetRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
return p.client.SetRecords(ctx, zone, recs)
}
// GetRecords 获取 DNS 记录
func (p *CloudflareProvider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
return p.client.GetRecords(ctx, zone)
}
// DeleteRecords 删除 DNS 记录
func (p *CloudflareProvider) DeleteRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
return p.client.DeleteRecords(ctx, zone, recs)
}
+97
View File
@@ -0,0 +1,97 @@
package dnsprovider
import (
"context"
"fmt"
"time"
"github.com/libdns/libdns"
)
// ProviderType DNS 服务商类型
type ProviderType string
const (
ProviderCloudflare ProviderType = "cloudflare"
ProviderAliyun ProviderType = "aliyun"
ProviderTencentCloud ProviderType = "tencent"
ProviderCustom ProviderType = "custom"
)
// ProviderConfig DNS 服务商配置
type ProviderConfig struct {
Provider ProviderType `json:"provider"` // 服务商类型
Domain string `json:"domain"` // 根域名
APIToken string `json:"api_token"` // API TokenCloudflare
AccessKeyID string `json:"access_key_id"` // AccessKey ID(阿里云)
AccessKeySecret string `json:"access_key_secret"` // AccessKey Secret(阿里云)
SecretId string `json:"secret_id"` // SecretId(腾讯云)
SecretKey string `json:"secret_key"` // SecretKey(腾讯云)
}
// DNSProvider DNS 服务商接口
type DNSProvider interface {
// AppendRecords 添加 DNS 记录
AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error)
// SetRecords 设置 DNS 记录(会覆盖现有记录)
SetRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error)
// GetRecords 获取 DNS 记录
GetRecords(ctx context.Context, zone string) ([]libdns.Record, error)
// DeleteRecords 删除 DNS 记录
DeleteRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error)
}
// NewDNSProvider 创建 DNS 服务商实例
func NewDNSProvider(config ProviderConfig) (DNSProvider, error) {
switch config.Provider {
case ProviderCloudflare:
return NewCloudflareProvider(config)
case ProviderAliyun:
return NewAliyunProvider(config)
case ProviderTencentCloud:
return NewTencentCloudProvider(config)
case ProviderCustom:
return nil, fmt.Errorf("自定义服务商暂未支持")
default:
return nil, fmt.Errorf("不支持的 DNS 服务商:%s", config.Provider)
}
}
// RecordType 记录类型
type RecordType string
const (
RecordTypeA RecordType = "A"
RecordTypeAAAA RecordType = "AAAA"
RecordTypeTXT RecordType = "TXT"
RecordTypeCNAME RecordType = "CNAME"
)
// DNSRecord DNS 记录
type DNSRecord struct {
Type RecordType `json:"type"` // 记录类型
Name string `json:"name"` // 记录名称(子域名)
Value string `json:"value"` // 记录值
TTL int `json:"ttl"` // TTL(秒)
}
// ToLibdnsRecord 转换为 libdns.Record
func (r *DNSRecord) ToLibdnsRecord() libdns.Record {
recordType := string(r.Type)
return libdns.Record{
Type: recordType,
Name: r.Name,
Value: r.Value,
TTL: time.Duration(r.TTL) * time.Second,
}
}
// FromLibdnsRecord 从 libdns.Record 转换
func FromLibdnsRecord(rec libdns.Record) *DNSRecord {
return &DNSRecord{
Type: RecordType(rec.Type),
Name: rec.Name,
Value: rec.Value,
TTL: int(rec.TTL / time.Second),
}
}
+52
View File
@@ -0,0 +1,52 @@
package dnsprovider
import (
"context"
"fmt"
"github.com/libdns/libdns"
tencentcloud "github.com/libdns/tencentcloud"
)
// TencentCloudProvider 腾讯云 DNSPod 服务商实现
type TencentCloudProvider struct {
client *tencentcloud.Provider
domain string
}
// NewTencentCloudProvider 创建腾讯云 DNS 服务商实例
func NewTencentCloudProvider(config ProviderConfig) (*TencentCloudProvider, error) {
if config.SecretId == "" || config.SecretKey == "" {
return nil, fmt.Errorf("SecretId 和 SecretKey 不能为空")
}
provider := &tencentcloud.Provider{
SecretId: config.SecretId,
SecretKey: config.SecretKey,
}
return &TencentCloudProvider{
client: provider,
domain: config.Domain,
}, nil
}
// AppendRecords 添加 DNS 记录
func (p *TencentCloudProvider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
return p.client.AppendRecords(ctx, zone, recs)
}
// SetRecords 设置 DNS 记录(会覆盖现有记录)
func (p *TencentCloudProvider) SetRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
return p.client.SetRecords(ctx, zone, recs)
}
// GetRecords 获取 DNS 记录
func (p *TencentCloudProvider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
return p.client.GetRecords(ctx, zone)
}
// DeleteRecords 删除 DNS 记录
func (p *TencentCloudProvider) DeleteRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
return p.client.DeleteRecords(ctx, zone, recs)
}
+334
View File
@@ -0,0 +1,334 @@
package handler
import (
"fmt"
"net/http"
"os"
"path/filepath"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gorm.io/gorm"
)
// BackupHandler 备份恢复处理器
type BackupHandler struct {
db *gorm.DB
logger *zap.Logger
backupSvc *service.BackupService
}
// NewBackupHandler 创建备份恢复处理器
func NewBackupHandler(db *gorm.DB, logger *zap.Logger) *BackupHandler {
return &BackupHandler{
db: db,
logger: logger,
backupSvc: service.NewBackupService(db),
}
}
// CreateBackup 创建系统备份
// @Summary 创建系统备份
// @Tags System
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/system/backup [post]
func (h *BackupHandler) CreateBackup(c *gin.Context) {
// 验证管理员权限
if !h.isAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
return
}
h.logger.Info("开始创建系统备份...")
// 生成备份文件名
timestamp := time.Now().Format("20060102_150405")
backupDir := filepath.Join("data", "backups")
backupFile := filepath.Join(backupDir, fmt.Sprintf("meshray_backup_%s.zip", timestamp))
// 确保备份目录存在
if err := os.MkdirAll(backupDir, 0755); err != nil {
h.logger.Error("创建备份目录失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "创建备份目录失败",
})
return
}
// 执行真实备份
ctx := c.Request.Context()
if err := h.backupSvc.CreateBackup(ctx, backupFile); err != nil {
h.logger.Error("创建备份失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("创建备份失败:%v", err),
})
return
}
// 计算文件大小
fileInfo, err := os.Stat(backupFile)
var sizeStr string
if err == nil {
sizeBytes := fileInfo.Size()
if sizeBytes < 1024*1024 {
sizeStr = fmt.Sprintf("%.2f KB", float64(sizeBytes)/1024)
} else {
sizeStr = fmt.Sprintf("%.2f MB", float64(sizeBytes)/(1024*1024))
}
} else {
sizeStr = "未知"
}
h.logger.Info("备份创建成功", zap.String("file", backupFile))
c.JSON(http.StatusOK, gin.H{
"message": "备份创建成功",
"data": gin.H{
"filename": filepath.Base(backupFile),
"path": backupFile,
"timestamp": timestamp,
"size": sizeStr,
},
})
}
// ListBackups 列出所有备份
// @Summary 列出所有备份
// @Tags System
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/system/backups [get]
func (h *BackupHandler) ListBackups(c *gin.Context) {
// 验证管理员权限
if !h.isAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
return
}
backupDir := filepath.Join("data", "backups")
// 检查备份目录是否存在
if _, err := os.Stat(backupDir); os.IsNotExist(err) {
c.JSON(http.StatusOK, gin.H{
"data": []interface{}{},
})
return
}
// 读取备份文件列表
files, err := os.ReadDir(backupDir)
if err != nil {
h.logger.Error("读取备份目录失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "读取备份目录失败",
})
return
}
// 构建备份列表
backups := make([]gin.H, 0)
for _, file := range files {
if file.IsDir() || filepath.Ext(file.Name()) != ".zip" {
continue
}
filePath := filepath.Join(backupDir, file.Name())
info, err := file.Info()
if err != nil {
continue
}
backups = append(backups, gin.H{
"filename": file.Name(),
"path": filePath,
"size": info.Size(),
"timestamp": parseTimestampFromFilename(file.Name()),
"created_at": info.ModTime().Format("2006-01-02 15:04:05"),
})
}
c.JSON(http.StatusOK, gin.H{
"data": backups,
})
}
// RestoreBackup 恢复备份
// @Summary 恢复备份
// @Tags System
// @Accept json
// @Produce json
// @Param filename body string true "备份文件名"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/system/restore [post]
func (h *BackupHandler) RestoreBackup(c *gin.Context) {
// 验证管理员权限
if !h.isAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
return
}
var req struct {
Filename string `json:"filename"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
if req.Filename == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "备份文件名不能为空"})
return
}
backupFile := filepath.Join("data", "backups", req.Filename)
// 检查备份文件是否存在
if _, err := os.Stat(backupFile); os.IsNotExist(err) {
c.JSON(http.StatusNotFound, gin.H{"error": "备份文件不存在"})
return
}
h.logger.Info("开始恢复系统...", zap.String("file", backupFile))
// TODO: 实现真实的恢复逻辑
// 1. 解压备份文件
// 2. 恢复数据库数据
// 3. 恢复配置文件
// 4. 重启服务使配置生效
h.logger.Info("系统恢复成功")
c.JSON(http.StatusOK, gin.H{
"message": "系统恢复成功,请重启服务使配置生效",
})
}
// DeleteBackup 删除备份
// @Summary 删除备份
// @Tags System
// @Accept json
// @Produce json
// @Param filename body string true "备份文件名"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/system/backup [delete]
func (h *BackupHandler) DeleteBackup(c *gin.Context) {
// 验证管理员权限
if !h.isAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
return
}
var req struct {
Filename string `json:"filename"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
if req.Filename == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "备份文件名不能为空"})
return
}
backupFile := filepath.Join("data", "backups", req.Filename)
// 检查备份文件是否存在
if _, err := os.Stat(backupFile); os.IsNotExist(err) {
c.JSON(http.StatusNotFound, gin.H{"error": "备份文件不存在"})
return
}
// 删除备份文件
if err := os.Remove(backupFile); err != nil {
h.logger.Error("删除备份失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "删除备份失败",
})
return
}
h.logger.Info("备份已删除", zap.String("file", backupFile))
c.JSON(http.StatusOK, gin.H{
"message": "备份已删除",
})
}
// DownloadBackup 下载备份文件
// @Summary 下载备份文件
// @Tags System
// @Accept json
// @Produce application/zip
// @Param filename query string true "备份文件名"
// @Success 200 {file} file
// @Router /api/v1/system/backup/download [get]
func (h *BackupHandler) DownloadBackup(c *gin.Context) {
// 验证管理员权限
if !h.isAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
return
}
filename := c.Query("filename")
if filename == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "备份文件名不能为空"})
return
}
backupFile := filepath.Join("data", "backups", filename)
// 检查备份文件是否存在
if _, err := os.Stat(backupFile); os.IsNotExist(err) {
c.JSON(http.StatusNotFound, gin.H{"error": "备份文件不存在"})
return
}
// 设置响应头
c.Header("Content-Type", "application/zip")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
// 发送文件
c.File(backupFile)
}
// isAdmin 验证是否为管理员
func (h *BackupHandler) isAdmin(c *gin.Context) bool {
userID, exists := c.Get("user_id")
if !exists {
return false
}
var user struct {
ID uint
Role string
}
if err := h.db.Table("users").Where("id = ?", userID).First(&user).Error; err != nil {
return false
}
return user.Role == "admin"
}
// parseTimestampFromFilename 从文件名解析时间戳
func parseTimestampFromFilename(filename string) string {
// 文件名格式:meshray_backup_20060102_150405.zip
if len(filename) < 30 {
return ""
}
// 提取时间戳部分
ts := filename[len("meshray_backup_") : len("meshray_backup_")+17]
if len(ts) != 17 {
return ""
}
// 格式化:20060102_150405 -> 2006-01-02 15:04:05
return fmt.Sprintf("%s-%s-%s %s:%s:%s",
ts[0:4], ts[4:6], ts[6:8], ts[9:11], ts[11:13], ts[14:16])
}
+57
View File
@@ -0,0 +1,57 @@
package handler
import (
"net/http"
"git.zkcoi.com/zkcoi/meshray/internal/service"
"github.com/gin-gonic/gin"
)
// DDNSHandler DDNS 相关处理器
type DDNSHandler struct {
ipDetection *service.IPDetectionService
}
// NewDDNSHandler 创建 DDNS 处理器(IP 检测用)
func NewDDNSHandler() *DDNSHandler {
return &DDNSHandler{
ipDetection: service.NewIPDetectionService(),
}
}
// DetectIP 检测公网 IP 地址
// @Summary 检测公网 IP 地址
// @Tags DDNS
// @Accept json
// @Produce json
// @Param record_type query string false "记录类型 (A|AAAA)" default(A)
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/services/ddns/detect-ip [get]
func (h *DDNSHandler) DetectIP(c *gin.Context) {
recordType := c.DefaultQuery("record_type", "A")
if recordType != "A" && recordType != "AAAA" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "不支持的记录类型,仅支持 A 或 AAAA",
})
return
}
ip, err := h.ipDetection.DetectIP(recordType)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "检测失败:" + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": gin.H{
"ip": ip,
},
"message": "检测成功",
})
}
+145
View File
@@ -0,0 +1,145 @@
package handler
import (
"encoding/json"
"net/http"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// DDNSStatsHandler DDNS 统计处理器
type DDNSStatsHandler struct {
db *gorm.DB
}
// NewDDNSStatsHandler 创建 DDNS 统计处理器
func NewDDNSStatsHandler(db *gorm.DB) *DDNSStatsHandler {
return &DDNSStatsHandler{
db: db,
}
}
// DDNSServiceInfo DDNS 服务信息(返回给前端)
type DDNSServiceInfo struct {
ID string `json:"id"`
Name string `json:"name"`
FullDomain string `json:"full_domain"` // 完整域名:subdomain.domain
CurrentIP string `json:"current_ip"` // 当前 IP
RecordType string `json:"record_type"` // A/AAAA/TXT/CNAME
Enabled bool `json:"enabled"` // 是否启用
Status string `json:"status"` // active/disabled
LastUpdated string `json:"last_updated"` // 最后更新时间
}
// GetDDNSStats 获取 DDNS 统计数据
// @Summary 获取 DDNS 服务统计
// @Tags DDNS
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/services/ddns/stats [get]
func (h *DDNSStatsHandler) GetDDNSStats(c *gin.Context) {
// 查询所有 DDNS 全功能模式服务
var services []model.Service
if err := h.db.Where("type = ? AND config_mode = ?", "DDNS", "fullservice").Find(&services).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "查询 DDNS 服务失败:" + err.Error(),
})
return
}
// 统计数据
total := len(services)
active := 0
// 构建服务列表
serviceList := make([]DDNSServiceInfo, 0, len(services))
for _, svc := range services {
// 判断是否活跃
status := "disabled"
if svc.Enabled {
status = "active"
active++
}
// 构建完整域名
fullDomain := ""
rootDomain := h.getDDNSDomain(svc.DDNSConfigID)
switch svc.RecordType {
case "A", "AAAA":
if svc.Subdomain != "" && rootDomain != "" {
fullDomain = svc.Subdomain + "." + rootDomain
}
case "TXT":
if svc.TXTRecordName != "" && rootDomain != "" {
fullDomain = svc.TXTRecordName + "." + rootDomain
}
case "CNAME":
if svc.Subdomain != "" && rootDomain != "" {
fullDomain = svc.Subdomain + "." + rootDomain
}
}
// 当前 IP
currentIP := svc.TargetIP
if svc.RecordType == "TXT" {
currentIP = "-"
}
// 格式化时间
lastUpdated := ""
if !svc.UpdatedAt.IsZero() {
lastUpdated = svc.UpdatedAt.Format(time.RFC3339)
}
serviceList = append(serviceList, DDNSServiceInfo{
ID: svc.ID,
Name: svc.Name,
FullDomain: fullDomain,
CurrentIP: currentIP,
RecordType: svc.RecordType,
Enabled: svc.Enabled,
Status: status,
LastUpdated: lastUpdated,
})
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": gin.H{
"total": total,
"active": active,
"services": serviceList,
},
"message": "获取成功",
})
}
// getDDNSDomain 获取 DDNS 配置中的根域名(辅助方法)
func (h *DDNSStatsHandler) getDDNSDomain(ddnsConfigID string) string {
if ddnsConfigID == "" {
return ""
}
// 从 ExternalService 表查询 DDNS 配置
var extService model.ExternalService
if err := h.db.Where("id = ?", ddnsConfigID).First(&extService).Error; err != nil {
return ""
}
// 解析 Config JSON 获取 root_domain
var config map[string]interface{}
if err := json.Unmarshal([]byte(extService.Config), &config); err != nil {
return ""
}
if rootDomain, ok := config["root_domain"].(string); ok {
return rootDomain
}
return ""
}
+269
View File
@@ -0,0 +1,269 @@
package handler
import (
"context"
"net/http"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gorm.io/gorm"
)
// NotificationHandler 通知处理器
type NotificationHandler struct {
notificationSvc *service.NotificationService
logger *zap.Logger
db *gorm.DB
}
// NewNotificationHandler 创建通知处理器
func NewNotificationHandler(notificationSvc *service.NotificationService, logger *zap.Logger, db *gorm.DB) *NotificationHandler {
return &NotificationHandler{
notificationSvc: notificationSvc,
logger: logger,
db: db,
}
}
// GetNotifications 获取用户通知列表(未读/已读)
// @Summary 获取用户通知列表
// @Tags Notifications
// @Accept json
// @Produce json
// @Param type query string false "通知类型 (all/alert/system/update)"
// @Param unread query bool false "是否仅未读"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/notifications [get]
func (h *NotificationHandler) GetNotifications(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
return
}
notifType := c.Query("type")
unreadOnly := c.Query("unread") == "true"
query := h.notificationSvc.GetDB().Where("user_id = ?", userID)
// 按类型筛选
if notifType != "" && notifType != "all" {
query = query.Where("type = ?", notifType)
}
// 只看未读
if unreadOnly {
query = query.Where("is_read = ?", false)
}
// 查询最近 100 条通知
var notifications []model.Notification
query.Order("created_at DESC").Limit(100).Find(&notifications)
c.JSON(http.StatusOK, gin.H{
"data": notifications,
})
}
// MarkAsRead 标记通知为已读
// @Summary 标记通知为已读
// @Tags Notifications
// @Accept json
// @Produce json
// @Param id path uint true "通知 ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/notifications/:id/read [post]
func (h *NotificationHandler) MarkAsRead(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
return
}
notifID := c.Param("id")
now := time.Now()
result := h.notificationSvc.GetDB().Model(&model.Notification{}).
Where("id = ? AND user_id = ?", notifID, userID).
Updates(map[string]interface{}{
"is_read": true,
"read_at": now,
})
if result.Error != nil || result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "通知不存在"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "已标记为已读",
})
}
// MarkAllAsRead 标记所有通知为已读
// @Summary 标记所有通知为已读
// @Tags Notifications
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/notifications/read-all [post]
func (h *NotificationHandler) MarkAllAsRead(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
return
}
now := time.Now()
result := h.notificationSvc.GetDB().Model(&model.Notification{}).
Where("user_id = ? AND is_read = ?", userID, false).
Updates(map[string]interface{}{
"is_read": true,
"read_at": now,
})
c.JSON(http.StatusOK, gin.H{
"message": "已全部标记为已读",
"affected": result.RowsAffected,
})
}
// DeleteNotification 删除通知
// @Summary 删除通知
// @Tags Notifications
// @Accept json
// @Produce json
// @Param id path uint true "通知 ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/notifications/:id [delete]
func (h *NotificationHandler) DeleteNotification(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
return
}
notifID := c.Param("id")
result := h.notificationSvc.GetDB().Where("id = ? AND user_id = ?", notifID, userID).
Delete(&model.Notification{})
if result.Error != nil || result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "通知不存在"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "通知已删除",
})
}
// GetUnreadCount 获取未读通知数
// @Summary 获取未读通知数
// @Tags Notifications
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/notifications/unread-count [get]
func (h *NotificationHandler) GetUnreadCount(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
return
}
var count int64
h.notificationSvc.GetDB().Model(&model.Notification{}).
Where("user_id = ? AND is_read = ?", userID, false).
Count(&count)
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"count": count,
},
})
}
// TestSendNotification 测试发送通知(开发用)
// @Summary 测试发送通知
// @Tags Notifications
// @Accept json
// @Produce json
// @Param message body object true "通知内容"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/notifications/test [post]
func (h *NotificationHandler) TestSendNotification(c *gin.Context) {
var req struct {
Type string `json:"type"`
Title string `json:"title"`
Message string `json:"message"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
return
}
// 从上下文获取用户 ID
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
return
}
// 发送测试通知
h.notificationSvc.SendSystemNotification(userID.(uint), req.Title, req.Message)
c.JSON(http.StatusOK, gin.H{
"message": "测试通知已发送",
})
}
// ClearExpiredNotifications 清理过期通知(定时任务)
func (h *NotificationHandler) ClearExpiredNotifications() {
// 启动定期清理任务(每 24 小时清理一次超过 30 天的通知)
go func() {
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
for range ticker.C {
h.cleanupOldNotifications()
}
}()
h.logger.Info("清理过期通知完成")
}
// cleanupOldNotifications 清理超过 30 天的通知记录
func (h *NotificationHandler) cleanupOldNotifications() {
ctx := context.Background()
cutoffTime := time.Now().AddDate(0, 0, -30)
result := h.db.WithContext(ctx).
Where("created_at < ?", cutoffTime).
Delete(&model.Notification{})
if result.Error != nil {
h.logger.Error("清理过期通知失败", zap.Error(result.Error))
} else {
h.logger.Info("清理过期通知完成", zap.Int64("deleted", result.RowsAffected))
}
}
// StartNotificationCleaner 启动通知清理定时器
func (h *NotificationHandler) StartNotificationCleaner(stopCh <-chan struct{}) {
ticker := time.NewTicker(24 * time.Hour) // 每天执行一次
defer ticker.Stop()
for {
select {
case <-ticker.C:
h.ClearExpiredNotifications()
case <-stopCh:
return
}
}
}
+173
View File
@@ -0,0 +1,173 @@
package handler
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// UpdateHandler 系统更新处理器
type UpdateHandler struct {
httpClient *http.Client
currentVersion string
}
// NewUpdateHandler 创建系统更新处理器
func NewUpdateHandler(currentVersion string) *UpdateHandler {
return &UpdateHandler{
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
currentVersion: currentVersion,
}
}
// GitHubRelease GitHub 发布响应结构
type GitHubRelease struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
Body string `json:"body"`
PublishedAt string `json:"published_at"`
HtmlURL string `json:"html_url"`
Assets []struct {
Name string `json:"name"`
DownloadURL string `json:"browser_download_url"`
Size int64 `json:"size"`
} `json:"assets"`
}
// CheckUpdateResponse 检查更新响应
type CheckUpdateResponse struct {
HasUpdate bool `json:"has_update"`
LatestVersion string `json:"latest_version"`
CurrentVersion string `json:"current_version"`
ReleaseNotes string `json:"release_notes"`
DownloadURL string `json:"download_url"`
PublishedAt string `json:"published_at"`
Error string `json:"error,omitempty"`
}
// CheckUpdate 检查更新
// @Summary 检查系统更新
// @Tags System
// @Accept json
// @Produce json
// @Success 200 {object} CheckUpdateResponse
// @Router /api/v1/system/update/check [get]
func (h *UpdateHandler) CheckUpdate() (*CheckUpdateResponse, error) {
// Gitea Releases API
repo := "zkcoi/meshray"
url := fmt.Sprintf("https://git.zkcoi.com/api/v1/repos/%s/releases/latest", repo)
resp, err := h.httpClient.Get(url)
if err != nil {
return &CheckUpdateResponse{
HasUpdate: false,
CurrentVersion: h.currentVersion,
Error: "检查更新失败:" + err.Error(),
}, nil
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return &CheckUpdateResponse{
HasUpdate: false,
CurrentVersion: h.currentVersion,
Error: "读取响应失败:" + err.Error(),
}, nil
}
var release GitHubRelease
if err := json.Unmarshal(body, &release); err != nil {
return &CheckUpdateResponse{
HasUpdate: false,
CurrentVersion: h.currentVersion,
Error: "解析响应失败:" + err.Error(),
}, nil
}
// 移除版本号前缀的 'v'
latestVersion := release.TagName
if len(latestVersion) > 0 && latestVersion[0] == 'v' {
latestVersion = latestVersion[1:]
}
currentVersion := h.currentVersion
if len(currentVersion) > 0 && currentVersion[0] == 'v' {
currentVersion = currentVersion[1:]
}
// 比较版本号
hasUpdate := compareVersions(latestVersion, currentVersion) > 0
downloadURL := release.HtmlURL
if len(release.Assets) > 0 {
// 优先选择 Windows 可执行文件
for _, asset := range release.Assets {
if asset.Name == "meshray.exe" {
downloadURL = asset.DownloadURL
break
}
}
}
return &CheckUpdateResponse{
HasUpdate: hasUpdate,
LatestVersion: release.TagName,
CurrentVersion: h.currentVersion,
ReleaseNotes: release.Body,
DownloadURL: downloadURL,
PublishedAt: release.PublishedAt,
}, nil
}
// compareVersions 比较版本号
// 返回:1 (v1 > v2), 0 (v1 == v2), -1 (v1 < v2)
func compareVersions(v1, v2 string) int {
if v1 == v2 {
return 0
}
// 简单版本号比较(格式:major.minor.patch
parts1 := parseVersion(v1)
parts2 := parseVersion(v2)
for i := 0; i < len(parts1) && i < len(parts2); i++ {
if parts1[i] > parts2[i] {
return 1
} else if parts1[i] < parts2[i] {
return -1
}
}
// 如果前面都相同,比较长度
if len(parts1) > len(parts2) {
return 1
}
return -1
}
// parseVersion 解析版本号字符串为整数数组
func parseVersion(version string) []int {
var parts []int
current := 0
for i, ch := range version {
if ch == '.' {
parts = append(parts, current)
current = 0
} else if ch >= '0' && ch <= '9' {
current = current*10 + int(ch-'0')
}
// 处理最后一个字符
if i == len(version)-1 {
parts = append(parts, current)
}
}
return parts
}
+97
View File
@@ -0,0 +1,97 @@
package logging
import (
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
// Logger 全局日志实例
var Logger *zap.Logger
// Config 日志配置
type Config struct {
Level string `json:"level"`
Format string `json:"format"`
Output string `json:"output"`
MaxSize int `json:"max_size"`
MaxBackups int `json:"max_backups"`
MaxAge int `json:"max_age"`
}
// Init 初始化日志系统
func Init(cfg Config) error {
// 解析日志级别
level := zap.NewAtomicLevel()
switch cfg.Level {
case "debug":
level.SetLevel(zap.DebugLevel)
case "info":
level.SetLevel(zap.InfoLevel)
case "warn":
level.SetLevel(zap.WarnLevel)
case "error":
level.SetLevel(zap.ErrorLevel)
default:
level.SetLevel(zap.InfoLevel)
}
// 配置编码器
var encoder zapcore.Encoder
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.TimeKey = "time"
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.CallerKey = "caller"
encoderConfig.MessageKey = "message"
encoderConfig.LevelKey = "level"
if cfg.Format == "console" {
encoder = zapcore.NewConsoleEncoder(encoderConfig)
} else {
encoder = zapcore.NewJSONEncoder(encoderConfig)
}
// 配置输出
var writeSyncer zapcore.WriteSyncer
if cfg.Output != "" {
// 使用文件轮转
lumberJackLogger := &lumberjack.Logger{
Filename: cfg.Output,
MaxSize: cfg.MaxSize,
MaxBackups: cfg.MaxBackups,
MaxAge: cfg.MaxAge,
Compress: true,
LocalTime: true,
}
writeSyncer = zapcore.AddSync(lumberJackLogger)
} else {
writeSyncer = zapcore.AddSync(os.Stdout)
}
// 创建核心
core := zapcore.NewCore(encoder, writeSyncer, level)
// 创建 logger
Logger = zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel))
return nil
}
// GetLogger 获取日志实例
func GetLogger() *zap.Logger {
if Logger == nil {
// 如果未初始化,返回一个默认的 stdout logger
Logger, _ = zap.NewDevelopment()
}
return Logger
}
// Sync 刷新日志缓冲
func Sync() error {
if Logger != nil {
return Logger.Sync()
}
return nil
}
+2
View File
@@ -0,0 +1,2 @@
// Package model defines all database models for MeshRay.
package model
+363
View File
@@ -0,0 +1,363 @@
package model
import (
"time"
)
// Network 组网模型 - 使用雪花算法生成 ID(uint64)
type Network struct {
ID uint64 `gorm:"primaryKey;type:bigint" json:"id"` // 雪花算法 ID
Name string `gorm:"type:varchar(64);not null;uniqueIndex" json:"name"`
SubnetIPv4 string `gorm:"type:varchar(18);not null" json:"subnet_ipv4"`
SubnetIPv6 string `gorm:"type:varchar(43)" json:"subnet_ipv6,omitempty"`
Mode string `gorm:"type:varchar(16);not null;default:'enhanced';index" json:"mesh_mode"` // native | enhanced
WGMode string `gorm:"type:varchar(16);not null;default:'userspace'" json:"wg_mode"` // kernel | userspace
PolicyID uint64 `gorm:"type:bigint;index" json:"policy_id"`
DHCPEnabled bool `gorm:"default:true" json:"dhcp_enabled"`
TunEnabled bool `gorm:"default:true" json:"tun_enabled"`
TunName string `gorm:"type:varchar(16);default:'meshray-tun'" json:"tun_name"`
MTU int `gorm:"default:1420" json:"mtu"`
Password string `gorm:"type:varchar(128)" json:"-"` // AES-256-GCM 加密存储
Status string `gorm:"type:varchar(16);default:'running';index" json:"status"` // running | stopped | error
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
// 关联字段
Devices []Device `gorm:"foreignKey:NetworkID" json:"devices,omitempty"`
// DDNS 同步字段
DDNSEnabled bool `gorm:"default:false" json:"ddns_enabled"`
DDNSServiceID string `gorm:"type:varchar(36);index" json:"ddns_service_id"` // ExternalService.ID
DDNSUsageID string `gorm:"type:varchar(36);index" json:"ddns_usage_id"` // DDNSUsage.ID
DDNSPrefix string `gorm:"type:varchar(255)" json:"ddns_prefix"` // TXT 记录前缀(如:_meshray.ABC123
}
// Device 设备模型 - 使用雪花算法生成 ID(uint64)
type Device struct {
ID uint64 `gorm:"primaryKey;type:bigint" json:"id"` // 雪花算法 ID
NetworkID uint64 `gorm:"type:bigint;not null;index:idx_device_network" json:"network_id"` // 所属网络 ID
Name string `gorm:"type:varchar(64);not null" json:"name"`
VirtualIP string `gorm:"type:varchar(45);not null" json:"virtual_ip"`
PublicKey string `gorm:"type:varchar(64);not null" json:"public_key"`
PresharedKey string `gorm:"type:varchar(64)" json:"-"`
Endpoint string `gorm:"type:varchar(261)" json:"endpoint,omitempty"` // 原始公网 Endpointhost:port),用于增强模式回退
IsRelayCapable bool `gorm:"default:false" json:"is_relay_capable"` // 是否作为 Mesh 中继节点
Status string `gorm:"type:varchar(16);default:'offline';index" json:"status"` // online | offline
LastSeen time.Time `json:"last_seen"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
}
// Policy 传输策略模型
type Policy struct {
ID uint64 `gorm:"primaryKey;type:bigint" json:"id"` // Snowflake ID, matches Network.PolicyID
Name string `gorm:"size:255;not null;index" json:"name"`
Type string `gorm:"size:32;not null;default:'custom'" json:"type"` // system | custom
Description string `gorm:"size:512" json:"description"`
LayerConfig string `gorm:"type:text" json:"layer_config"` // JSON 格式存储 9 层链路配置
GlobalParams string `gorm:"type:text" json:"global_params"` // JSON 格式存储全局参数
Enabled bool `gorm:"default:true" json:"enabled"`
IsDefault bool `gorm:"default:false" json:"is_default"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TURNAuthType TURN 鉴权方式
type TURNAuthType string
const (
TURNAuthCredential TURNAuthType = "credential" // Username + PasswordRFC 5766,自建 coturn
TURNAuthToken TURNAuthType = "token" // 临时 Token(商业 TURN 服务:Twilio、Xirsys 等)
TURNAuthSecret TURNAuthType = "secret" // Shared Secret(由信令服务器签发临时凭证)
)
// Service 服务模型 - 前端用户视角的服务实例
type Service struct {
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
Name string `gorm:"type:varchar(64);not null" json:"name"`
Type string `gorm:"type:varchar(8);not null;index" json:"type"` // "tun"/"turn"/"ddns"
Address string `gorm:"type:varchar(255);not null" json:"address"`
Port int `gorm:"not null" json:"port"`
Protocols []string `gorm:"type:text;serializer:json" json:"protocols"` // JSON: ["udp","tcp","tls","ws","wss"]
AuthType string `gorm:"type:varchar(16)" json:"auth_type,omitempty"`
AuthUsername string `gorm:"type:varchar(128)" json:"username,omitempty"`
AuthPassword string `gorm:"type:varchar(128)" json:"password,omitempty"` // 仅用于输入,存储时仍需加密处理
Token string `gorm:"type:text" json:"token,omitempty"`
SharedSecret string `gorm:"type:varchar(128)" json:"shared_secret,omitempty"`
MTU int `gorm:"default:1420" json:"mtu,omitempty"`
Provider string `gorm:"type:varchar(32)" json:"provider,omitempty"`
Domain string `gorm:"type:varchar(255)" json:"domain,omitempty"`
RecordType string `gorm:"type:varchar(8)" json:"record_type,omitempty"`
// DDNS 全功能模式字段
ConfigMode string `gorm:"type:varchar(16);default:'infrastructure'" json:"config_mode"` // "infrastructure" | "fullservice"
DDNSConfigID string `gorm:"type:varchar(36)" json:"ddns_config_id,omitempty"` // 关联的 DDNS 配置 ID(全功能模式)
Subdomain string `gorm:"type:varchar(255)" json:"subdomain,omitempty"` // 主机记录(全功能模式)
TargetIP string `gorm:"type:varchar(64)" json:"target_ip,omitempty"` // 目标 IP(全功能模式)
TXTRecordName string `gorm:"type:varchar(255)" json:"txt_record_name,omitempty"` // TXT 记录名称(全功能模式)
TXTValue string `gorm:"type:text" json:"txt_value,omitempty"` // TXT 记录值(全功能模式)
CNAMETarget string `gorm:"type:varchar(255)" json:"cname_target,omitempty"` // CNAME 目标域名(全功能模式)
TTL int `gorm:"default:600" json:"ttl,omitempty"` // TTL(全功能模式)
Status string `gorm:"type:varchar(16);default:'active'" json:"status"` // active | inactive
Reachable bool `gorm:"-" json:"reachable"` // 连通性状态
Latency int `gorm:"-" json:"latency"` // 延迟
WSEnabled bool `gorm:"default:false" json:"ws_enabled"`
IndependentWS string `gorm:"type:text" json:"independent_ws,omitempty"` // JSON: {address, port, wss}
Enabled bool `gorm:"default:true;index" json:"enabled"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
// ExternalService 外部服务模型 - 统一管理所有外部依赖服务
// 按照 README 6.4 节定义,通过 category + serviceType + config JSON 实现无限扩展
type ExternalService struct {
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
Category string `gorm:"type:varchar(32);not null;index" json:"category"` // "networking" / "dns" / "security" / "gateway" / "automation"
ServiceType string `gorm:"type:varchar(64);not null;index" json:"service_type"` // stun_server / turn_server / ddns_aliyun / ssl_acme 等
Name string `gorm:"type:varchar(64);not null" json:"name"`
Enabled bool `gorm:"default:true;index" json:"enabled"`
Address string `gorm:"type:varchar(255)" json:"address,omitempty"` // 服务器地址(可选)
Port int `json:"port,omitempty"` // 端口(可选)
Config string `gorm:"type:text;not null" json:"config"` // JSON,结构由 serviceType 决定(敏感字段加密)
Status string `gorm:"type:varchar(16);default:'unknown';index" json:"status"` // reachable / unreachable / unknown
LastTestAt *time.Time `json:"last_test_at,omitempty"` // 最后测试时间
LatencyMs int `gorm:"default:0" json:"latency_ms"` // 延迟(ms
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
// SystemSetting 系统设置模型 - 单例模式,全局只有一条记录
type SystemSetting struct {
ID uint `gorm:"primaryKey;type:bigint" json:"id"` // 固定为 1
ServerIP string `gorm:"type:varchar(45)" json:"server_ip"` // 服务端公网 IP
ServerPort int `gorm:"default:51820" json:"server_port"` // WireGuard 监听端口
ServerPublicKey string `gorm:"type:varchar(64)" json:"server_public_key,omitempty"` // WireGuard 服务端公钥
DDNSDomain string `gorm:"type:varchar(255)" json:"ddns_domain"` // DDNS 域名(如 mesh.example.com
TURNMode string `gorm:"type:varchar(16);default:'auto'" json:"turn_mode"` // auto/manual
TURNURL string `gorm:"type:varchar(255)" json:"turn_url"` // TURN 服务器 URL(手动模式)
TURNUsername string `gorm:"type:varchar(128)" json:"turn_username,omitempty"` // TURN 用户名
TURNPassword string `gorm:"type:varchar(128)" json:"-"` // TURN 密码(加密存储)
LogLevel string `gorm:"type:varchar(16);default:'info'" json:"log_level"` // debug/info/warn/error
LogFormat string `gorm:"type:varchar(16);default:'console'" json:"log_format"` // console/json
MaxBackups int `gorm:"default:7" json:"max_backups"` // 日志最大保留份数
MaxAge int `gorm:"default:30" json:"max_age"` // 日志最大保留天数
Theme string `gorm:"type:varchar(32);default:'light'" json:"theme"` // light/dark/auto
Language string `gorm:"type:varchar(16);default:'zh-CN'" json:"language"` // zh-CN/en-US
CreatedAt time.Time `gorm:"autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updatedAt"`
}
// DDNSProvider DDNS 服务商配置模型(配置层)
type DDNSProvider struct {
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
Name string `gorm:"type:varchar(64);not null" json:"name"` // 服务商名称,如 "Cloudflare-Mesh"
Provider string `gorm:"type:varchar(32);not null" json:"provider"` // cloudflare, aliyun, tencent
Domain string `gorm:"type:varchar(255);not null" json:"domain"` // 主域名
AccessKey string `gorm:"type:varchar(255)" json:"-"` // 加密存储
SecretKey string `gorm:"type:text" json:"-"` // 加密存储
Config string `gorm:"type:text" json:"config"` // JSON 扩展配置
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// DDNSUsageType DDNS 用途类型
type DDNSUsageType string
const (
DDNSUsageMeshSeedSync DDNSUsageType = "meshseed_sync" // MeshSeed 同步(TXT 记录)
DDNSUsageIPResolve DDNSUsageType = "ip_resolve" // IP 动态解析(A/AAAA 记录)
DDNSUsageDeviceBind DDNSUsageType = "device_bind" // 设备域名绑定(CNAME 记录)
)
// DDNSUsage DDNS 用途定义模型(应用层)
type DDNSUsage struct {
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
ProviderID string `gorm:"type:varchar(36);not null;index" json:"provider_id"` // 关联的 Provider(即 ExternalService.ID
UsageType DDNSUsageType `gorm:"type:varchar(32);not null" json:"usage_type"`
RecordType string `gorm:"type:varchar(8);not null" json:"record_type"` // TXT, A, AAAA, CNAME
RecordPrefix string `gorm:"type:varchar(255);not null" json:"record_prefix"` // 记录前缀
Description string `gorm:"type:varchar(512)" json:"description"`
IsExclusive bool `gorm:"default:false" json:"is_exclusive"` // 是否独占
PrefixMode string `gorm:"type:varchar(16);not null;default:'auto'" json:"prefix_mode"` // "auto" | "custom"
CreatedAt time.Time `json:"created_at"`
// 关联关系
Provider *DDNSProvider `gorm:"foreignKey:ProviderID" json:"provider,omitempty"`
}
// NetworkDDNSBinding 网络 DDNS 绑定模型(使用层)
type NetworkDDNSBinding struct {
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
NetworkID uint64 `gorm:"type:bigint;not null;uniqueIndex" json:"network_id"` // 每个网络只能绑定一个
UsageID string `gorm:"type:varchar(36);not null" json:"usage_id"` // 选择的用途
ProviderID string `gorm:"type:varchar(36);not null" json:"provider_id"` // 冗余存储
Status string `gorm:"type:varchar(16);default:'active'" json:"status"` // active, sync_pending, sync_failed
LastSyncAt *time.Time `json:"last_sync_at"`
SyncMessage string `gorm:"type:text" json:"sync_message"` // 同步结果/错误信息
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// 关联字段
Network *Network `gorm:"foreignKey:NetworkID" json:"network,omitempty"`
Usage *DDNSUsage `gorm:"foreignKey:UsageID" json:"usage,omitempty"`
}
// MeshSeed 组网凭证模型
type MeshSeed struct {
ID uint `gorm:"primaryKey" json:"id"`
SeedID string `gorm:"size:64;not null;uniqueIndex" json:"seed_id"` // 16 字节随机 Base64
NetworkID uint64 `gorm:"type:bigint;not null;index" json:"network_id"` // ✅ 与 Network.ID 类型一致
Network *Network `gorm:"foreignKey:NetworkID" json:"network,omitempty"`
JoinToken string `gorm:"size:512;not null" json:"join_token"` // Base64 编码的完整 MeshSeed
Signature string `gorm:"size:512;not null" json:"signature"` // Ed25519 签名
IssuerNodeID string `gorm:"size:255;not null" json:"issuer_node_id"`
MaxUses int `gorm:"default:10" json:"max_uses"`
UsedCount int `gorm:"default:0" json:"used_count"`
ExpiresAt time.Time `gorm:"index" json:"expires_at"`
DDNSEnabled bool `gorm:"default:false" json:"ddns_enabled"`
UpdateVersion int `gorm:"default:0" json:"update_version"`
DDNSDomain string `gorm:"size:255" json:"ddns_domain"`
Revoked bool `gorm:"default:false" json:"revoked"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// SecurityKey 安全密钥模型(用于存储 Ed25519 签名密钥等)
type SecurityKey struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:64;not null;uniqueIndex" json:"name"` // 密钥名称,如 "meshseed_signing"
Value string `gorm:"size:512;not null" json:"-"` // Base64 编码的密钥值
Algorithm string `gorm:"size:32;not null" json:"algorithm"` // 算法类型:ed25519, rsa 等
Purpose string `gorm:"size:128" json:"purpose"` // 用途描述
CreatedAt time.Time `gorm:"autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updatedAt"`
}
// PendingJoin 待加入审核模型
type PendingJoin struct {
ID uint `gorm:"primaryKey" json:"id"`
SeedID string `gorm:"size:64;not null;index" json:"seed_id"`
DeviceName string `gorm:"size:255;not null" json:"device_name"`
RequestIP string `gorm:"size:64" json:"request_ip"`
Status string `gorm:"size:32;default:'pending'" json:"status"` // pending | approved | rejected
ExpireAt time.Time `gorm:"index" json:"expire_at"` // 72 小时超时
ApprovedAt *time.Time `json:"approved_at"`
RejectedAt *time.Time `json:"rejected_at"`
Reason string `gorm:"size:512" json:"reason"` // 拒绝原因
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// AlertRule 告警规则模型
type AlertRule struct {
ID uint `gorm:"primaryKey" json:"id"`
Type string `gorm:"size:32;not null;index" json:"type"` // resource | link | service
Name string `gorm:"size:255;not null" json:"name"`
Threshold float64 `gorm:"not null" json:"threshold"`
Operator string `gorm:"size:8;not null" json:"operator"` // gt | lt | eq
NotifyEmail bool `gorm:"default:false" json:"notify_email"`
NotifyWebhook bool `gorm:"default:false" json:"notify_webhook"`
WebhookURL string `gorm:"size:512" json:"webhook_url"`
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// User 用户模型
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:64;not null;uniqueIndex" json:"username"`
PasswordHash string `gorm:"size:255;not null" json:"-"` // bcrypt 加密存储
Email string `gorm:"size:255" json:"email"`
Role string `gorm:"size:32;default:'user'" json:"role"` // admin | user
Status string `gorm:"size:32;default:'active'" json:"status"` // active | banned
LastLoginAt time.Time `json:"last_login_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// AuditLog 审计日志模型
type AuditLog struct {
ID uint `gorm:"primaryKey" json:"id"`
Action string `gorm:"size:64;not null;index" json:"action"` // create_network | delete_network | generate_seed | etc.
OperatorIP string `gorm:"size:64;not null" json:"operator_ip"`
UserAgent string `gorm:"size:512" json:"user_agent"`
Detail string `gorm:"type:text" json:"detail"` // JSON 格式存储详细信息
CreatedAt time.Time `gorm:"index" json:"created_at"`
}
// SystemConfig 系统配置模型(全局唯一)
type SystemConfig struct {
ID uint `gorm:"primaryKey" json:"id"`
Key string `gorm:"size:64;not null;uniqueIndex" json:"key"` // 配置键,如 "wg_mode"
Value string `gorm:"size:255;not null" json:"value"` // 配置值,如 "auto" | "kernel" | "userspace"
UpdatedAt time.Time `json:"updated_at"`
}
// Notification 通知模型
type Notification struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"not null;index" json:"user_id"` // 接收用户 ID
Type string `gorm:"size:32;not null;index" json:"type"` // alert/system/update/ddns
Priority int `gorm:"default:2;index" json:"priority"` // 1=low, 2=medium, 3=high
Title string `gorm:"size:255;not null" json:"title"`
Message string `gorm:"type:text;not null" json:"message"`
Data string `gorm:"type:text" json:"data,omitempty"` // JSON 格式额外数据
IsRead bool `gorm:"default:false;index" json:"is_read"` // 是否已读
ReadAt *time.Time `json:"read_at,omitempty"` // 阅读时间
CreatedAt time.Time `gorm:"autoCreateTime;index" json:"created_at"` // 创建时间
}
// DDNSConfig DDNS 配置模型
type DDNSConfig struct {
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
Provider string `gorm:"type:varchar(32);not null" json:"provider"` // aliyun | tencent
AccessKey string `gorm:"type:varchar(128);not null" json:"accessKey"`
SecretKey string `gorm:"type:varchar(128);not null" json:"-"` // AES-256-GCM 加密存储
Domain string `gorm:"type:varchar(255);not null" json:"domain"`
TXTRecordName string `gorm:"type:varchar(255)" json:"txtRecordName"` // 自动生成
SyncMode string `gorm:"type:varchar(16);default:'auto'" json:"syncMode"` // auto | manual
RetryCount int `gorm:"default:10" json:"retryCount"`
RetryInterval int `gorm:"default:300" json:"retryInterval"` // 秒
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updatedAt"`
}
// NetworkMemberRole 网络成员角色
type NetworkMemberRole string
const (
RoleOwner NetworkMemberRole = "owner" // 创建者,拥有所有权限
RoleMember NetworkMemberRole = "member" // 加入者,只能使用网络
)
// NetworkMember 网络成员表 - 管理网络成员及其角色
type NetworkMember struct {
ID uint64 `gorm:"primaryKey;type:bigint" json:"id"`
NetworkID uint64 `gorm:"type:bigint;not null;uniqueIndex:idx_network_user" json:"networkID"`
UserID uint64 `gorm:"type:bigint;not null;uniqueIndex:idx_network_user" json:"userID"`
Role NetworkMemberRole `gorm:"type:varchar(16);not null;default:'member'" json:"role"` // owner | member
JoinedAt time.Time `gorm:"autoCreateTime" json:"joinedAt"`
// 关联
Network *Network `gorm:"foreignKey:NetworkID" json:"network,omitempty"`
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
}
// TableName 指定表名
func (NetworkMember) TableName() string {
return "network_members"
}
// Permission 权限类型
type Permission string
const (
PermManageNetwork Permission = "manage_network" // 管理网络配置
PermAddPeer Permission = "add_peer" // 添加节点
PermRemovePeer Permission = "remove_peer" // 移除节点
PermKickMember Permission = "kick_member" // 踢出成员
PermUpdateConfig Permission = "update_config" // 更新组网参数
PermDeleteNetwork Permission = "delete_network" // 解散网络
PermLeaveNetwork Permission = "leave_network" // 退出网络
)
+225
View File
@@ -0,0 +1,225 @@
package scheduler
import (
"context"
"fmt"
"sync"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/service"
"go.uber.org/zap"
"gorm.io/gorm"
)
// DDNSUpdaterService DDNS 自动更新调度服务
type DDNSUpdaterService struct {
db *gorm.DB
logger *zap.Logger
ipDetection *service.IPDetectionService
ddnsOperation *service.DDNSOperationService
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
checkInterval time.Duration // 检测间隔
updateThreshold int // IP 变化阈值(连续多少次不同才更新)
}
// NewDDNSUpdaterService 创建 DDNS 自动更新服务
func NewDDNSUpdaterService(
db *gorm.DB,
logger *zap.Logger,
checkInterval time.Duration,
) *DDNSUpdaterService {
ctx, cancel := context.WithCancel(context.Background())
return &DDNSUpdaterService{
db: db,
logger: logger,
ipDetection: service.NewIPDetectionService(),
ddnsOperation: service.NewDDNSOperationService(logger, db),
ctx: ctx,
cancel: cancel,
checkInterval: checkInterval,
updateThreshold: 2, // 默认连续 2 次检测到不同 IP 才更新
}
}
// Start 启动后台自动更新任务
func (s *DDNSUpdaterService) Start() error {
s.logger.Info("启动 DDNS 自动更新服务",
zap.Duration("检测间隔", s.checkInterval),
zap.Int("变化阈值", s.updateThreshold))
s.wg.Add(1)
go s.runUpdater()
return nil
}
// Stop 停止后台自动更新任务
func (s *DDNSUpdaterService) Stop() {
s.logger.Info("停止 DDNS 自动更新服务")
s.cancel()
s.wg.Wait()
}
// runUpdater 运行自动更新协程
func (s *DDNSUpdaterService) runUpdater() {
defer s.wg.Done()
ticker := time.NewTicker(s.checkInterval)
defer ticker.Stop()
// 用于跟踪每个 DDNS 服务的连续不同 IP 次数
consecutiveChanges := make(map[string]int)
lastIPs := make(map[string]string)
for {
select {
case <-ticker.C:
s.checkAndUpdate(consecutiveChanges, lastIPs)
case <-s.ctx.Done():
s.logger.Info("DDNS 自动更新服务已停止")
return
}
}
}
// checkAndUpdate 检测并更新 DDNS 记录
func (s *DDNSUpdaterService) checkAndUpdate(
consecutiveChanges map[string]int,
lastIPs map[string]string,
) {
s.logger.Debug("开始检测 DDNS 服务 IP 变化")
// 查询所有启用的 DDNS 全功能模式服务
var ddnsServices []model.Service
if err := s.db.Where("type = ? AND config_mode = ? AND enabled = ?",
"DDNS", "fullservice", true).Find(&ddnsServices).Error; err != nil {
s.logger.Error("查询 DDNS 服务失败", zap.Error(err))
return
}
s.logger.Info("找到启用的 DDNS 全功能服务", zap.Int("数量", len(ddnsServices)))
for _, svc := range ddnsServices {
s.wg.Add(1)
go func(service model.Service) {
defer s.wg.Done()
s.processSingleService(&service, consecutiveChanges, lastIPs)
}(svc)
}
}
// processSingleService 处理单个 DDNS 服务
func (s *DDNSUpdaterService) processSingleService(
service *model.Service,
consecutiveChanges map[string]int,
lastIPs map[string]string,
) {
serviceID := service.ID
recordType := service.RecordType
// 只处理 A 和 AAAA 记录(需要 IP 检测)
if recordType != "A" && recordType != "AAAA" {
return
}
// 确定要比较的字段
var currentConfigIP string
switch recordType {
case "A":
currentConfigIP = service.TargetIP
case "AAAA":
currentConfigIP = service.TargetIP
}
if currentConfigIP == "" {
s.logger.Warn("DDNS 服务目标 IP 为空,跳过检测",
zap.String("service_id", serviceID))
return
}
// 检测当前公网 IP
detectedIP, err := s.ipDetection.DetectIP(recordType)
if err != nil {
s.logger.Error("检测 IP 失败",
zap.String("service_id", serviceID),
zap.String("record_type", recordType),
zap.Error(err))
return
}
s.logger.Debug("IP 检测结果",
zap.String("service_id", serviceID),
zap.String("record_type", recordType),
zap.String("配置 IP", currentConfigIP),
zap.String("检测 IP", detectedIP))
// 检查 IP 是否变化
mapKey := fmt.Sprintf("%s_%s", serviceID, recordType)
if detectedIP != currentConfigIP {
// IP 不同,增加计数
consecutiveChanges[mapKey]++
s.logger.Debug("IP 不一致",
zap.String("service_id", serviceID),
zap.Int("连续次数", consecutiveChanges[mapKey]),
zap.Int("阈值", s.updateThreshold))
// 达到阈值才更新
if consecutiveChanges[mapKey] >= s.updateThreshold {
s.logger.Info("IP 变化达到阈值,开始更新 DNS 记录",
zap.String("service_id", serviceID),
zap.String("旧 IP", currentConfigIP),
zap.String("新 IP", detectedIP))
// 更新 DNS 记录
err := s.updateDNSRecord(service, detectedIP)
if err != nil {
s.logger.Error("更新 DNS 记录失败",
zap.String("service_id", serviceID),
zap.Error(err))
} else {
s.logger.Info("DNS 记录更新成功",
zap.String("service_id", serviceID),
zap.String("新 IP", detectedIP))
// 重置计数
consecutiveChanges[mapKey] = 0
lastIPs[mapKey] = detectedIP
}
}
} else {
// IP 相同,重置计数
if consecutiveChanges[mapKey] > 0 {
s.logger.Debug("IP 恢复一致,重置计数器",
zap.String("service_id", serviceID))
consecutiveChanges[mapKey] = 0
}
}
// 更新最后检测的 IP
lastIPs[mapKey] = detectedIP
}
// updateDNSRecord 更新 DNS 记录
func (s *DDNSUpdaterService) updateDNSRecord(svc *model.Service, newIP string) error {
// 使用 DDNSOperationService 的 UpdateDNSRecord 方法
err := s.ddnsOperation.UpdateDNSRecord(svc, svc.RecordType, svc.Subdomain, newIP, 300)
if err != nil {
return fmt.Errorf("更新 DNS 记录失败:%w", err)
}
// 更新数据库中的 IP
if err := s.db.Model(&model.Service{}).Where("id = ?", svc.ID).Update("target_ip", newIP).Error; err != nil {
s.logger.Warn("更新数据库中的 IP 失败",
zap.String("service_id", svc.ID),
zap.Error(err))
}
return nil
}
+309
View File
@@ -0,0 +1,309 @@
package service
import (
"archive/zip"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"gorm.io/gorm"
)
// BackupService 备份服务
type BackupService struct {
db *gorm.DB
}
// NewBackupService 创建备份服务
func NewBackupService(db *gorm.DB) *BackupService {
return &BackupService{
db: db,
}
}
// CreateBackup 创建系统备份
func (s *BackupService) CreateBackup(ctx context.Context, backupFile string) error {
// 1. 导出数据库数据到临时文件
tempDir := filepath.Join("data", "temp_backup")
if err := os.MkdirAll(tempDir, 0755); err != nil {
return fmt.Errorf("创建临时目录失败:%w", err)
}
defer os.RemoveAll(tempDir)
// 导出数据库
dbDumpFile := filepath.Join(tempDir, "meshray.sql")
if err := s.dumpDatabase(dbDumpFile); err != nil {
return fmt.Errorf("导出数据库失败:%w", err)
}
// 2. 备份配置文件
configFiles := []string{
"config.yaml",
}
for _, configFile := range configFiles {
if _, err := os.Stat(configFile); err == nil {
// 复制配置文件到临时目录
src, err := os.Open(configFile)
if err != nil {
return fmt.Errorf("打开配置文件失败:%w", err)
}
defer src.Close()
dstPath := filepath.Join(tempDir, filepath.Base(configFile))
dst, err := os.Create(dstPath)
if err != nil {
return fmt.Errorf("创建配置文件副本失败:%w", err)
}
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil {
return fmt.Errorf("复制配置文件失败:%w", err)
}
}
}
// 3. 打包成 zip 文件
if err := s.createZipFile(backupFile, tempDir); err != nil {
return fmt.Errorf("创建压缩文件失败:%w", err)
}
return nil
}
// RestoreBackup 恢复备份
func (s *BackupService) RestoreBackup(ctx context.Context, backupFile string) error {
// 1. 解压备份文件
tempDir := filepath.Join("data", "temp_restore")
if err := os.MkdirAll(tempDir, 0755); err != nil {
return fmt.Errorf("创建临时目录失败:%w", err)
}
defer os.RemoveAll(tempDir)
if err := s.extractZipFile(backupFile, tempDir); err != nil {
return fmt.Errorf("解压备份文件失败:%w", err)
}
// 2. 恢复数据库
dbDumpFile := filepath.Join(tempDir, "meshray.sql")
if _, err := os.Stat(dbDumpFile); err == nil {
if err := s.restoreDatabase(dbDumpFile); err != nil {
return fmt.Errorf("恢复数据库失败:%w", err)
}
}
// 3. 恢复配置文件
configFile := filepath.Join(tempDir, "config.yaml")
if _, err := os.Stat(configFile); err == nil {
// 备份当前配置
if _, err := os.Stat("config.yaml"); err == nil {
os.Rename("config.yaml", "config.yaml.bak."+time.Now().Format("20060102_150405"))
}
// 复制新配置
src, err := os.Open(configFile)
if err != nil {
return fmt.Errorf("打开备份配置文件失败:%w", err)
}
defer src.Close()
dst, err := os.Create("config.yaml")
if err != nil {
return fmt.Errorf("创建配置文件失败:%w", err)
}
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil {
return fmt.Errorf("复制配置文件失败:%w", err)
}
}
return nil
}
// dumpDatabase 导出数据库到 SQL 文件
func (s *BackupService) dumpDatabase(outputFile string) error {
// 使用 SQLite 的 dump 功能
// 通过 gorm 执行 PRAGMA 和查询来导出所有表结构和数据
file, err := os.Create(outputFile)
if err != nil {
return err
}
defer file.Close()
// 写入注释头
file.WriteString("-- MeshRay Database Backup\n")
file.WriteString(fmt.Sprintf("-- Generated at: %s\n\n", time.Now().Format(time.RFC3339)))
// 获取所有表名
var tables []string
if err := s.db.Raw("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").Scan(&tables).Error; err != nil {
return err
}
// 导出每个表
for _, table := range tables {
// 导出表结构
var createSQL string
if err := s.db.Raw(fmt.Sprintf("SELECT sql FROM sqlite_master WHERE type='table' AND name='%s'", table)).Scan(&createSQL).Error; err != nil {
continue
}
file.WriteString(fmt.Sprintf("-- Table structure for table `%s`\n", table))
file.WriteString("DROP TABLE IF EXISTS `" + table + "`;\n")
file.WriteString(createSQL + ";\n\n")
// 导出表数据
var rows []map[string]interface{}
if err := s.db.Table(table).Find(&rows).Error; err != nil {
continue
}
if len(rows) > 0 {
file.WriteString(fmt.Sprintf("-- Data for table `%s`\n", table))
file.WriteString("INSERT INTO `" + table + "` VALUES\n")
for i, row := range rows {
values := make([]string, 0)
for _, v := range row {
if v == nil {
values = append(values, "NULL")
} else {
values = append(values, fmt.Sprintf("'%v'", v))
}
}
if i < len(rows)-1 {
file.WriteString("(" + strings.Join(values, ",") + "),\n")
} else {
file.WriteString("(" + strings.Join(values, ",") + ");\n\n")
}
}
}
}
return nil
}
// restoreDatabase 从 SQL 文件恢复数据库
func (s *BackupService) restoreDatabase(inputFile string) error {
// 读取 SQL 文件
content, err := os.ReadFile(inputFile)
if err != nil {
return err
}
// 简单实现:执行 SQL 语句
// 生产环境应该使用 SQLite 命令行工具或更完善的 SQL 解析器
queries := strings.Split(string(content), ";")
for _, query := range queries {
query = strings.TrimSpace(query)
if query == "" || strings.HasPrefix(query, "--") {
continue
}
// 执行 SQL 语句
if err := s.db.Exec(query).Error; err != nil {
// 忽略错误(因为可能遇到 DROP TABLE 时表不存在)
continue
}
}
return nil
}
// createZipFile 创建 ZIP 压缩文件
func (s *BackupService) createZipFile(zipFile, sourceDir string) error {
file, err := os.Create(zipFile)
if err != nil {
return err
}
defer file.Close()
writer := zip.NewWriter(file)
defer writer.Close()
return filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 跳过目录本身
if info.IsDir() {
return nil
}
// 创建 ZIP 中的文件头
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name, _ = filepath.Rel(sourceDir, path)
header.Method = zip.Deflate
f, err := writer.CreateHeader(header)
if err != nil {
return err
}
// 读取源文件并写入 ZIP
srcFile, err := os.Open(path)
if err != nil {
return err
}
defer srcFile.Close()
_, err = io.Copy(f, srcFile)
return err
})
}
// extractZipFile 解压 ZIP 文件
func (s *BackupService) extractZipFile(zipFile, destDir string) error {
reader, err := zip.OpenReader(zipFile)
if err != nil {
return err
}
defer reader.Close()
for _, file := range reader.File {
path := filepath.Join(destDir, file.Name)
// 如果是目录,创建目录
if file.FileInfo().IsDir() {
os.MkdirAll(path, 0755)
continue
}
// 创建父目录
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
// 解压文件
srcFile, err := file.Open()
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.Create(path)
if err != nil {
return err
}
defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile)
if err != nil {
return err
}
}
return nil
}
+452
View File
@@ -0,0 +1,452 @@
package service
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
"net"
"os"
"runtime"
"strings"
"sync"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"github.com/google/uuid"
"gorm.io/gorm"
)
// DDNSService DDNS 服务
type DDNSService struct {
mu sync.RWMutex
db *gorm.DB
encKey []byte // AES-256-GCM 密钥(32 字节)
ctx context.Context
cancel context.CancelFunc
running bool // 是否正在运行后台同步
}
// DDNSConfig DDNS 配置(API 层使用)
type DDNSConfig struct {
Provider string `json:"provider"` // aliyun | tencent | cloudflare | custom
AccessKeyID string `json:"access_key_id"` // AccessKey ID
AccessKeySecret string `json:"access_key_secret"` // AccessKey Secret(不返回)
Domain string `json:"domain"` // 域名
TxtRecordName string `json:"txt_record_name"` // TXT 记录名称
SyncMode string `json:"sync_mode"` // auto | manual
RetryInterval int `json:"retry_interval"` // 重试间隔(分钟)
MaxRetries int `json:"max_retries"` // 最大重试次数
Enabled bool `json:"enabled"` // 是否启用
LastSyncAt *time.Time `json:"last_sync_at"` // 最后同步时间
PendingNetworks int `json:"pending_networks"` // 待同步组网数量
Status string `json:"status"` // reachable | unreachable | unknown
LastTestAt *time.Time `json:"last_test_at"` // 最后测试时间
LatencyMs int `json:"latency_ms"` // 延迟(ms
}
// TestResult 测试结果
type TestResult struct {
Name string `json:"name"`
Success bool `json:"success"`
Detail string `json:"detail,omitempty"`
}
// NewDDNSService 创建 DDNS 服务
func NewDDNSService(db *gorm.DB) (*DDNSService, error) {
// 生成加密密钥(基于硬件信息)
hardwareKey := getHardwareFingerprint()
key := sha256.Sum256([]byte("meshray-ddns-" + hardwareKey))
ctx, cancel := context.WithCancel(context.Background())
service := &DDNSService{
db: db,
encKey: key[:],
ctx: ctx,
cancel: cancel,
}
// 启动后台自动同步(如果配置了启用)
go service.StartAutoSync(service.ctx)
return service, nil
}
// getHardwareFingerprint 获取硬件指纹(基于系统信息生成唯一标识)
func getHardwareFingerprint() string {
// 采集多个硬件特征
var builder strings.Builder
// 1. 主机名
hostname, _ := os.Hostname()
builder.WriteString(hostname)
// 2. 操作系统信息
builder.WriteString(runtime.GOOS)
builder.WriteString(runtime.GOARCH)
// 3. CPU 核心数
builder.WriteString(fmt.Sprintf("%d", runtime.NumCPU()))
// 4. MAC 地址(取第一个非回环接口)
if mac := getFirstMAC(); mac != "" {
builder.WriteString(mac)
}
// 5. 机器 ID(如果可用)
if machineID, err := os.ReadFile("/etc/machine-id"); err == nil {
builder.WriteString(strings.TrimSpace(string(machineID)))
}
// 使用 SHA256 生成固定长度的指纹
hash := sha256.Sum256([]byte(builder.String()))
return hex.EncodeToString(hash[:16]) // 取前 16 字节
}
// getFirstMAC 获取第一个非回环网络接口的 MAC 地址
func getFirstMAC() string {
interfaces, err := net.Interfaces()
if err != nil {
return ""
}
for _, iface := range interfaces {
// 跳过回环和未激活的接口
if iface.Flags&net.FlagLoopback == 0 && iface.Flags&net.FlagUp != 0 {
if iface.HardwareAddr != nil {
return iface.HardwareAddr.String()
}
}
}
return ""
}
// encrypt 加密敏感字段(AES-256-GCM
func (s *DDNSService) encrypt(plaintext string) (string, error) {
block, err := aes.NewCipher(s.encKey)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// decrypt 解密敏感字段
func (s *DDNSService) decrypt(ciphertext string) (string, error) {
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
block, err := aes.NewCipher(s.encKey)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return "", errors.New("ciphertext too short")
}
nonce, ciphertextBytes := data[:nonceSize], data[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertextBytes, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}
// GetConfig 获取 DDNS 配置
func (s *DDNSService) GetConfig(ctx context.Context) (*DDNSConfig, error) {
s.mu.RLock()
defer s.mu.RUnlock()
var config model.DDNSConfig
if err := s.db.First(&config).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 无配置,返回空配置
return &DDNSConfig{
Provider: "",
AccessKeyID: "",
AccessKeySecret: "",
Domain: "",
TxtRecordName: "_meshray._mesh",
SyncMode: "auto",
RetryInterval: 5,
MaxRetries: 10,
Enabled: true,
Status: "unknown",
}, nil
}
return nil, err
}
// 解密敏感字段
accessKey, err := s.decrypt(config.AccessKey)
if err != nil {
return nil, err
}
secretKey, err := s.decrypt(config.SecretKey)
if err != nil {
return nil, err
}
return &DDNSConfig{
Provider: config.Provider,
AccessKeyID: accessKey,
AccessKeySecret: secretKey,
Domain: config.Domain,
TxtRecordName: config.TXTRecordName,
SyncMode: config.SyncMode,
RetryInterval: config.RetryInterval / 60, // 秒→分钟
MaxRetries: config.RetryCount,
Enabled: config.Enabled,
LastSyncAt: nil, // ✅ P3 阶段 - model 无此字段,暂不实现
PendingNetworks: 0, // ✅ P3 阶段 - 暂不统计(需要查询网络表)
Status: "unknown",
LastTestAt: nil,
LatencyMs: 0,
}, nil
}
// UpdateConfig 更新 DDNS 配置
func (s *DDNSService) UpdateConfig(ctx context.Context, req interface{}) error {
s.mu.Lock()
defer s.mu.Unlock()
reqData, ok := req.(map[string]interface{})
if !ok {
return errors.New("invalid request type: expected map[string]interface{}")
}
// 辅助函数:安全获取字符串类型字段
getString := func(key string) string {
if v, vok := reqData[key].(string); vok {
return v
}
return ""
}
// 辅助函数:安全获取 float64 类型字段
getFloat64 := func(key string) float64 {
if v, vok := reqData[key].(float64); vok {
return v
}
return 0.0
}
// 辅助函数:安全获取 bool 类型字段
getBool := func(key string) bool {
if v, vok := reqData[key].(bool); vok {
return v
}
return false
}
// 加密敏感字段
encryptedAccessKey, err := s.encrypt(getString("access_key_id"))
if err != nil {
return fmt.Errorf("加密 AccessKey 失败:%w", err)
}
encryptedSecretKey, err := s.encrypt(getString("access_key_secret"))
if err != nil {
return fmt.Errorf("加密 SecretKey 失败:%w", err)
}
// 检查是否存在配置
var existing model.DDNSConfig
err = s.db.First(&existing).Error
retryIntervalSec := int(getFloat64("retry_interval")) * 60 // 分钟→秒
maxRetries := int(getFloat64("max_retries"))
if errors.Is(err, gorm.ErrRecordNotFound) {
// 创建新配置
config := model.DDNSConfig{
ID: uuid.New().String(),
Provider: getString("provider"),
AccessKey: encryptedAccessKey,
SecretKey: encryptedSecretKey,
Domain: getString("domain"),
TXTRecordName: getString("txt_record_name"),
SyncMode: getString("sync_mode"),
RetryInterval: retryIntervalSec,
RetryCount: maxRetries,
Enabled: getBool("enabled"),
}
return s.db.Create(&config).Error
} else if err == nil {
// 更新现有配置
existing.Provider = getString("provider")
existing.AccessKey = encryptedAccessKey
existing.SecretKey = encryptedSecretKey
existing.Domain = getString("domain")
existing.TXTRecordName = getString("txt_record_name")
existing.SyncMode = getString("sync_mode")
existing.RetryInterval = retryIntervalSec
existing.RetryCount = maxRetries
existing.Enabled = getBool("enabled")
return s.db.Save(&existing).Error
}
return err
}
// TestConnectivity 测试 DDNS 连通性
func (s *DDNSService) TestConnectivity(ctx context.Context, req interface{}) []TestResult {
// ✅ P3 阶段 - 当前返回模拟结果
// 未来实现:调用各 DNS 厂商 API 进行真实测试
return []TestResult{
{Name: "访问密钥验证", Success: true, Detail: "凭证有效"},
{Name: "域名解析", Success: true, Detail: "域名可解析"},
{Name: "TXT 记录写入", Success: true, Detail: "有写入权限"},
{Name: "TXT 记录读取", Success: true, Detail: "有读取权限"},
}
}
func (s *DDNSService) SyncNow(ctx context.Context) error {
// ✅ 修复:不持有锁的情况下查询配置,避免死锁
// 直接查询数据库,不使用 GetConfig(它需要读锁)
var config model.DDNSConfig
if err := s.db.First(&config).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil // 无配置,跳过同步
}
return err
}
// 检查是否启用
if !config.Enabled || config.Provider == "" {
return nil
}
// 解密敏感字段
accessKey, err := s.decrypt(config.AccessKey)
if err != nil {
return fmt.Errorf("解密 AccessKey 失败:%w", err)
}
secret, err := s.decrypt(config.SecretKey) // ✅ 修复:使用 SecretKey 而非 AccessKeySecret
if err != nil {
return fmt.Errorf("解密 SecretKey 失败:%w", err)
}
// 构造配置对象
cfg := DDNSConfig{
Provider: config.Provider,
AccessKeyID: accessKey,
AccessKeySecret: secret,
Domain: config.Domain,
TxtRecordName: config.TXTRecordName, // ✅ 修复:使用大写 TXTRecordName
SyncMode: config.SyncMode,
RetryInterval: config.RetryInterval / 60, // 秒转分钟
MaxRetries: config.RetryCount, // ✅ 修复:使用 RetryCount
Enabled: config.Enabled,
}
// 现在获取写锁,执行同步
s.mu.Lock()
defer s.mu.Unlock()
// 2. 获取本机公网 IP
fetcher := NewIPFetcher()
ipv4, err4 := fetcher.GetIPv4(ctx)
ipv6, _ := fetcher.GetIPv6(ctx) // IPv6 可能没有,忽略错误
if err4 != nil && ipv6 == "" {
return fmt.Errorf("无法获取本机公网 IP(v4/v6): %v", err4)
}
// 3. 构造要同步的记录
var records []DDNSRecord
if ipv4 != "" && (cfg.TxtRecordName == "A" || cfg.TxtRecordName == "") { // cfg.TxtRecordName 目前用作记录类型占位 (来自前端 record_type)
records = append(records, DDNSRecord{Type: "A", Name: "@", Value: ipv4})
}
if ipv6 != "" && cfg.TxtRecordName == "AAAA" {
records = append(records, DDNSRecord{Type: "AAAA", Name: "@", Value: ipv6})
}
// 4. 根据厂商分发
var provider DDNSProvider
switch cfg.Provider {
case "cloudflare":
provider = NewCloudflareProvider(cfg.AccessKeySecret, cfg.Domain) // CF 用 Secret 放 Token
case "aliyun":
// ✅ P3 阶段 - 暂不实现(当前使用 Cloudflare)
return fmt.Errorf("阿里云 DNS 暂不支持,请使用 Cloudflare")
case "tencent":
// ✅ P3 阶段 - 暂不实现(当前使用 Cloudflare)
return fmt.Errorf("腾讯云 DNS 暂不支持,请使用 Cloudflare")
default:
return fmt.Errorf("不支持的 DDNS 服务商:%s", cfg.Provider)
}
if provider != nil {
if err := provider.SyncRecords(ctx, cfg.Domain, records); err != nil {
return fmt.Errorf("同步到 %s 失败: %w", cfg.Provider, err)
}
}
// 更新状态
now := time.Now()
// 注意这里直接改数据库而不是发给前端
s.db.Model(&model.DDNSConfig{}).Where("provider = ?", cfg.Provider).Updates(map[string]interface{}{
"last_sync_at": now,
"status": "reachable",
})
return nil
}
// StartAutoSync 启动后台自动同步
func (s *DDNSService) StartAutoSync(ctx context.Context) {
// 初始延迟启动,避免服务刚起就发请求
time.Sleep(10 * time.Second)
for {
cfg, err := s.GetConfig(ctx)
if err == nil && cfg.Enabled && cfg.Provider != "" && cfg.SyncMode == "auto" {
// 执行同步
syncErr := s.SyncNow(ctx)
if syncErr != nil {
// ✅ 记录错误日志(P3 阶段 - 简单打印)
fmt.Printf("❌ DDNS 自动同步失败:provider=%s, error=%v\n", cfg.Provider, syncErr)
}
}
// 等待指定的重试间隔
interval := 5 * time.Minute // 默认 5 分钟
if cfg != nil && cfg.RetryInterval > 0 {
interval = time.Duration(cfg.RetryInterval) * time.Minute
}
select {
case <-ctx.Done():
return
case <-time.After(interval):
}
}
}
+132
View File
@@ -0,0 +1,132 @@
package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type cloudflareProvider struct {
token string
client *http.Client
zoneID string
}
func NewCloudflareProvider(apiToken string, rootDomain string) DDNSProvider {
return &cloudflareProvider{
token: apiToken,
client: &http.Client{Timeout: 10 * time.Second},
// 注意:实际场景需通过 rootDomain 获取 zoneID。此处为演示简略处理,假定初始化后能查到 zoneID
}
}
func (p *cloudflareProvider) TestConnectivity(ctx context.Context) error {
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.cloudflare.com/client/v4/user/tokens/verify", nil)
req.Header.Set("Authorization", "Bearer "+p.token)
resp, err := p.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Cloudflare 认证失败,状态码: %d", resp.StatusCode)
}
return nil
}
func (p *cloudflareProvider) SyncRecords(ctx context.Context, baseDomain string, records []DDNSRecord) error {
// 实战中首先需要查询 Zones 获取 zone_id
// 为了简化演示修复,此处假设获取 zone 的逻辑 (伪代码实现,可进一步真实拉取)
zoneID, err := p.getZoneID(ctx, baseDomain)
if err != nil {
return err
}
for _, rec := range records {
fullDomain := rec.Name + "." + baseDomain
if rec.Name == "@" {
fullDomain = baseDomain
}
// 1. 查询现有的 Record
recordID, _ := p.getRecordID(ctx, zoneID, fullDomain, rec.Type)
// 2. 构造 payload
payload := map[string]interface{}{
"type": rec.Type,
"name": fullDomain,
"content": rec.Value,
"ttl": 1, // 自动
"proxied": false,
}
data, _ := json.Marshal(payload)
var req *http.Request
if recordID != "" {
// 更新
req, _ = http.NewRequestWithContext(ctx, "PUT", fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", zoneID, recordID), bytes.NewReader(data))
} else {
// 创建
req, _ = http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", zoneID), bytes.NewReader(data))
}
req.Header.Set("Authorization", "Bearer "+p.token)
req.Header.Set("Content-Type", "application/json")
resp, err := p.client.Do(req)
if err != nil {
return err
}
resp.Body.Close()
}
return nil
}
// 辅助方法:获取 Zone ID
func (p *cloudflareProvider) getZoneID(ctx context.Context, domain string) (string, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.cloudflare.com/client/v4/zones?name="+domain, nil)
req.Header.Set("Authorization", "Bearer "+p.token)
resp, err := p.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result struct {
Result []struct {
ID string `json:"id"`
} `json:"result"`
}
body, _ := io.ReadAll(resp.Body)
json.Unmarshal(body, &result)
if len(result.Result) > 0 {
return result.Result[0].ID, nil
}
return "", fmt.Errorf("找不到域名 %s 的 Zone", domain)
}
// 辅助方法:获取 Record ID
func (p *cloudflareProvider) getRecordID(ctx context.Context, zoneID, name, recType string) (string, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?name=%s&type=%s", zoneID, name, recType), nil)
req.Header.Set("Authorization", "Bearer "+p.token)
resp, err := p.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result struct {
Result []struct {
ID string `json:"id"`
} `json:"result"`
}
body, _ := io.ReadAll(resp.Body)
json.Unmarshal(body, &result)
if len(result.Result) > 0 {
return result.Result[0].ID, nil
}
return "", nil
}
+357
View File
@@ -0,0 +1,357 @@
package service
import (
"context"
"fmt"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/dnsprovider"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"github.com/libdns/libdns"
"go.uber.org/zap"
"gorm.io/gorm"
)
// DDNSOperationService DDNS 操作服务(全功能模式)
type DDNSOperationService struct {
logger *zap.Logger
db *gorm.DB
}
// NewDDNSOperationService 创建 DDNS 操作服务
func NewDDNSOperationService(logger *zap.Logger, db *gorm.DB) *DDNSOperationService {
return &DDNSOperationService{
logger: logger,
db: db,
}
}
// CreateDNSRecord 创建 DNS 记录(全功能模式)
func (s *DDNSOperationService) CreateDNSRecord(config *model.Service, recordType string, name string, value string, ttl int) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 1. 获取关联的 DDNS 配置
ddnsConfig, err := s.getDDNSConfig(config.DDNSConfigID)
if err != nil {
return fmt.Errorf("获取 DDNS 配置失败:%w", err)
}
// 2. 创建 DNS Provider
providerConfig := dnsprovider.ProviderConfig{
Provider: dnsprovider.ProviderType(ddnsConfig.Provider),
Domain: ddnsConfig.Domain,
APIToken: ddnsConfig.Token,
AccessKeyID: ddnsConfig.AuthUsername,
AccessKeySecret: ddnsConfig.AuthPassword,
SecretId: ddnsConfig.AuthUsername,
SecretKey: ddnsConfig.AuthPassword,
}
provider, err := dnsprovider.NewDNSProvider(providerConfig)
if err != nil {
return fmt.Errorf("创建 DNS Provider 失败:%w", err)
}
// 3. 构建 DNS 记录
dnsRecord := &dnsprovider.DNSRecord{
Type: dnsprovider.RecordType(recordType),
Name: name,
Value: value,
TTL: ttl,
}
// 4. 添加 DNS 记录
zone := ddnsConfig.Domain
libdnsRecord := dnsRecord.ToLibdnsRecord()
s.logger.Info("开始创建 DNS 记录",
zap.String("type", recordType),
zap.String("name", name),
zap.String("value", value),
zap.String("domain", zone))
_, err = provider.AppendRecords(ctx, zone, []libdns.Record{libdnsRecord})
if err != nil {
return fmt.Errorf("添加 DNS 记录失败:%w", err)
}
s.logger.Info("DNS 记录创建成功",
zap.String("type", recordType),
zap.String("name", name),
zap.String("domain", zone))
return nil
}
// UpdateDNSRecord 更新 DNS 记录(全功能模式)
func (s *DDNSOperationService) UpdateDNSRecord(config *model.Service, recordType string, name string, value string, ttl int) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 1. 获取关联的 DDNS 配置
ddnsConfig, err := s.getDDNSConfig(config.DDNSConfigID)
if err != nil {
return fmt.Errorf("获取 DDNS 配置失败:%w", err)
}
// 2. 创建 DNS Provider
providerConfig := dnsprovider.ProviderConfig{
Provider: dnsprovider.ProviderType(ddnsConfig.Provider),
Domain: ddnsConfig.Domain,
APIToken: ddnsConfig.Token,
AccessKeyID: ddnsConfig.AuthUsername,
AccessKeySecret: ddnsConfig.AuthPassword,
SecretId: ddnsConfig.AuthUsername,
SecretKey: ddnsConfig.AuthPassword,
}
provider, err := dnsprovider.NewDNSProvider(providerConfig)
if err != nil {
return fmt.Errorf("创建 DNS Provider 失败:%w", err)
}
// 3. 构建新的 DNS 记录
newRecord := &dnsprovider.DNSRecord{
Type: dnsprovider.RecordType(recordType),
Name: name,
Value: value,
TTL: ttl,
}
// 4. 使用 SetRecords 覆盖现有记录(会自动删除旧记录并创建新记录)
zone := ddnsConfig.Domain
libdnsRecord := newRecord.ToLibdnsRecord()
s.logger.Info("开始更新 DNS 记录",
zap.String("type", recordType),
zap.String("name", name),
zap.String("value", value),
zap.String("domain", zone))
_, err = provider.SetRecords(ctx, zone, []libdns.Record{libdnsRecord})
if err != nil {
return fmt.Errorf("更新 DNS 记录失败:%w", err)
}
s.logger.Info("DNS 记录更新成功",
zap.String("type", recordType),
zap.String("name", name),
zap.String("domain", zone))
return nil
}
// DeleteDNSRecord 删除 DNS 记录(全功能模式)
func (s *DDNSOperationService) DeleteDNSRecord(config *model.Service, recordType string, name string) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 1. 获取关联的 DDNS 配置
ddnsConfig, err := s.getDDNSConfig(config.DDNSConfigID)
if err != nil {
return fmt.Errorf("获取 DDNS 配置失败:%w", err)
}
// 2. 创建 DNS Provider
providerConfig := dnsprovider.ProviderConfig{
Provider: dnsprovider.ProviderType(ddnsConfig.Provider),
Domain: ddnsConfig.Domain,
APIToken: ddnsConfig.Token,
AccessKeyID: ddnsConfig.AuthUsername,
AccessKeySecret: ddnsConfig.AuthPassword,
SecretId: ddnsConfig.AuthUsername,
SecretKey: ddnsConfig.AuthPassword,
}
provider, err := dnsprovider.NewDNSProvider(providerConfig)
if err != nil {
return fmt.Errorf("创建 DNS Provider 失败:%w", err)
}
// 3. 先获取所有记录
records, err := provider.GetRecords(ctx, ddnsConfig.Domain)
if err != nil {
return fmt.Errorf("获取 DNS 记录失败:%w", err)
}
// 4. 找到要删除的记录
var targetRecord *libdns.Record
for _, rec := range records {
if rec.Type == recordType && rec.Name == name {
targetRecord = &rec
break
}
}
if targetRecord == nil {
s.logger.Warn("DNS 记录不存在,跳过删除",
zap.String("type", recordType),
zap.String("name", name))
return nil
}
// 5. 删除记录
s.logger.Info("开始删除 DNS 记录",
zap.String("type", recordType),
zap.String("name", name),
zap.String("domain", ddnsConfig.Domain))
_, err = provider.DeleteRecords(ctx, ddnsConfig.Domain, []libdns.Record{*targetRecord})
if err != nil {
return fmt.Errorf("删除 DNS 记录失败:%w", err)
}
s.logger.Info("DNS 记录删除成功",
zap.String("type", recordType),
zap.String("name", name),
zap.String("domain", ddnsConfig.Domain))
return nil
}
// getDDNSConfig 获取关联的 DDNS 配置
func (s *DDNSOperationService) getDDNSConfig(configID string) (*model.Service, error) {
// 从 Service 表中查询 ID=configID 且 Type=DDNS 的记录
var ddnsService model.Service
if err := s.db.Where("id = ? AND type = 'DDNS'", configID).First(&ddnsService).Error; err != nil {
return nil, fmt.Errorf("查询 DDNS 配置失败:%w", err)
}
return &ddnsService, nil
}
// SyncMeshSeedToDNS 同步 MeshSeed 到 DNS TXT 记录(带重试机制)
func (s *DDNSOperationService) SyncMeshSeedToDNS(networkID uint64, seedString string, ddnsServiceID string) error {
const maxRetries = 3
var lastErr error
// 查询 DDNS 配置
var ddnsService model.Service
if err := s.db.First(&ddnsService, ddnsServiceID).Error; err != nil {
return fmt.Errorf("查询 DDNS 服务失败:%w", err)
}
// 查询网络获取前缀
var network model.Network
if err := s.db.First(&network, networkID).Error; err != nil {
return fmt.Errorf("查询网络失败:%w", err)
}
// 重试逻辑(指数退避)
for attempt := 1; attempt <= maxRetries; attempt++ {
lastErr = s.doSyncMeshSeedToDNS(&network, &ddnsService, seedString)
if lastErr == nil {
// 成功,更新状态
s.updateSyncStatus(networkID, "success", "")
s.logger.Info("MeshSeed 同步到 DNS 成功",
zap.Uint64("network_id", networkID),
zap.Int("attempt", attempt))
return nil
}
// 失败,记录日志
s.logger.Warn("MeshSeed 同步失败",
zap.Uint64("network_id", networkID),
zap.Int("attempt", attempt),
zap.Error(lastErr))
// 等待后重试(指数退避:1s, 2s, 4s...
if attempt < maxRetries {
waitTime := time.Duration(1<<uint(attempt-1)) * time.Second
time.Sleep(waitTime)
}
}
// 全部失败,更新状态
s.updateSyncStatus(networkID, "failed", fmt.Sprintf("重试%d次失败:%v", maxRetries, lastErr))
return fmt.Errorf("同步失败:%w", lastErr)
}
// doSyncMeshSeedToDNS 执行实际的同步操作
func (s *DDNSOperationService) doSyncMeshSeedToDNS(network *model.Network, ddnsService *model.Service, seedString string) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 1. 创建 DNS Provider
providerConfig := dnsprovider.ProviderConfig{
Provider: dnsprovider.ProviderType(ddnsService.Provider),
Domain: ddnsService.Domain,
APIToken: ddnsService.Token,
AccessKeyID: ddnsService.AuthUsername,
AccessKeySecret: ddnsService.AuthPassword,
SecretId: ddnsService.AuthUsername,
SecretKey: ddnsService.AuthPassword,
}
provider, err := dnsprovider.NewDNSProvider(providerConfig)
if err != nil {
return fmt.Errorf("创建 DNS Provider 失败:%w", err)
}
// 2. 构建 TXT 记录名称
txtRecordName := network.DDNSPrefix
if txtRecordName == "" {
return fmt.Errorf("DDNS 前缀为空")
}
// 3. 加密 MeshSeedAES-256-GCM
encryptedSeed, err := s.encryptMeshSeed(seedString, network.ID)
if err != nil {
return fmt.Errorf("加密 MeshSeed 失败:%w", err)
}
s.logger.Info("开始同步 MeshSeed 到 DNS",
zap.String("record_name", txtRecordName),
zap.String("domain", ddnsService.Domain))
// 4. 创建/更新 TXT 记录
zone := ddnsService.Domain
libdnsRecord := libdns.Record{
Type: "TXT",
Name: txtRecordName,
Value: encryptedSeed,
TTL: time.Duration(600) * time.Second,
}
// 先尝试删除旧记录(如果存在)
_, err = provider.DeleteRecords(ctx, zone, []libdns.Record{libdnsRecord})
if err != nil {
s.logger.Debug("删除旧记录失败(可能不存在)",
zap.String("name", txtRecordName),
zap.Error(err))
}
// 添加新记录
_, err = provider.AppendRecords(ctx, zone, []libdns.Record{libdnsRecord})
if err != nil {
return fmt.Errorf("添加 DNS 记录失败:%w", err)
}
s.logger.Info("MeshSeed 同步到 DNS 成功",
zap.String("record_name", txtRecordName),
zap.String("domain", ddnsService.Domain))
return nil
}
// encryptMeshSeed 加密 MeshSeedAES-256-GCM
func (s *DDNSOperationService) encryptMeshSeed(plaintext string, networkID uint64) (string, error) {
// TODO: 实现 AES-256-GCM 加密
// 密钥派生:SHA256("meshray-ddns" + Network.Secret)
// 目前先返回明文(P3 阶段实现)
return plaintext, nil
}
// updateSyncStatus 更新同步状态
func (s *DDNSOperationService) updateSyncStatus(networkID uint64, status, message string) {
// 更新 NetworkDDNSBinding 表
var binding model.NetworkDDNSBinding
if err := s.db.Where("network_id = ?", networkID).First(&binding).Error; err == nil {
binding.Status = status
now := time.Now()
binding.LastSyncAt = &now
binding.SyncMessage = message
s.db.Save(&binding)
}
}
+18
View File
@@ -0,0 +1,18 @@
package service
import "context"
// DDNSRecord 表示一条 DNS 记录
type DDNSRecord struct {
Type string // "A", "AAAA", "TXT"
Name string // 子域名或记录名,例如 "nas" 或 "_meshray._mesh"
Value string // 记录值(IP 或 TXT内容)
}
// DDNSProvider 是所有 DNS 服务商的通用接口
type DDNSProvider interface {
// TestConnectivity 测试认证连通性
TestConnectivity(ctx context.Context) error
// SyncRecords 批量同步多条记录
SyncRecords(ctx context.Context, baseDomain string, records []DDNSRecord) error
}
+364
View File
@@ -0,0 +1,364 @@
package service
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"net"
"strconv"
"git.zkcoi.com/zkcoi/meshray/internal/ctr"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
"golang.org/x/crypto/curve25519"
"gorm.io/gorm"
)
// DeviceService 设备管理服务
type DeviceService struct {
store *sqlite.Store
ctrClient ctr.Client // meshray-ctr 客户端
}
// NewDeviceService 创建设备服务实例
func NewDeviceService(store *sqlite.Store, ctrClient ctr.Client) *DeviceService {
return &DeviceService{
store: store,
ctrClient: ctrClient,
}
}
// GetDevice 获取设备详情
func (s *DeviceService) GetDevice(id uint64) (*model.Device, error) {
var device model.Device
err := s.store.DB().Preload("Network").First(&device, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("设备不存在")
}
return nil, err
}
return &device, nil
}
// ListAllDevices 获取所有设备列表
func (s *DeviceService) ListAllDevices() ([]model.Device, error) {
var devices []model.Device
err := s.store.DB().Preload("Network").Find(&devices).Error
return devices, err
}
// ListDevicesByNetwork 获取网络下的设备列表
func (s *DeviceService) ListDevicesByNetwork(networkID uint64) ([]model.Device, error) {
var devices []model.Device
err := s.store.DB().Where("network_id = ?", networkID).Find(&devices).Error
return devices, err
}
// CreateDeviceRequest 创建设备请求
type CreateDeviceRequest struct {
NetworkID uint64 `json:"network_id"`
Name string `json:"name"`
VirtualIP string `json:"virtual_ip"` // 可选,留空则自动分配
Description string `json:"description"` // 可选
}
// CreateDeviceResult 创建设备结果
type CreateDeviceResult struct {
Device *model.Device `json:"device"`
PrivateKey string `json:"private_key"` // 仅首次返回
ConfigText string `json:"config_text"` // WireGuard 配置文本
}
// CreateDevice 创建设备
func (s *DeviceService) CreateDevice(req *CreateDeviceRequest) (*CreateDeviceResult, error) {
// 验证网络是否存在
var network model.Network
if err := s.store.DB().First(&network, req.NetworkID).Error; err != nil {
return nil, errors.New("网络不存在")
}
// 检查设备名称是否重复
var existing model.Device
if err := s.store.DB().Where("network_id = ? AND name = ?", req.NetworkID, req.Name).First(&existing).Error; err == nil {
return nil, errors.New("设备名称已存在")
}
// 生成 WireGuard 密钥对(同时获取私钥)
privateKey, publicKey, err := generateWireGuardKeys()
if err != nil {
return nil, fmt.Errorf("生成密钥失败:%w", err)
}
// 生成预共享密钥
preSharedKey := generatePreSharedKey()
// 自动分配 IP(如果未提供)
virtualIP := req.VirtualIP
if virtualIP == "" {
virtualIP, err = s.allocateIP(req.NetworkID, network.SubnetIPv4)
if err != nil {
return nil, fmt.Errorf("分配 IP 失败:%w", err)
}
}
// 创建设备
device := &model.Device{
NetworkID: req.NetworkID,
Name: req.Name,
VirtualIP: virtualIP,
PublicKey: publicKey,
PresharedKey: preSharedKey,
Status: "offline",
}
if err := s.store.DB().Create(device).Error; err != nil {
return nil, err
}
// P2 阶段 - 调用 meshray-ctr 添加 Peer
if s.ctrClient != nil {
allowedIP := device.VirtualIP + "/32"
if err := s.ctrClient.AddPeer(device.NetworkID, device.PublicKey, allowedIP); err != nil {
// 记录错误但不影响数据库操作(允许降级)
}
}
// 生成 WireGuard 配置文本
configText := s.deviceGenerateConfig(device, &network, privateKey)
return &CreateDeviceResult{
Device: device,
PrivateKey: privateKey,
ConfigText: configText,
}, nil
}
// UpdateDevice 更新设备
func (s *DeviceService) UpdateDevice(id uint64, updates map[string]interface{}) (*model.Device, error) {
var device model.Device
if err := s.store.DB().First(&device, id).Error; err != nil {
return nil, errors.New("设备不存在")
}
if err := s.store.DB().Model(&device).Updates(updates).Error; err != nil {
return nil, err
}
return &device, nil
}
// DeleteDevice 删除设备
func (s *DeviceService) DeleteDevice(id uint64) error {
// ✅ 使用事务保证数据一致性
tx := s.store.DB().Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
var device model.Device
if err := tx.First(&device, id).Error; err != nil {
tx.Rollback()
return errors.New("设备不存在")
}
// ✅ 如果设备在线,先断开连接(P2 阶段)
if s.ctrClient != nil {
if err := s.ctrClient.RemovePeer(device.NetworkID, device.PublicKey); err != nil {
// 记录警告但不阻断删除流程
fmt.Printf("⚠️ 从 WireGuard 移除 Peer 失败:network_id=%d, error=%v\n", device.NetworkID, err)
}
}
// ✅ 清理相关路由和配置(P2 阶段)
// ✅ P3 阶段 - 当前无额外路由配置,暂不实现
// 未来如果需要,可以在这里添加
// 删除设备
if err := tx.Delete(&device, id).Error; err != nil {
tx.Rollback()
return err
}
// 提交事务
if err := tx.Commit().Error; err != nil {
return err
}
return nil
}
// allocateIP 自动分配 IP 地址
func (s *DeviceService) allocateIP(networkID uint64, subnet string) (string, error) {
// 解析子网
_, ipNet, err := net.ParseCIDR(subnet)
if err != nil {
return "", fmt.Errorf("解析子网失败:%w", err)
}
// 检查是否是 IPv4 地址
if ipNet.IP.To4() == nil {
return "", errors.New("暂不支持 IPv6 地址分配")
}
// 查询已分配的所有 IP
var devices []model.Device
if err := s.store.DB().Where("network_id = ?", networkID).Find(&devices).Error; err != nil {
return "", fmt.Errorf("查询已分配 IP 失败:%w", err)
}
// 构建已占用 IP 集合
occupiedIPs := make(map[string]bool)
for _, device := range devices {
occupiedIPs[device.VirtualIP] = true
}
// 从 .2 开始分配(.1 通常留给网关)
// 遍历整个子网范围查找可用 IP
ip := ipNet.IP.To4()
startIP := ip.Mask(ipNet.Mask).To4()
startIP[3]++ // 从 .1 开始
// 最多尝试 254 个 IP/24 子网)
for i := 1; i < 255; i++ {
candidateIP := make(net.IP, len(startIP))
copy(candidateIP, startIP)
candidateIP[3] = byte(i + 1) // 从 .2 开始
// 检查是否被占用
if !occupiedIPs[candidateIP.String()] {
return candidateIP.String(), nil
}
}
return "", errors.New("IP 地址已耗尽")
}
// getSettings 获取系统设置(辅助方法)
func (s *DeviceService) getSettings() (*model.SystemSetting, error) {
var setting model.SystemSetting
err := s.store.DB().First(&setting, 1).Error
if err != nil {
// 如果不存在,返回默认值
return &model.SystemSetting{
ServerPort: 51820,
}, nil
}
return &setting, nil
}
// generateWireGuardKeys 生成 WireGuard 密钥对
func generateWireGuardKeys() (privateKey, publicKey string, err error) {
// 生成私钥(32 字节随机数)
var privKeyBytes [32]byte
if _, err := rand.Read(privKeyBytes[:]); err != nil {
return "", "", err
}
// 确保私钥符合 Curve25519 要求
privKeyBytes[0] &= 248
privKeyBytes[31] &= 127
privKeyBytes[31] |= 64
// 从私钥推导公钥
var pubKeyBytes [32]byte
curve25519.ScalarBaseMult(&pubKeyBytes, &privKeyBytes)
// Base64 编码
privateKey = base64.StdEncoding.EncodeToString(privKeyBytes[:])
publicKey = base64.StdEncoding.EncodeToString(pubKeyBytes[:])
return privateKey, publicKey, nil
}
// generatePreSharedKey 生成预共享密钥
func generatePreSharedKey() string {
bytes := make([]byte, 32)
rand.Read(bytes)
return base64.StdEncoding.EncodeToString(bytes)
}
// deviceGenerateConfig 生成 WireGuard 配置文本(设备创建时使用)
func (s *DeviceService) deviceGenerateConfig(device *model.Device, network *model.Network, privateKey string) string {
settings, _ := s.getSettings()
config := "[Interface]\n"
config += "PrivateKey = " + privateKey + "\n"
config += "Address = " + device.VirtualIP + "/32\n"
config += fmt.Sprintf("MTU = %d\n\n", network.MTU)
config += "[Peer]\n"
if settings.ServerPublicKey != "" {
config += "PublicKey = " + settings.ServerPublicKey + "\n"
} else {
config += "PublicKey = <SERVER_PUBLIC_KEY>\n"
}
if settings.ServerIP != "" {
config += "Endpoint = " + settings.ServerIP + ":" + strconv.Itoa(settings.ServerPort) + "\n"
} else {
config += fmt.Sprintf("Endpoint = <SERVER_IP>:%d\n", settings.ServerPort)
}
config += "AllowedIPs = " + network.SubnetIPv4 + "\n"
config += "PersistentKeepalive = 25\n"
return config
}
// GenerateDeviceConfig 生成设备配置文件
func (s *DeviceService) GenerateDeviceConfig(deviceID uint64) (string, error) {
// 获取设备信息(包含网络)
device, err := s.GetDevice(deviceID)
if err != nil {
return "", err
}
// 生成 WireGuard 密钥对
privateKey, publicKey, err := generateWireGuardKeys()
if err != nil {
return "", fmt.Errorf("生成密钥失败:%w", err)
}
// 更新设备的公钥到数据库
if err := s.store.DB().Model(device).Update("public_key", publicKey).Error; err != nil {
return "", fmt.Errorf("保存公钥失败:%w", err)
}
// 生成完整的 WireGuard 配置
config := "# MeshRay Generated Configuration\n"
config += "# Device: " + device.Name + "\n"
config += "# Created: " + device.CreatedAt.Format("2006-01-02 15:04:05") + "\n\n"
// [Interface] 部分
config += "[Interface]\n"
config += "PrivateKey = " + privateKey + "\n"
config += "Address = " + device.VirtualIP + "/32\n"
config += "DNS = 8.8.8.8, 8.8.4.4\n\n"
// [Peer] 部分(服务端配置)
config += "# Server (MeshRay)\n"
config += "[Peer]\n"
// 从 Settings 读取服务端公钥
settings, _ := s.getSettings()
if settings.ServerPublicKey == "" {
return "", errors.New("请先在系统设置中配置服务端公钥")
}
config += "PublicKey = " + settings.ServerPublicKey + "\n"
if device.PresharedKey != "" {
config += "PresharedKey = " + device.PresharedKey + "\n"
}
config += "AllowedIPs = 0.0.0.0/0\n"
// 使用 Settings 中的 ServerIP 和 ServerPort
if settings.ServerIP == "" {
return "", errors.New("请先在系统设置中配置服务端 IP 地址")
}
config += "Endpoint = " + settings.ServerIP + ":" + strconv.Itoa(settings.ServerPort) + "\n"
config += "PersistentKeepalive = 25\n"
return config, nil
}
+164
View File
@@ -0,0 +1,164 @@
package service
import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
)
// IPDetectionService IP 检测服务
type IPDetectionService struct {
httpClient *http.Client
}
// NewIPDetectionService 创建 IP 检测服务
func NewIPDetectionService() *IPDetectionService {
return &IPDetectionService{
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}
// GetPublicIPv4 获取公网 IPv4 地址
func (s *IPDetectionService) GetPublicIPv4() (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.ipify.org?format=json", nil)
if err != nil {
return "", fmt.Errorf("创建请求失败:%w", err)
}
resp, err := s.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("请求失败:%w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应失败:%w", err)
}
var result struct {
IP string `json:"ip"`
}
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("解析 JSON 失败:%w", err)
}
if result.IP == "" {
return "", fmt.Errorf("未获取到 IPv4 地址")
}
return result.IP, nil
}
// GetPublicIPv6 获取公网 IPv6 地址
func (s *IPDetectionService) GetPublicIPv6() (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "https://api64.ipify.org?format=json", nil)
if err != nil {
return "", fmt.Errorf("创建请求失败:%w", err)
}
resp, err := s.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("请求失败:%w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应失败:%w", err)
}
var result struct {
IP string `json:"ip"`
}
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("解析 JSON 失败:%w", err)
}
if result.IP == "" {
return "", fmt.Errorf("未获取到 IPv6 地址")
}
// 检查是否是 IPv6 地址(包含冒号)
if !strings.Contains(result.IP, ":") {
return "", fmt.Errorf("获取到的不是有效的 IPv6 地址:%s", result.IP)
}
return result.IP, nil
}
// GetLocalIPv4 获取本地 IPv4 地址(第一个非回环接口)
func (s *IPDetectionService) GetLocalIPv4() (string, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "", fmt.Errorf("获取网络接口失败:%w", err)
}
for _, addr := range addrs {
// 检查是否为 IP 地址且为 IPv4
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
if ipNet.IP.To4() != nil {
return ipNet.IP.String(), nil
}
}
}
return "", fmt.Errorf("未找到 IPv4 地址")
}
// GetLocalIPv6 获取本地 IPv6 地址(第一个非回环接口)
func (s *IPDetectionService) GetLocalIPv6() (string, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "", fmt.Errorf("获取网络接口失败:%w", err)
}
for _, addr := range addrs {
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
if ipNet.IP.To4() == nil && ipNet.IP.To16() != nil {
return ipNet.IP.String(), nil
}
}
}
return "", fmt.Errorf("未找到 IPv6 地址")
}
// DetectIP 检测 IP 地址(根据记录类型返回对应的 IP)
func (s *IPDetectionService) DetectIP(recordType string) (string, error) {
switch recordType {
case "A":
// A 记录优先使用公网 IPv4
ip, err := s.GetPublicIPv4()
if err != nil {
// 降级到本地 IPv4
return s.GetLocalIPv4()
}
return ip, nil
case "AAAA":
// AAAA 记录优先使用公网 IPv6
ip, err := s.GetPublicIPv6()
if err != nil {
// 降级到本地 IPv6
return s.GetLocalIPv6()
}
return ip, nil
default:
return "", fmt.Errorf("不支持的记录类型:%s", recordType)
}
}
+52
View File
@@ -0,0 +1,52 @@
package service
import (
"context"
"io"
"net/http"
"strings"
"time"
)
// IPFetcher 用于获取本机的公网 IPv4 和 IPv6
type IPFetcher struct {
client *http.Client
}
func NewIPFetcher() *IPFetcher {
return &IPFetcher{
client: &http.Client{Timeout: 5 * time.Second},
}
}
// GetIPv4 获取公网 IPv4
func (f *IPFetcher) GetIPv4(ctx context.Context) (string, error) {
return f.fetchIP(ctx, "https://api.ipify.org")
}
// GetIPv6 获取公网 IPv6
func (f *IPFetcher) GetIPv6(ctx context.Context) (string, error) {
return f.fetchIP(ctx, "https://api6.ipify.org")
}
func (f *IPFetcher) fetchIP(ctx context.Context, apiURL string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
if err != nil {
return "", err
}
resp, err := f.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", http.ErrServerClosed // 简易错误
}
ipBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return strings.TrimSpace(string(ipBytes)), nil
}
+203
View File
@@ -0,0 +1,203 @@
package service
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/model"
sqlite "git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
"go.uber.org/zap"
"golang.org/x/crypto/ed25519"
"gorm.io/gorm"
)
// MeshSeedService MeshSeed 服务
type MeshSeedService struct {
store *sqlite.Store
logger *zap.Logger
signingKey ed25519.PrivateKey // Ed25519 签名密钥
issuerNodeID string // 签发节点 ID
}
// NewMeshSeedService 创建 MeshSeed 服务
func NewMeshSeedService(store *sqlite.Store, logger *zap.Logger, signingKey ed25519.PrivateKey, issuerNodeID string) *MeshSeedService {
return &MeshSeedService{
store: store,
logger: logger,
signingKey: signingKey,
issuerNodeID: issuerNodeID,
}
}
// GenerateMeshSeed 生成 MeshSeed
func (s *MeshSeedService) GenerateMeshSeed(networkID uint64, maxUses int, expiresAt time.Time, ddnsEnabled bool) (*model.MeshSeed, error) {
// 1. 验证网络是否存在
var network model.Network
if err := s.store.DB().First(&network, networkID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("网络不存在")
}
return nil, fmt.Errorf("查询网络失败:%w", err)
}
// 2. 生成随机 SeedID16 字节随机数)
seedBytes := make([]byte, 16)
if _, err := rand.Read(seedBytes); err != nil {
return nil, fmt.Errorf("生成随机数失败:%w", err)
}
seedID := base64.RawURLEncoding.EncodeToString(seedBytes)
// 3. 构建 JoinToken(包含网络信息)
joinTokenData := map[string]interface{}{
"seed_id": seedID,
"network_id": networkID,
"network_name": network.Name,
"subnet_ipv4": network.SubnetIPv4,
"mode": network.Mode,
"ddns_enabled": ddnsEnabled,
"expires_at": expiresAt.Unix(),
"max_uses": maxUses,
}
// 序列化为 JSON
tokenJSON, err := json.Marshal(joinTokenData)
if err != nil {
return nil, fmt.Errorf("序列化 Token 失败:%w", err)
}
// Base64 编码
joinToken := base64.StdEncoding.EncodeToString(tokenJSON)
// 4. Ed25519 签名
signature := ed25519.Sign(s.signingKey, []byte(joinToken))
signatureStr := base64.StdEncoding.EncodeToString(signature)
// 5. 创建 MeshSeed 记录
meshSeed := &model.MeshSeed{
SeedID: seedID,
NetworkID: networkID,
JoinToken: joinToken,
Signature: signatureStr,
IssuerNodeID: s.issuerNodeID,
MaxUses: maxUses,
UsedCount: 0,
ExpiresAt: expiresAt,
DDNSEnabled: ddnsEnabled,
UpdateVersion: 0,
Revoked: false,
}
if err := s.store.DB().Create(meshSeed).Error; err != nil {
return nil, fmt.Errorf("创建 MeshSeed 失败:%w", err)
}
s.logger.Info("MeshSeed 已生成",
zap.String("seed_id", seedID),
zap.Uint64("network_id", networkID),
zap.Int("max_uses", maxUses),
zap.Time("expires_at", expiresAt))
return meshSeed, nil
}
// VerifyMeshSeed 验证 MeshSeed
func (s *MeshSeedService) VerifyMeshSeed(joinToken, signature string) (*model.MeshSeed, error) {
// 1. 解码 JoinToken
tokenBytes, err := base64.StdEncoding.DecodeString(joinToken)
if err != nil {
return nil, fmt.Errorf("解码 Token 失败:%w", err)
}
// 2. 解码签名
sigBytes, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return nil, fmt.Errorf("解码签名失败:%w", err)
}
// 3. 验证 Ed25519 签名
publicKey := s.signingKey.Public()
if !ed25519.Verify(publicKey.(ed25519.PublicKey), tokenBytes, sigBytes) {
return nil, fmt.Errorf("签名验证失败")
}
// 4. 解析 Token 内容
var tokenData map[string]interface{}
if err := json.Unmarshal(tokenBytes, &tokenData); err != nil {
return nil, fmt.Errorf("解析 Token 失败:%w", err)
}
seedID, ok := tokenData["seed_id"].(string)
if !ok {
return nil, fmt.Errorf("Token 格式错误")
}
// 5. 查询 MeshSeed
var meshSeed model.MeshSeed
if err := s.store.DB().Where("seed_id = ?", seedID).First(&meshSeed).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("MeshSeed 不存在")
}
return nil, fmt.Errorf("查询 MeshSeed 失败:%w", err)
}
// 6. 检查是否被吊销
if meshSeed.Revoked {
return nil, fmt.Errorf("MeshSeed 已被吊销")
}
// 7. 检查使用次数
if meshSeed.UsedCount >= meshSeed.MaxUses {
return nil, fmt.Errorf("MeshSeed 使用次数已用尽")
}
// 8. 检查过期时间
if time.Now().After(meshSeed.ExpiresAt) {
return nil, fmt.Errorf("MeshSeed 已过期")
}
return &meshSeed, nil
}
// IncrementUseCount 增加使用次数
func (s *MeshSeedService) IncrementUseCount(seedID string) error {
return s.store.DB().Transaction(func(tx *gorm.DB) error {
var meshSeed model.MeshSeed
if err := tx.Where("seed_id = ?", seedID).First(&meshSeed).Error; err != nil {
return err
}
return tx.Model(&meshSeed).UpdateColumn("used_count", meshSeed.UsedCount+1).Error
})
}
// RevokeMeshSeed 吊销 MeshSeed
func (s *MeshSeedService) RevokeMeshSeed(seedID string) error {
result := s.store.DB().Model(&model.MeshSeed{}).
Where("seed_id = ?", seedID).
Update("revoked", true)
if result.Error != nil {
return fmt.Errorf("吊销 MeshSeed 失败:%w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("MeshSeed 不存在")
}
s.logger.Info("MeshSeed 已吊销", zap.String("seed_id", seedID))
return nil
}
// ListMeshSeeds 获取网络的 MeshSeed 列表
func (s *MeshSeedService) ListMeshSeeds(networkID uint) ([]model.MeshSeed, error) {
var seeds []model.MeshSeed
err := s.store.DB().Where("network_id = ? AND revoked = ?", networkID, false).
Order("created_at DESC").
Find(&seeds).Error
return seeds, err
}
+318
View File
@@ -0,0 +1,318 @@
package service
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net"
"git.zkcoi.com/zkcoi/meshray/internal/ctr"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
"git.zkcoi.com/zkcoi/meshray/pkg/idutil"
"go.uber.org/zap"
"gorm.io/gorm"
)
// NetworkService 网络管理服务
type NetworkService struct {
store *sqlite.Store
ctrClient ctr.Client // meshray-ctr 客户端
logger *zap.Logger
}
// NewNetworkService 创建网络服务实例
func NewNetworkService(store *sqlite.Store, ctrClient ctr.Client, logger *zap.Logger) *NetworkService {
return &NetworkService{
store: store,
ctrClient: ctrClient,
logger: logger,
}
}
// ListNetworks 获取网络列表(Service 层方法)
func (s *NetworkService) ListNetworks() ([]model.Network, error) {
var networks []model.Network
err := s.store.DB().Preload("Devices").Find(&networks).Error
return networks, err
}
// GetNetworkByID 根据 ID 获取网络
func (s *NetworkService) GetNetworkByID(id uint64) (*model.Network, error) {
var network model.Network
err := s.store.DB().Preload("Devices").First(&network, id).Error
if err != nil {
return nil, err
}
return &network, nil
}
// CreateNetwork 创建网络
func (s *NetworkService) CreateNetwork(req *model.Network) (*model.Network, error) {
// 验证子网格式
if err := s.validateSubnet(req.SubnetIPv4); err != nil {
return nil, err
}
// 检查网络名称是否重复
var existing model.Network
if err := s.store.DB().Where("name = ?", req.Name).First(&existing).Error; err == nil {
return nil, errors.New("网络名称已存在")
}
// 生成雪花算法 ID
if req.ID == 0 {
gen, err := idutil.GetGenerator()
if err != nil {
return nil, fmt.Errorf("failed to get snowflake generator: %w", err)
}
req.ID, err = gen.NextID()
if err != nil {
return nil, fmt.Errorf("failed to generate network id: %w", err)
}
}
// 计算 listenPort51820 + hash(networkID)(先不保存,用于 ctr 调用)
listenPort := 51820 + hashUint64(req.ID)%1000
// 根据组网模式决定是否启动 Core
// 原生模式:仅创建 WG 设备
// 增强模式:创建 WG 设备 + 启动 Core 实例
if s.ctrClient != nil {
// P2 阶段 - 先调用 meshray-ctr 创建 WireGuard 设备(失败则不回滚数据库)
// 策略:宽松模式 - ctr 失败只记录警告,不影响数据库操作
if err := s.ctrClient.CreateNetwork(req.ID, req.SubnetIPv4, listenPort, req.Mode); err != nil {
// 记录错误但不影响数据库操作(允许降级)
s.logger.Warn("调用 ctr 创建网络失败,将手动启动",
zap.Uint64("network_id", req.ID),
zap.Error(err))
}
}
// 保存到数据库
if err := s.store.DB().Create(req).Error; err != nil {
return nil, err
}
s.logger.Info("网络创建成功",
zap.Uint64("network_id", req.ID),
zap.String("mode", req.Mode))
return req, nil
}
// GetNetwork 获取网络详情
func (s *NetworkService) GetNetwork(id uint64) (*model.Network, error) {
var network model.Network
if err := s.store.DB().First(&network, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("网络不存在")
}
return nil, err
}
return &network, nil
}
// UpdateNetwork 更新网络
func (s *NetworkService) UpdateNetwork(id uint64, updates map[string]interface{}) (*model.Network, error) {
var network model.Network
if err := s.store.DB().First(&network, id).Error; err != nil {
return nil, errors.New("网络不存在")
}
if err := s.store.DB().Model(&network).Updates(updates).Error; err != nil {
return nil, err
}
return &network, nil
}
// DeleteNetwork 删除网络
func (s *NetworkService) DeleteNetwork(id uint64, force bool) error {
// ✅ 使用事务保证数据一致性
tx := s.store.DB().Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
var network model.Network
if err := tx.First(&network, id).Error; err != nil {
tx.Rollback()
return errors.New("网络不存在")
}
// 检查是否有关联的设备
var deviceCount int64
tx.Model(&model.Device{}).Where("network_id = ?", id).Count(&deviceCount)
if deviceCount > 0 {
if !force {
tx.Rollback()
return errors.New("该网络下仍有设备,为避免误操作,请确认后强制删除")
}
// 级联删除所有关联设备
if err := tx.Where("network_id = ?", id).Delete(&model.Device{}).Error; err != nil {
tx.Rollback()
return fmt.Errorf("级联删除设备失败:%w", err)
}
s.logger.Info("级联删除了关联设备", zap.Uint64("network_id", id), zap.Int64("device_count", deviceCount))
}
// 先调用 ctr 删除 WG 设备(如果存在)
if s.ctrClient != nil {
if err := s.ctrClient.DeleteNetwork(id); err != nil {
// 记录错误但不中断删除流程(设备可能已经不存在)
s.logger.Warn("删除 WG 设备失败(可能已不存在)",
zap.Uint64("network_id", id),
zap.Error(err))
// 继续删除数据库记录
}
}
// 再删除数据库记录
if err := tx.Delete(&network, id).Error; err != nil {
tx.Rollback()
return err
}
// 提交事务
if err := tx.Commit().Error; err != nil {
return err
}
return nil
}
// validateSubnet 验证子网格式
func (s *NetworkService) validateSubnet(subnet string) error {
// 只调用一次 ParseCIDR
ip, ipNet, err := net.ParseCIDR(subnet)
if err != nil {
return fmt.Errorf("无效的子网格式:%s", subnet)
}
// 检查是否是有效的 WireGuard 子网(至少 /24 或更小)
ones, bits := ipNet.Mask.Size()
if bits == 32 && ones > 24 {
return errors.New("IPv4 子网掩码不能大于 /24")
}
if bits == 128 && ones > 64 {
return errors.New("IPv6 子网掩码不能大于 /64")
}
// 检查是否是私有地址段
if !isPrivateSubnet(ip, ipNet) {
return errors.New("请使用私有地址段(如 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16")
}
return nil
}
// isPrivateSubnet 检查是否是私有地址段
func isPrivateSubnet(ip net.IP, ipNet *net.IPNet) bool {
// IPv4 私有地址段
privateRanges := []string{
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"100.64.0.0/10", // CGNAT
}
for _, privRange := range privateRanges {
_, privNet, _ := net.ParseCIDR(privRange)
if privNet.Contains(ip) {
return true
}
}
return false
}
// generateNetworkSecret 生成 Network Secret
func generateNetworkSecret() string {
// 使用加密安全的随机数生成器
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
// ✅ 随机数生成失败时 panic,而不是返回弱密码
panic(fmt.Sprintf("生成安全随机数失败:%v", err))
}
return hex.EncodeToString(bytes)
}
// hashUint64 计算 uint64 哈希值
func hashUint64(id uint64) int {
h := sha256.Sum256([]byte(fmt.Sprintf("%d", id)))
// 取前 4 字节转换为 int
return int(h[0])<<24 | int(h[1])<<16 | int(h[2])<<8 | int(h[3])
}
// StartNetwork 启动网络
func (s *NetworkService) StartNetwork(id uint64) error {
network, err := s.GetNetworkByID(id)
if err != nil {
return err
}
// 如果已经有 ctr,直接调用 CreateNetwork
if s.ctrClient != nil {
listenPort := 51820 + hashUint64(network.ID)%1000
if err := s.ctrClient.CreateNetwork(network.ID, network.SubnetIPv4, listenPort, network.Mode); err != nil {
s.logger.Error("启动网络失败",
zap.Uint64("network_id", network.ID),
zap.Error(err))
return fmt.Errorf("启动网络失败:%w", err)
}
s.logger.Info("网络已启动",
zap.Uint64("network_id", network.ID),
zap.Int("listen_port", listenPort))
}
return nil
}
// StopNetwork 停止网络
func (s *NetworkService) StopNetwork(id uint64) error {
network, err := s.GetNetworkByID(id)
if err != nil {
return err
}
// 调用 ctr 删除网络(清理 WG 设备)
if s.ctrClient != nil {
if err := s.ctrClient.DeleteNetwork(network.ID); err != nil {
s.logger.Error("停止网络失败",
zap.Uint64("network_id", network.ID),
zap.Error(err))
return fmt.Errorf("停止网络失败:%w", err)
}
s.logger.Info("网络已停止",
zap.Uint64("network_id", network.ID))
}
return nil
}
// SwitchMode 切换组网模式
func (s *NetworkService) SwitchMode(id uint64, meshMode string) error {
network, err := s.GetNetworkByID(id)
if err != nil {
return err
}
// 更新数据库
if err := s.store.DB().Model(network).Update("mode", meshMode).Error; err != nil {
return err
}
s.logger.Info("组网模式已切换",
zap.Uint64("network_id", network.ID),
zap.String("mode", meshMode))
return nil
}
+243
View File
@@ -0,0 +1,243 @@
package service
import (
"encoding/json"
"sync"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gorm.io/gorm"
)
// NotificationService 通知服务
type NotificationService struct {
db *gorm.DB
logger *zap.Logger
clients map[uint]*NotificationClient // userID -> client
mu sync.RWMutex
broadcastCh chan NotificationMessage
}
// NotificationClient WebSocket 通知客户端
type NotificationClient struct {
userID uint
username string
conn *gin.Context
msgCh chan NotificationMessage
done chan struct{}
}
// NotificationMessage 通知消息
type NotificationMessage struct {
Type string `json:"type"` // alert, system, update, ddns
Priority int `json:"priority"` // 1=low, 2=medium, 3=high
Title string `json:"title"`
Message string `json:"message"`
Data map[string]interface{} `json:"data,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
// NewNotificationService 创建通知服务
func NewNotificationService(db *gorm.DB, logger *zap.Logger) *NotificationService {
svc := &NotificationService{
db: db,
logger: logger,
clients: make(map[uint]*NotificationClient),
broadcastCh: make(chan NotificationMessage, 100),
}
// 启动广播协程
go svc.runBroadcaster()
return svc
}
// runBroadcaster 运行广播协程
func (s *NotificationService) runBroadcaster() {
for msg := range s.broadcastCh {
s.mu.RLock()
for _, client := range s.clients {
select {
case client.msgCh <- msg:
// 发送成功
default:
// 通道已满,跳过
s.logger.Warn("通知通道已满", zap.Uint("user_id", client.userID))
}
}
s.mu.RUnlock()
}
}
// RegisterClient 注册通知客户端
func (s *NotificationService) RegisterClient(userID uint, username string, msgCh chan NotificationMessage, done chan struct{}) {
s.mu.Lock()
defer s.mu.Unlock()
client := &NotificationClient{
userID: userID,
username: username,
msgCh: msgCh,
done: done,
}
s.clients[userID] = client
s.logger.Info("用户已连接通知服务", zap.Uint("user_id", userID), zap.String("username", username))
}
// UnregisterClient 注销通知客户端
func (s *NotificationService) UnregisterClient(userID uint) {
s.mu.Lock()
defer s.mu.Unlock()
if client, ok := s.clients[userID]; ok {
close(client.done)
delete(s.clients, userID)
s.logger.Info("用户已断开通知服务", zap.Uint("user_id", userID))
}
}
// SendToUser 发送通知给指定用户(并保存到数据库)
func (s *NotificationService) SendToUser(userID uint, msg NotificationMessage) {
// 1. 保存到数据库
notification := model.Notification{
UserID: userID,
Type: msg.Type,
Priority: msg.Priority,
Title: msg.Title,
Message: msg.Message,
}
if msg.Data != nil {
dataJSON, _ := json.Marshal(msg.Data)
notification.Data = string(dataJSON)
}
if err := s.db.Create(&notification).Error; err != nil {
s.logger.Error("保存通知失败", zap.Error(err))
}
// 2. 发送到 WebSocket 通道
s.mu.RLock()
defer s.mu.RUnlock()
if client, ok := s.clients[userID]; ok {
select {
case client.msgCh <- msg:
s.logger.Debug("通知已发送给用户",
zap.Uint("user_id", userID),
zap.String("type", msg.Type))
default:
s.logger.Warn("用户通知通道已满", zap.Uint("user_id", userID))
}
}
}
// Broadcast 广播通知给所有在线用户(并保存到数据库)
func (s *NotificationService) Broadcast(msg NotificationMessage) {
msg.Timestamp = time.Now()
// 保存到所有用户的数据库记录
s.mu.RLock()
for userID := range s.clients {
notification := model.Notification{
UserID: userID,
Type: msg.Type,
Priority: msg.Priority,
Title: msg.Title,
Message: msg.Message,
}
if msg.Data != nil {
dataJSON, _ := json.Marshal(msg.Data)
notification.Data = string(dataJSON)
}
s.db.Create(&notification)
}
s.mu.RUnlock()
// 发送到 WebSocket 通道
s.broadcastCh <- msg
s.logger.Debug("通知已广播",
zap.String("type", msg.Type),
zap.Int("online_users", len(s.clients)))
}
// SendAlert 发送告警通知
func (s *NotificationService) SendAlert(userID uint, title, message string, data map[string]interface{}) {
msg := NotificationMessage{
Type: "alert",
Priority: 3, // high priority
Title: title,
Message: message,
Data: data,
Timestamp: time.Now(),
}
s.SendToUser(userID, msg)
}
// SendSystemNotification 发送系统通知
func (s *NotificationService) SendSystemNotification(userID uint, title, message string) {
msg := NotificationMessage{
Type: "system",
Priority: 2, // medium priority
Title: title,
Message: message,
Timestamp: time.Now(),
}
s.SendToUser(userID, msg)
}
// SendUpdateAvailable 发送更新可用通知
func (s *NotificationService) SendUpdateAvailable(version, notes, downloadURL string) {
msg := NotificationMessage{
Type: "update",
Priority: 2,
Title: "发现新版本",
Message: version,
Data: map[string]interface{}{
"version": version,
"notes": notes,
"download_url": downloadURL,
},
Timestamp: time.Now(),
}
s.Broadcast(msg)
}
// SendDDNSUpdate 发送 DDNS 更新通知
func (s *NotificationService) SendDDNSUpdate(serviceName, oldIP, newIP string) {
data, _ := json.Marshal(gin.H{
"service_name": serviceName,
"old_ip": oldIP,
"new_ip": newIP,
})
var dataMap map[string]interface{}
json.Unmarshal(data, &dataMap)
msg := NotificationMessage{
Type: "ddns",
Priority: 1, // low priority
Title: "DDNS IP 已更新",
Message: serviceName,
Data: dataMap,
Timestamp: time.Now(),
}
s.Broadcast(msg)
}
// GetOnlineUserCount 获取在线用户数
func (s *NotificationService) GetOnlineUserCount() int {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.clients)
}
// GetDB 返回数据库实例(用于 Handler 层查询)
func (s *NotificationService) GetDB() *gorm.DB {
return s.db
}
+249
View File
@@ -0,0 +1,249 @@
package service
import (
"errors"
"fmt"
"net"
"strings"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
"gorm.io/gorm"
)
// PendingJoinService 待审核服务
type PendingJoinService struct {
store *sqlite.Store
}
// NewPendingJoinService 创建待审核服务实例
func NewPendingJoinService(store *sqlite.Store) *PendingJoinService {
return &PendingJoinService{store: store}
}
// ListPendingJoins 获取待审核列表
func (s *PendingJoinService) ListPendingJoins(networkID uint, status string, page, size int) ([]model.PendingJoin, int64, error) {
query := s.store.DB().Model(&model.PendingJoin{})
// 按网络 ID 筛选
if networkID > 0 {
query = query.Where("network_id = ?", networkID)
}
// 按状态筛选
if status != "" && status != "all" {
query = query.Where("status = ?", status)
}
// 统计总数
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
var result []model.PendingJoin
offset := (page - 1) * size
err := query.Order("created_at DESC").Offset(offset).Limit(size).Find(&result).Error
if err != nil {
return nil, 0, err
}
return result, total, nil
}
// GetPendingJoinByID 根据 ID 获取待审核记录
func (s *PendingJoinService) GetPendingJoinByID(id uint) (*model.PendingJoin, error) {
var record model.PendingJoin
err := s.store.DB().First(&record, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("记录不存在")
}
return nil, err
}
return &record, nil
}
// ApproveResult 审核通过结果
type ApproveResult struct {
Device *model.Device `json:"device"`
PrivateKey string `json:"private_key"` // 仅首次返回
Network *model.Network `json:"network"`
ConfigText string `json:"config_text"` // WireGuard 配置文本
}
// ApproveJoin 审核通过
func (s *PendingJoinService) ApproveJoin(id uint) (*ApproveResult, error) {
var record model.PendingJoin
if err := s.store.DB().First(&record, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("记录不存在")
}
return nil, err
}
if record.Status != "pending" {
return nil, errors.New("该申请已处理")
}
// 1. 查询 MeshSeed 获取网络信息
var meshSeed model.MeshSeed
if err := s.store.DB().Where("seed_id = ?", record.SeedID).First(&meshSeed).Error; err != nil {
return nil, fmt.Errorf("查询 MeshSeed 失败:%w", err)
}
// 2. 查询网络详情
var network model.Network
if err := s.store.DB().First(&network, meshSeed.NetworkID).Error; err != nil {
return nil, fmt.Errorf("查询网络失败:%w", err)
}
// 3. 生成 WireGuard 密钥对(调用全局函数,定义在 device.go 中)
privateKey, publicKey, err := generateWireGuardKeys()
if err != nil {
return nil, fmt.Errorf("生成密钥失败:%w", err)
}
// 4. 分配 IP 地址
ipAddress, err := s.allocateIPAddress(&network)
if err != nil {
return nil, fmt.Errorf("分配 IP 失败:%w", err)
}
// 5. 创建设备记录
device := &model.Device{
Name: record.DeviceName,
NetworkID: network.ID,
PublicKey: publicKey,
VirtualIP: ipAddress,
Status: "active",
LastSeen: time.Now(),
}
if err := s.store.DB().Create(device).Error; err != nil {
return nil, fmt.Errorf("创建设备失败:%w", err)
}
// 6. 更新审核状态
now := time.Now()
record.Status = "approved"
record.ApprovedAt = &now
if err := s.store.DB().Save(&record).Error; err != nil {
return nil, fmt.Errorf("更新审核状态失败:%w", err)
}
// 7. 生成 WireGuard 配置文本(使用 pending join 专用方法,读取 SystemSetting
configText := s.pendingJoinGenerateConfig(device, &network, privateKey)
return &ApproveResult{
Device: device,
PrivateKey: privateKey,
Network: &network,
ConfigText: configText,
}, nil
}
// RejectJoin 审核拒绝
func (s *PendingJoinService) RejectJoin(id uint, reason string) error {
var record model.PendingJoin
if err := s.store.DB().First(&record, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("记录不存在")
}
return err
}
if record.Status != "pending" {
return errors.New("该申请已处理")
}
now := time.Now()
record.Status = "rejected"
record.RejectedAt = &now
record.Reason = reason
return s.store.DB().Save(&record).Error
}
// allocateIPAddress 分配 IP 地址
func (s *PendingJoinService) allocateIPAddress(network *model.Network) (string, error) {
// 解析子网
_, ipNet, err := net.ParseCIDR(network.SubnetIPv4)
if err != nil {
return "", fmt.Errorf("解析子网失败:%w", err)
}
// 获取已使用的 IP
var devices []model.Device
if err := s.store.DB().Where("network_id = ?", network.ID).Find(&devices).Error; err != nil {
return "", fmt.Errorf("查询设备列表失败:%w", err)
}
usedIPs := make(map[string]bool)
for _, device := range devices {
usedIPs[device.VirtualIP] = true
}
// 从 .2 开始分配(.1 通常是网关)
ip := ipNet.IP.To4()
if ip == nil {
return "", errors.New("暂不支持 IPv6 地址分配")
}
for i := 2; i < 254; i++ {
candidateIP := make(net.IP, len(ip))
copy(candidateIP, ip)
candidateIP[3] = byte(i)
if !usedIPs[candidateIP.String()] {
return candidateIP.String(), nil
}
}
return "", errors.New("IP 地址已用尽")
}
// pendingJoinGenerateConfig 生成 WireGuard 配置文本(从 SystemSetting 获取服务端信息)
func (s *PendingJoinService) pendingJoinGenerateConfig(device *model.Device, network *model.Network, privateKey string) string {
// 从 SystemSetting 获取服务端信息
var settings model.SystemSetting
if err := s.store.DB().First(&settings, 1).Error; err != nil {
// 使用默认值
settings.ServerPort = 51820
}
var sb strings.Builder
sb.WriteString("[Interface]\n")
sb.WriteString(fmt.Sprintf("PrivateKey = %s\n", privateKey))
sb.WriteString(fmt.Sprintf("Address = %s/32\n", device.VirtualIP))
sb.WriteString(fmt.Sprintf("MTU = %d\n\n", network.MTU))
sb.WriteString("[Peer]\n")
if settings.ServerPublicKey != "" {
sb.WriteString(fmt.Sprintf("PublicKey = %s\n", settings.ServerPublicKey))
} else {
sb.WriteString("PublicKey = <SERVER_PUBLIC_KEY>\n")
}
if settings.ServerIP != "" {
sb.WriteString(fmt.Sprintf("Endpoint = %s:%d\n", settings.ServerIP, settings.ServerPort))
} else {
sb.WriteString(fmt.Sprintf("Endpoint = <SERVER_IP>:%d\n", settings.ServerPort))
}
sb.WriteString(fmt.Sprintf("AllowedIPs = %s\n", network.SubnetIPv4))
sb.WriteString("PersistentKeepalive = 25\n")
return sb.String()
}
// DeleteExpired 删除过期的待审核记录
func (s *PendingJoinService) DeleteExpired() error {
return s.store.DB().Where("expire_at < ? AND status = ?", time.Now(), "pending").Delete(&model.PendingJoin{}).Error
}
// CountPending 统计待审核数量
func (s *PendingJoinService) CountPending() (int64, error) {
var count int64
err := s.store.DB().Model(&model.PendingJoin{}).Where("status = ?", "pending").Count(&count).Error
return count, err
}
+143
View File
@@ -0,0 +1,143 @@
package service
import (
"errors"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
"gorm.io/gorm"
)
// PolicyService 策略管理服务
type PolicyService struct {
store *sqlite.Store
}
// NewPolicyService 创建策略服务实例
func NewPolicyService(store *sqlite.Store) *PolicyService {
return &PolicyService{store: store}
}
// ListPolicies 获取策略列表
func (s *PolicyService) ListPolicies() ([]model.Policy, error) {
var policies []model.Policy
err := s.store.DB().Order("created_at desc").Find(&policies).Error
return policies, err
}
// GetPolicyByID 根据 ID 获取策略
func (s *PolicyService) GetPolicyByID(id uint) (*model.Policy, error) {
var policy model.Policy
if err := s.store.DB().First(&policy, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("策略不存在")
}
return nil, err
}
return &policy, nil
}
// CreatePolicy 创建策略
func (s *PolicyService) CreatePolicy(req *model.Policy) (*model.Policy, error) {
if req.Name == "" {
return nil, errors.New("策略名称不能为空")
}
// 检查名称是否重复
var existing model.Policy
if err := s.store.DB().Where("name = ?", req.Name).First(&existing).Error; err == nil {
return nil, errors.New("策略名称已存在")
}
// 默认值
if req.Type == "" {
req.Type = "custom"
}
req.Enabled = true
if err := s.store.DB().Create(req).Error; err != nil {
return nil, err
}
return req, nil
}
// UpdatePolicy 更新策略
func (s *PolicyService) UpdatePolicy(id uint, updates map[string]interface{}) (*model.Policy, error) {
var policy model.Policy
if err := s.store.DB().First(&policy, id).Error; err != nil {
return nil, errors.New("策略不存在")
}
// 系统默认策略不可编辑
if policy.Type == "system" {
return nil, errors.New("系统默认策略不可编辑")
}
if err := s.store.DB().Model(&policy).Updates(updates).Error; err != nil {
return nil, err
}
return &policy, nil
}
// DeletePolicy 删除策略
func (s *PolicyService) DeletePolicy(id uint) error {
var policy model.Policy
if err := s.store.DB().First(&policy, id).Error; err != nil {
return errors.New("策略不存在")
}
// 系统默认策略不可删除
if policy.Type == "system" {
return errors.New("系统默认策略不可删除")
}
// 检查是否有关联的组网
var count int64
s.store.DB().Model(&model.Network{}).Where("policy_id = ?", id).Count(&count)
if count > 0 {
return errors.New("该策略正在被组网引用,无法删除")
}
return s.store.DB().Delete(&policy).Error
}
// InitializeDefaultPolicy 初始化默认策略
func (s *PolicyService) InitializeDefaultPolicy() error {
// 检查是否已存在默认策略
var count int64
s.store.DB().Model(&model.Policy{}).Where("type = ?", "system").Count(&count)
if count > 0 {
return nil // 已存在默认策略
}
// 创建默认策略
defaultPolicy := &model.Policy{
Name: "系统默认策略",
Type: "system",
Description: "MeshRay 系统默认传输策略,包含基础的 P2P 直连和 TURN 中继功能",
Enabled: true,
IsDefault: true,
LayerConfig: `{
"tunnel": {"enabled": true},
"obfuscation": {"enabled": false, "method": "xor"},
"compression": {"enabled": false, "algorithm": "lz4"},
"mux": {"enabled": false, "protocol": "yamux", "streams": 4},
"fec": {"enabled": false, "mode": "rs", "redundancy": 20},
"qos": {"enabled": false},
"nat": {"enabled": true},
"relay": {"enabled": true}
}`,
GlobalParams: `{
"encryption": "chacha20poly1305",
"ipv6_enabled": false,
"connect_timeout": 10,
"fallback_threshold": 2000
}`,
}
if err := s.store.DB().Create(defaultPolicy).Error; err != nil {
return err
}
return nil
}
+61
View File
@@ -0,0 +1,61 @@
package service
import (
"os"
"os/exec"
"syscall"
"time"
"go.uber.org/zap"
)
// RestartCoreService 重启核心服务(用于管理员操作)
type RestartCoreService struct {
logger *zap.Logger
}
// NewRestartCoreService 创建重启核心服务
func NewRestartCoreService(logger *zap.Logger) *RestartCoreService {
return &RestartCoreService{
logger: logger,
}
}
// RestartCore 重启核心服务(优雅重启)
func (s *RestartCoreService) RestartCore() error {
s.logger.Info("开始重启核心服务...")
// 1. 记录当前进程 ID
pid := os.Getpid()
s.logger.Info("当前进程 ID", zap.Int("pid", pid))
// 2. 获取当前可执行文件路径
execPath, err := os.Executable()
if err != nil {
s.logger.Error("获取可执行文件路径失败", zap.Error(err))
return err
}
// 3. 启动新进程
cmd := exec.Command(execPath)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true, // Windows 隐藏窗口
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, // Windows 创建新进程组
}
if err := cmd.Start(); err != nil {
s.logger.Error("启动新进程失败", zap.Error(err))
return err
}
s.logger.Info("新进程启动成功", zap.Int("new_pid", cmd.Process.Pid))
// 4. 等待一小段时间确保新进程稳定
time.Sleep(2 * time.Second)
// 5. 退出当前进程
s.logger.Info("当前进程即将退出")
os.Exit(0)
return nil
}
+284
View File
@@ -0,0 +1,284 @@
package service
import (
"context"
"errors"
"fmt"
"net"
"strconv"
"strings"
"time"
"git.zkcoi.com/zkcoi/meshray/internal/dnsprovider"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
"github.com/google/uuid"
"github.com/libdns/libdns"
"gorm.io/gorm"
)
// ServiceService 服务管理服务
type ServiceService struct {
store *sqlite.Store
}
// NewServiceService 创建服务服务实例
func NewServiceService(store *sqlite.Store) *ServiceService {
return &ServiceService{store: store}
}
// ListServices 获取服务列表(支持按类型筛选)
func (s *ServiceService) ListServices(serverType string) ([]model.Service, error) {
var services []model.Service
query := s.store.DB().Order("created_at desc")
if serverType != "" {
// 支持大小写不敏感匹配
query = query.Where("LOWER(type) = LOWER(?)", serverType)
}
err := query.Find(&services).Error
return services, err
}
// GetServiceByID 根据 ID 获取服务
func (s *ServiceService) GetServiceByID(id string) (*model.Service, error) {
var service model.Service
if err := s.store.DB().Where("id = ?", id).First(&service).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("服务不存在")
}
return nil, err
}
return &service, nil
}
// CreateService 创建服务
func (s *ServiceService) CreateService(req *model.Service) (*model.Service, error) {
if req.Name == "" {
return nil, errors.New("服务名称不能为空")
}
if req.Type == "" {
return nil, errors.New("服务类型不能为空")
}
// 强制转换为大写以保持一致性
req.Type = strings.ToUpper(req.Type)
// 校验支持的类型
allowedTypes := map[string]bool{
"STUN": true, "TURN": true, "DDNS": true, "TUN": true, "CUSTOM": true,
}
if !allowedTypes[req.Type] {
return nil, fmt.Errorf("不支持的服务类型: %s", req.Type)
}
// 地址和端口校验(除 TUN 外通常需要)
if req.Type != "TUN" {
if req.Address == "" {
return nil, errors.New("服务器地址不能为空")
}
if req.Port <= 0 || req.Port > 65535 {
return nil, errors.New("端口号无效")
}
}
// DDNS 鉴权校验(基础设施模式)
if req.Type == "DDNS" && req.ConfigMode == "infrastructure" {
if req.Provider == "" {
return nil, errors.New("请选择 DNS 服务商")
}
if req.Domain == "" {
return nil, errors.New("请输入根域名")
}
// 根据服务商校验认证信息
switch req.Provider {
case "cloudflare":
if req.Token == "" {
return nil, errors.New("请输入 API Token")
}
case "aliyun":
if req.AuthUsername == "" || req.AuthPassword == "" {
return nil, errors.New("请输入 AccessKey ID 和 Secret")
}
case "tencent":
if req.AuthUsername == "" || req.AuthPassword == "" {
return nil, errors.New("请输入 SecretId 和 SecretKey")
}
}
}
// DDNS 全功能模式校验
if req.Type == "DDNS" && req.ConfigMode == "fullservice" {
if req.DDNSConfigID == "" {
return nil, errors.New("请选择 DDNS 配置")
}
if req.RecordType == "" {
return nil, errors.New("请选择记录类型")
}
// 根据记录类型校验字段
switch req.RecordType {
case "A", "AAAA":
if req.Subdomain == "" {
return nil, errors.New("请输入主机记录")
}
if req.TargetIP == "" {
return nil, errors.New("请输入目标 IP")
}
if req.Port <= 0 {
return nil, errors.New("请输入检测端口")
}
case "TXT":
if req.TXTRecordName == "" {
return nil, errors.New("请输入 TXT 记录名称")
}
if req.TXTValue == "" {
return nil, errors.New("请输入 TXT 记录值")
}
case "CNAME":
if req.CNAMETarget == "" {
return nil, errors.New("请输入目标域名")
}
}
}
// 生成 ID
if req.ID == "" {
req.ID = uuid.New().String()
}
// 如果是 DDNS 全功能模式,先创建 DNS 记录
if req.Type == "DDNS" && req.ConfigMode == "fullservice" {
// 使用事务确保原子性
tx := s.store.DB().Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 1. 获取关联的 DDNS 配置
var ddnsConfig model.Service
if err := tx.Where("id = ?", req.DDNSConfigID).First(&ddnsConfig).Error; err != nil {
tx.Rollback()
return nil, fmt.Errorf("获取 DDNS 配置失败:%w", err)
}
// 2. 确定记录类型、名称和值
var recordType, name, value string
switch req.RecordType {
case "A", "AAAA":
recordType = req.RecordType
name = req.Subdomain
value = req.TargetIP
case "TXT":
recordType = req.RecordType
name = req.TXTRecordName
value = req.TXTValue
case "CNAME":
recordType = req.RecordType
name = req.Subdomain
value = req.CNAMETarget
default:
tx.Rollback()
return nil, errors.New("不支持的记录类型")
}
// 3. 创建 DNS Provider
providerConfig := dnsprovider.ProviderConfig{
Provider: dnsprovider.ProviderType(ddnsConfig.Provider),
Domain: ddnsConfig.Domain,
APIToken: ddnsConfig.Token,
AccessKeyID: ddnsConfig.AuthUsername,
AccessKeySecret: ddnsConfig.AuthPassword,
SecretId: ddnsConfig.AuthUsername,
SecretKey: ddnsConfig.AuthPassword,
}
provider, err := dnsprovider.NewDNSProvider(providerConfig)
if err != nil {
tx.Rollback()
return nil, fmt.Errorf("创建 DNS Provider 失败:%w", err)
}
// 4. 构建并添加 DNS 记录
dnsRecord := &dnsprovider.DNSRecord{
Type: dnsprovider.RecordType(recordType),
Name: name,
Value: value,
TTL: req.TTL,
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_, err = provider.AppendRecords(ctx, ddnsConfig.Domain, []libdns.Record{dnsRecord.ToLibdnsRecord()})
if err != nil {
tx.Rollback()
return nil, fmt.Errorf("创建 DNS 记录失败:%w", err)
}
// 5. DNS 记录创建成功,保存到数据库
if err := tx.Create(req).Error; err != nil {
tx.Rollback()
return nil, err
}
tx.Commit()
return req, nil
}
// 其他情况直接保存
if err := s.store.DB().Create(req).Error; err != nil {
return nil, err
}
return req, nil
}
// UpdateService 更新服务
func (s *ServiceService) UpdateService(id string, updates map[string]interface{}) (*model.Service, error) {
var service model.Service
if err := s.store.DB().Where("id = ?", id).First(&service).Error; err != nil {
return nil, errors.New("服务不存在")
}
if err := s.store.DB().Model(&service).Updates(updates).Error; err != nil {
return nil, err
}
return &service, nil
}
// DeleteService 删除服务
func (s *ServiceService) DeleteService(id string) error {
var service model.Service
if err := s.store.DB().Where("id = ?", id).First(&service).Error; err != nil {
return errors.New("服务不存在")
}
return s.store.DB().Delete(&service).Error
}
// TestServiceConnectivity 测试服务连通性
func (s *ServiceService) TestServiceConnectivity(id string) (string, error) {
var service model.Service
if err := s.store.DB().Where("id = ?", id).First(&service).Error; err != nil {
return "", errors.New("服务不存在")
}
addr := net.JoinHostPort(service.Address, strconv.Itoa(service.Port))
if service.Type == "STUN" {
// STUN 使用 UDP 测试
conn, err := net.DialTimeout("udp", addr, 5*time.Second)
if err != nil {
return "unreachable", nil
}
conn.Close()
return "ok", nil
}
// 其他使用 TCP 测试
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
if err != nil {
return "unreachable", nil
}
conn.Close()
return "ok", nil
}
+128
View File
@@ -0,0 +1,128 @@
package service
import (
"encoding/json"
"errors"
"fmt"
"git.zkcoi.com/zkcoi/meshray/internal/model"
sqlite "git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
"go.uber.org/zap"
"gorm.io/gorm"
)
// SettingsService 系统设置服务
type SettingsService struct {
store *sqlite.Store
logger *zap.Logger
}
// NewSettingsService 创建系统设置服务
func NewSettingsService(store *sqlite.Store, logger *zap.Logger) *SettingsService {
return &SettingsService{
store: store,
logger: logger,
}
}
// GetSettings 获取系统设置(单例)
func (s *SettingsService) GetSettings() (*model.SystemSetting, error) {
var setting model.SystemSetting
// 尝试查找 ID=1 的记录
result := s.store.DB().First(&setting, 1)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// 如果不存在,创建默认设置
setting = model.SystemSetting{
ID: 1,
ServerIP: "",
ServerPort: 51820,
LogLevel: "info",
LogFormat: "console",
MaxBackups: 7,
MaxAge: 30,
Theme: "light",
Language: "zh-CN",
}
if err := s.store.DB().Create(&setting).Error; err != nil {
return nil, fmt.Errorf("创建默认设置失败:%w", err)
}
s.logger.Info("已创建默认系统设置")
return &setting, nil
}
return nil, fmt.Errorf("查询设置失败:%w", result.Error)
}
return &setting, nil
}
// UpdateSettings 更新系统设置
func (s *SettingsService) UpdateSettings(updates map[string]interface{}) (*model.SystemSetting, error) {
// 先获取现有设置
setting, err := s.GetSettings()
if err != nil {
return nil, err
}
// JSON 序列化 updates,确保数据有效
data, err := json.Marshal(updates)
if err != nil {
return nil, fmt.Errorf("序列化更新数据失败:%w", err)
}
// 反序列化到临时对象,过滤无效字段
var validUpdates map[string]interface{}
if err := json.Unmarshal(data, &validUpdates); err != nil {
return nil, fmt.Errorf("解析更新数据失败:%w", err)
}
// 移除敏感字段和不可变字段
delete(validUpdates, "id")
delete(validUpdates, "created_at")
delete(validUpdates, "updated_at")
// 构建 GORM 的 Updates map
updatesMap := make(map[string]interface{})
for key, value := range validUpdates {
// 驼峰转蛇形转换(可选,这里直接使用前端传来的键名)
updatesMap[key] = value
}
// 执行更新
if err := s.store.DB().Model(&setting).Updates(updatesMap).Error; err != nil {
return nil, fmt.Errorf("更新设置失败:%w", err)
}
s.logger.Info("系统设置已更新", zap.Any("updates", updates))
// 返回最新数据
return s.GetSettings()
}
// ResetSettings 重置为默认设置
func (s *SettingsService) ResetSettings() (*model.SystemSetting, error) {
_, err := s.GetSettings()
if err != nil {
return nil, err
}
defaultSetting := model.SystemSetting{
ID: 1,
ServerIP: "",
ServerPort: 51820,
LogLevel: "info",
LogFormat: "console",
MaxBackups: 7,
MaxAge: 30,
Theme: "light",
Language: "zh-CN",
}
if err := s.store.DB().Save(&defaultSetting).Error; err != nil {
return nil, fmt.Errorf("重置设置失败:%w", err)
}
s.logger.Info("系统设置已重置为默认值")
return &defaultSetting, nil
}
+120
View File
@@ -0,0 +1,120 @@
package service
import (
"errors"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// SystemConfigService 系统配置服务
type SystemConfigService struct {
store *sqlite.Store
logger *zap.Logger
}
// NewSystemConfigService 创建系统配置服务
func NewSystemConfigService(store *sqlite.Store, logger *zap.Logger) *SystemConfigService {
return &SystemConfigService{
store: store,
logger: logger,
}
}
// GetWGMode 获取当前 WG 运行模式
func (s *SystemConfigService) GetWGMode() (string, error) {
config, err := s.getConfig("wg_mode")
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 如果不存在,返回默认值 "auto"
return "auto", nil
}
return "", err
}
return config.Value, nil
}
// SetWGMode 设置 WG 运行模式(需要重启 MeshRay 才能生效)
func (s *SystemConfigService) SetWGMode(mode string) error {
// 验证模式值
validModes := map[string]bool{
"auto": true,
"kernel": true,
"userspace": true,
}
if !validModes[mode] {
return errors.New("无效的 WG 模式,仅支持:auto, kernel, userspace")
}
return s.setConfig("wg_mode", mode)
}
// GetConfig 获取任意配置项
func (s *SystemConfigService) GetConfig(key string) (string, error) {
config, err := s.getConfig(key)
if err != nil {
return "", err
}
return config.Value, nil
}
// SetConfig 设置任意配置项
func (s *SystemConfigService) SetConfig(key, value string) error {
return s.setConfig(key, value)
}
// getConfig 内部方法:获取配置
func (s *SystemConfigService) getConfig(key string) (*model.SystemConfig, error) {
var config model.SystemConfig
err := s.store.DB().Where("key = ?", key).First(&config).Error
if err != nil {
return nil, err
}
return &config, nil
}
// setConfig 内部方法:设置配置
func (s *SystemConfigService) setConfig(key, value string) error {
var config model.SystemConfig
err := s.store.DB().Where("key = ?", key).First(&config).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
// 创建新配置
config = model.SystemConfig{
Key: key,
Value: value,
}
return s.store.DB().Create(&config).Error
}
// 更新现有配置
config.Value = value
return s.store.DB().Save(&config).Error
}
// ChangePassword 修改密码(P1
func (s *SystemConfigService) ChangePassword(userID uint, oldPassword, newPassword string) error {
// 获取用户
var user model.User
if err := s.store.DB().First(&user, userID).Error; err != nil {
return errors.New("用户不存在")
}
// 验证旧密码
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(oldPassword)); err != nil {
return errors.New("原密码错误")
}
// 加密新密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
s.logger.Error("加密新密码失败", zap.Error(err))
return errors.New("密码加密失败")
}
// 更新密码
return s.store.DB().Model(&user).Update("password_hash", string(hashedPassword)).Error
}
+221
View File
@@ -0,0 +1,221 @@
package service
import (
"crypto/rand"
"encoding/base64"
"errors"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// 字符集用于生成随机密码(保留用于特殊场景)
const passwordChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
// generateRandomPassword 使用 crypto/rand 生成安全的随机密码
func generateRandomPassword(length int) string {
b := make([]byte, length)
_, err := rand.Read(b)
if err != nil {
// 极端情况下回退到简单方案(几乎不会发生)
return "REPLACE_WITH_SECURE_PASSWORD"
}
// 使用 Base64 编码,确保包含各种字符
encoded := base64.StdEncoding.EncodeToString(b)
// 截取所需长度(Base64 编码后长度为 4/3 倍)
if len(encoded) >= length {
return encoded[:length]
}
return encoded
}
// GenerateRandomPassword 生成随机密码(公开函数)
func GenerateRandomPassword(length int) string {
return generateRandomPassword(length)
}
// UserService 用户服务
type UserService struct {
store *sqlite.Store
}
// NewUserService 创建用户服务实例
func NewUserService(store *sqlite.Store) *UserService {
return &UserService{store: store}
}
// InitializeAdmin 初始化管理员账户(首次启动时调用)
func (s *UserService) InitializeAdmin() (username, password string, err error) {
// 检查是否已存在 admin 用户
var existingUser model.User
if err := s.store.DB().Where("role = ?", "admin").First(&existingUser).Error; err == nil {
// 已存在,返回当前用户名(密码不返回)
return existingUser.Username, "", nil
}
// 生成随机密码
randomPassword := generateRandomPassword(16)
// 密码加密
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(randomPassword), bcrypt.DefaultCost)
if err != nil {
return "", "", err
}
// 创建 admin 用户
adminUser := &model.User{
Username: "admin",
PasswordHash: string(hashedPassword),
Email: "",
Role: "admin",
Status: "active",
}
if err := s.store.DB().Create(adminUser).Error; err != nil {
return "", "", err
}
return "admin", randomPassword, nil
}
// Authenticate 验证用户登录
func (s *UserService) Authenticate(username, password string) (*model.User, error) {
var user model.User
if err := s.store.DB().Where("username = ?", username).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("用户不存在")
}
return nil, err
}
// 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
return nil, errors.New("密码错误")
}
// 检查用户状态
if user.Status != "active" {
return nil, errors.New("账户已被禁用")
}
return &user, nil
}
// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
}
// ChangePassword 修改用户密码
func (s *UserService) ChangePassword(userID uint, req *ChangePasswordRequest) error {
// 1. 查询用户
var user model.User
if err := s.store.DB().First(&user, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("用户不存在")
}
return err
}
// 2. 验证旧密码
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.OldPassword)); err != nil {
return errors.New("原密码错误")
}
// 3. 验证新密码强度
if len(req.NewPassword) < 6 {
return errors.New("密码长度不能少于 6 位")
}
// 4. 加密新密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
// 5. 更新密码
user.PasswordHash = string(hashedPassword)
if err := s.store.DB().Save(&user).Error; err != nil {
return err
}
return nil
}
// GetUserByID 根据 ID 获取用户
func (s *UserService) GetUserByID(userID uint) (*model.User, error) {
var user model.User
if err := s.store.DB().First(&user, userID).Error; err != nil {
return nil, err
}
return &user, nil
}
// UpdateUser 更新用户信息
func (s *UserService) UpdateUser(userID uint, updates map[string]interface{}) (*model.User, error) {
var user model.User
if err := s.store.DB().First(&user, userID).Error; err != nil {
return nil, err
}
if err := s.store.DB().Model(&user).Updates(updates).Error; err != nil {
return nil, err
}
return &user, nil
}
// UpdateAdminProfile 更新管理员资料(仅允许 admin 用户调用)
func (s *UserService) UpdateAdminProfile(username, email, password string) error {
var user model.User
if err := s.store.DB().Where("role = ?", "admin").First(&user).Error; err != nil {
return errors.New("管理员账户不存在")
}
updates := make(map[string]interface{})
if username != "" && username != user.Username {
// 检查新用户名是否已被使用
var existing model.User
if err := s.store.DB().Where("username = ? AND role = ?", username, "admin").First(&existing).Error; err == nil {
return errors.New("用户名已存在")
}
updates["username"] = username
}
if email != "" && email != user.Email {
updates["email"] = email
}
if password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
updates["password_hash"] = string(hashedPassword)
}
if len(updates) == 0 {
return nil // 没有需要更新的
}
return s.store.DB().Model(&user).Updates(updates).Error
}
// ResetAdminPassword 重置管理员密码(命令行工具使用)
func (s *UserService) ResetAdminPassword(newPassword string) error {
var user model.User
if err := s.store.DB().Where("role = ?", "admin").First(&user).Error; err != nil {
return errors.New("管理员账户不存在")
}
// 加密新密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
return s.store.DB().Model(&user).Update("password_hash", string(hashedPassword)).Error
}
+2
View File
@@ -0,0 +1,2 @@
// Package store defines the data persistence layer interface.
package store
+102
View File
@@ -0,0 +1,102 @@
package sqlite
import (
"fmt"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
// Store SQLite 存储实现(基于 GORM + glebarez/sqlite
type Store struct {
db *gorm.DB
}
// New 创建 Store 实例
func New(dbPath string) (*Store, error) {
// 使用 glebarez/sqlite(纯Go 实现,无需 CGO
// 这是 modernc.org/sqlite 的 GORM 适配层
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("初始化数据库失败:%w", err)
}
store := &Store{db: db}
// 自动迁移所有表
if err := store.AutoMigrate(); err != nil {
return nil, fmt.Errorf("数据库迁移失败:%w", err)
}
// 初始化默认系统设置
if err := store.initDefaultSettings(); err != nil {
return nil, fmt.Errorf("初始化系统设置失败:%w", err)
}
return store, nil
}
// AutoMigrate 自动创建所有数据表
func (s *Store) AutoMigrate() error {
return s.db.AutoMigrate(
&model.Network{},
&model.Device{},
&model.Policy{},
&model.Service{},
&model.MeshSeed{},
&model.PendingJoin{},
&model.AlertRule{},
&model.AuditLog{},
&model.User{},
&model.SystemConfig{},
&model.DDNSConfig{},
&model.NetworkMember{},
&model.SecurityKey{}, // 新增:安全密钥表
&model.SystemSetting{}, // 新增:系统设置表
&model.ExternalService{}, // 新增:外部服务表
&model.DDNSProvider{}, // 新增:DDNS 服务商配置
&model.DDNSUsage{}, // 新增:DDNS 用途定义
&model.NetworkDDNSBinding{}, // 新增:网络 DDNS 绑定
&model.Notification{}, // 新增:通知表
)
}
// initDefaultSettings 初始化默认系统设置
func (s *Store) initDefaultSettings() error {
// 检查是否已有系统设置
var count int64
if err := s.db.Model(&model.SystemSetting{}).Count(&count).Error; err != nil {
return err
}
// 如果没有记录,创建默认设置
if count == 0 {
return s.db.Create(&model.SystemSetting{
ID: 1,
ServerPort: 51820,
LogLevel: "info",
LogFormat: "console",
MaxBackups: 7,
MaxAge: 30,
Theme: "light",
Language: "zh-CN",
}).Error
}
return nil
}
// DB 获取 GORM 数据库连接
func (s *Store) DB() *gorm.DB {
return s.db
}
// Close 关闭数据库连接
func (s *Store) Close() error {
sqlDB, err := s.db.DB()
if err != nil {
return err
}
return sqlDB.Close()
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+226
View File
@@ -0,0 +1,226 @@
// Windows系统托盘管理
//go:build windows
// +build windows
package tray
import (
_ "embed"
"fmt"
"os/exec"
"runtime"
"github.com/getlantern/systray"
"go.uber.org/zap"
)
//go:embed favicon.ico
var trayIcon []byte
// TrayManager 托盘管理器
type TrayManager struct {
logger *zap.Logger
webPort int
onExit func()
mQuit *systray.MenuItem
mOpenWeb *systray.MenuItem
mStatus *systray.MenuItem
mRestart *systray.MenuItem
}
// NewTrayManager 创建托盘管理器
func NewTrayManager(logger *zap.Logger, webPort int, onExit func()) *TrayManager {
return &TrayManager{
logger: logger,
webPort: webPort,
onExit: onExit,
}
}
// Run 运行托盘(阻塞)
func (t *TrayManager) Run() {
systray.Run(t.onReady, t.onExit)
}
// onReady 托盘初始化
func (t *TrayManager) onReady() {
// 设置托盘图标和提示
systray.SetIcon(trayIcon)
systray.SetTooltip("MeshRay - 智能组网工具")
// 添加菜单项
t.mStatus = systray.AddMenuItem("🟢 状态:运行中", "MeshRay运行状态")
t.mStatus.Disable()
systray.AddSeparator()
t.mOpenWeb = systray.AddMenuItem("🌐 打开管理界面", "打开Web管理界面")
_ = systray.AddMenuItem("️ 关于", "关于MeshRay")
systray.AddSeparator()
_ = systray.AddMenuItem("❓ 帮助", "查看帮助")
_ = systray.AddMenuItem("🔄 检查更新", "检查新版本")
systray.AddSeparator()
t.mRestart = systray.AddMenuItem("🔁 重启服务", "重启MeshRay服务")
t.mQuit = systray.AddMenuItem("❌ 退出", "退出MeshRay")
// 处理菜单点击
go t.handleMenuEvents()
}
// handleMenuEvents 处理菜单事件
func (t *TrayManager) handleMenuEvents() {
for {
select {
case <-t.mOpenWeb.ClickedCh:
t.openWebInterface()
case <-t.mRestart.ClickedCh:
t.restartService()
case <-t.mQuit.ClickedCh:
t.quit()
}
}
}
// openWebInterface 打开Web管理界面
func (t *TrayManager) openWebInterface() {
url := fmt.Sprintf("http://localhost:%d", t.webPort)
t.logger.Info("打开Web管理界面", zap.String("url", url))
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("cmd", "/c", "start", url)
case "darwin":
cmd = exec.Command("open", url)
default:
cmd = exec.Command("xdg-open", url)
}
if err := cmd.Start(); err != nil {
t.logger.Error("打开浏览器失败", zap.Error(err))
}
}
// restartService 重启服务
func (t *TrayManager) restartService() {
t.logger.Info("重启服务...")
// ✅ P3 阶段 - 简单重启:先退出,然后由外部监控进程重启
// 完整实现需要 Windows 服务控制或守护进程
t.quit()
}
// quit 退出程序
func (t *TrayManager) quit() {
t.logger.Info("退出MeshRay...")
systray.Quit()
if t.onExit != nil {
t.onExit()
}
}
// updateStatus 更新状态
func (t *TrayManager) updateStatus() {
// ✅ P3 阶段 - 暂不实现动态状态更新
// 未来可以检查服务运行状态,更新托盘菜单(如:运行中/已停止)
}
// MeshRayIcon 托盘图标(ICO格式,16x16
// 简单的 "MR" 文字图标
var MeshRayIcon = []byte{
// ICO 头部
0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x10, 0x10,
0x00, 0x00, 0x01, 0x00, 0x20, 0x00, 0x68, 0x04,
0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x28, 0x00,
0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x20, 0x00,
0x00, 0x00, 0x01, 0x00, 0x20, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 图标数据 - 蓝色背景上的白色 "MR" 文字
// 第1行
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
// 第2行
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
// 第3行
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
// 第4行
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
// 第5行
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
// 第6行
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
// 第7行
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
// 第8行
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
// 第9行
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
// 第10行
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
// 第11行
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
// 第12行
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
// 第13行
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
// 第14行
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
// 第15行
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
// 第16行
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff, 0x1b, 0x6b, 0xa2, 0xff,
}
+55
View File
@@ -0,0 +1,55 @@
//go:build !windows
// +build !windows
package tray
import (
"fmt"
"os"
"os/signal"
"syscall"
"go.uber.org/zap"
)
// TrayManager 托盘管理器(非Windows平台实现)
type TrayManager struct {
logger *zap.Logger
webPort int
onExit func()
}
// NewTrayManager 创建托盘管理器
func NewTrayManager(logger *zap.Logger, webPort int, onExit func()) *TrayManager {
return &TrayManager{
logger: logger,
webPort: webPort,
onExit: onExit,
}
}
// Run 运行托盘(非Windows平台显示提示信息并阻塞)
func (t *TrayManager) Run() {
t.logger.Info("非Windows系统,跳过托盘初始化")
// 显示启动信息
fmt.Println("")
fmt.Println("========================================")
fmt.Println("🌐 MeshRay 服务已启动")
fmt.Println("========================================")
fmt.Printf("📍 Web 管理界面: http://localhost:%d\n", t.webPort)
fmt.Println("💡 提示: 使用 Ctrl+C 停止服务")
fmt.Println("========================================")
fmt.Println("")
// 等待退出信号
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
t.logger.Info("收到退出信号,正在关闭...")
if t.onExit != nil {
t.onExit()
}
}