mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-09 03:06:37 +08:00
增加出口负载均衡
This commit is contained in:
Binary file not shown.
@@ -428,8 +428,14 @@ func (w *WebSocketReporter) handleAddService(data interface{}) error {
|
|||||||
return fmt.Errorf("序列化数据失败: %v", err)
|
return fmt.Errorf("序列化数据失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 预处理:将字符串格式的 duration 转换为纳秒数
|
||||||
|
processedData, err := w.preprocessDurationFields(jsonData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("预处理duration字段失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
var services []config.ServiceConfig
|
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)
|
return fmt.Errorf("解析服务配置失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,8 +449,14 @@ func (w *WebSocketReporter) handleUpdateService(data interface{}) error {
|
|||||||
return fmt.Errorf("序列化数据失败: %v", err)
|
return fmt.Errorf("序列化数据失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 预处理:将字符串格式的 duration 转换为纳秒数
|
||||||
|
processedData, err := w.preprocessDurationFields(jsonData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("预处理duration字段失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
var services []config.ServiceConfig
|
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)
|
return fmt.Errorf("解析服务配置失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -977,3 +989,49 @@ func isValidHostname(hostname string) bool {
|
|||||||
|
|
||||||
return true
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ CREATE TABLE `forward` (
|
|||||||
`tunnel_id` int(10) NOT NULL,
|
`tunnel_id` int(10) NOT NULL,
|
||||||
`in_port` int(10) NOT NULL,
|
`in_port` int(10) NOT NULL,
|
||||||
`out_port` int(10) DEFAULT 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',
|
`in_flow` bigint(20) NOT NULL DEFAULT '0',
|
||||||
`out_flow` bigint(20) NOT NULL DEFAULT '0',
|
`out_flow` bigint(20) NOT NULL DEFAULT '0',
|
||||||
`created_time` bigint(20) NOT NULL,
|
`created_time` bigint(20) NOT NULL,
|
||||||
@@ -53,7 +54,7 @@ CREATE TABLE `node` (
|
|||||||
`id` int(10) NOT NULL,
|
`id` int(10) NOT NULL,
|
||||||
`name` varchar(100) NOT NULL,
|
`name` varchar(100) NOT NULL,
|
||||||
`secret` varchar(100) NOT NULL,
|
`secret` varchar(100) NOT NULL,
|
||||||
`ip` varchar(100) DEFAULT NULL,
|
`ip` longtext DEFAULT NULL,
|
||||||
`server_ip` varchar(100) NOT NULL,
|
`server_ip` varchar(100) NOT NULL,
|
||||||
`created_time` bigint(20) NOT NULL,
|
`created_time` bigint(20) NOT NULL,
|
||||||
`updated_time` bigint(20) DEFAULT NULL,
|
`updated_time` bigint(20) DEFAULT NULL,
|
||||||
|
|||||||
@@ -439,6 +439,25 @@ UPDATE \`node\`
|
|||||||
SET \`server_ip\` = \`ip\`
|
SET \`server_ip\` = \`ip\`
|
||||||
WHERE \`server_ip\` IS NULL;
|
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(如果不存在)
|
-- tunnel 表:添加 tcp_listen_addr、udp_listen_addr、protocol(如果不存在)
|
||||||
|
|
||||||
-- tcp_listen_addr
|
-- tcp_listen_addr
|
||||||
@@ -517,6 +536,48 @@ DEALLOCATE PREPARE stmt;
|
|||||||
UPDATE \`tunnel\`
|
UPDATE \`tunnel\`
|
||||||
SET \`traffic_ratio\` = 1.0
|
SET \`traffic_ratio\` = 1.0
|
||||||
WHERE \`traffic_ratio\` IS NULL;
|
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
|
EOF
|
||||||
|
|
||||||
# 检查数据库容器
|
# 检查数据库容器
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ public class ForwardDto {
|
|||||||
|
|
||||||
@NotBlank(message = "远程地址不能为空")
|
@NotBlank(message = "远程地址不能为空")
|
||||||
private String remoteAddr;
|
private String remoteAddr;
|
||||||
|
|
||||||
|
private String strategy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 入口端口(可选,为空时自动分配)
|
* 入口端口(可选,为空时自动分配)
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ public class ForwardUpdateDto {
|
|||||||
|
|
||||||
@NotBlank(message = "远程地址不能为空")
|
@NotBlank(message = "远程地址不能为空")
|
||||||
private String remoteAddr;
|
private String remoteAddr;
|
||||||
|
|
||||||
|
private String strategy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 入口端口(可选,为空时自动分配)
|
* 入口端口(可选,为空时自动分配)
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ public class ForwardWithTunnelDto {
|
|||||||
*/
|
*/
|
||||||
private Long outFlow;
|
private Long outFlow;
|
||||||
|
|
||||||
|
private String strategy;
|
||||||
// /**
|
// /**
|
||||||
// * 入口端口开始
|
// * 入口端口开始
|
||||||
// */
|
// */
|
||||||
|
|||||||
@@ -31,21 +31,21 @@ public class GostUtil {
|
|||||||
return WebSocketServer.send_msg(node_id, req, "DeleteLimiters");
|
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();
|
JSONArray services = new JSONArray();
|
||||||
String[] protocols = {"tcp", "udp"};
|
String[] protocols = {"tcp", "udp"};
|
||||||
for (String protocol : protocols) {
|
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);
|
services.add(service);
|
||||||
}
|
}
|
||||||
return WebSocketServer.send_msg(node_id, services, "AddService");
|
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();
|
JSONArray services = new JSONArray();
|
||||||
String[] protocols = {"tcp", "udp"};
|
String[] protocols = {"tcp", "udp"};
|
||||||
for (String protocol : protocols) {
|
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);
|
services.add(service);
|
||||||
}
|
}
|
||||||
return WebSocketServer.send_msg(node_id, services, "UpdateService");
|
return WebSocketServer.send_msg(node_id, services, "UpdateService");
|
||||||
@@ -60,6 +60,90 @@ public class GostUtil {
|
|||||||
return WebSocketServer.send_msg(node_id, data, "DeleteService");
|
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) {
|
public static GostDto PauseService(Long node_id, String name) {
|
||||||
JSONObject data = new JSONObject();
|
JSONObject data = new JSONObject();
|
||||||
JSONArray services = new JSONArray();
|
JSONArray services = new JSONArray();
|
||||||
@@ -162,60 +246,6 @@ public class GostUtil {
|
|||||||
return WebSocketServer.send_msg(node_id, data, "DeleteChains");
|
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) {
|
private static JSONObject createLimiterData(Long name, String speed) {
|
||||||
JSONObject data = new JSONObject();
|
JSONObject data = new JSONObject();
|
||||||
data.put("name", name.toString());
|
data.put("name", name.toString());
|
||||||
@@ -225,7 +255,7 @@ public class GostUtil {
|
|||||||
return data;
|
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();
|
JSONObject service = new JSONObject();
|
||||||
service.put("name", name + "_" + protocol);
|
service.put("name", name + "_" + protocol);
|
||||||
if (Objects.equals(protocol, "tcp")){
|
if (Objects.equals(protocol, "tcp")){
|
||||||
@@ -249,7 +279,7 @@ public class GostUtil {
|
|||||||
|
|
||||||
// 端口转发需要配置转发器
|
// 端口转发需要配置转发器
|
||||||
if (isPortForwarding(fow_type)) {
|
if (isPortForwarding(fow_type)) {
|
||||||
JSONObject forwarder = createForwarder(protocol, remoteAddr);
|
JSONObject forwarder = createForwarder(protocol, remoteAddr, strategy);
|
||||||
service.put("forwarder", forwarder);
|
service.put("forwarder", forwarder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,14 +304,31 @@ public class GostUtil {
|
|||||||
return listener;
|
return listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JSONObject createForwarder(String protocol, String remoteAddr) {
|
private static JSONObject createForwarder(String protocol, String remoteAddr, String strategy) {
|
||||||
JSONObject forwarder = new JSONObject();
|
JSONObject forwarder = new JSONObject();
|
||||||
JSONArray nodes = new JSONArray();
|
JSONArray nodes = new JSONArray();
|
||||||
JSONObject node = new JSONObject();
|
|
||||||
node.put("name", protocol);
|
String[] split = remoteAddr.split(",");
|
||||||
node.put("addr", remoteAddr);
|
int num = 1;
|
||||||
nodes.add(node);
|
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);
|
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;
|
return forwarder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ public class Forward extends BaseEntity{
|
|||||||
|
|
||||||
private String remoteAddr;
|
private String remoteAddr;
|
||||||
|
|
||||||
|
private String strategy;
|
||||||
|
|
||||||
private Long inFlow;
|
private Long inFlow;
|
||||||
|
|
||||||
private Long outFlow;
|
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) {
|
if (serviceResult.getCode() != 0) {
|
||||||
GostUtil.DeleteChains(nodeInfo.getInNode().getId(), serviceName);
|
GostUtil.DeleteChains(nodeInfo.getInNode().getId(), serviceName);
|
||||||
if (nodeInfo.getOutNode() != null) {
|
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) {
|
if (serviceResult.getCode() != 0) {
|
||||||
updateForwardStatusToError(forward);
|
updateForwardStatusToError(forward);
|
||||||
return serviceResult;
|
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) {
|
private R createRemoteService(Node outNode, String serviceName, Forward forward, String protocol) {
|
||||||
GostDto result = GostUtil.AddRemoteService(outNode.getId(),
|
GostDto result = GostUtil.AddRemoteService(outNode.getId(), serviceName, forward.getOutPort(), forward.getRemoteAddr(), protocol, forward.getStrategy());
|
||||||
serviceName, forward.getOutPort(),
|
|
||||||
forward.getRemoteAddr(), protocol);
|
|
||||||
return isGostOperationSuccess(result) ? R.ok() : R.err(result.getMsg());
|
return isGostOperationSuccess(result) ? R.ok() : R.err(result.getMsg());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建主服务
|
* 创建主服务
|
||||||
*/
|
*/
|
||||||
private R createMainService(Node inNode, String serviceName, Forward forward, Integer limiter, Integer tunnelType, Tunnel 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,
|
GostDto result = GostUtil.AddService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy);
|
||||||
forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel);
|
|
||||||
return isGostOperationSuccess(result) ? R.ok() : R.err(result.getMsg());
|
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(),
|
GostDto createResult = GostUtil.UpdateRemoteService(outNode.getId(),
|
||||||
serviceName, forward.getOutPort(),
|
serviceName, forward.getOutPort(),
|
||||||
forward.getRemoteAddr());
|
forward.getRemoteAddr(), protocol, forward.getStrategy());
|
||||||
if (createResult.getMsg().contains(GOST_NOT_FOUND_MSG)) {
|
if (createResult.getMsg().contains(GOST_NOT_FOUND_MSG)) {
|
||||||
createResult = GostUtil.AddRemoteService(outNode.getId(),
|
createResult = GostUtil.AddRemoteService(outNode.getId(),
|
||||||
serviceName, forward.getOutPort(),
|
serviceName, forward.getOutPort(),
|
||||||
forward.getRemoteAddr(),protocol);
|
forward.getRemoteAddr(),protocol, forward.getStrategy());
|
||||||
}
|
}
|
||||||
return isGostOperationSuccess(createResult) ? R.ok() : R.err(createResult.getMsg());
|
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) {
|
private R updateMainService(Node inNode, String serviceName, Forward forward, Integer limiter, Integer tunnelType, Tunnel tunnel, String strategy) {
|
||||||
GostDto result = GostUtil.UpdateService(inNode.getId(), serviceName,
|
GostDto result = GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy);
|
||||||
forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel);
|
|
||||||
|
|
||||||
if (result.getMsg().contains(GOST_NOT_FOUND_MSG)) {
|
if (result.getMsg().contains(GOST_NOT_FOUND_MSG)) {
|
||||||
result = GostUtil.AddService(inNode.getId(), serviceName,
|
result = GostUtil.AddService(inNode.getId(), serviceName, forward.getInPort(), limiter, forward.getRemoteAddr(), tunnelType, tunnel, strategy);
|
||||||
forward.getInPort(), limiter, forward.getRemoteAddr(),
|
|
||||||
tunnelType, tunnel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return isGostOperationSuccess(result) ? R.ok() : R.err(result.getMsg());
|
return isGostOperationSuccess(result) ? R.ok() : R.err(result.getMsg());
|
||||||
|
|||||||
@@ -727,7 +727,7 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
// 3. 根据隧道类型执行不同的诊断策略
|
// 3. 根据隧道类型执行不同的诊断策略
|
||||||
if (tunnel.getType() == TUNNEL_TYPE_PORT_FORWARD) {
|
if (tunnel.getType() == TUNNEL_TYPE_PORT_FORWARD) {
|
||||||
// 端口转发:只给入口节点发送诊断指令,ping谷歌DNS
|
// 端口转发:只给入口节点发送诊断指令,ping谷歌DNS
|
||||||
DiagnosisResult inResult = performPingDiagnosisWithConnectionCheck(inNode, "8.8.8.8", "入口->外网");
|
DiagnosisResult inResult = performPingDiagnosisWithConnectionCheck(inNode, "www.google.com", "入口->外网");
|
||||||
results.add(inResult);
|
results.add(inResult);
|
||||||
} else {
|
} else {
|
||||||
// 隧道转发:入口ping出口,出口ping谷歌DNS
|
// 隧道转发:入口ping出口,出口ping谷歌DNS
|
||||||
@@ -735,7 +735,7 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
results.add(inToOutResult);
|
results.add(inToOutResult);
|
||||||
|
|
||||||
// 先检查出口节点的真实连接状态,然后再进行诊断
|
// 先检查出口节点的真实连接状态,然后再进行诊断
|
||||||
DiagnosisResult outToExternalResult = performPingDiagnosisWithConnectionCheck(outNode, "8.8.8.8", "出口->外网");
|
DiagnosisResult outToExternalResult = performPingDiagnosisWithConnectionCheck(outNode, "www.google.com", "出口->外网");
|
||||||
results.add(outToExternalResult);
|
results.add(outToExternalResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -499,8 +499,7 @@ public class UserTunnelServiceImpl extends ServiceImpl<UserTunnelMapper, UserTun
|
|||||||
String serviceName = buildServiceName(forward.getId(), Long.valueOf(userId), userTunnel.getId());
|
String serviceName = buildServiceName(forward.getId(), Long.valueOf(userId), userTunnel.getId());
|
||||||
|
|
||||||
// 6. 更新入口节点的主服务限速配置(使用批量UpdateService接口)
|
// 6. 更新入口节点的主服务限速配置(使用批量UpdateService接口)
|
||||||
GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), speedId,
|
GostUtil.UpdateService(inNode.getId(), serviceName, forward.getInPort(), speedId, forward.getRemoteAddr(), tunnel.getType(), tunnel, forward.getStrategy());
|
||||||
forward.getRemoteAddr(), tunnel.getType(), tunnel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
f.updated_time AS updatedTime,
|
f.updated_time AS updatedTime,
|
||||||
f.user_name AS userName,
|
f.user_name AS userName,
|
||||||
f.in_flow AS inFlow,
|
f.in_flow AS inFlow,
|
||||||
|
f.strategy AS strategy,
|
||||||
f.out_flow AS outFlow,
|
f.out_flow AS outFlow,
|
||||||
t.name AS tunnelName,
|
t.name AS tunnelName,
|
||||||
t.in_ip AS inIp,
|
t.in_ip AS inIp,
|
||||||
@@ -51,6 +52,7 @@
|
|||||||
f.user_name AS userName,
|
f.user_name AS userName,
|
||||||
f.in_flow AS inFlow,
|
f.in_flow AS inFlow,
|
||||||
f.out_flow AS outFlow,
|
f.out_flow AS outFlow,
|
||||||
|
f.strategy AS strategy,
|
||||||
t.name AS tunnelName,
|
t.name AS tunnelName,
|
||||||
t.in_ip AS inIp,
|
t.in_ip AS inIp,
|
||||||
t.in_port_sta AS inPortSta,
|
t.in_port_sta AS inPortSta,
|
||||||
|
|||||||
@@ -81,6 +81,18 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</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-row">
|
||||||
<div class="info-item full-width">
|
<div class="info-item full-width">
|
||||||
@@ -124,8 +136,12 @@
|
|||||||
></el-button>
|
></el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="address-value">
|
<div class="address-value">
|
||||||
<span class="value address-text">
|
<span
|
||||||
{{ forward.remoteAddr }}
|
class="value address-text"
|
||||||
|
@click="showTargetAddressDialog(forward.remoteAddr, '目标地址')"
|
||||||
|
:class="{ 'clickable': hasMultipleTargetAddresses(forward.remoteAddr) }"
|
||||||
|
>
|
||||||
|
{{ formatTargetAddress(forward.remoteAddr) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -226,13 +242,35 @@
|
|||||||
<el-form-item label="远程地址" prop="remoteAddr">
|
<el-form-item label="远程地址" prop="remoteAddr">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="forwardForm.remoteAddr"
|
v-model="forwardForm.remoteAddr"
|
||||||
placeholder="例如: 192.168.1.100:8080 或 [2001:db8::1]:8080"
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="一行一个地址"
|
||||||
clearable
|
clearable
|
||||||
>
|
>
|
||||||
<template slot="prepend">目标</template>
|
|
||||||
</el-input>
|
</el-input>
|
||||||
<div class="form-hint">
|
<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>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@@ -359,7 +397,8 @@ export default {
|
|||||||
name: '',
|
name: '',
|
||||||
tunnelId: null,
|
tunnelId: null,
|
||||||
inPort: null,
|
inPort: null,
|
||||||
remoteAddr: ''
|
remoteAddr: '',
|
||||||
|
strategy: 'fifo' // 默认主备模式
|
||||||
},
|
},
|
||||||
|
|
||||||
// 表单验证规则
|
// 表单验证规则
|
||||||
@@ -399,32 +438,38 @@ export default {
|
|||||||
{
|
{
|
||||||
validator: (rule, value, callback) => {
|
validator: (rule, value, callback) => {
|
||||||
if (value) {
|
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+$/;
|
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+$/;
|
||||||
|
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+$/;
|
||||||
// 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 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+$/;
|
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+$/;
|
for (let i = 0; i < addresses.length; i++) {
|
||||||
|
const addr = addresses[i];
|
||||||
if (ipv4Pattern.test(value) || ipv6FullPattern.test(value) || domainPattern.test(value)) {
|
if (!ipv4Pattern.test(addr) && !ipv6FullPattern.test(addr) && !domainPattern.test(addr)) {
|
||||||
callback();
|
callback(new Error(`第${i + 1}行地址格式错误: ${addr}\n请使用正确格式,如: 192.168.1.100:8080 或 [2001:db8::1]:8080 或 example.com:8080`));
|
||||||
} else {
|
return;
|
||||||
callback(new Error('请输入正确的地址格式,如: 192.168.1.100:8080 或 [2001:db8::1]:8080 或 example.com:8080'));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
callback();
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trigger: 'blur'
|
trigger: 'blur'
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
strategy: [
|
||||||
|
{ required: true, message: '请选择负载均衡策略', trigger: 'change' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -550,7 +595,9 @@ export default {
|
|||||||
this.forwardForm = {
|
this.forwardForm = {
|
||||||
...row,
|
...row,
|
||||||
userId: row.userId, // 确保userId被正确设置
|
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.handleTunnelChange(row.tunnelId);
|
||||||
this.dialogVisible = true;
|
this.dialogVisible = true;
|
||||||
@@ -654,6 +701,13 @@ export default {
|
|||||||
this.submitLoading = true;
|
this.submitLoading = true;
|
||||||
let res;
|
let res;
|
||||||
|
|
||||||
|
// 处理远程地址:将换行符转换为逗号分隔
|
||||||
|
const processedRemoteAddr = this.forwardForm.remoteAddr
|
||||||
|
.split('\n')
|
||||||
|
.map(addr => addr.trim())
|
||||||
|
.filter(addr => addr)
|
||||||
|
.join(',');
|
||||||
|
|
||||||
if (this.isEdit) {
|
if (this.isEdit) {
|
||||||
// 更新时确保包含必要字段
|
// 更新时确保包含必要字段
|
||||||
const updateData = {
|
const updateData = {
|
||||||
@@ -662,7 +716,8 @@ export default {
|
|||||||
name: this.forwardForm.name,
|
name: this.forwardForm.name,
|
||||||
tunnelId: this.forwardForm.tunnelId,
|
tunnelId: this.forwardForm.tunnelId,
|
||||||
inPort: this.forwardForm.inPort || null,
|
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);
|
res = await updateForward(updateData);
|
||||||
} else {
|
} else {
|
||||||
@@ -671,7 +726,8 @@ export default {
|
|||||||
name: this.forwardForm.name,
|
name: this.forwardForm.name,
|
||||||
tunnelId: this.forwardForm.tunnelId,
|
tunnelId: this.forwardForm.tunnelId,
|
||||||
inPort: this.forwardForm.inPort || null,
|
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);
|
res = await createForward(createData);
|
||||||
}
|
}
|
||||||
@@ -701,7 +757,8 @@ export default {
|
|||||||
name: '',
|
name: '',
|
||||||
tunnelId: null,
|
tunnelId: null,
|
||||||
inPort: null,
|
inPort: null,
|
||||||
remoteAddr: ''
|
remoteAddr: '',
|
||||||
|
strategy: 'fifo'
|
||||||
};
|
};
|
||||||
this.selectedTunnel = null;
|
this.selectedTunnel = null;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -858,6 +915,74 @@ export default {
|
|||||||
this.addressDialogVisible = true;
|
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) {
|
async copyAddress(addressItem) {
|
||||||
try {
|
try {
|
||||||
@@ -911,6 +1036,34 @@ export default {
|
|||||||
return '未知';
|
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 '未知策略';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user