- interfaces.exclude in steward.yaml (names or shell patterns) leaves interfaces out of GET /interfaces, the web UI's picker and 'steward interface list'. Subnets on them are still served. - With the ping check on, active leases are pinged every minute; a client that answers gets a last-seen time (leases.last_seen, migration 4). Not answering never takes a client offline. - The activity log filters by subnet (--subnet, the Activity page, GET /events?subnet=). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
2.1 KiB
Go
85 lines
2.1 KiB
Go
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 ""
|
|
}
|