diff --git a/docker-compose-v4.yml b/docker-compose-v4.yml index 639df85..b841498 100644 --- a/docker-compose-v4.yml +++ b/docker-compose-v4.yml @@ -76,6 +76,7 @@ volumes: networks: gost-network: + name: gost-network driver: bridge ipam: config: diff --git a/docker-compose-v6.yml b/docker-compose-v6.yml index a0a26bf..5e338e3 100644 --- a/docker-compose-v6.yml +++ b/docker-compose-v6.yml @@ -69,13 +69,16 @@ services: volumes: mysql_data: + name: mysql_data driver: local backend_logs: + name: backend_logs driver: local networks: gost-network: + name: gost-network driver: bridge enable_ipv6: true ipam: diff --git a/go-gost/x/internal/util/crypto/aes.go b/go-gost/x/internal/util/crypto/aes.go new file mode 100644 index 0000000..26c3a5f --- /dev/null +++ b/go-gost/x/internal/util/crypto/aes.go @@ -0,0 +1,124 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "fmt" +) + +// AESCrypto AES 加密器结构 +type AESCrypto struct { + key []byte +} + +// NewAESCrypto 创建新的 AES 加密器 +// secret: 用于生成密钥的密码字符串 +func NewAESCrypto(secret string) (*AESCrypto, error) { + if secret == "" { + return nil, fmt.Errorf("密钥不能为空") + } + + // 使用 SHA256 将密码转换为 32 字节密钥 + hash := sha256.Sum256([]byte(secret)) + + return &AESCrypto{ + key: hash[:], + }, nil +} + +// Encrypt 加密数据 +// data: 要加密的原始数据 +// 返回: base64编码的加密数据 +func (a *AESCrypto) Encrypt(data []byte) (string, error) { + if len(data) == 0 { + return "", fmt.Errorf("待加密数据不能为空") + } + + // 创建 AES cipher + block, err := aes.NewCipher(a.key) + if err != nil { + return "", fmt.Errorf("创建 AES cipher 失败: %v", err) + } + + // 使用 GCM 模式 + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("创建 GCM 失败: %v", err) + } + + // 生成随机 nonce + nonce := make([]byte, gcm.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return "", fmt.Errorf("生成 nonce 失败: %v", err) + } + + // 加密数据 + ciphertext := gcm.Seal(nil, nonce, data, nil) + + // 组合 nonce + ciphertext + encrypted := append(nonce, ciphertext...) + + // 返回 base64 编码结果 + return base64.StdEncoding.EncodeToString(encrypted), nil +} + +// Decrypt 解密数据 +// encryptedData: base64编码的加密数据 +// 返回: 解密后的原始数据 +func (a *AESCrypto) Decrypt(encryptedData string) ([]byte, error) { + if encryptedData == "" { + return nil, fmt.Errorf("加密数据不能为空") + } + + // base64 解码 + encrypted, err := base64.StdEncoding.DecodeString(encryptedData) + if err != nil { + return nil, fmt.Errorf("base64 解码失败: %v", err) + } + + // 创建 AES cipher + block, err := aes.NewCipher(a.key) + if err != nil { + return nil, fmt.Errorf("创建 AES cipher 失败: %v", err) + } + + // 使用 GCM 模式 + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("创建 GCM 失败: %v", err) + } + + nonceSize := gcm.NonceSize() + if len(encrypted) < nonceSize { + return nil, fmt.Errorf("加密数据长度不足") + } + + // 分离 nonce 和 ciphertext + nonce := encrypted[:nonceSize] + ciphertext := encrypted[nonceSize:] + + // 解密数据 + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, fmt.Errorf("解密失败: %v", err) + } + + return plaintext, nil +} + +// EncryptString 加密字符串并返回加密后的字符串 +func (a *AESCrypto) EncryptString(data string) (string, error) { + return a.Encrypt([]byte(data)) +} + +// DecryptString 解密字符串并返回解密后的字符串 +func (a *AESCrypto) DecryptString(encryptedData string) (string, error) { + plaintext, err := a.Decrypt(encryptedData) + if err != nil { + return "", err + } + return string(plaintext), nil +} \ No newline at end of file diff --git a/go-gost/x/service/traffic_reporter.go b/go-gost/x/service/traffic_reporter.go index cbe1642..4bcf2b8 100644 --- a/go-gost/x/service/traffic_reporter.go +++ b/go-gost/x/service/traffic_reporter.go @@ -11,11 +11,13 @@ import ( "github.com/go-gost/core/observer/stats" "github.com/go-gost/x/config" + "github.com/go-gost/x/internal/util/crypto" "github.com/go-gost/x/registry" ) var httpReportURL string var configReportURL string +var httpAESCrypto *crypto.AESCrypto // 新增:HTTP上报加密器 // TrafficReportItem 流量报告项(压缩格式) type TrafficReportItem struct { @@ -28,6 +30,16 @@ type TrafficReportItem struct { func SetHTTPReportURL(addr string, secret string) { httpReportURL = "http://" + addr + "/flow/upload?secret=" + secret configReportURL = "http://" + addr + "/flow/config?secret=" + secret + + // 创建 AES 加密器 + var err error + httpAESCrypto, err = crypto.NewAESCrypto(secret) + if err != nil { + fmt.Printf("❌ 创建 HTTP AES 加密器失败: %v\n", err) + httpAESCrypto = nil + } else { + fmt.Printf("🔐 HTTP AES 加密器创建成功\n") + } } // sendTrafficReport 发送流量报告到HTTP接口 @@ -37,7 +49,32 @@ func sendTrafficReport(ctx context.Context, reportItems []TrafficReportItem) (bo return false, fmt.Errorf("序列化报告数据失败: %v", err) } - req, err := http.NewRequestWithContext(ctx, "POST", httpReportURL, bytes.NewBuffer(jsonData)) + var requestBody []byte + + // 如果有加密器,则加密数据 + if httpAESCrypto != nil { + encryptedData, err := httpAESCrypto.Encrypt(jsonData) + if err != nil { + fmt.Printf("⚠️ 加密流量报告失败,发送原始数据: %v\n", err) + requestBody = jsonData + } else { + // 创建加密消息包装器 + encryptedMessage := map[string]interface{}{ + "encrypted": true, + "data": encryptedData, + "timestamp": time.Now().Unix(), + } + requestBody, err = json.Marshal(encryptedMessage) + if err != nil { + fmt.Printf("⚠️ 序列化加密流量报告失败,发送原始数据: %v\n", err) + requestBody = jsonData + } + } + } else { + requestBody = jsonData + } + + req, err := http.NewRequestWithContext(ctx, "POST", httpReportURL, bytes.NewBuffer(requestBody)) if err != nil { return false, fmt.Errorf("创建HTTP请求失败: %v", err) } @@ -88,13 +125,38 @@ func sendConfigReport(ctx context.Context) (bool, error) { return false, fmt.Errorf("获取配置数据失败: %v", err) } - req, err := http.NewRequestWithContext(ctx, "POST", configReportURL, bytes.NewBuffer(configData)) + var requestBody []byte + + // 如果有加密器,则加密数据 + if httpAESCrypto != nil { + encryptedData, err := httpAESCrypto.Encrypt(configData) + if err != nil { + fmt.Printf("⚠️ 加密配置报告失败,发送原始数据: %v\n", err) + requestBody = configData + } else { + // 创建加密消息包装器 + encryptedMessage := map[string]interface{}{ + "encrypted": true, + "data": encryptedData, + "timestamp": time.Now().Unix(), + } + requestBody, err = json.Marshal(encryptedMessage) + if err != nil { + fmt.Printf("⚠️ 序列化加密配置报告失败,发送原始数据: %v\n", err) + requestBody = configData + } + } + } else { + requestBody = configData + } + + req, err := http.NewRequestWithContext(ctx, "POST", configReportURL, bytes.NewBuffer(requestBody)) if err != nil { return false, fmt.Errorf("创建HTTP请求失败: %v", err) } req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", "GOST-Config-Reporter/1.0") + req.Header.Set("User-Agent", "Config-Reporter/1.0") client := &http.Client{ Timeout: 10 * time.Second, // 配置上报可以稍长一些 diff --git a/go-gost/x/socket/websocket_reporter.go b/go-gost/x/socket/websocket_reporter.go index 840b6b9..fa5b046 100644 --- a/go-gost/x/socket/websocket_reporter.go +++ b/go-gost/x/socket/websocket_reporter.go @@ -16,6 +16,7 @@ import ( "time" "github.com/go-gost/x/config" + "github.com/go-gost/x/internal/util/crypto" "github.com/gorilla/websocket" "github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/host" @@ -90,13 +91,24 @@ type WebSocketReporter struct { ctx context.Context cancel context.CancelFunc connected bool - connecting bool // 新增:正在连接状态 - connMutex sync.Mutex // 新增:连接状态锁 + connecting bool // 新增:正在连接状态 + connMutex sync.Mutex // 新增:连接状态锁 + aesCrypto *crypto.AESCrypto // 新增:AES加密器 } // NewWebSocketReporter 创建一个新的WebSocket报告器 -func NewWebSocketReporter(serverURL string) *WebSocketReporter { +func NewWebSocketReporter(serverURL string, secret string) *WebSocketReporter { ctx, cancel := context.WithCancel(context.Background()) + + // 创建 AES 加密器 + aesCrypto, err := crypto.NewAESCrypto(secret) + if err != nil { + fmt.Printf("❌ 创建 AES 加密器失败: %v\n", err) + aesCrypto = nil + } else { + fmt.Printf("🔐 AES 加密器创建成功\n") + } + return &WebSocketReporter{ url: serverURL, reconnectTime: 5 * time.Second, // 重连间隔 @@ -106,6 +118,7 @@ func NewWebSocketReporter(serverURL string) *WebSocketReporter { cancel: cancel, connected: false, connecting: false, + aesCrypto: aesCrypto, } } @@ -134,7 +147,7 @@ func (w *WebSocketReporter) run() { w.connMutex.Lock() needConnect := !w.connected && !w.connecting w.connMutex.Unlock() - + if needConnect { if err := w.connect(); err != nil { fmt.Printf("❌ WebSocket连接失败: %v,%v后重试\n", err, w.reconnectTime) @@ -167,12 +180,12 @@ func (w *WebSocketReporter) run() { func (w *WebSocketReporter) connect() error { w.connMutex.Lock() defer w.connMutex.Unlock() - + // 如果已经在连接中或已连接,直接返回 if w.connecting || w.connected { return nil } - + // 设置连接中状态 w.connecting = true defer func() { @@ -242,7 +255,7 @@ func (w *WebSocketReporter) handleConnection() { w.connMutex.Lock() isConnected := w.connected w.connMutex.Unlock() - + if !isConnected { return } @@ -276,7 +289,7 @@ func (w *WebSocketReporter) collectSystemInfo() SystemInfo { func (w *WebSocketReporter) sendSystemInfo(sysInfo SystemInfo) error { w.connMutex.Lock() defer w.connMutex.Unlock() - + if w.conn == nil || !w.connected { return fmt.Errorf("连接未建立") } @@ -287,10 +300,35 @@ func (w *WebSocketReporter) sendSystemInfo(sysInfo SystemInfo) error { return fmt.Errorf("序列化系统信息失败: %v", err) } + var messageData []byte + + // 如果有加密器,则加密数据 + if w.aesCrypto != nil { + encryptedData, err := w.aesCrypto.Encrypt(jsonData) + if err != nil { + fmt.Printf("⚠️ 加密失败,发送原始数据: %v\n", err) + messageData = jsonData + } else { + // 创建加密消息包装器 + encryptedMessage := map[string]interface{}{ + "encrypted": true, + "data": encryptedData, + "timestamp": time.Now().Unix(), + } + messageData, err = json.Marshal(encryptedMessage) + if err != nil { + fmt.Printf("⚠️ 序列化加密消息失败,发送原始数据: %v\n", err) + messageData = jsonData + } + } + } else { + messageData = jsonData + } + // 设置写入超时 w.conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) - if err := w.conn.WriteMessage(websocket.TextMessage, jsonData); err != nil { + if err := w.conn.WriteMessage(websocket.TextMessage, messageData); err != nil { w.connected = false // 标记连接已断开 return fmt.Errorf("写入消息失败: %v", err) } @@ -309,7 +347,7 @@ func (w *WebSocketReporter) receiveMessages() { conn := w.conn connected := w.connected w.connMutex.Unlock() - + if conn == nil || !connected { return } @@ -338,6 +376,30 @@ func (w *WebSocketReporter) receiveMessages() { func (w *WebSocketReporter) handleReceivedMessage(messageType int, message []byte) { switch messageType { case websocket.TextMessage: + // 先检查是否是加密消息 + var encryptedWrapper struct { + Encrypted bool `json:"encrypted"` + Data string `json:"data"` + Timestamp int64 `json:"timestamp"` + } + + // 尝试解析为加密消息格式 + if err := json.Unmarshal(message, &encryptedWrapper); err == nil && encryptedWrapper.Encrypted { + if w.aesCrypto != nil { + // 解密数据 + decryptedData, err := w.aesCrypto.Decrypt(encryptedWrapper.Data) + if err != nil { + fmt.Printf("❌ 解密失败: %v\n", err) + w.sendErrorResponse("DecryptError", fmt.Sprintf("解密失败: %v", err)) + return + } + message = decryptedData + } else { + fmt.Printf("❌ 收到加密消息但没有加密器\n") + w.sendErrorResponse("NoDecryptor", "没有可用的解密器") + return + } + } // 先尝试解析是否是压缩消息 var compressedMsg struct { Type string `json:"type"` @@ -744,7 +806,7 @@ func (w *WebSocketReporter) handleCall(data interface{}) error { func (w *WebSocketReporter) sendResponse(response CommandResponse) { w.connMutex.Lock() defer w.connMutex.Unlock() - + if w.conn == nil || !w.connected { fmt.Printf("❌ 无法发送响应:连接未建立\n") return @@ -756,19 +818,44 @@ func (w *WebSocketReporter) sendResponse(response CommandResponse) { return } + var messageData []byte + + // 如果有加密器,则加密数据 + if w.aesCrypto != nil { + encryptedData, err := w.aesCrypto.Encrypt(jsonData) + if err != nil { + fmt.Printf("⚠️ 加密响应失败,发送原始数据: %v\n", err) + messageData = jsonData + } else { + // 创建加密消息包装器 + encryptedMessage := map[string]interface{}{ + "encrypted": true, + "data": encryptedData, + "timestamp": time.Now().Unix(), + } + messageData, err = json.Marshal(encryptedMessage) + if err != nil { + fmt.Printf("⚠️ 序列化加密响应失败,发送原始数据: %v\n", err) + messageData = jsonData + } + } + } else { + messageData = jsonData + } + // 检查消息大小,如果超过10MB则记录警告 - if len(jsonData) > 10*1024*1024 { - fmt.Printf("⚠️ 响应消息过大 (%.2f MB),可能会被拒绝\n", float64(len(jsonData))/(1024*1024)) + if len(messageData) > 10*1024*1024 { + fmt.Printf("⚠️ 响应消息过大 (%.2f MB),可能会被拒绝\n", float64(len(messageData))/(1024*1024)) } // 设置较长的写入超时,以应对大消息 timeout := 5 * time.Second - if len(jsonData) > 1024*1024 { + if len(messageData) > 1024*1024 { timeout = 30 * time.Second } w.conn.SetWriteDeadline(time.Now().Add(timeout)) - if err := w.conn.WriteMessage(websocket.TextMessage, jsonData); err != nil { + if err := w.conn.WriteMessage(websocket.TextMessage, messageData); err != nil { fmt.Printf("❌ 发送响应失败: %v\n", err) w.connected = false } @@ -852,7 +939,7 @@ func StartWebSocketReporterWithConfig(Addr string, Secret string, Version string fmt.Printf("🔗 WebSocket连接URL: %s\n", fullURL) - reporter := NewWebSocketReporter(fullURL) + reporter := NewWebSocketReporter(fullURL, Secret) // Pass Secret here reporter.Start() return reporter } diff --git a/gost.sql b/gost.sql index c10c60e..0011d07 100644 --- a/gost.sql +++ b/gost.sql @@ -37,7 +37,6 @@ CREATE TABLE `forward` ( `out_port` int(10) DEFAULT NULL, `remote_addr` varchar(100) NOT NULL, `strategy` varchar(100) NOT NULL DEFAULT 'fifo', - `proxy_protocol` int(10) NOT NULL DEFAULT 0, `in_flow` bigint(20) NOT NULL DEFAULT '0', `out_flow` bigint(20) NOT NULL DEFAULT '0', `created_time` bigint(20) NOT NULL, diff --git a/install.sh b/install.sh index 7a7fc33..804ced0 100755 --- a/install.sh +++ b/install.sh @@ -144,19 +144,15 @@ install_gost() { # 打印版本 echo "🔎 gost 版本:$($INSTALL_DIR/gost -V)" - # 写入 config.json + # 写入 config.json (安装时总是创建新的) CONFIG_FILE="$INSTALL_DIR/config.json" - if [[ -f "$CONFIG_FILE" ]]; then - echo "⏭️ 跳过配置文件: config.json (已存在)" - else - echo "📄 创建新配置: config.json" - cat > "$CONFIG_FILE" < "$CONFIG_FILE" </dev/null || echo "") + + if [[ -n "$DB_INFO" ]]; then + DB_NAME=$(echo "$DB_INFO" | grep "^DB_NAME=" | cut -d'=' -f2) + DB_PASSWORD=$(echo "$DB_INFO" | grep "^DB_PASSWORD=" | cut -d'=' -f2) + DB_USER=$(echo "$DB_INFO" | grep "^DB_USER=" | cut -d'=' -f2) + + echo "✅ 从容器环境变量读取数据库配置成功" + else + echo "❌ 无法从容器获取数据库配置,尝试从 .env 文件读取..." + + if [[ -f ".env" ]]; then + DB_NAME=$(grep "^DB_NAME=" .env | cut -d'=' -f2 2>/dev/null) + DB_PASSWORD=$(grep "^DB_PASSWORD=" .env | cut -d'=' -f2 2>/dev/null) + DB_USER=$(grep "^DB_USER=" .env | cut -d'=' -f2 2>/dev/null) + + if [[ -n "$DB_NAME" && -n "$DB_PASSWORD" && -n "$DB_USER" ]]; then + echo "✅ 从 .env 文件读取数据库配置成功" + else + echo "❌ .env 文件中的数据库配置不完整" + return 1 + fi + else + echo "❌ 未找到 .env 文件" + return 1 + fi + fi + fi + + # 检查必要的数据库配置 + if [[ -z "$DB_PASSWORD" || -z "$DB_USER" || -z "$DB_NAME" ]]; then + echo "❌ 数据库配置不完整(缺少必要参数)" + return 1 + fi + + echo "📋 数据库配置:" + echo " 数据库名: $DB_NAME" + echo " 用户名: $DB_USER" + + # 检查数据库容器是否运行 + if ! docker ps --format "{{.Names}}" | grep -q "^gost-mysql$"; then + echo "❌ 数据库容器未运行,无法导出数据" + echo "🔍 当前运行的容器:" + docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" + return 1 + fi + + # 生成数据库备份文件 + SQL_FILE="database_backup_$(date +%Y%m%d_%H%M%S).sql" + echo "📝 导出数据库备份: $SQL_FILE" + + # 使用 mysqldump 导出数据库 + echo "⏳ 正在导出数据库..." + if docker exec gost-mysql mysqldump -u "$DB_USER" -p"$DB_PASSWORD" --single-transaction --routines --triggers "$DB_NAME" > "$SQL_FILE" 2>/dev/null; then + echo "✅ 数据库导出成功" + else + echo "⚠️ 使用用户密码失败,尝试root密码..." + if docker exec gost-mysql mysqldump -u root -p"$DB_PASSWORD" --single-transaction --routines --triggers "$DB_NAME" > "$SQL_FILE" 2>/dev/null; then + echo "✅ 数据库导出成功" + else + echo "❌ 数据库导出失败" + rm -f "$SQL_FILE" + return 1 + fi + fi + + # 检查文件大小 + if [[ -f "$SQL_FILE" ]] && [[ -s "$SQL_FILE" ]]; then + FILE_SIZE=$(du -h "$SQL_FILE" | cut -f1) + echo "📁 文件位置: $(pwd)/$SQL_FILE" + echo "📊 文件大小: $FILE_SIZE" + else + echo "❌ 导出的文件为空或不存在" + rm -f "$SQL_FILE" + return 1 + fi +} + # 卸载功能 uninstall_panel() { echo "🗑️ 开始卸载面板..." @@ -796,7 +908,7 @@ main() { # 显示交互式菜单 while true; do show_menu - read -p "请输入选项 (1-4): " choice + read -p "请输入选项 (1-5): " choice case $choice in 1) @@ -812,11 +924,15 @@ main() { break ;; 4) + export_migration_sql + break + ;; + 5) echo "👋 退出脚本" exit 0 ;; *) - echo "❌ 无效选项,请输入 1-4" + echo "❌ 无效选项,请输入 1-5" echo "" ;; esac diff --git a/springboot-backend/src/main/java/com/admin/common/dto/ForwardDto.java b/springboot-backend/src/main/java/com/admin/common/dto/ForwardDto.java index 0ad1db0..6dfc939 100644 --- a/springboot-backend/src/main/java/com/admin/common/dto/ForwardDto.java +++ b/springboot-backend/src/main/java/com/admin/common/dto/ForwardDto.java @@ -26,9 +26,5 @@ public class ForwardDto { @Min(value = 1, message = "端口号不能小于1") @Max(value = 65535, message = "端口号不能大于65535") private Integer inPort; - - /** - * 是否启用代理协议(0: 禁用, 1: 启用) - */ - private Integer proxyProtocol = 0; // 设置默认值为0(禁用) + } \ No newline at end of file diff --git a/springboot-backend/src/main/java/com/admin/common/dto/ForwardUpdateDto.java b/springboot-backend/src/main/java/com/admin/common/dto/ForwardUpdateDto.java index c77f6f7..2f89e17 100644 --- a/springboot-backend/src/main/java/com/admin/common/dto/ForwardUpdateDto.java +++ b/springboot-backend/src/main/java/com/admin/common/dto/ForwardUpdateDto.java @@ -32,9 +32,4 @@ public class ForwardUpdateDto { @Min(value = 1, message = "端口号不能小于1") @Max(value = 65535, message = "端口号不能大于65535") private Integer inPort; - - /** - * 是否启用代理协议(0: 禁用, 1: 启用) - */ - private Integer proxyProtocol = 0; // 设置默认值为0(禁用) } \ No newline at end of file diff --git a/springboot-backend/src/main/java/com/admin/common/dto/ForwardWithTunnelDto.java b/springboot-backend/src/main/java/com/admin/common/dto/ForwardWithTunnelDto.java index 6157a28..b6df8cc 100644 --- a/springboot-backend/src/main/java/com/admin/common/dto/ForwardWithTunnelDto.java +++ b/springboot-backend/src/main/java/com/admin/common/dto/ForwardWithTunnelDto.java @@ -85,9 +85,4 @@ public class ForwardWithTunnelDto { private Long outFlow; private String strategy; - - /** - * 是否启用代理协议(0: 禁用, 1: 启用) - */ - private Integer proxyProtocol; } \ No newline at end of file diff --git a/springboot-backend/src/main/java/com/admin/common/utils/AESCrypto.java b/springboot-backend/src/main/java/com/admin/common/utils/AESCrypto.java new file mode 100644 index 0000000..1f12452 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/utils/AESCrypto.java @@ -0,0 +1,168 @@ +package com.admin.common.utils; + +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Base64; + +/** + * AES加密工具类 + * 使用AES-256-GCM模式,与Go端保持兼容 + */ +@Slf4j +public class AESCrypto { + + private static final String ALGORITHM = "AES"; + private static final String TRANSFORMATION = "AES/GCM/NoPadding"; + private static final int GCM_IV_LENGTH = 12; // GCM推荐的IV长度 + private static final int GCM_TAG_LENGTH = 16; // GCM认证标签长度 + + private final SecretKeySpec secretKey; + private final SecureRandom secureRandom; + + /** + * 构造函数 + * @param secret 密钥字符串,将使用SHA-256转换为32字节密钥 + */ + public AESCrypto(String secret) { + if (secret == null || secret.isEmpty()) { + throw new IllegalArgumentException("密钥不能为空"); + } + + try { + // 使用SHA-256将密码转换为32字节密钥,与Go端保持一致 + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + byte[] keyBytes = sha256.digest(secret.getBytes(StandardCharsets.UTF_8)); + this.secretKey = new SecretKeySpec(keyBytes, ALGORITHM); + this.secureRandom = new SecureRandom(); + + log.debug("AES加密器初始化成功"); + } catch (Exception e) { + log.error("AES加密器初始化失败", e); + throw new RuntimeException("AES加密器初始化失败: " + e.getMessage(), e); + } + } + + /** + * 加密数据 + * @param data 要加密的原始数据 + * @return Base64编码的加密数据,格式为: nonce + ciphertext + */ + public String encrypt(byte[] data) { + if (data == null || data.length == 0) { + throw new IllegalArgumentException("待加密数据不能为空"); + } + + try { + // 生成随机IV(nonce) + byte[] iv = new byte[GCM_IV_LENGTH]; + secureRandom.nextBytes(iv); + + // 创建GCM参数规范 + GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv); + + // 初始化Cipher + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec); + + // 加密数据 + byte[] ciphertext = cipher.doFinal(data); + + // 组合IV + ciphertext,与Go端格式保持一致 + ByteBuffer buffer = ByteBuffer.allocate(iv.length + ciphertext.length); + buffer.put(iv); + buffer.put(ciphertext); + + // 返回Base64编码结果 + return Base64.getEncoder().encodeToString(buffer.array()); + + } catch (Exception e) { + log.error("数据加密失败", e); + throw new RuntimeException("数据加密失败: " + e.getMessage(), e); + } + } + + /** + * 加密字符串 + * @param data 要加密的字符串 + * @return Base64编码的加密数据 + */ + public String encrypt(String data) { + if (data == null) { + throw new IllegalArgumentException("待加密字符串不能为空"); + } + return encrypt(data.getBytes(StandardCharsets.UTF_8)); + } + + /** + * 解密数据 + * @param encryptedData Base64编码的加密数据 + * @return 解密后的原始数据 + */ + public byte[] decrypt(String encryptedData) { + if (encryptedData == null || encryptedData.isEmpty()) { + throw new IllegalArgumentException("加密数据不能为空"); + } + + try { + // Base64解码 + byte[] encrypted = Base64.getDecoder().decode(encryptedData); + + if (encrypted.length < GCM_IV_LENGTH) { + throw new IllegalArgumentException("加密数据长度不足"); + } + + // 分离IV和密文 + ByteBuffer buffer = ByteBuffer.wrap(encrypted); + byte[] iv = new byte[GCM_IV_LENGTH]; + buffer.get(iv); + + byte[] ciphertext = new byte[buffer.remaining()]; + buffer.get(ciphertext); + + // 创建GCM参数规范 + GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv); + + // 初始化Cipher + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec); + + // 解密数据 + return cipher.doFinal(ciphertext); + + } catch (Exception e) { + log.error("数据解密失败", e); + throw new RuntimeException("数据解密失败: " + e.getMessage(), e); + } + } + + /** + * 解密字符串 + * @param encryptedData Base64编码的加密数据 + * @return 解密后的字符串 + */ + public String decryptString(String encryptedData) { + byte[] decrypted = decrypt(encryptedData); + return new String(decrypted, StandardCharsets.UTF_8); + } + + /** + * 创建AES加密器实例 + * @param secret 密钥字符串 + * @return AES加密器实例,如果创建失败返回null + */ + public static AESCrypto create(String secret) { + try { + return new AESCrypto(secret); + } catch (Exception e) { + log.error("创建AES加密器失败: {}", e.getMessage()); + return null; + } + } +} \ No newline at end of file diff --git a/springboot-backend/src/main/java/com/admin/common/utils/GostUtil.java b/springboot-backend/src/main/java/com/admin/common/utils/GostUtil.java index 98f23ee..aee6bd7 100644 --- a/springboot-backend/src/main/java/com/admin/common/utils/GostUtil.java +++ b/springboot-backend/src/main/java/com/admin/common/utils/GostUtil.java @@ -31,21 +31,21 @@ public class GostUtil { return WebSocketServer.send_msg(node_id, req, "DeleteLimiters"); } - public static GostDto AddService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy, Integer proxy_protocol) { + public static GostDto AddService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy) { JSONArray services = new JSONArray(); String[] protocols = {"tcp", "udp"}; for (String protocol : protocols) { - JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy, proxy_protocol); + JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy); services.add(service); } return WebSocketServer.send_msg(node_id, services, "AddService"); } - public static GostDto UpdateService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy, Integer proxy_protocol) { + public static GostDto UpdateService(Long node_id, String name, Integer in_port, Integer limiter, String remoteAddr, Integer fow_type, Tunnel tunnel, String strategy) { JSONArray services = new JSONArray(); String[] protocols = {"tcp", "udp"}; for (String protocol : protocols) { - JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy, proxy_protocol); + JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy); services.add(service); } return WebSocketServer.send_msg(node_id, services, "UpdateService"); @@ -255,7 +255,7 @@ public class GostUtil { return data; } - private static JSONObject createServiceConfig(String name, Integer in_port, Integer limiter, String remoteAddr, String protocol, Integer fow_type, Tunnel tunnel, String strategy, Integer proxy_protocol) { + private static JSONObject createServiceConfig(String name, Integer in_port, Integer limiter, String remoteAddr, String protocol, Integer fow_type, Tunnel tunnel, String strategy) { JSONObject service = new JSONObject(); service.put("name", name + "_" + protocol); if (Objects.equals(protocol, "tcp")){ @@ -279,13 +279,9 @@ public class GostUtil { // 端口转发需要配置转发器 if (isPortForwarding(fow_type)) { - JSONObject forwarder = createForwarder(protocol, remoteAddr, strategy); + JSONObject forwarder = createForwarder(remoteAddr, strategy); service.put("forwarder", forwarder); } - JSONObject metadata = new JSONObject(); - metadata.put("proxyProtocol", proxy_protocol); - service.put("metadata", metadata); - return service; } @@ -312,7 +308,7 @@ public class GostUtil { return listener; } - private static JSONObject createForwarder(String protocol, String remoteAddr, String strategy) { + private static JSONObject createForwarder(String remoteAddr, String strategy) { JSONObject forwarder = new JSONObject(); JSONArray nodes = new JSONArray(); diff --git a/springboot-backend/src/main/java/com/admin/common/utils/WebSocketServer.java b/springboot-backend/src/main/java/com/admin/common/utils/WebSocketServer.java index a35f030..ceb8051 100644 --- a/springboot-backend/src/main/java/com/admin/common/utils/WebSocketServer.java +++ b/springboot-backend/src/main/java/com/admin/common/utils/WebSocketServer.java @@ -6,6 +6,7 @@ import com.admin.common.dto.GostDto; import com.admin.common.task.CheckGostConfigAsync; import com.admin.entity.Node; import com.admin.service.NodeService; +import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -42,6 +43,25 @@ public class WebSocketServer extends TextWebSocketHandler { // 存储等待响应的请求,key为requestId,value为CompletableFuture private static final ConcurrentHashMap> pendingRequests = new ConcurrentHashMap<>(); + // 缓存加密器实例,避免重复创建 + private static final ConcurrentHashMap cryptoCache = new ConcurrentHashMap<>(); + + /** + * 加密消息包装器 + */ + public static class EncryptedMessage { + private boolean encrypted; + private String data; + private Long timestamp; + + // getters and setters + public boolean isEncrypted() { return encrypted; } + public void setEncrypted(boolean encrypted) { this.encrypted = encrypted; } + public String getData() { return data; } + public void setData(String data) { this.data = data; } + public Long getTimestamp() { return timestamp; } + public void setTimestamp(Long timestamp) { this.timestamp = timestamp; } + } //接受客户端消息 @Override @@ -51,15 +71,19 @@ public class WebSocketServer extends TextWebSocketHandler { String id = session.getAttributes().get("id").toString(); String type = session.getAttributes().get("type").toString(); + String nodeSecret = (String) session.getAttributes().get("nodeSecret"); + + // 尝试解密消息 + String decryptedPayload = decryptMessageIfNeeded(message.getPayload(), nodeSecret); - if (message.getPayload().contains("memory_usage")){ + if (decryptedPayload.contains("memory_usage")){ // 先发送确认消息 - sendToUser(session, "{\"type\":\"call\"}"); - }else if (message.getPayload().contains("requestId")) { - log.info("收到消息: {}", message.getPayload()); + sendToUser(session, "{\"type\":\"call\"}", nodeSecret); + }else if (decryptedPayload.contains("requestId")) { + log.info("收到消息: {}", decryptedPayload); // 处理命令响应消息 try { - JSONObject responseJson = JSONObject.parseObject(message.getPayload()); + JSONObject responseJson = JSONObject.parseObject(decryptedPayload); String requestId = responseJson.getString("requestId"); String responseMessage = responseJson.getString("message"); String responseType = responseJson.getString("type"); @@ -91,7 +115,7 @@ public class WebSocketServer extends TextWebSocketHandler { log.error("处理响应消息失败: {}", e.getMessage(), e); } } else { - log.info("收到消息: {}", message.getPayload()); + log.info("收到消息: {}", decryptedPayload); } // 如果是节点类型,转发消息给其他会话 @@ -99,13 +123,13 @@ public class WebSocketServer extends TextWebSocketHandler { JSONObject jsonObject = new JSONObject(); jsonObject.put("id", id); jsonObject.put("type", "info"); - jsonObject.put("data", message.getPayload()); + jsonObject.put("data", decryptedPayload); String broadcastMessage = jsonObject.toJSONString(); // 异步处理广播消息,避免阻塞当前线程 for (WebSocketSession targetSession : activeSessions) { if (targetSession != null && targetSession.isOpen() && !targetSession.equals(session)) { - sendToUser(targetSession, broadcastMessage); + sendToUser(targetSession, broadcastMessage, null); } } } @@ -115,6 +139,78 @@ public class WebSocketServer extends TextWebSocketHandler { } } + /** + * 尝试解密消息(如果需要) + */ + private String decryptMessageIfNeeded(String payload, String nodeSecret) { + if (payload == null || payload.trim().isEmpty()) { + return payload; + } + + try { + // 尝试解析为加密消息格式 + EncryptedMessage encryptedMessage = JSON.parseObject(payload, EncryptedMessage.class); + + if (encryptedMessage.isEncrypted() && encryptedMessage.getData() != null) { + // 获取或创建加密器 + AESCrypto crypto = getOrCreateCrypto(nodeSecret); + if (crypto == null) { + log.warn("⚠️ 收到加密消息但无法创建解密器,使用原始数据"); + return payload; + } + + // 解密数据 + String decryptedData = crypto.decryptString(encryptedMessage.getData()); + log.debug("🔓 WebSocket消息解密成功"); + return decryptedData; + } + } catch (Exception e) { + // 解析失败,可能是非加密格式,直接返回原始数据 + log.debug("WebSocket消息未加密或解密失败,使用原始数据: {}", e.getMessage()); + } + + return payload; + } + + /** + * 加密消息(如果可能) + */ + private static String encryptMessageIfPossible(String message, String nodeSecret) { + if (message == null || nodeSecret == null) { + return message; + } + + try { + AESCrypto crypto = getOrCreateCrypto(nodeSecret); + if (crypto != null) { + String encryptedData = crypto.encrypt(message); + + // 创建加密消息包装器 + JSONObject encryptedMessage = new JSONObject(); + encryptedMessage.put("encrypted", true); + encryptedMessage.put("data", encryptedData); + encryptedMessage.put("timestamp", System.currentTimeMillis()); + + log.debug("🔐 WebSocket消息加密成功"); + return encryptedMessage.toJSONString(); + } + } catch (Exception e) { + log.warn("⚠️ WebSocket消息加密失败,发送原始数据: {}", e.getMessage()); + } + + return message; + } + + /** + * 获取或创建加密器实例 + */ + private static AESCrypto getOrCreateCrypto(String secret) { + if (secret == null || secret.isEmpty()) { + return null; + } + return cryptoCache.computeIfAbsent(secret, AESCrypto::create); + } + // 建立连接 @Override public void afterConnectionEstablished(WebSocketSession session) { @@ -232,7 +328,7 @@ public class WebSocketServer extends TextWebSocketHandler { boolean shouldUpdateOffline = true; try { // 尝试发送验证消息,如果发送成功说明连接可能还活跃 - sendToUser(session, "{\"type\":\"call\"}"); + sendToUser(session, "{\"type\":\"call\"}", null); log.warn("节点 {} 连接关闭但仍能发送消息,可能是假断开", nodeId); shouldUpdateOffline = false; } catch (Exception e) { @@ -277,6 +373,12 @@ public class WebSocketServer extends TextWebSocketHandler { // 点对点发送消息 @SneakyThrows public static void sendToUser(WebSocketSession socketSession, String message) { + sendToUser(socketSession, message, null); + } + + // 点对点发送消息(支持加密) + @SneakyThrows + public static void sendToUser(WebSocketSession socketSession, String message, String nodeSecret) { if (socketSession != null && socketSession.isOpen()) { String sessionId = socketSession.getId(); Object lock = sessionLocks.computeIfAbsent(sessionId, k -> new Object()); @@ -284,7 +386,15 @@ public class WebSocketServer extends TextWebSocketHandler { synchronized (lock) { try { if (socketSession.isOpen()) { - socketSession.sendMessage(new TextMessage(message)); + // 如果是节点连接且有密钥,尝试加密消息 + String finalMessage = message; + if (nodeSecret != null && !nodeSecret.isEmpty()) { + String type = (String) socketSession.getAttributes().get("type"); + if ("1".equals(type)) { // 节点连接 + finalMessage = encryptMessageIfPossible(message, nodeSecret); + } + } + socketSession.sendMessage(new TextMessage(finalMessage)); } } catch (Exception e) { log.error("发送WebSocket消息失败 [sessionId={}]: {}", sessionId, e.getMessage()); @@ -354,13 +464,15 @@ public class WebSocketServer extends TextWebSocketHandler { CompletableFuture future = new CompletableFuture<>(); pendingRequests.put(requestId, future); + // 获取节点密钥用于加密 + String nodeSecret = (String) nodeSession.getAttributes().get("nodeSecret"); try { JSONObject data = new JSONObject(); data.put("type", type); data.put("data", msg); data.put("requestId", requestId); - sendToUser(nodeSession, data.toJSONString()); + sendToUser(nodeSession, data.toJSONString(), nodeSecret); GostDto result = future.get(10, TimeUnit.SECONDS); log.debug("成功发送消息到节点 {} 并收到响应: {}", node_id, result.getMsg()); diff --git a/springboot-backend/src/main/java/com/admin/config/EncryptionConfig.java b/springboot-backend/src/main/java/com/admin/config/EncryptionConfig.java new file mode 100644 index 0000000..16b56f8 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/config/EncryptionConfig.java @@ -0,0 +1,81 @@ +package com.admin.config; + +import com.admin.common.utils.AESCrypto; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * 加密配置管理类 + * 统一管理AES加密功能,为HTTP上报和WebSocket通信提供加密支持 + */ +@Component +@Slf4j +public class EncryptionConfig { + + // 缓存加密器实例,避免重复创建 + private static final ConcurrentHashMap CRYPTO_CACHE = new ConcurrentHashMap<>(); + + /** + * 获取或创建AES加密器实例 + * + * @param secret 密钥字符串 + * @return AES加密器实例,如果创建失败返回null + */ + public static AESCrypto getOrCreateCrypto(String secret) { + if (secret == null || secret.isEmpty()) { + return null; + } + return CRYPTO_CACHE.computeIfAbsent(secret, AESCrypto::create); + } + + /** + * 检测消息是否为加密格式 + * + * @param message 消息内容 + * @return 如果是加密格式返回true,否则返回false + */ + public static boolean isEncryptedMessage(String message) { + if (message == null || message.trim().isEmpty()) { + return false; + } + + try { + // 简单检查是否包含加密标识 + return message.contains("\"encrypted\":true") || message.contains("\"encrypted\": true"); + } catch (Exception e) { + return false; + } + } + + /** + * 清理缓存的加密器实例 + * + * @param secret 要清理的密钥 + */ + public static void clearCrypto(String secret) { + if (secret != null) { + CRYPTO_CACHE.remove(secret); + log.debug("已清理密钥对应的加密器实例"); + } + } + + /** + * 清理所有缓存的加密器实例 + */ + public static void clearAllCrypto() { + int size = CRYPTO_CACHE.size(); + CRYPTO_CACHE.clear(); + log.info("已清理所有加密器实例缓存,共清理 {} 个实例", size); + } + + /** + * 获取当前缓存的加密器数量 + * + * @return 缓存的加密器数量 + */ + public static int getCacheSize() { + return CRYPTO_CACHE.size(); + } +} \ No newline at end of file diff --git a/springboot-backend/src/main/java/com/admin/controller/FlowController.java b/springboot-backend/src/main/java/com/admin/controller/FlowController.java index 84faa04..3865af3 100644 --- a/springboot-backend/src/main/java/com/admin/controller/FlowController.java +++ b/springboot-backend/src/main/java/com/admin/controller/FlowController.java @@ -5,11 +5,15 @@ import com.admin.common.dto.FlowDto; import com.admin.common.dto.GostConfigDto; import com.admin.common.lang.R; import com.admin.common.task.CheckGostConfigAsync; +import com.admin.common.utils.AESCrypto; import com.admin.common.utils.GostUtil; import com.admin.entity.*; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import org.springframework.web.bind.annotation.*; +import lombok.extern.slf4j.Slf4j; import javax.annotation.Resource; import java.math.BigDecimal; @@ -41,6 +45,7 @@ import java.util.stream.Collectors; @RestController @RequestMapping("/flow") @CrossOrigin +@Slf4j public class FlowController extends BaseController { // 常量定义 @@ -55,19 +60,51 @@ public class FlowController extends BaseController { private static final ConcurrentHashMap TUNNEL_LOCKS = new ConcurrentHashMap<>(); private static final ConcurrentHashMap FORWARD_LOCKS = new ConcurrentHashMap<>(); + // 缓存加密器实例,避免重复创建 + private static final ConcurrentHashMap CRYPTO_CACHE = new ConcurrentHashMap<>(); @Resource CheckGostConfigAsync checkGostConfigAsync; - @PostMapping("/config") - @LogAnnotation - public String config(@RequestBody GostConfigDto gostConfigDto, String secret) { - Node node = nodeService.getOne(new QueryWrapper().eq("secret", secret)); - if (node == null) return SUCCESS_RESPONSE; - checkGostConfigAsync.cleanNodeConfigs(node.getId().toString(), gostConfigDto); - return SUCCESS_RESPONSE; + /** + * 加密消息包装器 + */ + public static class EncryptedMessage { + private boolean encrypted; + private String data; + private Long timestamp; + + // getters and setters + public boolean isEncrypted() { return encrypted; } + public void setEncrypted(boolean encrypted) { this.encrypted = encrypted; } + public String getData() { return data; } + public void setData(String data) { this.data = data; } + public Long getTimestamp() { return timestamp; } + public void setTimestamp(Long timestamp) { this.timestamp = timestamp; } } + @PostMapping("/config") + @LogAnnotation + public String config(@RequestBody String rawData, String secret) { + Node node = nodeService.getOne(new QueryWrapper().eq("secret", secret)); + if (node == null) return SUCCESS_RESPONSE; + + try { + // 尝试解密数据 + String decryptedData = decryptIfNeeded(rawData, secret); + + // 解析为GostConfigDto + GostConfigDto gostConfigDto = JSON.parseObject(decryptedData, GostConfigDto.class); + checkGostConfigAsync.cleanNodeConfigs(node.getId().toString(), gostConfigDto); + + log.info("🔓 节点 {} 配置数据接收成功{}", node.getId(), isEncryptedMessage(rawData) ? "(已解密)" : ""); + + } catch (Exception e) { + log.error("处理节点 {} 配置数据失败: {}", node.getId(), e.getMessage()); + } + + return SUCCESS_RESPONSE; + } @RequestMapping("/test") @LogAnnotation @@ -78,32 +115,107 @@ public class FlowController extends BaseController { /** * 处理流量数据上报 * - * @param flowDataList 流量数据列表 - * @param secret 节点密钥 + * @param rawData 原始数据(可能是加密的) + * @param secret 节点密钥 * @return 处理结果 */ @RequestMapping("/upload") @LogAnnotation - public String uploadFlowData(@RequestBody List flowDataList, String secret) { + public String uploadFlowData(@RequestBody String rawData, String secret) { // 1. 验证节点权限 if (!isValidNode(secret)) { return SUCCESS_RESPONSE; } - if (flowDataList.isEmpty()) { + + try { + // 2. 尝试解密数据 + String decryptedData = decryptIfNeeded(rawData, secret); + + // 3. 解析为FlowDto列表 + List flowDataList = JSON.parseArray(decryptedData, FlowDto.class); + + if (flowDataList.isEmpty()) { + return SUCCESS_RESPONSE; + } + if (Objects.equals(flowDataList.get(0).getN(), "web_api")) { + return SUCCESS_RESPONSE; + } + + // 记录日志 + log.debug("🔓 节点流量数据接收成功{}", + isEncryptedMessage(rawData) ? "(已解密)" : ""); + + // 4. 处理流量数据 + return processFlowData(flowDataList); + + } catch (Exception e) { + log.error("处理流量数据失败: {}", e.getMessage(), e); return SUCCESS_RESPONSE; } - if(Objects.equals(flowDataList.get(0).getN(), "web_api")){ - return SUCCESS_RESPONSE; + } + + /** + * 检测消息是否为加密格式 + */ + private boolean isEncryptedMessage(String data) { + try { + JSONObject json = JSON.parseObject(data); + return json.getBooleanValue("encrypted"); + } catch (Exception e) { + return false; + } + } + + /** + * 根据需要解密数据 + */ + private String decryptIfNeeded(String rawData, String secret) { + if (rawData == null || rawData.trim().isEmpty()) { + throw new IllegalArgumentException("数据不能为空"); } + try { + // 尝试解析为加密消息格式 + EncryptedMessage encryptedMessage = JSON.parseObject(rawData, EncryptedMessage.class); + + if (encryptedMessage.isEncrypted() && encryptedMessage.getData() != null) { + // 获取或创建加密器 + AESCrypto crypto = getOrCreateCrypto(secret); + if (crypto == null) { + log.warn("⚠️ 收到加密消息但无法创建解密器,使用原始数据"); + return rawData; + } + + // 解密数据 + String decryptedData = crypto.decryptString(encryptedMessage.getData()); + log.debug("🔓 数据解密成功"); + return decryptedData; + } + } catch (Exception e) { + // 解析失败,可能是非加密格式,直接返回原始数据 + log.debug("数据未加密或解密失败,使用原始数据: {}", e.getMessage()); + } + + return rawData; + } + /** + * 获取或创建加密器实例 + */ + private AESCrypto getOrCreateCrypto(String secret) { + return CRYPTO_CACHE.computeIfAbsent(secret, AESCrypto::create); + } + + /** + * 处理流量数据的核心逻辑 + */ + private String processFlowData(List flowDataList) { // 2. 解析服务名称获取ID信息 String[] serviceIds = parseServiceName(flowDataList.get(0).getN()); String forwardId = serviceIds[0]; String userId = serviceIds[1]; String userTunnelId = serviceIds[2]; - // 3. 一次性查询相关实体,避免后续重复查询 Forward forward = forwardService.getById(forwardId); User user = userService.getById(userId); @@ -111,16 +223,13 @@ public class FlowController extends BaseController { if (!Objects.equals(userTunnelId, DEFAULT_USER_TUNNEL_ID)) { userTunnel = userTunnelService.getById(userTunnelId); } + // 4. 处理流量倍率 List validFlowData = filterFlowData(flowDataList, forward); - - // 5. 计算总流量 FlowStatistics flowStats = calculateTotalFlow(validFlowData); - - // 6. 获取流量计费类型 int flowType = getFlowType(forward); diff --git a/springboot-backend/src/main/java/com/admin/entity/Forward.java b/springboot-backend/src/main/java/com/admin/entity/Forward.java index 4a1d688..53f5d86 100644 --- a/springboot-backend/src/main/java/com/admin/entity/Forward.java +++ b/springboot-backend/src/main/java/com/admin/entity/Forward.java @@ -38,7 +38,4 @@ public class Forward extends BaseEntity{ private Long outFlow; - private Integer proxyProtocol; - - } diff --git a/springboot-backend/src/main/java/com/admin/service/impl/ForwardServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/ForwardServiceImpl.java index 2a9469e..f901474 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/ForwardServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/ForwardServiceImpl.java @@ -835,7 +835,7 @@ public class ForwardServiceImpl extends ServiceImpl impl // 创建主服务 - R serviceResult = createMainService(nodeInfo.getInNode(), serviceName, forward, limiter, tunnel.getType(), tunnel, forward.getStrategy(), forward.getProxyProtocol()); + R serviceResult = createMainService(nodeInfo.getInNode(), serviceName, forward, limiter, tunnel.getType(), tunnel, forward.getStrategy()); if (serviceResult.getCode() != 0) { GostUtil.DeleteChains(nodeInfo.getInNode().getId(), serviceName); if (nodeInfo.getOutNode() != null) { @@ -1008,8 +1008,8 @@ public class ForwardServiceImpl extends ServiceImpl impl /** * 创建主服务 */ - private R createMainService(Node inNode, String serviceName, Forward forward, Integer limiter, Integer tunnelType, Tunnel tunnel, String strategy, Integer proxy_protocol) { - GostDto result = GostUtil.AddService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy, proxy_protocol); + private R createMainService(Node inNode, String serviceName, Forward forward, Integer limiter, Integer tunnelType, Tunnel tunnel, String strategy) { + GostDto result = GostUtil.AddService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy); return isGostOperationSuccess(result) ? R.ok() : R.err(result.getMsg()); } @@ -1049,10 +1049,10 @@ public class ForwardServiceImpl extends ServiceImpl impl * 更新主服务 */ private R updateMainService(Node inNode, String serviceName, Forward forward, Integer limiter, Integer tunnelType, Tunnel tunnel, String strategy) { - GostDto result = GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy, forward.getProxyProtocol()); + GostDto result = GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy); if (result.getMsg().contains(GOST_NOT_FOUND_MSG)) { - result = GostUtil.AddService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy, forward.getProxyProtocol()); + result = GostUtil.AddService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy); } return isGostOperationSuccess(result) ? R.ok() : R.err(result.getMsg()); diff --git a/springboot-backend/src/main/java/com/admin/service/impl/UserTunnelServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/UserTunnelServiceImpl.java index 4b8e517..ff3b907 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/UserTunnelServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/UserTunnelServiceImpl.java @@ -499,7 +499,7 @@ public class UserTunnelServiceImpl extends ServiceImpl t.id === forward.tunnelId); setSelectedTunnel(tunnel || null); @@ -302,8 +297,7 @@ export default function ForwardPage() { tunnelId: form.tunnelId, inPort: form.inPort, remoteAddr: processedRemoteAddr, - strategy: addressCount > 1 ? form.strategy : 'fifo', - proxyProtocol: form.proxyProtocol ? 1 : 0 // 提交时需要将 boolean 转换为数字 + strategy: addressCount > 1 ? form.strategy : 'fifo' }; res = await updateForward(updateData); } else { @@ -313,8 +307,7 @@ export default function ForwardPage() { tunnelId: form.tunnelId, inPort: form.inPort, remoteAddr: processedRemoteAddr, - strategy: addressCount > 1 ? form.strategy : 'fifo', - proxyProtocol: form.proxyProtocol ? 1 : 0 // 创建时需要将 boolean 转换为数字 + strategy: addressCount > 1 ? form.strategy : 'fifo' }; res = await createForward(createData); } @@ -909,18 +902,7 @@ export default function ForwardPage() { )} -
-
-
- 启用PROXY Protocol - 不知道干嘛的就别动 -
- setForm(prev => ({ ...prev, proxyProtocol: checked }))} - /> -
-
+