diff --git a/core/internal/server/network/agent_networkmanager.go b/core/internal/server/network/agent_networkmanager.go index a659b35f1..4a3e1a341 100644 --- a/core/internal/server/network/agent_networkmanager.go +++ b/core/internal/server/network/agent_networkmanager.go @@ -545,12 +545,18 @@ func (a *SecretAgent) GetSecrets( out[settingName] = sec } if settingName == "vpn" && a.backend != nil && !isPKCS11 && (vpnUsername != "" || reply.Save) { - pw := reply.Secrets["password"] + secrets := make(map[string]string) + for k, v := range reply.Secrets { + if k != "username" { + secrets[k] = v + } + } a.backend.pendingVPNSaveMu.Lock() a.backend.pendingVPNSave = &pendingVPNCredentials{ ConnectionPath: string(path), Username: vpnUsername, - Password: pw, + Password: reply.Secrets["password"], + Secrets: secrets, SavePassword: reply.Save, } a.backend.pendingVPNSaveMu.Unlock() @@ -790,7 +796,23 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string { fields = []string{"username", "password"} } case strings.Contains(vpnService, "openvpn"): - if connType == "password" || connType == "password-tls" { + switch connType { + case "tls": + // Cert auth wants the private-key passphrase, not a password. + if secretFlagsNotRequired(dataMap["cert-pass-flags"]) { + return nil + } + return []string{"cert-pass"} + case "password-tls": + fields = []string{} + if dataMap["username"] == "" { + fields = append(fields, "username") + } + fields = append(fields, "password") + if !secretFlagsNotRequired(dataMap["cert-pass-flags"]) { + fields = append(fields, "cert-pass") + } + case "password": if dataMap["username"] == "" { fields = []string{"username", "password"} } @@ -805,6 +827,19 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string { return fields } +// NetworkManager secret flags: bit 1 (value 2) marks a secret as not-saved, +// bit 2 (value 4) as not-required. A not-required secret must not be prompted. +func secretFlagsNotRequired(flags string) bool { + if flags == "" { + return false + } + n, err := strconv.Atoi(flags) + if err != nil { + return false + } + return n&0x4 != 0 +} + func needsExternalBrowserAuth(protocol, authType, username string, data map[string]string) bool { if method, ok := data["saml-auth-method"]; ok { if method == "REDIRECT" || method == "POST" { diff --git a/core/internal/server/network/agent_networkmanager_test.go b/core/internal/server/network/agent_networkmanager_test.go index 701a5b19e..2b7636420 100644 --- a/core/internal/server/network/agent_networkmanager_test.go +++ b/core/internal/server/network/agent_networkmanager_test.go @@ -315,6 +315,44 @@ func TestInferVPNFields_GPSaml(t *testing.T) { expectedLen: 1, shouldHave: []string{"password"}, }, + { + name: "OpenVPN cert auth (tls) - private-key passphrase", + vpnService: "org.freedesktop.NetworkManager.openvpn", + dataMap: map[string]string{ + "connection-type": "tls", + }, + expectedLen: 1, + shouldHave: []string{"cert-pass"}, + }, + { + name: "OpenVPN cert auth (tls) with passphrase-less key", + vpnService: "org.freedesktop.NetworkManager.openvpn", + dataMap: map[string]string{ + "connection-type": "tls", + "cert-pass-flags": "4", + }, + expectedLen: 0, + }, + { + name: "OpenVPN password-tls no username", + vpnService: "org.freedesktop.NetworkManager.openvpn", + dataMap: map[string]string{ + "connection-type": "password-tls", + }, + expectedLen: 3, + shouldHave: []string{"username", "password", "cert-pass"}, + }, + { + name: "OpenVPN password-tls with username, passphrase-less key", + vpnService: "org.freedesktop.NetworkManager.openvpn", + dataMap: map[string]string{ + "connection-type": "password-tls", + "username": "john", + "cert-pass-flags": "4", + }, + expectedLen: 1, + shouldHave: []string{"password"}, + }, } for _, tt := range tests { diff --git a/core/internal/server/network/backend_networkmanager.go b/core/internal/server/network/backend_networkmanager.go index 89b65fa3a..e292bdfe7 100644 --- a/core/internal/server/network/backend_networkmanager.go +++ b/core/internal/server/network/backend_networkmanager.go @@ -90,7 +90,10 @@ type pendingVPNCredentials struct { ConnectionPath string Username string Password string - SavePassword bool + // Secrets holds all VPN secret fields keyed by name (e.g. "cert-pass"); + // falls back to Password under the "password" key when empty. + Secrets map[string]string + SavePassword bool } type cachedVPNCredentials struct { diff --git a/core/internal/server/network/backend_networkmanager_vpn.go b/core/internal/server/network/backend_networkmanager_vpn.go index c4cb854ac..ea17be858 100644 --- a/core/internal/server/network/backend_networkmanager_vpn.go +++ b/core/internal/server/network/backend_networkmanager_vpn.go @@ -863,13 +863,17 @@ func (b *NetworkManagerBackend) saveVPNCredentials(creds *pendingVPNCredentials) log.Infof("[saveVPNCredentials] Saving username") } - // Save password if requested + // Save secrets if requested if creds.SavePassword { - data["password-flags"] = "0" - secs := make(map[string]string) - secs["password"] = creds.Password + secs := creds.Secrets + if len(secs) == 0 { + secs = map[string]string{"password": creds.Password} + } + for field := range secs { + data[field+"-flags"] = "0" + } vpn["secrets"] = dbus.MakeVariant(secs) - log.Infof("[saveVPNCredentials] Saving password with password-flags=0") + log.Infof("[saveVPNCredentials] Saving %d secret field(s) with flags=0", len(secs)) } vpn["data"] = dbus.MakeVariant(data) diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 9a88287a8..4cb098e40 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -1027,17 +1027,6 @@ Singleton { } ] - // Standalone bar xray is unsafe when windows can render beneath its surface - function _standaloneBarXrayAvailable(configs) { - const list = configs || []; - const activeBars = list.filter(c => c && c.enabled && (c.visible ?? true)); - const gapsOverride = (typeof CompositorService !== "undefined" && CompositorService.isHyprland) ? hyprlandLayoutGapsOverride : niriLayoutGapsOverride; - const layoutGaps = gapsOverride >= 0 ? gapsOverride : Math.max(4, (list[0]?.spacing ?? 4)); - return activeBars.every(c => !c.autoHide && !(c.useOverlayLayer ?? false) && (c.spacing ?? 4) + (c.bottomGap ?? 0) + layoutGaps >= 0); - } - - readonly property bool standaloneBarXrayAvailable: _standaloneBarXrayAvailable(barConfigs) - property bool desktopClockEnabled: false property string desktopClockStyle: "analog" property real desktopClockTransparency: 0.8 @@ -2479,17 +2468,13 @@ Singleton { if (index === -1) return; const positionChanged = updates.position !== undefined && configs[index].position !== updates.position; - const barXrayTargetWasAvailable = _standaloneBarXrayAvailable(configs); if (updates.autoHide === false || updates.visible === false) setBarIpcReveal(barId, false); Object.assign(configs[index], updates); - const sanitizedConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs; - barConfigs = sanitizedConfigs; + barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs; updateBarConfigs(); - if (!frameEnabled && _standaloneBarXrayAvailable(sanitizedConfigs) !== barXrayTargetWasAvailable) - updateCompositorLayout(); if (positionChanged) { NotificationService.dismissAllPopups(); } diff --git a/quickshell/Services/HyprlandService.qml b/quickshell/Services/HyprlandService.qml index 4184badb0..40b4d5ad5 100644 --- a/quickshell/Services/HyprlandService.qml +++ b/quickshell/Services/HyprlandService.qml @@ -341,10 +341,11 @@ Singleton { const borderSize = (typeof SettingsData !== "undefined" && SettingsData.hyprlandLayoutBorderSize >= 0) ? SettingsData.hyprlandLayoutBorderSize : defaultBorderSize; const resizeOnBorder = (typeof SettingsData !== "undefined" && SettingsData.hyprlandResizeOnBorder) ? true : false; const frameEnabled = typeof SettingsData !== "undefined" && SettingsData.frameEnabled; - const frameConnectedMode = frameEnabled && SettingsData.frameMode === "connected"; // Hyprland `xray = false` is still early-development; unset already samples real content, so only force xray=true - // Connected frame mode has no separate bar/frame surface to target - const barFrameTargetNamespace = !frameEnabled ? (SettingsData.standaloneBarXrayAvailable ? "dms:bar" : null) : (frameConnectedMode ? null : "dms:frame"); + // dms:frame only in separate mode — connected-mode frame blur overlaps windows via popouts/arcs + const xrayNamespaces = ["dms:bar"]; + if (frameEnabled && SettingsData.frameMode !== "connected") + xrayNamespaces.push("dms:frame"); const generalLines = []; if (manageGaps) @@ -371,13 +372,15 @@ hl.layer_rule({ }) `; } - if (layoutBarXrayEnabled && barFrameTargetNamespace) { - content += ` + if (layoutBarXrayEnabled) { + for (const ns of xrayNamespaces) { + content += ` hl.layer_rule({ - match = { namespace = "^${barFrameTargetNamespace}$" }, + match = { namespace = "^${ns}$" }, xray = true, }) `; + } } // Marker persists the preference even while the rule has no target if (!layoutBarXrayEnabled) { diff --git a/quickshell/Services/NiriService.qml b/quickshell/Services/NiriService.qml index e81a49141..e8202484c 100644 --- a/quickshell/Services/NiriService.qml +++ b/quickshell/Services/NiriService.qml @@ -1189,19 +1189,18 @@ Singleton { const gaps = gapsOverride >= 0 ? gapsOverride : defaultGaps; const borderSize = (typeof SettingsData !== "undefined" && SettingsData.niriLayoutBorderSize >= 0) ? SettingsData.niriLayoutBorderSize : defaultBorderSize; const frameEnabled = typeof SettingsData !== "undefined" && SettingsData.frameEnabled; - const frameConnectedMode = frameEnabled && SettingsData.frameMode === "connected"; - // Connected frame mode has no separate bar/frame surface to target - const barFrameTargetNamespace = !frameEnabled ? (SettingsData.standaloneBarXrayAvailable ? "dms:bar" : null) : (frameConnectedMode ? null : "dms:frame"); + // dms:frame only in separate mode — connected-mode frame blur overlaps windows via popouts/arcs + const excludeNamespaces = ["dms:bar"]; + if (frameEnabled && SettingsData.frameMode !== "connected") + excludeNamespaces.push("dms:frame"); // Xray is niri's default blur, so only the off state needs a rule. - // A single rule excluding the bar/frame keeps its blur on the wallpaper - // without a second namespace-matched override rule. let xrayRules = ""; if (!layoutXrayEnabled) { - const excludeLine = (layoutBarXrayEnabled && barFrameTargetNamespace) ? `\n exclude namespace="^${barFrameTargetNamespace}$"` : ""; + const excludeLines = layoutBarXrayEnabled ? excludeNamespaces.map(ns => `\n exclude namespace="^${ns}$"`).join("") : ""; xrayRules += ` -layer-rule {${excludeLine} +layer-rule {${excludeLines} background-effect { xray false }