diff --git a/go-gost/gost b/go-gost/gost index 1067e54..012bb42 100755 Binary files a/go-gost/gost and b/go-gost/gost differ diff --git a/go-gost/x/socket/websocket_reporter.go b/go-gost/x/socket/websocket_reporter.go index 22294e3..4f8eaee 100644 --- a/go-gost/x/socket/websocket_reporter.go +++ b/go-gost/x/socket/websocket_reporter.go @@ -428,8 +428,14 @@ func (w *WebSocketReporter) handleAddService(data interface{}) error { return fmt.Errorf("序列化数据失败: %v", err) } + // 预处理:将字符串格式的 duration 转换为纳秒数 + processedData, err := w.preprocessDurationFields(jsonData) + if err != nil { + return fmt.Errorf("预处理duration字段失败: %v", err) + } + var services []config.ServiceConfig - if err := json.Unmarshal(jsonData, &services); err != nil { + if err := json.Unmarshal(processedData, &services); err != nil { return fmt.Errorf("解析服务配置失败: %v", err) } @@ -443,8 +449,14 @@ func (w *WebSocketReporter) handleUpdateService(data interface{}) error { return fmt.Errorf("序列化数据失败: %v", err) } + // 预处理:将字符串格式的 duration 转换为纳秒数 + processedData, err := w.preprocessDurationFields(jsonData) + if err != nil { + return fmt.Errorf("预处理duration字段失败: %v", err) + } + var services []config.ServiceConfig - if err := json.Unmarshal(jsonData, &services); err != nil { + if err := json.Unmarshal(processedData, &services); err != nil { return fmt.Errorf("解析服务配置失败: %v", err) } @@ -977,3 +989,49 @@ func isValidHostname(hostname string) bool { return true } + +// preprocessDurationFields 预处理 JSON 数据中的 duration 字段 +func (w *WebSocketReporter) preprocessDurationFields(jsonData []byte) ([]byte, error) { + var rawData interface{} + if err := json.Unmarshal(jsonData, &rawData); err != nil { + return nil, err + } + + // 递归处理 duration 字段 + processed := w.processDurationInData(rawData) + + return json.Marshal(processed) +} + +// processDurationInData 递归处理数据中的 duration 字段 +func (w *WebSocketReporter) processDurationInData(data interface{}) interface{} { + switch v := data.(type) { + case []interface{}: + // 处理数组 + for i, item := range v { + v[i] = w.processDurationInData(item) + } + return v + case map[string]interface{}: + // 处理对象 + for key, value := range v { + if key == "selector" { + // 处理 selector 对象中的 failTimeout + if selectorObj, ok := value.(map[string]interface{}); ok { + if failTimeoutVal, exists := selectorObj["failTimeout"]; exists { + if failTimeoutStr, ok := failTimeoutVal.(string); ok { + // 将字符串格式的 duration 转换为纳秒数 + if duration, err := time.ParseDuration(failTimeoutStr); err == nil { + selectorObj["failTimeout"] = int64(duration) + } + } + } + } + } + v[key] = w.processDurationInData(value) + } + return v + default: + return v + } +} diff --git a/gost.sql b/gost.sql index c4f4cab..f3c5edf 100644 --- a/gost.sql +++ b/gost.sql @@ -35,7 +35,8 @@ CREATE TABLE `forward` ( `tunnel_id` int(10) NOT NULL, `in_port` int(10) NOT NULL, `out_port` int(10) DEFAULT NULL, - `remote_addr` varchar(100) NOT NULL, + `remote_addr` longtext NOT NULL, + `strategy` varchar(100) NOT NULL DEFAULT 'fifo' COMMENT '负载均衡策略', `in_flow` bigint(20) NOT NULL DEFAULT '0', `out_flow` bigint(20) NOT NULL DEFAULT '0', `created_time` bigint(20) NOT NULL, @@ -53,7 +54,7 @@ CREATE TABLE `node` ( `id` int(10) NOT NULL, `name` varchar(100) NOT NULL, `secret` varchar(100) NOT NULL, - `ip` varchar(100) DEFAULT NULL, + `ip` longtext DEFAULT NULL, `server_ip` varchar(100) NOT NULL, `created_time` bigint(20) NOT NULL, `updated_time` bigint(20) DEFAULT NULL, diff --git a/panel_install.sh b/panel_install.sh index 27bc332..e3f6dbb 100755 --- a/panel_install.sh +++ b/panel_install.sh @@ -439,6 +439,25 @@ UPDATE \`node\` SET \`server_ip\` = \`ip\` WHERE \`server_ip\` IS NULL; +-- node 表:修改 ip 字段类型为 longtext +SET @sql = ( + SELECT IF( + EXISTS ( + SELECT 1 + FROM information_schema.COLUMNS + WHERE table_schema = DATABASE() + AND table_name = 'node' + AND column_name = 'ip' + AND data_type = 'varchar' + ), + 'ALTER TABLE \`node\` MODIFY COLUMN \`ip\` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;', + 'SELECT "Column \`ip\` not exists or already modified in \`node\`";' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + -- tunnel 表:添加 tcp_listen_addr、udp_listen_addr、protocol(如果不存在) -- tcp_listen_addr @@ -517,6 +536,48 @@ DEALLOCATE PREPARE stmt; UPDATE \`tunnel\` SET \`traffic_ratio\` = 1.0 WHERE \`traffic_ratio\` IS NULL; + +-- forward 表:修改 remote_addr 字段类型为 longtext +SET @sql = ( + SELECT IF( + EXISTS ( + SELECT 1 + FROM information_schema.COLUMNS + WHERE table_schema = DATABASE() + AND table_name = 'forward' + AND column_name = 'remote_addr' + AND data_type = 'varchar' + ), + 'ALTER TABLE \`forward\` MODIFY COLUMN \`remote_addr\` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL;', + 'SELECT "Column \`remote_addr\` not exists or already modified in \`forward\`";' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- forward 表:添加 strategy 字段(负载均衡策略) +SET @sql = ( + SELECT IF( + NOT EXISTS ( + SELECT 1 + FROM information_schema.COLUMNS + WHERE table_schema = DATABASE() + AND table_name = 'forward' + AND column_name = 'strategy' + ), + 'ALTER TABLE \`forward\` ADD COLUMN \`strategy\` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT "fifo" COMMENT "负载均衡策略";', + 'SELECT "Column \`strategy\` already exists in \`forward\`";' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 为现有数据设置默认负载均衡策略 +UPDATE \`forward\` +SET \`strategy\` = 'fifo' +WHERE \`strategy\` IS NULL; EOF # 检查数据库容器 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 b39cc79..a3306f2 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 @@ -17,6 +17,8 @@ public class ForwardDto { @NotBlank(message = "远程地址不能为空") private String remoteAddr; + + private String strategy; /** * 入口端口(可选,为空时自动分配) 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 c75d4c7..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 @@ -23,6 +23,8 @@ public class ForwardUpdateDto { @NotBlank(message = "远程地址不能为空") private String remoteAddr; + + private String strategy; /** * 入口端口(可选,为空时自动分配) 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 a7cff38..8427e17 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 @@ -84,6 +84,7 @@ public class ForwardWithTunnelDto { */ private Long outFlow; + private String strategy; // /** // * 入口端口开始 // */ 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 31a1ade..b6064c6 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) { + 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); + 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) { + 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); + JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type, tunnel, strategy); services.add(service); } return WebSocketServer.send_msg(node_id, services, "UpdateService"); @@ -60,6 +60,90 @@ public class GostUtil { return WebSocketServer.send_msg(node_id, data, "DeleteService"); } + public static GostDto AddRemoteService(Long node_id, String name, Integer out_port, String remoteAddr, String protocol, String strategy) { + JSONObject data = new JSONObject(); + data.put("name", name + "_tls"); + data.put("addr", ":" + out_port); + JSONObject handler = new JSONObject(); + handler.put("type", "relay"); + data.put("handler", handler); + JSONObject listener = new JSONObject(); + listener.put("type", protocol); + data.put("listener", listener); + JSONObject forwarder = new JSONObject(); + JSONArray nodes = new JSONArray(); + + String[] split = remoteAddr.split(","); + int num = 1; + for (String addr : split) { + JSONObject node = new JSONObject(); + node.put("name", "node_" + num ); + node.put("addr", addr); + nodes.add(node); + num ++; + } + if (strategy == null || strategy.equals("")){ + strategy = "fifo"; + } + forwarder.put("nodes", nodes); + JSONObject selector = new JSONObject(); + selector.put("strategy", strategy); + selector.put("maxFails", 1); + selector.put("failTimeout", "10s"); + forwarder.put("selector", selector); + + data.put("forwarder", forwarder); + JSONArray services = new JSONArray(); + services.add(data); + return WebSocketServer.send_msg(node_id, services, "AddService"); + } + + public static GostDto UpdateRemoteService(Long node_id, String name, Integer out_port, String remoteAddr,String protocol, String strategy) { + JSONObject data = new JSONObject(); + data.put("name", name + "_tls"); + data.put("addr", ":" + out_port); + JSONObject handler = new JSONObject(); + handler.put("type", "relay"); + data.put("handler", handler); + JSONObject listener = new JSONObject(); + listener.put("type", protocol); + data.put("listener", listener); + JSONObject forwarder = new JSONObject(); + JSONArray nodes = new JSONArray(); + + String[] split = remoteAddr.split(","); + int num = 1; + for (String addr : split) { + JSONObject node = new JSONObject(); + node.put("name", "node_" + num ); + node.put("addr", addr); + nodes.add(node); + num ++; + } + if (strategy == null || strategy.equals("")){ + strategy = "fifo"; + } + forwarder.put("nodes", nodes); + JSONObject selector = new JSONObject(); + selector.put("strategy", strategy); + selector.put("maxFails", 1); + selector.put("failTimeout", "10s"); + forwarder.put("selector", selector); + + data.put("forwarder", forwarder); + JSONArray services = new JSONArray(); + services.add(data); + return WebSocketServer.send_msg(node_id, services, "UpdateService"); + } + + public static GostDto DeleteRemoteService(Long node_id, String name) { + JSONArray data = new JSONArray(); + data.add(name + "_tls"); + JSONObject req = new JSONObject(); + req.put("services", data); + return WebSocketServer.send_msg(node_id, req, "DeleteService"); + } + public static GostDto PauseService(Long node_id, String name) { JSONObject data = new JSONObject(); JSONArray services = new JSONArray(); @@ -162,60 +246,6 @@ public class GostUtil { return WebSocketServer.send_msg(node_id, data, "DeleteChains"); } - public static GostDto AddRemoteService(Long node_id, String name, Integer out_port, String remoteAddr, String protocol) { - JSONObject data = new JSONObject(); - data.put("name", name + "_tls"); - data.put("addr", ":" + out_port); - JSONObject handler = new JSONObject(); - handler.put("type", "relay"); - data.put("handler", handler); - JSONObject listener = new JSONObject(); - listener.put("type", protocol); - data.put("listener", listener); - JSONObject forwarder = new JSONObject(); - JSONArray nodes = new JSONArray(); - JSONObject node = new JSONObject(); - node.put("name", name + "_node"); - node.put("addr", remoteAddr); - nodes.add(node); - forwarder.put("nodes", nodes); - data.put("forwarder", forwarder); - JSONArray services = new JSONArray(); - services.add(data); - return WebSocketServer.send_msg(node_id, services, "AddService"); - } - - public static GostDto UpdateRemoteService(Long node_id, String name, Integer out_port, String remoteAddr) { - JSONObject data = new JSONObject(); - data.put("name", name + "_tls"); - data.put("addr", ":" + out_port); - JSONObject handler = new JSONObject(); - handler.put("type", "relay"); - data.put("handler", handler); - JSONObject listener = new JSONObject(); - listener.put("type", "tls"); - data.put("listener", listener); - JSONObject forwarder = new JSONObject(); - JSONArray nodes = new JSONArray(); - JSONObject node = new JSONObject(); - node.put("name", name + "_node"); - node.put("addr", remoteAddr); - nodes.add(node); - forwarder.put("nodes", nodes); - data.put("forwarder", forwarder); - JSONArray services = new JSONArray(); - services.add(data); - return WebSocketServer.send_msg(node_id, services, "UpdateService"); - } - - public static GostDto DeleteRemoteService(Long node_id, String name) { - JSONArray data = new JSONArray(); - data.add(name + "_tls"); - JSONObject req = new JSONObject(); - req.put("services", data); - return WebSocketServer.send_msg(node_id, req, "DeleteService"); - } - private static JSONObject createLimiterData(Long name, String speed) { JSONObject data = new JSONObject(); data.put("name", name.toString()); @@ -225,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) { + 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")){ @@ -249,7 +279,7 @@ public class GostUtil { // 端口转发需要配置转发器 if (isPortForwarding(fow_type)) { - JSONObject forwarder = createForwarder(protocol, remoteAddr); + JSONObject forwarder = createForwarder(protocol, remoteAddr, strategy); service.put("forwarder", forwarder); } @@ -274,14 +304,31 @@ public class GostUtil { return listener; } - private static JSONObject createForwarder(String protocol, String remoteAddr) { + private static JSONObject createForwarder(String protocol, String remoteAddr, String strategy) { JSONObject forwarder = new JSONObject(); JSONArray nodes = new JSONArray(); - JSONObject node = new JSONObject(); - node.put("name", protocol); - node.put("addr", remoteAddr); - nodes.add(node); + + String[] split = remoteAddr.split(","); + int num = 1; + for (String addr : split) { + JSONObject node = new JSONObject(); + node.put("name", "node_" + num ); + node.put("addr", addr); + nodes.add(node); + num ++; + } + + if (strategy == null || strategy.equals("")){ + strategy = "fifo"; + } + forwarder.put("nodes", nodes); + + JSONObject selector = new JSONObject(); + selector.put("strategy", strategy); + selector.put("maxFails", 1); + selector.put("failTimeout", "10s"); + forwarder.put("selector", selector); return forwarder; } 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 7744854..6beff83 100644 --- a/springboot-backend/src/main/java/com/admin/entity/Forward.java +++ b/springboot-backend/src/main/java/com/admin/entity/Forward.java @@ -32,6 +32,8 @@ public class Forward extends BaseEntity{ private String remoteAddr; + private String strategy; + private Long inFlow; private Long outFlow; 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 5c46db5..9dd686a 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 @@ -659,7 +659,7 @@ public class ForwardServiceImpl extends ServiceImpl impl } // 创建主服务 - R serviceResult = createMainService(nodeInfo.getInNode(), serviceName, forward, limiter, tunnel.getType(), tunnel); + 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) { @@ -693,7 +693,7 @@ public class ForwardServiceImpl extends ServiceImpl impl } // 更新主服务 - R serviceResult = updateMainService(nodeInfo.getInNode(), serviceName, forward, limiter, tunnel.getType(), tunnel); + R serviceResult = updateMainService(nodeInfo.getInNode(), serviceName, forward, limiter, tunnel.getType(), tunnel, forward.getStrategy()); if (serviceResult.getCode() != 0) { updateForwardStatusToError(forward); return serviceResult; @@ -825,18 +825,15 @@ public class ForwardServiceImpl extends ServiceImpl impl * 创建远程服务 */ private R createRemoteService(Node outNode, String serviceName, Forward forward, String protocol) { - GostDto result = GostUtil.AddRemoteService(outNode.getId(), - serviceName, forward.getOutPort(), - forward.getRemoteAddr(), protocol); + GostDto result = GostUtil.AddRemoteService(outNode.getId(), serviceName, forward.getOutPort(), forward.getRemoteAddr(), protocol, forward.getStrategy()); return isGostOperationSuccess(result) ? R.ok() : R.err(result.getMsg()); } /** * 创建主服务 */ - private R createMainService(Node inNode, String serviceName, Forward forward, Integer limiter, Integer tunnelType, Tunnel tunnel) { - GostDto result = GostUtil.AddService(inNode.getId(), serviceName, - forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel); + 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()); } @@ -863,11 +860,11 @@ public class ForwardServiceImpl extends ServiceImpl impl // 创建新远程服务 GostDto createResult = GostUtil.UpdateRemoteService(outNode.getId(), serviceName, forward.getOutPort(), - forward.getRemoteAddr()); + forward.getRemoteAddr(), protocol, forward.getStrategy()); if (createResult.getMsg().contains(GOST_NOT_FOUND_MSG)) { createResult = GostUtil.AddRemoteService(outNode.getId(), serviceName, forward.getOutPort(), - forward.getRemoteAddr(),protocol); + forward.getRemoteAddr(),protocol, forward.getStrategy()); } return isGostOperationSuccess(createResult) ? R.ok() : R.err(createResult.getMsg()); } @@ -875,14 +872,11 @@ public class ForwardServiceImpl extends ServiceImpl impl /** * 更新主服务 */ - private R updateMainService(Node inNode, String serviceName, Forward forward, Integer limiter, Integer tunnelType, Tunnel tunnel) { - GostDto result = GostUtil.UpdateService(inNode.getId(), serviceName, - forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel); + 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); if (result.getMsg().contains(GOST_NOT_FOUND_MSG)) { - result = GostUtil.AddService(inNode.getId(), serviceName, - forward.getInPort(), limiter, forward.getRemoteAddr(), - tunnelType, tunnel); + 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/TunnelServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java index 1734f51..e5a6b64 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java @@ -727,7 +727,7 @@ public class TunnelServiceImpl extends ServiceImpl impleme // 3. 根据隧道类型执行不同的诊断策略 if (tunnel.getType() == TUNNEL_TYPE_PORT_FORWARD) { // 端口转发:只给入口节点发送诊断指令,ping谷歌DNS - DiagnosisResult inResult = performPingDiagnosisWithConnectionCheck(inNode, "8.8.8.8", "入口->外网"); + DiagnosisResult inResult = performPingDiagnosisWithConnectionCheck(inNode, "www.google.com", "入口->外网"); results.add(inResult); } else { // 隧道转发:入口ping出口,出口ping谷歌DNS @@ -735,7 +735,7 @@ public class TunnelServiceImpl extends ServiceImpl impleme results.add(inToOutResult); // 先检查出口节点的真实连接状态,然后再进行诊断 - DiagnosisResult outToExternalResult = performPingDiagnosisWithConnectionCheck(outNode, "8.8.8.8", "出口->外网"); + DiagnosisResult outToExternalResult = performPingDiagnosisWithConnectionCheck(outNode, "www.google.com", "出口->外网"); results.add(outToExternalResult); } 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 74b4c6a..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,8 +499,7 @@ public class UserTunnelServiceImpl extends ServiceImpl + +
+
+ 负载策略: + + {{ getStrategyText(forward.strategy) }} + +
+
@@ -124,8 +136,12 @@ >
- - {{ forward.remoteAddr }} + + {{ formatTargetAddress(forward.remoteAddr) }}
@@ -226,13 +242,35 @@ -
- 格式: IPv4(IP:端口)、IPv6([IPv6]:端口)或域名:端口 + 格式: IPv4(IP:端口)、IPv6([IPv6]:端口)或域名:端口
+ 支持多个地址,每行一个地址 +
+
+ + + + + + + + +
+ 多个目标地址的负载均衡策略,主备模式优先使用第一个地址
@@ -359,7 +397,8 @@ export default { name: '', tunnelId: null, inPort: null, - remoteAddr: '' + remoteAddr: '', + strategy: 'fifo' // 默认主备模式 }, // 表单验证规则 @@ -399,32 +438,38 @@ export default { { validator: (rule, value, callback) => { if (value) { - // IPv4格式:192.168.1.1:8080 + // 按行分割地址 + const addresses = value.split('\n').map(addr => addr.trim()).filter(addr => addr); + + if (addresses.length === 0) { + callback(new Error('请输入至少一个有效地址')); + return; + } + + // 地址验证正则 const ipv4Pattern = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):\d+$/; - - // IPv6格式(带方括号):[2001:db8::1]:8080 - const ipv6BracketPattern = /^\[([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}\]:\d+$/; - - // IPv6格式(简化版本,包括::1, ::, ::ffff:192.0.2.1等) - const ipv6SimplifiedPattern = /^\[(::1|::|::ffff:[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|([0-9a-fA-F]{1,4}:){1,7}:|:([0-9a-fA-F]{1,4}:){1,6}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,6}:([0-9a-fA-F]{1,4}:){1,6}[0-9a-fA-F]{1,4})\]:\d+$/; - - // 域名格式:example.com:8080 + const ipv6FullPattern = /^\[((([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4}|:))|(([0-9a-fA-F]{1,4}:){6}(:[0-9a-fA-F]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-fA-F]{1,4}:){5}(((:[0-9a-fA-F]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-fA-F]{1,4}:){4}(((:[0-9a-fA-F]{1,4}){1,3})|((:[0-9a-fA-F]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){3}(((:[0-9a-fA-F]{1,4}){1,4})|((:[0-9a-fA-F]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){2}(((:[0-9a-fA-F]{1,4}){1,5})|((:[0-9a-fA-F]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){1}(((:[0-9a-fA-F]{1,4}){1,6})|((:[0-9a-fA-F]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-fA-F]{1,4}){1,7})|((:[0-9a-fA-F]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))\]:\d+$/; const domainPattern = /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*:\d+$/; - // 更完整的IPv6正则(支持各种压缩格式) - const ipv6FullPattern = /^\[((([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4}|:))|(([0-9a-fA-F]{1,4}:){6}(:[0-9a-fA-F]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-fA-F]{1,4}:){5}(((:[0-9a-fA-F]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-fA-F]{1,4}:){4}(((:[0-9a-fA-F]{1,4}){1,3})|((:[0-9a-fA-F]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){3}(((:[0-9a-fA-F]{1,4}){1,4})|((:[0-9a-fA-F]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){2}(((:[0-9a-fA-F]{1,4}){1,5})|((:[0-9a-fA-F]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){1}(((:[0-9a-fA-F]{1,4}){1,6})|((:[0-9a-fA-F]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-fA-F]{1,4}){1,7})|((:[0-9a-fA-F]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))\]:\d+$/; - - if (ipv4Pattern.test(value) || ipv6FullPattern.test(value) || domainPattern.test(value)) { - callback(); - } else { - callback(new Error('请输入正确的地址格式,如: 192.168.1.100:8080 或 [2001:db8::1]:8080 或 example.com:8080')); + // 验证每个地址 + for (let i = 0; i < addresses.length; i++) { + const addr = addresses[i]; + if (!ipv4Pattern.test(addr) && !ipv6FullPattern.test(addr) && !domainPattern.test(addr)) { + callback(new Error(`第${i + 1}行地址格式错误: ${addr}\n请使用正确格式,如: 192.168.1.100:8080 或 [2001:db8::1]:8080 或 example.com:8080`)); + return; + } } + + callback(); } else { callback(); } }, trigger: 'blur' } + ], + strategy: [ + { required: true, message: '请选择负载均衡策略', trigger: 'change' } ] } } @@ -550,7 +595,9 @@ export default { this.forwardForm = { ...row, userId: row.userId, // 确保userId被正确设置 - inPort: row.inPort || null // 设置入口端口 + inPort: row.inPort || null, // 设置入口端口 + remoteAddr: row.remoteAddr ? row.remoteAddr.split(',').map(addr => addr.trim()).join('\n') : '', // 将逗号分隔转换为换行分隔 + strategy: row.strategy || 'fifo' // 设置负载均衡策略,默认主备模式 }; this.handleTunnelChange(row.tunnelId); this.dialogVisible = true; @@ -654,6 +701,13 @@ export default { this.submitLoading = true; let res; + // 处理远程地址:将换行符转换为逗号分隔 + const processedRemoteAddr = this.forwardForm.remoteAddr + .split('\n') + .map(addr => addr.trim()) + .filter(addr => addr) + .join(','); + if (this.isEdit) { // 更新时确保包含必要字段 const updateData = { @@ -662,7 +716,8 @@ export default { name: this.forwardForm.name, tunnelId: this.forwardForm.tunnelId, inPort: this.forwardForm.inPort || null, - remoteAddr: this.forwardForm.remoteAddr + remoteAddr: processedRemoteAddr, + strategy: this.getAddressCount(this.forwardForm.remoteAddr) > 1 ? this.forwardForm.strategy : 'fifo' }; res = await updateForward(updateData); } else { @@ -671,7 +726,8 @@ export default { name: this.forwardForm.name, tunnelId: this.forwardForm.tunnelId, inPort: this.forwardForm.inPort || null, - remoteAddr: this.forwardForm.remoteAddr + remoteAddr: processedRemoteAddr, + strategy: this.getAddressCount(this.forwardForm.remoteAddr) > 1 ? this.forwardForm.strategy : 'fifo' }; res = await createForward(createData); } @@ -701,7 +757,8 @@ export default { name: '', tunnelId: null, inPort: null, - remoteAddr: '' + remoteAddr: '', + strategy: 'fifo' }; this.selectedTunnel = null; this.$nextTick(() => { @@ -858,6 +915,74 @@ export default { this.addressDialogVisible = true; }, + // 显示目标地址弹窗 + showTargetAddressDialog(addressString, title) { + if (!addressString) return; + + const addresses = addressString.split(',').map(addr => addr.trim()).filter(addr => addr); + + // 如果只有一个地址,直接复制,不显示弹窗 + if (addresses.length <= 1) { + this.copyToClipboard(addressString, title); + return; + } + + // 格式化地址列表 + this.addressList = addresses.map((address, index) => { + // 提取IP部分用于显示 + let displayIp = address; + if (address.includes(':')) { + if (address.startsWith('[')) { + // IPv6格式:[2001:db8::1]:8080 + const match = address.match(/^\[(.+)\]:/); + displayIp = match ? match[1] : address; + } else { + // IPv4格式:192.168.1.1:8080 或域名格式 + const lastColonIndex = address.lastIndexOf(':'); + displayIp = lastColonIndex > 0 ? address.substring(0, lastColonIndex) : address; + } + } + + return { + id: index, + ip: displayIp, + address: address, + copying: false // 复制状态 + }; + }); + + this.addressDialogTitle = `${title}列表 (共${addresses.length}个)`; + this.addressDialogVisible = true; + }, + + // 格式化目标地址显示 + formatTargetAddress(addressString) { + if (!addressString) return ''; + + const addresses = addressString.split(',').map(addr => addr.trim()).filter(addr => addr); + + if (addresses.length === 0) return ''; + if (addresses.length === 1) return addresses[0]; + + // 多个地址时显示第一个地址 + 数量提示 + return `${addresses[0]} 等${addresses.length}个目标`; + }, + + // 检查是否有多个目标地址 + hasMultipleTargetAddresses(addressString) { + if (!addressString) return false; + const addresses = addressString.split(',').map(addr => addr.trim()).filter(addr => addr); + return addresses.length > 1; + }, + + // 获取地址数量(用于表单中策略选择显示控制) + getAddressCount(addressString) { + if (!addressString) return 0; + // 按行分割地址 + const addresses = addressString.split('\n').map(addr => addr.trim()).filter(addr => addr); + return addresses.length; + }, + // 复制单个地址 async copyAddress(addressItem) { try { @@ -911,6 +1036,34 @@ export default { return '未知'; } }, + + // 获取策略类型 + getStrategyType(strategy) { + switch (strategy) { + case 'fifo': + return 'primary'; // 蓝色 - 主备模式 + case 'round': + return 'success'; // 绿色 - 轮询模式 + case 'rand': + return 'warning'; // 橙色 - 随机模式 + default: + return 'info'; // 默认 + } + }, + + // 获取策略文本 + getStrategyText(strategy) { + switch (strategy) { + case 'fifo': + return '主备模式'; + case 'round': + return '轮询模式'; + case 'rand': + return '随机模式'; + default: + return '未知策略'; + } + },