1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-10 07:25:37 -05:00
Files
DankMaterialShell/backend/internal/server/network/agent_networkmanager.go
2025-11-12 17:18:45 -05:00

529 lines
17 KiB
Go

package network
import (
"context"
"errors"
"fmt"
"time"
"github.com/AvengeMedia/DankMaterialShell/backend/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/backend/internal/log"
"github.com/godbus/dbus/v5"
)
const (
nmAgentManagerPath = "/org/freedesktop/NetworkManager/AgentManager"
nmAgentManagerIface = "org.freedesktop.NetworkManager.AgentManager"
nmSecretAgentIface = "org.freedesktop.NetworkManager.SecretAgent"
agentObjectPath = "/org/freedesktop/NetworkManager/SecretAgent"
agentIdentifier = "com.danklinux.NMAgent"
)
type SecretAgent struct {
conn *dbus.Conn
objPath dbus.ObjectPath
id string
prompts PromptBroker
manager *Manager
backend *NetworkManagerBackend
}
type nmVariantMap map[string]dbus.Variant
type nmSettingMap map[string]nmVariantMap
const introspectXML = `
<node>
<interface name="org.freedesktop.NetworkManager.SecretAgent">
<method name="GetSecrets">
<arg type="a{sa{sv}}" name="connection" direction="in"/>
<arg type="o" name="connection_path" direction="in"/>
<arg type="s" name="setting_name" direction="in"/>
<arg type="as" name="hints" direction="in"/>
<arg type="u" name="flags" direction="in"/>
<arg type="a{sa{sv}}" name="secrets" direction="out"/>
</method>
<method name="DeleteSecrets">
<arg type="a{sa{sv}}" name="connection" direction="in"/>
<arg type="o" name="connection_path" direction="in"/>
</method>
<method name="DeleteSecrets2">
<arg type="o" name="connection_path" direction="in"/>
<arg type="s" name="setting" direction="in"/>
</method>
<method name="CancelGetSecrets">
<arg type="o" name="connection_path" direction="in"/>
<arg type="s" name="setting_name" direction="in"/>
</method>
</interface>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="data" type="s" direction="out"/>
</method>
</interface>
</node>`
func NewSecretAgent(prompts PromptBroker, manager *Manager, backend *NetworkManagerBackend) (*SecretAgent, error) {
c, err := dbus.ConnectSystemBus()
if err != nil {
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
}
sa := &SecretAgent{
conn: c,
objPath: dbus.ObjectPath(agentObjectPath),
id: agentIdentifier,
prompts: prompts,
manager: manager,
backend: backend,
}
if err := c.Export(sa, sa.objPath, nmSecretAgentIface); err != nil {
c.Close()
return nil, fmt.Errorf("failed to export secret agent: %w", err)
}
if err := c.Export(sa, sa.objPath, "org.freedesktop.DBus.Introspectable"); err != nil {
c.Close()
return nil, fmt.Errorf("failed to export introspection: %w", err)
}
mgr := c.Object("org.freedesktop.NetworkManager", dbus.ObjectPath(nmAgentManagerPath))
call := mgr.Call(nmAgentManagerIface+".Register", 0, sa.id)
if call.Err != nil {
c.Close()
return nil, fmt.Errorf("failed to register agent with NetworkManager: %w", call.Err)
}
log.Infof("[SecretAgent] Registered with NetworkManager (id=%s, unique name=%s, fixed path=%s)", sa.id, c.Names()[0], sa.objPath)
return sa, nil
}
func (a *SecretAgent) Close() {
if a.conn != nil {
mgr := a.conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath(nmAgentManagerPath))
mgr.Call(nmAgentManagerIface+".Unregister", 0, a.id)
a.conn.Close()
}
}
func (a *SecretAgent) GetSecrets(
conn map[string]nmVariantMap,
path dbus.ObjectPath,
settingName string,
hints []string,
flags uint32,
) (nmSettingMap, *dbus.Error) {
log.Infof("[SecretAgent] GetSecrets called: path=%s, setting=%s, hints=%v, flags=%d",
path, settingName, hints, flags)
const (
NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION = 0x1
NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW = 0x2
NM_SECRET_AGENT_GET_SECRETS_FLAG_USER_REQUESTED = 0x4
)
connType, displayName, vpnSvc := readConnTypeAndName(conn)
ssid := readSSID(conn)
fields := fieldsNeeded(settingName, hints)
log.Infof("[SecretAgent] connType=%s, name=%s, vpnSvc=%s, fields=%v, flags=%d", connType, displayName, vpnSvc, fields, flags)
if a.backend != nil {
a.backend.stateMutex.RLock()
isConnecting := a.backend.state.IsConnecting
connectingSSID := a.backend.state.ConnectingSSID
isConnectingVPN := a.backend.state.IsConnectingVPN
connectingVPNUUID := a.backend.state.ConnectingVPNUUID
a.backend.stateMutex.RUnlock()
switch connType {
case "802-11-wireless":
// If we're connecting to a WiFi network, only respond if it's the one we're connecting to
if isConnecting && connectingSSID != ssid {
log.Infof("[SecretAgent] Ignoring WiFi request for SSID '%s' - we're connecting to '%s'", ssid, connectingSSID)
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
}
case "vpn", "wireguard":
var connUuid string
if c, ok := conn["connection"]; ok {
if v, ok := c["uuid"]; ok {
if s, ok2 := v.Value().(string); ok2 {
connUuid = s
}
}
}
// If we're connecting to a VPN, only respond if it's the one we're connecting to
// This prevents interfering with nmcli/other tools when our app isn't connecting
if isConnectingVPN && connUuid != connectingVPNUUID {
log.Infof("[SecretAgent] Ignoring VPN request for UUID '%s' - we're connecting to '%s'", connUuid, connectingVPNUUID)
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
}
}
}
if len(fields) == 0 {
// For VPN connections with no hints, we can't provide a proper UI.
// Defer to other agents (like nm-applet or VPN-specific auth dialogs)
// that can handle the VPN type properly (e.g., OpenConnect with SAML, etc.)
if settingName == "vpn" {
log.Infof("[SecretAgent] VPN with empty hints - deferring to other agents for %s", vpnSvc)
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
}
const (
NM_SETTING_SECRET_FLAG_NONE = 0
NM_SETTING_SECRET_FLAG_AGENT_OWNED = 1
NM_SETTING_SECRET_FLAG_NOT_SAVED = 2
NM_SETTING_SECRET_FLAG_NOT_REQUIRED = 4
)
var passwordFlags uint32 = 0xFFFF
switch settingName {
case "802-11-wireless-security":
if wifiSecSettings, ok := conn["802-11-wireless-security"]; ok {
if flagsVariant, ok := wifiSecSettings["psk-flags"]; ok {
if pwdFlags, ok := flagsVariant.Value().(uint32); ok {
passwordFlags = pwdFlags
}
}
}
case "802-1x":
if dot1xSettings, ok := conn["802-1x"]; ok {
if flagsVariant, ok := dot1xSettings["password-flags"]; ok {
if pwdFlags, ok := flagsVariant.Value().(uint32); ok {
passwordFlags = pwdFlags
}
}
}
}
if passwordFlags == 0xFFFF {
log.Warnf("[SecretAgent] Could not determine password-flags for empty hints - returning NoSecrets error")
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
} else if passwordFlags&NM_SETTING_SECRET_FLAG_NOT_REQUIRED != 0 {
log.Infof("[SecretAgent] Secrets not required (flags=%d)", passwordFlags)
out := nmSettingMap{}
out[settingName] = nmVariantMap{}
return out, nil
} else if passwordFlags&NM_SETTING_SECRET_FLAG_AGENT_OWNED != 0 {
log.Warnf("[SecretAgent] Secrets are agent-owned but we don't store secrets (flags=%d) - returning NoSecrets error", passwordFlags)
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
} else {
log.Infof("[SecretAgent] No secrets needed, using system stored secrets (flags=%d)", passwordFlags)
out := nmSettingMap{}
out[settingName] = nmVariantMap{}
return out, nil
}
}
reason := reasonFromFlags(flags)
if a.manager != nil && connType == "802-11-wireless" && a.manager.WasRecentlyFailed(ssid) {
reason = "wrong-password"
}
var connId, connUuid string
if c, ok := conn["connection"]; ok {
if v, ok := c["id"]; ok {
if s, ok2 := v.Value().(string); ok2 {
connId = s
}
}
if v, ok := c["uuid"]; ok {
if s, ok2 := v.Value().(string); ok2 {
connUuid = s
}
}
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
token, err := a.prompts.Ask(ctx, PromptRequest{
Name: displayName,
SSID: ssid,
ConnType: connType,
VpnService: vpnSvc,
SettingName: settingName,
Fields: fields,
Hints: hints,
Reason: reason,
ConnectionId: connId,
ConnectionUuid: connUuid,
ConnectionPath: string(path),
})
if err != nil {
log.Warnf("[SecretAgent] Failed to create prompt: %v", err)
return nil, dbus.MakeFailedError(err)
}
log.Infof("[SecretAgent] Waiting for user input (token=%s)", token)
reply, err := a.prompts.Wait(ctx, token)
if err != nil {
log.Warnf("[SecretAgent] Prompt failed or cancelled: %v", err)
// Clear connecting state immediately on cancellation
if a.backend != nil {
a.backend.stateMutex.Lock()
wasConnecting := a.backend.state.IsConnecting
wasConnectingVPN := a.backend.state.IsConnectingVPN
cancelledSSID := a.backend.state.ConnectingSSID
if wasConnecting || wasConnectingVPN {
log.Infof("[SecretAgent] Clearing connecting state due to cancelled prompt")
a.backend.state.IsConnecting = false
a.backend.state.ConnectingSSID = ""
a.backend.state.IsConnectingVPN = false
a.backend.state.ConnectingVPNUUID = ""
}
a.backend.stateMutex.Unlock()
// If this was a WiFi connection that was just cancelled, remove the connection profile
// (it was created with AddConnection but activation was cancelled)
if wasConnecting && cancelledSSID != "" && connType == "802-11-wireless" {
log.Infof("[SecretAgent] Removing connection profile for cancelled WiFi connection: %s", cancelledSSID)
if err := a.backend.ForgetWiFiNetwork(cancelledSSID); err != nil {
log.Warnf("[SecretAgent] Failed to remove cancelled connection profile: %v", err)
}
}
if (wasConnecting || wasConnectingVPN) && a.backend.onStateChange != nil {
a.backend.onStateChange()
}
}
if reply.Cancel || errors.Is(err, errdefs.ErrSecretPromptCancelled) {
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.UserCanceled", nil)
}
if errors.Is(err, errdefs.ErrSecretPromptTimeout) {
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.Failed", nil)
}
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.Failed", nil)
}
log.Infof("[SecretAgent] User provided secrets, save=%v", reply.Save)
out := nmSettingMap{}
sec := nmVariantMap{}
for k, v := range reply.Secrets {
sec[k] = dbus.MakeVariant(v)
}
out[settingName] = sec
switch settingName {
case "802-1x":
log.Infof("[SecretAgent] Returning 802-1x enterprise secrets with %d fields", len(sec))
case "vpn":
log.Infof("[SecretAgent] Returning VPN secrets with %d fields for %s", len(sec), vpnSvc)
}
// If save=true, persist secrets in background after returning to NetworkManager
// This MUST happen after we return secrets, in a goroutine
if reply.Save {
go func() {
log.Infof("[SecretAgent] Persisting secrets with Update2: path=%s, setting=%s", path, settingName)
// Get existing connection settings
connObj := a.conn.Object("org.freedesktop.NetworkManager", path)
var existingSettings map[string]map[string]dbus.Variant
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existingSettings); err != nil {
log.Warnf("[SecretAgent] GetSettings failed: %v", err)
return
}
// Build minimal settings with ONLY the section we're updating
// This avoids D-Bus type serialization issues with complex types like IPv6 addresses
settings := make(map[string]map[string]dbus.Variant)
// Copy connection section (required for Update2)
if connSection, ok := existingSettings["connection"]; ok {
settings["connection"] = connSection
}
// Update settings based on type
switch settingName {
case "vpn":
// Set password-flags=0 and add secrets to vpn section
vpn, ok := existingSettings["vpn"]
if !ok {
vpn = make(map[string]dbus.Variant)
}
// Get existing data map (vpn.data is string->string)
var data map[string]string
if dataVariant, ok := vpn["data"]; ok {
if dm, ok := dataVariant.Value().(map[string]string); ok {
data = make(map[string]string)
for k, v := range dm {
data[k] = v
}
} else {
data = make(map[string]string)
}
} else {
data = make(map[string]string)
}
// Update password-flags to 0 (system-stored)
data["password-flags"] = "0"
vpn["data"] = dbus.MakeVariant(data)
// Add secrets (vpn.secrets is string->string)
secs := make(map[string]string)
for k, v := range reply.Secrets {
secs[k] = v
}
vpn["secrets"] = dbus.MakeVariant(secs)
settings["vpn"] = vpn
log.Infof("[SecretAgent] Updated VPN settings: password-flags=0, secrets with %d fields", len(secs))
case "802-11-wireless-security":
// Set psk-flags=0 for WiFi
wifiSec, ok := existingSettings["802-11-wireless-security"]
if !ok {
wifiSec = make(map[string]dbus.Variant)
}
wifiSec["psk-flags"] = dbus.MakeVariant(uint32(0))
// Add PSK secret
if psk, ok := reply.Secrets["psk"]; ok {
wifiSec["psk"] = dbus.MakeVariant(psk)
log.Infof("[SecretAgent] Updated WiFi settings: psk-flags=0")
}
settings["802-11-wireless-security"] = wifiSec
case "802-1x":
// Set password-flags=0 for 802.1x
dot1x, ok := existingSettings["802-1x"]
if !ok {
dot1x = make(map[string]dbus.Variant)
}
dot1x["password-flags"] = dbus.MakeVariant(uint32(0))
// Add password secret
if password, ok := reply.Secrets["password"]; ok {
dot1x["password"] = dbus.MakeVariant(password)
log.Infof("[SecretAgent] Updated 802.1x settings: password-flags=0")
}
settings["802-1x"] = dot1x
}
// Call Update2 with correct signature:
// Update2(IN settings, IN flags, IN args) -> OUT result
// flags: 0x1 = to-disk
var result map[string]dbus.Variant
err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0,
settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result)
if err != nil {
log.Warnf("[SecretAgent] Update2(to-disk) failed: %v", err)
} else {
log.Infof("[SecretAgent] Successfully persisted secrets to disk for %s", settingName)
}
}()
}
return out, nil
}
func (a *SecretAgent) DeleteSecrets(conn map[string]nmVariantMap, path dbus.ObjectPath) *dbus.Error {
ssid := readSSID(conn)
log.Infof("[SecretAgent] DeleteSecrets called: path=%s, SSID=%s", path, ssid)
return nil
}
func (a *SecretAgent) DeleteSecrets2(path dbus.ObjectPath, setting string) *dbus.Error {
log.Infof("[SecretAgent] DeleteSecrets2 (alternate) called: path=%s, setting=%s", path, setting)
return nil
}
func (a *SecretAgent) CancelGetSecrets(path dbus.ObjectPath, settingName string) *dbus.Error {
log.Infof("[SecretAgent] CancelGetSecrets called: path=%s, setting=%s", path, settingName)
if a.prompts != nil {
if err := a.prompts.Cancel(string(path), settingName); err != nil {
log.Warnf("[SecretAgent] Failed to cancel prompt: %v", err)
}
}
return nil
}
func (a *SecretAgent) Introspect() (string, *dbus.Error) {
return introspectXML, nil
}
func readSSID(conn map[string]nmVariantMap) string {
if w, ok := conn["802-11-wireless"]; ok {
if v, ok := w["ssid"]; ok {
if b, ok := v.Value().([]byte); ok {
return string(b)
}
if s, ok := v.Value().(string); ok {
return s
}
}
}
return ""
}
func readConnTypeAndName(conn map[string]nmVariantMap) (string, string, string) {
var connType, name, svc string
if c, ok := conn["connection"]; ok {
if v, ok := c["type"]; ok {
if s, ok2 := v.Value().(string); ok2 {
connType = s
}
}
if v, ok := c["id"]; ok {
if s, ok2 := v.Value().(string); ok2 {
name = s
}
}
}
if vpn, ok := conn["vpn"]; ok {
if v, ok := vpn["service-type"]; ok {
if s, ok2 := v.Value().(string); ok2 {
svc = s
}
}
}
if name == "" && connType == "802-11-wireless" {
name = readSSID(conn)
}
return connType, name, svc
}
func fieldsNeeded(setting string, hints []string) []string {
switch setting {
case "802-11-wireless-security":
return []string{"psk"}
case "802-1x":
return []string{"identity", "password"}
case "vpn":
return hints
default:
return []string{}
}
}
func reasonFromFlags(flags uint32) string {
const (
NM_SECRET_AGENT_GET_SECRETS_FLAG_NONE = 0x0
NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION = 0x1
NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW = 0x2
NM_SECRET_AGENT_GET_SECRETS_FLAG_USER_REQUESTED = 0x4
NM_SECRET_AGENT_GET_SECRETS_FLAG_WPS_PBC_ACTIVE = 0x8
NM_SECRET_AGENT_GET_SECRETS_FLAG_ONLY_SYSTEM = 0x80000000
NM_SECRET_AGENT_GET_SECRETS_FLAG_NO_ERRORS = 0x40000000
)
if flags&NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW != 0 {
return "wrong-password"
}
if flags&NM_SECRET_AGENT_GET_SECRETS_FLAG_USER_REQUESTED != 0 {
return "user-requested"
}
return "required"
}