1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-24 03:55:23 -04:00

fix(network): fix excessive network prompting on failures, add VPN

connection statuses
This commit is contained in:
bbedward
2026-06-23 13:58:58 -04:00
parent 0e7901ebbe
commit 28f40afccf
15 changed files with 471 additions and 31 deletions
@@ -361,11 +361,27 @@ func (a *SecretAgent) GetSecrets(
}
}
// Phase 4: Non-interactive secret retrieval (keyring).
// Always try the keyring even when REQUEST_NEW is set — the vault may have
// been unlocked by a prior call's Prompt flow, making the lookup non-interactive.
if secretOut := a.trySecretService(connUuid, settingName, fields); secretOut != nil {
return secretOut, nil
// Phase 4: Non-interactive secret retrieval. REQUEST_NEW means NM thinks the
// secret is wrong, so force a prompt; otherwise reuse keyring then cached secret.
requestNew := flags&nmSecretAgentFlagRequestNew != 0
if requestNew {
if a.backend != nil {
a.backend.clearCachedWiFiSecret(connUuid)
}
} else {
if secretOut := a.trySecretService(connUuid, settingName, fields); secretOut != nil {
return secretOut, nil
}
switch settingName {
case "802-11-wireless-security", "802-1x":
if a.backend != nil {
if cached := a.backend.lookupCachedWiFiSecret(connUuid, settingName); cached != nil {
log.Infof("[SecretAgent] Reusing cached WiFi secret for %s (no REQUEST_NEW)", connUuid)
return buildWiFiSecretsResponse(settingName, cached), nil
}
}
}
}
// Phase 5: If interaction is not allowed, we're done.
@@ -436,6 +452,8 @@ func (a *SecretAgent) GetSecrets(
}
a.backend.stateMutex.Unlock()
a.backend.clearCachedWiFiSecret(connUuid)
// If this was a WiFi connection that was just cancelled, remove the connection profile
// (it was created with AddConnection but activation was cancelled)
// Only do this for newly created connections, not pre-existing ones.
@@ -539,6 +557,13 @@ func (a *SecretAgent) GetSecrets(
log.Infof("[SecretAgent] Queued credentials persist for after connection succeeds")
}
if a.backend != nil {
switch settingName {
case "802-11-wireless-security", "802-1x":
a.backend.cacheWiFiSecret(connUuid, ssid, settingName, reply.Secrets)
}
}
return out, nil
}
@@ -906,6 +931,27 @@ func reasonFromFlags(flags uint32) string {
return "required"
}
func buildWiFiSecretsResponse(settingName string, secrets map[string]string) nmSettingMap {
sec := nmVariantMap{}
switch settingName {
case "802-1x":
for k, v := range secrets {
switch k {
case "password", "private-key-password", "phase2-private-key-password", "pin":
sec[k] = dbus.MakeVariant(v)
}
}
default:
for k, v := range secrets {
sec[k] = dbus.MakeVariant(v)
}
}
out := nmSettingMap{}
out[settingName] = sec
return out
}
func buildGPSamlSecretsResponse(settingName, cookie, host, fingerprint string) nmSettingMap {
out := nmSettingMap{}
vpnSec := nmVariantMap{}
@@ -374,6 +374,57 @@ func TestSecretAgent_GetSecrets_NoInteractionFlag(t *testing.T) {
assert.Contains(t, err.Error(), "NoSecrets")
}
func TestBuildWiFiSecretsResponse(t *testing.T) {
t.Run("wpa-psk returns psk", func(t *testing.T) {
out := buildWiFiSecretsResponse("802-11-wireless-security", map[string]string{"psk": "hunter2"})
sec, ok := out["802-11-wireless-security"]
assert.True(t, ok)
assert.Equal(t, "hunter2", sec["psk"].Value())
})
t.Run("802-1x keeps secrets and drops identity", func(t *testing.T) {
out := buildWiFiSecretsResponse("802-1x", map[string]string{
"identity": "john",
"password": "hunter2",
})
sec := out["802-1x"]
assert.Equal(t, "hunter2", sec["password"].Value())
_, hasIdentity := sec["identity"]
assert.False(t, hasIdentity, "identity is persisted separately, not returned as a secret")
})
}
func TestWiFiSecretCache(t *testing.T) {
b := &NetworkManagerBackend{}
b.cacheWiFiSecret("uuid-1", "HomeNet", "802-11-wireless-security", map[string]string{"psk": "hunter2"})
got := b.lookupCachedWiFiSecret("uuid-1", "802-11-wireless-security")
assert.Equal(t, map[string]string{"psk": "hunter2"}, got)
assert.Nil(t, b.lookupCachedWiFiSecret("uuid-1", "802-1x"), "setting mismatch must miss")
assert.Nil(t, b.lookupCachedWiFiSecret("uuid-2", "802-11-wireless-security"), "uuid mismatch must miss")
assert.Nil(t, b.lookupCachedWiFiSecret("", "802-11-wireless-security"), "empty uuid must miss")
// REQUEST_NEW path clears by uuid.
b.clearCachedWiFiSecret("uuid-1")
assert.Nil(t, b.lookupCachedWiFiSecret("uuid-1", "802-11-wireless-security"))
// Returned map is a copy: mutating it must not affect the cache.
b.cacheWiFiSecret("uuid-1", "HomeNet", "802-11-wireless-security", map[string]string{"psk": "hunter2"})
got = b.lookupCachedWiFiSecret("uuid-1", "802-11-wireless-security")
got["psk"] = "tampered"
assert.Equal(t, "hunter2", b.lookupCachedWiFiSecret("uuid-1", "802-11-wireless-security")["psk"])
// Terminal-state path clears by SSID.
b.clearCachedWiFiSecretBySSID("OtherNet")
assert.NotNil(t, b.lookupCachedWiFiSecret("uuid-1", "802-11-wireless-security"), "ssid mismatch must not clear")
b.clearCachedWiFiSecretBySSID("HomeNet")
assert.Nil(t, b.lookupCachedWiFiSecret("uuid-1", "802-11-wireless-security"))
}
func TestNmVariantMap(t *testing.T) {
// Test that nmVariantMap and nmSettingMap work correctly
settingMap := make(nmSettingMap)
+2
View File
@@ -79,4 +79,6 @@ type BackendState struct {
IsConnectingVPN bool
ConnectingVPNUUID string
LastError string
VPNError string
VPNErrorUuid string
}
@@ -16,6 +16,9 @@ const (
dbusNMWiredInterface = "org.freedesktop.NetworkManager.Device.Wired"
dbusNMWirelessInterface = "org.freedesktop.NetworkManager.Device.Wireless"
dbusNMAccessPointInterface = "org.freedesktop.NetworkManager.AccessPoint"
dbusNMActiveConnInterface = "org.freedesktop.NetworkManager.Connection.Active"
dbusNMVPNConnInterface = "org.freedesktop.NetworkManager.VPN.Connection"
dbusNMActiveConnPath = "/org/freedesktop/NetworkManager/ActiveConnection"
dbusPropsInterface = "org.freedesktop.DBus.Properties"
NmDeviceStateReasonWrongPassword = 8
@@ -77,6 +80,8 @@ type NetworkManagerBackend struct {
cachedPKCS11Mu sync.Mutex
cachedGPSamlCookie *cachedGPSamlCookie
cachedGPSamlMu sync.Mutex
cachedWiFiSecret *cachedWiFiSecret
cachedWiFiSecretMu sync.Mutex
onStateChange func()
}
@@ -99,6 +104,15 @@ type cachedPKCS11PIN struct {
PIN string
}
// cachedWiFiSecret reuses a just-entered WiFi/802.1x secret across repeat
// GetSecrets calls in one activation, so NM retries don't re-prompt.
type cachedWiFiSecret struct {
ConnectionUUID string
SSID string
SettingName string
Secrets map[string]string
}
type cachedGPSamlCookie struct {
ConnectionUUID string
Cookie string
@@ -340,6 +354,104 @@ func (b *NetworkManagerBackend) CancelCredentials(token string) error {
})
}
// mergeStoredSecrets re-fetches stored secrets and folds them into settings
// before an Update. GetSettings never returns secrets and Update replaces the
// whole connection, so a bare GetSettings->Update wipes system-owned passwords
// (e.g. an OpenVPN password with password-flags=0). Only fills keys that aren't
// already being set, so an explicit credential change still wins.
func mergeStoredSecrets(conn gonetworkmanager.Connection, settings gonetworkmanager.ConnectionSettings) {
for setting := range settings {
switch setting {
case "vpn", "802-11-wireless-security", "802-1x":
default:
continue
}
secrets, err := conn.GetSecrets(setting)
if err != nil {
continue
}
section, ok := secrets[setting]
if !ok {
continue
}
for k, v := range section {
if _, exists := settings[setting][k]; exists {
continue
}
settings[setting][k] = v
}
}
}
func (b *NetworkManagerBackend) cacheWiFiSecret(connUUID, ssid, settingName string, secrets map[string]string) {
if connUUID == "" || len(secrets) == 0 {
return
}
copied := make(map[string]string, len(secrets))
for k, v := range secrets {
copied[k] = v
}
b.cachedWiFiSecretMu.Lock()
b.cachedWiFiSecret = &cachedWiFiSecret{
ConnectionUUID: connUUID,
SSID: ssid,
SettingName: settingName,
Secrets: copied,
}
b.cachedWiFiSecretMu.Unlock()
}
func (b *NetworkManagerBackend) lookupCachedWiFiSecret(connUUID, settingName string) map[string]string {
if connUUID == "" {
return nil
}
b.cachedWiFiSecretMu.Lock()
defer b.cachedWiFiSecretMu.Unlock()
cached := b.cachedWiFiSecret
if cached == nil || cached.ConnectionUUID != connUUID || cached.SettingName != settingName {
return nil
}
copied := make(map[string]string, len(cached.Secrets))
for k, v := range cached.Secrets {
copied[k] = v
}
return copied
}
func (b *NetworkManagerBackend) clearCachedWiFiSecret(connUUID string) {
b.cachedWiFiSecretMu.Lock()
defer b.cachedWiFiSecretMu.Unlock()
if connUUID == "" {
b.cachedWiFiSecret = nil
return
}
if b.cachedWiFiSecret != nil && b.cachedWiFiSecret.ConnectionUUID == connUUID {
b.cachedWiFiSecret = nil
}
}
func (b *NetworkManagerBackend) clearCachedWiFiSecretBySSID(ssid string) {
if ssid == "" {
return
}
b.cachedWiFiSecretMu.Lock()
defer b.cachedWiFiSecretMu.Unlock()
if b.cachedWiFiSecret != nil && b.cachedWiFiSecret.SSID == ssid {
b.cachedWiFiSecret = nil
}
}
func (b *NetworkManagerBackend) ensureWiFiDevice() error {
if b.wifiDev != nil {
return nil
@@ -136,6 +136,29 @@ func (b *NetworkManagerBackend) startSignalPump() error {
}
}
// activating->activated/failed fires on the active-connection object, not the
// manager's ActiveConnections property. VPN.Connection covers plugin VPNs;
// Connection.Active covers the rest, including WireGuard.
if err := conn.AddMatchSignal(
dbus.WithMatchPathNamespace(dbus.ObjectPath(dbusNMActiveConnPath)),
dbus.WithMatchInterface(dbusNMVPNConnInterface),
dbus.WithMatchMember("VpnStateChanged"),
); err != nil {
conn.RemoveSignal(signals)
conn.Close()
return err
}
if err := conn.AddMatchSignal(
dbus.WithMatchPathNamespace(dbus.ObjectPath(dbusNMActiveConnPath)),
dbus.WithMatchInterface(dbusNMActiveConnInterface),
dbus.WithMatchMember("StateChanged"),
); err != nil {
conn.RemoveSignal(signals)
conn.Close()
return err
}
b.sigWG.Add(1)
go func() {
defer b.sigWG.Done()
@@ -193,6 +216,16 @@ func (b *NetworkManagerBackend) stopSignalPump() {
dbus.WithMatchInterface(dbusNMInterface),
dbus.WithMatchMember("DeviceRemoved"),
)
b.dbusConn.RemoveMatchSignal(
dbus.WithMatchPathNamespace(dbus.ObjectPath(dbusNMActiveConnPath)),
dbus.WithMatchInterface(dbusNMVPNConnInterface),
dbus.WithMatchMember("VpnStateChanged"),
)
b.dbusConn.RemoveMatchSignal(
dbus.WithMatchPathNamespace(dbus.ObjectPath(dbusNMActiveConnPath)),
dbus.WithMatchInterface(dbusNMActiveConnInterface),
dbus.WithMatchMember("StateChanged"),
)
for _, info := range b.wifiDevices {
b.dbusConn.RemoveMatchSignal(
@@ -234,6 +267,20 @@ func (b *NetworkManagerBackend) handleDBusSignal(sig *dbus.Signal) {
return
}
if sig.Name == dbusNMVPNConnInterface+".VpnStateChanged" {
if len(sig.Body) >= 2 {
state, _ := sig.Body[0].(uint32)
reason, _ := sig.Body[1].(uint32)
b.handleVPNStateChange(state, reason)
}
return
}
if sig.Name == dbusNMActiveConnInterface+".StateChanged" {
b.handleActiveConnectionStateChange()
return
}
if sig.Name == "org.freedesktop.NetworkManager.DeviceAdded" {
if len(sig.Body) >= 1 {
if devicePath, ok := sig.Body[0].(dbus.ObjectPath); ok {
@@ -320,6 +367,27 @@ func (b *NetworkManagerBackend) handleNetworkManagerChange(changes map[string]db
}
}
func (b *NetworkManagerBackend) handleActiveConnectionStateChange() {
b.updateVPNConnectionState()
b.ListActiveVPN()
if b.onStateChange != nil {
b.onStateChange()
}
}
func (b *NetworkManagerBackend) handleVPNStateChange(state, reason uint32) {
if state == nmVPNStateFailed {
b.stateMutex.Lock()
if uuid := b.state.ConnectingVPNUUID; uuid != "" {
b.state.VPNError = vpnFailureMessage(reason)
b.state.VPNErrorUuid = uuid
}
b.stateMutex.Unlock()
}
b.handleActiveConnectionStateChange()
}
func (b *NetworkManagerBackend) handleDeviceChange(devicePath dbus.ObjectPath, changes map[string]dbus.Variant) {
var needsUpdate bool
var stateChanged bool
@@ -213,6 +213,7 @@ func (b *NetworkManagerBackend) updateWiFiState() error {
}
var forgetSSID string
var doneSSID string
b.stateMutex.Lock()
@@ -226,6 +227,7 @@ func (b *NetworkManagerBackend) updateWiFiState() error {
b.state.IsConnecting = false
b.state.ConnectingSSID = ""
b.state.LastError = ""
doneSSID = connectingSSID
case failed || (disconnected && !connected):
log.Warnf("[updateWiFiState] Connection failed: SSID=%s, state=%d", connectingSSID, state)
b.state.IsConnecting = false
@@ -240,6 +242,8 @@ func (b *NetworkManagerBackend) updateWiFiState() error {
b.lastFailedSSID = connectingSSID
b.lastFailedTime = time.Now().Unix()
b.failedMutex.Unlock()
doneSSID = connectingSSID
}
}
@@ -252,6 +256,10 @@ func (b *NetworkManagerBackend) updateWiFiState() error {
b.stateMutex.Unlock()
if doneSSID != "" {
b.clearCachedWiFiSecretBySSID(doneSSID)
}
if forgetSSID != "" {
log.Infof("[updateWiFiState] User cancelled authentication, removing connection profile for %s", forgetSSID)
if err := b.ForgetWiFiNetwork(forgetSSID); err != nil {
@@ -17,6 +17,24 @@ import (
"github.com/godbus/dbus/v5"
)
const nmVPNStateFailed = 6
// vpnFailureMessage maps NMVpnConnectionStateReason to a user-facing message.
func vpnFailureMessage(reason uint32) string {
switch reason {
case 9: // NO_SECRETS
return "Authentication required"
case 10: // LOGIN_FAILED
return "Authentication failed"
case 6: // CONNECT_TIMEOUT
return "Connection timed out"
case 7, 8: // SERVICE_START_TIMEOUT, SERVICE_START_FAILED
return "VPN service failed to start"
default:
return "VPN connection failed"
}
}
func (b *NetworkManagerBackend) ListVPNProfiles() ([]VPNProfile, error) {
s := b.settings
if s == nil {
@@ -199,25 +217,38 @@ func (b *NetworkManagerBackend) ListActiveVPN() ([]VPNActive, error) {
}
func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool) error {
// Drop any stale connecting state from a prior attempt that never resolved;
// a leftover flag makes the secret agent refuse secrets for other VPNs.
b.stateMutex.Lock()
b.state.IsConnectingVPN = false
b.state.ConnectingVPNUUID = ""
b.state.VPNError = ""
b.state.VPNErrorUuid = ""
b.stateMutex.Unlock()
if singleActive {
active, err := b.ListActiveVPN()
if err == nil && len(active) > 0 {
alreadyConnected := false
for _, vpn := range active {
if vpn.UUID == uuidOrName || vpn.Name == uuidOrName {
alreadyConnected = true
break
if vpn.UUID != uuidOrName && vpn.Name != uuidOrName {
continue
}
switch vpn.State {
case "activated", "activating":
alreadyConnected = true
}
break
}
if !alreadyConnected {
if err := b.DisconnectAllVPN(); err != nil {
log.Warnf("Failed to disconnect existing VPNs: %v", err)
}
time.Sleep(500 * time.Millisecond)
} else {
if alreadyConnected {
return nil
}
if err := b.DisconnectAllVPN(); err != nil {
log.Warnf("Failed to disconnect existing VPNs: %v", err)
}
time.Sleep(500 * time.Millisecond)
}
}
@@ -723,6 +754,8 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.IsConnectingVPN = false
b.state.ConnectingVPNUUID = ""
b.state.LastError = ""
b.state.VPNError = ""
b.state.VPNErrorUuid = ""
b.stateMutex.Unlock()
// Clear cached PKCS11 PIN and SAML cookie on success
@@ -748,6 +781,10 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.IsConnectingVPN = false
b.state.ConnectingVPNUUID = ""
b.state.LastError = "VPN connection failed"
if b.state.VPNError == "" {
b.state.VPNError = "VPN connection failed"
}
b.state.VPNErrorUuid = connectingVPNUUID
b.stateMutex.Unlock()
// Clear cached PKCS11 PIN and SAML cookie on failure
@@ -768,6 +805,10 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.IsConnectingVPN = false
b.state.ConnectingVPNUUID = ""
b.state.LastError = "VPN connection failed"
if b.state.VPNError == "" {
b.state.VPNError = "VPN connection failed"
}
b.state.VPNErrorUuid = connectingVPNUUID
b.stateMutex.Unlock()
// Clear cached PKCS11 PIN and SAML cookie
@@ -1217,6 +1258,8 @@ func (b *NetworkManagerBackend) UpdateVPNConfig(connUUID string, updates map[str
delete(ipv6, "dns")
}
mergeStoredSecrets(conn, settings)
if err := conn.Update(settings); err != nil {
return fmt.Errorf("failed to update connection: %w", err)
}
@@ -1311,6 +1354,8 @@ func (b *NetworkManagerBackend) SetVPNCredentials(connUUID string, username stri
delete(ipv6, "dns")
}
mergeStoredSecrets(conn, settings)
if err := conn.Update(settings); err != nil {
return fmt.Errorf("failed to update connection: %w", err)
}
@@ -956,6 +956,8 @@ func (b *NetworkManagerBackend) SetWiFiAutoconnect(ssid string, autoconnect bool
delete(ipv6, "dns")
}
mergeStoredSecrets(conn, settings)
err = conn.Update(settings)
if err != nil {
return fmt.Errorf("failed to update connection: %w", err)
+5
View File
@@ -135,6 +135,8 @@ func (m *Manager) syncStateFromBackend() error {
m.state.ConnectingSSID = backendState.ConnectingSSID
m.state.ConnectingDevice = backendState.ConnectingDevice
m.state.LastError = backendState.LastError
m.state.VPNError = backendState.VPNError
m.state.VPNErrorUuid = backendState.VPNErrorUuid
m.stateMutex.Unlock()
return nil
@@ -216,6 +218,9 @@ func stateChangedMeaningfully(old, new *NetworkState) bool {
if old.LastError != new.LastError {
return true
}
if old.VPNError != new.VPNError || old.VPNErrorUuid != new.VPNErrorUuid {
return true
}
if len(old.WiFiNetworks) != len(new.WiFiNetworks) {
return true
}
+2
View File
@@ -121,6 +121,8 @@ type NetworkState struct {
ConnectingSSID string `json:"connectingSSID"`
ConnectingDevice string `json:"connectingDevice,omitempty"`
LastError string `json:"lastError"`
VPNError string `json:"vpnError"`
VPNErrorUuid string `json:"vpnErrorUuid"`
}
type ConnectionRequest struct {