From 867102b82c4ec964fcc088b84730eb025e8c4ba5 Mon Sep 17 00:00:00 2001 From: bbedward Date: Fri, 10 Jul 2026 10:36:22 -0400 Subject: [PATCH] network/iwd: improve bad credential handling related #2804 port: 1.5 --- .gitignore | 3 + core/internal/server/network/agent_iwd.go | 35 +++-- core/internal/server/network/backend_iwd.go | 32 +++++ .../server/network/backend_iwd_signals.go | 1 + .../server/network/backend_iwd_test.go | 128 ++++++++++++++++++ .../server/network/backend_iwd_wifi.go | 93 ++++++++++++- quickshell/Services/DMSNetworkService.qml | 6 + 7 files changed, 282 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index e80ea00b3..f7a36aa87 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,6 @@ __pycache__ distro/void/temp/ distro/void/hostdir/ distro/void/masterdir*/ + +# Often gets built +core/dms diff --git a/core/internal/server/network/agent_iwd.go b/core/internal/server/network/agent_iwd.go index 5d49f04f2..01a76899f 100644 --- a/core/internal/server/network/agent_iwd.go +++ b/core/internal/server/network/agent_iwd.go @@ -22,13 +22,14 @@ type ConnectionStateChecker interface { } type IWDAgent struct { - conn *dbus.Conn - objPath dbus.ObjectPath - prompts PromptBroker - onUserCanceled func() - onPromptRetry func(ssid string) - lastRequestSSID string - stateChecker ConnectionStateChecker + conn *dbus.Conn + objPath dbus.ObjectPath + prompts PromptBroker + onUserCanceled func() + onPromptRetry func(ssid string) + takePendingSecret func(ssid string) (string, bool) + lastRequestSSID string + stateChecker ConnectionStateChecker } const iwdAgentIntrospectXML = ` @@ -119,6 +120,13 @@ func (a *IWDAgent) RequestPassphrase(network dbus.ObjectPath) (string, *dbus.Err return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) } + if a.takePendingSecret != nil { + if psk, ok := a.takePendingSecret(ssid); ok { + a.lastRequestSSID = ssid + return psk, nil + } + } + if a.prompts == nil { if a.onUserCanceled != nil { a.onUserCanceled() @@ -126,19 +134,24 @@ func (a *IWDAgent) RequestPassphrase(network dbus.ObjectPath) (string, *dbus.Err return "", dbus.NewError("net.connman.iwd.Agent.Error.Canceled", nil) } - if a.lastRequestSSID == ssid { - if a.onPromptRetry != nil { - a.onPromptRetry(ssid) - } + retry := a.lastRequestSSID == ssid + if retry && a.onPromptRetry != nil { + a.onPromptRetry(ssid) } a.lastRequestSSID = ssid + reason := "" + if retry { + reason = "wrong-password" + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() token, err := a.prompts.Ask(ctx, PromptRequest{ SSID: ssid, Fields: []string{"psk"}, + Reason: reason, }) if err != nil { if a.onUserCanceled != nil { diff --git a/core/internal/server/network/backend_iwd.go b/core/internal/server/network/backend_iwd.go index 8dadb7b4b..536d032f2 100644 --- a/core/internal/server/network/backend_iwd.go +++ b/core/internal/server/network/backend_iwd.go @@ -24,6 +24,7 @@ const ( type connectAttempt struct { ssid string netPath dbus.ObjectPath + saved bool start time.Time deadline time.Time sawAuthish bool @@ -53,6 +54,37 @@ type IWDBackend struct { attemptMutex sync.RWMutex recentScans map[string]time.Time recentScansMu sync.Mutex + pendingPSK *pendingReplacementPSK + pendingPSKMu sync.Mutex +} + +type pendingReplacementPSK struct { + ssid string + psk string + expires time.Time +} + +func (b *IWDBackend) storePendingPSK(ssid, psk string) { + b.pendingPSKMu.Lock() + b.pendingPSK = &pendingReplacementPSK{ + ssid: ssid, + psk: psk, + expires: time.Now().Add(30 * time.Second), + } + b.pendingPSKMu.Unlock() +} + +func (b *IWDBackend) takePendingPSK(ssid string) (string, bool) { + b.pendingPSKMu.Lock() + defer b.pendingPSKMu.Unlock() + + pending := b.pendingPSK + if pending == nil || pending.ssid != ssid || time.Now().After(pending.expires) { + return "", false + } + + b.pendingPSK = nil + return pending.psk, true } func NewIWDBackend() (*IWDBackend, error) { diff --git a/core/internal/server/network/backend_iwd_signals.go b/core/internal/server/network/backend_iwd_signals.go index 25fff740b..e4541f5f2 100644 --- a/core/internal/server/network/backend_iwd_signals.go +++ b/core/internal/server/network/backend_iwd_signals.go @@ -18,6 +18,7 @@ func (b *IWDBackend) StartMonitoring(onStateChange func()) error { } agent.onUserCanceled = b.OnUserCanceledPrompt agent.onPromptRetry = b.OnPromptRetry + agent.takePendingSecret = b.takePendingPSK b.iwdAgent = agent } diff --git a/core/internal/server/network/backend_iwd_test.go b/core/internal/server/network/backend_iwd_test.go index 7e41552ae..1d0102d5d 100644 --- a/core/internal/server/network/backend_iwd_test.go +++ b/core/internal/server/network/backend_iwd_test.go @@ -1,6 +1,7 @@ package network import ( + "context" "testing" "time" @@ -314,6 +315,133 @@ func TestConnectAttempt_Finalization(t *testing.T) { backend.stateMutex.RUnlock() } +func TestIWDBackend_PendingPSK(t *testing.T) { + backend, _ := NewIWDBackend() + + _, ok := backend.takePendingPSK("Home") + assert.False(t, ok) + + backend.storePendingPSK("Home", "newpass") + + _, ok = backend.takePendingPSK("Other") + assert.False(t, ok, "pending PSK should not match a different SSID") + + psk, ok := backend.takePendingPSK("Home") + assert.True(t, ok) + assert.Equal(t, "newpass", psk) + + _, ok = backend.takePendingPSK("Home") + assert.False(t, ok, "pending PSK should be consumed on take") + + backend.storePendingPSK("Home", "newpass") + backend.pendingPSKMu.Lock() + backend.pendingPSK.expires = time.Now().Add(-time.Second) + backend.pendingPSKMu.Unlock() + + _, ok = backend.takePendingPSK("Home") + assert.False(t, ok, "expired pending PSK should not be returned") +} + +type fakePromptBroker struct { + asked chan PromptRequest + reply PromptReply + replyErr error +} + +func (f *fakePromptBroker) Ask(ctx context.Context, req PromptRequest) (string, error) { + f.asked <- req + return "token", nil +} + +func (f *fakePromptBroker) Wait(ctx context.Context, token string) (PromptReply, error) { + return f.reply, f.replyErr +} + +func (f *fakePromptBroker) Resolve(token string, reply PromptReply) error { return nil } + +func (f *fakePromptBroker) Cancel(path string, setting string) error { return nil } + +func TestIWDBackend_BadCredentialsSavedNetwork_PromptsReplacement(t *testing.T) { + backend, _ := NewIWDBackend() + backend.state = &BackendState{} + broker := &fakePromptBroker{ + asked: make(chan PromptRequest, 1), + reply: PromptReply{Cancel: true}, + } + backend.promptBroker = broker + + att := &connectAttempt{ + ssid: "Home", + netPath: "/test", + saved: true, + start: time.Now(), + deadline: time.Now().Add(15 * time.Second), + } + + backend.finalizeAttempt(att, "bad-credentials") + + select { + case req := <-broker.asked: + assert.Equal(t, "Home", req.SSID) + assert.Equal(t, "wrong-password", req.Reason) + assert.Equal(t, []string{"psk"}, req.Fields) + case <-time.After(time.Second): + t.Fatal("expected replacement credentials prompt for saved network") + } +} + +func TestIWDBackend_BadCredentialsUnsavedNetwork_NoReplacementPrompt(t *testing.T) { + backend, _ := NewIWDBackend() + backend.state = &BackendState{} + broker := &fakePromptBroker{ + asked: make(chan PromptRequest, 1), + reply: PromptReply{Cancel: true}, + } + backend.promptBroker = broker + + att := &connectAttempt{ + ssid: "Home", + netPath: "/test", + start: time.Now(), + deadline: time.Now().Add(15 * time.Second), + } + + backend.finalizeAttempt(att, "bad-credentials") + + select { + case <-broker.asked: + t.Fatal("unsaved network should not trigger a replacement prompt") + case <-time.After(100 * time.Millisecond): + } +} + +func TestIWDBackend_BadCredentialsAfterPromptRetry_NoReplacementPrompt(t *testing.T) { + backend, _ := NewIWDBackend() + backend.state = &BackendState{} + broker := &fakePromptBroker{ + asked: make(chan PromptRequest, 1), + reply: PromptReply{Cancel: true}, + } + backend.promptBroker = broker + + att := &connectAttempt{ + ssid: "Home", + netPath: "/test", + saved: true, + sawPromptRetry: true, + start: time.Now(), + deadline: time.Now().Add(15 * time.Second), + } + + backend.finalizeAttempt(att, "bad-credentials") + + select { + case <-broker.asked: + t.Fatal("attempt that already prompted should not trigger a replacement prompt") + case <-time.After(100 * time.Millisecond): + } +} + func TestConnectAttempt_DoubleFinalization(t *testing.T) { backend, _ := NewIWDBackend() backend.state = &BackendState{} diff --git a/core/internal/server/network/backend_iwd_wifi.go b/core/internal/server/network/backend_iwd_wifi.go index f98c16f07..a086445f5 100644 --- a/core/internal/server/network/backend_iwd_wifi.go +++ b/core/internal/server/network/backend_iwd_wifi.go @@ -1,10 +1,12 @@ package network import ( + "context" "fmt" "time" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" + "github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/godbus/dbus/v5" ) @@ -476,6 +478,73 @@ func (b *IWDBackend) finalizeAttempt(att *connectAttempt, code string) { if b.onStateChange != nil { b.onStateChange() } + + if code == errdefs.ErrBadCredentials { + b.maybeReplaceSavedPSK(att) + } +} + +func (b *IWDBackend) maybeReplaceSavedPSK(att *connectAttempt) { + if b.promptBroker == nil || !att.saved { + return + } + + att.mu.Lock() + prompted := att.sawPromptRetry + att.mu.Unlock() + if prompted { + return + } + + b.sigWG.Add(1) + go func() { + defer b.sigWG.Done() + b.requestReplacementPSK(att.ssid) + }() +} + +func (b *IWDBackend) requestReplacementPSK(ssid string) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + go func() { + select { + case <-b.stopChan: + cancel() + case <-ctx.Done(): + } + }() + + token, err := b.promptBroker.Ask(ctx, PromptRequest{ + SSID: ssid, + SettingName: "802-11-wireless-security", + Fields: []string{"psk"}, + Reason: "wrong-password", + }) + if err != nil { + log.Warnf("failed to request replacement credentials for %s: %v", ssid, err) + return + } + + reply, err := b.promptBroker.Wait(ctx, token) + if err != nil || reply.Cancel { + return + } + + psk, ok := reply.Secrets["psk"] + if !ok || psk == "" { + return + } + + if err := b.ForgetWiFiNetwork(ssid); err != nil { + log.Warnf("failed to forget %s before credential replacement: %v", ssid, err) + } + + b.storePendingPSK(ssid, psk) + + if err := b.ConnectWiFi(ConnectionRequest{SSID: ssid}); err != nil { + log.Warnf("failed to reconnect %s with replacement credentials: %v", ssid, err) + } } func (b *IWDBackend) startAttemptWatchdog(att *connectAttempt) { @@ -560,7 +629,7 @@ func (b *IWDBackend) ConnectWiFi(req ConnectionRequest) error { return fmt.Errorf("no WiFi device available") } - networkPath, err := b.findNetworkPath(req.SSID) + networkPath, saved, err := b.findNetworkPath(req.SSID) if err != nil { b.setConnectError(errdefs.ErrNoSuchSSID) if b.onStateChange != nil { @@ -572,6 +641,7 @@ func (b *IWDBackend) ConnectWiFi(req ConnectionRequest) error { att := &connectAttempt{ ssid: req.SSID, netPath: networkPath, + saved: saved, start: time.Now(), deadline: time.Now().Add(15 * time.Second), } @@ -619,26 +689,39 @@ func (b *IWDBackend) ConnectWiFi(req ConnectionRequest) error { return nil } -func (b *IWDBackend) findNetworkPath(ssid string) (dbus.ObjectPath, error) { +func (b *IWDBackend) findNetworkPath(ssid string) (dbus.ObjectPath, bool, error) { obj := b.conn.Object(iwdBusName, iwdObjectPath) var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant err := obj.Call(dbusObjectManager+".GetManagedObjects", 0).Store(&objects) if err != nil { - return "", err + return "", false, err } + var netPath dbus.ObjectPath + saved := false for path, interfaces := range objects { if netProps, ok := interfaces[iwdNetworkInterface]; ok { if nameVar, ok := netProps["Name"]; ok { if name, ok := nameVar.Value().(string); ok && name == ssid { - return path, nil + netPath = path + } + } + } + if knownProps, ok := interfaces[iwdKnownNetworkInterface]; ok { + if nameVar, ok := knownProps["Name"]; ok { + if name, ok := nameVar.Value().(string); ok && name == ssid { + saved = true } } } } - return "", fmt.Errorf("network not found") + if netPath == "" { + return "", false, fmt.Errorf("network not found") + } + + return netPath, saved, nil } func (b *IWDBackend) DisconnectWiFi() error { diff --git a/quickshell/Services/DMSNetworkService.qml b/quickshell/Services/DMSNetworkService.qml index 6284ec576..a6788ad1f 100644 --- a/quickshell/Services/DMSNetworkService.qml +++ b/quickshell/Services/DMSNetworkService.qml @@ -577,6 +577,12 @@ Singleton { credentialsRequested = false; + if (credentialsReason === "wrong-password" && credentialsSSID && credentialsSetting === "802-11-wireless-security") { + pendingConnectionSSID = credentialsSSID; + pendingConnectionStartTime = Date.now(); + connectionStatus = "connecting"; + } + DMSService.sendRequest("network.credentials.submit", params, response => { if (response.error) { log.warn("Failed to submit credentials:", response.error);