- 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>
96 lines
2.0 KiB
Go
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)
|
|
}
|
|
}
|