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 ff50b492b1)
This commit is contained in:
Pedro Jose Soto Rivera
2026-10-07 12:58:37 -04:00
committed by bbedward
parent 58a356bb05
commit bc4916a132
3 changed files with 166 additions and 28 deletions
+42 -17
View File
@@ -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()
@@ -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
}
}
}
@@ -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")
}