Implement wpa_supplicant backend

This commit is contained in:
nemunaire 2026-01-01 22:19:57 +07:00
commit 04ada45f44
7 changed files with 874 additions and 2 deletions

View file

@ -5,6 +5,7 @@ import (
"github.com/nemunaire/repeater/internal/wifi/backend"
"github.com/nemunaire/repeater/internal/wifi/iwd"
"github.com/nemunaire/repeater/internal/wifi/wpasupplicant"
)
// createBackend creates the appropriate WiFi backend based on the backend name
@ -13,8 +14,7 @@ func createBackend(backendName string) (backend.WiFiBackend, error) {
case "iwd":
return iwd.NewIWDBackend(), nil
case "wpasupplicant":
// TODO: Implement wpa_supplicant backend
return nil, fmt.Errorf("wpa_supplicant backend not yet implemented")
return wpasupplicant.NewWPABackend(), nil
default:
return nil, fmt.Errorf("invalid wifi backend: %s (must be 'iwd' or 'wpasupplicant')", backendName)
}

View file

@ -0,0 +1,265 @@
package wpasupplicant
import (
"fmt"
"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 {
// Create network configuration
config := make(map[string]interface{})
config["ssid"] = fmt.Sprintf("\"%s\"", ssid) // wpa_supplicant expects quoted SSID
if password != "" {
// For WPA/WPA2-PSK networks
config["psk"] = fmt.Sprintf("\"%s\"", password)
} else {
// For open networks
config["key_mgmt"] = "NONE"
}
// Add network
networkPath, err := b.iface.AddNetwork(config)
if err != nil {
return fmt.Errorf("failed to add network: %v", err)
}
// Store current network path for cleanup
b.currentNetwork = networkPath
// Select (connect to) the network
err = b.iface.SelectNetwork(networkPath)
if err != nil {
// Clean up network on failure
b.iface.RemoveNetwork(networkPath)
return fmt.Errorf("failed to select network: %v", err)
}
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)
}
// Remove the network configuration if we have one
if b.currentNetwork != "" && b.currentNetwork != "/" {
b.iface.RemoveNetwork(b.currentNetwork)
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)
}
}

View file

@ -0,0 +1,157 @@
package wpasupplicant
import (
"fmt"
"github.com/godbus/dbus/v5"
)
// BSS represents a wpa_supplicant BSS (Basic Service Set) object
type BSS struct {
path dbus.ObjectPath
conn *dbus.Conn
obj dbus.BusObject
}
// BSSProperties holds the properties of a BSS
type BSSProperties struct {
SSID []byte
BSSID []byte
Signal int16 // Signal strength in dBm
Frequency uint32 // Frequency in MHz
Privacy bool // Whether encryption is enabled
RSN map[string]dbus.Variant
WPA map[string]dbus.Variant
}
// NewBSS creates a new BSS instance
func NewBSS(conn *dbus.Conn, path dbus.ObjectPath) *BSS {
return &BSS{
path: path,
conn: conn,
obj: conn.Object(Service, path),
}
}
// GetProperties returns all properties of the BSS
func (b *BSS) GetProperties() (*BSSProperties, error) {
props := &BSSProperties{}
// Get SSID
if ssidProp, err := b.obj.GetProperty(BSSInterface + ".SSID"); err == nil {
if ssid, ok := ssidProp.Value().([]byte); ok {
props.SSID = ssid
}
}
// Get BSSID
if bssidProp, err := b.obj.GetProperty(BSSInterface + ".BSSID"); err == nil {
if bssid, ok := bssidProp.Value().([]byte); ok {
props.BSSID = bssid
}
}
// Get Signal
if signalProp, err := b.obj.GetProperty(BSSInterface + ".Signal"); err == nil {
if signal, ok := signalProp.Value().(int16); ok {
props.Signal = signal
}
}
// Get Frequency
if freqProp, err := b.obj.GetProperty(BSSInterface + ".Frequency"); err == nil {
if freq, ok := freqProp.Value().(uint16); ok {
props.Frequency = uint32(freq)
}
}
// Get Privacy
if privacyProp, err := b.obj.GetProperty(BSSInterface + ".Privacy"); err == nil {
if privacy, ok := privacyProp.Value().(bool); ok {
props.Privacy = privacy
}
}
// Get RSN (WPA2) information
if rsnProp, err := b.obj.GetProperty(BSSInterface + ".RSN"); err == nil {
if rsn, ok := rsnProp.Value().(map[string]dbus.Variant); ok {
props.RSN = rsn
}
}
// Get WPA information
if wpaProp, err := b.obj.GetProperty(BSSInterface + ".WPA"); err == nil {
if wpa, ok := wpaProp.Value().(map[string]dbus.Variant); ok {
props.WPA = wpa
}
}
return props, nil
}
// GetSSIDString returns the SSID as a string
func (b *BSS) GetSSIDString() (string, error) {
prop, err := b.obj.GetProperty(BSSInterface + ".SSID")
if err != nil {
return "", fmt.Errorf("failed to get SSID property: %v", err)
}
ssid, ok := prop.Value().([]byte)
if !ok {
return "", fmt.Errorf("SSID property is not a byte array")
}
return string(ssid), nil
}
// GetBSSIDString returns the BSSID as a formatted MAC address string
func (b *BSS) GetBSSIDString() (string, error) {
prop, err := b.obj.GetProperty(BSSInterface + ".BSSID")
if err != nil {
return "", fmt.Errorf("failed to get BSSID property: %v", err)
}
bssid, ok := prop.Value().([]byte)
if !ok || len(bssid) != 6 {
return "", fmt.Errorf("BSSID property is not a valid MAC address")
}
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x",
bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]), nil
}
// GetSignal returns the signal strength in dBm
func (b *BSS) GetSignal() (int16, error) {
prop, err := b.obj.GetProperty(BSSInterface + ".Signal")
if err != nil {
return 0, fmt.Errorf("failed to get Signal property: %v", err)
}
signal, ok := prop.Value().(int16)
if !ok {
return 0, fmt.Errorf("Signal property is not an int16")
}
return signal, nil
}
// DetermineSecurityType determines the security type based on BSS properties
func (p *BSSProperties) DetermineSecurityType() string {
// Check for WPA2 (RSN)
if len(p.RSN) > 0 {
return "psk"
}
// Check for WPA
if len(p.WPA) > 0 {
return "psk"
}
// Check for WEP (privacy but no WPA/RSN)
if p.Privacy {
return "wep"
}
// Open network
return "open"
}

