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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user