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) } }