feat: add openconnect

This commit is contained in:
duorameng
2026-06-25 20:06:38 +08:00
parent fac1582fe0
commit 90f06de298
49 changed files with 3854 additions and 969 deletions
+43 -53
View File
@@ -7,6 +7,7 @@ import (
"github.com/engigu/baihu-panel/internal/constant"
"github.com/engigu/baihu-panel/internal/database"
"github.com/engigu/baihu-panel/internal/executor"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/models"
@@ -78,7 +79,11 @@ func GetAgentWSManager() *AgentWSManager {
ipFailCount: make(map[string]int),
remoteWaiters: make(map[string]chan *models.AgentTaskResult),
}
go agentWSManager.cleanupLoop()
// 启动时,先将所有 "online" 状态的 Agent 重置为 "offline"
NewAgentService().ResetAllAgentsToOffline()
// 将清理任务注册到系统内部 Cron,每 30 秒执行一次
executor.GetSysCron().AddJob("@every 30s", agentWSManager.cleanupLoop)
})
return agentWSManager
}
@@ -282,66 +287,51 @@ func (m *AgentWSManager) OnlineCount() int {
return len(m.connections)
}
// cleanupLoop 清理超时连接
// cleanupLoop 清理超时连接 (由 SysCron 每 30 秒调用一次)
func (m *AgentWSManager) cleanupLoop() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
defer func() {
if r := recover(); r != nil {
logger.Errorf("[AgentWS] cleanupLoop panic: %v", r)
}
}()
// 启动时,先将所有 "online" 状态的 Agent 重置为 "offline"
// 因为 WebSocket 连接在应用启动时是空的,所有 Agent 客观上都是离线状态
// 等它们重新连接上来后,会变为 "online"
NewAgentService().ResetAllAgentsToOffline()
m.mu.Lock()
defer m.mu.Unlock()
now := time.Now()
for range ticker.C {
func() {
defer func() {
if r := recover(); r != nil {
logger.Errorf("[AgentWS] cleanupLoop panic: %v", r)
}
}()
m.mu.Lock()
now := time.Now()
// 清理超时连接
for agentID, conn := range m.connections {
if now.Sub(conn.LastPing) > 2*time.Minute {
// 减少 IP 连接计数
if conn.IP != "" {
if count, ok := m.ipConnections[conn.IP]; ok && count > 0 {
m.ipConnections[conn.IP] = count - 1
}
}
conn.Close()
delete(m.connections, agentID)
// 更新数据库状态
database.DB.Model(&models.Agent{}).Where("id = ?", agentID).Update("status", constant.AgentStatusOffline)
logger.Infof("[AgentWS] Agent #%s 心跳超时,已断开", agentID)
// 清理超时连接
for agentID, conn := range m.connections {
if now.Sub(conn.LastPing) > 2*time.Minute {
// 减少 IP 连接计数
if conn.IP != "" {
if count, ok := m.ipConnections[conn.IP]; ok && count > 0 {
m.ipConnections[conn.IP] = count - 1
}
}
conn.Close()
delete(m.connections, agentID)
// 更新数据库状态
database.DB.Model(&models.Agent{}).Where("id = ?", agentID).Update("status", constant.AgentStatusOffline)
logger.Infof("[AgentWS] Agent #%s 心跳超时,已断开", agentID)
}
}
// 定期清理数据库中的过期状态(处理服务重启或异常终止的情况)
// 有些 Agent 虽然没有连接,但数据库状态可能是 "online"
cutoff := now.Add(-2 * time.Minute)
database.DB.Model(&models.Agent{}).
Where("status = ? AND last_seen < ?", constant.AgentStatusOnline, cutoff).
Update("status", constant.AgentStatusOffline)
// 定期清理数据库中的过期状态(处理服务重启或异常终止的情况)
cutoff := now.Add(-2 * time.Minute)
database.DB.Model(&models.Agent{}).
Where("status = ? AND last_seen < ?", constant.AgentStatusOnline, cutoff).
Update("status", constant.AgentStatusOffline)
// 清理过期的限流记录(超过 10 分钟未活动)
// 清理过期的限流记录(超过 10 分钟未活动)
for ip, lastAttempt := range m.ipLastAttempt {
if now.Sub(lastAttempt) > 10*time.Minute {
delete(m.ipLastAttempt, ip)
delete(m.ipFailCount, ip)
// 只清理没有活跃连接的 IP 计数
if m.ipConnections[ip] == 0 {
delete(m.ipConnections, ip)
}
}
// 清理过期的限流记录(超过 10 分钟未活动)
for ip, lastAttempt := range m.ipLastAttempt {
if now.Sub(lastAttempt) > 10*time.Minute {
delete(m.ipLastAttempt, ip)
delete(m.ipFailCount, ip)
// 只清理没有活跃连接的 IP 计数
if m.ipConnections[ip] == 0 {
delete(m.ipConnections, ip)
}
m.mu.Unlock()
}()
}
}
}
+85
View File
@@ -0,0 +1,85 @@
package services
import (
"strings"
"github.com/engigu/baihu-panel/internal/database"
"github.com/engigu/baihu-panel/internal/models"
"github.com/engigu/baihu-panel/internal/utils"
)
type InterconnectService struct{}
func NewInterconnectService() *InterconnectService {
return &InterconnectService{}
}
func (s *InterconnectService) GetNodes() ([]*models.InterconnectNode, error) {
var nodes []*models.InterconnectNode
err := database.DB.Find(&nodes).Error
return nodes, err
}
func (s *InterconnectService) GetNodeByID(id string) (*models.InterconnectNode, error) {
var node models.InterconnectNode
err := database.DB.Where("id = ?", id).First(&node).Error
return &node, err
}
func (s *InterconnectService) CreateNode(name, url, token, remark string) (*models.InterconnectNode, error) {
nodeID := utils.GenerateID()
if url == "" {
url = "tunnel://" + nodeID
}
node := &models.InterconnectNode{
ID: nodeID,
Name: name,
URL: url,
Token: strings.ToLower(token),
Remark: remark,
CreatedAt: models.Now(),
UpdatedAt: models.Now(),
}
err := database.DB.Create(node).Error
return node, err
}
func (s *InterconnectService) UpdateNode(id, name, url, token, remark string) (*models.InterconnectNode, error) {
node, err := s.GetNodeByID(id)
if err != nil {
return nil, err
}
node.Name = name
if url != "" {
node.URL = url
}
node.Token = token
node.Remark = remark
node.UpdatedAt = models.Now()
err = database.DB.Save(node).Error
return node, err
}
func (s *InterconnectService) DeleteNode(id string) error {
return database.DB.Where("id = ?", id).Delete(&models.InterconnectNode{}).Error
}
func (s *InterconnectService) GetNodeByToken(token string) (*models.InterconnectNode, error) {
var node models.InterconnectNode
err := database.DB.Where("token = ?", token).First(&node).Error
return &node, err
}
func (s *InterconnectService) UpdateNodeMonitorData(id string, metrics models.NodeMetrics) error {
now := models.Now()
return database.DB.Model(&models.InterconnectNode{}).
Where("id = ?", id).
Select("status", "metrics", "last_heartbeat_at", "updated_at").
Updates(models.InterconnectNode{
Status: "online",
Metrics: metrics,
LastHeartbeatAt: &now,
UpdatedAt: now,
}).Error
}
+132
View File
@@ -0,0 +1,132 @@
package services
import (
"math/rand"
"runtime"
"sync"
"time"
"github.com/engigu/baihu-panel/internal/constant"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/mem"
)
type HostMetrics struct {
CPUPercent float64
VMem *mem.VirtualMemoryStat
DiskUsage *disk.UsageStat
HostInfo *host.InfoStat
}
type MonitorService struct {
hostMu sync.RWMutex
lastUpdate time.Time
metrics HostMetrics
}
var (
monitorServiceInstance *MonitorService
monitorServiceOnce sync.Once
)
// GetMonitorService 获取系统监控服务单例
func GetMonitorService() *MonitorService {
monitorServiceOnce.Do(func() {
monitorServiceInstance = &MonitorService{}
})
return monitorServiceInstance
}
// GetHostMetrics 获取并返回物理机状态(带有缓存和演示模式伪装)
func (ms *MonitorService) GetHostMetrics() HostMetrics {
ms.hostMu.Lock()
defer ms.hostMu.Unlock()
// 缓存 2 秒
if time.Since(ms.lastUpdate) < 2*time.Second && ms.metrics.VMem != nil {
return ms.metrics
}
if constant.DemoMode {
ms.updateDemoMetrics()
return ms.metrics
}
cpuPercents, _ := cpu.Percent(0, false)
if len(cpuPercents) > 0 {
ms.metrics.CPUPercent = cpuPercents[0]
}
ms.metrics.VMem, _ = mem.VirtualMemory()
ms.metrics.DiskUsage, _ = disk.Usage("/")
ms.metrics.HostInfo, _ = host.Info()
ms.lastUpdate = time.Now()
// 提供默认值防空指针
if ms.metrics.VMem == nil {
ms.metrics.VMem = &mem.VirtualMemoryStat{}
}
if ms.metrics.DiskUsage == nil {
ms.metrics.DiskUsage = &disk.UsageStat{}
}
if ms.metrics.HostInfo == nil {
ms.metrics.HostInfo = &host.InfoStat{}
}
return ms.metrics
}
type RuntimeMetrics struct {
NumGoroutine int
MemStats runtime.MemStats
}
var (
runtimeMu sync.RWMutex
lastRuntime time.Time
cachedRuntime RuntimeMetrics
)
// GetRuntimeMetrics 获取 Go 运行时指标(缓存 2 秒,防止高并发下频繁触发 STW)
func (ms *MonitorService) GetRuntimeMetrics() RuntimeMetrics {
runtimeMu.Lock()
defer runtimeMu.Unlock()
if time.Since(lastRuntime) < 2*time.Second && cachedRuntime.NumGoroutine > 0 {
return cachedRuntime
}
cachedRuntime.NumGoroutine = runtime.NumGoroutine()
runtime.ReadMemStats(&cachedRuntime.MemStats)
lastRuntime = time.Now()
return cachedRuntime
}
func (ms *MonitorService) updateDemoMetrics() {
ms.metrics.CPUPercent = 10 + rand.Float64()*40 // 10% - 50% 的随机 CPU 波动
totalMem := uint64(8 * 1024 * 1024 * 1024) // 8GB 内存
usedMem := uint64(float64(totalMem) * (0.3 + rand.Float64()*0.3)) // 30% - 60% 随机使用率
ms.metrics.VMem = &mem.VirtualMemoryStat{
Total: totalMem,
Used: usedMem,
UsedPercent: float64(usedMem) / float64(totalMem) * 100,
}
totalDisk := uint64(500 * 1024 * 1024 * 1024) // 500GB 硬盘
usedDisk := uint64(float64(totalDisk) * 0.45) // 固定 45% 使用率
ms.metrics.DiskUsage = &disk.UsageStat{
Total: totalDisk,
Used: usedDisk,
UsedPercent: float64(usedDisk) / float64(totalDisk) * 100,
}
ms.metrics.HostInfo = &host.InfoStat{
Platform: "Demo Environment",
OS: "linux",
Uptime: uint64(time.Now().Unix() - 1700000000), // 生成一个较长且持续增加的运行时间
}
ms.lastUpdate = time.Now()
}