Peers: optional latency check with per-peer setting

The server pings a peer's tunnel address every 30 s and shows the median
of the last 5 minutes in the peer list (with a 1-hour sparkline) and a
24-hour chart on the peer page. Off by default; "active" pings only
while the device sends traffic, "always" keeps the tunnel up.
This commit is contained in:
Daniel Redetzke
2026-10-04 14:28:44 +03:00
parent b0be0b0ceb
commit 7391aac429
14 changed files with 737 additions and 33 deletions
+90
View File
@@ -0,0 +1,90 @@
//go:build linux
package main
import (
"errors"
"math/rand/v2"
"net"
"net/netip"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
// listenICMP opens an unprivileged ICMP socket, which the kernel allows for
// the groups in net.ipv4.ping_group_range (systemd opens it to all groups).
// Running as root, a raw socket works too.
func listenICMP() (c *icmp.PacketConn, raw bool, err error) {
if c, err = icmp.ListenPacket("udp4", "0.0.0.0"); err == nil {
return c, false, nil
}
if c, err2 := icmp.ListenPacket("ip4:icmp", "0.0.0.0"); err2 == nil {
return c, true, nil
}
return nil, false, errors.New("cannot open an ICMP socket (" + err.Error() + "): add the service's group to the sysctl net.ipv4.ping_group_range")
}
func (k *linuxKernel) Ping(dsts []netip.Addr, timeout time.Duration) (map[netip.Addr]time.Duration, error) {
conn, raw, err := listenICMP()
if err != nil {
return nil, err
}
defer conn.Close()
// On an unprivileged socket the kernel sets the ID and delivers only the
// socket's own replies; on a raw socket the ID tells them apart.
id := rand.IntN(0xffff) + 1
bySeq := map[int]netip.Addr{}
sent := map[netip.Addr]time.Time{}
for i, d := range dsts {
seq := i + 1
b, err := (&icmp.Message{Type: ipv4.ICMPTypeEcho, Body: &icmp.Echo{ID: id, Seq: seq, Data: []byte(appName)}}).Marshal(nil)
if err != nil {
return nil, err
}
var to net.Addr = &net.UDPAddr{IP: d.AsSlice()}
if raw {
to = &net.IPAddr{IP: d.AsSlice()}
}
sent[d] = time.Now()
if _, err := conn.WriteTo(b, to); err != nil {
delete(sent, d) // e.g. the peer has no endpoint: counts as lost
continue
}
bySeq[seq] = d
}
out := map[netip.Addr]time.Duration{}
_ = conn.SetReadDeadline(time.Now().Add(timeout))
buf := make([]byte, 1500)
for len(out) < len(sent) {
n, from, err := conn.ReadFrom(buf)
if err != nil {
break // deadline reached
}
now := time.Now()
m, err := icmp.ParseMessage(1, buf[:n])
if err != nil || m.Type != ipv4.ICMPTypeEchoReply {
continue
}
e, ok := m.Body.(*icmp.Echo)
if !ok || (raw && e.ID != id) {
continue
}
var src netip.Addr
switch a := from.(type) {
case *net.UDPAddr:
src, _ = netip.AddrFromSlice(a.IP)
case *net.IPAddr:
src, _ = netip.AddrFromSlice(a.IP)
}
src = src.Unmap()
if d, ok := bySeq[e.Seq]; !ok || d != src {
continue
}
if _, dup := out[src]; !dup {
out[src] = now.Sub(sent[src])
}
}
return out, nil
}