diff --git a/pkg/agent/collector/monitor.go b/pkg/agent/collector/monitor.go index bcd2c26..c7bf472 100644 --- a/pkg/agent/collector/monitor.go +++ b/pkg/agent/collector/monitor.go @@ -10,8 +10,6 @@ import ( "strings" "time" - probing "github.com/prometheus-community/pro-bing" - "github.com/dushixiang/pika/internal/protocol" ) @@ -245,10 +243,9 @@ func (c *MonitorCollector) checkICMP(item protocol.MonitorItem) protocol.Monitor CheckedAt: time.Now().UnixMilli(), } - // 获取配置,使用默认值 icmpCfg := item.ICMPConfig - timeout := 5 // 默认 5 秒 - count := 4 // 默认 Ping 4 次 + timeout := 5 + count := 4 if icmpCfg != nil { if icmpCfg.Timeout > 0 { timeout = icmpCfg.Timeout @@ -258,45 +255,25 @@ func (c *MonitorCollector) checkICMP(item protocol.MonitorItem) protocol.Monitor } } - // 创建 Pinger - pinger, err := probing.NewPinger(item.Target) - if err != nil { + if !isValidPingTarget(item.Target) { result.Status = "down" - result.Error = fmt.Sprintf("create pinger failed: %v", err) + result.Error = fmt.Sprintf("invalid ping target: %s", item.Target) return result } - // 配置 Pinger - pinger.Count = count - pinger.Timeout = time.Duration(timeout) * time.Second - pinger.Interval = 100 * time.Millisecond - - // 尝试以非特权模式运行(使用 UDP) - pinger.SetPrivileged(false) - - // 执行 Ping - err = pinger.Run() + stats, err := pingHost(item.Target, count, timeout) if err != nil { - // 如果非特权模式失败,尝试特权模式(需要 root 权限或 CAP_NET_RAW) - pinger.SetPrivileged(true) - err = pinger.Run() - if err != nil { - result.Status = "down" - result.Error = fmt.Sprintf("ping failed: %v", err) - return result - } + result.Status = "down" + result.Error = fmt.Sprintf("ping failed: %v", err) + return result } - // 获取统计信息 - stats := pinger.Statistics() - - // 检查是否有成功的包 if stats.PacketsRecv > 0 { result.Status = "up" - result.ResponseTime = stats.AvgRtt.Milliseconds() - packetLoss := int(stats.PacketLoss) + result.ResponseTime = stats.AvgRttMs + loss := int(stats.PacketLoss) result.Message = fmt.Sprintf("ICMP Echo Reply - %d/%d packets, %dms avg, %d%% loss", - stats.PacketsRecv, stats.PacketsSent, stats.AvgRtt.Milliseconds(), packetLoss) + stats.PacketsRecv, stats.PacketsSent, stats.AvgRttMs, loss) } else { result.Status = "down" result.Error = fmt.Sprintf("all %d ping attempts failed (timeout: %ds)", count, timeout) @@ -305,3 +282,29 @@ func (c *MonitorCollector) checkICMP(item protocol.MonitorItem) protocol.Monitor return result } + +// pingStats 跨平台 ping 统计结果 +type pingStats struct { + PacketsSent int + PacketsRecv int + AvgRttMs int64 + PacketLoss float64 +} + +// isValidPingTarget 校验 ping 目标,避免在 Windows 下被 ping.exe 解析为 flag 或注入额外参数 +func isValidPingTarget(target string) bool { + if target == "" || strings.HasPrefix(target, "-") { + return false + } + for _, r := range target { + switch { + case r >= 'a' && r <= 'z': + case r >= 'A' && r <= 'Z': + case r >= '0' && r <= '9': + case r == '.', r == ':', r == '-', r == '_': + default: + return false + } + } + return true +} diff --git a/pkg/agent/collector/monitor_icmp_unix.go b/pkg/agent/collector/monitor_icmp_unix.go new file mode 100644 index 0000000..fbd3089 --- /dev/null +++ b/pkg/agent/collector/monitor_icmp_unix.go @@ -0,0 +1,37 @@ +//go:build !windows + +package collector + +import ( + "time" + + probing "github.com/prometheus-community/pro-bing" +) + +func pingHost(target string, count, timeoutSec int) (*pingStats, error) { + pinger, err := probing.NewPinger(target) + if err != nil { + return nil, err + } + + pinger.Count = count + pinger.Timeout = time.Duration(timeoutSec) * time.Second + pinger.Interval = 100 * time.Millisecond + pinger.SetPrivileged(false) + + if err := pinger.Run(); err != nil { + // 非特权 UDP ICMP 不可用时回退到 raw socket(需要 root / CAP_NET_RAW) + pinger.SetPrivileged(true) + if err := pinger.Run(); err != nil { + return nil, err + } + } + + s := pinger.Statistics() + return &pingStats{ + PacketsSent: s.PacketsSent, + PacketsRecv: s.PacketsRecv, + AvgRttMs: s.AvgRtt.Milliseconds(), + PacketLoss: s.PacketLoss, + }, nil +} diff --git a/pkg/agent/collector/monitor_icmp_windows.go b/pkg/agent/collector/monitor_icmp_windows.go new file mode 100644 index 0000000..562ffa9 --- /dev/null +++ b/pkg/agent/collector/monitor_icmp_windows.go @@ -0,0 +1,79 @@ +//go:build windows + +package collector + +import ( + "bytes" + "context" + "fmt" + "os/exec" + "regexp" + "strconv" + "syscall" + "time" +) + +// "TTL=" 在 Windows ping.exe 各语言版本输出中都保留为英文字面量, +// 因此用作回包行的锚点比 "Reply from" / "来自" 之类本地化文案更稳妥 +var ( + winPingReplyAnchor = regexp.MustCompile(`(?i)TTL[=:]`) + winPingTimeRe = regexp.MustCompile(`(?i)[=<]\s*(\d+)\s*ms`) +) + +func pingHost(target string, count, timeoutSec int) (*pingStats, error) { + if count <= 0 { + count = 4 + } + if timeoutSec <= 0 { + timeoutSec = 5 + } + + perReplyMs := timeoutSec * 1000 + + // 进程级超时取每包超时 × 包数 + 余量;ping 自身在包间还会停 ~1s + overall := time.Duration(timeoutSec*count+5) * time.Second + ctx, cancel := context.WithTimeout(context.Background(), overall) + defer cancel() + + cmd := exec.CommandContext(ctx, "ping", + "-n", strconv.Itoa(count), + "-w", strconv.Itoa(perReplyMs), + target, + ) + cmd.SysProcAttr = &syscall.SysProcAttr{ + HideWindow: true, + CreationFlags: 0x08000000, // CREATE_NO_WINDOW,避免在前台 console 下闪窗 + } + + out, runErr := cmd.CombinedOutput() + if ctx.Err() == context.DeadlineExceeded { + return nil, fmt.Errorf("ping command timed out after %v", overall) + } + if runErr != nil && len(out) == 0 { + return nil, fmt.Errorf("ping command failed: %w", runErr) + } + + stats := &pingStats{PacketsSent: count} + var totalRtt int64 + for _, line := range bytes.Split(out, []byte("\n")) { + if !winPingReplyAnchor.Match(line) { + continue + } + m := winPingTimeRe.FindSubmatch(line) + if m == nil { + continue + } + rtt, err := strconv.Atoi(string(m[1])) + if err != nil { + continue + } + stats.PacketsRecv++ + totalRtt += int64(rtt) + } + + if stats.PacketsRecv > 0 { + stats.AvgRttMs = totalRtt / int64(stats.PacketsRecv) + } + stats.PacketLoss = float64(stats.PacketsSent-stats.PacketsRecv) * 100 / float64(stats.PacketsSent) + return stats, nil +}