wpa_supplicant: Reuse existing networks and preserve saved ones on connect

Connect() called AddNetwork unconditionally, creating duplicate entries for
the same SSID, and SelectNetwork's side-effect of disabling all other
networks was being persisted by SaveConfig — making previously saved
networks appear erased. Disconnect() also removed the current network from
the config. Now reuse an existing network entry when the SSID matches,
re-enable other networks after SelectNetwork, and keep entries on
disconnect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nemunaire 2026-05-01 18:25:30 +08:00
commit 77370eff19
3 changed files with 102 additions and 23 deletions

View file

@ -152,35 +152,69 @@ func (b *WPABackend) IsScanning() (bool, error) {
// Connect connects to a WiFi network // Connect connects to a WiFi network
func (b *WPABackend) Connect(ssid, password string) error { func (b *WPABackend) Connect(ssid, password string) error {
// Create network configuration // Look up existing network with the same SSID to avoid creating duplicates
config := make(map[string]interface{}) existingNetworks, err := b.iface.GetNetworks()
config["ssid"] = ssid // Raw SSID string, no quotes
if password != "" {
// For WPA/WPA2-PSK networks
config["psk"] = password // Raw password string, no quotes
} else {
// For open networks
config["key_mgmt"] = "NONE"
}
// Add network
networkPath, err := b.iface.AddNetwork(config)
if err != nil { if err != nil {
return fmt.Errorf("failed to add network: %v", err) return fmt.Errorf("failed to list configured networks: %v", err)
} }
// Store current network path for cleanup var networkPath dbus.ObjectPath
createdNew := false
for _, p := range existingNetworks {
net := NewNetwork(b.conn, p)
netSSID, err := net.GetSSID()
if err != nil || netSSID != ssid {
continue
}
networkPath = p
break
}
if networkPath == "" {
// Create network configuration
config := make(map[string]interface{})
config["ssid"] = ssid // Raw SSID string, no quotes
if password != "" {
// For WPA/WPA2-PSK networks
config["psk"] = password // Raw password string, no quotes
} else {
// For open networks
config["key_mgmt"] = "NONE"
}
networkPath, err = b.iface.AddNetwork(config)
if err != nil {
return fmt.Errorf("failed to add network: %v", err)
}
createdNew = true
}
// Store current network path
b.currentNetwork = networkPath b.currentNetwork = networkPath
// Select (connect to) the network // Select (connect to) the network. Note: SelectNetwork disables every
// other configured network as a side-effect.
err = b.iface.SelectNetwork(networkPath) err = b.iface.SelectNetwork(networkPath)
if err != nil { if err != nil {
// Clean up network on failure if createdNew {
b.iface.RemoveNetwork(networkPath) b.iface.RemoveNetwork(networkPath)
}
return fmt.Errorf("failed to select network: %v", err) return fmt.Errorf("failed to select network: %v", err)
} }
// Re-enable previously configured networks so SelectNetwork's side-effect
// doesn't mark them all as disabled in the persisted config.
for _, p := range existingNetworks {
if p == networkPath {
continue
}
if err := b.iface.EnableNetwork(p); err != nil {
fmt.Printf("Warning: failed to re-enable network %s: %v\n", p, err)
}
}
// Save the configuration to persist it across reboots // Save the configuration to persist it across reboots
if err := b.iface.SaveConfig(); err != nil { if err := b.iface.SaveConfig(); err != nil {
// Log warning but don't fail - connection still works // Log warning but don't fail - connection still works
@ -197,11 +231,7 @@ func (b *WPABackend) Disconnect() error {
return fmt.Errorf("failed to disconnect: %v", err) return fmt.Errorf("failed to disconnect: %v", err)
} }
// Remove the network configuration if we have one b.currentNetwork = ""
if b.currentNetwork != "" && b.currentNetwork != "/" {
b.iface.RemoveNetwork(b.currentNetwork)
b.currentNetwork = ""
}
return nil return nil
} }

View file

@ -107,6 +107,16 @@ func (i *WPAInterface) SelectNetwork(networkPath dbus.ObjectPath) error {
return nil return nil
} }
// EnableNetwork marks a network configuration as enabled (eligible for auto-connect)
func (i *WPAInterface) EnableNetwork(networkPath dbus.ObjectPath) error {
netObj := i.conn.Object(Service, networkPath)
err := netObj.SetProperty(NetworkInterface+".Enabled", dbus.MakeVariant(true))
if err != nil {
return fmt.Errorf("failed to enable network: %v", err)
}
return nil
}
// RemoveNetwork removes a network configuration // RemoveNetwork removes a network configuration
func (i *WPAInterface) RemoveNetwork(networkPath dbus.ObjectPath) error { func (i *WPAInterface) RemoveNetwork(networkPath dbus.ObjectPath) error {
err := i.obj.Call(InterfaceInterface+".RemoveNetwork", 0, networkPath).Err err := i.obj.Call(InterfaceInterface+".RemoveNetwork", 0, networkPath).Err
@ -116,6 +126,21 @@ func (i *WPAInterface) RemoveNetwork(networkPath dbus.ObjectPath) error {
return nil return nil
} }
// GetNetworks returns the object paths of all configured networks
func (i *WPAInterface) GetNetworks() ([]dbus.ObjectPath, error) {
prop, err := i.obj.GetProperty(InterfaceInterface + ".Networks")
if err != nil {
return nil, fmt.Errorf("failed to get Networks property: %v", err)
}
networks, ok := prop.Value().([]dbus.ObjectPath)
if !ok {
return nil, fmt.Errorf("Networks property is not an array of ObjectPath")
}
return networks, nil
}
// Disconnect disconnects from the current network // Disconnect disconnects from the current network
func (i *WPAInterface) Disconnect() error { func (i *WPAInterface) Disconnect() error {
err := i.obj.Call(InterfaceInterface+".Disconnect", 0).Err err := i.obj.Call(InterfaceInterface+".Disconnect", 0).Err

View file

@ -1,6 +1,8 @@
package wpasupplicant package wpasupplicant
import ( import (
"strings"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@ -39,3 +41,25 @@ func (n *Network) GetProperties() (map[string]dbus.Variant, error) {
return props, nil return props, nil
} }
// GetSSID returns the configured SSID, stripping the wrapping quotes
// that wpa_supplicant stores around string-form SSIDs.
func (n *Network) GetSSID() (string, error) {
props, err := n.GetProperties()
if err != nil {
return "", err
}
ssidVariant, ok := props["ssid"]
if !ok {
return "", nil
}
ssid, ok := ssidVariant.Value().(string)
if !ok {
return "", nil
}
// wpa_supplicant returns quoted SSIDs like "MyNetwork"
return strings.Trim(ssid, `"`), nil
}