From 2fd00cd0b5ef96c77085f3edf3dbb1518c5aa966 Mon Sep 17 00:00:00 2001 From: xiaomiku01 Date: Sat, 11 Apr 2026 21:09:12 +0800 Subject: [PATCH] feat(agent): use native ICMP sockets with fallback to system ping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the ping-command-only implementation with a three-tier approach using golang.org/x/net/icmp: 1. Raw socket (ip4:icmp) — works with root or CAP_NET_RAW 2. Unprivileged datagram socket (udp4) — works on Linux/macOS without special privileges 3. System ping command — fallback when neither socket works The method is auto-detected on first probe and cached for all subsequent calls, avoiding repeated failed attempts. Co-Authored-By: Claude Opus 4.6 (1M context) --- agent/probe_ping.go | 138 +++++++++++++++++++++++++++++++++++++++++++- go.mod | 2 +- 2 files changed, 138 insertions(+), 2 deletions(-) diff --git a/agent/probe_ping.go b/agent/probe_ping.go index 64a961e3..ad73a4bc 100644 --- a/agent/probe_ping.go +++ b/agent/probe_ping.go @@ -1,17 +1,153 @@ package agent import ( + "net" + "os" "os/exec" "regexp" "runtime" "strconv" + "sync" "time" + + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" + + "log/slog" ) var pingTimeRegex = regexp.MustCompile(`time[=<]([\d.]+)\s*ms`) -// probeICMP executes system ping command and parses latency. Returns -1 on failure. +// icmpMethod tracks which ICMP approach to use. Once a method succeeds or +// all native methods fail, the choice is cached so subsequent probes skip +// the trial-and-error overhead. +type icmpMethod int + +const ( + icmpUntried icmpMethod = iota // haven't tried yet + icmpRaw // privileged raw socket (ip4:icmp) + icmpDatagram // unprivileged datagram socket (udp4) + icmpExecFallback // shell out to system ping command +) + +var ( + icmpMode icmpMethod + icmpModeMu sync.Mutex +) + +// probeICMP sends an ICMP echo request and measures round-trip latency. +// It tries native raw socket first, then unprivileged datagram socket, +// and falls back to the system ping command if both fail. +// Returns latency in milliseconds, or -1 on failure. func probeICMP(target string) float64 { + icmpModeMu.Lock() + mode := icmpMode + icmpModeMu.Unlock() + + switch mode { + case icmpRaw: + return probeICMPNative("ip4:icmp", "0.0.0.0", target) + case icmpDatagram: + return probeICMPNative("udp4", "0.0.0.0", target) + case icmpExecFallback: + return probeICMPExec(target) + default: + // First call — probe which method works + return probeICMPDetect(target) + } +} + +// probeICMPDetect tries each ICMP method in order and caches the first +// one that succeeds. +func probeICMPDetect(target string) float64 { + // 1. Try privileged raw socket + if ms := probeICMPNative("ip4:icmp", "0.0.0.0", target); ms >= 0 { + icmpModeMu.Lock() + icmpMode = icmpRaw + icmpModeMu.Unlock() + slog.Info("ICMP probe using raw socket") + return ms + } + + // 2. Try unprivileged datagram socket (Linux/macOS) + if ms := probeICMPNative("udp4", "0.0.0.0", target); ms >= 0 { + icmpModeMu.Lock() + icmpMode = icmpDatagram + icmpModeMu.Unlock() + slog.Info("ICMP probe using unprivileged datagram socket") + return ms + } + + // 3. Fall back to system ping command + slog.Info("ICMP probe falling back to system ping command") + icmpModeMu.Lock() + icmpMode = icmpExecFallback + icmpModeMu.Unlock() + return probeICMPExec(target) +} + +// probeICMPNative sends an ICMP echo request using Go's x/net/icmp package. +// network is "ip4:icmp" for raw sockets or "udp4" for unprivileged datagram sockets. +func probeICMPNative(network, listenAddr, target string) float64 { + // Resolve the target to an IP address + dst, err := net.ResolveIPAddr("ip4", target) + if err != nil { + return -1 + } + + conn, err := icmp.ListenPacket(network, listenAddr) + if err != nil { + return -1 + } + defer conn.Close() + + // Build ICMP echo request + msg := &icmp.Message{ + Type: ipv4.ICMPTypeEcho, + Code: 0, + Body: &icmp.Echo{ + ID: os.Getpid() & 0xffff, + Seq: 1, + Data: []byte("beszel-probe"), + }, + } + msgBytes, err := msg.Marshal(nil) + if err != nil { + return -1 + } + + // Set deadline before sending + conn.SetDeadline(time.Now().Add(3 * time.Second)) + + start := time.Now() + if _, err := conn.WriteTo(msgBytes, dst); err != nil { + return -1 + } + + // Read reply + buf := make([]byte, 1500) + for { + n, _, err := conn.ReadFrom(buf) + if err != nil { + return -1 + } + + // Parse the ICMP protocol number based on network type + proto := 1 // ICMPv4 + reply, err := icmp.ParseMessage(proto, buf[:n]) + if err != nil { + return -1 + } + + if reply.Type == ipv4.ICMPTypeEchoReply { + return float64(time.Since(start).Microseconds()) / 1000.0 + } + // Ignore non-echo-reply messages (e.g. destination unreachable) and keep reading + } +} + +// probeICMPExec falls back to the system ping command. Returns -1 on failure. +func probeICMPExec(target string) float64 { var cmd *exec.Cmd switch runtime.GOOS { case "windows": diff --git a/go.mod b/go.mod index 2ae2066b..b4b10d6d 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/stretchr/testify v1.11.1 golang.org/x/crypto v0.49.0 golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 + golang.org/x/net v0.52.0 golang.org/x/sys v0.42.0 gopkg.in/yaml.v3 v3.0.1 howett.net/plist v1.0.1 @@ -56,7 +57,6 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/image v0.38.0 // indirect - golang.org/x/net v0.52.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/term v0.41.0 // indirect