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>
308 lines
7.8 KiB
Go
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)
|
|
}
|
|
}
|