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
+153
-34
No files matched your search
@@ -10,8 +10,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
probing "github.com/prometheus-community/pro-bing"
|
|
||||||
|
|
||||||
"github.com/dushixiang/pika/internal/protocol"
|
"github.com/dushixiang/pika/internal/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -245,10 +243,9 @@ func (c *MonitorCollector) checkICMP(item protocol.MonitorItem) protocol.Monitor
|
|||||||
CheckedAt: time.Now().UnixMilli(),
|
CheckedAt: time.Now().UnixMilli(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取配置,使用默认值
|
|
||||||
icmpCfg := item.ICMPConfig
|
icmpCfg := item.ICMPConfig
|
||||||
timeout := 5 // 默认 5 秒
|
timeout := 5
|
||||||
count := 4 // 默认 Ping 4 次
|
count := 4
|
||||||
if icmpCfg != nil {
|
if icmpCfg != nil {
|
||||||
if icmpCfg.Timeout > 0 {
|
if icmpCfg.Timeout > 0 {
|
||||||
timeout = icmpCfg.Timeout
|
timeout = icmpCfg.Timeout
|
||||||
@@ -258,45 +255,25 @@ func (c *MonitorCollector) checkICMP(item protocol.MonitorItem) protocol.Monitor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建 Pinger
|
if !isValidPingTarget(item.Target) {
|
||||||
pinger, err := probing.NewPinger(item.Target)
|
|
||||||
if err != nil {
|
|
||||||
result.Status = "down"
|
result.Status = "down"
|
||||||
result.Error = fmt.Sprintf("create pinger failed: %v", err)
|
result.Error = fmt.Sprintf("invalid ping target: %s", item.Target)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// 配置 Pinger
|
stats, err := pingHost(item.Target, count, timeout)
|
||||||
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 {
|
if err != nil {
|
||||||
// 如果非特权模式失败,尝试特权模式(需要 root 权限或 CAP_NET_RAW)
|
result.Status = "down"
|
||||||
pinger.SetPrivileged(true)
|
result.Error = fmt.Sprintf("ping failed: %v", err)
|
||||||
err = pinger.Run()
|
return result
|
||||||
if err != nil {
|
|
||||||
result.Status = "down"
|
|
||||||
result.Error = fmt.Sprintf("ping failed: %v", err)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取统计信息
|
|
||||||
stats := pinger.Statistics()
|
|
||||||
|
|
||||||
// 检查是否有成功的包
|
|
||||||
if stats.PacketsRecv > 0 {
|
if stats.PacketsRecv > 0 {
|
||||||
result.Status = "up"
|
result.Status = "up"
|
||||||
result.ResponseTime = stats.AvgRtt.Milliseconds()
|
result.ResponseTime = stats.AvgRttMs
|
||||||
packetLoss := int(stats.PacketLoss)
|
loss := int(stats.PacketLoss)
|
||||||
result.Message = fmt.Sprintf("ICMP Echo Reply - %d/%d packets, %dms avg, %d%% loss",
|
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 {
|
} else {
|
||||||
result.Status = "down"
|
result.Status = "down"
|
||||||
result.Error = fmt.Sprintf("all %d ping attempts failed (timeout: %ds)", count, timeout)
|
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
|
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