diff --git a/internal/app/app.go b/internal/app/app.go index b07f246..27299df 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -51,8 +51,8 @@ func (a *App) Initialize(cfg *config.Config) error { // Store config reference a.Config = cfg - // Initialize WiFi D-Bus connection - if err := wifi.Initialize(cfg.WifiInterface); err != nil { + // Initialize WiFi backend + if err := wifi.Initialize(cfg.WifiInterface, cfg.WifiBackend); err != nil { return err } diff --git a/internal/config/cli.go b/internal/config/cli.go index 9907f0c..a975875 100644 --- a/internal/config/cli.go +++ b/internal/config/cli.go @@ -8,6 +8,7 @@ import ( func declareFlags(o *Config) { flag.StringVar(&o.Bind, "bind", ":8081", "Bind port/socket") flag.StringVar(&o.WifiInterface, "wifi-interface", "wlan0", "WiFi interface name") + flag.StringVar(&o.WifiBackend, "wifi-backend", "", "WiFi backend to use: 'iwd' or 'wpasupplicant' (required)") flag.BoolVar(&o.UseARPDiscovery, "use-arp-discovery", true, "Use ARP table for device discovery instead of DHCP leases") flag.StringVar(&o.DHCPLeasesPath, "dhcp-leases-path", "/var/lib/dhcp/dhcpd.leases", "Path to DHCP leases file") flag.StringVar(&o.ARPTablePath, "arp-table-path", "/proc/net/arp", "Path to ARP table file") diff --git a/internal/config/config.go b/internal/config/config.go index d4a60fb..89f1d9f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,6 +11,7 @@ import ( type Config struct { Bind string WifiInterface string + WifiBackend string UseARPDiscovery bool DHCPLeasesPath string ARPTablePath string @@ -75,6 +76,11 @@ func ConsolidateConfig() (opts *Config, err error) { return } + // Validate configuration + if opts.WifiBackend != "iwd" && opts.WifiBackend != "wpasupplicant" { + log.Fatalf("wifi-backend must be set to 'iwd' or 'wpasupplicant' (got: '%s')", opts.WifiBackend) + } + return } diff --git a/internal/wifi/backend/types.go b/internal/wifi/backend/types.go new file mode 100644 index 0000000..1346e9b --- /dev/null +++ b/internal/wifi/backend/types.go @@ -0,0 +1,56 @@ +package backend + +// WiFiBackend is the interface that must be implemented by all WiFi backends (iwd, wpa_supplicant, etc.) +type WiFiBackend interface { + // Lifecycle Management + Initialize(interfaceName string) error + Close() error + + // Network Discovery + ScanNetworks() error + GetOrderedNetworks() ([]BackendNetwork, error) + IsScanning() (bool, error) + + // Connection Management + Connect(ssid, password string) error + Disconnect() error + GetConnectionState() (ConnectionState, error) + GetConnectedSSID() string + + // Event Monitoring + StartEventMonitoring(callbacks EventCallbacks) error + StopEventMonitoring() +} + +// BackendNetwork represents a WiFi network in a backend-agnostic format. +// Both iwd and wpa_supplicant backends convert their native representations to this type. +type BackendNetwork struct { + SSID string + SignalDBm int16 // Signal strength in dBm (-100 to 0) + SecurityType string // "open", "wep", "psk", "8021x" + BSSID string // MAC address of the access point + Frequency uint32 // Frequency in MHz (e.g., 2412 for channel 1, 5180 for channel 36) +} + +// ConnectionState represents the WiFi connection state in a backend-agnostic way. +type ConnectionState string + +const ( + StateConnected ConnectionState = "connected" + StateDisconnected ConnectionState = "disconnected" + StateConnecting ConnectionState = "connecting" + StateDisconnecting ConnectionState = "disconnecting" +) + +// EventCallbacks defines callback functions that backends use to notify the wifi package of events. +// This allows the wifi package to remain backend-agnostic while still receiving real-time updates. +type EventCallbacks struct { + // OnStateChange is called when the connection state changes + OnStateChange func(state ConnectionState, ssid string) + + // OnScanComplete is called when a network scan completes + OnScanComplete func() + + // OnSignalUpdate is called when signal strength changes for the connected network + OnSignalUpdate func(ssid string, signalDBm int16) +} diff --git a/internal/wifi/factory.go b/internal/wifi/factory.go new file mode 100644 index 0000000..5b49c7a --- /dev/null +++ b/internal/wifi/factory.go @@ -0,0 +1,21 @@ +package wifi + +import ( + "fmt" + + "github.com/nemunaire/repeater/internal/wifi/backend" + "github.com/nemunaire/repeater/internal/wifi/iwd" +) + +// createBackend creates the appropriate WiFi backend based on the backend name +func createBackend(backendName string) (backend.WiFiBackend, error) { + switch backendName { + case "iwd": + return iwd.NewIWDBackend(), nil + case "wpasupplicant": + // TODO: Implement wpa_supplicant backend + return nil, fmt.Errorf("wpa_supplicant backend not yet implemented") + default: + return nil, fmt.Errorf("invalid wifi backend: %s (must be 'iwd' or 'wpasupplicant')", backendName) + } +} diff --git a/internal/wifi/iwd/backend.go b/internal/wifi/iwd/backend.go new file mode 100644 index 0000000..615a280 --- /dev/null +++ b/internal/wifi/iwd/backend.go @@ -0,0 +1,255 @@ +package iwd + +import ( + "fmt" + + "github.com/godbus/dbus/v5" + "github.com/nemunaire/repeater/internal/wifi/backend" +) + +const ( + AgentPath = "/com/github/nemunaire/repeater/agent" +) + +// IWDBackend implements the WiFiBackend interface for iwd (Intel Wireless Daemon) +type IWDBackend struct { + conn *dbus.Conn + manager *Manager + station *Station + agent *Agent + agentManager *AgentManager + signalMonitor *SignalMonitor + interfaceName string + callbacks backend.EventCallbacks +} + +// NewIWDBackend creates a new IWD backend instance +func NewIWDBackend() *IWDBackend { + return &IWDBackend{} +} + +// Initialize initializes the iwd backend with the given interface name +func (b *IWDBackend) 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("échec de connexion à D-Bus: %v", err) + } + + // Find station for interface + b.manager = NewManager(b.conn) + b.station, err = b.manager.FindStation(interfaceName) + if err != nil { + return fmt.Errorf("impossible de trouver la station pour %s: %v", interfaceName, err) + } + + // Create and register agent for credential callbacks + b.agent = NewAgent(b.conn, dbus.ObjectPath(AgentPath)) + if err := b.agent.Export(); err != nil { + return fmt.Errorf("échec de l'export de l'agent: %v", err) + } + + b.agentManager = NewAgentManager(b.conn) + if err := b.agentManager.RegisterAgent(dbus.ObjectPath(AgentPath)); err != nil { + b.agent.Unexport() + return fmt.Errorf("échec de l'enregistrement de l'agent: %v", err) + } + + return nil +} + +// Close closes the D-Bus connection and unregisters the agent +func (b *IWDBackend) Close() error { + if b.agentManager != nil && b.agent != nil { + b.agentManager.UnregisterAgent(dbus.ObjectPath(AgentPath)) + b.agent.Unexport() + } + if b.conn != nil { + b.conn.Close() + } + return nil +} + +// ScanNetworks triggers a network scan +func (b *IWDBackend) ScanNetworks() error { + err := b.station.Scan() + if err != nil { + return fmt.Errorf("erreur lors du scan: %v", err) + } + return nil +} + +// GetOrderedNetworks returns networks sorted by signal strength in backend-agnostic format +func (b *IWDBackend) GetOrderedNetworks() ([]backend.BackendNetwork, error) { + networkInfos, err := b.station.GetOrderedNetworks() + if err != nil { + return nil, fmt.Errorf("erreur lors de la récupération des réseaux: %v", err) + } + + var networks []backend.BackendNetwork + seenSSIDs := make(map[string]bool) + + for _, netInfo := range networkInfos { + network := NewNetwork(b.conn, netInfo.Path) + props, err := network.GetProperties() + if err != nil { + continue + } + + if props.Name == "" || seenSSIDs[props.Name] { + continue + } + seenSSIDs[props.Name] = true + + // Convert iwd network to backend-agnostic format + backendNet := backend.BackendNetwork{ + SSID: props.Name, + SignalDBm: netInfo.Signal / 100, // iwd provides 100*dBm, convert to dBm + SecurityType: props.Type, + BSSID: generateSyntheticBSSID(props.Name), // iwd doesn't expose BSSID + Frequency: 0, // iwd doesn't expose frequency in GetOrderedNetworks + } + + networks = append(networks, backendNet) + } + + return networks, nil +} + +// IsScanning checks if a scan is currently in progress +func (b *IWDBackend) IsScanning() (bool, error) { + return b.station.IsScanning() +} + +// Connect connects to a WiFi network +func (b *IWDBackend) Connect(ssid, password string) error { + // Store passphrase in agent for callback + if password != "" { + b.agent.SetPassphrase(ssid, password) + } + + // Ensure passphrase is cleared after connection attempt + defer func() { + if password != "" { + b.agent.ClearPassphrase(ssid) + } + }() + + // Get network object + network, err := b.station.GetNetwork(ssid) + if err != nil { + return fmt.Errorf("réseau '%s' non trouvé: %v", ssid, err) + } + + // Connect - iwd will call agent.RequestPassphrase() if needed + if err := network.Connect(); err != nil { + return fmt.Errorf("erreur lors de la connexion: %v", err) + } + + return nil +} + +// Disconnect disconnects from the current WiFi network +func (b *IWDBackend) Disconnect() error { + if err := b.station.Disconnect(); err != nil { + return fmt.Errorf("erreur lors de la déconnexion: %v", err) + } + return nil +} + +// GetConnectionState returns the current WiFi connection state +func (b *IWDBackend) GetConnectionState() (backend.ConnectionState, error) { + state, err := b.station.GetState() + if err != nil { + return backend.StateDisconnected, err + } + return mapIWDState(state), nil +} + +// GetConnectedSSID returns the SSID of the currently connected network +func (b *IWDBackend) GetConnectedSSID() string { + network, err := b.station.GetConnectedNetwork() + if err != nil { + return "" + } + + props, err := network.GetProperties() + if err != nil { + return "" + } + + return props.Name +} + +// StartEventMonitoring starts monitoring WiFi events +func (b *IWDBackend) StartEventMonitoring(callbacks backend.EventCallbacks) error { + b.callbacks = callbacks + + // Create signal monitor + b.signalMonitor = NewSignalMonitor(b.conn, b.station) + + // Register callbacks - wrap to convert iwd types to backend types + b.signalMonitor.OnStateChange(func(state StationState, ssid string) { + if b.callbacks.OnStateChange != nil { + b.callbacks.OnStateChange(mapIWDState(state), ssid) + } + }) + + b.signalMonitor.OnScanComplete(func() { + if b.callbacks.OnScanComplete != nil { + b.callbacks.OnScanComplete() + } + }) + + // Start monitoring + return b.signalMonitor.Start() +} + +// StopEventMonitoring stops monitoring WiFi events +func (b *IWDBackend) StopEventMonitoring() { + if b.signalMonitor != nil { + b.signalMonitor.Stop() + } +} + +// mapIWDState maps iwd-specific states to backend-agnostic states +func mapIWDState(state StationState) backend.ConnectionState { + switch state { + case StateConnected: + return backend.StateConnected + case StateConnecting: + return backend.StateConnecting + case StateDisconnecting: + return backend.StateDisconnecting + case StateDisconnected: + return backend.StateDisconnected + case StateRoaming: + // Map roaming to connected since we're still connected during roaming + return backend.StateConnected + default: + return backend.StateDisconnected + } +} + +// generateSyntheticBSSID generates a consistent fake BSSID from SSID +// (iwd doesn't expose real BSSID) +func generateSyntheticBSSID(ssid string) string { + // Use a simple hash approach - consistent per SSID + hash := 0 + for _, c := range ssid { + hash = ((hash << 5) - hash) + int(c) + } + + // Generate 6 bytes for MAC address + b1 := byte((hash >> 0) & 0xff) + b2 := byte((hash >> 8) & 0xff) + b3 := byte((hash >> 16) & 0xff) + b4 := byte((hash >> 24) & 0xff) + b5 := byte(len(ssid) & 0xff) + b6 := byte((len(ssid) >> 8) & 0xff) + + return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", b1, b2, b3, b4, b5, b6) +} diff --git a/internal/wifi/wifi.go b/internal/wifi/wifi.go index ee678c1..dac04c3 100644 --- a/internal/wifi/wifi.go +++ b/internal/wifi/wifi.go @@ -6,102 +6,54 @@ import ( "strings" "time" - "github.com/godbus/dbus/v5" "github.com/gorilla/websocket" "github.com/nemunaire/repeater/internal/models" - "github.com/nemunaire/repeater/internal/wifi/iwd" -) - -const ( - AGENT_PATH = "/com/github/nemunaire/repeater/agent" + "github.com/nemunaire/repeater/internal/wifi/backend" ) var ( - wlanInterface string - dbusConn *dbus.Conn - iwdManager *iwd.Manager - station *iwd.Station - agent *iwd.Agent - agentManager *iwd.AgentManager - eventMonitor *iwd.SignalMonitor + wifiBackend backend.WiFiBackend wifiBroadcaster *WifiBroadcaster ) -// Initialize initializes the WiFi service with iwd D-Bus connection -func Initialize(interfaceName string) error { - wlanInterface = interfaceName +// Initialize initializes the WiFi service with the specified backend +func Initialize(interfaceName string, backendName string) error { + // Create the appropriate backend using the factory var err error - - // Connect to D-Bus - dbusConn, err = dbus.SystemBus() + wifiBackend, err = createBackend(backendName) if err != nil { - return fmt.Errorf("échec de connexion à D-Bus: %v", err) + return err } - // Find station for interface - iwdManager = iwd.NewManager(dbusConn) - station, err = iwdManager.FindStation(interfaceName) - if err != nil { - return fmt.Errorf("impossible de trouver la station pour %s: %v", interfaceName, err) - } - - // Create and register agent for credential callbacks - agent = iwd.NewAgent(dbusConn, dbus.ObjectPath(AGENT_PATH)) - if err := agent.Export(); err != nil { - return fmt.Errorf("échec de l'export de l'agent: %v", err) - } - - agentManager = iwd.NewAgentManager(dbusConn) - if err := agentManager.RegisterAgent(dbus.ObjectPath(AGENT_PATH)); err != nil { - agent.Unexport() - return fmt.Errorf("échec de l'enregistrement de l'agent: %v", err) - } - - return nil + // Initialize the backend + return wifiBackend.Initialize(interfaceName) } -// Close closes the D-Bus connection and unregisters the agent +// Close closes the backend connection func Close() { - if agentManager != nil && agent != nil { - agentManager.UnregisterAgent(dbus.ObjectPath(AGENT_PATH)) - agent.Unexport() - } - if dbusConn != nil { - dbusConn.Close() + if wifiBackend != nil { + wifiBackend.Close() } } // GetCachedNetworks returns previously discovered networks without triggering a scan func GetCachedNetworks() ([]models.WiFiNetwork, error) { - // Get ordered networks without scanning - networkInfos, err := station.GetOrderedNetworks() + // Get ordered networks from backend + backendNetworks, err := wifiBackend.GetOrderedNetworks() if err != nil { return nil, fmt.Errorf("erreur lors de la récupération des réseaux: %v", err) } - var networks []models.WiFiNetwork - seenSSIDs := make(map[string]bool) - - for _, netInfo := range networkInfos { - network := iwd.NewNetwork(dbusConn, netInfo.Path) - props, err := network.GetProperties() - if err != nil { - continue - } - - if props.Name == "" || seenSSIDs[props.Name] { - continue - } - seenSSIDs[props.Name] = true - + // Convert backend networks to models + networks := make([]models.WiFiNetwork, 0, len(backendNetworks)) + for _, backendNet := range backendNetworks { wifiNet := models.WiFiNetwork{ - SSID: props.Name, - Signal: signalToStrength(int(netInfo.Signal) / 100), - Security: mapSecurityType(props.Type), - BSSID: generateSyntheticBSSID(props.Name), - Channel: 0, + SSID: backendNet.SSID, + Signal: signalToStrength(int(backendNet.SignalDBm)), + Security: mapSecurityType(backendNet.SecurityType), + BSSID: backendNet.BSSID, + Channel: 0, // Not yet exposed by backends } - networks = append(networks, wifiNet) } @@ -116,47 +68,34 @@ func GetCachedNetworks() ([]models.WiFiNetwork, error) { // ScanNetworks scans for available WiFi networks func ScanNetworks() ([]models.WiFiNetwork, error) { // Check if already scanning - scanning, err := station.IsScanning() + scanning, err := wifiBackend.IsScanning() if err == nil && scanning { time.Sleep(3 * time.Second) } else { // Trigger scan - err := station.Scan() + err := wifiBackend.ScanNetworks() if err != nil && !strings.Contains(err.Error(), "rejected") { return nil, fmt.Errorf("erreur lors du scan: %v", err) } time.Sleep(2 * time.Second) } - // Get ordered networks - networkInfos, err := station.GetOrderedNetworks() + // Get ordered networks from backend + backendNetworks, err := wifiBackend.GetOrderedNetworks() if err != nil { return nil, fmt.Errorf("erreur lors de la récupération des réseaux: %v", err) } - var networks []models.WiFiNetwork - seenSSIDs := make(map[string]bool) - - for _, netInfo := range networkInfos { - network := iwd.NewNetwork(dbusConn, netInfo.Path) - props, err := network.GetProperties() - if err != nil { - continue - } - - if props.Name == "" || seenSSIDs[props.Name] { - continue - } - seenSSIDs[props.Name] = true - + // Convert backend networks to models + networks := make([]models.WiFiNetwork, 0, len(backendNetworks)) + for _, backendNet := range backendNetworks { wifiNet := models.WiFiNetwork{ - SSID: props.Name, - Signal: signalToStrength(int(netInfo.Signal) / 100), - Security: mapSecurityType(props.Type), - BSSID: generateSyntheticBSSID(props.Name), - Channel: 0, + SSID: backendNet.SSID, + Signal: signalToStrength(int(backendNet.SignalDBm)), + Security: mapSecurityType(backendNet.SecurityType), + BSSID: backendNet.BSSID, + Channel: 0, // Not yet exposed by backends } - networks = append(networks, wifiNet) } @@ -173,29 +112,11 @@ func ScanNetworks() ([]models.WiFiNetwork, error) { return networks, nil } -// Connect connects to a WiFi network using iwd agent callback +// Connect connects to a WiFi network func Connect(ssid, password string) error { - // Store passphrase in agent for callback - if password != "" { - agent.SetPassphrase(ssid, password) - } - - // Ensure passphrase is cleared after connection attempt - defer func() { - if password != "" { - agent.ClearPassphrase(ssid) - } - }() - - // Get network object - network, err := station.GetNetwork(ssid) - if err != nil { - return fmt.Errorf("réseau '%s' non trouvé: %v", ssid, err) - } - - // Connect - iwd will call agent.RequestPassphrase() if needed - if err := network.Connect(); err != nil { - return fmt.Errorf("erreur lors de la connexion: %v", err) + // Use backend to connect + if err := wifiBackend.Connect(ssid, password); err != nil { + return err } // Poll for connection @@ -211,65 +132,51 @@ func Connect(ssid, password string) error { // Disconnect disconnects from the current WiFi network func Disconnect() error { - if err := station.Disconnect(); err != nil { - return fmt.Errorf("erreur lors de la déconnexion: %v", err) - } - return nil + return wifiBackend.Disconnect() } -// IsConnected checks if WiFi is connected using iwd +// IsConnected checks if WiFi is connected func IsConnected() bool { - state, err := station.GetState() + state, err := wifiBackend.GetConnectionState() if err != nil { return false } - return state == iwd.StateConnected + return state == backend.StateConnected } // GetConnectedSSID returns the SSID of the currently connected network func GetConnectedSSID() string { - network, err := station.GetConnectedNetwork() - if err != nil { - return "" - } - - props, err := network.GetProperties() - if err != nil { - return "" - } - - return props.Name + return wifiBackend.GetConnectedSSID() } // GetConnectionState returns the current WiFi connection state func GetConnectionState() string { - state, err := station.GetState() + state, err := wifiBackend.GetConnectionState() if err != nil { - return string(iwd.StateDisconnected) + return string(backend.StateDisconnected) } return string(state) } -// StartEventMonitoring initializes D-Bus signal monitoring and WebSocket broadcasting +// StartEventMonitoring initializes signal monitoring and WebSocket broadcasting func StartEventMonitoring() error { // Initialize broadcaster wifiBroadcaster = NewWifiBroadcaster() - // Create signal monitor - eventMonitor = iwd.NewSignalMonitor(dbusConn, station) + // Set up callbacks + callbacks := backend.EventCallbacks{ + OnStateChange: handleStateChange, + OnScanComplete: handleScanComplete, + } - // Register callbacks - eventMonitor.OnStateChange(handleStateChange) - eventMonitor.OnScanComplete(handleScanComplete) - - // Start monitoring - return eventMonitor.Start() + // Start backend monitoring + return wifiBackend.StartEventMonitoring(callbacks) } -// StopEventMonitoring stops D-Bus signal monitoring +// StopEventMonitoring stops signal monitoring func StopEventMonitoring() { - if eventMonitor != nil { - eventMonitor.Stop() + if wifiBackend != nil { + wifiBackend.StopEventMonitoring() } } @@ -288,7 +195,7 @@ func UnregisterWebSocketClient(conn *websocket.Conn) { } // handleStateChange is called when WiFi connection state changes -func handleStateChange(newState iwd.StationState, connectedSSID string) { +func handleStateChange(newState backend.ConnectionState, connectedSSID string) { if wifiBroadcaster != nil { wifiBroadcaster.BroadcastStateChange(string(newState), connectedSSID) } @@ -303,9 +210,9 @@ func handleScanComplete() { } } -// mapSecurityType maps iwd security types to display format -func mapSecurityType(iwdType string) string { - switch iwdType { +// mapSecurityType maps backend security types to display format +func mapSecurityType(securityType string) string { + switch securityType { case "open": return "Open" case "wep": @@ -319,25 +226,6 @@ func mapSecurityType(iwdType string) string { } } -// generateSyntheticBSSID generates a consistent fake BSSID from SSID -func generateSyntheticBSSID(ssid string) string { - // Use a simple hash approach - consistent per SSID - hash := 0 - for _, c := range ssid { - hash = ((hash << 5) - hash) + int(c) - } - - // Generate 6 bytes for MAC address - b1 := byte((hash >> 0) & 0xff) - b2 := byte((hash >> 8) & 0xff) - b3 := byte((hash >> 16) & 0xff) - b4 := byte((hash >> 24) & 0xff) - b5 := byte(len(ssid) & 0xff) - b6 := byte((len(ssid) >> 8) & 0xff) - - return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", b1, b2, b3, b4, b5, b6) -} - // signalToStrength converts signal level (dBm) to strength (1-5) func signalToStrength(level int) int { if level >= -30 {