diff --git a/go-backend/internal/http/handler/control_plane.go b/go-backend/internal/http/handler/control_plane.go index 0e69019..ea8e563 100644 --- a/go-backend/internal/http/handler/control_plane.go +++ b/go-backend/internal/http/handler/control_plane.go @@ -42,6 +42,7 @@ type diagnosisWorkItem struct { toNode chainNodeRecord hasChainHop bool ipPreference string + protocol string } type diagnosisExecOptions struct { @@ -692,6 +693,7 @@ func (h *Handler) prepareForwardDiagnosis(forward *forwardRecord) (string, []dia } ipPreference := h.repo.GetTunnelIPPreference(forward.TunnelID) + protocol := strings.ToLower(strings.TrimSpace(tunnel.Protocol)) inNodes, chainHops, outNodes := splitChainNodeGroups(chainRows) workItems := make([]diagnosisWorkItem, 0, len(chainRows)*2+len(targets)) @@ -706,6 +708,7 @@ func (h *Handler) prepareForwardDiagnosis(forward *forwardRecord) (string, []dia targetIP: target.IP, targetPort: target.Port, description: description, + protocol: protocol, metadata: map[string]interface{}{ "fromChainType": 1, }, @@ -723,6 +726,7 @@ func (h *Handler) prepareForwardDiagnosis(forward *forwardRecord) (string, []dia hasChainHop: true, ipPreference: ipPreference, description: description, + protocol: protocol, metadata: map[string]interface{}{ "fromChainType": 1, "toChainType": 2, @@ -739,6 +743,7 @@ func (h *Handler) prepareForwardDiagnosis(forward *forwardRecord) (string, []dia hasChainHop: true, ipPreference: ipPreference, description: description, + protocol: protocol, metadata: map[string]interface{}{ "fromChainType": 1, "toChainType": 3, @@ -759,6 +764,7 @@ func (h *Handler) prepareForwardDiagnosis(forward *forwardRecord) (string, []dia hasChainHop: true, ipPreference: ipPreference, description: description, + protocol: protocol, metadata: map[string]interface{}{ "fromChainType": 2, "fromInx": currentNode.Inx, @@ -776,6 +782,7 @@ func (h *Handler) prepareForwardDiagnosis(forward *forwardRecord) (string, []dia hasChainHop: true, ipPreference: ipPreference, description: description, + protocol: protocol, metadata: map[string]interface{}{ "fromChainType": 2, "fromInx": currentNode.Inx, @@ -795,6 +802,7 @@ func (h *Handler) prepareForwardDiagnosis(forward *forwardRecord) (string, []dia targetIP: target.IP, targetPort: target.Port, description: description, + protocol: protocol, metadata: map[string]interface{}{ "fromChainType": 3, }, @@ -810,6 +818,7 @@ func (h *Handler) prepareForwardDiagnosis(forward *forwardRecord) (string, []dia targetIP: target.IP, targetPort: target.Port, description: description, + protocol: protocol, metadata: map[string]interface{}{ "fromChainType": 1, }, @@ -864,6 +873,7 @@ func (h *Handler) prepareTunnelDiagnosis(tunnelID int64) (string, string, []diag } ipPreference := h.repo.GetTunnelIPPreference(tunnelID) + protocol := strings.ToLower(strings.TrimSpace(tunnel.Protocol)) inNodes, chainHops, outNodes := splitChainNodeGroups(chainRows) workItems := make([]diagnosisWorkItem, 0, len(chainRows)*2) @@ -876,6 +886,7 @@ func (h *Handler) prepareTunnelDiagnosis(tunnelID int64) (string, string, []diag targetIP: "www.bing.com", targetPort: 443, description: description, + protocol: protocol, metadata: map[string]interface{}{ "fromChainType": 1, }, @@ -892,6 +903,7 @@ func (h *Handler) prepareTunnelDiagnosis(tunnelID int64) (string, string, []diag hasChainHop: true, ipPreference: ipPreference, description: description, + protocol: protocol, metadata: map[string]interface{}{ "fromChainType": 1, "toChainType": 2, @@ -908,6 +920,7 @@ func (h *Handler) prepareTunnelDiagnosis(tunnelID int64) (string, string, []diag hasChainHop: true, ipPreference: ipPreference, description: description, + protocol: protocol, metadata: map[string]interface{}{ "fromChainType": 1, "toChainType": 3, @@ -928,6 +941,7 @@ func (h *Handler) prepareTunnelDiagnosis(tunnelID int64) (string, string, []diag hasChainHop: true, ipPreference: ipPreference, description: description, + protocol: protocol, metadata: map[string]interface{}{ "fromChainType": 2, "fromInx": currentNode.Inx, @@ -945,6 +959,7 @@ func (h *Handler) prepareTunnelDiagnosis(tunnelID int64) (string, string, []diag hasChainHop: true, ipPreference: ipPreference, description: description, + protocol: protocol, metadata: map[string]interface{}{ "fromChainType": 2, "fromInx": currentNode.Inx, @@ -963,6 +978,7 @@ func (h *Handler) prepareTunnelDiagnosis(tunnelID int64) (string, string, []diag targetIP: "www.bing.com", targetPort: 443, description: description, + protocol: protocol, metadata: map[string]interface{}{ "fromChainType": 3, }, @@ -976,6 +992,7 @@ func (h *Handler) prepareTunnelDiagnosis(tunnelID int64) (string, string, []diag targetIP: "www.bing.com", targetPort: 443, description: description, + protocol: protocol, metadata: map[string]interface{}{ "fromChainType": 1, }, @@ -1095,9 +1112,9 @@ func (h *Handler) executeDiagnosisWorkItem(workItem diagnosisWorkItem, options d single := make([]map[string]interface{}, 0, 1) nodeCache := map[int64]*nodeRecord{} if workItem.hasChainHop { - h.appendChainHopDiagnosis(&single, nodeCache, workItem.fromNodeID, workItem.toNode, workItem.description, workItem.metadata, workItem.ipPreference, options) + h.appendChainHopDiagnosis(&single, nodeCache, workItem.fromNodeID, workItem.toNode, workItem.description, workItem.metadata, workItem.ipPreference, workItem.protocol, options) } else { - h.appendPathDiagnosis(&single, nodeCache, workItem.fromNodeID, workItem.targetIP, workItem.targetPort, workItem.description, workItem.metadata, options) + h.appendPathDiagnosis(&single, nodeCache, workItem.fromNodeID, workItem.targetIP, workItem.targetPort, workItem.description, workItem.metadata, workItem.protocol, options) } if len(single) == 0 { @@ -1223,14 +1240,14 @@ func (h *Handler) appendFailedDiagnosis(results *[]map[string]interface{}, nodeC item["nodeName"] = node.Name } if strings.TrimSpace(message) == "" { - message = "TCP连接失败" + message = "连接失败" } item["success"] = false item["message"] = message *results = append(*results, item) } -func (h *Handler) appendPathDiagnosis(results *[]map[string]interface{}, nodeCache map[int64]*nodeRecord, fromNodeID int64, targetIP string, targetPort int, description string, metadata map[string]interface{}, options diagnosisExecOptions) { +func (h *Handler) appendPathDiagnosis(results *[]map[string]interface{}, nodeCache map[int64]*nodeRecord, fromNodeID int64, targetIP string, targetPort int, description string, metadata map[string]interface{}, protocol string, options diagnosisExecOptions) { item := newDiagnosisResultItem(fromNodeID, targetIP, targetPort, description, metadata) fromNode, err := h.cachedNode(nodeCache, fromNodeID) @@ -1247,9 +1264,9 @@ func (h *Handler) appendPathDiagnosis(results *[]map[string]interface{}, nodeCac pingErr error ) if fromNode.IsRemote == 1 { - pingData, pingErr = h.tcpPingViaRemoteNode(fromNode, targetIP, targetPort, options) + pingData, pingErr = h.pingViaRemoteNode(fromNode, targetIP, targetPort, protocol, options) } else { - pingData, pingErr = h.tcpPingViaNode(fromNodeID, targetIP, targetPort, options) + pingData, pingErr = h.pingViaNode(fromNodeID, targetIP, targetPort, protocol, options) } if pingErr != nil { item["success"] = false @@ -1266,21 +1283,21 @@ func (h *Handler) appendPathDiagnosis(results *[]map[string]interface{}, nodeCac message := strings.TrimSpace(asString(pingData["message"])) if success { if message == "" { - message = "TCP连接成功" + message = "连接成功" } } else { if message == "" { message = strings.TrimSpace(asString(pingData["errorMessage"])) } if message == "" { - message = "TCP连接失败" + message = "连接失败" } } item["message"] = message *results = append(*results, item) } -func (h *Handler) appendChainHopDiagnosis(results *[]map[string]interface{}, nodeCache map[int64]*nodeRecord, fromNodeID int64, toNode chainNodeRecord, description string, metadata map[string]interface{}, ipPreference string, options diagnosisExecOptions) { +func (h *Handler) appendChainHopDiagnosis(results *[]map[string]interface{}, nodeCache map[int64]*nodeRecord, fromNodeID int64, toNode chainNodeRecord, description string, metadata map[string]interface{}, ipPreference string, protocol string, options diagnosisExecOptions) { fromNode, _ := h.cachedNode(nodeCache, fromNodeID) targetNode, err := h.cachedNode(nodeCache, toNode.NodeID) if err != nil { @@ -1292,7 +1309,7 @@ func (h *Handler) appendChainHopDiagnosis(results *[]map[string]interface{}, nod h.appendFailedDiagnosis(results, nodeCache, fromNodeID, strings.Trim(strings.TrimSpace(targetNode.ServerIP), "[]"), toNode.Port, description, metadata, err.Error()) return } - h.appendPathDiagnosis(results, nodeCache, fromNodeID, targetIP, targetPort, description, metadata, options) + h.appendPathDiagnosis(results, nodeCache, fromNodeID, targetIP, targetPort, description, metadata, protocol, options) } func resolveChainProbeTarget(fromNode, targetNode *nodeRecord, preferredPort int, ipPreference string, connectIp string) (string, int, error) { @@ -1392,6 +1409,44 @@ func (h *Handler) tcpPingViaRemoteNode(node *nodeRecord, ip string, port int, op }) } +func isUDPBasedProtocol(protocol string) bool { + p := strings.ToLower(strings.TrimSpace(protocol)) + return p == "kcp" || p == "udp" || p == "quic" +} + +func (h *Handler) pingViaNode(nodeID int64, ip string, port int, protocol string, options diagnosisExecOptions) (map[string]interface{}, error) { + if isUDPBasedProtocol(protocol) { + return h.udpPingViaNode(nodeID, ip, port, options) + } + return h.tcpPingViaNode(nodeID, ip, port, options) +} + +func (h *Handler) pingViaRemoteNode(node *nodeRecord, ip string, port int, protocol string, options diagnosisExecOptions) (map[string]interface{}, error) { + return h.tcpPingViaRemoteNode(node, ip, port, options) +} + +func (h *Handler) udpPingViaNode(nodeID int64, ip string, port int, options diagnosisExecOptions) (map[string]interface{}, error) { + if options.commandTimeout <= 0 { + options.commandTimeout = diagnosisCommandTimeout + } + if options.pingTimeoutMS <= 0 { + options.pingTimeoutMS = int(diagnosisCommandTimeout / time.Millisecond) + } + res, err := h.sendNodeCommandWithTimeout(nodeID, "UdpPing", map[string]interface{}{ + "ip": ip, + "port": port, + "count": 4, + "timeout": options.pingTimeoutMS, + }, options.commandTimeout, false, false) + if err != nil { + return nil, err + } + if res.Data == nil { + return nil, errors.New("节点未返回诊断数据") + } + return res.Data, nil +} + func splitRemoteTargets(remoteAddr string) []string { parts := strings.Split(remoteAddr, ",") out := make([]string, 0, len(parts)) diff --git a/go-backend/internal/store/model/model.go b/go-backend/internal/store/model/model.go index 3111963..605ec5c 100644 --- a/go-backend/internal/store/model/model.go +++ b/go-backend/internal/store/model/model.go @@ -546,6 +546,7 @@ type TunnelRecord struct { Status int Flow int64 TrafficRatio float64 + Protocol string } type UserQuotaView struct { diff --git a/go-backend/internal/store/repo/repository_flow.go b/go-backend/internal/store/repo/repository_flow.go index 2248df2..8d1ef83 100644 --- a/go-backend/internal/store/repo/repository_flow.go +++ b/go-backend/internal/store/repo/repository_flow.go @@ -158,6 +158,7 @@ func (r *Repository) GetTunnelRecord(tunnelID int64) (*model.TunnelRecord, error Status: t.Status, Flow: t.Flow, TrafficRatio: t.TrafficRatio, + Protocol: t.Protocol, } if tr.Flow <= 0 { tr.Flow = 1 diff --git a/go-gost/x/socket/websocket_reporter.go b/go-gost/x/socket/websocket_reporter.go index 2032145..72100b8 100644 --- a/go-gost/x/socket/websocket_reporter.go +++ b/go-gost/x/socket/websocket_reporter.go @@ -845,6 +845,13 @@ func (w *WebSocketReporter) routeCommand(cmd CommandMessage) { response.Data = tcpPingResult // needSaveConfig = false (默认值) + // UDP Ping 诊断命令(只读,不需要保存配置) + case "UdpPing": + var udpPingResult TcpPingResponse + udpPingResult, err = w.handleUdpPing(cmd.Data) + response.Type = "UdpPingResponse" + response.Data = udpPingResult + // Service monitor check (read-only) case "ServiceMonitorCheck": var checkResult ServiceMonitorCheckResult @@ -1673,6 +1680,64 @@ func (w *WebSocketReporter) handleTcpPing(data interface{}) (TcpPingResponse, er return response, nil } +func (w *WebSocketReporter) handleUdpPing(data interface{}) (TcpPingResponse, error) { + jsonData, err := json.Marshal(data) + if err != nil { + return TcpPingResponse{}, fmt.Errorf("序列化UDP ping数据失败: %v", err) + } + + var req TcpPingRequest + if err := json.Unmarshal(jsonData, &req); err != nil { + return TcpPingResponse{}, fmt.Errorf("解析UDP ping请求失败: %v", err) + } + + if net.ParseIP(req.IP) == nil && !isValidHostname(req.IP) { + return TcpPingResponse{ + IP: req.IP, + Port: req.Port, + Success: false, + ErrorMessage: "无效的IP地址或主机名", + RequestId: req.RequestId, + }, nil + } + + 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 + } + + avgTime, packetLoss, err := udpPingHost(req.IP, req.Port, req.Count, req.Timeout) + + response := TcpPingResponse{ + IP: req.IP, + Port: req.Port, + RequestId: req.RequestId, + } + + if err != nil { + response.Success = false + response.ErrorMessage = err.Error() + } else { + response.Success = true + response.AverageTime = avgTime + response.PacketLoss = packetLoss + } + + return response, nil +} + // handleServiceMonitorCheck executes a service monitor check on this node. // It always returns a result (command execution is considered successful even if the check fails). func (w *WebSocketReporter) handleServiceMonitorCheck(data interface{}) (ServiceMonitorCheckResult, error) { @@ -1951,6 +2016,76 @@ func tcpPingHost(ip string, port int, count int, timeoutMs int) (float64, float6 return avgTime, packetLoss, nil } +func udpPingHost(ip string, port int, count int, timeoutMs int) (float64, float64, error) { + var totalTime float64 + var successCount int + + timeout := time.Duration(timeoutMs) * time.Millisecond + target := net.JoinHostPort(ip, fmt.Sprintf("%d", port)) + + fmt.Printf("🔍 开始UDP ping测试: %s,次数: %d,超时: %dms\n", target, count, timeoutMs) + + if net.ParseIP(ip) == nil { + fmt.Printf("🔍 检测到域名,正在解析DNS...\n") + dnsStart := time.Now() + + addrs, err := net.LookupHost(ip) + dnsDuration := time.Since(dnsStart) + + if err != nil { + return 0, 100.0, fmt.Errorf("DNS解析失败: %v", err) + } + if len(addrs) == 0 { + return 0, 100.0, fmt.Errorf("DNS解析未返回任何IP地址") + } + + fmt.Printf("✅ DNS解析完成 (%.2fms),解析到 %d 个IP: %v\n", + dnsDuration.Seconds()*1000, len(addrs), addrs) + + target = net.JoinHostPort(addrs[0], fmt.Sprintf("%d", port)) + fmt.Printf("🎯 使用IP地址进行测试: %s\n", target) + } else { + fmt.Printf("🎯 使用IP地址进行测试: %s\n", target) + } + + addr, err := net.ResolveUDPAddr("udp", target) + if err != nil { + return 0, 100.0, fmt.Errorf("解析UDP地址失败: %v", err) + } + + for i := 0; i < count; i++ { + start := time.Now() + + conn, err := net.DialTimeout("udp", addr.String(), timeout) + + elapsed := time.Since(start) + + if err != nil { + fmt.Printf(" 第%d次UDP连接失败: %v (%.2fms)\n", i+1, err, elapsed.Seconds()*1000) + } else { + fmt.Printf(" 第%d次UDP连接成功: %.2fms\n", i+1, elapsed.Seconds()*1000) + conn.Close() + totalTime += elapsed.Seconds() * 1000 + successCount++ + } + + if i < count-1 { + time.Sleep(100 * time.Millisecond) + } + } + + if successCount == 0 { + return 0, 100.0, fmt.Errorf("所有UDP连接尝试都失败") + } + + avgTime := totalTime / float64(successCount) + packetLoss := float64(count-successCount) / float64(count) * 100 + + fmt.Printf("✅ UDP ping完成: 平均连接时间 %.2fms,失败率 %.1f%%\n", avgTime, packetLoss) + + return avgTime, packetLoss, nil +} + // isValidHostname 验证主机名格式 func isValidHostname(hostname string) bool { if len(hostname) == 0 || len(hostname) > 253 {