去除代理头支持, 增加节点面板通信加密 增加导出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
@@ -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());