Extract helpers to bring three methods under the 20 cognitive-complexity
limit without changing behavior:
- DaemonFeed.statusStreamLoop: split out handleStatusRecvErr and emitStatus
- Tray.applyStatus: split out consumePendingConnectLogin and
refreshMenuItemsForStatus
- xembedHost.flattenMenu: split out menuItemFromLayout plus propString /
propBool / propInt32 dbusmenu property accessors
Missed in the previous commit. The StatusContext is the only frontend
consumer of the renamed service (the modules/main/.../peers/Peers.tsx
React component is a different identifier — unchanged).
The build/config.yml that wails3 init scaffolded shipped with 'My Company',
'My Product', 'com.mycompany.myproduct' and '(c) 2025, My Company' template
defaults. The per-platform assets generated from it (Info.plist,
Info.dev.plist, info.json, nsis/wails_tools.nsh) carried the same strings,
which were visible in macOS Finder Get Info, Windows .exe Properties and
the NSIS installer.
Updated to the NetBird identity used by the legacy Fyne UI on main:
- companyName / copyright -> 'NetBird GmbH' (matches main release.yml's
COPYRIGHT env passed to goversioninfo)
- productName -> 'NetBird'
- productIdentifier -> 'io.netbird.client' (matches CFBundleIdentifier)
- description -> 'NetBird desktop client'
- darwin NSHumanReadableCopyright -> 'NetBird GmbH'
- windows LegalCopyright -> 'NetBird GmbH'
- nsis INFO_COPYRIGHT -> 'NetBird GmbH'
Version fields (0.0.1) are left in place: release builds get the real
version via goversioninfo (Windows) and sign-pipelines (macOS .app),
so the placeholder is only visible in local task package / task run
output and doesn't reach release artifacts.
The 1542-line tray.go grew into a 14-feature kitchen sink. Split it
into feature-coherent same-package siblings, give the daemon-stream
service a name that matches what it actually does, and trim the
cargo-cult context.WithCancel pattern from click handlers.
File layout (tray.go: 1542 → ~470 lines):
- tray_status.go onStatusEvent / applyStatus / status indicator
- tray_icon.go applyIcon / iconForState (tray icon painting)
- tray_events.go onSystemEvent + eventTitle / titleCase, plus a
shouldSkipSystemEvent helper that names the
three "daemon notification we don't surface"
filters
- tray_session.go session-expiry row + warning notification flow +
handleSessionExpired (moved from tray.go)
- tray_profiles.go loadConfig / loadProfiles / switchProfile
- tray_exitnodes.go exit-node submenu (rebuild / refresh / toggle)
Mutex split: the kitchen-sink t.mu becomes four domain-scoped mutexes
so a long-running gRPC call in one domain can't block status-push
readers in another:
- statusMu connected / lastStatus / lastDaemonVersion /
lastNetworksRevision / pendingConnectLogin
- sessionMu sessionExpiresAt (read by the 30s ticker,
written by applySessionExpiry on every status push)
- profileMu activeProfile / activeUsername /
notificationsEnabled / switchCancel
- exitNodesMu row cache (read in reapplyMenuState's Repaint copy)
- exitNodesRebuildMu serialises ListNetworks + submenu rebuild +
SetMenu (already separate, kept)
Service rename: the "Peers" service handled the daemon's full
SubscribeStatus snapshot (peers, daemon version, management/signal
link state, networks revision, SSO deadline) plus the SubscribeEvents
notification stream and the profile-switch suppression filter. Peers
was a misleading name for a daemon-stream fan-out service. Rename to
DaemonFeed in services/, profileswitcher's stored reference, the
TrayServices struct, main.go wiring, and every doc comment that
referenced it. peers.go → daemon_feed.go. The Status.Peers field
itself (the peer list in the snapshot) is unchanged.
Event constant renames (wire strings unchanged so the frontend keeps
working without regenerating bindings beyond the rename):
- EventStatus → EventStatusSnapshot
Payload is a full Status struct (daemon-wide snapshot), not just
a state-change ping — name the value-shape.
- EventSystem → EventDaemonNotification
Payload is a daemon SystemEvent meant to drive an OS toast or a
Recent Events row. "System" was too generic; "Notification"
matches what consumers do with it.
Concurrency fixes:
- WaitExtendAuthSession now preempts a previous in-flight wait
via the existing SetWaitCancel/CancelWait infrastructure on
PendingFlow, the same pattern WaitSSOLogin uses. The previous
waiter exits with codes.Canceled; the authsession service
translates that to ExtendResult{Preempted: true} so the tray
and the about-to-expire dialog stay silent on the losing flow
instead of showing a false-failure toast. Without this, both
a tray "Extend now" click and a dialog "Stay connected" click
on the same deadline started two parallel IdP polls, and
whichever lost the device-code check painted a bogus error.
- mgmClient.ExtendAuthSession drops the dead backoff retry loop.
The loop only retried on codes.Canceled, but the inner mgmCtx
was derived from context.Background() and never cancelled, so
every real error went straight to backoff.Permanent on the
first attempt. Replace with a single
context.WithTimeout(c.ctx, ConnectTimeout) call; daemon
shutdown now interrupts the RPC and behaviour on real errors
is unchanged.
Click-handler hygiene: six call sites used the cargo-cult
context.WithCancel(context.Background()) + defer cancel() pattern
without ever calling cancel() externally. Replace with
context.Background() directly (loadConfig, loadProfiles,
runExtendSession, dismissSessionWarning, handleConnect's Up,
handleDisconnect's Down). The one site that genuinely needs the
cancel — switchProfile, which stores it in t.switchCancel so
handleDisconnect can preempt the switch — keeps WithCancel.
Helper extraction: shouldSkipSystemEvent groups the three
"daemon notification we drop on the floor" checks
(new_version_available metadata, progress_window metadata, the
::/0 partner of an exit-node default-route event) behind a single
named predicate. Each had a comment explaining why; collecting
them moves the rationale into the helper docstring and shrinks
onSystemEvent to a router.
The backoff loop only retried on codes.Canceled, but mgmCtx was derived
from context.Background() and never cancelled by anything — so every
real error path (Unavailable, DeadlineExceeded, etc.) went through
backoff.Permanent on the first attempt. The loop was a no-op wrapper
that just held the call open for the daemon's lifetime regardless of
shutdown.
Replace with a single context.WithTimeout(c.ctx, ConnectTimeout) call.
Daemon shutdown now interrupts the RPC; behaviour on real errors is
unchanged.
When the tray "Extend now" notification action and the about-to-expire
dialog both start a flow for the same deadline, the daemon was running
two independent IdP polls and the older one surfaced an InvalidArgument
toast as soon as the second RequestExtend overwrote the pending flow.
Follow the WaitSSOLogin pattern: at the top of WaitExtendAuthSession
cancel the previous wait (the SetWaitCancel/CancelWait pair on
PendingFlow already existed but was unused), then register the new
wait's cancel. Preempted callers exit with codes.Canceled; the
authsession service translates that into ExtendResult{Preempted: true}
so the tray and the React dialog can stay silent on the losing flow
instead of showing a false-failure toast / error dialog.
* add SSO session extend flow (management)
Adds the management-server half of the SSO session-extension feature:
- New ExtendAuthSession gRPC RPC that refreshes a peer's session expiry
using a fresh JWT, validated through the same pipeline as Login but
without tearing down the tunnel or redoing the NetworkMap sync.
- Per-peer SessionExpiresAt timestamp on every LoginResponse and
SyncResponse so connected clients learn the deadline on the existing
long-lived stream, and admin-side changes (toggling expiration,
changing the expiration window) reach every peer within seconds.
- SessionExpiresAt(...) helper on Peer that derives the absolute UTC
deadline from LastLogin + the account-level PeerLoginExpiration
setting, returning zero when the peer is not SSO-tracked or expiration
is disabled.
The matching client-side consumer of these fields lands separately.
* encode SessionExpiresAt as 3-state on the wire
Previously the `sessionExpiresAt` field on LoginResponse, SyncResponse
and ExtendAuthSessionResponse was 2-state: a valid timestamp meant
"new deadline", and nil meant "clear". That conflated two distinct
meanings — "no info in this snapshot" vs "expiry is explicitly off /
peer is not SSO-tracked" — so a Sync push that legitimately couldn't
compute the deadline (settings lookup failed) would silently clear the
client's anchor and lose the warning window.
Three states now, encoded on the same field number (no .proto schema
churn — only comments and the server-side encoder change):
- nil pointer (field absent) → "no info"; client preserves anchor
- &Timestamp{} (seconds=0, nanos=0) → explicit "disabled / not SSO"
sentinel; client clears
- valid timestamp → new absolute UTC deadline
A new encodeSessionExpiresAt helper centralises the zero/non-zero
encoding and is shared by the Sync, Login and ExtendAuthSession
builders. The Sync builder still emits nil when settings are missing.
Login and ExtendAuthSession always carry an authoritative value.
The matching client-side decoder lands on feature/session-extend.
* add UserExtendedPeerSession activity event
ExtendAuthSession previously reused UserLoggedInPeer for its audit
record, which conflated two distinct user actions: a full interactive
SSO login (tunnel re-established, network map resync) versus an
in-place deadline refresh (tunnel untouched). Auditors reading the log
couldn't tell which one happened, and downstream dashboards/alerts on
"login" volume were polluted by routine extends.
Adds a dedicated UserExtendedPeerSession Activity (code 125,
"user.peer.session.extend") and switches ExtendPeerSession over to it.
The peer-extend audit trail is now distinguishable from interactive
logins.
* make ExtendAuthSession JWT-retry backoff cancellable
Skip the retry log and 200ms wait on the final attempt, and replace the
uncancellable time.Sleep with a select on time.After/ctx.Done so an
upstream cancellation aborts the wait instead of running it to
completion.
All Wails3 Taskfiles passed -buildvcs=false to go build, which disables
the automatic VCS info embedding Go 1.18+ does by default. As a result
runtime/debug.ReadBuildInfo() returned an empty vcs.revision in our
netbird-ui binary, so the upcoming version.NetbirdCommit() helper from
PR #6263 could not display the git sha for dev builds.
Removed from build:native in all three platform Taskfiles plus the
Windows build:console and the Dockerfile.cross cross-compile script.
go version -m bin/netbird-ui now reports vcs.revision and vcs.modified.
pnpm 11 requires Node.js >= 22.13 (uses node:sqlite, added in 22.5),
but the release workflow still pinned Node 20. After bumping pnpm to
v11 in the previous commit, the frontend build hook now fails with
ERR_UNKNOWN_BUILTIN_MODULE 'node:sqlite' until Node also moves to 22.
The frontend uses pnpm 11 (packageManager field, v11 lockfile, and the
allowBuilds key in pnpm-workspace.yaml is a pnpm 10+ feature), but the
release_ui job's pnpm/action-setup was pinned to v9. v9 rejects the
workspace file with 'packages field missing or empty' before the
frontend build hook can run.
pnpm 11 blocks dependency build scripts by default and exits non-zero
when any are skipped, which made task build fail at install:frontend:deps.
esbuild's postinstall is required to fetch the platform-specific binary.
* Updates rosenpass version
go-rosenpass v0.4.0 → v0.5.42 bump — detailed findings
Change summary
cunicu.li/go-rosenpass v0.4.0 → v0.5.42 (target)
cilium/ebpf v0.15.0 → v0.19.0 (transitive)
gopacket/gopacket v1.1.1 → v1.4.0 (transitive)
wireguard 2023-07 → 2023-12 (transitive)
wireguard/wgctrl 2023-04 → 2024-12 (transitive)
Wire interop
v0.4.0 (in v0.70.5) <-> v0.5.42 OK
v0.5.42 <-> v0.5.42 OK
Quantum resistance: true both ends
---
**Replay error eliminated.**
Before (on v0.4.0):
`ERROR Failed to handle message: failed to load biscuit (ICR1): detected replay`
Recurring every ~50ms for minutes at a time. Gone entirely after both ends upgraded to v0.5.42. Upstream fix in biscuit/replay handling between v0.4.x and v0.5.x series.
* Fixup [::]:port socket trying to send to v4
* Adds more tests on netbird<->rosenpass interactions
* Anticipates rp handler creation before generateConfig
* [client] Moves deterministic key gen into rosenpass
* go mod tidy
* Adds reminder to reason about rosenpass surface area
* Apply code rabbit suggestions
Exit nodes are mutually exclusive, but the RouteSelector stores routes with
default-on semantics, so every available exit node reported as selected at once.
Reconcile exit-node selection on each network map (and on runtime selection):
keep at most one selected — the user's persisted pick, else whatever management
marks for auto-apply (SkipAutoApply=false), else none. Never auto-activate an
exit node the map doesn't request; it stays off until the user picks it.
The server deselects sibling exit nodes when the user activates one (leaving
non-exit routes untouched), and the tray/React exit-node toggle now appends so
activating an exit node no longer wipes network-route selections.
Make the tray Exit Node submenu selectable (mutually exclusive, sourced from
ListNetworks by NetID) instead of read-only.
Add networksRevision to the status snapshot, bumped by the route manager on
network-map and selection changes, so the tray and the React NetworksContext
re-fetch ListNetworks via the push stream instead of polling. The peer-status
route list only carries chosen routes, so a candidate exit node appearing or
disappearing would otherwise never reach the UI.
Binding OnClick/OnRightClick to call OpenMenu() on macOS routes the menu
open through showMenu(), which runs the blocking [button mouseDown:] inside
a dispatched block on the serial main GCD queue. While the menu is open that
block never returns, starving every other main-queue task — both tray item
updates and the webview event delivery that drives React freeze until the
menu closes.
Revert to the pre-d9f0189 state: no click handlers bound, native NSStatusItem
auto-show for left-click, Wails default rightClickHandler for right-click.
refreshSessionExpiresLabel() is kept for the follow-up fix.
The menu reorganisation removed the About brand-mark bitmap and rerouted
every openRoute caller to WindowManager auxiliary windows, leaving both
the iconMenuNetbird embed (all three platforms) and the openRoute helper
unreferenced. Remove them so the unused linter passes.