diff --git a/cmd/repeater/static/app.js b/cmd/repeater/static/app.js
index bd29eec..3813ab8 100644
--- a/cmd/repeater/static/app.js
+++ b/cmd/repeater/static/app.js
@@ -5,7 +5,11 @@ const appState = {
autoScrollLogs: true,
ws: null,
reconnectAttempts: 0,
- maxReconnectAttempts: 5
+ maxReconnectAttempts: 5,
+ connectedSSID: null,
+ networks: [],
+ uptime: 0,
+ uptimeInterval: null
};
// Initialize the application
@@ -30,7 +34,8 @@ async function initializeApp() {
// Start periodic updates
startPeriodicUpdates();
- showToast('success', 'Connecté', 'Interface web chargée avec succès');
+ // Start uptime counter
+ startUptimeCounter();
}
// ===== API Functions =====
@@ -62,6 +67,7 @@ async function scanWifi() {
const response = await fetch('/api/wifi/scan');
const networks = await response.json();
+ appState.networks = networks;
displayWifiNetworks(networks);
showToast('success', 'Scan WiFi', `${networks.length} réseau(x) trouvé(s)`);
} catch (error) {
@@ -128,6 +134,10 @@ async function disconnectWifi() {
if (response.ok) {
showToast('success', 'Déconnecté', 'Déconnexion WiFi réussie');
await loadStatus();
+ // Force refresh WiFi list to remove green highlighting
+ if (appState.networks.length > 0) {
+ displayWifiNetworks(appState.networks);
+ }
} else {
throw new Error('Disconnection failed');
}
@@ -288,10 +298,22 @@ function updateStatusDisplay(status) {
appState.hotspotEnabled = status.hotspotEnabled;
+ // Check if connectedSSID changed and refresh WiFi list if needed
+ const connectedSSIDChanged = appState.connectedSSID !== status.connectedSSID;
+ appState.connectedSSID = status.connectedSSID;
+
+ if (connectedSSIDChanged && appState.networks.length > 0) {
+ displayWifiNetworks(appState.networks);
+ }
+
// Update stats
document.getElementById('connectedDevices').textContent = status.connectedCount;
document.getElementById('dataUsage').textContent = `${status.dataUsage.toFixed(1)} MB`;
+
+ // Update uptime in state (will be incremented by the interval)
+ appState.uptime = status.uptime;
document.getElementById('uptime').textContent = formatUptime(status.uptime);
+
document.getElementById('currentNetwork').textContent = status.connectedSSID || '-';
}
@@ -307,15 +329,21 @@ function displayWifiNetworks(networks) {
networks.forEach(network => {
const wifiItem = document.createElement('div');
wifiItem.className = 'wifi-item';
+
+ // Mark the currently connected network
+ if (appState.connectedSSID && network.ssid === appState.connectedSSID) {
+ wifiItem.classList.add('connected');
+ }
+
wifiItem.onclick = () => selectWifi(network, wifiItem);
wifiItem.innerHTML = `
@@ -533,3 +561,16 @@ function startPeriodicUpdates() {
loadDevices();
}, 10000);
}
+
+function startUptimeCounter() {
+ // Clear any existing interval
+ if (appState.uptimeInterval) {
+ clearInterval(appState.uptimeInterval);
+ }
+
+ // Increment uptime every second
+ appState.uptimeInterval = setInterval(() => {
+ appState.uptime++;
+ document.getElementById('uptime').textContent = formatUptime(appState.uptime);
+ }, 1000);
+}
diff --git a/cmd/repeater/static/style.css b/cmd/repeater/static/style.css
index 2dc82b0..e2de87c 100644
--- a/cmd/repeater/static/style.css
+++ b/cmd/repeater/static/style.css
@@ -130,6 +130,7 @@ body {
gap: 1rem;
align-items: center;
transition: all 0.3s ease;
+ max-width: 100%;
}
.stat-card:hover {
@@ -154,6 +155,7 @@ body {
.stat-content {
flex: 1;
+ min-width: 0;
}
.stat-value {
@@ -162,6 +164,9 @@ body {
color: var(--text-primary);
line-height: 1;
margin-bottom: 0.25rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.stat-label {
@@ -413,6 +418,16 @@ body {
border-left: 4px solid #667eea;
}
+.wifi-item.connected {
+ background: #d1fae5;
+ border-left-color: var(--success-color) !important;
+}
+
+.wifi-item.connected .wifi-ssid {
+ color: var(--success-color);
+ font-weight: 700;
+}
+
.wifi-item.loading {
justify-content: center;
color: var(--text-secondary);
diff --git a/internal/app/app.go b/internal/app/app.go
index e4d5d0e..3924f2a 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -3,6 +3,9 @@ package app
import (
"embed"
"log"
+ "os"
+ "strconv"
+ "strings"
"sync"
"time"
@@ -71,6 +74,52 @@ func (a *App) Shutdown() {
logging.AddLog("Système", "Application arrêtée")
}
+// getSystemUptime reads system uptime from /proc/uptime
+func getSystemUptime() int64 {
+ data, err := os.ReadFile("/proc/uptime")
+ if err != nil {
+ log.Printf("Error reading /proc/uptime: %v", err)
+ return 0
+ }
+
+ fields := strings.Fields(string(data))
+ if len(fields) == 0 {
+ return 0
+ }
+
+ uptime, err := strconv.ParseFloat(fields[0], 64)
+ if err != nil {
+ log.Printf("Error parsing uptime: %v", err)
+ return 0
+ }
+
+ return int64(uptime)
+}
+
+// getInterfaceBytes reads rx and tx bytes for a network interface
+func getInterfaceBytes(interfaceName string) (rxBytes, txBytes int64) {
+ rxPath := "/sys/class/net/" + interfaceName + "/statistics/rx_bytes"
+ txPath := "/sys/class/net/" + interfaceName + "/statistics/tx_bytes"
+
+ // Read RX bytes
+ rxData, err := os.ReadFile(rxPath)
+ if err != nil {
+ log.Printf("Error reading rx_bytes for %s: %v", interfaceName, err)
+ } else {
+ rxBytes, _ = strconv.ParseInt(strings.TrimSpace(string(rxData)), 10, 64)
+ }
+
+ // Read TX bytes
+ txData, err := os.ReadFile(txPath)
+ if err != nil {
+ log.Printf("Error reading tx_bytes for %s: %v", interfaceName, err)
+ } else {
+ txBytes, _ = strconv.ParseInt(strings.TrimSpace(string(txData)), 10, 64)
+ }
+
+ return rxBytes, txBytes
+}
+
// periodicStatusUpdate updates WiFi connection status periodically
func (a *App) periodicStatusUpdate() {
ticker := time.NewTicker(5 * time.Second)
@@ -79,10 +128,16 @@ func (a *App) periodicStatusUpdate() {
for range ticker.C {
a.StatusMutex.Lock()
a.Status.Connected = wifi.IsConnected()
- if !a.Status.Connected {
- a.Status.ConnectedSSID = ""
+ a.Status.ConnectedSSID = wifi.GetConnectedSSID()
+ a.Status.Uptime = getSystemUptime()
+
+ // Get network data usage for WiFi interface
+ if a.Config != nil {
+ rxBytes, txBytes := getInterfaceBytes(a.Config.WifiInterface)
+ // Convert to MB and sum rx + tx
+ a.Status.DataUsage = float64(rxBytes+txBytes) / (1024 * 1024)
}
- a.Status.Uptime = int64(time.Since(a.StartTime).Seconds())
+
a.StatusMutex.Unlock()
}
}
diff --git a/internal/wifi/iwd/agent.go b/internal/wifi/iwd/agent.go
new file mode 100644
index 0000000..3a53ba6
--- /dev/null
+++ b/internal/wifi/iwd/agent.go
@@ -0,0 +1,170 @@
+package iwd
+
+import (
+ "fmt"
+ "sync"
+
+ "github.com/godbus/dbus/v5"
+ "github.com/godbus/dbus/v5/introspect"
+)
+
+const agentIntrospectXML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+
+// Agent implements the net.connman.iwd.Agent interface for credential callbacks
+type Agent struct {
+ conn *dbus.Conn
+ path dbus.ObjectPath
+ passphraseStore map[string]string
+ mu sync.RWMutex
+}
+
+// NewAgent creates a new Agent instance
+func NewAgent(conn *dbus.Conn, path dbus.ObjectPath) *Agent {
+ return &Agent{
+ conn: conn,
+ path: path,
+ passphraseStore: make(map[string]string),
+ }
+}
+
+// SetPassphrase stores a passphrase for a given network SSID
+func (a *Agent) SetPassphrase(ssid, passphrase string) {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ a.passphraseStore[ssid] = passphrase
+}
+
+// ClearPassphrase removes the stored passphrase for a network
+func (a *Agent) ClearPassphrase(ssid string) {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ delete(a.passphraseStore, ssid)
+}
+
+// Export registers the agent object on D-Bus
+func (a *Agent) Export() error {
+ err := a.conn.Export(a, a.path, AgentInterface)
+ if err != nil {
+ return fmt.Errorf("failed to export agent: %v", err)
+ }
+
+ err = a.conn.Export(introspect.Introspectable(agentIntrospectXML), a.path, "org.freedesktop.DBus.Introspectable")
+ if err != nil {
+ a.conn.Export(nil, a.path, AgentInterface)
+ return fmt.Errorf("failed to export introspection: %v", err)
+ }
+
+ return nil
+}
+
+// Unexport unregisters the agent from D-Bus
+func (a *Agent) Unexport() {
+ a.conn.Export(nil, a.path, AgentInterface)
+ a.conn.Export(nil, a.path, "org.freedesktop.DBus.Introspectable")
+}
+
+// getNetworkSSID queries the network object to get its SSID (Name property)
+func (a *Agent) getNetworkSSID(networkPath dbus.ObjectPath) (string, error) {
+ obj := a.conn.Object(Service, networkPath)
+ variant, err := obj.GetProperty(NetworkInterface + ".Name")
+ if err != nil {
+ return "", fmt.Errorf("failed to get network name: %v", err)
+ }
+
+ name, ok := variant.Value().(string)
+ if !ok {
+ return "", fmt.Errorf("network name is not a string")
+ }
+
+ return name, nil
+}
+
+// RequestPassphrase is called by iwd when connecting to PSK networks
+func (a *Agent) RequestPassphrase(network dbus.ObjectPath) (string, *dbus.Error) {
+ fmt.Printf("[Agent] RequestPassphrase called for network: %s\n", network)
+
+ ssid, err := a.getNetworkSSID(network)
+ if err != nil {
+ fmt.Printf("[Agent] Failed to get SSID: %v\n", err)
+ return "", dbus.MakeFailedError(fmt.Errorf("failed to get network SSID: %v", err))
+ }
+
+ fmt.Printf("[Agent] Network SSID: %s\n", ssid)
+
+ a.mu.RLock()
+ passphrase, ok := a.passphraseStore[ssid]
+ a.mu.RUnlock()
+
+ if !ok {
+ fmt.Printf("[Agent] No passphrase stored for SSID: %s\n", ssid)
+ return "", dbus.MakeFailedError(fmt.Errorf("no passphrase stored for network '%s'", ssid))
+ }
+
+ fmt.Printf("[Agent] Returning passphrase for SSID: %s\n", ssid)
+ return passphrase, nil
+}
+
+// RequestPrivateKeyPassphrase is called for encrypted private keys
+func (a *Agent) RequestPrivateKeyPassphrase(network dbus.ObjectPath) (string, *dbus.Error) {
+ // Not implemented for now
+ return "", dbus.MakeFailedError(fmt.Errorf("RequestPrivateKeyPassphrase not implemented"))
+}
+
+// RequestUserNameAndPassword is called for enterprise networks
+func (a *Agent) RequestUserNameAndPassword(network dbus.ObjectPath) (string, string, *dbus.Error) {
+ // Not implemented for now
+ return "", "", dbus.MakeFailedError(fmt.Errorf("RequestUserNameAndPassword not implemented"))
+}
+
+// RequestUserPassword is called for enterprise networks with known username
+func (a *Agent) RequestUserPassword(network dbus.ObjectPath, user string) (string, *dbus.Error) {
+ // Not implemented for now
+ return "", dbus.MakeFailedError(fmt.Errorf("RequestUserPassword not implemented"))
+}
+
+// Cancel is called when a request is canceled
+func (a *Agent) Cancel(reason string) *dbus.Error {
+ // Nothing to do, just acknowledge
+ return nil
+}
+
+// Release is called when the agent is unregistered
+func (a *Agent) Release() *dbus.Error {
+ // Cleanup if needed
+ a.mu.Lock()
+ a.passphraseStore = make(map[string]string)
+ a.mu.Unlock()
+ return nil
+}
diff --git a/internal/wifi/iwd/agentmanager.go b/internal/wifi/iwd/agentmanager.go
new file mode 100644
index 0000000..7ea6c77
--- /dev/null
+++ b/internal/wifi/iwd/agentmanager.go
@@ -0,0 +1,39 @@
+package iwd
+
+import (
+ "fmt"
+
+ "github.com/godbus/dbus/v5"
+)
+
+// AgentManager handles agent registration with iwd
+type AgentManager struct {
+ conn *dbus.Conn
+ obj dbus.BusObject
+}
+
+// NewAgentManager creates a new AgentManager instance
+func NewAgentManager(conn *dbus.Conn) *AgentManager {
+ return &AgentManager{
+ conn: conn,
+ obj: conn.Object(Service, "/net/connman/iwd"),
+ }
+}
+
+// RegisterAgent registers an agent with iwd
+func (am *AgentManager) RegisterAgent(agentPath dbus.ObjectPath) error {
+ err := am.obj.Call(AgentManagerInterface+".RegisterAgent", 0, agentPath).Err
+ if err != nil {
+ return fmt.Errorf("failed to register agent: %v", err)
+ }
+ return nil
+}
+
+// UnregisterAgent unregisters an agent from iwd
+func (am *AgentManager) UnregisterAgent(agentPath dbus.ObjectPath) error {
+ err := am.obj.Call(AgentManagerInterface+".UnregisterAgent", 0, agentPath).Err
+ if err != nil {
+ return fmt.Errorf("failed to unregister agent: %v", err)
+ }
+ return nil
+}
diff --git a/internal/wifi/iwd/manager.go b/internal/wifi/iwd/manager.go
new file mode 100644
index 0000000..f636664
--- /dev/null
+++ b/internal/wifi/iwd/manager.go
@@ -0,0 +1,71 @@
+package iwd
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/godbus/dbus/v5"
+)
+
+// Manager handles iwd object discovery via ObjectManager
+type Manager struct {
+ conn *dbus.Conn
+ obj dbus.BusObject
+}
+
+// NewManager creates a new Manager instance
+func NewManager(conn *dbus.Conn) *Manager {
+ return &Manager{
+ conn: conn,
+ obj: conn.Object(Service, dbus.ObjectPath(ManagerPath)),
+ }
+}
+
+// GetManagedObjects returns all iwd managed objects
+func (m *Manager) GetManagedObjects() (map[dbus.ObjectPath]map[string]map[string]dbus.Variant, error) {
+ var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
+ err := m.obj.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&objects)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get managed objects: %v", err)
+ }
+ return objects, nil
+}
+
+// FindStation finds the Station object for the given interface name
+func (m *Manager) FindStation(interfaceName string) (*Station, error) {
+ objects, err := m.GetManagedObjects()
+ if err != nil {
+ return nil, err
+ }
+
+ // First, find the device with matching interface name
+ var devicePath dbus.ObjectPath
+ for path, interfaces := range objects {
+ if deviceProps, ok := interfaces[DeviceInterface]; ok {
+ if nameVariant, ok := deviceProps["Name"]; ok {
+ if name, ok := nameVariant.Value().(string); ok && name == interfaceName {
+ devicePath = path
+ break
+ }
+ }
+ }
+ }
+
+ if devicePath == "" {
+ return nil, fmt.Errorf("device with interface '%s' not found", interfaceName)
+ }
+
+ // Now find the station object under this device
+ // Station path is typically the same as device path or a child of it
+ for path, interfaces := range objects {
+ if _, ok := interfaces[StationInterface]; ok {
+ // Check if this station belongs to our device
+ // Station path should be the device path or start with it
+ if path == devicePath || strings.HasPrefix(string(path), string(devicePath)+"/") {
+ return NewStation(m.conn, path), nil
+ }
+ }
+ }
+
+ return nil, fmt.Errorf("station for device '%s' not found", interfaceName)
+}
diff --git a/internal/wifi/iwd/network.go b/internal/wifi/iwd/network.go
new file mode 100644
index 0000000..1563a95
--- /dev/null
+++ b/internal/wifi/iwd/network.go
@@ -0,0 +1,64 @@
+package iwd
+
+import (
+ "fmt"
+
+ "github.com/godbus/dbus/v5"
+)
+
+// Network represents an iwd Network interface
+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),
+ }
+}
+
+// GetProperties retrieves all network properties
+func (n *Network) GetProperties() (*NetworkProperties, error) {
+ var props map[string]dbus.Variant
+ err := n.obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, NetworkInterface).Store(&props)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get network properties: %v", err)
+ }
+
+ netProps := &NetworkProperties{}
+
+ if nameVariant, ok := props["Name"]; ok {
+ if name, ok := nameVariant.Value().(string); ok {
+ netProps.Name = name
+ }
+ }
+
+ if typeVariant, ok := props["Type"]; ok {
+ if netType, ok := typeVariant.Value().(string); ok {
+ netProps.Type = netType
+ }
+ }
+
+ if connectedVariant, ok := props["Connected"]; ok {
+ if connected, ok := connectedVariant.Value().(bool); ok {
+ netProps.Connected = connected
+ }
+ }
+
+ return netProps, nil
+}
+
+// Connect initiates a connection to this network
+// Credentials are provided via the registered agent's RequestPassphrase callback
+func (n *Network) Connect() error {
+ err := n.obj.Call(NetworkInterface+".Connect", 0).Err
+ if err != nil {
+ return fmt.Errorf("connect failed: %v", err)
+ }
+ return nil
+}
diff --git a/internal/wifi/iwd/station.go b/internal/wifi/iwd/station.go
new file mode 100644
index 0000000..7abed90
--- /dev/null
+++ b/internal/wifi/iwd/station.go
@@ -0,0 +1,137 @@
+package iwd
+
+import (
+ "fmt"
+
+ "github.com/godbus/dbus/v5"
+)
+
+// Station represents an iwd Station interface
+type Station struct {
+ path dbus.ObjectPath
+ conn *dbus.Conn
+ obj dbus.BusObject
+}
+
+// NewStation creates a new Station instance
+func NewStation(conn *dbus.Conn, path dbus.ObjectPath) *Station {
+ return &Station{
+ path: path,
+ conn: conn,
+ obj: conn.Object(Service, path),
+ }
+}
+
+// Scan triggers a network scan
+func (s *Station) Scan() error {
+ err := s.obj.Call(StationInterface+".Scan", 0).Err
+ if err != nil {
+ return fmt.Errorf("scan failed: %v", err)
+ }
+ return nil
+}
+
+// IsScanning checks if a scan is currently in progress
+func (s *Station) IsScanning() (bool, error) {
+ prop, err := s.obj.GetProperty(StationInterface + ".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
+}
+
+// GetOrderedNetworks returns networks sorted by signal strength
+func (s *Station) GetOrderedNetworks() ([]NetworkInfo, error) {
+ var result []struct {
+ Path dbus.ObjectPath
+ Signal int16
+ }
+
+ err := s.obj.Call(StationInterface+".GetOrderedNetworks", 0).Store(&result)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get ordered networks: %v", err)
+ }
+
+ networks := make([]NetworkInfo, len(result))
+ for i, r := range result {
+ networks[i] = NetworkInfo{
+ Path: r.Path,
+ Signal: r.Signal,
+ }
+ }
+
+ return networks, nil
+}
+
+// GetState returns the current connection state
+func (s *Station) GetState() (StationState, error) {
+ prop, err := s.obj.GetProperty(StationInterface + ".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 StationState(state), nil
+}
+
+// Disconnect disconnects from the current network
+func (s *Station) Disconnect() error {
+ err := s.obj.Call(StationInterface+".Disconnect", 0).Err
+ if err != nil {
+ return fmt.Errorf("disconnect failed: %v", err)
+ }
+ return nil
+}
+
+// GetNetwork finds and returns a Network object by SSID
+func (s *Station) GetNetwork(ssid string) (*Network, error) {
+ networks, err := s.GetOrderedNetworks()
+ if err != nil {
+ return nil, err
+ }
+
+ // Find the network with matching SSID
+ for _, netInfo := range networks {
+ network := NewNetwork(s.conn, netInfo.Path)
+ props, err := network.GetProperties()
+ if err != nil {
+ continue
+ }
+
+ if props.Name == ssid {
+ return network, nil
+ }
+ }
+
+ return nil, fmt.Errorf("network '%s' not found", ssid)
+}
+
+// GetConnectedNetwork returns the currently connected network
+func (s *Station) GetConnectedNetwork() (*Network, error) {
+ prop, err := s.obj.GetProperty(StationInterface + ".ConnectedNetwork")
+ if err != nil {
+ return nil, fmt.Errorf("failed to get ConnectedNetwork property: %v", err)
+ }
+
+ path, ok := prop.Value().(dbus.ObjectPath)
+ if !ok {
+ return nil, fmt.Errorf("ConnectedNetwork property is not an ObjectPath")
+ }
+
+ // Check if path is empty (not connected)
+ if path == "/" || path == "" {
+ return nil, fmt.Errorf("not connected to any network")
+ }
+
+ return NewNetwork(s.conn, path), nil
+}
diff --git a/internal/wifi/iwd/types.go b/internal/wifi/iwd/types.go
new file mode 100644
index 0000000..76624c4
--- /dev/null
+++ b/internal/wifi/iwd/types.go
@@ -0,0 +1,38 @@
+package iwd
+
+import "github.com/godbus/dbus/v5"
+
+const (
+ // D-Bus service and interfaces
+ Service = "net.connman.iwd"
+ ManagerPath = "/"
+ DeviceInterface = "net.connman.iwd.Device"
+ StationInterface = "net.connman.iwd.Station"
+ NetworkInterface = "net.connman.iwd.Network"
+ AgentInterface = "net.connman.iwd.Agent"
+ AgentManagerInterface = "net.connman.iwd.AgentManager"
+)
+
+// NetworkInfo represents a network with its signal strength
+type NetworkInfo struct {
+ Path dbus.ObjectPath
+ Signal int16 // 100 * dBm (0 to -10000)
+}
+
+// NetworkProperties holds network properties
+type NetworkProperties struct {
+ Name string // SSID
+ Type string // "open", "wep", "psk", "8021x"
+ Connected bool
+}
+
+// StationState represents the connection state
+type StationState string
+
+const (
+ StateConnected StationState = "connected"
+ StateDisconnected StationState = "disconnected"
+ StateConnecting StationState = "connecting"
+ StateDisconnecting StationState = "disconnecting"
+ StateRoaming StationState = "roaming"
+)
diff --git a/internal/wifi/wifi.go b/internal/wifi/wifi.go
index e7cb20d..5dd6656 100644
--- a/internal/wifi/wifi.go
+++ b/internal/wifi/wifi.go
@@ -2,48 +2,67 @@ package wifi
import (
"fmt"
- "os/exec"
"sort"
"strings"
"time"
"github.com/godbus/dbus/v5"
"github.com/nemunaire/repeater/internal/models"
+ "github.com/nemunaire/repeater/internal/wifi/iwd"
)
const (
- WPA_CONF = "/etc/wpa_supplicant/wpa_supplicant.conf"
-
- // D-Bus constants for wpa_supplicant
- WPA_SUPPLICANT_SERVICE = "fi.w1.wpa_supplicant1"
- WPA_SUPPLICANT_PATH = "/fi/w1/wpa_supplicant1"
- WPA_SUPPLICANT_IFACE = "fi.w1.wpa_supplicant1"
- WPA_INTERFACE_IFACE = "fi.w1.wpa_supplicant1.Interface"
- WPA_BSS_IFACE = "fi.w1.wpa_supplicant1.BSS"
- WPA_NETWORK_IFACE = "fi.w1.wpa_supplicant1.Network"
+ AGENT_PATH = "/com/github/nemunaire/repeater/agent"
)
var (
wlanInterface string
dbusConn *dbus.Conn
- wpaSupplicant dbus.BusObject
+ iwdManager *iwd.Manager
+ station *iwd.Station
+ agent *iwd.Agent
+ agentManager *iwd.AgentManager
)
-// Initialize initializes the WiFi service with D-Bus connection
+// Initialize initializes the WiFi service with iwd D-Bus connection
func Initialize(interfaceName string) error {
wlanInterface = interfaceName
var err error
+
+ // Connect to D-Bus
dbusConn, err = dbus.SystemBus()
if err != nil {
- return fmt.Errorf("failed to connect to D-Bus: %v", err)
+ return fmt.Errorf("échec de connexion à D-Bus: %v", 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)
}
- wpaSupplicant = dbusConn.Object(WPA_SUPPLICANT_SERVICE, dbus.ObjectPath(WPA_SUPPLICANT_PATH))
return nil
}
-// Close closes the D-Bus connection
+// Close closes the D-Bus connection and unregisters the agent
func Close() {
+ if agentManager != nil && agent != nil {
+ agentManager.UnregisterAgent(dbus.ObjectPath(AGENT_PATH))
+ agent.Unexport()
+ }
if dbusConn != nil {
dbusConn.Close()
}
@@ -51,100 +70,52 @@ func Close() {
// ScanNetworks scans for available WiFi networks
func ScanNetworks() ([]models.WiFiNetwork, error) {
- interfacePath, err := getWiFiInterfacePath()
- if err != nil {
- return nil, fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err)
- }
-
- wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath)
-
- // Check current scanning state
- scanning, err := wifiInterface.GetProperty(WPA_INTERFACE_IFACE + ".Scanning")
- if err == nil && scanning.Value().(bool) {
- // Scan already in progress, wait for it to complete
+ // Check if already scanning
+ scanning, err := station.IsScanning()
+ if err == nil && scanning {
time.Sleep(3 * time.Second)
} else {
- // Trigger a scan
- call := wifiInterface.Call(WPA_INTERFACE_IFACE+".Scan", 0, map[string]dbus.Variant{"Type": dbus.MakeVariant("active")})
- if call.Err != nil {
- // If scan is rejected, it might be too soon after a previous scan
- // Try to use cached results instead
- if strings.Contains(call.Err.Error(), "rejected") {
- // Continue to retrieve existing BSS list
- } else {
- return nil, fmt.Errorf("erreur lors du scan: %v", call.Err)
- }
- } else {
- // Wait for scan to complete
- time.Sleep(2 * time.Second)
+ // Trigger scan
+ err := station.Scan()
+ if err != nil && !strings.Contains(err.Error(), "rejected") {
+ return nil, fmt.Errorf("erreur lors du scan: %v", err)
}
+ time.Sleep(2 * time.Second)
}
- // Retrieve BSS list
- bssePaths, err := wifiInterface.GetProperty(WPA_INTERFACE_IFACE + ".BSSs")
+ // Get ordered networks
+ networkInfos, err := station.GetOrderedNetworks()
if err != nil {
- return nil, fmt.Errorf("erreur lors de la récupération des BSS: %v", err)
+ 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 _, bssPath := range bssePaths.Value().([]dbus.ObjectPath) {
- bss := dbusConn.Object(WPA_SUPPLICANT_SERVICE, bssPath)
-
- // Get BSS properties
- var props map[string]dbus.Variant
- err = bss.Call("org.freedesktop.DBus.Properties.GetAll", 0, WPA_BSS_IFACE).Store(&props)
+ for _, netInfo := range networkInfos {
+ network := iwd.NewNetwork(dbusConn, netInfo.Path)
+ props, err := network.GetProperties()
if err != nil {
continue
}
- network := models.WiFiNetwork{}
-
- // Extract SSID
- if ssidBytes, ok := props["SSID"].Value().([]byte); ok {
- network.SSID = string(ssidBytes)
- }
-
- // Skip duplicates and empty SSIDs
- if network.SSID == "" || seenSSIDs[network.SSID] {
+ if props.Name == "" || seenSSIDs[props.Name] {
continue
}
- seenSSIDs[network.SSID] = true
+ seenSSIDs[props.Name] = true
- // Extract BSSID
- if bssidBytes, ok := props["BSSID"].Value().([]byte); ok {
- network.BSSID = fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x",
- bssidBytes[0], bssidBytes[1], bssidBytes[2], bssidBytes[3], bssidBytes[4], bssidBytes[5])
+ wifiNet := models.WiFiNetwork{
+ SSID: props.Name,
+ Signal: signalToStrength(int(netInfo.Signal) / 100),
+ Security: mapSecurityType(props.Type),
+ BSSID: generateSyntheticBSSID(props.Name),
+ Channel: 0,
}
- // Extract signal strength
- if signal, ok := props["Signal"].Value().(int16); ok {
- network.Signal = signalToStrength(int(signal))
- }
-
- // Extract frequency and calculate channel
- if frequency, ok := props["Frequency"].Value().(uint16); ok {
- network.Channel = frequencyToChannel(int(frequency))
- }
-
- // Determine security
- if privacyVal, ok := props["Privacy"].Value().(bool); ok && privacyVal {
- if wpaProps, ok := props["WPA"].Value().(map[string]dbus.Variant); ok && len(wpaProps) > 0 {
- network.Security = "WPA"
- } else if rsnProps, ok := props["RSN"].Value().(map[string]dbus.Variant); ok && len(rsnProps) > 0 {
- network.Security = "WPA2"
- } else {
- network.Security = "WEP"
- }
- } else {
- network.Security = "Open"
- }
-
- networks = append(networks, network)
+ networks = append(networks, wifiNet)
}
- // Sort by signal strength
+ // Sort by signal strength (descending)
sort.Slice(networks, func(i, j int) bool {
return networks[i].Signal > networks[j].Signal
})
@@ -152,37 +123,32 @@ func ScanNetworks() ([]models.WiFiNetwork, error) {
return networks, nil
}
-// Connect connects to a WiFi network using D-Bus
+// Connect connects to a WiFi network using iwd agent callback
func Connect(ssid, password string) error {
- interfacePath, err := getWiFiInterfacePath()
- if err != nil {
- return fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err)
- }
-
- wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath)
-
- // Create a new network
- networkConfig := map[string]dbus.Variant{
- "ssid": dbus.MakeVariant(ssid),
- }
-
+ // Store passphrase in agent for callback
if password != "" {
- networkConfig["psk"] = dbus.MakeVariant(password)
+ agent.SetPassphrase(ssid, password)
}
- var networkPath dbus.ObjectPath
- err = wifiInterface.Call(WPA_INTERFACE_IFACE+".AddNetwork", 0, networkConfig).Store(&networkPath)
+ // 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("erreur lors de l'ajout du réseau: %v", err)
+ return fmt.Errorf("réseau '%s' non trouvé: %v", ssid, err)
}
- // Select the network
- err = wifiInterface.Call(WPA_INTERFACE_IFACE+".SelectNetwork", 0, networkPath).Err
- if err != nil {
- return fmt.Errorf("erreur lors de la sélection du réseau: %v", 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)
}
- // Wait for connection
+ // Poll for connection
for i := 0; i < 20; i++ {
time.Sleep(500 * time.Millisecond)
if IsConnected() {
@@ -195,81 +161,69 @@ func Connect(ssid, password string) error {
// Disconnect disconnects from the current WiFi network
func Disconnect() error {
- interfacePath, err := getWiFiInterfacePath()
- if err != nil {
- return fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err)
- }
-
- wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath)
-
- // Disconnect
- err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Disconnect", 0).Err
- if err != nil {
+ if err := station.Disconnect(); err != nil {
return fmt.Errorf("erreur lors de la déconnexion: %v", err)
}
-
- // Remove all networks
- var networks []dbus.ObjectPath
- err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Get", 0, WPA_INTERFACE_IFACE, "Networks").Store(&networks)
- if err == nil {
- for _, networkPath := range networks {
- wifiInterface.Call(WPA_INTERFACE_IFACE+".RemoveNetwork", 0, networkPath)
- }
- }
-
return nil
}
-// IsConnected checks if WiFi is connected using D-Bus
+// IsConnected checks if WiFi is connected using iwd
func IsConnected() bool {
- interfacePath, err := getWiFiInterfacePath()
+ state, err := station.GetState()
if err != nil {
return false
}
-
- wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath)
- var state string
- err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Get", 0, WPA_INTERFACE_IFACE, "State").Store(&state)
- if err != nil {
- return false
- }
-
- return state == "completed"
+ return state == iwd.StateConnected
}
-// IsConnectedLegacy checks if WiFi is connected using iwconfig (fallback)
-func IsConnectedLegacy() bool {
- cmd := exec.Command("iwconfig", wlanInterface)
- output, err := cmd.Output()
+// GetConnectedSSID returns the SSID of the currently connected network
+func GetConnectedSSID() string {
+ network, err := station.GetConnectedNetwork()
if err != nil {
- return false
+ return ""
}
- return strings.Contains(string(output), "Access Point:")
+ props, err := network.GetProperties()
+ if err != nil {
+ return ""
+ }
+
+ return props.Name
}
-// getWiFiInterfacePath retrieves the D-Bus path for the WiFi interface
-func getWiFiInterfacePath() (dbus.ObjectPath, error) {
- var interfacePath dbus.ObjectPath
- err := wpaSupplicant.Call(WPA_SUPPLICANT_IFACE+".GetInterface", 0, wlanInterface).Store(&interfacePath)
- if err != nil {
- return "", fmt.Errorf("erreur lors de la récupération des interfaces: %v", err)
+// mapSecurityType maps iwd security types to display format
+func mapSecurityType(iwdType string) string {
+ switch iwdType {
+ case "open":
+ return "Open"
+ case "wep":
+ return "WEP"
+ case "psk":
+ return "WPA2"
+ case "8021x":
+ return "WPA2"
+ default:
+ return "WPA2"
}
-
- return interfacePath, nil
}
-// frequencyToChannel converts WiFi frequency to channel number
-func frequencyToChannel(frequency int) int {
- if frequency >= 2412 && frequency <= 2484 {
- if frequency == 2484 {
- return 14
- }
- return (frequency-2412)/5 + 1
- } else if frequency >= 5170 && frequency <= 5825 {
- return (frequency - 5000) / 5
+// 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)
}
- return 0
+
+ // 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)