repeater/internal/wifi/wpasupplicant/backend.go
Pierre-Olivier Mercier 11ae69de56 wpasupplicant: Disable D-Bus auto-activation on all calls
Every method call and property access to fi.w1.wpa_supplicant1 used flag
0, leaving D-Bus service activation enabled. Any stray request — a startup
race before `service wpa_supplicant start` takes effect, or a leaked call
while the Ethernet uplink is the chosen path — would make the bus daemon
spawn `wpa_supplicant -u` from its .service file, bypassing repeater's own
launch of the daemon.

Route every interaction through callNoAutoStart/getPropNoAutoStart/
setPropNoAutoStart, which set dbus.FlagNoAutoStart. A leaked call now fails
cleanly with ServiceUnknown instead of resurrecting the daemon, leaving
repeater as the sole launcher.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:44:59 +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 := callNoAutoStart(b.wpasupplicant, Service+".GetInterface", 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 = callNoAutoStart(b.wpasupplicant, Service+".CreateInterface", 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)
}
}