From 4eb896629d8bddfa613f1eaf6372ee51634f5ed4 Mon Sep 17 00:00:00 2001 From: bbedward Date: Fri, 21 Nov 2025 12:59:12 -0500 Subject: [PATCH] net: fix VPN prompting for password --- .../server/network/agent_networkmanager.go | 148 +++++++++++------- .../network/backend_networkmanager_vpn.go | 16 +- quickshell/Modals/WifiPasswordModal.qml | 114 ++++++++++---- .../ControlCenter/ControlCenterPopout.qml | 3 + 4 files changed, 181 insertions(+), 100 deletions(-) diff --git a/core/internal/server/network/agent_networkmanager.go b/core/internal/server/network/agent_networkmanager.go index e02317d9..9fde5e2a 100644 --- a/core/internal/server/network/agent_networkmanager.go +++ b/core/internal/server/network/agent_networkmanager.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strconv" "time" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" @@ -125,8 +126,9 @@ func (a *SecretAgent) GetSecrets( connType, displayName, vpnSvc := readConnTypeAndName(conn) ssid := readSSID(conn) fields := fieldsNeeded(settingName, hints) + vpnPasswordFlags := readVPNPasswordFlags(conn, settingName) - log.Infof("[SecretAgent] connType=%s, name=%s, vpnSvc=%s, fields=%v, flags=%d", connType, displayName, vpnSvc, fields, flags) + log.Infof("[SecretAgent] connType=%s, name=%s, vpnSvc=%s, fields=%v, flags=%d, vpnPasswordFlags=%d", connType, displayName, vpnSvc, fields, flags, vpnPasswordFlags) if a.backend != nil { a.backend.stateMutex.RLock() @@ -163,57 +165,70 @@ func (a *SecretAgent) GetSecrets( } 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) - } + if a.backend != nil { + a.backend.stateMutex.RLock() + isConnectingVPN := a.backend.state.IsConnectingVPN + a.backend.stateMutex.RUnlock() - 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 - ) + if !isConnectingVPN { + 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) + } - 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 - } - } + log.Infof("[SecretAgent] VPN with empty hints but we're connecting - prompting for password") + fields = []string{"password"} + } else { + 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) } } - 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 + if len(fields) == 0 { + 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 + } } } @@ -343,13 +358,11 @@ func (a *SecretAgent) GetSecrets( // 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 { @@ -364,11 +377,9 @@ func (a *SecretAgent) GetSecrets( 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 @@ -379,14 +390,12 @@ func (a *SecretAgent) GetSecrets( 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") @@ -394,14 +403,12 @@ func (a *SecretAgent) GetSecrets( 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") @@ -507,6 +514,39 @@ func fieldsNeeded(setting string, hints []string) []string { } } +func readVPNPasswordFlags(conn map[string]nmVariantMap, settingName string) uint32 { + if settingName != "vpn" { + return 0xFFFF + } + + vpnSettings, ok := conn["vpn"] + if !ok { + return 0xFFFF + } + + dataVariant, ok := vpnSettings["data"] + if !ok { + return 0xFFFF + } + + dataMap, ok := dataVariant.Value().(map[string]string) + if !ok { + return 0xFFFF + } + + flagsStr, ok := dataMap["password-flags"] + if !ok { + return 0xFFFF + } + + flags64, err := strconv.ParseUint(flagsStr, 10, 32) + if err != nil { + return 0xFFFF + } + + return uint32(flags64) +} + func reasonFromFlags(flags uint32) string { const ( NM_SECRET_AGENT_GET_SECRETS_FLAG_NONE = 0x0 diff --git a/core/internal/server/network/backend_networkmanager_vpn.go b/core/internal/server/network/backend_networkmanager_vpn.go index a4250787..a6a442a4 100644 --- a/core/internal/server/network/backend_networkmanager_vpn.go +++ b/core/internal/server/network/backend_networkmanager_vpn.go @@ -235,7 +235,7 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool) } nm := b.nmConn.(gonetworkmanager.NetworkManager) - activeConn, err := nm.ActivateConnection(targetConn, nil, nil) + _, err = nm.ActivateConnection(targetConn, nil, nil) if err != nil { b.stateMutex.Lock() b.state.IsConnectingVPN = false @@ -249,20 +249,6 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool) return fmt.Errorf("failed to activate VPN: %w", err) } - if activeConn != nil { - state, _ := activeConn.GetPropertyState() - if state == 2 { - b.stateMutex.Lock() - b.state.IsConnectingVPN = false - b.state.ConnectingVPNUUID = "" - b.stateMutex.Unlock() - b.ListActiveVPN() - if b.onStateChange != nil { - b.onStateChange() - } - } - } - return nil } diff --git a/quickshell/Modals/WifiPasswordModal.qml b/quickshell/Modals/WifiPasswordModal.qml index 8298cf67..37e93825 100644 --- a/quickshell/Modals/WifiPasswordModal.qml +++ b/quickshell/Modals/WifiPasswordModal.qml @@ -10,6 +10,7 @@ DankModal { id: root layerNamespace: "dms:wifi-password" + keepPopoutsOpen: true HyprlandFocusGrab { windows: [root] @@ -108,7 +109,11 @@ DankModal { shouldBeVisible: false width: 420 - height: requiresEnterprise ? 430 : 230 + height: { + if (requiresEnterprise) return 430 + if (isVpnPrompt) return 260 + return 230 + } onShouldBeVisibleChanged: () => { if (!shouldBeVisible) { wifiPasswordInput = "" @@ -321,7 +326,7 @@ DankModal { if (passwordInput.text) secrets["password"] = passwordInput.text if (wifiAnonymousIdentityInput) secrets["anonymous-identity"] = wifiAnonymousIdentityInput } - NetworkService.submitCredentials(promptToken, secrets, true) + NetworkService.submitCredentials(promptToken, secrets, savePasswordCheckbox.checked) } else { const username = requiresEnterprise ? usernameInput.text : "" NetworkService.connectToWifi( @@ -436,44 +441,91 @@ DankModal { } } - Row { + Column { spacing: Theme.spacingS + width: parent.width - Rectangle { - id: showPasswordCheckbox + Row { + spacing: Theme.spacingS - property bool checked: false + Rectangle { + id: showPasswordCheckbox - width: 20 - height: 20 - radius: 4 - color: checked ? Theme.primary : "transparent" - border.color: checked ? Theme.primary : Theme.outlineButton - border.width: 2 + property bool checked: false - DankIcon { - anchors.centerIn: parent - name: "check" - size: 12 - color: Theme.background - visible: parent.checked + width: 20 + height: 20 + radius: 4 + color: checked ? Theme.primary : "transparent" + border.color: checked ? Theme.primary : Theme.outlineButton + border.width: 2 + + DankIcon { + anchors.centerIn: parent + name: "check" + size: 12 + color: Theme.background + visible: parent.checked + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: () => { + showPasswordCheckbox.checked = !showPasswordCheckbox.checked + } + } } - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: () => { - showPasswordCheckbox.checked = !showPasswordCheckbox.checked - } + StyledText { + text: I18n.tr("Show password") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter } } - StyledText { - text: I18n.tr("Show password") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter + Row { + spacing: Theme.spacingS + visible: isVpnPrompt + + Rectangle { + id: savePasswordCheckbox + + property bool checked: false + + width: 20 + height: 20 + radius: 4 + color: checked ? Theme.primary : "transparent" + border.color: checked ? Theme.primary : Theme.outlineButton + border.width: 2 + + DankIcon { + anchors.centerIn: parent + name: "check" + size: 12 + color: Theme.background + visible: parent.checked + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: () => { + savePasswordCheckbox.checked = !savePasswordCheckbox.checked + } + } + } + + StyledText { + text: I18n.tr("Save password") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } } } @@ -565,7 +617,7 @@ DankModal { if (passwordInput.text) secrets["password"] = passwordInput.text if (wifiAnonymousIdentityInput) secrets["anonymous-identity"] = wifiAnonymousIdentityInput } - NetworkService.submitCredentials(promptToken, secrets, true) + NetworkService.submitCredentials(promptToken, secrets, savePasswordCheckbox.checked) } else { const username = requiresEnterprise ? usernameInput.text : "" NetworkService.connectToWifi( diff --git a/quickshell/Modules/ControlCenter/ControlCenterPopout.qml b/quickshell/Modules/ControlCenter/ControlCenterPopout.qml index ed3dbce0..625178a5 100644 --- a/quickshell/Modules/ControlCenter/ControlCenterPopout.qml +++ b/quickshell/Modules/ControlCenter/ControlCenterPopout.qml @@ -77,9 +77,12 @@ DankPopout { screen: triggerScreen shouldBeVisible: false + property bool credentialsPromptOpen: NetworkService.credentialsRequested + WlrLayershell.keyboardFocus: { if (!shouldBeVisible) return WlrKeyboardFocus.None if (powerMenuOpen) return WlrKeyboardFocus.None + if (credentialsPromptOpen) return WlrKeyboardFocus.None if (CompositorService.isHyprland) return WlrKeyboardFocus.OnDemand return WlrKeyboardFocus.Exclusive }