7391aac429
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.
91 lines
2.4 KiB
Go
91 lines
2.4 KiB
Go
//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
|
|
}
|