Harden API surface and station/wifi backends
Bind to localhost by default and stop echoing backend errors (which can embed credentials or low-level details) back over the API and log broadcast. Validate hotspot SSID/passphrase/channel before writing hostapd.conf and tighten its mode to 0600 since it stores the WPA PSK. Restrict WebSocket upgrades to same-origin so a LAN browser can't be turned into a proxy for the API. Guard shared state: status reads/writes go through StatusMutex (the periodic updater races with the toggle and status handlers otherwise), broadcastToWebSockets no longer mutates the client map under RLock, and station-event callbacks now run under SafeGo so a panic in app code can't take down the daemon. Stop channels in hostapd, dhcp, and iwd signal monitors are now closed under sync.Once to survive concurrent Stop calls. App.Shutdown is idempotent and waits for the periodic loops before closing backends, so signal-driven and deferred shutdowns no longer race. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
77370eff19
commit
07f8673f2f
14 changed files with 237 additions and 85 deletions
|
|
@ -1,10 +1,27 @@
|
|||
package backend
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SafeGo runs fn in a goroutine and recovers from panics so a misbehaving
|
||||
// callback (registered by app code) cannot bring down the daemon. Backends
|
||||
// dispatch event callbacks asynchronously; without recovery, a single
|
||||
// panic in user code crashes the whole process.
|
||||
func SafeGo(name string, fn func()) {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("panic in %s: %v\n%s", name, r, debug.Stack())
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
}()
|
||||
}
|
||||
|
||||
// StationBackend defines the interface for station/device discovery backends.
|
||||
// Implementations include ARP-based, DHCP-based, and Hostapd DBus-based discovery.
|
||||
type StationBackend interface {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ type Backend struct {
|
|||
lastStations map[string]backend.Station // Key: MAC address
|
||||
callbacks backend.EventCallbacks
|
||||
stopChan chan struct{}
|
||||
stopOnce sync.Once
|
||||
mu sync.RWMutex
|
||||
running bool
|
||||
}
|
||||
|
|
@ -101,7 +102,7 @@ func (b *Backend) StartEventMonitoring(callbacks backend.EventCallbacks) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// StopEventMonitoring stops event monitoring
|
||||
// StopEventMonitoring stops event monitoring. Idempotent — see hostapd backend.
|
||||
func (b *Backend) StopEventMonitoring() {
|
||||
b.mu.Lock()
|
||||
if !b.running {
|
||||
|
|
@ -111,7 +112,7 @@ func (b *Backend) StopEventMonitoring() {
|
|||
b.running = false
|
||||
b.mu.Unlock()
|
||||
|
||||
close(b.stopChan)
|
||||
b.stopOnce.Do(func() { close(b.stopChan) })
|
||||
}
|
||||
|
||||
// SupportsRealTimeEvents returns false (DHCP is polling-based)
|
||||
|
|
@ -155,15 +156,17 @@ func (b *Backend) checkForChanges() {
|
|||
for mac, st := range currentMap {
|
||||
if _, exists := b.lastStations[mac]; !exists {
|
||||
// New station connected
|
||||
if b.callbacks.OnStationConnected != nil {
|
||||
go b.callbacks.OnStationConnected(st)
|
||||
if cb := b.callbacks.OnStationConnected; cb != nil {
|
||||
stCopy := st
|
||||
backend.SafeGo("OnStationConnected", func() { cb(stCopy) })
|
||||
}
|
||||
} else {
|
||||
// Check for updates (IP change, hostname change, etc.)
|
||||
oldStation := b.lastStations[mac]
|
||||
if oldStation.IP != st.IP || oldStation.Hostname != st.Hostname {
|
||||
if b.callbacks.OnStationUpdated != nil {
|
||||
go b.callbacks.OnStationUpdated(st)
|
||||
if cb := b.callbacks.OnStationUpdated; cb != nil {
|
||||
stCopy := st
|
||||
backend.SafeGo("OnStationUpdated", func() { cb(stCopy) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -173,8 +176,9 @@ func (b *Backend) checkForChanges() {
|
|||
for mac := range b.lastStations {
|
||||
if _, exists := currentMap[mac]; !exists {
|
||||
// Station disconnected
|
||||
if b.callbacks.OnStationDisconnected != nil {
|
||||
go b.callbacks.OnStationDisconnected(mac)
|
||||
if cb := b.callbacks.OnStationDisconnected; cb != nil {
|
||||
macCopy := mac
|
||||
backend.SafeGo("OnStationDisconnected", func() { cb(macCopy) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,19 +25,25 @@ func parseDHCPLeases(path string) ([]models.DHCPLease, error) {
|
|||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
fields := strings.Fields(line)
|
||||
|
||||
if strings.HasPrefix(line, "lease ") {
|
||||
ip := strings.Fields(line)[1]
|
||||
currentLease = models.DHCPLease{IP: ip}
|
||||
} else if strings.Contains(line, "hardware ethernet") {
|
||||
mac := strings.Fields(line)[2]
|
||||
mac = strings.TrimSuffix(mac, ";")
|
||||
currentLease.MAC = mac
|
||||
} else if strings.Contains(line, "client-hostname") {
|
||||
hostname := strings.Fields(line)[1]
|
||||
hostname = strings.Trim(hostname, `";`)
|
||||
currentLease.Hostname = hostname
|
||||
} else if line == "}" {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "lease "):
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
currentLease = models.DHCPLease{IP: fields[1]}
|
||||
case strings.Contains(line, "hardware ethernet"):
|
||||
if len(fields) < 3 {
|
||||
continue
|
||||
}
|
||||
currentLease.MAC = strings.TrimSuffix(fields[2], ";")
|
||||
case strings.Contains(line, "client-hostname"):
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
currentLease.Hostname = strings.Trim(fields[1], `";`)
|
||||
case line == "}":
|
||||
if currentLease.IP != "" && currentLease.MAC != "" {
|
||||
leases = append(leases, currentLease)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ type Backend struct {
|
|||
stations map[string]*HostapdStation // Key: MAC address
|
||||
callbacks backend.EventCallbacks
|
||||
|
||||
mu sync.RWMutex
|
||||
running bool
|
||||
stopCh chan struct{}
|
||||
mu sync.RWMutex
|
||||
running bool
|
||||
stopCh chan struct{}
|
||||
stopOnce sync.Once
|
||||
|
||||
// IP correlation - will be populated by periodic DHCP lease correlation
|
||||
ipByMAC map[string]string // MAC -> IP mapping
|
||||
|
|
@ -129,7 +130,9 @@ func (b *Backend) StartEventMonitoring(callbacks backend.EventCallbacks) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// StopEventMonitoring stops event monitoring
|
||||
// StopEventMonitoring stops event monitoring. Safe to call multiple times —
|
||||
// without sync.Once, racing callers could each pass the running guard before
|
||||
// either closed the channel, panicking on the second close.
|
||||
func (b *Backend) StopEventMonitoring() {
|
||||
b.mu.Lock()
|
||||
if !b.running {
|
||||
|
|
@ -139,7 +142,7 @@ func (b *Backend) StopEventMonitoring() {
|
|||
b.running = false
|
||||
b.mu.Unlock()
|
||||
|
||||
close(b.stopCh)
|
||||
b.stopOnce.Do(func() { close(b.stopCh) })
|
||||
}
|
||||
|
||||
// SupportsRealTimeEvents returns false (hostapd_cli uses polling)
|
||||
|
|
@ -186,9 +189,9 @@ func (b *Backend) checkStationChanges() error {
|
|||
if _, exists := b.stations[mac]; !exists {
|
||||
// New station connected
|
||||
b.stations[mac] = station
|
||||
if b.callbacks.OnStationConnected != nil {
|
||||
if cb := b.callbacks.OnStationConnected; cb != nil {
|
||||
st := b.convertStation(mac, station)
|
||||
go b.callbacks.OnStationConnected(st)
|
||||
backend.SafeGo("OnStationConnected", func() { cb(st) })
|
||||
}
|
||||
log.Printf("Station connected: %s", mac)
|
||||
}
|
||||
|
|
@ -200,8 +203,9 @@ func (b *Backend) checkStationChanges() error {
|
|||
// Station disconnected
|
||||
delete(b.stations, mac)
|
||||
delete(b.ipByMAC, mac)
|
||||
if b.callbacks.OnStationDisconnected != nil {
|
||||
go b.callbacks.OnStationDisconnected(mac)
|
||||
if cb := b.callbacks.OnStationDisconnected; cb != nil {
|
||||
macCopy := mac
|
||||
backend.SafeGo("OnStationDisconnected", func() { cb(macCopy) })
|
||||
}
|
||||
log.Printf("Station disconnected: %s", mac)
|
||||
}
|
||||
|
|
@ -336,9 +340,9 @@ func (b *Backend) UpdateIPMapping(macToIP map[string]string) {
|
|||
// Trigger update callbacks for stations that got new/changed IPs
|
||||
for mac := range updated {
|
||||
if station, exists := b.stations[mac]; exists {
|
||||
if b.callbacks.OnStationUpdated != nil {
|
||||
if cb := b.callbacks.OnStationUpdated; cb != nil {
|
||||
st := b.convertStation(mac, station)
|
||||
go b.callbacks.OnStationUpdated(st)
|
||||
backend.SafeGo("OnStationUpdated", func() { cb(st) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue