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
func (b *WPABackend) Connect(ssid, password string) error {
// 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"
}
// Add network
networkPath, err := b.iface.AddNetwork(config)
// Look up existing network with the same SSID to avoid creating duplicates
existingNetworks, err := b.iface.GetNetworks()
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
// 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)
if err != nil {
// Clean up network on failure
b.iface.RemoveNetwork(networkPath)
if createdNew {
b.iface.RemoveNetwork(networkPath)
}
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
if err := b.iface.SaveConfig(); err != nil {
// 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)
}
// Remove the network configuration if we have one
if b.currentNetwork != "" && b.currentNetwork != "/" {
b.iface.RemoveNetwork(b.currentNetwork)
b.currentNetwork = ""
}
b.currentNetwork = ""
return nil
}

View file

@ -107,6 +107,16 @@ func (i *WPAInterface) SelectNetwork(networkPath dbus.ObjectPath) error {
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
func (i *WPAInterface) RemoveNetwork(networkPath dbus.ObjectPath) error {
err := i.obj.Call(InterfaceInterface+".RemoveNetwork", 0, networkPath).Err
@ -116,6 +126,21 @@ func (i *WPAInterface) RemoveNetwork(networkPath dbus.ObjectPath) error {
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
func (i *WPAInterface) Disconnect() error {
err := i.obj.Call(InterfaceInterface+".Disconnect", 0).Err

View file

@ -1,6 +1,8 @@
package wpasupplicant
import (
"strings"
"github.com/godbus/dbus/v5"
)
@ -39,3 +41,25 @@ func (n *Network) GetProperties() (map[string]dbus.Variant, error) {
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
}