diff --git a/springboot-backend/src/main/java/com/admin/common/utils/GostUtil.java b/springboot-backend/src/main/java/com/admin/common/utils/GostUtil.java index f2557f8..5a1ff7f 100644 --- a/springboot-backend/src/main/java/com/admin/common/utils/GostUtil.java +++ b/springboot-backend/src/main/java/com/admin/common/utils/GostUtil.java @@ -7,6 +7,7 @@ import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import org.apache.commons.lang3.StringUtils; +import java.util.regex.Pattern; import java.util.List; import java.util.Map; import java.util.Objects; @@ -43,6 +44,7 @@ public class GostUtil { public static GostDto AddChains(Long node_id, List chainTunnels, Map node_s) { JSONArray nodes = new JSONArray(); + Node fromNode = node_s.get(node_id); for (ChainTunnel chainTunnel : chainTunnels) { JSONObject dialer = new JSONObject(); dialer.put("type", chainTunnel.getProtocol()); @@ -53,7 +55,11 @@ public class GostUtil { Node node_info = node_s.get(chainTunnel.getNodeId()); JSONObject node = new JSONObject(); node.put("name", "node_" + chainTunnel.getInx()); - node.put("addr", processServerAddress(node_info.getServerIp() + ":" + chainTunnel.getPort())); + + String dialHost = (fromNode != null && node_info != null) + ? selectDialHost(fromNode, node_info) + : (node_info != null ? node_info.getServerIp() : null); + node.put("addr", processServerAddress(dialHost + ":" + chainTunnel.getPort())); node.put("connector", connector); node.put("dialer", dialer); @@ -291,4 +297,103 @@ public class GostUtil { long colonCount = address.chars().filter(ch -> ch == ':').count(); return colonCount >= 2; } + + /** + * v4 优先:当两端都有 v4 时选择 v4,否则尝试 v6。 + * 用于节点之间建立链路(A -> B 需要选择 B 的地址族,且 A 需要支持该地址族)。 + */ + public static String selectDialHost(Node fromNode, Node toNode) { + if (fromNode == null || toNode == null) { + throw new IllegalArgumentException("node is null"); + } + + boolean fromV4 = supportsV4(fromNode); + boolean fromV6 = supportsV6(fromNode); + boolean toV4 = supportsV4(toNode); + boolean toV6 = supportsV6(toNode); + + if (fromV4 && toV4) { + return pickToAddressV4(toNode); + } + if (fromV6 && toV6) { + return pickToAddressV6(toNode); + } + + throw new RuntimeException( + "节点链路不兼容:" + safeName(fromNode) + "(v4=" + fromV4 + ",v6=" + fromV6 + ") -> " + + safeName(toNode) + "(v4=" + toV4 + ",v6=" + toV6 + ")" + ); + } + + private static String safeName(Node node) { + if (node.getName() == null || node.getName().isBlank()) { + return "node_" + node.getId(); + } + return node.getName(); + } + + private static boolean supportsV4(Node node) { + if (StrUtil.isNotBlank(node.getServerIpV4())) { + return true; + } + + String legacy = node.getServerIp(); + if (StrUtil.isBlank(legacy)) { + return false; + } + + legacy = legacy.trim(); + if (looksLikeIpv4(legacy)) { + return true; + } + if (isIPv6Address(legacy)) { + return false; + } + + // 域名/其它:无法判断,按双栈处理以保持兼容 + return true; + } + + private static boolean supportsV6(Node node) { + if (StrUtil.isNotBlank(node.getServerIpV6())) { + return true; + } + + String legacy = node.getServerIp(); + if (StrUtil.isBlank(legacy)) { + return false; + } + + legacy = legacy.trim(); + if (isIPv6Address(legacy)) { + return true; + } + if (looksLikeIpv4(legacy)) { + return false; + } + + // 域名/其它:无法判断,按双栈处理以保持兼容 + return true; + } + + private static String pickToAddressV4(Node toNode) { + if (StrUtil.isNotBlank(toNode.getServerIpV4())) { + return toNode.getServerIpV4().trim(); + } + String legacy = toNode.getServerIp(); + return legacy != null ? legacy.trim() : null; + } + + private static String pickToAddressV6(Node toNode) { + if (StrUtil.isNotBlank(toNode.getServerIpV6())) { + return toNode.getServerIpV6().trim(); + } + String legacy = toNode.getServerIp(); + return legacy != null ? legacy.trim() : null; + } + + private static boolean looksLikeIpv4(String value) { + Pattern ipv4 = Pattern.compile("^(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]?)$"); + return ipv4.matcher(value).matches(); + } } diff --git a/springboot-backend/src/main/java/com/admin/service/impl/ForwardServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/ForwardServiceImpl.java index 3f56b35..4246f09 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/ForwardServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/ForwardServiceImpl.java @@ -548,7 +548,7 @@ public class ForwardServiceImpl extends ServiceImpl impl Node toNode = nodeService.getById(firstChainNode.getNodeId()); if (toNode != null) { DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( - fromNode, toNode.getServerIp(), firstChainNode.getPort(), + fromNode, GostUtil.selectDialHost(fromNode, toNode), firstChainNode.getPort(), "入口(" + fromNode.getName() + ")->第1跳(" + toNode.getName() + ")" ); result.setFromChainType(1); @@ -562,7 +562,7 @@ public class ForwardServiceImpl extends ServiceImpl impl Node toNode = nodeService.getById(outNode.getNodeId()); if (toNode != null) { DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( - fromNode, toNode.getServerIp(), outNode.getPort(), + fromNode, GostUtil.selectDialHost(fromNode, toNode), outNode.getPort(), "入口(" + fromNode.getName() + ")->出口(" + toNode.getName() + ")" ); result.setFromChainType(1); @@ -587,7 +587,7 @@ public class ForwardServiceImpl extends ServiceImpl impl Node toNode = nodeService.getById(nextNode.getNodeId()); if (toNode != null) { DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( - fromNode, toNode.getServerIp(), nextNode.getPort(), + fromNode, GostUtil.selectDialHost(fromNode, toNode), nextNode.getPort(), "第" + (i + 1) + "跳(" + fromNode.getName() + ")->第" + (i + 2) + "跳(" + toNode.getName() + ")" ); result.setFromChainType(2); @@ -602,7 +602,7 @@ public class ForwardServiceImpl extends ServiceImpl impl Node toNode = nodeService.getById(outNode.getNodeId()); if (toNode != null) { DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( - fromNode, toNode.getServerIp(), outNode.getPort(), + fromNode, GostUtil.selectDialHost(fromNode, toNode), outNode.getPort(), "第" + (i + 1) + "跳(" + fromNode.getName() + ")->出口(" + toNode.getName() + ")" ); result.setFromChainType(2); diff --git a/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java index 7620b26..5f7e60a 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java @@ -118,13 +118,25 @@ public class TunnelServiceImpl extends ServiceImpl impleme tunnel.setCreatedTime(currentTime); tunnel.setUpdatedTime(currentTime); if (StringUtils.isEmpty(tunnel.getInIp())){ - StringBuilder in_ip = new StringBuilder(); + java.util.LinkedHashSet inIps = new java.util.LinkedHashSet<>(); for (ChainTunnel chainTunnel : tunnelDto.getInNodeId()) { Node node = nodes.get(chainTunnel.getNodeId()); - in_ip.append(node.getServerIp()).append(","); + if (node == null) continue; + if (cn.hutool.core.util.StrUtil.isNotBlank(node.getServerIpV4())) { + inIps.add(node.getServerIpV4().trim()); + } + if (cn.hutool.core.util.StrUtil.isNotBlank(node.getServerIpV6())) { + inIps.add(node.getServerIpV6().trim()); + } + if (cn.hutool.core.util.StrUtil.isBlank(node.getServerIpV4()) + && cn.hutool.core.util.StrUtil.isBlank(node.getServerIpV6()) + && cn.hutool.core.util.StrUtil.isNotBlank(node.getServerIp())) { + inIps.add(node.getServerIp().trim()); + } + } + if (!inIps.isEmpty()) { + tunnel.setInIp(String.join(",", inIps)); } - in_ip.deleteCharAt(in_ip.length() - 1); - tunnel.setInIp(in_ip.toString()); } this.save(tunnel); @@ -143,11 +155,33 @@ public class TunnelServiceImpl extends ServiceImpl impleme for (ChainTunnel in_node : tunnelDto.getInNodeId()) { // 创建Chain, 指向chainNode的第一跳。如果chainNode为空就是指向出口 if (tunnelDto.getChainNodes().isEmpty()) { // 指向出口 - GostDto gostDto = GostUtil.AddChains(in_node.getNodeId(), tunnelDto.getOutNodeId(), nodes); - isError(gostDto); + GostDto gostDto; + try { + gostDto = GostUtil.AddChains(in_node.getNodeId(), tunnelDto.getOutNodeId(), nodes); + } catch (RuntimeException e) { + this.removeById(tunnel.getId()); + chainTunnelService.remove(new QueryWrapper().eq("tunnel_id", tunnel.getId())); + return R.err(e.getMessage()); + } + if (!Objects.equals(gostDto.getMsg(), "OK")) { + this.removeById(tunnel.getId()); + chainTunnelService.remove(new QueryWrapper().eq("tunnel_id", tunnel.getId())); + return R.err(gostDto.getMsg()); + } } else { - GostDto gostDto = GostUtil.AddChains(in_node.getNodeId(), tunnelDto.getChainNodes().getFirst(), nodes);// 指向第一跳 + GostDto gostDto; + try { + gostDto = GostUtil.AddChains(in_node.getNodeId(), tunnelDto.getChainNodes().getFirst(), nodes);// 指向第一跳 + } catch (RuntimeException e) { + this.removeById(tunnel.getId()); + chainTunnelService.remove(new QueryWrapper().eq("tunnel_id", tunnel.getId())); + for (JSONObject chainSuccess : chain_success) { + GostDto deleteChains = GostUtil.DeleteChains(chainSuccess.getLong("node_id"), chainSuccess.getString("name")); + System.out.println(deleteChains); + } + return R.err(e.getMessage()); + } if (Objects.equals(gostDto.getMsg(), "OK")){ JSONObject data = new JSONObject(); data.put("node_id", in_node.getNodeId()); @@ -171,7 +205,18 @@ public class TunnelServiceImpl extends ServiceImpl impleme for (ChainTunnel chainTunnel : chainTunnels1) { int inx = i+1; if (inx >= tunnelDto.getChainNodes().size()) { // 指向出口 - GostDto gostDto = GostUtil.AddChains(chainTunnel.getNodeId(), tunnelDto.getOutNodeId(), nodes); + GostDto gostDto; + try { + gostDto = GostUtil.AddChains(chainTunnel.getNodeId(), tunnelDto.getOutNodeId(), nodes); + } catch (RuntimeException e) { + this.removeById(tunnel.getId()); + chainTunnelService.remove(new QueryWrapper().eq("tunnel_id", tunnel.getId())); + for (JSONObject chainSuccess : chain_success) { + GostDto deleteChains = GostUtil.DeleteChains(chainSuccess.getLong("node_id"), chainSuccess.getString("name")); + System.out.println(deleteChains); + } + return R.err(e.getMessage()); + } if (Objects.equals(gostDto.getMsg(), "OK")){ JSONObject data = new JSONObject(); data.put("node_id", chainTunnel.getNodeId()); @@ -187,7 +232,18 @@ public class TunnelServiceImpl extends ServiceImpl impleme return R.err(gostDto.getMsg()); } } else { - GostDto gostDto = GostUtil.AddChains(chainTunnel.getNodeId(), tunnelDto.getChainNodes().get(inx), nodes); + GostDto gostDto; + try { + gostDto = GostUtil.AddChains(chainTunnel.getNodeId(), tunnelDto.getChainNodes().get(inx), nodes); + } catch (RuntimeException e) { + this.removeById(tunnel.getId()); + chainTunnelService.remove(new QueryWrapper().eq("tunnel_id", tunnel.getId())); + for (JSONObject chainSuccess : chain_success) { + GostDto deleteChains = GostUtil.DeleteChains(chainSuccess.getLong("node_id"), chainSuccess.getString("name")); + System.out.println(deleteChains); + } + return R.err(e.getMessage()); + } if (Objects.equals(gostDto.getMsg(), "OK")){ JSONObject data = new JSONObject(); data.put("node_id", chainTunnel.getNodeId()); @@ -457,19 +513,29 @@ public class TunnelServiceImpl extends ServiceImpl impleme tunnel.setInIp(tunnelUpdateDto.getInIp()); if (StringUtils.isEmpty(tunnel.getInIp())) { - StringBuilder inIp = new StringBuilder(); + java.util.LinkedHashSet inIps = new java.util.LinkedHashSet<>(); List chainTunnels = chainTunnelService.list( new QueryWrapper().eq("tunnel_id", tunnel.getId()).eq("chain_type", 1) ); for (ChainTunnel chainTunnel : chainTunnels) { Node node = nodeService.getById(chainTunnel.getNodeId()); if (node == null) return R.err("隧道节点数据错误,部分节点不存在"); - inIp.append(node.getServerIp()).append(","); + + if (cn.hutool.core.util.StrUtil.isNotBlank(node.getServerIpV4())) { + inIps.add(node.getServerIpV4().trim()); + } + if (cn.hutool.core.util.StrUtil.isNotBlank(node.getServerIpV6())) { + inIps.add(node.getServerIpV6().trim()); + } + if (cn.hutool.core.util.StrUtil.isBlank(node.getServerIpV4()) + && cn.hutool.core.util.StrUtil.isBlank(node.getServerIpV6()) + && cn.hutool.core.util.StrUtil.isNotBlank(node.getServerIp())) { + inIps.add(node.getServerIp().trim()); + } } - if (inIp.length() > 0) { - inIp.deleteCharAt(inIp.length() - 1); + if (!inIps.isEmpty()) { + tunnel.setInIp(String.join(",", inIps)); } - tunnel.setInIp(inIp.toString()); } tunnel.setUpdatedTime(System.currentTimeMillis()); @@ -596,7 +662,7 @@ public class TunnelServiceImpl extends ServiceImpl impleme Node toNode = nodeService.getById(firstChainNode.getNodeId()); if (toNode != null) { DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( - fromNode, toNode.getServerIp(), firstChainNode.getPort(), + fromNode, GostUtil.selectDialHost(fromNode, toNode), firstChainNode.getPort(), "入口(" + fromNode.getName() + ")->第1跳(" + toNode.getName() + ")" ); result.setFromChainType(1); // 入口 @@ -610,7 +676,7 @@ public class TunnelServiceImpl extends ServiceImpl impleme Node toNode = nodeService.getById(outNode.getNodeId()); if (toNode != null) { DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( - fromNode, toNode.getServerIp(), outNode.getPort(), + fromNode, GostUtil.selectDialHost(fromNode, toNode), outNode.getPort(), "入口(" + fromNode.getName() + ")->出口(" + toNode.getName() + ")" ); result.setFromChainType(1); @@ -634,7 +700,7 @@ public class TunnelServiceImpl extends ServiceImpl impleme Node toNode = nodeService.getById(nextNode.getNodeId()); if (toNode != null) { DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( - fromNode, toNode.getServerIp(), nextNode.getPort(), + fromNode, GostUtil.selectDialHost(fromNode, toNode), nextNode.getPort(), "第" + (i + 1) + "跳(" + fromNode.getName() + ")->第" + (i + 2) + "跳(" + toNode.getName() + ")" ); result.setFromChainType(2); @@ -649,7 +715,7 @@ public class TunnelServiceImpl extends ServiceImpl impleme Node toNode = nodeService.getById(outNode.getNodeId()); if (toNode != null) { DiagnosisResult result = performTcpPingDiagnosisWithConnectionCheck( - fromNode, toNode.getServerIp(), outNode.getPort(), + fromNode, GostUtil.selectDialHost(fromNode, toNode), outNode.getPort(), "第" + (i + 1) + "跳(" + fromNode.getName() + ")->出口(" + toNode.getName() + ")" ); result.setFromChainType(2); @@ -740,7 +806,12 @@ public class TunnelServiceImpl extends ServiceImpl impleme } private void isError(GostDto gostDto){ - + if (gostDto == null) { + throw new RuntimeException("节点无响应"); + } + if (!Objects.equals(gostDto.getMsg(), "OK")) { + throw new RuntimeException(gostDto.getMsg()); + } } private DiagnosisResult performTcpPingDiagnosis(Node node, String targetIp, int port, String description) {