View file

@ -0,0 +1,146 @@
package wpasupplicant
import (
"fmt"
"github.com/godbus/dbus/v5"
)
// WPAInterface represents a wpa_supplicant Interface object
type WPAInterface struct {
path dbus.ObjectPath
conn *dbus.Conn
obj dbus.BusObject
}
// NewWPAInterface creates a new WPAInterface instance
func NewWPAInterface(conn *dbus.Conn, path dbus.ObjectPath) *WPAInterface {
return &WPAInterface{
path: path,
conn: conn,
obj: conn.Object(Service, path),
}
}
// Scan triggers a network scan
func (i *WPAInterface) Scan(scanType string) error {
args := map[string]interface{}{
"Type": scanType, // "active" or "passive"
}
err := i.obj.Call(InterfaceInterface+".Scan", 0, args).Err
if err != nil {
return fmt.Errorf("scan failed: %v", err)
}
return nil
}
// GetBSSs returns a list of BSS (Basic Service Set) object paths
func (i *WPAInterface) GetBSSs() ([]dbus.ObjectPath, error) {
prop, err := i.obj.GetProperty(InterfaceInterface + ".BSSs")
if err != nil {
return nil, fmt.Errorf("failed to get BSSs property: %v", err)
}
bsss, ok := prop.Value().([]dbus.ObjectPath)
if !ok {
return nil, fmt.Errorf("BSSs property is not an array of ObjectPath")
}
return bsss, nil
}
// GetState returns the current connection state
func (i *WPAInterface) GetState() (WPAState, error) {
prop, err := i.obj.GetProperty(InterfaceInterface + ".State")
if err != nil {
return "", fmt.Errorf("failed to get State property: %v", err)
}
state, ok := prop.Value().(string)
if !ok {
return "", fmt.Errorf("State property is not a string")
}
return WPAState(state), nil
}
// GetCurrentBSS returns the currently connected BSS object path
func (i *WPAInterface) GetCurrentBSS() (dbus.ObjectPath, error) {
prop, err := i.obj.GetProperty(InterfaceInterface + ".CurrentBSS")
if err != nil {
return "", fmt.Errorf("failed to get CurrentBSS property: %v", err)
}
bss, ok := prop.Value().(dbus.ObjectPath)
if !ok {
return "", fmt.Errorf("CurrentBSS property is not an ObjectPath")
}
return bss, nil
}
// AddNetwork creates a new network configuration
func (i *WPAInterface) AddNetwork(config map[string]interface{}) (dbus.ObjectPath, error) {
var networkPath dbus.ObjectPath
// Convert config to proper DBus variant format
dbusConfig := make(map[string]dbus.Variant)
for key, value := range config {
dbusConfig[key] = dbus.MakeVariant(value)
}
err := i.obj.Call(InterfaceInterface+".AddNetwork", 0, dbusConfig).Store(&networkPath)
if err != nil {
return "", fmt.Errorf("failed to add network: %v", err)
}
return networkPath, nil
}
// SelectNetwork connects to a network
func (i *WPAInterface) SelectNetwork(networkPath dbus.ObjectPath) error {
err := i.obj.Call(InterfaceInterface+".SelectNetwork", 0, networkPath).Err
if err != nil {
return fmt.Errorf("failed to select 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
if err != nil {
return fmt.Errorf("failed to remove network: %v", err)
}
return nil
}
// Disconnect disconnects from the current network
func (i *WPAInterface) Disconnect() error {
err := i.obj.Call(InterfaceInterface+".Disconnect", 0).Err
if err != nil {
return fmt.Errorf("disconnect failed: %v", err)
}
return nil
}
// GetPath returns the D-Bus object path for this interface
func (i *WPAInterface) GetPath() dbus.ObjectPath {
return i.path
}
// GetScanning returns whether a scan is currently in progress
func (i *WPAInterface) GetScanning() (bool, error) {
prop, err := i.obj.GetProperty(InterfaceInterface + ".Scanning")
if err != nil {
return false, fmt.Errorf("failed to get Scanning property: %v", err)
}
scanning, ok := prop.Value().(bool)
if !ok {
return false, fmt.Errorf("Scanning property is not a boolean")
}
return scanning, nil
}

View file

@ -0,0 +1,41 @@
package wpasupplicant
import (
"github.com/godbus/dbus/v5"
)
// Network represents a wpa_supplicant Network configuration object
type Network struct {
path dbus.ObjectPath
conn *dbus.Conn
obj dbus.BusObject
}
// NewNetwork creates a new Network instance
func NewNetwork(conn *dbus.Conn, path dbus.ObjectPath) *Network {
return &Network{
path: path,
conn: conn,
obj: conn.Object(Service, path),
}
}
// GetPath returns the D-Bus object path for this network
func (n *Network) GetPath() dbus.ObjectPath {
return n.path
}
// GetProperties returns properties of the network configuration
func (n *Network) GetProperties() (map[string]dbus.Variant, error) {
prop, err := n.obj.GetProperty(NetworkInterface + ".Properties")
if err != nil {
return nil, err
}
props, ok := prop.Value().(map[string]dbus.Variant)
if !ok {
return nil, nil
}
return props, nil
}

View file

@ -0,0 +1,236 @@
package wpasupplicant
import (
"log"
"sync"
"github.com/godbus/dbus/v5"
"github.com/nemunaire/repeater/internal/wifi/backend"
)
// SignalMonitor monitors D-Bus signals from wpa_supplicant
type SignalMonitor struct {
conn *dbus.Conn
iface *WPAInterface
callbacks backend.EventCallbacks
// Signal channel
signalChan chan *dbus.Signal
// Control
stopChan chan struct{}
mu sync.RWMutex
running bool
// State tracking
lastState WPAState
}
// NewSignalMonitor creates a new signal monitor
func NewSignalMonitor(conn *dbus.Conn, iface *WPAInterface) *SignalMonitor {
return &SignalMonitor{
conn: conn,
iface: iface,
signalChan: make(chan *dbus.Signal, 100),
stopChan: make(chan struct{}),
}
}
// Start begins monitoring D-Bus signals
func (sm *SignalMonitor) Start(callbacks backend.EventCallbacks) error {
sm.mu.Lock()
if sm.running {
sm.mu.Unlock()
return nil
}
sm.running = true
sm.callbacks = callbacks
sm.mu.Unlock()
interfacePath := sm.iface.GetPath()
// Add signal match for PropertiesChanged on Interface
matchOptions := []dbus.MatchOption{
dbus.WithMatchObjectPath(interfacePath),
dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
dbus.WithMatchMember("PropertiesChanged"),
}
if err := sm.conn.AddMatchSignal(matchOptions...); err != nil {
sm.mu.Lock()
sm.running = false
sm.mu.Unlock()
return err
}
// Add signal match for ScanDone
scanDoneOptions := []dbus.MatchOption{
dbus.WithMatchObjectPath(interfacePath),
dbus.WithMatchInterface(InterfaceInterface),
dbus.WithMatchMember("ScanDone"),
}
if err := sm.conn.AddMatchSignal(scanDoneOptions...); err != nil {
sm.mu.Lock()
sm.running = false
sm.mu.Unlock()
return err
}
// Register signal channel
sm.conn.Signal(sm.signalChan)
// Get initial state
state, err := sm.iface.GetState()
if err == nil {
sm.lastState = state
}
// Start monitoring goroutine
go sm.monitor()
log.Printf("D-Bus signal monitoring started for wpa_supplicant interface %s", interfacePath)
return nil
}
// Stop stops monitoring D-Bus signals
func (sm *SignalMonitor) Stop() {
sm.mu.Lock()
if !sm.running {
sm.mu.Unlock()
return
}
sm.running = false
sm.mu.Unlock()
// Signal stop
close(sm.stopChan)
// Remove signal channel
sm.conn.RemoveSignal(sm.signalChan)
log.Printf("D-Bus signal monitoring stopped for wpa_supplicant")
}
// monitor is the main signal processing loop
func (sm *SignalMonitor) monitor() {
for {
select {
case sig := <-sm.signalChan:
sm.handleSignal(sig)
case <-sm.stopChan:
return
}
}
}
// handleSignal processes a D-Bus signal
func (sm *SignalMonitor) handleSignal(sig *dbus.Signal) {
// Handle ScanDone signal
if sig.Name == InterfaceInterface+".ScanDone" {
sm.handleScanDone(sig)
return
}
// Handle PropertiesChanged signals
if sig.Name != "org.freedesktop.DBus.Properties.PropertiesChanged" {
return
}
// Verify signal is from Interface
if len(sig.Body) < 2 {
return
}
interfaceName, ok := sig.Body[0].(string)
if !ok || interfaceName != InterfaceInterface {
return
}
// Parse changed properties
changedProps, ok := sig.Body[1].(map[string]dbus.Variant)
if !ok {
return
}
// Check for State property change
if stateVariant, ok := changedProps["State"]; ok {
if state, ok := stateVariant.Value().(string); ok {
sm.handleStateChange(WPAState(state))
}
}
// Check for CurrentBSS property change (connection status)
if _, ok := changedProps["CurrentBSS"]; ok {
// BSS changed, trigger state update
sm.handleConnectionChange()
}
}
// handleStateChange processes a state change
func (sm *SignalMonitor) handleStateChange(state WPAState) {
sm.lastState = state
sm.mu.RLock()
callback := sm.callbacks.OnStateChange
sm.mu.RUnlock()
if callback == nil {
return
}
// Map wpa_supplicant state to backend state
backendState := mapWPAState(state)
// Get connected SSID if connected
ssid := ""
if backendState == backend.StateConnected {
if bssPath, err := sm.iface.GetCurrentBSS(); err == nil && bssPath != "/" {
bss := NewBSS(sm.conn, bssPath)
if ssidStr, err := bss.GetSSIDString(); err == nil {
ssid = ssidStr
}
}
}
callback(backendState, ssid)
}
// handleConnectionChange processes connection changes
func (sm *SignalMonitor) handleConnectionChange() {
// Get current state and trigger state change callback
state, err := sm.iface.GetState()
if err != nil {
return
}
sm.handleStateChange(state)
}
// handleScanDone processes scan completion
func (sm *SignalMonitor) handleScanDone(sig *dbus.Signal) {
sm.mu.RLock()
callback := sm.callbacks.OnScanComplete
sm.mu.RUnlock()
if callback != nil {
callback()
}
}
// mapWPAState maps wpa_supplicant states to backend-agnostic states
func mapWPAState(state WPAState) backend.ConnectionState {
switch state {
case StateCompleted:
return backend.StateConnected
case StateAuthenticating, StateAssociating, StateAssociated, State4WayHandshake, StateGroupHandshake:
return backend.StateConnecting
case StateDisconnected, StateInactive, StateInterfaceDisabled:
return backend.StateDisconnected
case StateScanning:
// Keep as disconnected if just scanning
return backend.StateDisconnected
default:
return backend.StateDisconnected
}
}

View file

@ -0,0 +1,27 @@
package wpasupplicant
const (
// D-Bus service and interfaces
Service = "fi.w1.wpa_supplicant1"
RootPath = "/fi/w1/wpa_supplicant1"
InterfaceInterface = "fi.w1.wpa_supplicant1.Interface"
BSSInterface = "fi.w1.wpa_supplicant1.BSS"
NetworkInterface = "fi.w1.wpa_supplicant1.Network"
)
// WPAState represents the wpa_supplicant connection state
type WPAState string
const (
// wpa_supplicant state strings
StateDisconnected WPAState = "disconnected"
StateInactive WPAState = "inactive"
StateScanning WPAState = "scanning"
StateAuthenticating WPAState = "authenticating"
StateAssociating WPAState = "associating"
StateAssociated WPAState = "associated"
State4WayHandshake WPAState = "4way_handshake"
StateGroupHandshake WPAState = "group_handshake"
StateCompleted WPAState = "completed"
StateInterfaceDisabled WPAState = "interface_disabled"
)