repeater/internal/wifi/wpasupplicant/backend.go
Pierre-Olivier Mercier 0797f7dd50 wpa_supplicant: Use log package and tolerate GetNetworks failures
Switch warning prints to the log package for consistent output, and
fall back to AddNetwork when listing existing networks fails instead
of aborting Connect entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:14:38 +08:00

308 lines
7.8 KiB
Go

package wpasupplicant
import (
"fmt"
"log"
"time"
"github.com/godbus/dbus/v5"
"github.com/nemunaire/repeater/internal/wifi/backend"
)
// WPABackend implements the WiFiBackend interface for wpa_supplicant
type WPABackend struct {
conn *dbus.Conn
wpasupplicant dbus.BusObject
iface *WPAInterface
signalMonitor *SignalMonitor
interfaceName string
currentNetwork dbus.ObjectPath
}
// NewWPABackend creates a new wpa_supplicant backend instance
func NewWPABackend() *WPABackend {
return &WPABackend{}
}
// Initialize initializes the wpa_supplicant backend with the given interface name
func (b *WPABackend) Initialize(interfaceName string) error {
b.interfaceName = interfaceName
var err error
// Connect to D-Bus
b.conn, err = dbus.SystemBus()
if err != nil {
return fmt.Errorf("failed to connect to D-Bus: %v", err)
}
// Get wpa_supplicant root object
b.wpasupplicant = b.conn.Object(Service, dbus.ObjectPath(RootPath))
// Get interface path for the given interface name
interfacePath, err := b.getInterfacePath(interfaceName)
if err != nil {
return fmt.Errorf("failed to get interface for %s: %v", interfaceName, err)
}
b.iface = NewWPAInterface(b.conn, interfacePath)
return nil
}
// getInterfacePath gets or creates the wpa_supplicant Interface object path
func (b *WPABackend) getInterfacePath(interfaceName string) (dbus.ObjectPath, error) {
var interfacePath dbus.ObjectPath
// Try to get existing interface
err := b.wpasupplicant.Call(Service+".GetInterface", 0, interfaceName).Store(&interfacePath)
if err == nil {
return interfacePath, nil
}
// Interface doesn't exist, create it
args := map[string]dbus.Variant{
"Ifname": dbus.MakeVariant(interfaceName),
}
err = b.wpasupplicant.Call(Service+".CreateInterface", 0, args).Store(&interfacePath)
if err != nil {
return "", fmt.Errorf("failed to create interface: %v", err)
}
return interfacePath, nil
}
// Close closes the D-Bus connection
func (b *WPABackend) Close() error {
if b.conn != nil {
b.conn.Close()
}
return nil
}
// ScanNetworks triggers a network scan
func (b *WPABackend) ScanNetworks() error {
err := b.iface.Scan("active")
if err != nil {
return fmt.Errorf("failed to trigger scan: %v", err)
}
return nil
}
// GetOrderedNetworks returns networks sorted by signal strength in backend-agnostic format
func (b *WPABackend) GetOrderedNetworks() ([]backend.BackendNetwork, error) {
// Get BSS list
bssPaths, err := b.iface.GetBSSs()
if err != nil {
return nil, fmt.Errorf("failed to get BSSs: %v", err)
}
var networks []backend.BackendNetwork
seenSSIDs := make(map[string]bool)
// Iterate through BSSs and collect network info
for _, bssPath := range bssPaths {
bss := NewBSS(b.conn, bssPath)
props, err := bss.GetProperties()
if err != nil {
continue
}
ssid := string(props.SSID)
if ssid == "" || seenSSIDs[ssid] {
continue
}
seenSSIDs[ssid] = true
// Get BSSID string
bssidStr, err := bss.GetBSSIDString()
if err != nil {
bssidStr = ""
}
// Convert to backend-agnostic format
backendNet := backend.BackendNetwork{
SSID: ssid,
SignalDBm: props.Signal,
SecurityType: props.DetermineSecurityType(),
BSSID: bssidStr,
Frequency: props.Frequency,
}
networks = append(networks, backendNet)
}
// Sort by signal strength (descending)
// Note: This is a simple bubble sort for demonstration
// In production, use sort.Slice
for i := 0; i < len(networks); i++ {
for j := i + 1; j < len(networks); j++ {
if networks[j].SignalDBm > networks[i].SignalDBm {
networks[i], networks[j] = networks[j], networks[i]
}
}
}
return networks, nil
}
// IsScanning checks if a scan is currently in progress
func (b *WPABackend) IsScanning() (bool, error) {
return b.iface.GetScanning()
}
// Connect connects to a WiFi network
func (b *WPABackend) Connect(ssid, password string) error {
// Best-effort lookup of existing networks, used both to avoid creating
// duplicate entries for the same SSID and to re-enable other entries
// after SelectNetwork's "disable all others" side-effect. If listing
// fails, fall back to plain AddNetwork — saving still works.
existingNetworks, err := b.iface.GetNetworks()
if err != nil {
log.Printf("wpa_supplicant: failed to list configured networks: %v", err)
existingNetworks = nil
}
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. Note: SelectNetwork disables every
// other configured network as a side-effect.
if err := b.iface.SelectNetwork(networkPath); err != nil {
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 {
log.Printf("wpa_supplicant: failed to re-enable network %s: %v", p, err)
}
}
// Save the configuration to persist it across reboots. Requires
// update_config=1 in the wpa_supplicant.conf wpa_supplicant was started
// with, and that file being writable by the wpa_supplicant process.
if err := b.iface.SaveConfig(); err != nil {
log.Printf("wpa_supplicant: SaveConfig failed: %v", err)
} else {
log.Printf("wpa_supplicant: configuration saved for SSID %q", ssid)
}
return nil
}
// Disconnect disconnects from the current WiFi network
func (b *WPABackend) Disconnect() error {
// Disconnect from current network
if err := b.iface.Disconnect(); err != nil {
return fmt.Errorf("failed to disconnect: %v", err)
}
b.currentNetwork = ""
return nil
}
// GetConnectionState returns the current WiFi connection state
func (b *WPABackend) GetConnectionState() (backend.ConnectionState, error) {
state, err := b.iface.GetState()
if err != nil {
return backend.StateDisconnected, err
}
return mapWPAState(state), nil
}
// GetConnectedSSID returns the SSID of the currently connected network
func (b *WPABackend) GetConnectedSSID() string {
// Get current BSS
bssPath, err := b.iface.GetCurrentBSS()
if err != nil || bssPath == "/" {
return ""
}
// Get BSS object
bss := NewBSS(b.conn, bssPath)
ssid, err := bss.GetSSIDString()
if err != nil {
return ""
}
return ssid
}
// StartEventMonitoring starts monitoring WiFi events
func (b *WPABackend) StartEventMonitoring(callbacks backend.EventCallbacks) error {
// Create signal monitor
b.signalMonitor = NewSignalMonitor(b.conn, b.iface)
// Start monitoring
return b.signalMonitor.Start(callbacks)
}
// StopEventMonitoring stops monitoring WiFi events
func (b *WPABackend) StopEventMonitoring() {
if b.signalMonitor != nil {
b.signalMonitor.Stop()
}
}
// Wait for scan to complete (helper method)
func (b *WPABackend) waitForScanComplete(timeout time.Duration) error {
start := time.Now()
for {
if time.Since(start) > timeout {
return fmt.Errorf("scan timeout")
}
scanning, err := b.iface.GetScanning()
if err != nil {
return err
}
if !scanning {
return nil
}
time.Sleep(100 * time.Millisecond)
}
}