增加出口负载均衡

This commit is contained in:
qaq
2025-07-01 15:13:47 +08:00
parent 0414c886c1
commit af53f98426
14 changed files with 437 additions and 115 deletions
BIN
View File
Binary file not shown.
+60 -2
View File
@@ -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
}
}
+3 -2
View File
@@ -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,
+61
View File
@@ -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
# 检查数据库容器
@@ -17,6 +17,8 @@ public class ForwardDto {
@NotBlank(message = "远程地址不能为空")
private String remoteAddr;
private String strategy;
/**
* 入口端口(可选,为空时自动分配)
@@ -23,6 +23,8 @@ public class ForwardUpdateDto {
@NotBlank(message = "远程地址不能为空")
private String remoteAddr;
private String strategy;
/**
* 入口端口(可选,为空时自动分配)
@@ -84,6 +84,7 @@ public class ForwardWithTunnelDto {
*/
private Long outFlow;
private String strategy;
// /**
// * 入口端口开始
// */
@@ -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;
}
@@ -32,6 +32,8 @@ public class Forward extends BaseEntity{
private String remoteAddr;
private String strategy;
private Long inFlow;
private Long outFlow;
@@ -659,7 +659,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> 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<ForwardMapper, Forward> 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<ForwardMapper, Forward> 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<ForwardMapper, Forward> 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<ForwardMapper, Forward> 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());
@@ -727,7 +727,7 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> 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<TunnelMapper, Tunnel> impleme
results.add(inToOutResult);
// 先检查出口节点的真实连接状态,然后再进行诊断
DiagnosisResult outToExternalResult = performPingDiagnosisWithConnectionCheck(outNode, "8.8.8.8", "出口->外网");
DiagnosisResult outToExternalResult = performPingDiagnosisWithConnectionCheck(outNode, "www.google.com", "出口->外网");
results.add(outToExternalResult);
}
@@ -499,8 +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);
GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), speedId, forward.getRemoteAddr(), tunnel.getType(), tunnel, forward.getStrategy());
}
}
}
@@ -17,6 +17,7 @@
f.updated_time AS updatedTime,
f.user_name AS userName,
f.in_flow AS inFlow,
f.strategy AS strategy,
f.out_flow AS outFlow,
t.name AS tunnelName,
t.in_ip AS inIp,
@@ -51,6 +52,7 @@
f.user_name AS userName,
f.in_flow AS inFlow,
f.out_flow AS outFlow,
f.strategy AS strategy,
t.name AS tunnelName,
t.in_ip AS inIp,
t.in_port_sta AS inPortSta,
+179 -26
View File
@@ -81,6 +81,18 @@
</el-tag>
</div>
</div>
<div class="info-row inline-on-mobile" v-if="hasMultipleTargetAddresses(forward.remoteAddr)">
<div class="info-item">
<span class="label">负载策略:</span>
<el-tag
:type="getStrategyType(forward.strategy)"
size="small"
>
{{ getStrategyText(forward.strategy) }}
</el-tag>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
@@ -124,8 +136,12 @@
></el-button>
</div>
<div class="address-value">
<span class="value address-text">
{{ forward.remoteAddr }}
<span
class="value address-text"
@click="showTargetAddressDialog(forward.remoteAddr, '目标地址')"
:class="{ 'clickable': hasMultipleTargetAddresses(forward.remoteAddr) }"
>
{{ formatTargetAddress(forward.remoteAddr) }}
</span>
</div>
</div>
@@ -226,13 +242,35 @@
<el-form-item label="远程地址" prop="remoteAddr">
<el-input
v-model="forwardForm.remoteAddr"
placeholder="例如: 192.168.1.100:8080 或 [2001:db8::1]:8080"
type="textarea"
:rows="4"
placeholder="一行一个地址"
clearable
>
<template slot="prepend">目标</template>
</el-input>
<div class="form-hint">
格式: IPv4(IP:端口)、IPv6([IPv6]:端口)或域名:端口
格式: IPv4(IP:端口)、IPv6([IPv6]:端口)或域名:端口<br>
支持多个地址,每行一个地址
</div>
</el-form-item>
<!-- 负载均衡策略选择 -->
<el-form-item
label="负载策略"
prop="strategy"
v-if="getAddressCount(forwardForm.remoteAddr) > 1"
>
<el-select
v-model="forwardForm.strategy"
placeholder="请选择负载均衡策略"
style="width: 100%;"
>
<el-option value="fifo" label="主备模式 - 自上而下"></el-option>
<el-option value="round" label="轮询模式 - 依次轮换"></el-option>
<el-option value="rand" label="随机模式 - 随机选择"></el-option>
</el-select>
<div class="form-hint">
多个目标地址的负载均衡策略,主备模式优先使用第一个地址
</div>
</el-form-item>
@@ -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 '未知策略';
}
},