Implement wpa_supplicant backend
This commit is contained in:
parent
79c28da9c5
commit
04ada45f44
7 changed files with 874 additions and 2 deletions
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
"github.com/nemunaire/repeater/internal/wifi/backend"
|
"github.com/nemunaire/repeater/internal/wifi/backend"
|
||||||
"github.com/nemunaire/repeater/internal/wifi/iwd"
|
"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
|
// createBackend creates the appropriate WiFi backend based on the backend name
|
||||||
|
|
@ -13,8 +14,7 @@ func createBackend(backendName string) (backend.WiFiBackend, error) {
|
||||||
case "iwd":
|
case "iwd":
|
||||||
return iwd.NewIWDBackend(), nil
|
return iwd.NewIWDBackend(), nil
|
||||||
case "wpasupplicant":
|
case "wpasupplicant":
|
||||||
// TODO: Implement wpa_supplicant backend
|
return wpasupplicant.NewWPABackend(), nil
|
||||||
return nil, fmt.Errorf("wpa_supplicant backend not yet implemented")
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("invalid wifi backend: %s (must be 'iwd' or 'wpasupplicant')", backendName)
|
return nil, fmt.Errorf("invalid wifi backend: %s (must be 'iwd' or 'wpasupplicant')", backendName)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
265
internal/wifi/wpasupplicant/backend.go
Normal file
265
internal/wifi/wpasupplicant/backend.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
157
internal/wifi/wpasupplicant/bss.go
Normal file
157
internal/wifi/wpasupplicant/bss.go
Normal 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"
|
||||||
|
}
|
||||||
146
internal/wifi/wpasupplicant/interface.go
Normal file
146
internal/wifi/wpasupplicant/interface.go
Normal 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
|
||||||
|
}
|
||||||
41
internal/wifi/wpasupplicant/network.go
Normal file
41
internal/wifi/wpasupplicant/network.go
Normal 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
|
||||||
|
}
|
||||||
236
internal/wifi/wpasupplicant/signals.go
Normal file
236
internal/wifi/wpasupplicant/signals.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
27
internal/wifi/wpasupplicant/types.go
Normal file
27
internal/wifi/wpasupplicant/types.go
Normal 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"
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue