package dhcpd import ( "context" "log/slog" "net" "net/netip" "os" "sync" "sync/atomic" "time" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" ) // ICMPPinger pings with an unprivileged ICMP socket (Linux "ping sockets", // allowed for the groups in net.ipv4.ping_group_range, which systemd opens // to everyone). When the socket can't be opened, the check is skipped and // Unavailable says why. type ICMPPinger struct { Log *slog.Logger seq atomic.Uint32 once sync.Once // Unavailable is why pings can't be sent ("" when they can). Unavailable atomic.Value } // Ping reports whether a answers an echo request within timeout. func (p *ICMPPinger) Ping(ctx context.Context, a netip.Addr, timeout time.Duration) bool { conn, err := icmp.ListenPacket("udp4", "0.0.0.0") if err != nil { p.once.Do(func() { msg := "can't send pings, so addresses aren't checked before they are offered and clients' last-seen times aren't known: " + err.Error() + " (check net.ipv4.ping_group_range)" p.Unavailable.Store(msg) if p.Log != nil { p.Log.Warn(msg) } }) return false } defer conn.Close() seq := int(p.seq.Add(1) & 0xffff) msg := icmp.Message{Type: ipv4.ICMPTypeEcho, Body: &icmp.Echo{ID: os.Getpid() & 0xffff, Seq: seq, Data: []byte("steward")}} b, err := msg.Marshal(nil) if err != nil { return false } dst := &net.UDPAddr{IP: a.AsSlice()} if _, err := conn.WriteTo(b, dst); err != nil { return false } deadline := time.Now().Add(timeout) if d, ok := ctx.Deadline(); ok && d.Before(deadline) { deadline = d } conn.SetReadDeadline(deadline) buf := make([]byte, 1500) for { n, from, err := conn.ReadFrom(buf) if err != nil { return false } ua, ok := from.(*net.UDPAddr) if !ok || !ua.IP.Equal(dst.IP) { continue } m, err := icmp.ParseMessage(1, buf[:n]) if err != nil { continue } if e, ok := m.Body.(*icmp.Echo); ok && m.Type == ipv4.ICMPTypeEchoReply && e.Seq == seq { return true } } } // Problem says why pings can't be sent ("" when they can). func (p *ICMPPinger) Problem() string { if v, ok := p.Unavailable.Load().(string); ok { return v } return "" }