Windows 下 ICMP 探测改用 ping.exe,避开 raw socket 权限限制
pro-bing 在 Windows 上必须 SetPrivileged(true) 走原始套接字,普通账号 没有权限会直接失败。按平台拆分实现:unix 继续走 pro-bing,windows 调 系统 ping 并以 TTL= 作为跨语言锚点解析输出;同时校验 target 防止被 ping.exe 当作 flag 解析。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
304b113c3b
commit
fe388df67d
3 files changed
+150
-31
No files matched your search
@@ -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()
|
||||
if err != nil {
|
||||
// 如果非特权模式失败,尝试特权模式(需要 root 权限或 CAP_NET_RAW)
|
||||
pinger.SetPrivileged(true)
|
||||
err = pinger.Run()
|
||||
stats, err := pingHost(item.Target, count, timeout)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user