Initial commit: proxy management platform
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
// Client Agent 客户端
|
||||
type Client struct {
|
||||
schedulerHost string
|
||||
apiKey string
|
||||
nodeID string
|
||||
name string
|
||||
region string
|
||||
heartbeatInterval time.Duration
|
||||
reportInterval time.Duration
|
||||
logger *zap.Logger
|
||||
httpClient *http.Client
|
||||
connMutex sync.RWMutex
|
||||
wsConn *websocket.Conn
|
||||
stats *NodeStats
|
||||
statsMutex sync.RWMutex
|
||||
commandChan chan Command
|
||||
}
|
||||
|
||||
// NodeStats 节点统计
|
||||
type NodeStats struct {
|
||||
Online bool `json:"online"`
|
||||
WARPConnected bool `json:"warp_connected"`
|
||||
CurrentIP string `json:"current_ip"`
|
||||
IPRegion string `json:"ip_region"`
|
||||
Connections int `json:"connections"`
|
||||
TrafficUsedGB float64 `json:"traffic_used_gb"`
|
||||
Unlocks map[string]Unlock `json:"unlocks"`
|
||||
CPUUsage float64 `json:"cpu_usage"`
|
||||
MemoryUsage float64 `json:"memory_usage"`
|
||||
NetworkInMbps float64 `json:"network_in_mbps"`
|
||||
NetworkOutMbps float64 `json:"network_out_mbps"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
// Unlock 解锁状态
|
||||
type Unlock struct {
|
||||
Unlocked bool `json:"unlocked"`
|
||||
Region string `json:"region"`
|
||||
}
|
||||
|
||||
// Command 调度中心指令
|
||||
type Command struct {
|
||||
Action string `json:"action"`
|
||||
Params map[string]interface{} `json:"params"`
|
||||
}
|
||||
|
||||
// NewClient 创建 Agent 客户端
|
||||
func NewClient(
|
||||
schedulerHost string,
|
||||
apiKey string,
|
||||
nodeID string,
|
||||
name string,
|
||||
region string,
|
||||
heartbeatInterval time.Duration,
|
||||
reportInterval time.Duration,
|
||||
logger *zap.Logger,
|
||||
) *Client {
|
||||
return &Client{
|
||||
schedulerHost: schedulerHost,
|
||||
apiKey: apiKey,
|
||||
nodeID: nodeID,
|
||||
name: name,
|
||||
region: region,
|
||||
heartbeatInterval: heartbeatInterval,
|
||||
reportInterval: reportInterval,
|
||||
logger: logger,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
stats: &NodeStats{
|
||||
Online: true,
|
||||
Unlocks: make(map[string]Unlock),
|
||||
LastUpdate: time.Now(),
|
||||
},
|
||||
commandChan: make(chan Command, 10),
|
||||
}
|
||||
}
|
||||
|
||||
// StartHeartbeat 启动心跳
|
||||
func (c *Client) StartHeartbeat(ctx context.Context) {
|
||||
ticker := time.NewTicker(c.heartbeatInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
c.sendHeartbeat(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendHeartbeat 发送心跳
|
||||
func (c *Client) sendHeartbeat(ctx context.Context) {
|
||||
c.statsMutex.RLock()
|
||||
stats := *c.stats
|
||||
c.statsMutex.RUnlock()
|
||||
|
||||
// 收集系统指标
|
||||
stats.CPUUsage = c.getCPUUsage()
|
||||
stats.MemoryUsage = c.getMemoryUsage()
|
||||
stats.Connections = c.getConnections()
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"node_id": c.nodeID,
|
||||
"online": stats.Online,
|
||||
"warp_connected": stats.WARPConnected,
|
||||
"current_ip": stats.CurrentIP,
|
||||
"ip_region": stats.IPRegion,
|
||||
"connections": stats.Connections,
|
||||
"traffic_used_gb": stats.TrafficUsedGB,
|
||||
"unlocks": stats.Unlocks,
|
||||
"cpu_usage": stats.CPUUsage,
|
||||
"memory_usage": stats.MemoryUsage,
|
||||
"network_in_mbps": stats.NetworkInMbps,
|
||||
"network_out_mbps": stats.NetworkOutMbps,
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/v1/agent/heartbeat", c.schedulerHost)
|
||||
|
||||
body, err := c.post(ctx, url, payload)
|
||||
if err != nil {
|
||||
c.logger.Error("发送心跳失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// 解析响应,获取指令
|
||||
var resp struct {
|
||||
Message string `json:"message"`
|
||||
Commands []Command `json:"commands"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
c.logger.Error("解析心跳响应失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// 处理指令
|
||||
for _, cmd := range resp.Commands {
|
||||
c.logger.Info("收到调度中心指令",
|
||||
zap.String("action", cmd.Action),
|
||||
zap.Any("params", cmd.Params),
|
||||
)
|
||||
select {
|
||||
case c.commandChan <- cmd:
|
||||
default:
|
||||
c.logger.Warn("指令队列已满")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReportUnlock 上报解锁状态
|
||||
func (c *Client) ReportUnlock(ctx context.Context, unlocks map[string]struct {
|
||||
Unlocked bool `json:"unlocked"`
|
||||
Region string `json:"region"`
|
||||
}) {
|
||||
// 更新本地状态
|
||||
c.statsMutex.Lock()
|
||||
for service, status := range unlocks {
|
||||
c.stats.Unlocks[service] = Unlock{
|
||||
Unlocked: status.Unlocked,
|
||||
Region: status.Region,
|
||||
}
|
||||
}
|
||||
c.statsMutex.Unlock()
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"node_id": c.nodeID,
|
||||
"unlocks": unlocks,
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/v1/agent/unlock/report", c.schedulerHost)
|
||||
|
||||
_, err := c.post(ctx, url, payload)
|
||||
if err != nil {
|
||||
c.logger.Error("上报解锁状态失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Info("解锁状态已上报")
|
||||
}
|
||||
|
||||
// ReportIPChange 上报 IP 变更
|
||||
func (c *Client) ReportIPChange(ctx context.Context, oldIP, newIP string, success bool, reason string) {
|
||||
payload := map[string]interface{}{
|
||||
"node_id": c.nodeID,
|
||||
"old_ip": oldIP,
|
||||
"new_ip": newIP,
|
||||
"success": success,
|
||||
"reason": reason,
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/v1/agent/ip/change/result", c.schedulerHost)
|
||||
|
||||
_, err := c.post(ctx, url, payload)
|
||||
if err != nil {
|
||||
c.logger.Error("上报 IP 变更失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Info("IP 变更已上报",
|
||||
zap.String("old_ip", oldIP),
|
||||
zap.String("new_ip", newIP),
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateStats 更新统计信息
|
||||
func (c *Client) UpdateStats(stats *NodeStats) {
|
||||
c.statsMutex.Lock()
|
||||
defer c.statsMutex.Unlock()
|
||||
|
||||
if stats.CurrentIP != "" {
|
||||
c.stats.CurrentIP = stats.CurrentIP
|
||||
}
|
||||
if stats.IPRegion != "" {
|
||||
c.stats.IPRegion = stats.IPRegion
|
||||
}
|
||||
if stats.WARPConnected != c.stats.WARPConnected {
|
||||
c.stats.WARPConnected = stats.WARPConnected
|
||||
}
|
||||
c.stats.Connections = stats.Connections
|
||||
c.stats.LastUpdate = time.Now()
|
||||
}
|
||||
|
||||
// GetCommands 获取指令通道
|
||||
func (c *Client) GetCommands() <-chan Command {
|
||||
return c.commandChan
|
||||
}
|
||||
|
||||
// post 发送 POST 请求
|
||||
func (c *Client) post(ctx context.Context, url string, payload interface{}) ([]byte, error) {
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-API-Key", c.apiKey)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// getCPUUsage 获取 CPU 使用率
|
||||
func (c *Client) getCPUUsage() float64 {
|
||||
// 简化实现,实际应该使用 gopsutil
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// getMemoryUsage 获取内存使用率
|
||||
func (c *Client) getMemoryUsage() float64 {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
return float64(m.Sys) / 1024 / 1024
|
||||
}
|
||||
|
||||
// getConnections 获取连接数
|
||||
func (c *Client) getConnections() int {
|
||||
// TODO: 从 SOCKS5 服务器获取实际连接数
|
||||
return 0
|
||||
}
|
||||
|
||||
// CommandHandler 指令处理器
|
||||
type CommandHandler struct {
|
||||
client *Client
|
||||
warpClient WarpClient
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// WarpClient WARP 客户端接口
|
||||
type WarpClient interface {
|
||||
RefreshIP(ctx context.Context) (string, error)
|
||||
GetCurrentIP(ctx context.Context) (string, error)
|
||||
Connect(ctx context.Context) error
|
||||
Disconnect(ctx context.Context) error
|
||||
}
|
||||
|
||||
// NewCommandHandler 创建指令处理器
|
||||
func NewCommandHandler(client *Client, warpClient WarpClient, logger *zap.Logger) *CommandHandler {
|
||||
return &CommandHandler{
|
||||
client: client,
|
||||
warpClient: warpClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动指令处理
|
||||
func (h *CommandHandler) Start(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case cmd := <-h.client.GetCommands():
|
||||
h.handleCommand(ctx, cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleCommand 处理指令
|
||||
func (h *CommandHandler) handleCommand(ctx context.Context, cmd Command) {
|
||||
switch cmd.Action {
|
||||
case "refresh_ip":
|
||||
h.handleRefreshIP(ctx, cmd)
|
||||
case "connect_warp":
|
||||
h.handleConnectWARP(ctx, cmd)
|
||||
case "disconnect_warp":
|
||||
h.handleDisconnectWARP(ctx, cmd)
|
||||
default:
|
||||
h.logger.Warn("未知指令", zap.String("action", cmd.Action))
|
||||
}
|
||||
}
|
||||
|
||||
// handleRefreshIP 处理刷新 IP 指令
|
||||
func (h *CommandHandler) handleRefreshIP(ctx context.Context, cmd Command) {
|
||||
h.logger.Info("执行刷新 IP 指令")
|
||||
|
||||
reason, _ := cmd.Params["reason"].(string)
|
||||
|
||||
// 获取旧 IP
|
||||
oldIP, _ := h.warpClient.GetCurrentIP(ctx)
|
||||
|
||||
// 刷新 IP
|
||||
newIP, err := h.warpClient.RefreshIP(ctx)
|
||||
if err != nil {
|
||||
h.logger.Error("刷新 IP 失败", zap.Error(err))
|
||||
h.client.ReportIPChange(ctx, oldIP, "", false, reason)
|
||||
return
|
||||
}
|
||||
|
||||
// 上报结果
|
||||
h.client.ReportIPChange(ctx, oldIP, newIP, true, reason)
|
||||
|
||||
// 更新本地状态
|
||||
h.client.UpdateStats(&NodeStats{
|
||||
CurrentIP: newIP,
|
||||
WARPConnected: true,
|
||||
})
|
||||
}
|
||||
|
||||
// handleConnectWARP 处理连接 WARP 指令
|
||||
func (h *CommandHandler) handleConnectWARP(ctx context.Context, cmd Command) {
|
||||
h.logger.Info("执行连接 WARP 指令")
|
||||
|
||||
if err := h.warpClient.Connect(ctx); err != nil {
|
||||
h.logger.Error("连接 WARP 失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
ip, _ := h.warpClient.GetCurrentIP(ctx)
|
||||
h.client.UpdateStats(&NodeStats{
|
||||
WARPConnected: true,
|
||||
CurrentIP: ip,
|
||||
})
|
||||
}
|
||||
|
||||
// handleDisconnectWARP 处理断开 WARP 指令
|
||||
func (h *CommandHandler) handleDisconnectWARP(ctx context.Context, cmd Command) {
|
||||
h.logger.Info("执行断开 WARP 指令")
|
||||
|
||||
if err := h.warpClient.Disconnect(ctx); err != nil {
|
||||
h.logger.Error("断开 WARP 失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
h.client.UpdateStats(&NodeStats{
|
||||
WARPConnected: false,
|
||||
CurrentIP: "",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Config 调度中心配置
|
||||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
SOCKS5 SOCKS5Config `mapstructure:"socks5"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
Redis RedisConfig `mapstructure:"redis"`
|
||||
Scheduler SchedulerConfig `mapstructure:"scheduler"`
|
||||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Mode string `mapstructure:"mode"`
|
||||
}
|
||||
|
||||
type SOCKS5Config struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
MaxConnections int `mapstructure:"max_connections"`
|
||||
Timeout int `mapstructure:"timeout"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
User string `mapstructure:"user"`
|
||||
Password string `mapstructure:"password"`
|
||||
Database string `mapstructure:"database"`
|
||||
SSLMode string `mapstructure:"sslmode"`
|
||||
}
|
||||
|
||||
func (c DatabaseConfig) DSN() string {
|
||||
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||
c.Host, c.Port, c.User, c.Password, c.Database, c.SSLMode)
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Password string `mapstructure:"password"`
|
||||
DB int `mapstructure:"db"`
|
||||
}
|
||||
|
||||
func (c RedisConfig) Addr() string {
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||
}
|
||||
|
||||
type SchedulerConfig struct {
|
||||
Strategy string `mapstructure:"strategy"`
|
||||
HealthCheckInterval int `mapstructure:"health_check_interval"`
|
||||
UnlockCheckInterval int `mapstructure:"unlock_check_interval"`
|
||||
NodeTimeout int `mapstructure:"node_timeout"`
|
||||
}
|
||||
|
||||
type LoggingConfig struct {
|
||||
Level string `mapstructure:"level"`
|
||||
Output string `mapstructure:"output"`
|
||||
File string `mapstructure:"file"`
|
||||
}
|
||||
|
||||
// Load 加载配置
|
||||
func Load(configPath string) (*Config, error) {
|
||||
viper.SetConfigFile(configPath)
|
||||
viper.SetConfigType("yaml")
|
||||
|
||||
// 设置默认值
|
||||
viper.SetDefault("server.host", "0.0.0.0")
|
||||
viper.SetDefault("server.port", 8080)
|
||||
viper.SetDefault("server.mode", "release")
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// AgentConfig 节点 Agent 配置
|
||||
type AgentConfig struct {
|
||||
Agent AgentSettings `mapstructure:"agent"`
|
||||
Scheduler SchedulerConn `mapstructure:"scheduler"`
|
||||
WARP WARPConfig `mapstructure:"warp"`
|
||||
SOCKS5 SOCKS5Config `mapstructure:"socks5"`
|
||||
Unlock UnlockConfig `mapstructure:"unlock"`
|
||||
Routing RoutingConfig `mapstructure:"routing"`
|
||||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
}
|
||||
|
||||
type AgentSettings struct {
|
||||
NodeID string `mapstructure:"node_id"`
|
||||
Name string `mapstructure:"name"`
|
||||
Region string `mapstructure:"region"`
|
||||
}
|
||||
|
||||
type SchedulerConn struct {
|
||||
Host string `mapstructure:"host"`
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
HeartbeatInterval int `mapstructure:"heartbeat_interval"`
|
||||
ReportInterval int `mapstructure:"report_interval"`
|
||||
}
|
||||
|
||||
type WARPConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
SOCKS5Port int `mapstructure:"socks5_port"`
|
||||
RefreshCooldown int `mapstructure:"refresh_cooldown"`
|
||||
MaxRefreshRetries int `mapstructure:"max_refresh_retries"`
|
||||
RefreshRetryDelayMin int `mapstructure:"refresh_retry_delay_min"`
|
||||
RefreshRetryDelayMax int `mapstructure:"refresh_retry_delay_max"`
|
||||
}
|
||||
|
||||
type UnlockConfig struct {
|
||||
CheckInterval int `mapstructure:"check_interval"`
|
||||
Services []ServiceConfig `mapstructure:"services"`
|
||||
}
|
||||
|
||||
type ServiceConfig struct {
|
||||
Name string `mapstructure:"name"`
|
||||
URL string `mapstructure:"url"`
|
||||
SuccessKeywords []string `mapstructure:"success_keywords"`
|
||||
FailKeywords []string `mapstructure:"fail_keywords"`
|
||||
}
|
||||
|
||||
type RoutingConfig struct {
|
||||
WARPRoutes []RouteRule `mapstructure:"warp_routes"`
|
||||
DirectRoutes []RouteRule `mapstructure:"direct_routes"`
|
||||
}
|
||||
|
||||
type RouteRule struct {
|
||||
Port int `mapstructure:"port"`
|
||||
Domains []string `mapstructure:"domains"`
|
||||
}
|
||||
|
||||
// LoadAgent 加载 Agent 配置
|
||||
func LoadAgent(configPath string) (*AgentConfig, error) {
|
||||
viper.SetConfigFile(configPath)
|
||||
viper.SetConfigType("yaml")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("读取配置文件失败: %w", err)
|
||||
}
|
||||
|
||||
var config AgentConfig
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
return nil, fmt.Errorf("解析配置失败: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"proxy-platform/internal/models"
|
||||
"proxy-platform/internal/repository"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// ListUsers 获取用户列表
|
||||
func ListUsers(repo *repository.UserRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
users, total, err := repo.List(offset, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": users,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUser 创建用户
|
||||
func CreateUser(repo *repository.UserRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
TrafficQuota int64 `json:"traffic_quota"`
|
||||
ExpireDays int `json:"expire_days"`
|
||||
NodeGroupID *uint `json:"node_group_id"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 密码加密
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
|
||||
return
|
||||
}
|
||||
|
||||
user := &models.User{
|
||||
Username: req.Username,
|
||||
PasswordHash: string(hashedPassword),
|
||||
TrafficQuota: req.TrafficQuota,
|
||||
NodeGroupID: req.NodeGroupID,
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
if req.ExpireDays > 0 {
|
||||
expireAt := time.Now().AddDate(0, 0, req.ExpireDays)
|
||||
user.ExpireAt = &expireAt
|
||||
}
|
||||
|
||||
if err := repo.Create(user); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, user)
|
||||
}
|
||||
}
|
||||
|
||||
// GetUser 获取用户
|
||||
func GetUser(repo *repository.UserRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := repo.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateUser 更新用户
|
||||
func UpdateUser(repo *repository.UserRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := repo.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Password *string `json:"password"`
|
||||
TrafficQuota *int64 `json:"traffic_quota"`
|
||||
ExpireDays *int `json:"expire_days"`
|
||||
NodeGroupID *uint `json:"node_group_id"`
|
||||
Status *string `json:"status"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Password != nil {
|
||||
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
|
||||
user.PasswordHash = string(hashedPassword)
|
||||
}
|
||||
if req.TrafficQuota != nil {
|
||||
user.TrafficQuota = *req.TrafficQuota
|
||||
}
|
||||
if req.ExpireDays != nil {
|
||||
expireAt := time.Now().AddDate(0, 0, *req.ExpireDays)
|
||||
user.ExpireAt = &expireAt
|
||||
}
|
||||
if req.NodeGroupID != nil {
|
||||
user.NodeGroupID = req.NodeGroupID
|
||||
}
|
||||
if req.Status != nil {
|
||||
user.Status = *req.Status
|
||||
}
|
||||
|
||||
if err := repo.Update(user); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func DeleteUser(repo *repository.UserRepository) gin.HandlerFunc {
|
||||
return func(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 := repo.Delete(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
|
||||
}
|
||||
}
|
||||
|
||||
// ListNodes 获取节点列表
|
||||
func ListNodes(repo *repository.NodeRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
nodes, err := repo.List()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": nodes})
|
||||
}
|
||||
}
|
||||
|
||||
// CreateNode 创建节点
|
||||
func CreateNode(repo *repository.NodeRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
NodeID string `json:"node_id" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Host string `json:"host" binding:"required"`
|
||||
Port int `json:"port"`
|
||||
Region string `json:"region"`
|
||||
Country string `json:"country"`
|
||||
Weight int `json:"weight"`
|
||||
MaxConnections int `json:"max_connections"`
|
||||
NodeGroupID *uint `json:"node_group_id"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Port == 0 {
|
||||
req.Port = 1080
|
||||
}
|
||||
if req.Weight == 0 {
|
||||
req.Weight = 100
|
||||
}
|
||||
if req.MaxConnections == 0 {
|
||||
req.MaxConnections = 1000
|
||||
}
|
||||
|
||||
node := &models.Node{
|
||||
NodeID: req.NodeID,
|
||||
Name: req.Name,
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Region: req.Region,
|
||||
Country: req.Country,
|
||||
Weight: req.Weight,
|
||||
MaxConnections: req.MaxConnections,
|
||||
NodeGroupID: req.NodeGroupID,
|
||||
Status: "offline",
|
||||
WARPStatus: "disconnected",
|
||||
}
|
||||
|
||||
if err := repo.Create(node); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, node)
|
||||
}
|
||||
}
|
||||
|
||||
// GetNode 获取节点
|
||||
func GetNode(repo *repository.NodeRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
|
||||
return
|
||||
}
|
||||
|
||||
node, err := repo.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "节点不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, node)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateNode 更新节点
|
||||
func UpdateNode(repo *repository.NodeRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
|
||||
return
|
||||
}
|
||||
|
||||
node, err := repo.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "节点不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name *string `json:"name"`
|
||||
Host *string `json:"host"`
|
||||
Port *int `json:"port"`
|
||||
Weight *int `json:"weight"`
|
||||
MaxConnections *int `json:"max_connections"`
|
||||
Status *string `json:"status"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
node.Name = *req.Name
|
||||
}
|
||||
if req.Host != nil {
|
||||
node.Host = *req.Host
|
||||
}
|
||||
if req.Port != nil {
|
||||
node.Port = *req.Port
|
||||
}
|
||||
if req.Weight != nil {
|
||||
node.Weight = *req.Weight
|
||||
}
|
||||
if req.MaxConnections != nil {
|
||||
node.MaxConnections = *req.MaxConnections
|
||||
}
|
||||
if req.Status != nil {
|
||||
node.Status = *req.Status
|
||||
}
|
||||
|
||||
if err := repo.Update(node); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, node)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteNode 删除节点
|
||||
func DeleteNode(repo *repository.NodeRepository) gin.HandlerFunc {
|
||||
return func(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 := repo.Delete(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshNodeIP 刷新节点 IP
|
||||
func RefreshNodeIP(repo *repository.NodeRepository, logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
logger.Info("收到刷新 IP 请求", zap.String("node_id", id))
|
||||
|
||||
// TODO: 发送刷新指令到 Agent
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "刷新指令已发送",
|
||||
"node_id": id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// AgentHeartbeat Agent 心跳
|
||||
func AgentHeartbeat(repo *repository.NodeRepository, logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Online bool `json:"online"`
|
||||
WARPConnected bool `json:"warp_connected"`
|
||||
CurrentIP string `json:"current_ip"`
|
||||
IPRegion string `json:"ip_region"`
|
||||
Connections int `json:"connections"`
|
||||
TrafficUsedGB float64 `json:"traffic_used_gb"`
|
||||
Unlocks map[string]bool `json:"unlocks"`
|
||||
CPUUsage float64 `json:"cpu_usage"`
|
||||
MemoryUsage float64 `json:"memory_usage"`
|
||||
NetworkInMbps float64 `json:"network_in_mbps"`
|
||||
NetworkOutMbps float64 `json:"network_out_mbps"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("收到心跳",
|
||||
zap.String("node_id", req.NodeID),
|
||||
zap.Bool("online", req.Online),
|
||||
zap.String("ip", req.CurrentIP),
|
||||
)
|
||||
|
||||
// 更新节点状态
|
||||
warpStatus := "disconnected"
|
||||
if req.WARPConnected {
|
||||
warpStatus = "connected"
|
||||
}
|
||||
|
||||
status := "offline"
|
||||
if req.Online {
|
||||
status = "online"
|
||||
}
|
||||
|
||||
if err := repo.UpdateStatus(req.NodeID, status, warpStatus); err != nil {
|
||||
logger.Error("更新节点状态失败", zap.Error(err))
|
||||
}
|
||||
|
||||
if req.CurrentIP != "" {
|
||||
if err := repo.UpdateIP(req.NodeID, req.CurrentIP, req.IPRegion); err != nil {
|
||||
logger.Error("更新节点 IP 失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := repo.UpdateConnections(req.NodeID, req.Connections); err != nil {
|
||||
logger.Error("更新节点连接数失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 返回指令(如果有)
|
||||
commands := []interface{}{}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "心跳已接收",
|
||||
"commands": commands,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ReportUnlockStatus 上报解锁状态
|
||||
func ReportUnlockStatus(repo *repository.UnlockStatusRepository, logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Unlocks map[string]struct {
|
||||
Unlocked bool `json:"unlocked"`
|
||||
Region string `json:"region"`
|
||||
} `json:"unlocks"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("收到解锁状态上报",
|
||||
zap.String("node_id", req.NodeID),
|
||||
zap.Any("unlocks", req.Unlocks),
|
||||
)
|
||||
|
||||
// 查找节点
|
||||
node, err := (&repository.NodeRepository{}).FindByNodeID(req.NodeID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "节点不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 更新解锁状态
|
||||
for service, status := range req.Unlocks {
|
||||
if err := repo.Upsert(node.ID, service, status.Unlocked, status.Region); err != nil {
|
||||
logger.Error("更新解锁状态失败",
|
||||
zap.String("service", service),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "解锁状态已更新"})
|
||||
}
|
||||
}
|
||||
|
||||
// ReportIPChange 上报 IP 变更
|
||||
func ReportIPChange(nodeRepo *repository.NodeRepository, logRepo *repository.IPChangeLogRepository, logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
NodeID string `json:"node_id"`
|
||||
OldIP string `json:"old_ip"`
|
||||
NewIP string `json:"new_ip"`
|
||||
Success bool `json:"success"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("收到 IP 变更上报",
|
||||
zap.String("node_id", req.NodeID),
|
||||
zap.String("old_ip", req.OldIP),
|
||||
zap.String("new_ip", req.NewIP),
|
||||
zap.Bool("success", req.Success),
|
||||
)
|
||||
|
||||
// 查找节点
|
||||
node, err := nodeRepo.FindByNodeID(req.NodeID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "节点不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
log := &models.IPChangeLog{
|
||||
NodeID: node.ID,
|
||||
OldIP: req.OldIP,
|
||||
NewIP: req.NewIP,
|
||||
Reason: req.Reason,
|
||||
Success: req.Success,
|
||||
}
|
||||
logRepo.Create(log)
|
||||
|
||||
// 更新节点 IP
|
||||
if req.Success && req.NewIP != "" {
|
||||
nodeRepo.UpdateIP(req.NodeID, req.NewIP, "")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "IP 变更已记录"})
|
||||
}
|
||||
}
|
||||
|
||||
// ListRules 获取规则列表
|
||||
func ListRules(repo *repository.IPRefreshRuleRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
rules, err := repo.List()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": rules})
|
||||
}
|
||||
}
|
||||
|
||||
// CreateRule 创建规则
|
||||
func CreateRule(repo *repository.IPRefreshRuleRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req models.IPRefreshRule
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo.Create(&req); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, req)
|
||||
}
|
||||
}
|
||||
|
||||
// GetRule 获取规则
|
||||
func GetRule(repo *repository.IPRefreshRuleRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
|
||||
return
|
||||
}
|
||||
|
||||
rule, err := repo.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "规则不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, rule)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateRule 更新规则
|
||||
func UpdateRule(repo *repository.IPRefreshRuleRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
|
||||
return
|
||||
}
|
||||
|
||||
rule, err := repo.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "规则不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(rule); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo.Update(rule); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, rule)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteRule 删除规则
|
||||
func DeleteRule(repo *repository.IPRefreshRuleRepository) gin.HandlerFunc {
|
||||
return func(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 := repo.Delete(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
|
||||
}
|
||||
}
|
||||
|
||||
// GetOverview 获取概览统计
|
||||
func GetOverview(repos *repository.Repositories) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// TODO: 实现统计逻辑
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"total_nodes": 0,
|
||||
"online_nodes": 0,
|
||||
"total_users": 0,
|
||||
"active_users": 0,
|
||||
"today_traffic": 0,
|
||||
"total_traffic": 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetTrafficStats 获取流量统计
|
||||
func GetTrafficStats(repo *repository.ConnectionLogRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// TODO: 实现统计逻辑
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": []interface{}{},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// User 用户模型
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"uniqueIndex;size:64;not null" json:"username"`
|
||||
PasswordHash string `gorm:"size:255;not null" json:"-"`
|
||||
TrafficQuota int64 `gorm:"default:0" json:"traffic_quota"` // 流量配额(字节)
|
||||
TrafficUsed int64 `gorm:"default:0" json:"traffic_used"` // 已用流量(字节)
|
||||
ExpireAt *time.Time `json:"expire_at"`
|
||||
NodeGroupID *uint `json:"node_group_id"`
|
||||
NodeGroup *NodeGroup `json:"node_group,omitempty"`
|
||||
Status string `gorm:"type:varchar(20);default:'active'" json:"status"` // active, suspended, expired
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// NodeGroup 节点组
|
||||
type NodeGroup struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:64;not null" json:"name"`
|
||||
Description string `gorm:"size:255" json:"description"`
|
||||
Nodes []Node `json:"nodes,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Node 节点模型
|
||||
type Node struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
NodeID string `gorm:"uniqueIndex;size:64;not null" json:"node_id"`
|
||||
Name string `gorm:"size:64;not null" json:"name"`
|
||||
Host string `gorm:"size:255;not null" json:"host"`
|
||||
Port int `gorm:"default:1080" json:"port"`
|
||||
Region string `gorm:"size:32" json:"region"`
|
||||
Country string `gorm:"size:32" json:"country"`
|
||||
CurrentIP string `gorm:"size:64" json:"current_ip"`
|
||||
IPRegion string `gorm:"size:32" json:"ip_region"`
|
||||
Weight int `gorm:"default:100" json:"weight"`
|
||||
MaxConnections int `gorm:"default:1000" json:"max_connections"`
|
||||
CurrentConnections int `gorm:"default:0" json:"current_connections"`
|
||||
Status string `gorm:"type:varchar(20);default:'offline'" json:"status"` // online, offline, maintenance
|
||||
WARPStatus string `gorm:"type:varchar(20);default:'disconnected'" json:"warp_status"` // connected, disconnected, error
|
||||
UnlockStatuses []UnlockStatus `json:"unlock_statuses,omitempty"`
|
||||
NodeGroupID *uint `json:"node_group_id"`
|
||||
NodeGroup *NodeGroup `json:"node_group,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
LastHeartbeat *time.Time `json:"last_heartbeat"`
|
||||
}
|
||||
|
||||
// UnlockStatus 解锁状态
|
||||
type UnlockStatus struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
NodeID uint `gorm:"not null;uniqueIndex:idx_node_service" json:"node_id"`
|
||||
Service string `gorm:"size:32;not null;uniqueIndex:idx_node_service" json:"service"` // gpt, netflix, disney...
|
||||
Unlocked bool `gorm:"default:false" json:"unlocked"`
|
||||
Region string `gorm:"size:32" json:"region"`
|
||||
DetectedAt time.Time `json:"detected_at"`
|
||||
}
|
||||
|
||||
// IPChangeLog IP 变更日志
|
||||
type IPChangeLog struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
NodeID uint `gorm:"not null;index" json:"node_id"`
|
||||
OldIP string `gorm:"size:64" json:"old_ip"`
|
||||
NewIP string `gorm:"size:64" json:"new_ip"`
|
||||
Reason string `gorm:"size:64" json:"reason"` // unlock_failure, usage_threshold, scheduled...
|
||||
Success bool `gorm:"default:true" json:"success"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ConnectionLog 连接日志
|
||||
type ConnectionLog struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null;index" json:"user_id"`
|
||||
NodeID uint `gorm:"not null;index" json:"node_id"`
|
||||
ClientIP string `gorm:"size:64" json:"client_ip"`
|
||||
TargetHost string `gorm:"size:255" json:"target_host"`
|
||||
TargetPort int `json:"target_port"`
|
||||
BytesIn int64 `gorm:"default:0" json:"bytes_in"`
|
||||
BytesOut int64 `gorm:"default:0" json:"bytes_out"`
|
||||
Duration int `gorm:"default:0" json:"duration"` // 毫秒
|
||||
Status string `gorm:"type:varchar(20)" json:"status"` // success, failed, timeout
|
||||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||||
}
|
||||
|
||||
// IPRefreshRule IP 刷新规则
|
||||
type IPRefreshRule struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
NodeGroupID *uint `json:"node_group_id"` // NULL 表示全局规则
|
||||
TriggerType string `gorm:"type:varchar(32);not null" json:"trigger_type"` // unlock_failure, usage_count, usage_traffic, scheduled, anomaly
|
||||
TriggerValue string `gorm:"type:text" json:"trigger_value"` // JSON 配置
|
||||
Cooldown int `gorm:"default:300" json:"cooldown"` // 冷却时间(秒)
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// NodeStats 节点统计缓存
|
||||
type NodeStats struct {
|
||||
NodeID string `json:"node_id"`
|
||||
CPUUsage float64 `json:"cpu_usage"`
|
||||
MemoryUsage float64 `json:"memory_usage"`
|
||||
NetworkInMbps float64 `json:"network_in_mbps"`
|
||||
NetworkOutMbps float64 `json:"network_out_mbps"`
|
||||
Connections int `json:"connections"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"proxy-platform/internal/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserRepository 用户仓库
|
||||
type UserRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserRepository(db *gorm.DB) *UserRepository {
|
||||
return &UserRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *UserRepository) Create(user *models.User) error {
|
||||
return r.db.Create(user).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindByUsername(username string) (*models.User, error) {
|
||||
var user models.User
|
||||
err := r.db.Where("username = ?", username).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindByID(id uint) (*models.User, error) {
|
||||
var user models.User
|
||||
err := r.db.First(&user, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) Update(user *models.User) error {
|
||||
return r.db.Save(user).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) UpdateTraffic(userID uint, bytesIn, bytesOut int64) error {
|
||||
return r.db.Model(&models.User{}).
|
||||
Where("id = ?", userID).
|
||||
Updates(map[string]interface{}{
|
||||
"traffic_used": gorm.Expr("traffic_used + ?", bytesIn+bytesOut),
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) List(offset, limit int) ([]models.User, int64, error) {
|
||||
var users []models.User
|
||||
var total int64
|
||||
|
||||
r.db.Model(&models.User{}).Count(&total)
|
||||
err := r.db.Offset(offset).Limit(limit).Find(&users).Error
|
||||
return users, total, err
|
||||
}
|
||||
|
||||
func (r *UserRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.User{}, id).Error
|
||||
}
|
||||
|
||||
// NodeRepository 节点仓库
|
||||
type NodeRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewNodeRepository(db *gorm.DB) *NodeRepository {
|
||||
return &NodeRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *NodeRepository) Create(node *models.Node) error {
|
||||
return r.db.Create(node).Error
|
||||
}
|
||||
|
||||
func (r *NodeRepository) FindByNodeID(nodeID string) (*models.Node, error) {
|
||||
var node models.Node
|
||||
err := r.db.Where("node_id = ?", nodeID).First(&node).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &node, nil
|
||||
}
|
||||
|
||||
func (r *NodeRepository) FindByID(id uint) (*models.Node, error) {
|
||||
var node models.Node
|
||||
err := r.db.Preload("UnlockStatuses").First(&node, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &node, nil
|
||||
}
|
||||
|
||||
func (r *NodeRepository) Update(node *models.Node) error {
|
||||
return r.db.Save(node).Error
|
||||
}
|
||||
|
||||
func (r *NodeRepository) UpdateStatus(nodeID string, status string, warpStatus string) error {
|
||||
return r.db.Model(&models.Node{}).
|
||||
Where("node_id = ?", nodeID).
|
||||
Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"warp_status": warpStatus,
|
||||
"last_heartbeat": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *NodeRepository) UpdateIP(nodeID string, newIP string, ipRegion string) error {
|
||||
return r.db.Model(&models.Node{}).
|
||||
Where("node_id = ?", nodeID).
|
||||
Updates(map[string]interface{}{
|
||||
"current_ip": newIP,
|
||||
"ip_region": ipRegion,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *NodeRepository) UpdateConnections(nodeID string, connections int) error {
|
||||
return r.db.Model(&models.Node{}).
|
||||
Where("node_id = ?", nodeID).
|
||||
Update("current_connections", connections).Error
|
||||
}
|
||||
|
||||
func (r *NodeRepository) List() ([]models.Node, error) {
|
||||
var nodes []models.Node
|
||||
err := r.db.Preload("UnlockStatuses").Find(&nodes).Error
|
||||
return nodes, err
|
||||
}
|
||||
|
||||
func (r *NodeRepository) ListOnline() ([]models.Node, error) {
|
||||
var nodes []models.Node
|
||||
err := r.db.Where("status = ?", "online").
|
||||
Preload("UnlockStatuses").
|
||||
Find(&nodes).Error
|
||||
return nodes, err
|
||||
}
|
||||
|
||||
func (r *NodeRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.Node{}, id).Error
|
||||
}
|
||||
|
||||
// UnlockStatusRepository 解锁状态仓库
|
||||
type UnlockStatusRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUnlockStatusRepository(db *gorm.DB) *UnlockStatusRepository {
|
||||
return &UnlockStatusRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *UnlockStatusRepository) Upsert(nodeID uint, service string, unlocked bool, region string) error {
|
||||
return r.db.Exec(`
|
||||
INSERT INTO unlock_statuses (node_id, service, unlocked, region, detected_at)
|
||||
VALUES (?, ?, ?, ?, NOW())
|
||||
ON CONFLICT (node_id, service)
|
||||
DO UPDATE SET unlocked = EXCLUDED.unlocked, region = EXCLUDED.region, detected_at = NOW()
|
||||
`, nodeID, service, unlocked, region).Error
|
||||
}
|
||||
|
||||
func (r *UnlockStatusRepository) FindByNodeID(nodeID uint) ([]models.UnlockStatus, error) {
|
||||
var statuses []models.UnlockStatus
|
||||
err := r.db.Where("node_id = ?", nodeID).Find(&statuses).Error
|
||||
return statuses, err
|
||||
}
|
||||
|
||||
// IPChangeLogRepository IP 变更日志仓库
|
||||
type IPChangeLogRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewIPChangeLogRepository(db *gorm.DB) *IPChangeLogRepository {
|
||||
return &IPChangeLogRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *IPChangeLogRepository) Create(log *models.IPChangeLog) error {
|
||||
return r.db.Create(log).Error
|
||||
}
|
||||
|
||||
func (r *IPChangeLogRepository) FindByNodeID(nodeID uint, limit int) ([]models.IPChangeLog, error) {
|
||||
var logs []models.IPChangeLog
|
||||
err := r.db.Where("node_id = ?", nodeID).
|
||||
Order("created_at DESC").
|
||||
Limit(limit).
|
||||
Find(&logs).Error
|
||||
return logs, err
|
||||
}
|
||||
|
||||
// ConnectionLogRepository 连接日志仓库
|
||||
type ConnectionLogRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewConnectionLogRepository(db *gorm.DB) *ConnectionLogRepository {
|
||||
return &ConnectionLogRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *ConnectionLogRepository) Create(log *models.ConnectionLog) error {
|
||||
return r.db.Create(log).Error
|
||||
}
|
||||
|
||||
func (r *ConnectionLogRepository) GetStatsByUser(userID uint, startTime, endTime time.Time) (map[string]interface{}, error) {
|
||||
var result struct {
|
||||
TotalBytes int64
|
||||
TotalSeconds int
|
||||
Connections int64
|
||||
}
|
||||
|
||||
err := r.db.Model(&models.ConnectionLog{}).
|
||||
Where("user_id = ? AND created_at BETWEEN ? AND ?", userID, startTime, endTime).
|
||||
Select("SUM(bytes_in + bytes_out) as total_bytes, SUM(duration) as total_seconds, COUNT(*) as connections").
|
||||
Scan(&result).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_bytes": result.TotalBytes,
|
||||
"total_seconds": result.TotalSeconds,
|
||||
"connections": result.Connections,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IPRefreshRuleRepository IP 刷新规则仓库
|
||||
type IPRefreshRuleRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewIPRefreshRuleRepository(db *gorm.DB) *IPRefreshRuleRepository {
|
||||
return &IPRefreshRuleRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *IPRefreshRuleRepository) Create(rule *models.IPRefreshRule) error {
|
||||
return r.db.Create(rule).Error
|
||||
}
|
||||
|
||||
func (r *IPRefreshRuleRepository) FindByID(id uint) (*models.IPRefreshRule, error) {
|
||||
var rule models.IPRefreshRule
|
||||
err := r.db.First(&rule, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rule, nil
|
||||
}
|
||||
|
||||
func (r *IPRefreshRuleRepository) List() ([]models.IPRefreshRule, error) {
|
||||
var rules []models.IPRefreshRule
|
||||
err := r.db.Find(&rules).Error
|
||||
return rules, err
|
||||
}
|
||||
|
||||
func (r *IPRefreshRuleRepository) Update(rule *models.IPRefreshRule) error {
|
||||
return r.db.Save(rule).Error
|
||||
}
|
||||
|
||||
func (r *IPRefreshRuleRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.IPRefreshRule{}, id).Error
|
||||
}
|
||||
|
||||
// Repositories 仓库集合
|
||||
type Repositories struct {
|
||||
User *UserRepository
|
||||
Node *NodeRepository
|
||||
UnlockStatus *UnlockStatusRepository
|
||||
IPChangeLog *IPChangeLogRepository
|
||||
ConnectionLog *ConnectionLogRepository
|
||||
IPRefreshRule *IPRefreshRuleRepository
|
||||
}
|
||||
|
||||
func NewRepositories(db *gorm.DB) *Repositories {
|
||||
return &Repositories{
|
||||
User: NewUserRepository(db),
|
||||
Node: NewNodeRepository(db),
|
||||
UnlockStatus: NewUnlockStatusRepository(db),
|
||||
IPChangeLog: NewIPChangeLogRepository(db),
|
||||
ConnectionLog: NewConnectionLogRepository(db),
|
||||
IPRefreshRule: NewIPRefreshRuleRepository(db),
|
||||
}
|
||||
}
|
||||
|
||||
// HealthChecker 健康检查接口
|
||||
type HealthChecker interface {
|
||||
Ping(ctx context.Context) error
|
||||
}
|
||||
|
||||
func (r *UserRepository) Ping(ctx context.Context) error {
|
||||
return r.db.WithContext(ctx).Raw("SELECT 1").Error
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"proxy-platform/internal/models"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoAvailableNode = errors.New("没有可用的节点")
|
||||
)
|
||||
|
||||
// Strategy 负载均衡策略
|
||||
type Strategy string
|
||||
|
||||
const (
|
||||
StrategyLeastLatency Strategy = "least_latency"
|
||||
StrategyLeastConnections Strategy = "least_connections"
|
||||
StrategyWeightedRoundRobin Strategy = "weighted_round_robin"
|
||||
StrategyRandom Strategy = "random"
|
||||
)
|
||||
|
||||
// Selector 节点选择器
|
||||
type Selector struct {
|
||||
repo NodeRepository
|
||||
cache NodeCache
|
||||
logger *zap.Logger
|
||||
strategy Strategy
|
||||
randPool *sync.Pool
|
||||
mu sync.RWMutex
|
||||
rrIndex int
|
||||
}
|
||||
|
||||
// NodeRepository 节点数据访问接口
|
||||
type NodeRepository interface {
|
||||
ListOnline() ([]models.Node, error)
|
||||
FindByNodeID(nodeID string) (*models.Node, error)
|
||||
UpdateConnections(nodeID string, connections int) error
|
||||
}
|
||||
|
||||
// NodeCache 节点缓存接口
|
||||
type NodeCache interface {
|
||||
GetStats(nodeID string) (*models.NodeStats, bool)
|
||||
SetStats(nodeID string, stats *models.NodeStats)
|
||||
GetAllStats() map[string]*models.NodeStats
|
||||
}
|
||||
|
||||
// NewSelector 创建节点选择器
|
||||
func NewSelector(repo NodeRepository, cache NodeCache, strategy Strategy, logger *zap.Logger) *Selector {
|
||||
return &Selector{
|
||||
repo: repo,
|
||||
cache: cache,
|
||||
logger: logger,
|
||||
strategy: strategy,
|
||||
randPool: &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Select 选择最优节点
|
||||
func (s *Selector) Select(ctx context.Context, targetHost string, targetPort int, requiredServices []string) (*models.Node, error) {
|
||||
// 1. 获取在线节点列表
|
||||
nodes, err := s.repo.ListOnline()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(nodes) == 0 {
|
||||
return nil, ErrNoAvailableNode
|
||||
}
|
||||
|
||||
// 2. 过滤满足解锁需求的节点
|
||||
if len(requiredServices) > 0 {
|
||||
nodes = s.filterByUnlockStatus(nodes, requiredServices)
|
||||
if len(nodes) == 0 {
|
||||
return nil, ErrNoAvailableNode
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 过滤未达到连接上限的节点
|
||||
nodes = s.filterByConnectionLimit(nodes)
|
||||
if len(nodes) == 0 {
|
||||
return nil, ErrNoAvailableNode
|
||||
}
|
||||
|
||||
// 4. 根据策略选择节点
|
||||
var selected *models.Node
|
||||
switch s.strategy {
|
||||
case StrategyLeastLatency:
|
||||
selected = s.selectLeastLatency(nodes)
|
||||
case StrategyLeastConnections:
|
||||
selected = s.selectLeastConnections(nodes)
|
||||
case StrategyWeightedRoundRobin:
|
||||
selected = s.selectWeightedRoundRobin(nodes)
|
||||
case StrategyRandom:
|
||||
selected = s.selectRandom(nodes)
|
||||
default:
|
||||
selected = s.selectLeastLatency(nodes)
|
||||
}
|
||||
|
||||
return selected, nil
|
||||
}
|
||||
|
||||
// filterByUnlockStatus 过滤满足解锁需求的节点
|
||||
func (s *Selector) filterByUnlockStatus(nodes []models.Node, services []string) []models.Node {
|
||||
var result []models.Node
|
||||
|
||||
for _, node := range nodes {
|
||||
// 构建节点的解锁服务映射
|
||||
unlockMap := make(map[string]bool)
|
||||
for _, status := range node.UnlockStatuses {
|
||||
unlockMap[status.Service] = status.Unlocked
|
||||
}
|
||||
|
||||
// 检查是否满足所有需求
|
||||
allMatched := true
|
||||
for _, service := range services {
|
||||
if !unlockMap[service] {
|
||||
allMatched = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allMatched {
|
||||
result = append(result, node)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// filterByConnectionLimit 过滤未达到连接上限的节点
|
||||
func (s *Selector) filterByConnectionLimit(nodes []models.Node) []models.Node {
|
||||
var result []models.Node
|
||||
|
||||
for _, node := range nodes {
|
||||
stats, ok := s.cache.GetStats(node.NodeID)
|
||||
if !ok {
|
||||
// 没有缓存数据,使用数据库中的连接数
|
||||
if node.CurrentConnections < node.MaxConnections {
|
||||
result = append(result, node)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if stats.Connections < node.MaxConnections {
|
||||
result = append(result, node)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// selectLeastLatency 选择延迟最低的节点
|
||||
func (s *Selector) selectLeastLatency(nodes []models.Node) *models.Node {
|
||||
var selected *models.Node
|
||||
minLatency := time.Duration(1<<63 - 1)
|
||||
|
||||
for i := range nodes {
|
||||
stats, ok := s.cache.GetStats(nodes[i].NodeID)
|
||||
if ok {
|
||||
latency := time.Duration(stats.CPUUsage * 100) // 简化:用 CPU 使用率模拟延迟
|
||||
if latency < minLatency {
|
||||
minLatency = latency
|
||||
selected = &nodes[i]
|
||||
}
|
||||
} else {
|
||||
// 没有缓存数据,使用权重作为候选
|
||||
if selected == nil || nodes[i].Weight > selected.Weight {
|
||||
selected = &nodes[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selected
|
||||
}
|
||||
|
||||
// selectLeastConnections 选择连接数最少的节点
|
||||
func (s *Selector) selectLeastConnections(nodes []models.Node) *models.Node {
|
||||
var selected *models.Node
|
||||
minConnections := int(^uint(0) >> 1) // Max int
|
||||
|
||||
for i := range nodes {
|
||||
stats, ok := s.cache.GetStats(nodes[i].NodeID)
|
||||
connCount := nodes[i].CurrentConnections
|
||||
if ok {
|
||||
connCount = stats.Connections
|
||||
}
|
||||
|
||||
if connCount < minConnections {
|
||||
minConnections = connCount
|
||||
selected = &nodes[i]
|
||||
}
|
||||
}
|
||||
|
||||
return selected
|
||||
}
|
||||
|
||||
// selectWeightedRoundRobin 加权轮询选择
|
||||
func (s *Selector) selectWeightedRoundRobin(nodes []models.Node) *models.Node {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// 计算总权重
|
||||
totalWeight := 0
|
||||
for _, node := range nodes {
|
||||
totalWeight += node.Weight
|
||||
}
|
||||
|
||||
if totalWeight == 0 {
|
||||
return &nodes[0]
|
||||
}
|
||||
|
||||
// 轮询选择
|
||||
s.rrIndex = (s.rrIndex + 1) % totalWeight
|
||||
|
||||
currentWeight := 0
|
||||
for i := range nodes {
|
||||
currentWeight += nodes[i].Weight
|
||||
if s.rrIndex < currentWeight {
|
||||
return &nodes[i]
|
||||
}
|
||||
}
|
||||
|
||||
return &nodes[0]
|
||||
}
|
||||
|
||||
// selectRandom 随机选择
|
||||
func (s *Selector) selectRandom(nodes []models.Node) *models.Node {
|
||||
r := s.randPool.Get().(*rand.Rand)
|
||||
defer s.randPool.Put(r)
|
||||
|
||||
idx := r.Intn(len(nodes))
|
||||
return &nodes[idx]
|
||||
}
|
||||
|
||||
// HealthChecker 健康检查器
|
||||
type HealthChecker struct {
|
||||
repo NodeRepository
|
||||
cache NodeCache
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewHealthChecker(repo NodeRepository, cache NodeCache, logger *zap.Logger) *HealthChecker {
|
||||
return &HealthChecker{
|
||||
repo: repo,
|
||||
cache: cache,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Check 检查节点健康状态
|
||||
func (h *HealthChecker) Check(ctx context.Context, node *models.Node) error {
|
||||
stats, ok := h.cache.GetStats(node.NodeID)
|
||||
if !ok {
|
||||
return errors.New("节点未上报状态")
|
||||
}
|
||||
|
||||
// 检查是否超时
|
||||
if time.Since(stats.LastUpdate) > 30*time.Second {
|
||||
return errors.New("节点心跳超时")
|
||||
}
|
||||
|
||||
// 检查 WARP 状态
|
||||
if stats.Connections < 0 {
|
||||
return errors.New("节点状态异常")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RuleEngine 规则引擎
|
||||
type RuleEngine struct {
|
||||
rules []IPRefreshRule
|
||||
actions map[string]ActionFunc
|
||||
mu sync.RWMutex
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
type IPRefreshRule struct {
|
||||
ID uint
|
||||
NodeGroupID *uint
|
||||
TriggerType string // unlock_failure, usage_count, usage_traffic, scheduled, anomaly
|
||||
TriggerValue map[string]interface{}
|
||||
Cooldown time.Duration
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
type ActionFunc func(ctx context.Context, node *models.Node, reason string) error
|
||||
|
||||
func NewRuleEngine(logger *zap.Logger) *RuleEngine {
|
||||
return &RuleEngine{
|
||||
rules: make([]IPRefreshRule, 0),
|
||||
actions: make(map[string]ActionFunc),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterAction 注册动作
|
||||
func (e *RuleEngine) RegisterAction(name string, action ActionFunc) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
e.actions[name] = action
|
||||
}
|
||||
|
||||
// AddRule 添加规则
|
||||
func (e *RuleEngine) AddRule(rule IPRefreshRule) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
e.rules = append(e.rules, rule)
|
||||
}
|
||||
|
||||
// Evaluate 评估规则
|
||||
func (e *RuleEngine) Evaluate(ctx context.Context, node *models.Node, event string, data map[string]interface{}) error {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
|
||||
for _, rule := range e.rules {
|
||||
if !rule.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if rule.TriggerType != event {
|
||||
continue
|
||||
}
|
||||
|
||||
// 触发规则
|
||||
e.logger.Info("规则触发",
|
||||
zap.Uint("rule_id", rule.ID),
|
||||
zap.String("trigger", event),
|
||||
zap.String("node_id", node.NodeID),
|
||||
)
|
||||
|
||||
// 执行动作
|
||||
if action, ok := e.actions["refresh_ip"]; ok {
|
||||
return action(ctx, node, event)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
package socks5
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnsupportedVersion = errors.New("unsupported SOCKS version")
|
||||
ErrUnsupportedMethod = errors.New("unsupported authentication method")
|
||||
ErrAuthenticationFailed = errors.New("authentication failed")
|
||||
ErrUnsupportedCommand = errors.New("unsupported command")
|
||||
ErrUnsupportedAddrType = errors.New("unsupported address type")
|
||||
)
|
||||
|
||||
const (
|
||||
SOCKS5Version = 0x05
|
||||
NoAuth = 0x00
|
||||
UserPassAuth = 0x02
|
||||
NoAcceptable = 0xFF
|
||||
|
||||
ConnectCommand = 0x01
|
||||
BindCommand = 0x02
|
||||
AssociateCommand = 0x03
|
||||
|
||||
IPv4Address = 0x01
|
||||
FQDNAddress = 0x03
|
||||
IPv6Address = 0x04
|
||||
)
|
||||
|
||||
// Authenticator 认证接口
|
||||
type Authenticator interface {
|
||||
Authenticate(username, password string) (uint, bool)
|
||||
}
|
||||
|
||||
// BackendSelector 后端节点选择器
|
||||
type BackendSelector interface {
|
||||
SelectBackend(ctx context.Context, targetHost string, targetPort int, services []string) (string, int, error)
|
||||
ReleaseBackend(host string, port int, bytesIn, bytesOut int64)
|
||||
}
|
||||
|
||||
// Server SOCKS5 服务器
|
||||
type Server struct {
|
||||
host string
|
||||
port int
|
||||
maxConnections int
|
||||
timeout time.Duration
|
||||
auth Authenticator
|
||||
selector BackendSelector
|
||||
logger *zap.Logger
|
||||
connections int64
|
||||
connMutex sync.Mutex
|
||||
connSem chan struct{}
|
||||
}
|
||||
|
||||
// NewServer 创建 SOCKS5 服务器
|
||||
func NewServer(host string, port int, maxConnections int, timeout int, auth Authenticator, selector BackendSelector, logger *zap.Logger) *Server {
|
||||
return &Server{
|
||||
host: host,
|
||||
port: port,
|
||||
maxConnections: maxConnections,
|
||||
timeout: time.Duration(timeout) * time.Second,
|
||||
auth: auth,
|
||||
selector: selector,
|
||||
logger: logger,
|
||||
connSem: make(chan struct{}, maxConnections),
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务器
|
||||
func (s *Server) Start(ctx context.Context) error {
|
||||
addr := fmt.Sprintf("%s:%d", s.host, s.port)
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("监听失败: %w", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
s.logger.Info("SOCKS5 服务器启动", zap.String("addr", addr), zap.Int("max_connections", s.maxConnections))
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
s.logger.Error("接受连接失败", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 连接数限制
|
||||
select {
|
||||
case s.connSem <- struct{}{}:
|
||||
go s.handleConnection(ctx, conn)
|
||||
default:
|
||||
s.logger.Warn("达到最大连接数限制", zap.Int64("connections", s.connections))
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleConnection 处理单个连接
|
||||
func (s *Server) handleConnection(ctx context.Context, clientConn net.Conn) {
|
||||
defer func() {
|
||||
clientConn.Close()
|
||||
<-s.connSem
|
||||
s.connMutex.Lock()
|
||||
s.connections--
|
||||
s.connMutex.Unlock()
|
||||
}()
|
||||
|
||||
s.connMutex.Lock()
|
||||
s.connections++
|
||||
s.connMutex.Unlock()
|
||||
|
||||
// 设置超时
|
||||
clientConn.SetDeadline(time.Now().Add(s.timeout))
|
||||
|
||||
// 1. 协议握手
|
||||
username, userID, err := s.handshake(clientConn)
|
||||
if err != nil {
|
||||
s.logger.Error("握手失败", zap.Error(err), zap.String("client", clientConn.RemoteAddr().String()))
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 读取请求
|
||||
targetHost, targetPort, err := s.readRequest(clientConn)
|
||||
if err != nil {
|
||||
s.logger.Error("读取请求失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Info("连接请求",
|
||||
zap.String("username", username),
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("target", fmt.Sprintf("%s:%d", targetHost, targetPort)),
|
||||
)
|
||||
|
||||
// 3. 选择后端节点
|
||||
backendHost, backendPort, err := s.selector.SelectBackend(ctx, targetHost, targetPort, nil)
|
||||
if err != nil {
|
||||
s.logger.Error("选择节点失败", zap.Error(err))
|
||||
s.sendReply(clientConn, 0x04, net.IPv4zero, 0) // Host unreachable
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 连接后端
|
||||
backendAddr := fmt.Sprintf("%s:%d", backendHost, backendPort)
|
||||
backendConn, err := net.DialTimeout("tcp", backendAddr, s.timeout)
|
||||
if err != nil {
|
||||
s.logger.Error("连接后端失败", zap.Error(err), zap.String("backend", backendAddr))
|
||||
s.sendReply(clientConn, 0x04, net.IPv4zero, 0)
|
||||
return
|
||||
}
|
||||
defer backendConn.Close()
|
||||
|
||||
// 5. 发送成功响应
|
||||
s.sendReply(clientConn, 0x00, net.IPv4zero, 0)
|
||||
|
||||
// 6. 数据转发
|
||||
bytesIn, bytesOut := s.relay(clientConn, backendConn)
|
||||
|
||||
// 7. 释放后端节点
|
||||
s.selector.ReleaseBackend(backendHost, backendPort, bytesIn, bytesOut)
|
||||
|
||||
s.logger.Info("连接结束",
|
||||
zap.String("username", username),
|
||||
zap.Int64("bytes_in", bytesIn),
|
||||
zap.Int64("bytes_out", bytesOut),
|
||||
)
|
||||
}
|
||||
|
||||
// handshake 协议握手
|
||||
func (s *Server) handshake(conn net.Conn) (string, uint, error) {
|
||||
// 读取客户端 hello
|
||||
buf := make([]byte, 2)
|
||||
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
if buf[0] != SOCKS5Version {
|
||||
return "", 0, ErrUnsupportedVersion
|
||||
}
|
||||
|
||||
nMethods := int(buf[1])
|
||||
methods := make([]byte, nMethods)
|
||||
if _, err := io.ReadFull(conn, methods); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
// 检查是否支持用户密码认证
|
||||
supportUserPass := false
|
||||
for _, m := range methods {
|
||||
if m == UserPassAuth {
|
||||
supportUserPass = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 发送选择的认证方法
|
||||
if supportUserPass {
|
||||
conn.Write([]byte{SOCKS5Version, UserPassAuth})
|
||||
} else {
|
||||
conn.Write([]byte{SOCKS5Version, NoAuth})
|
||||
return "", 0, nil // 无认证
|
||||
}
|
||||
|
||||
// 用户密码认证
|
||||
authBuf := make([]byte, 2)
|
||||
if _, err := io.ReadFull(conn, authBuf); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
ulen := int(authBuf[1])
|
||||
usernameBuf := make([]byte, ulen)
|
||||
if _, err := io.ReadFull(conn, usernameBuf); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
plen := int(authBuf[2])
|
||||
passwordBuf := make([]byte, plen)
|
||||
if _, err := io.ReadFull(conn, passwordBuf); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
username := string(usernameBuf)
|
||||
password := string(passwordBuf)
|
||||
|
||||
// 认证
|
||||
userID, ok := s.auth.Authenticate(username, password)
|
||||
if !ok {
|
||||
conn.Write([]byte{0x01, 0x01}) // 认证失败
|
||||
return "", 0, ErrAuthenticationFailed
|
||||
}
|
||||
|
||||
conn.Write([]byte{0x01, 0x00}) // 认证成功
|
||||
return username, userID, nil
|
||||
}
|
||||
|
||||
// readRequest 读取请求
|
||||
func (s *Server) readRequest(conn net.Conn) (string, int, error) {
|
||||
buf := make([]byte, 4)
|
||||
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
if buf[0] != SOCKS5Version {
|
||||
return "", 0, ErrUnsupportedVersion
|
||||
}
|
||||
|
||||
if buf[1] != ConnectCommand {
|
||||
return "", 0, ErrUnsupportedCommand
|
||||
}
|
||||
|
||||
// 读取目标地址
|
||||
var host string
|
||||
var port int
|
||||
|
||||
switch buf[3] {
|
||||
case IPv4Address:
|
||||
addr := make([]byte, 4)
|
||||
if _, err := io.ReadFull(conn, addr); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
host = net.IP(addr).String()
|
||||
|
||||
case FQDNAddress:
|
||||
lenBuf := make([]byte, 1)
|
||||
if _, err := io.ReadFull(conn, lenBuf); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
fqdn := make([]byte, lenBuf[0])
|
||||
if _, err := io.ReadFull(conn, fqdn); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
host = string(fqdn)
|
||||
|
||||
case IPv6Address:
|
||||
addr := make([]byte, 16)
|
||||
if _, err := io.ReadFull(conn, addr); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
host = net.IP(addr).String()
|
||||
|
||||
default:
|
||||
return "", 0, ErrUnsupportedAddrType
|
||||
}
|
||||
|
||||
// 读取端口
|
||||
portBuf := make([]byte, 2)
|
||||
if _, err := io.ReadFull(conn, portBuf); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
port = int(binary.BigEndian.Uint16(portBuf))
|
||||
|
||||
return host, port, nil
|
||||
}
|
||||
|
||||
// sendReply 发送响应
|
||||
func (s *Server) sendReply(conn net.Conn, status byte, ip net.IP, port int) {
|
||||
reply := []byte{
|
||||
SOCKS5Version,
|
||||
status,
|
||||
0x00, // RSV
|
||||
IPv4Address,
|
||||
}
|
||||
reply = append(reply, ip.To4()...)
|
||||
reply = append(reply, []byte{byte(port >> 8), byte(port)}...)
|
||||
conn.Write(reply)
|
||||
}
|
||||
|
||||
// relay 数据转发
|
||||
func (s *Server) relay(client, backend net.Conn) (int64, int64) {
|
||||
var bytesIn, bytesOut int64
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
// 客户端 -> 后端
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
n, _ := io.Copy(backend, client)
|
||||
bytesOut = n
|
||||
}()
|
||||
|
||||
// 后端 -> 客户端
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
n, _ := io.Copy(client, backend)
|
||||
bytesIn = n
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
return bytesIn, bytesOut
|
||||
}
|
||||
|
||||
// GetConnections 获取当前连接数
|
||||
func (s *Server) GetConnections() int64 {
|
||||
s.connMutex.Lock()
|
||||
defer s.connMutex.Unlock()
|
||||
return s.connections
|
||||
}
|
||||
|
||||
// Dialer SOCKS5 客户端连接器
|
||||
type Dialer struct {
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// NewDialer 创建连接器
|
||||
func NewDialer(host string, port int, username, password string, timeout time.Duration) *Dialer {
|
||||
return &Dialer{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Username: username,
|
||||
Password: password,
|
||||
Timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// Dial 通过 SOCKS5 代理连接目标
|
||||
func (d *Dialer) Dial(targetHost string, targetPort int) (net.Conn, error) {
|
||||
proxyAddr := net.JoinHostPort(d.Host, strconv.Itoa(d.Port))
|
||||
conn, err := net.DialTimeout("tcp", proxyAddr, d.Timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 发送 hello
|
||||
hello := []byte{SOCKS5Version, 2, NoAuth, UserPassAuth}
|
||||
if _, err := conn.Write(hello); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
resp := make([]byte, 2)
|
||||
if _, err := io.ReadFull(conn, resp); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp[0] != SOCKS5Version {
|
||||
conn.Close()
|
||||
return nil, ErrUnsupportedVersion
|
||||
}
|
||||
|
||||
// 认证
|
||||
if resp[1] == UserPassAuth {
|
||||
auth := []byte{0x01, byte(len(d.Username))}
|
||||
auth = append(auth, []byte(d.Username)...)
|
||||
auth = append(auth, byte(len(d.Password)))
|
||||
auth = append(auth, []byte(d.Password)...)
|
||||
if _, err := conn.Write(auth); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authResp := make([]byte, 2)
|
||||
if _, err := io.ReadFull(conn, authResp); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if authResp[1] != 0x00 {
|
||||
conn.Close()
|
||||
return nil, ErrAuthenticationFailed
|
||||
}
|
||||
}
|
||||
|
||||
// 发送连接请求
|
||||
req := []byte{SOCKS5Version, ConnectCommand, 0x00, FQDNAddress, byte(len(targetHost))}
|
||||
req = append(req, []byte(targetHost)...)
|
||||
req = append(req, []byte{byte(targetPort >> 8), byte(targetPort)}...)
|
||||
if _, err := conn.Write(req); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
reply := make([]byte, 10)
|
||||
if _, err := io.ReadFull(conn, reply); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if reply[1] != 0x00 {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("SOCKS5 连接失败: status %d", reply[1])
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
package unlock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ServiceConfig 服务检测配置
|
||||
type ServiceConfig struct {
|
||||
Name string
|
||||
URL string
|
||||
SuccessKeywords []string
|
||||
FailKeywords []string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// Result 解锁检测结果
|
||||
type Result struct {
|
||||
Service string `json:"service"`
|
||||
Unlocked bool `json:"unlocked"`
|
||||
Region string `json:"region"`
|
||||
Error string `json:"error,omitempty"`
|
||||
CheckedAt time.Time `json:"checked_at"`
|
||||
}
|
||||
|
||||
// Detector 解锁检测器
|
||||
type Detector struct {
|
||||
proxyHost string
|
||||
proxyPort int
|
||||
timeout time.Duration
|
||||
logger *zap.Logger
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewDetector 创建解锁检测器
|
||||
func NewDetector(proxyHost string, proxyPort int, timeout int, logger *zap.Logger) *Detector {
|
||||
// 创建通过 SOCKS5 代理的 HTTP 客户端
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
// 通过 SOCKS5 代理连接
|
||||
return dialSocks5(ctx, proxyHost, proxyPort, addr, timeout)
|
||||
},
|
||||
},
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
}
|
||||
|
||||
return &Detector{
|
||||
proxyHost: proxyHost,
|
||||
proxyPort: proxyPort,
|
||||
timeout: time.Duration(timeout) * time.Second,
|
||||
logger: logger,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckService 检测单个服务
|
||||
func (d *Detector) CheckService(ctx context.Context, config ServiceConfig) (*Result, error) {
|
||||
result := &Result{
|
||||
Service: config.Name,
|
||||
CheckedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", config.URL, nil)
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
return result, err
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
|
||||
|
||||
resp, err := d.client.Do(req)
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
d.logger.Error("请求失败", zap.String("service", config.Name), zap.Error(err))
|
||||
return result, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
return result, err
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
|
||||
// 检查失败关键词
|
||||
for _, keyword := range config.FailKeywords {
|
||||
if strings.Contains(bodyStr, keyword) {
|
||||
result.Unlocked = false
|
||||
result.Error = fmt.Sprintf("检测到失败关键词: %s", keyword)
|
||||
d.logger.Info("解锁检测失败",
|
||||
zap.String("service", config.Name),
|
||||
zap.String("keyword", keyword),
|
||||
)
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 检查成功关键词
|
||||
for _, keyword := range config.SuccessKeywords {
|
||||
if strings.Contains(bodyStr, keyword) {
|
||||
result.Unlocked = true
|
||||
// 尝试提取区域信息
|
||||
result.Region = extractRegion(config.Name, bodyStr)
|
||||
d.logger.Info("解锁检测成功",
|
||||
zap.String("service", config.Name),
|
||||
zap.String("region", result.Region),
|
||||
)
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有匹配任何关键词,根据 HTTP 状态码判断
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 400 {
|
||||
result.Unlocked = true
|
||||
result.Region = "??"
|
||||
} else {
|
||||
result.Unlocked = false
|
||||
result.Error = fmt.Sprintf("HTTP 状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CheckAll 检测所有服务
|
||||
func (d *Detector) CheckAll(ctx context.Context, configs []ServiceConfig) map[string]*Result {
|
||||
results := make(map[string]*Result)
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
|
||||
for _, config := range configs {
|
||||
wg.Add(1)
|
||||
go func(cfg ServiceConfig) {
|
||||
defer wg.Done()
|
||||
|
||||
result, err := d.CheckService(ctx, cfg)
|
||||
if err != nil {
|
||||
d.logger.Error("检测服务失败", zap.String("service", cfg.Name), zap.Error(err))
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
results[cfg.Name] = result
|
||||
mu.Unlock()
|
||||
}(config)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
// dialSocks5 通过 SOCKS5 代理连接
|
||||
func dialSocks5(ctx context.Context, proxyHost string, proxyPort int, target string, timeout int) (net.Conn, error) {
|
||||
// 连接到代理服务器
|
||||
proxyAddr := fmt.Sprintf("%s:%d", proxyHost, proxyPort)
|
||||
conn, err := net.DialTimeout("tcp", proxyAddr, time.Duration(timeout)*time.Second)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("连接代理失败: %w", err)
|
||||
}
|
||||
|
||||
// SOCKS5 握手
|
||||
// 发送认证方法
|
||||
_, err = conn.Write([]byte{0x05, 0x01, 0x00}) // 无认证
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
buf := make([]byte, 2)
|
||||
_, err = io.ReadFull(conn, buf)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if buf[0] != 0x05 {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("不支持的 SOCKS 版本: %d", buf[0])
|
||||
}
|
||||
|
||||
// 解析目标地址
|
||||
host, port, err := net.SplitHostPort(target)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建连接请求
|
||||
req := []byte{0x05, 0x01, 0x00} // CONNECT
|
||||
|
||||
// 判断是 IP 还是域名
|
||||
ip := net.ParseIP(host)
|
||||
if ip != nil {
|
||||
if ip.To4() != nil {
|
||||
req = append(req, 0x01) // IPv4
|
||||
req = append(req, ip.To4()...)
|
||||
} else {
|
||||
req = append(req, 0x04) // IPv6
|
||||
req = append(req, ip.To16()...)
|
||||
}
|
||||
} else {
|
||||
req = append(req, 0x03) // 域名
|
||||
req = append(req, byte(len(host)))
|
||||
req = append(req, []byte(host)...)
|
||||
}
|
||||
|
||||
// 添加端口
|
||||
portNum := 0
|
||||
fmt.Sscanf(port, "%d", &portNum)
|
||||
req = append(req, byte(portNum>>8), byte(portNum&0xFF))
|
||||
|
||||
// 发送请求
|
||||
_, err = conn.Write(req)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
resp := make([]byte, 10)
|
||||
_, err = io.ReadFull(conn, resp)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp[1] != 0x00 {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("SOCKS5 连接失败: status %d", resp[1])
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// extractRegion 提取区域信息
|
||||
func extractRegion(service string, body string) string {
|
||||
// 根据不同服务提取区域
|
||||
switch service {
|
||||
case "netflix":
|
||||
// Netflix 通常在页面中包含区域信息
|
||||
if strings.Contains(body, "US") {
|
||||
return "US"
|
||||
} else if strings.Contains(body, "UK") {
|
||||
return "UK"
|
||||
} else if strings.Contains(body, "JP") {
|
||||
return "JP"
|
||||
}
|
||||
case "youtube":
|
||||
// YouTube 可能包含区域设置
|
||||
if strings.Contains(body, "\"gl\":\"US\"") {
|
||||
return "US"
|
||||
}
|
||||
}
|
||||
|
||||
// 默认返回未知
|
||||
return "??"
|
||||
}
|
||||
|
||||
// DefaultServices 默认服务配置
|
||||
var DefaultServices = []ServiceConfig{
|
||||
{
|
||||
Name: "gpt",
|
||||
URL: "https://chat.openai.com/",
|
||||
SuccessKeywords: []string{"challenges", "signup", "login"},
|
||||
FailKeywords: []string{"Access denied", "unavailable", "not available"},
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
{
|
||||
Name: "netflix",
|
||||
URL: "https://www.netflix.com/title/80018499",
|
||||
SuccessKeywords: []string{"netflix.com", "watch"},
|
||||
FailKeywords: []string{"not available", "nflxvideo.net", "error"},
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
{
|
||||
Name: "disney",
|
||||
URL: "https://www.disneyplus.com/",
|
||||
SuccessKeywords: []string{"disneyplus.com", "disney"},
|
||||
FailKeywords: []string{"unavailable", "not available"},
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
{
|
||||
Name: "youtube",
|
||||
URL: "https://www.youtube.com/",
|
||||
SuccessKeywords: []string{"youtube.com", "ytInitialPlayerResponse"},
|
||||
FailKeywords: []string{},
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
{
|
||||
Name: "claude",
|
||||
URL: "https://claude.ai/",
|
||||
SuccessKeywords: []string{"claude.ai", "anthropic"},
|
||||
FailKeywords: []string{"Access denied", "unavailable"},
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
{
|
||||
Name: "gemini",
|
||||
URL: "https://gemini.google.com/",
|
||||
SuccessKeywords: []string{"gemini", "google"},
|
||||
FailKeywords: []string{"unavailable"},
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package warp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrWARPNotInstalled = errors.New("WARP 未安装")
|
||||
ErrWARPNotConnected = errors.New("WARP 未连接")
|
||||
ErrIPRefreshFailed = errors.New("IP 刷新失败")
|
||||
ErrIPPoolExhausted = errors.New("IP 池耗尽")
|
||||
)
|
||||
|
||||
// WARPStatus WARP 状态
|
||||
type WARPStatus struct {
|
||||
Connected bool
|
||||
AccountID string
|
||||
DeviceID string
|
||||
ClientID string
|
||||
CurrentIP string
|
||||
Country string
|
||||
ConnectTime time.Time
|
||||
}
|
||||
|
||||
// Client WARP 客户端
|
||||
type Client struct {
|
||||
socksPort int
|
||||
refreshCooldown time.Duration
|
||||
maxRefreshRetries int
|
||||
retryDelayMin time.Duration
|
||||
retryDelayMax time.Duration
|
||||
logger *zap.Logger
|
||||
mu sync.Mutex
|
||||
lastRefresh time.Time
|
||||
currentIP string
|
||||
ipHistory []string
|
||||
}
|
||||
|
||||
// NewClient 创建 WARP 客户端
|
||||
func NewClient(socksPort int, refreshCooldown int, maxRefreshRetries int, retryDelayMin int, retryDelayMax int, logger *zap.Logger) *Client {
|
||||
return &Client{
|
||||
socksPort: socksPort,
|
||||
refreshCooldown: time.Duration(refreshCooldown) * time.Second,
|
||||
maxRefreshRetries: maxRefreshRetries,
|
||||
retryDelayMin: time.Duration(retryDelayMin) * time.Second,
|
||||
retryDelayMax: time.Duration(retryDelayMax) * time.Second,
|
||||
logger: logger,
|
||||
ipHistory: make([]string, 0, 100),
|
||||
}
|
||||
}
|
||||
|
||||
// Connect 连接 WARP
|
||||
func (c *Client) Connect(ctx context.Context) error {
|
||||
// 检查是否安装
|
||||
if !c.isInstalled() {
|
||||
return ErrWARPNotInstalled
|
||||
}
|
||||
|
||||
// 连接 WARP
|
||||
cmd := exec.CommandContext(ctx, "warp-cli", "connect")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("WARP 连接失败: %w, output: %s", err, string(output))
|
||||
}
|
||||
|
||||
// 等待连接建立
|
||||
for i := 0; i < 10; i++ {
|
||||
time.Sleep(1 * time.Second)
|
||||
status, err := c.Status(ctx)
|
||||
if err == nil && status.Connected {
|
||||
c.currentIP = status.CurrentIP
|
||||
c.lastRefresh = time.Now()
|
||||
c.logger.Info("WARP 连接成功", zap.String("ip", status.CurrentIP))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return ErrWARPNotConnected
|
||||
}
|
||||
|
||||
// Disconnect 断开 WARP
|
||||
func (c *Client) Disconnect(ctx context.Context) error {
|
||||
cmd := exec.CommandContext(ctx, "warp-cli", "disconnect")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("WARP 断开失败: %w, output: %s", err, string(output))
|
||||
}
|
||||
c.currentIP = ""
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status 获取 WARP 状态
|
||||
func (c *Client) Status(ctx context.Context) (*WARPStatus, error) {
|
||||
cmd := exec.CommandContext(ctx, "warp-cli", "status")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
status := &WARPStatus{}
|
||||
outputStr := string(output)
|
||||
|
||||
// 解析状态
|
||||
if strings.Contains(outputStr, "Status: Connected") {
|
||||
status.Connected = true
|
||||
} else if strings.Contains(outputStr, "Status: Disconnected") {
|
||||
status.Connected = false
|
||||
return status, nil
|
||||
}
|
||||
|
||||
// 获取当前 IP
|
||||
ip, err := c.GetCurrentIP(ctx)
|
||||
if err == nil {
|
||||
status.CurrentIP = ip
|
||||
c.currentIP = ip
|
||||
}
|
||||
|
||||
// 获取国家信息
|
||||
country, _ := c.GetCountry(ctx)
|
||||
status.Country = country
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
// RefreshIP 刷新 IP
|
||||
func (c *Client) RefreshIP(ctx context.Context) (string, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// 检查冷却时间
|
||||
if time.Since(c.lastRefresh) < c.refreshCooldown {
|
||||
waitTime := c.refreshCooldown - time.Since(c.lastRefresh)
|
||||
c.logger.Warn("IP 刷新冷却中", zap.Duration("wait_time", waitTime))
|
||||
return "", fmt.Errorf("冷却中,请等待 %v", waitTime)
|
||||
}
|
||||
|
||||
// 记录旧 IP
|
||||
oldIP := c.currentIP
|
||||
|
||||
// 重试刷新
|
||||
for attempt := 0; attempt < c.maxRefreshRetries; attempt++ {
|
||||
c.logger.Info("尝试刷新 IP",
|
||||
zap.Int("attempt", attempt+1),
|
||||
zap.Int("max_retries", c.maxRefreshRetries),
|
||||
zap.String("old_ip", oldIP),
|
||||
)
|
||||
|
||||
// 断开连接
|
||||
if err := c.Disconnect(ctx); err != nil {
|
||||
c.logger.Error("断开 WARP 失败", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 随机等待
|
||||
delay := c.retryDelayMin + time.Duration(randInt(0, int(c.retryDelayMax-c.retryDelayMin)))
|
||||
time.Sleep(delay)
|
||||
|
||||
// 重新连接
|
||||
if err := c.Connect(ctx); err != nil {
|
||||
c.logger.Error("连接 WARP 失败", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取新 IP
|
||||
newIP, err := c.GetCurrentIP(ctx)
|
||||
if err != nil {
|
||||
c.logger.Error("获取新 IP 失败", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否真的换到了新 IP
|
||||
if newIP != oldIP {
|
||||
c.currentIP = newIP
|
||||
c.lastRefresh = time.Now()
|
||||
c.ipHistory = append(c.ipHistory, newIP)
|
||||
c.logger.Info("IP 刷新成功",
|
||||
zap.String("old_ip", oldIP),
|
||||
zap.String("new_ip", newIP),
|
||||
zap.Int("attempt", attempt+1),
|
||||
)
|
||||
return newIP, nil
|
||||
}
|
||||
|
||||
c.logger.Warn("获取到相同 IP,重试中",
|
||||
zap.String("ip", newIP),
|
||||
)
|
||||
}
|
||||
|
||||
return "", ErrIPPoolExhausted
|
||||
}
|
||||
|
||||
// GetCurrentIP 获取当前 IP
|
||||
func (c *Client) GetCurrentIP(ctx context.Context) (string, error) {
|
||||
// 通过 WARP 出口获取 IP
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(nil), // 使用 WARP 接口
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
// 使用 WARP 的 SOCKS5 代理
|
||||
return net.DialTimeout("tcp", "127.0.0.1:"+strconv.Itoa(c.socksPort), 5*time.Second)
|
||||
},
|
||||
},
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Get("https://cloudflare.com/cdn-cgi/trace")
|
||||
if err != nil {
|
||||
// 回退:使用系统默认出口
|
||||
return c.getIPFromSystem()
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 解析 IP
|
||||
lines := strings.Split(string(body), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "ip=") {
|
||||
return strings.TrimPrefix(line, "ip="), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("无法解析 IP")
|
||||
}
|
||||
|
||||
// getIPFromSystem 从系统获取 IP
|
||||
func (c *Client) getIPFromSystem() (string, error) {
|
||||
resp, err := http.Get("https://cloudflare.com/cdn-cgi/trace")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(body), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "ip=") {
|
||||
return strings.TrimPrefix(line, "ip="), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("无法解析 IP")
|
||||
}
|
||||
|
||||
// GetCountry 获取国家
|
||||
func (c *Client) GetCountry(ctx context.Context) (string, error) {
|
||||
// 使用 WARP 出口
|
||||
resp, err := http.Get("https://cloudflare.com/cdn-cgi/trace")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(body), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "loc=") {
|
||||
return strings.TrimPrefix(line, "loc="), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "??", nil
|
||||
}
|
||||
|
||||
// isInstalled 检查是否安装
|
||||
func (c *Client) isInstalled() bool {
|
||||
_, err := exec.LookPath("warp-cli")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// GetIPHistory 获取 IP 历史
|
||||
func (c *Client) GetIPHistory() []string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
result := make([]string, len(c.ipHistory))
|
||||
copy(result, c.ipHistory)
|
||||
return result
|
||||
}
|
||||
|
||||
// RegisterNewAccount 注册新账号(用于获取新 IP 池)
|
||||
func (c *Client) RegisterNewAccount(ctx context.Context) error {
|
||||
// 删除当前账号
|
||||
cmd := exec.CommandContext(ctx, "warp-cli", "delete")
|
||||
_, _ = cmd.CombinedOutput()
|
||||
|
||||
// 注册新账号
|
||||
cmd = exec.CommandContext(ctx, "warp-cli", "register")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("注册新账号失败: %w, output: %s", err, string(output))
|
||||
}
|
||||
|
||||
c.logger.Info("注册新 WARP 账号成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
// randInt 生成随机整数
|
||||
func randInt(min, max int) int {
|
||||
return min + int(randFloat()*float64(max-min+1))
|
||||
}
|
||||
|
||||
// randFloat 生成随机浮点数
|
||||
func randFloat() float64 {
|
||||
return float64(time.Now().UnixNano()%1000) / 1000.0
|
||||
}
|
||||
|
||||
// ParseWARPAccount 从输出解析账号信息
|
||||
func ParseWARPAccount(output string) (accountID, deviceID, clientID string) {
|
||||
accountRegex := regexp.MustCompile(`Account ID:\s*([a-f0-9-]+)`)
|
||||
deviceRegex := regexp.MustCompile(`Device ID:\s*([a-f0-9-]+)`)
|
||||
clientRegex := regexp.MustCompile(`Client ID:\s*([a-f0-9-]+)`)
|
||||
|
||||
if match := accountRegex.FindStringSubmatch(output); len(match) > 1 {
|
||||
accountID = match[1]
|
||||
}
|
||||
if match := deviceRegex.FindStringSubmatch(output); len(match) > 1 {
|
||||
deviceID = match[1]
|
||||
}
|
||||
if match := clientRegex.FindStringSubmatch(output); len(match) > 1 {
|
||||
clientID = match[1]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user