diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 0c85c20..f11d84e 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -71,7 +71,7 @@ jobs: if: steps.java-changes.outputs.java == 'true' uses: actions/setup-java@v3 with: - java-version: 8 + java-version: 21 distribution: 'temurin' - name: Cache Maven dependencies diff --git a/go-gost/gost b/go-gost/gost index 526fc2b..2272ae8 100755 Binary files a/go-gost/gost and b/go-gost/gost differ diff --git a/go-gost/main.go b/go-gost/main.go index c3f25c0..2dfa2f7 100644 --- a/go-gost/main.go +++ b/go-gost/main.go @@ -119,7 +119,7 @@ func main() { log := xlogger.NewLogger() logger.SetDefault(log) - wsReporter := socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, "1.0.4") + wsReporter := socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, "1.0.6") defer wsReporter.Stop() service.SetHTTPReportURL(config.Addr, config.Secret) diff --git a/go-gost/x/socket/websocket_reporter.go b/go-gost/x/socket/websocket_reporter.go index fa5b046..2de97a5 100644 --- a/go-gost/x/socket/websocket_reporter.go +++ b/go-gost/x/socket/websocket_reporter.go @@ -8,9 +8,6 @@ import ( "fmt" "net" "net/url" - "os/exec" - "runtime" - "strconv" "strings" "sync" // 新增:用于管理连接状态的互斥锁 "time" @@ -65,19 +62,22 @@ type CommandResponse struct { RequestId string `json:"requestId,omitempty"` } -// PingRequest ping请求结构体 -type PingRequest struct { +// TcpPingRequest TCP ping请求结构体 +type TcpPingRequest struct { IP string `json:"ip"` + Port int `json:"port"` Count int `json:"count"` + Timeout int `json:"timeout"` // 超时时间(毫秒) RequestId string `json:"requestId,omitempty"` } -// PingResponse ping响应结构体 -type PingResponse struct { +// TcpPingResponse TCP ping响应结构体 +type TcpPingResponse struct { IP string `json:"ip"` + Port int `json:"port"` Success bool `json:"success"` - AverageTime float64 `json:"averageTime"` // 平均延迟(ms) - PacketLoss float64 `json:"packetLoss"` // 丢包率(%) + AverageTime float64 `json:"averageTime"` // 平均连接时间(ms) + PacketLoss float64 `json:"packetLoss"` // 连接失败率(%) ErrorMessage string `json:"errorMessage,omitempty"` RequestId string `json:"requestId,omitempty"` } @@ -517,12 +517,12 @@ func (w *WebSocketReporter) routeCommand(cmd CommandMessage) { err = w.handleDeleteLimiter(cmd.Data) response.Type = "DeleteLimitersResponse" - // Ping 诊断命令 - case "Ping": - var pingResult PingResponse - pingResult, err = w.handlePing(cmd.Data) - response.Type = "PingResponse" - response.Data = pingResult + // TCP Ping 诊断命令 + case "TcpPing": + var tcpPingResult TcpPingResponse + tcpPingResult, err = w.handleTcpPing(cmd.Data) + response.Type = "TcpPingResponse" + response.Data = tcpPingResult default: err = fmt.Errorf("未知命令类型: %s", cmd.Type) @@ -944,38 +944,54 @@ func StartWebSocketReporterWithConfig(Addr string, Secret string, Version string return reporter } -// handlePing 处理ping诊断命令 -func (w *WebSocketReporter) handlePing(data interface{}) (PingResponse, error) { +// handleTcpPing 处理TCP ping诊断命令 +func (w *WebSocketReporter) handleTcpPing(data interface{}) (TcpPingResponse, error) { jsonData, err := json.Marshal(data) if err != nil { - return PingResponse{}, fmt.Errorf("序列化ping数据失败: %v", err) + return TcpPingResponse{}, fmt.Errorf("序列化TCP ping数据失败: %v", err) } - var req PingRequest + var req TcpPingRequest if err := json.Unmarshal(jsonData, &req); err != nil { - return PingResponse{}, fmt.Errorf("解析ping请求失败: %v", err) + return TcpPingResponse{}, fmt.Errorf("解析TCP ping请求失败: %v", err) } // 验证IP地址格式 if net.ParseIP(req.IP) == nil && !isValidHostname(req.IP) { - return PingResponse{ + return TcpPingResponse{ IP: req.IP, + Port: req.Port, Success: false, ErrorMessage: "无效的IP地址或主机名", RequestId: req.RequestId, }, nil } - // 设置默认ping次数 + // 验证端口范围 + if req.Port <= 0 || req.Port > 65535 { + return TcpPingResponse{ + IP: req.IP, + Port: req.Port, + Success: false, + ErrorMessage: "无效的端口号,范围应为1-65535", + RequestId: req.RequestId, + }, nil + } + + // 设置默认值 if req.Count <= 0 { req.Count = 4 } + if req.Timeout <= 0 { + req.Timeout = 5000 // 默认5秒超时 + } - // 执行ping操作 - avgTime, packetLoss, err := pingHost(req.IP, req.Count) + // 执行TCP ping操作 + avgTime, packetLoss, err := tcpPingHost(req.IP, req.Port, req.Count, req.Timeout) - response := PingResponse{ + response := TcpPingResponse{ IP: req.IP, + Port: req.Port, RequestId: req.RequestId, } @@ -991,138 +1007,52 @@ func (w *WebSocketReporter) handlePing(data interface{}) (PingResponse, error) { return response, nil } -// pingHost 执行ping操作,返回平均延迟和丢包率 -func pingHost(ip string, count int) (float64, float64, error) { - var cmd *exec.Cmd +// tcpPingHost 执行TCP连接测试,返回平均连接时间和失败率 +func tcpPingHost(ip string, port int, count int, timeoutMs int) (float64, float64, error) { + var totalTime float64 + var successCount int + + timeout := time.Duration(timeoutMs) * time.Millisecond + target := fmt.Sprintf("%s:%d", ip, port) - // 根据操作系统选择不同的ping命令 - switch runtime.GOOS { - case "windows": - cmd = exec.Command("ping", "-n", strconv.Itoa(count), ip) - case "darwin", "linux": - cmd = exec.Command("ping", "-c", strconv.Itoa(count), ip) - default: - return 0, 0, fmt.Errorf("不支持的操作系统: %s", runtime.GOOS) - } + fmt.Printf("🔍 开始TCP ping测试: %s,次数: %d,超时: %dms\n", target, count, timeoutMs) - output, err := cmd.Output() - if err != nil { - return 0, 0, fmt.Errorf("ping命令执行失败: %v", err) - } - - // 解析ping输出 - return parsePingOutput(string(output), runtime.GOOS) -} - -// parsePingOutput 解析ping命令输出,提取平均延迟和丢包率 -func parsePingOutput(output, osType string) (float64, float64, error) { - lines := strings.Split(output, "\n") - - switch osType { - case "windows": - return parsePingOutputWindows(lines) - case "darwin", "linux": - return parsePingOutputUnix(lines) - default: - return 0, 0, fmt.Errorf("不支持的操作系统类型") - } -} - -// parsePingOutputWindows 解析Windows系统的ping输出 -func parsePingOutputWindows(lines []string) (float64, float64, error) { - var avgTime float64 - var packetLoss float64 - - for _, line := range lines { - line = strings.TrimSpace(line) - - // 查找平均延迟 (例如: "最短 = 1ms,最长 = 2ms,平均 = 1ms") - if strings.Contains(line, "平均") && strings.Contains(line, "ms") { - parts := strings.Split(line, "平均 = ") - if len(parts) > 1 { - avgPart := strings.Split(parts[1], "ms")[0] - if avg, err := strconv.ParseFloat(avgPart, 64); err == nil { - avgTime = avg - } - } + for i := 0; i < count; i++ { + start := time.Now() + + // 创建带超时的TCP连接 + conn, err := net.DialTimeout("tcp", target, timeout) + + elapsed := time.Since(start) + + if err != nil { + fmt.Printf(" 第%d次连接失败: %v (%.2fms)\n", i+1, err, elapsed.Seconds()*1000) + } else { + fmt.Printf(" 第%d次连接成功: %.2fms\n", i+1, elapsed.Seconds()*1000) + conn.Close() + totalTime += elapsed.Seconds() * 1000 // 转换为毫秒 + successCount++ } - - // 查找丢包率 (例如: "丢失 = 0 (0% 丢失)") - if strings.Contains(line, "丢失") && strings.Contains(line, "%") { - if strings.Contains(line, "(0%") { - packetLoss = 0 - } else { - // 提取百分比 - start := strings.Index(line, "(") - end := strings.Index(line, "%") - if start != -1 && end != -1 && start < end { - lossStr := line[start+1 : end] - if loss, err := strconv.ParseFloat(lossStr, 64); err == nil { - packetLoss = loss - } - } - } + + // 如果不是最后一次,等待一下再进行下次测试 + if i < count-1 { + time.Sleep(100 * time.Millisecond) } } + if successCount == 0 { + return 0, 100.0, fmt.Errorf("所有TCP连接尝试都失败") + } + + avgTime := totalTime / float64(successCount) + packetLoss := float64(count-successCount) / float64(count) * 100 + + fmt.Printf("✅ TCP ping完成: 平均连接时间 %.2fms,失败率 %.1f%%\n", avgTime, packetLoss) + return avgTime, packetLoss, nil } -// parsePingOutputUnix 解析Unix系统(Linux/macOS)的ping输出 -func parsePingOutputUnix(lines []string) (float64, float64, error) { - var avgTime float64 - var packetLoss float64 - for _, line := range lines { - line = strings.TrimSpace(line) - - // 查找统计行 (例如: "4 packets transmitted, 4 received, 0% packet loss") - if strings.Contains(line, "packet loss") { - parts := strings.Split(line, "%") - if len(parts) > 0 { - // 查找百分比前的数字 - lossStr := strings.Fields(parts[0]) - if len(lossStr) > 0 { - if loss, err := strconv.ParseFloat(lossStr[len(lossStr)-1], 64); err == nil { - packetLoss = loss - } - } - } - } - - // 查找往返时间统计 (例如: "round-trip min/avg/max/stddev = 0.123/0.456/0.789/0.012 ms") - if strings.Contains(line, "round-trip") && strings.Contains(line, "=") { - parts := strings.Split(line, "=") - if len(parts) > 1 { - times := strings.TrimSpace(parts[1]) - times = strings.Split(times, " ")[0] // 去掉末尾的"ms" - timeValues := strings.Split(times, "/") - if len(timeValues) >= 2 { - if avg, err := strconv.ParseFloat(timeValues[1], 64); err == nil { - avgTime = avg - } - } - } - } - - // macOS的格式可能不同,查找avg (例如: "min/avg/max/stddev = 0.123/0.456/0.789/0.012 ms") - if strings.Contains(line, "min/avg/max") && strings.Contains(line, "=") { - parts := strings.Split(line, "=") - if len(parts) > 1 { - times := strings.TrimSpace(parts[1]) - times = strings.Split(times, " ")[0] // 去掉末尾的"ms" - timeValues := strings.Split(times, "/") - if len(timeValues) >= 2 { - if avg, err := strconv.ParseFloat(timeValues[1], 64); err == nil { - avgTime = avg - } - } - } - } - } - - return avgTime, packetLoss, nil -} // isValidHostname 验证主机名格式 func isValidHostname(hostname string) bool { diff --git a/install.sh b/install.sh index e44c97c..6335d90 100755 --- a/install.sh +++ b/install.sh @@ -121,6 +121,7 @@ install_gost() { # 询问是否有加速下载地址 echo "" echo "📥 检查下载地址..." + echo "加速下载地址需提供完整的地址,浏览器打开就能直接下载的那种!!!!!" read -p "是否有加速下载地址?(留空使用默认地址): " custom_url if [[ -n "$custom_url" ]]; then DOWNLOAD_URL="$custom_url" diff --git a/springboot-backend/Dockerfile b/springboot-backend/Dockerfile index 6434f6a..7cdaca8 100644 --- a/springboot-backend/Dockerfile +++ b/springboot-backend/Dockerfile @@ -1,21 +1,23 @@ # 构建阶段 -FROM maven:3.8.6-jdk-8 AS build +FROM maven:3.9.6-openjdk-21 AS build WORKDIR /app COPY pom.xml . COPY src ./src RUN mvn clean package -DskipTests # 运行阶段 -FROM openjdk:8-jre-alpine +FROM openjdk:21-jre-slim WORKDIR /app ENV JAVA_OPTS="-Xmx512m -Xms256m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai" ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US:en ENV LC_ALL=en_US.UTF-8 -RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories \ - && apk update \ - && apk --no-cache add ttf-dejavu fontconfig +RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources \ + && apt-get update \ + && apt-get install -y --no-install-recommends fontconfig fonts-dejavu \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* COPY --from=build /app/target/*.jar app.jar EXPOSE 6365 diff --git a/springboot-backend/pom.xml b/springboot-backend/pom.xml index 015f49a..dfc8aa8 100644 --- a/springboot-backend/pom.xml +++ b/springboot-backend/pom.xml @@ -8,10 +8,12 @@ admin admin - 1.8 + 21 UTF-8 UTF-8 - 2.6.13 + 2.7.18 + 21 + 21 @@ -22,19 +24,14 @@ com.alibaba.fastjson2 fastjson2 - 2.0.43 - - - - com.sun.mail - javax.mail - 1.6.2 + 2.0.43 org.projectlombok lombok + 1.18.30 true @@ -72,8 +69,9 @@ - mysql - mysql-connector-java + com.mysql + mysql-connector-j + 8.2.0 runtime @@ -97,12 +95,13 @@ org.apache.httpcomponents httpclient + 4.5.14 org.freemarker freemarker - 2.3.31 + 2.3.31 @@ -139,11 +138,18 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.11.0 - 1.8 - 1.8 + 21 + 21 UTF-8 + + + org.projectlombok + lombok + 1.18.30 + + diff --git a/springboot-backend/src/main/java/com/admin/common/interceptor/JwtInterceptor.java b/springboot-backend/src/main/java/com/admin/common/interceptor/JwtInterceptor.java index 29d84aa..ad52cca 100644 --- a/springboot-backend/src/main/java/com/admin/common/interceptor/JwtInterceptor.java +++ b/springboot-backend/src/main/java/com/admin/common/interceptor/JwtInterceptor.java @@ -9,6 +9,7 @@ import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; + /** * JWT拦截器,验证用户是否登录 */ diff --git a/springboot-backend/src/main/java/com/admin/common/task/ResetFlowAsync.java b/springboot-backend/src/main/java/com/admin/common/task/ResetFlowAsync.java index 98e4702..2665019 100644 --- a/springboot-backend/src/main/java/com/admin/common/task/ResetFlowAsync.java +++ b/springboot-backend/src/main/java/com/admin/common/task/ResetFlowAsync.java @@ -68,9 +68,12 @@ public class ResetFlowAsync { */ private void resetUserFlow(int currentDay, int lastDayOfMonth) { try { - // flowResetTime字段存储的是1-31的数字,表示每月第几号重置 + // flowResetTime字段存储的是0-31的数字,0表示不重置,1-31表示每月第几号重置 // 构建查询条件:重置日期等于今天,或者重置日期大于当月最大天数且今天是月末 + // 排除flowResetTime为0的记录(不重置) QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.ne("flow_reset_time", 0); // 排除不重置的用户 + if (currentDay == lastDayOfMonth) { // 如果今天是月末,查询重置日期等于今天或者大于当月最大天数的记录 // 例如:当月30天,但用户设置31号重置,则在30号执行重置 @@ -118,9 +121,12 @@ public class ResetFlowAsync { */ private void resetUserTunnelFlow(int currentDay, int lastDayOfMonth) { try { - // flowResetTime字段存储的是1-31的数字,表示每月第几号重置 + // flowResetTime字段存储的是0-31的数字,0表示不重置,1-31表示每月第几号重置 // 构建查询条件:重置日期等于今天,或者重置日期大于当月最大天数且今天是月末 + // 排除flowResetTime为0的记录(不重置) QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.ne("flow_reset_time", 0); // 排除不重置的用户隧道 + if (currentDay == lastDayOfMonth) { // 如果今天是月末,查询重置日期等于今天或者大于当月最大天数的记录 // 例如:当月30天,但用户设置31号重置,则在30号执行重置 diff --git a/springboot-backend/src/main/java/com/admin/config/WebSocketInterceptor.java b/springboot-backend/src/main/java/com/admin/config/WebSocketInterceptor.java index 3bb74e0..85fb0cb 100644 --- a/springboot-backend/src/main/java/com/admin/config/WebSocketInterceptor.java +++ b/springboot-backend/src/main/java/com/admin/config/WebSocketInterceptor.java @@ -21,6 +21,7 @@ import java.util.Objects; @Configuration @Slf4j public class WebSocketInterceptor extends HttpSessionHandshakeInterceptor { + @Resource NodeService nodeService; 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 f901474..a592c58 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 @@ -411,9 +411,10 @@ public class ForwardServiceImpl extends ServiceImpl impl String[] remoteAddresses = forward.getRemoteAddr().split(","); String targetAddress = remoteAddresses[0].trim(); - // 提取IP部分(去掉端口) + // 提取IP和端口 String targetIp = extractIpFromAddress(targetAddress); - if (targetIp == null) { + int targetPort = extractPortFromAddress(targetAddress); + if (targetIp == null || targetPort == -1) { return R.err("无法解析目标地址: " + targetAddress); } @@ -421,22 +422,22 @@ public class ForwardServiceImpl extends ServiceImpl impl // 6. 根据隧道类型执行不同的诊断策略 if (tunnel.getType() == TUNNEL_TYPE_PORT_FORWARD) { - // 端口转发:入口节点直接ping目标地址 - DiagnosisResult result = performPingDiagnosis(inNode, targetIp, "转发->目标"); + // 端口转发:入口节点直接TCP ping目标地址 + DiagnosisResult result = performTcpPingDiagnosis(inNode, targetIp, targetPort, "转发->目标"); results.add(result); } else { - // 隧道转发:入口ping出口,出口ping目标 + // 隧道转发:入口TCP ping出口,出口TCP ping目标 Node outNode = nodeService.getNodeById(tunnel.getOutNodeId()); if (outNode == null) { return R.err("出口节点不存在"); } - // 入口ping出口 - DiagnosisResult inToOutResult = performPingDiagnosis(inNode, outNode.getServerIp(), "入口->出口"); + // 入口TCP ping出口(使用转发的出口端口) + DiagnosisResult inToOutResult = performTcpPingDiagnosis(inNode, outNode.getServerIp(), forward.getOutPort(), "入口->出口"); results.add(inToOutResult); - // 出口ping目标 - DiagnosisResult outToTargetResult = performPingDiagnosis(outNode, targetIp, "出口->目标"); + // 出口TCP ping目标 + DiagnosisResult outToTargetResult = performTcpPingDiagnosis(outNode, targetIp, targetPort, "出口->目标"); results.add(outToTargetResult); } @@ -481,58 +482,101 @@ public class ForwardServiceImpl extends ServiceImpl impl } /** - * 执行ping诊断 + * 从地址字符串中提取端口号 + * 支持格式: ip:port, [ipv6]:port, domain:port + */ + private int extractPortFromAddress(String address) { + if (address == null || address.trim().isEmpty()) { + return -1; + } + + address = address.trim(); + + // IPv6格式: [ipv6]:port + if (address.startsWith("[")) { + int closeBracket = address.indexOf(']'); + if (closeBracket > 1 && closeBracket + 1 < address.length() && address.charAt(closeBracket + 1) == ':') { + String portStr = address.substring(closeBracket + 2); + try { + return Integer.parseInt(portStr); + } catch (NumberFormatException e) { + return -1; + } + } + } + + // IPv4或域名格式: ip:port 或 domain:port + int lastColon = address.lastIndexOf(':'); + if (lastColon > 0 && lastColon + 1 < address.length()) { + String portStr = address.substring(lastColon + 1); + try { + return Integer.parseInt(portStr); + } catch (NumberFormatException e) { + return -1; + } + } + + // 如果没有端口,返回-1表示无法解析 + return -1; + } + + /** + * 执行TCP ping诊断 * - * @param node 执行ping的节点 + * @param node 执行TCP ping的节点 * @param targetIp 目标IP地址 + * @param port 目标端口 * @param description 诊断描述 * @return 诊断结果 */ - private DiagnosisResult performPingDiagnosis(Node node, String targetIp, String description) { + private DiagnosisResult performTcpPingDiagnosis(Node node, String targetIp, int port, String description) { try { - // 构建ping请求数据 - JSONObject pingData = new JSONObject(); - pingData.put("ip", targetIp); - pingData.put("count", 4); + // 构建TCP ping请求数据 + JSONObject tcpPingData = new JSONObject(); + tcpPingData.put("ip", targetIp); + tcpPingData.put("port", port); + tcpPingData.put("count", 4); + tcpPingData.put("timeout", 5000); // 5秒超时 - // 发送ping命令到节点 - GostDto gostResult = WebSocketServer.send_msg(node.getId(), pingData, "Ping"); + // 发送TCP ping命令到节点 + GostDto gostResult = WebSocketServer.send_msg(node.getId(), tcpPingData, "TcpPing"); DiagnosisResult result = new DiagnosisResult(); result.setNodeId(node.getId()); result.setNodeName(node.getName()); result.setTargetIp(targetIp); + result.setTargetPort(port); result.setDescription(description); result.setTimestamp(System.currentTimeMillis()); if (gostResult != null && "OK".equals(gostResult.getMsg())) { - // 尝试解析ping响应数据 + // 尝试解析TCP ping响应数据 try { if (gostResult.getData() != null) { - JSONObject pingResponse = (JSONObject) gostResult.getData(); - boolean success = pingResponse.getBooleanValue("success"); + JSONObject tcpPingResponse = (JSONObject) gostResult.getData(); + boolean success = tcpPingResponse.getBooleanValue("success"); result.setSuccess(success); if (success) { - result.setMessage("ping成功"); - result.setAverageTime(pingResponse.getDoubleValue("averageTime")); - result.setPacketLoss(pingResponse.getDoubleValue("packetLoss")); + result.setMessage("TCP连接成功"); + result.setAverageTime(tcpPingResponse.getDoubleValue("averageTime")); + result.setPacketLoss(tcpPingResponse.getDoubleValue("packetLoss")); } else { - result.setMessage(pingResponse.getString("errorMessage")); + result.setMessage(tcpPingResponse.getString("errorMessage")); result.setAverageTime(-1.0); result.setPacketLoss(100.0); } } else { // 没有详细数据,使用默认值 result.setSuccess(true); - result.setMessage("ping成功"); + result.setMessage("TCP连接成功"); result.setAverageTime(0.0); result.setPacketLoss(0.0); } } catch (Exception e) { - // 解析响应数据失败,但ping命令本身成功了 + // 解析响应数据失败,但TCP ping命令本身成功了 result.setSuccess(true); - result.setMessage("ping成功,但无法解析详细数据"); + result.setMessage("TCP连接成功,但无法解析详细数据"); result.setAverageTime(0.0); result.setPacketLoss(0.0); } @@ -549,6 +593,7 @@ public class ForwardServiceImpl extends ServiceImpl impl result.setNodeId(node.getId()); result.setNodeName(node.getName()); result.setTargetIp(targetIp); + result.setTargetPort(port); result.setDescription(description); result.setSuccess(false); result.setMessage("诊断执行异常: " + e.getMessage()); @@ -1326,6 +1371,7 @@ public class ForwardServiceImpl extends ServiceImpl impl private Long nodeId; private String nodeName; private String targetIp; + private Integer targetPort; private String description; private boolean success; private String message; 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 d0eeb5b..43d3244 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 @@ -643,16 +643,17 @@ public class TunnelServiceImpl extends ServiceImpl impleme // 3. 根据隧道类型执行不同的诊断策略 if (tunnel.getType() == TUNNEL_TYPE_PORT_FORWARD) { - // 端口转发:只给入口节点发送诊断指令,ping谷歌DNS - DiagnosisResult inResult = performPingDiagnosisWithConnectionCheck(inNode, "www.google.com", "入口->外网"); + // 端口转发:只给入口节点发送诊断指令,TCP ping谷歌443端口 + DiagnosisResult inResult = performTcpPingDiagnosisWithConnectionCheck(inNode, "www.google.com", 443, "入口->外网"); results.add(inResult); } else { - // 隧道转发:入口ping出口,出口ping谷歌DNS - DiagnosisResult inToOutResult = performPingDiagnosisWithConnectionCheck(inNode, outNode.getServerIp(), "入口->出口"); + // 隧道转发:入口TCP ping出口,出口TCP ping谷歌443端口 + int outNodePort = getOutNodeTcpPort(tunnel.getId()); + DiagnosisResult inToOutResult = performTcpPingDiagnosisWithConnectionCheck(inNode, outNode.getServerIp(), outNodePort, "入口->出口"); results.add(inToOutResult); // 先检查出口节点的真实连接状态,然后再进行诊断 - DiagnosisResult outToExternalResult = performPingDiagnosisWithConnectionCheck(outNode, "www.google.com", "出口->外网"); + DiagnosisResult outToExternalResult = performTcpPingDiagnosisWithConnectionCheck(outNode, "www.google.com", 443, "出口->外网"); results.add(outToExternalResult); } @@ -668,58 +669,78 @@ public class TunnelServiceImpl extends ServiceImpl impleme } /** - * 执行ping诊断 + * 获取出口节点的TCP端口 + * 通过隧道ID查找转发服务的出口端口,如果没有则使用默认SSH端口22 * - * @param node 执行ping的节点 + * @param tunnelId 隧道ID + * @return TCP端口号 + */ + private int getOutNodeTcpPort(Long tunnelId) { + List forwards = forwardService.list(new QueryWrapper().eq("tunnel_id", tunnelId)); + if (!forwards.isEmpty()) { + return forwards.get(0).getOutPort(); + } + // 如果没有转发服务,使用默认SSH端口22 + return 22; + } + + /** + * 执行TCP ping诊断 + * + * @param node 执行TCP ping的节点 * @param targetIp 目标IP地址 + * @param port 目标端口 * @param description 诊断描述 * @return 诊断结果 */ - private DiagnosisResult performPingDiagnosis(Node node, String targetIp, String description) { + private DiagnosisResult performTcpPingDiagnosis(Node node, String targetIp, int port, String description) { try { - // 构建ping请求数据 - JSONObject pingData = new JSONObject(); - pingData.put("ip", targetIp); - pingData.put("count", 4); + // 构建TCP ping请求数据 + JSONObject tcpPingData = new JSONObject(); + tcpPingData.put("ip", targetIp); + tcpPingData.put("port", port); + tcpPingData.put("count", 4); + tcpPingData.put("timeout", 5000); // 5秒超时 - // 发送ping命令到节点 - GostDto gostResult = WebSocketServer.send_msg(node.getId(), pingData, "Ping"); + // 发送TCP ping命令到节点 + GostDto gostResult = WebSocketServer.send_msg(node.getId(), tcpPingData, "TcpPing"); DiagnosisResult result = new DiagnosisResult(); result.setNodeId(node.getId()); result.setNodeName(node.getName()); result.setTargetIp(targetIp); + result.setTargetPort(port); result.setDescription(description); result.setTimestamp(System.currentTimeMillis()); if (gostResult != null && "OK".equals(gostResult.getMsg())) { - // 尝试解析ping响应数据 + // 尝试解析TCP ping响应数据 try { if (gostResult.getData() != null) { - JSONObject pingResponse = (JSONObject) gostResult.getData(); - boolean success = pingResponse.getBooleanValue("success"); + JSONObject tcpPingResponse = (JSONObject) gostResult.getData(); + boolean success = tcpPingResponse.getBooleanValue("success"); result.setSuccess(success); if (success) { - result.setMessage("ping成功"); - result.setAverageTime(pingResponse.getDoubleValue("averageTime")); - result.setPacketLoss(pingResponse.getDoubleValue("packetLoss")); + result.setMessage("TCP连接成功"); + result.setAverageTime(tcpPingResponse.getDoubleValue("averageTime")); + result.setPacketLoss(tcpPingResponse.getDoubleValue("packetLoss")); } else { - result.setMessage(pingResponse.getString("errorMessage")); + result.setMessage(tcpPingResponse.getString("errorMessage")); result.setAverageTime(-1.0); result.setPacketLoss(100.0); } } else { // 没有详细数据,使用默认值 result.setSuccess(true); - result.setMessage("ping成功"); + result.setMessage("TCP连接成功"); result.setAverageTime(0.0); result.setPacketLoss(0.0); } } catch (Exception e) { - // 解析响应数据失败,但ping命令本身成功了 + // 解析响应数据失败,但TCP ping命令本身成功了 result.setSuccess(true); - result.setMessage("ping成功,但无法解析详细数据"); + result.setMessage("TCP连接成功,但无法解析详细数据"); result.setAverageTime(0.0); result.setPacketLoss(0.0); } @@ -736,6 +757,7 @@ public class TunnelServiceImpl extends ServiceImpl impleme result.setNodeId(node.getId()); result.setNodeName(node.getName()); result.setTargetIp(targetIp); + result.setTargetPort(port); result.setDescription(description); result.setSuccess(false); result.setMessage("诊断执行异常: " + e.getMessage()); @@ -747,23 +769,25 @@ public class TunnelServiceImpl extends ServiceImpl impleme } /** - * 执行ping诊断(带连接状态检查) + * 执行TCP ping诊断(带连接状态检查) * - * @param node 执行ping的节点 + * @param node 执行TCP ping的节点 * @param targetIp 目标IP地址 + * @param port 目标端口 * @param description 诊断描述 * @return 诊断结果 */ - private DiagnosisResult performPingDiagnosisWithConnectionCheck(Node node, String targetIp, String description) { + private DiagnosisResult performTcpPingDiagnosisWithConnectionCheck(Node node, String targetIp, int port, String description) { DiagnosisResult result = new DiagnosisResult(); result.setNodeId(node.getId()); result.setNodeName(node.getName()); result.setTargetIp(targetIp); + result.setTargetPort(port); result.setDescription(description); result.setTimestamp(System.currentTimeMillis()); try { - return performPingDiagnosis(node, targetIp, description); + return performTcpPingDiagnosis(node, targetIp, port, description); } catch (Exception e) { result.setSuccess(false); result.setMessage("连接检查异常: " + e.getMessage()); @@ -817,6 +841,7 @@ public class TunnelServiceImpl extends ServiceImpl impleme private Long nodeId; private String nodeName; private String targetIp; + private Integer targetPort; private String description; private boolean success; private String message; diff --git a/vite-frontend/src/layouts/admin.tsx b/vite-frontend/src/layouts/admin.tsx index 4d95665..41bd84a 100644 --- a/vite-frontend/src/layouts/admin.tsx +++ b/vite-frontend/src/layouts/admin.tsx @@ -260,7 +260,7 @@ export default function AdminLayout({

哆啦A梦

-

v1.0.7

+

v1.0.8

diff --git a/vite-frontend/src/pages/dashboard.tsx b/vite-frontend/src/pages/dashboard.tsx index bed4783..9865e8a 100644 --- a/vite-frontend/src/pages/dashboard.tsx +++ b/vite-frontend/src/pages/dashboard.tsx @@ -341,7 +341,8 @@ export default function DashboardPage() { }; const formatResetTime = (resetDay?: number): string => { - if (!resetDay) return ''; + if (resetDay === undefined || resetDay === null) return ''; + if (resetDay === 0) return '不重置'; const now = new Date(); const currentDay = now.getDate(); @@ -592,7 +593,7 @@ export default function DashboardPage() {

{userInfo.flow === 99999 ? '无限制' : `${calculateUsagePercentage('flow').toFixed(1)}%`}

- {userInfo.flowResetTime && ( + {(userInfo.flowResetTime !== undefined && userInfo.flowResetTime !== null) && (
@@ -682,7 +683,7 @@ export default function DashboardPage() { {tunnelExpStatus.text} - {tunnel.flowResetTime && ( + {(tunnel.flowResetTime !== undefined && tunnel.flowResetTime !== null) && ( {formatResetTime(tunnel.flowResetTime)} diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index 7039e41..ea67a1c 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -74,6 +74,7 @@ interface DiagnosisResult { nodeName: string; nodeId: string; targetIp: string; + targetPort?: number; message?: string; averageTime?: number; packetLoss?: number; @@ -1075,14 +1076,18 @@ export default function ForwardPage() {
目标地址: - {result.targetIp} + + {result.targetIp}{result.targetPort ? ':' + result.targetPort : ''} +
) : (
目标地址: - {result.targetIp} + + {result.targetIp}{result.targetPort ? ':' + result.targetPort : ''} +
- 目标地址: {result.targetIp} + 目标地址: {result.targetIp}:{result.targetPort}
) : (
- 目标地址: {result.targetIp} + 目标地址: {result.targetIp}:{result.targetPort}
重置日期 - 每月{user.flowResetTime || 1}号 + {user.flowResetTime === 0 ? '不重置' : `每月${user.flowResetTime}号`}
{user.expTime && (
@@ -716,6 +716,9 @@ export default function UserPage() { setUserForm(prev => ({ ...prev, flowResetTime: Number(value) })); }} > + + 不重置 + {Array.from({ length: 31 }, (_, i) => i + 1).map(day => ( 每月{day}号(0点重置) @@ -836,6 +839,9 @@ export default function UserPage() { setTunnelForm(prev => ({ ...prev, flowResetTime: Number(value) })); }} > + + 不重置 + {Array.from({ length: 31 }, (_, i) => i + 1).map(day => ( 每月{day}号(0点重置) @@ -919,7 +925,7 @@ export default function UserPage() { {userTunnel.speedLimitName || '不限速'} - 每月{userTunnel.flowResetTime}号 + {userTunnel.flowResetTime === 0 ? '不重置' : `每月${userTunnel.flowResetTime}号`} {formatDate(userTunnel.expTime)}
@@ -1012,6 +1018,9 @@ export default function UserPage() { setEditTunnelForm(prev => prev ? { ...prev, flowResetTime: Number(value) } : null); }} > + + 不重置 + {Array.from({ length: 31 }, (_, i) => i + 1).map(day => ( 每月{day}号(0点重置)