Files
steward/internal/dhcpd/seen.go
T
abrown38andClaude Opus 5.5 5e6c61cc8e Hidden interfaces, last-seen pings, activity by subnet
- 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>
2026-10-18 17:50:21 +01:00

96 lines
2.0 KiB
Go

package dhcpd
import (
"context"
"net/netip"
"sync"
"time"
"steward/internal/model"
)
// How often active leases' addresses are pinged for their last-seen time,
// and how many pings run at once.
const (
seenEvery = time.Minute
seenWorkers = 16
)
// watchSeen pings the active leases' addresses every seenEvery.
func (s *Server) watchSeen(ctx context.Context) {
t := time.NewTicker(seenEvery)
defer t.Stop()
for {
s.CheckSeen(ctx)
select {
case <-ctx.Done():
return
case <-t.C:
}
}
}
// CheckSeen pings every active lease's address once (when the ping check
// is on), and records when each that answered was seen. A client that
// doesn't answer keeps its last time, or none: many block pings, and stay
// online while their lease lasts.
func (s *Server) CheckSeen(ctx context.Context) {
cfg := s.cfg.Load()
if s.pinger == nil || !cfg.settings.PingCheck {
return
}
now := s.now()
var active []model.Lease
s.mu.Lock()
for _, l := range s.leases {
if l.ActiveAt(now) {
active = append(active, *l)
}
}
s.mu.Unlock()
timeout := cfg.settings.EffectivePingTimeout()
var (
mu sync.Mutex
seen = map[netip.Addr]time.Time{}
wg sync.WaitGroup
sem = make(chan struct{}, seenWorkers)
)
for _, l := range active {
if ctx.Err() != nil {
break
}
sem <- struct{}{}
wg.Add(1)
go func() {
defer func() { <-sem; wg.Done() }()
if s.pinger.Ping(ctx, l.IP, timeout) {
mu.Lock()
seen[l.IP] = s.now()
mu.Unlock()
}
}()
}
wg.Wait()
if len(seen) == 0 {
return
}
s.mu.Lock()
defer s.mu.Unlock()
for _, was := range active {
at, ok := seen[was.IP]
if !ok {
continue
}
// The address may have gone to another client meanwhile.
l := s.leases[was.IP]
if l == nil || l.MAC != was.MAC || l.ClientID != was.ClientID || !l.ActiveAt(at) {
delete(seen, was.IP)
continue
}
l.LastSeen = &at
}
if err := s.store.SetLastSeen(ctx, seen); err != nil {
s.warnOnce("last seen", time.Hour, "saving clients' last-seen times failed", "err", err)
}
}