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

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