repeater/internal/wifi/iwd/signals.go
Pierre-Olivier Mercier 07f8673f2f 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>
2026-05-01 21:56:50 +08:00

224 lines
4.9 KiB
Go

package iwd
import (
"log"
"sync"
"github.com/godbus/dbus/v5"
)
// SignalMonitor monitors D-Bus signals from iwd
type SignalMonitor struct {
conn *dbus.Conn
station *Station
// Signal channel
signalChan chan *dbus.Signal
// Callbacks
onStateChange func(state StationState, ssid string)
onScanComplete func()
// Control
stopChan chan struct{}
stopOnce sync.Once
mu sync.RWMutex
running bool
// State tracking
lastScanning bool
}
// NewSignalMonitor creates a new signal monitor
func NewSignalMonitor(conn *dbus.Conn, station *Station) *SignalMonitor {
return &SignalMonitor{
conn: conn,
station: station,
signalChan: make(chan *dbus.Signal, 100),
stopChan: make(chan struct{}),
}
}
// OnStateChange registers a callback for state changes
func (sm *SignalMonitor) OnStateChange(callback func(state StationState, ssid string)) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.onStateChange = callback
}
// OnScanComplete registers a callback for scan completion
func (sm *SignalMonitor) OnScanComplete(callback func()) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.onScanComplete = callback
}
// Start begins monitoring D-Bus signals
func (sm *SignalMonitor) Start() error {
sm.mu.Lock()
if sm.running {
sm.mu.Unlock()
return nil
}
sm.running = true
sm.mu.Unlock()
// Subscribe to PropertiesChanged signals for Station interface
stationPath := sm.station.GetPath()
// Add signal match for PropertiesChanged on Station interface
matchOptions := []dbus.MatchOption{
dbus.WithMatchObjectPath(stationPath),
dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
dbus.WithMatchMember("PropertiesChanged"),
}
if err := sm.conn.AddMatchSignal(matchOptions...); err != nil {
sm.mu.Lock()
sm.running = false
sm.mu.Unlock()
return err
}
// Register signal channel
sm.conn.Signal(sm.signalChan)
// Get initial scanning state
scanning, err := sm.station.IsScanning()
if err == nil {
sm.lastScanning = scanning
}
// Start monitoring goroutine
go sm.monitor()
log.Printf("D-Bus signal monitoring started for station %s", stationPath)
return nil
}
// Stop stops monitoring D-Bus signals. Idempotent: stopOnce guards the
// channel close so concurrent Stop() callers do not panic on double-close.
func (sm *SignalMonitor) Stop() {
sm.mu.Lock()
if !sm.running {
sm.mu.Unlock()
return
}
sm.running = false
sm.mu.Unlock()
sm.stopOnce.Do(func() { close(sm.stopChan) })
// Remove signal channel
sm.conn.RemoveSignal(sm.signalChan)
log.Printf("D-Bus signal monitoring stopped")
}
// monitor is the main signal processing loop
func (sm *SignalMonitor) monitor() {
for {
select {
case sig := <-sm.signalChan:
sm.handleSignal(sig)
case <-sm.stopChan:
return
}
}
}
// handleSignal processes a D-Bus signal
func (sm *SignalMonitor) handleSignal(sig *dbus.Signal) {
// Only process PropertiesChanged signals
if sig.Name != "org.freedesktop.DBus.Properties.PropertiesChanged" {
return
}
// Verify signal is from Station interface
if len(sig.Body) < 2 {
return
}
interfaceName, ok := sig.Body[0].(string)
if !ok || interfaceName != StationInterface {
return
}
// Parse changed properties
changedProps, ok := sig.Body[1].(map[string]dbus.Variant)
if !ok {
return
}
// Check for State property change
if stateVariant, ok := changedProps["State"]; ok {
if state, ok := stateVariant.Value().(string); ok {
sm.handleStateChange(StationState(state))
}
}
// Check for Scanning property change
if scanningVariant, ok := changedProps["Scanning"]; ok {
if scanning, ok := scanningVariant.Value().(bool); ok {
sm.handleScanningChange(scanning)
}
}
// Check for ConnectedNetwork property change
if _, ok := changedProps["ConnectedNetwork"]; ok {
// Network connection changed, trigger state update
sm.handleConnectionChange()
}
}
// handleStateChange processes a state change
func (sm *SignalMonitor) handleStateChange(state StationState) {
sm.mu.RLock()
callback := sm.onStateChange
sm.mu.RUnlock()
if callback == nil {
return
}
// Get connected SSID if connected
ssid := ""
if state == StateConnected {
network, err := sm.station.GetConnectedNetwork()
if err == nil {
props, err := network.GetProperties()
if err == nil {
ssid = props.Name
}
}
}
callback(state, ssid)
}
// handleScanningChange processes scanning state changes
func (sm *SignalMonitor) handleScanningChange(scanning bool) {
// Detect scan completion (transition from true to false)
if sm.lastScanning && !scanning {
sm.mu.RLock()
callback := sm.onScanComplete
sm.mu.RUnlock()
if callback != nil {
callback()
}
}
sm.lastScanning = scanning
}
// handleConnectionChange processes connection changes
func (sm *SignalMonitor) handleConnectionChange() {
// Get current state and trigger state change callback
state, err := sm.station.GetState()
if err != nil {
return
}
sm.handleStateChange(state)
}