From bc4916a13286c427bf215ebe204e39b44fff3703 Mon Sep 17 00:00:00 2001 From: Pedro Jose Soto Rivera <159482355+Pedroj-64@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:23:01 -0500 Subject: [PATCH] network(iwd): follow the Wi-Fi device when iwd recreates it (#3707) When iwd restarts it recreates wlan0 under a new D-Bus path. Only the Station interface was tracked on InterfacesAdded, so devicePath kept pointing at the dead object: the PropertiesChanged match for the new device was never registered, updateState() read Powered from the old path, and SetWiFiEnabled() failed with "No matching method found". WiFiEnabled stayed false after handleStationRemoved() until DMS was restarted. Handle the Device interface on InterfacesAdded/InterfacesRemoved: move devicePath and its signal match to the new object and take Name and Powered from the signal. Process every interface in an InterfacesRemoved signal instead of stopping at the first match. Refs #3495 (cherry picked from commit ff50b492b102116b5ea0355d2abf4f1385c694b1) --- core/internal/server/network/backend_iwd.go | 59 +++++++++++----- .../server/network/backend_iwd_signals.go | 67 +++++++++++++++--- .../server/network/backend_iwd_test.go | 68 +++++++++++++++++++ 3 files changed, 166 insertions(+), 28 deletions(-) diff --git a/core/internal/server/network/backend_iwd.go b/core/internal/server/network/backend_iwd.go index a9b14c002..002e73c88 100644 --- a/core/internal/server/network/backend_iwd.go +++ b/core/internal/server/network/backend_iwd.go @@ -154,23 +154,7 @@ func (b *IWDBackend) applyManagedObjects(objects map[dbus.ObjectPath]map[string] } if _, hasDevice := interfaces[iwdDeviceInterface]; hasDevice { b.devicePath = path - - if devProps, ok := interfaces[iwdDeviceInterface]; ok { - if nameVar, ok := devProps["Name"]; ok { - if name, ok := nameVar.Value().(string); ok { - b.stateMutex.Lock() - b.state.WiFiDevice = name - b.stateMutex.Unlock() - } - } - if poweredVar, ok := devProps["Powered"]; ok { - if powered, ok := poweredVar.Value().(bool); ok { - b.stateMutex.Lock() - b.state.WiFiEnabled = powered - b.stateMutex.Unlock() - } - } - } + b.applyDeviceProps(interfaces[iwdDeviceInterface]) } if _, hasAdapter := interfaces[iwdAdapterInterface]; hasAdapter { b.adapterPath = path @@ -193,6 +177,47 @@ func (b *IWDBackend) applyManagedObjects(objects map[dbus.ObjectPath]map[string] return nil } +func (b *IWDBackend) applyDeviceProps(props map[string]dbus.Variant) { + if nameVar, ok := props["Name"]; ok { + if name, ok := nameVar.Value().(string); ok { + b.stateMutex.Lock() + b.state.WiFiDevice = name + b.stateMutex.Unlock() + } + } + if poweredVar, ok := props["Powered"]; ok { + if powered, ok := poweredVar.Value().(bool); ok { + b.stateMutex.Lock() + b.state.WiFiEnabled = powered + b.stateMutex.Unlock() + } + } +} + +// setDevice records a device that appeared at a new path, which is what +// happens when iwd restarts and recreates wlan0. It returns the path it +// replaced so the caller can move the signal match over. +func (b *IWDBackend) setDevice(path dbus.ObjectPath, props map[string]dbus.Variant) (prev dbus.ObjectPath, changed bool) { + if path == "" || path == b.devicePath { + return "", false + } + prev = b.devicePath + b.devicePath = path + b.applyDeviceProps(props) + return prev, true +} + +func (b *IWDBackend) clearDevice(path dbus.ObjectPath) bool { + if path == "" || path != b.devicePath { + return false + } + b.devicePath = "" + b.stateMutex.Lock() + b.state.WiFiEnabled = false + b.stateMutex.Unlock() + return true +} + func (b *IWDBackend) GetCurrentState() (*BackendState, error) { b.stateMutex.RLock() defer b.stateMutex.RUnlock() diff --git a/core/internal/server/network/backend_iwd_signals.go b/core/internal/server/network/backend_iwd_signals.go index e4541f5f2..60ea87625 100644 --- a/core/internal/server/network/backend_iwd_signals.go +++ b/core/internal/server/network/backend_iwd_signals.go @@ -26,12 +26,7 @@ func (b *IWDBackend) StartMonitoring(onStateChange func()) error { b.conn.Signal(sigChan) if b.devicePath != "" { - err := b.conn.AddMatchSignal( - dbus.WithMatchObjectPath(b.devicePath), - dbus.WithMatchInterface(dbusPropertiesInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - if err != nil { + if err := b.addDeviceSignalMatch(b.devicePath); err != nil { return fmt.Errorf("failed to add device signal match: %w", err) } } @@ -86,6 +81,48 @@ func (b *IWDBackend) addStationSignalMatch(path dbus.ObjectPath) error { ) } +func (b *IWDBackend) addDeviceSignalMatch(path dbus.ObjectPath) error { + return b.conn.AddMatchSignal( + dbus.WithMatchObjectPath(path), + dbus.WithMatchInterface(dbusPropertiesInterface), + dbus.WithMatchMember("PropertiesChanged"), + ) +} + +func (b *IWDBackend) removeDeviceSignalMatch(path dbus.ObjectPath) { + err := b.conn.RemoveMatchSignal( + dbus.WithMatchObjectPath(path), + dbus.WithMatchInterface(dbusPropertiesInterface), + dbus.WithMatchMember("PropertiesChanged"), + ) + if err != nil { + log.Debugf("Failed to remove iwd device signal match for %s: %v", path, err) + } +} + +func (b *IWDBackend) handleDeviceAdded(path dbus.ObjectPath, props map[string]dbus.Variant) bool { + prev, changed := b.setDevice(path, props) + if !changed { + return false + } + + if prev != "" { + b.removeDeviceSignalMatch(prev) + } + if err := b.addDeviceSignalMatch(path); err != nil { + log.Warnf("Failed to add iwd device signal match for %s: %v", path, err) + } + return true +} + +func (b *IWDBackend) handleDeviceRemoved(path dbus.ObjectPath) bool { + if !b.clearDevice(path) { + return false + } + b.removeDeviceSignalMatch(path) + return true +} + func (b *IWDBackend) handleStationAdded(path dbus.ObjectPath) bool { if path == "" || path == b.stationPath { return false @@ -139,6 +176,12 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) { if len(sig.Body) >= 2 { path, _ := sig.Body[0].(dbus.ObjectPath) if interfaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant); ok { + // Device before Station: handleStationAdded reads Powered from devicePath. + if props, ok := interfaces[iwdDeviceInterface]; ok { + if b.handleDeviceAdded(path, props) && b.onStateChange != nil { + b.onStateChange() + } + } if _, ok := interfaces[iwdStationInterface]; ok { if b.handleStationAdded(path) && b.onStateChange != nil { b.onStateChange() @@ -159,17 +202,19 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) { path, _ := sig.Body[0].(dbus.ObjectPath) if interfaces, ok := sig.Body[1].([]string); ok { for _, iface := range interfaces { - if iface == iwdStationInterface { + switch iface { + case iwdDeviceInterface: + if b.handleDeviceRemoved(path) && b.onStateChange != nil { + b.onStateChange() + } + case iwdStationInterface: if b.handleStationRemoved(path) && b.onStateChange != nil { b.onStateChange() } - break - } - if iface == iwdKnownNetworkInterface { + case iwdKnownNetworkInterface: if b.refreshWiFiNetworkState() && b.onStateChange != nil { b.onStateChange() } - break } } } diff --git a/core/internal/server/network/backend_iwd_test.go b/core/internal/server/network/backend_iwd_test.go index 1d0102d5d..2a1d883a6 100644 --- a/core/internal/server/network/backend_iwd_test.go +++ b/core/internal/server/network/backend_iwd_test.go @@ -460,3 +460,71 @@ func TestConnectAttempt_DoubleFinalization(t *testing.T) { assert.Equal(t, "bad-credentials", backend.state.LastError) backend.stateMutex.RUnlock() } + +func TestIWDBackend_SetDevice_FollowsRecreatedDevice(t *testing.T) { + backend, _ := NewIWDBackend() + err := backend.applyManagedObjects(map[dbus.ObjectPath]map[string]map[string]dbus.Variant{ + "/net/connman/iwd/0/3": { + iwdDeviceInterface: {"Name": dbus.MakeVariant("wlan0"), "Powered": dbus.MakeVariant(true)}, + iwdStationInterface: {}, + }, + }) + assert.NoError(t, err) + + // iwd restarts: the old objects go away and wlan0 comes back with a new ifindex. + assert.True(t, backend.handleStationRemoved("/net/connman/iwd/0/3")) + assert.True(t, backend.clearDevice("/net/connman/iwd/0/3")) + + prev, changed := backend.setDevice("/net/connman/iwd/0/7", map[string]dbus.Variant{ + "Name": dbus.MakeVariant("wlan0"), + "Powered": dbus.MakeVariant(true), + }) + + assert.True(t, changed) + assert.Empty(t, prev) + assert.Equal(t, dbus.ObjectPath("/net/connman/iwd/0/7"), backend.devicePath) + + enabled, _ := backend.GetWiFiEnabled() + assert.True(t, enabled, "Wi-Fi must be enabled again once the recreated device reports Powered") +} + +func TestIWDBackend_SetDevice_ReplacesPathWithoutRemovalSignal(t *testing.T) { + backend, _ := NewIWDBackend() + backend.devicePath = "/net/connman/iwd/0/3" + + prev, changed := backend.setDevice("/net/connman/iwd/0/7", map[string]dbus.Variant{ + "Powered": dbus.MakeVariant(true), + }) + + assert.True(t, changed) + assert.Equal(t, dbus.ObjectPath("/net/connman/iwd/0/3"), prev) + assert.Equal(t, dbus.ObjectPath("/net/connman/iwd/0/7"), backend.devicePath) +} + +func TestIWDBackend_SetDevice_SamePathIsNoop(t *testing.T) { + backend, _ := NewIWDBackend() + backend.devicePath = "/net/connman/iwd/0/3" + + _, changed := backend.setDevice("/net/connman/iwd/0/3", map[string]dbus.Variant{ + "Powered": dbus.MakeVariant(false), + }) + assert.False(t, changed) + + _, changed = backend.setDevice("", nil) + assert.False(t, changed) +} + +func TestIWDBackend_ClearDevice(t *testing.T) { + backend, _ := NewIWDBackend() + backend.devicePath = "/net/connman/iwd/0/3" + + assert.False(t, backend.clearDevice("/net/connman/iwd/0/9"), "unrelated path must be ignored") + assert.Equal(t, dbus.ObjectPath("/net/connman/iwd/0/3"), backend.devicePath) + + assert.True(t, backend.clearDevice("/net/connman/iwd/0/3")) + assert.Empty(t, backend.devicePath) + + enabled, _ := backend.GetWiFiEnabled() + assert.False(t, enabled) + assert.ErrorContains(t, backend.SetWiFiEnabled(true), "no WiFi device available") +}