Implementation with iwd

This commit is contained in:
nemunaire 2026-01-01 17:04:21 +07:00
commit 17d665e21a
10 changed files with 755 additions and 171 deletions

View file

@ -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 = `
<div class="wifi-info">
<div class="wifi-ssid">${escapeHtml(network.ssid)}</div>
<div class="wifi-details">
<span>${network.security}</span>
<span>Canal ${network.channel}</span>
<span>${network.bssid}</span>
<span>${escapeHtml(network.security)}</span>
<span>Canal ${escapeHtml(String(network.channel))}</span>
<span>${escapeHtml(network.bssid)}</span>
</div>
</div>
<div class="wifi-signal">
@ -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);
}

View file

@ -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);

View file

@ -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()
}
}

170
internal/wifi/iwd/agent.go Normal file
View file

@ -0,0 +1,170 @@
package iwd
import (
"fmt"
"sync"
"github.com/godbus/dbus/v5"
"github.com/godbus/dbus/v5/introspect"
)
const agentIntrospectXML = `
<node>
<interface name="net.connman.iwd.Agent">
<method name="Release">
</method>
<method name="RequestPassphrase">
<arg name="network" type="o" direction="in"/>
<arg name="passphrase" type="s" direction="out"/>
</method>
<method name="RequestPrivateKeyPassphrase">
<arg name="network" type="o" direction="in"/>
<arg name="passphrase" type="s" direction="out"/>
</method>
<method name="RequestUserNameAndPassword">
<arg name="network" type="o" direction="in"/>
<arg name="username" type="s" direction="out"/>
<arg name="password" type="s" direction="out"/>
</method>
<method name="RequestUserPassword">
<arg name="network" type="o" direction="in"/>
<arg name="user" type="s" direction="in"/>
<arg name="password" type="s" direction="out"/>
</method>
<method name="Cancel">
<arg name="reason" type="s" direction="in"/>
</method>
</interface>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="xml" type="s" direction="out"/>
</method>
</interface>
</node>`
// 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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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"
)

View file

@ -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)