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

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