Initial commit: proxy management platform

This commit is contained in:
2026-06-10 21:52:17 +00:00
commit 1a00a87024
47 changed files with 6747 additions and 0 deletions
+397
View File
@@ -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: "",
})
}
+162
View File
@@ -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
}
+625
View File
@@ -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{}{},
})
}
}
+116
View File
@@ -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"`
}
+292
View File
@@ -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
}
+350
View File
@@ -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
}
+445
View File
@@ -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
}
+320
View File
@@ -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,
},
}
+349
View File
@@ -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
}