去除代理头支持, 增加节点面板通信加密 增加导出sql

This commit is contained in:
qaq
2025-07-11 16:24:12 +08:00
parent b690e68951
commit e313a1953c
21 changed files with 960 additions and 143 deletions
+1
View File
@@ -76,6 +76,7 @@ volumes:
networks:
gost-network:
name: gost-network
driver: bridge
ipam:
config:
+3
View File
@@ -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:
+124
View File
@@ -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
}
+65 -3
View File
@@ -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, // 配置上报可以稍长一些
+103 -16
View File
@@ -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
}
-1
View File
@@ -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,
+3 -7
View File
@@ -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" <<EOF
echo "📄 创建新配置: config.json"
cat > "$CONFIG_FILE" <<EOF
{
"addr": "$SERVER_ADDR",
"secret": "$SECRET"
}
EOF
fi
# 写入 gost.json
GOST_CONFIG="$INSTALL_DIR/gost.json"
+142 -26
View File
@@ -137,7 +137,8 @@ show_menu() {
echo "1. 安装面板"
echo "2. 更新面板"
echo "3. 卸载面板"
echo "4. 退出"
echo "4. 导出数据库备份"
echo "5. 退出"
echo "==============================================="
}
@@ -184,8 +185,15 @@ install_panel() {
DOCKER_COMPOSE_URL=$(get_docker_compose_url)
echo "📡 选择配置文件:$(basename "$DOCKER_COMPOSE_URL")"
curl -L -o docker-compose.yml "$DOCKER_COMPOSE_URL"
curl -L -o gost.sql "$GOST_SQL_URL"
echo "✅ 下载完成"
# 检查 gost.sql 是否已存在
if [[ -f "gost.sql" ]]; then
echo "⏭️ 跳过下载: gost.sql (使用当前位置的文件)"
else
echo "📡 下载数据库初始化文件..."
curl -L -o gost.sql "$GOST_SQL_URL"
fi
echo "✅ 文件准备完成"
# 自动检测并配置 IPv6 支持
if check_ipv6_support; then
@@ -667,6 +675,24 @@ UPDATE \`tunnel\`
SET \`traffic_ratio\` = 1.0
WHERE \`traffic_ratio\` IS NULL;
-- forward 表:删除 proxy_protocol 字段(如果存在)
SET @sql = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'forward'
AND column_name = 'proxy_protocol'
),
'ALTER TABLE \`forward\` DROP COLUMN \`proxy_protocol\`;',
'SELECT "Column \`proxy_protocol\` not exists in \`forward\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- forward 表:修改 remote_addr 字段类型为 longtext
SET @sql = (
SELECT IF(
@@ -709,28 +735,7 @@ UPDATE \`forward\`
SET \`strategy\` = 'fifo'
WHERE \`strategy\` IS NULL;
-- forward 表:添加 proxy_protocol 字段
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'forward'
AND column_name = 'proxy_protocol'
),
'ALTER TABLE \`forward\` ADD COLUMN \`proxy_protocol\` INT(10) NOT NULL DEFAULT 0 COMMENT "Proxy Protocol 支持";',
'SELECT "Column \`proxy_protocol\` already exists in \`forward\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 为现有数据设置默认 proxy_protocol 值
UPDATE \`forward\`
SET \`proxy_protocol\` = 0
WHERE \`proxy_protocol\` IS NULL;
EOF
# 检查数据库容器
@@ -765,6 +770,113 @@ EOF
echo "✅ 更新完成"
}
# 导出数据库备份
export_migration_sql() {
echo "📄 开始导出数据库备份..."
# 获取数据库配置信息
echo "🔍 获取数据库配置信息..."
# 先检查后端容器是否在运行
if ! docker ps --format "{{.Names}}" | grep -q "^springboot-backend$"; then
echo "❌ 后端容器未运行,尝试从 .env 文件读取配置..."
# 从 .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
else
# 从容器环境变量获取数据库信息
DB_INFO=$(docker exec springboot-backend env | grep "^DB_" 2>/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
@@ -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(禁用)
}
@@ -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(禁用)
}
@@ -85,9 +85,4 @@ public class ForwardWithTunnelDto {
private Long outFlow;
private String strategy;
/**
* 是否启用代理协议(0: 禁用, 1: 启用)
*/
private Integer proxyProtocol;
}
@@ -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;
}
}
}
@@ -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();
@@ -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<String, CompletableFuture<GostDto>> pendingRequests = new ConcurrentHashMap<>();
// 缓存加密器实例,避免重复创建
private static final ConcurrentHashMap<String, AESCrypto> 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<GostDto> 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());
@@ -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<String, AESCrypto> 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();
}
}
@@ -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<String, Object> TUNNEL_LOCKS = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String, Object> FORWARD_LOCKS = new ConcurrentHashMap<>();
// 缓存加密器实例,避免重复创建
private static final ConcurrentHashMap<String, AESCrypto> CRYPTO_CACHE = new ConcurrentHashMap<>();
@Resource
CheckGostConfigAsync checkGostConfigAsync;
@PostMapping("/config")
@LogAnnotation
public String config(@RequestBody GostConfigDto gostConfigDto, String secret) {
Node node = nodeService.getOne(new QueryWrapper<Node>().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<Node>().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<FlowDto> 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<FlowDto> 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<FlowDto> 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<FlowDto> validFlowData = filterFlowData(flowDataList, forward);
// 5. 计算总流量
FlowStatistics flowStats = calculateTotalFlow(validFlowData);
// 6. 获取流量计费类型
int flowType = getFlowType(forward);
@@ -38,7 +38,4 @@ public class Forward extends BaseEntity{
private Long outFlow;
private Integer proxyProtocol;
}
@@ -835,7 +835,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> 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<ForwardMapper, Forward> 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<ForwardMapper, Forward> 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());
@@ -499,7 +499,7 @@ public class UserTunnelServiceImpl extends ServiceImpl<UserTunnelMapper, UserTun
String serviceName = buildServiceName(forward.getId(), Long.valueOf(userId), userTunnel.getId());
// 6. 更新入口节点的主服务限速配置(使用批量UpdateService接口)
GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), speedId, forward.getRemoteAddr(), tunnel.getType(), tunnel, forward.getStrategy(), forward.getProxyProtocol());
GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), speedId, forward.getRemoteAddr(), tunnel.getType(), tunnel, forward.getStrategy());
}
}
}
@@ -14,7 +14,6 @@
f.remote_addr AS remoteAddr,
f.status,
f.created_time AS createdTime,
f.proxy_protocol AS proxyProtocol,
f.updated_time AS updatedTime,
f.user_name AS userName,
f.in_flow AS inFlow,
@@ -48,7 +47,6 @@
f.updated_time AS updatedTime,
f.user_name AS userName,
f.in_flow AS inFlow,
f.proxy_protocol AS proxyProtocol,
f.out_flow AS outFlow,
f.strategy AS strategy,
t.name AS tunnelName,
+6 -24
View File
@@ -33,7 +33,6 @@ interface Forward {
inPort: number;
remoteAddr: string;
strategy: string;
proxyProtocol: number; // 添加 proxy_protocol 字段
status: number;
inFlow: number;
outFlow: number;
@@ -58,7 +57,6 @@ interface ForwardForm {
inPort: number | null;
remoteAddr: string;
strategy: string;
proxyProtocol: boolean; // 在表单中使用 boolean 类型更方便
}
interface AddressItem {
@@ -108,8 +106,7 @@ export default function ForwardPage() {
tunnelId: null,
inPort: null,
remoteAddr: '',
strategy: 'fifo',
proxyProtocol: false // 新增 proxyProtocol 字段
strategy: 'fifo'
});
// 表单验证错误
@@ -205,8 +202,7 @@ export default function ForwardPage() {
tunnelId: null,
inPort: null,
remoteAddr: '',
strategy: 'fifo',
proxyProtocol: false
strategy: 'fifo'
});
setSelectedTunnel(null);
setErrors({});
@@ -223,8 +219,7 @@ export default function ForwardPage() {
tunnelId: forward.tunnelId,
inPort: forward.inPort,
remoteAddr: forward.remoteAddr.split(',').join('\n'),
strategy: forward.strategy || 'fifo',
proxyProtocol: forward.proxyProtocol === 1 // 编辑时需要将数字转换为 boolean
strategy: forward.strategy || 'fifo'
});
const tunnel = tunnels.find(t => 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() {
</Select>
)}
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex flex-col">
<span className="text-sm font-medium text-foreground">启用PROXY Protocol</span>
<span className="text-xs text-default-500">不知道干嘛的就别动</span>
</div>
<Switch
isSelected={form.proxyProtocol}
onValueChange={(checked) => setForm(prev => ({ ...prev, proxyProtocol: checked }))}
/>
</div>
</div>
</div>
</ModalBody>
<ModalFooter>