diff --git a/internal/wifi/wpasupplicant/backend.go b/internal/wifi/wpasupplicant/backend.go index 5383f88..6639a65 100644 --- a/internal/wifi/wpasupplicant/backend.go +++ b/internal/wifi/wpasupplicant/backend.go @@ -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 } diff --git a/internal/wifi/wpasupplicant/interface.go b/internal/wifi/wpasupplicant/interface.go index a33cfa7..e27d1a0 100644 --- a/internal/wifi/wpasupplicant/interface.go +++ b/internal/wifi/wpasupplicant/interface.go @@ -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 diff --git a/internal/wifi/wpasupplicant/network.go b/internal/wifi/wpasupplicant/network.go index 3152a6b..c018e3f 100644 --- a/internal/wifi/wpasupplicant/network.go +++ b/internal/wifi/wpasupplicant/network.go @@ -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 +}