mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-29 16:06:36 +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)
|
||||
}
|
||||
|
||||
// 预处理:将字符串格式的 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 '未知策略';
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user