Commit Graph
3396 Commits
Author SHA1 Message Date
Zoltan Papp 08a2b63675 [client] propagate exit-node deselect to synthesized v6 (::/0) route (#6296)
* [client] propagate exit-node deselect to synthesized v6 (::/0) route

When a client deselects an IPv4 exit node, the auto-generated IPv6 default
route (::/0) was still selected and pushed onto the tunnel interface, even
though the user disabled the exit node. On an exit node without a real IPv6
egress this blackholes IPv6 traffic, and because clients prefer IPv6 (happy
eyeballs) it can break general connectivity.

Root cause: the synthesized v6 route gets a different NetID than its v4 base
(base + "-v6"). The route selector keys deselects by NetID and defaults
unknown NetIDs to selected, so the "-v6" entry was never matched by the v4
deselect. The effectiveNetID() mirror that solves exactly this is used by
HasUserSelectionForRoute and FilterSelectedExitNodes, but categorizeUserSelection
called the raw IsSelected(), bypassing it and mis-categorizing the v6 pair as
user-selected.

Add RouteSelector.IsSelectedForExitNode(), which applies effectiveNetID before
the selection check, and use it in categorizeUserSelection. IsSelected() is left
untouched so non-exit code paths don't make unrelated "*-v6" routes inherit v4
state. Adds regression tests for the v4/v6 deselect mirror and explicit-v6
override.

* [client] add DIAG logging to trace exit-node v6 (::/0) route filtering

Temporary diagnostics to find why a deselected v4 exit node's synthesized
::/0 route still reaches the tunnel. Logs the full install path: incoming
client networks, route-selector state before/after the management-driven
update, what updateExitNodeSelections deselects/selects, and per-route
KEEP/SKIP/DROP decisions in FilterSelectedExitNodes and applyExitNodeFilter.
To be reverted once the real root cause is confirmed from a client log.

* [client] clear orphaned v6 exit selection when v4 pair is toggled

Root cause of the leaking ::/0 route, confirmed from client logs: the
synthesized "-v6" exit route could stay explicitly selected in the persisted
route-selector state while its v4 base was deselected (selected=[...-v6],
deselected=[...v4base]). Because the v6 entry then has its own explicit state,
effectiveNetID stops mirroring the v4 base, so FilterSelectedExitNodes keeps
::/0 and it is installed on the tunnel even though the user disabled the exit
node. This happened because the iOS SDK's deselect only pairs the "-v6" sibling
via ExpandV6ExitPairs when the v6 route is present in the current routesMap; a
deselect at a moment it wasn't expanded left the v6 selection orphaned.

Fix at the selector write path so it is independent of routesMap timing: when a
v4 exit NetID is selected or deselected, clear any orphaned explicit state on
its "-v6" sibling (clearPairedV6Locked), unless the sibling is part of the same
batch (the deliberate ExpandV6ExitPairs case). The v6 then falls back to
inheriting the v4 base via effectiveNetID, so a v4 deselect also drops ::/0 and
a v4 select brings both back.

Adds regression tests: a stale explicit v6 selection is cleared by a later v4
deselect, and an explicit v6 select made in the same batch is preserved.

* [ios] compute route connection status in the bridge

The iOS bridge exposed a route's Network as a possibly comma-joined string
("0.0.0.0/0, ::/0" for a merged exit node) but no connection status, forcing
the UI to infer status by string-matching that joined value against peer
routes — which never matched for the merged exit node, leaving it stuck as
not-connected. Android already computes status in the core (findBestRoutePeer).

Mirror that here: add a Status field to RoutesSelectionInfo and compute it from
the connected peers' route tables, matching the route's primary prefix, a merged
exit node's extra v6 prefix, or a dynamic route's domain pattern (the key the
route manager records). The UI can now read the status directly.

* [client] remove exit-node v6 DIAG logging and tidy routeselector

Drop the temporary DIAG diagnostics added to trace the leaking ::/0 route
(the root cause is fixed and confirmed). Also reorganize routeselector.go so
the exit-node helpers (clearPairedV6Locked, isExitNode) sit next to the
exit-node code paths and MarshalJSON/UnmarshalJSON are grouped together.

* [client] mirror v4 exit selection onto v6 pair at write time

The synthesized "-v6" exit route shares its v4 base's NetID plus a "-v6"
suffix. Selection state was reconciled at read time via effectiveNetID, a
mirror that could only be applied on exit-node code paths, which forced a
parallel IsSelectedForExitNode() alongside IsSelected() and a clearPairedV6Locked()
orphan cleanup on every toggle. That machinery still missed the case observed
in the field: a persisted state with the v4 base deselected but its "-v6"
sibling explicitly selected (orphaned). Because effectiveNetID returns the v6
entry itself once it carries explicit state, and clearPairedV6Locked only fires
on a live toggle, the loaded orphan survived and the ::/0 route leaked onto the
tunnel despite the exit node being disabled, breaking IPv6 (happy eyeballs).

Treat the v4/v6 exit pair as a single toggle and keep state consistent at write
time instead. RouteSelector.SyncPairedSelection forces the "-v6" entry to match
its v4 base unconditionally, resetting any orphaned explicit state. The route
manager, which knows the route prefixes, computes the pairs (V6ExitMergeSet) and
calls it from updateRouteSelectorFromManagement before selection is read, so both
collectExitNodeInfo and FilterSelectedExitNodes see consistent state, including
pairs loaded from persisted selector state.

This removes effectiveNetID, IsSelectedForExitNode and clearPairedV6Locked; the
selector is literal again and no longer needs the "exit-node paths only" caveat.
HasUserSelectionForRoute and applyExitNodeFilter use the raw NetID.

Adds a selector test for SyncPairedSelection (including the orphaned-v6 case) and
a route-manager test reproducing the persisted-orphan scenario from the field log.

* [client] add DIAG logging to trace v6 exit-pair mirror

The write-time mirror did not eliminate the leak in field testing. Re-add the
DIAG diagnostics around the exit-node selection flow to capture a fresh trace:

- UpdateRoutes: incoming client networks, selector state before/after the
  management update, and the networks remaining after FilterSelectedExitNodes.
- mirrorV6ExitPairSelections: the NetIDs present in this update and the v6 pairs
  V6ExitMergeSet derives from them (reveals whether the v4 base and its ::/0 pair
  are present in the same update so the pair can be matched).
- SyncPairedSelection: the base/paired state before and after the sync.
- FilterSelectedExitNodes / applyExitNodeFilter: per-route SKIP/KEEP/DROP and the
  selection lookups behind each decision.
- updateExitNodeSelections / logExitNodeUpdate: categorization and deselect set.

Temporary; to be removed once the root cause is confirmed.

* [client] remove v6 exit-pair mirror DIAG logging

Drop the temporary DIAG diagnostics added to trace the v4/v6 exit-pair mirror.
The field log confirmed the write-time mirror keeps the pair consistent (the
::/0 route is only ever applied alongside its v4 base and is dropped on deselect),
so the diagnostics are no longer needed.
2026-06-16 12:27:58 +02:00
Eduard Gert 4ae37919c0 keep general settings tab visible when update settings are locked 2026-06-16 12:24:06 +02:00
Eduard Gert 951e08e35c add mdm helper (windows) (can be deleted later) 2026-06-16 12:11:47 +02:00
Eduard Gert 64c9551a4d remove "Loading..." from profile dropdown and show skeleton 2026-06-16 12:09:49 +02:00
Eduard Gert 44156b6256 mdm test script (can be deleted later) 2026-06-16 11:33:10 +02:00
Eduard Gert ebaaddc14b show macos dock icon when any window is visible 2026-06-16 11:32:04 +02:00
Maycon Santos b3f9e6588a [management] sync openapi spec and test for diff on workflows (#6437)
* [management] sync openapi spec and test for diff on workflows

* [management] pin oapi-codegen version to v2.7.1
2026-06-15 17:53:25 +02:00
Pascal Fischer 967e2d6864 [management] network map for affected peers (#6105) 2026-06-15 17:43:22 +02:00
Zoltan Papp e7c1d364c3 [management] treat ci- builds as development for remote jobs (#6436)
* fix(management): treat ci- builds as development for remote jobs

CI snapshot builds use a "ci-<sha>" version string that did not match
IsDevelopmentVersion, so the remote-jobs minimum-version gate rejected
them. Recognize the "ci-" prefix as a development build.

* fix(management): treat dev- builds as development for remote jobs

Dev snapshot builds use a "dev-<sha>" version string that did not match
IsDevelopmentVersion, so the remote-jobs minimum-version gate rejected
them. Recognize the "dev-" prefix as a development build, alongside the
existing "ci-" prefix.
2026-06-15 17:22:40 +02:00
Viktor Liu a44198fd77 [client] Add dialWebSocket method to WASM client (#5980) 2026-06-15 16:43:24 +02:00
Viktor Liu b57f714350 [client] Drop signaling-side ICE candidate filter, drop overlay STUN at mux read-side instead (#6142) 2026-06-15 16:37:03 +02:00
Viktor Liu f893abc41d [client] Recover from tun device read/write panics and restart the client (#6419) 2026-06-15 16:36:00 +02:00
Eduard Gert 28d85ecae3 adjust offset on macos 2026-06-15 16:27:44 +02:00
Eduard Gert c37f24d1b0 adjust vertical offset on macos 2026-06-15 16:13:03 +02:00
Eduard Gert c7a338c006 update config on config changed event 2026-06-15 16:13:03 +02:00
braginini 8f200e8489 Change browser login wording 2026-06-15 15:46:33 +02:00
Eduard Gert 780296a7db force disable spellcheck for inputs 2026-06-15 14:53:37 +02:00
Eduard Gert 91cbae8910 fix session expired dialog 2026-06-15 14:15:39 +02:00
Eduard Gert 4f72be6a42 fix mdm advanced view 2026-06-15 14:00:05 +02:00
Eduard Gert 83c9e5b0d1 fix allowserverssh mdm 2026-06-15 13:54:07 +02:00
Eduard Gert 47efdd839c Merge remote-tracking branch 'origin/ui-refactor' into ui-refactor 2026-06-15 13:20:34 +02:00
Eduard Gert 90be56788a fix mdm allowserverssh 2026-06-15 13:20:16 +02:00
Zoltán Papp 715a2b0943 docs(contributing): update UI build guidelines for Wails v3
The UI client migrated from Fyne to Wails v3 + React. Update the
Requirements and Build sections accordingly, fix the Windows installer
UI build command to use the task runner, bump the Go version to 1.25,
and drop the now-redundant client/ui/README.md.
2026-06-15 13:19:45 +02:00
Zoltán Papp b6246e7c02 refactor(ui): extract critical severity into a constant
The critical-event gate compared against a bare "critical" literal while
the matching string is produced from the proto SystemEvent_CRITICAL enum in
systemEventFromProto. Add services.SeverityCritical next to the Status*
constants so the two sides share one definition.
2026-06-15 12:31:23 +02:00
Zoltán Papp 37eb150535 fix(ui): match severity gate case-insensitively
The critical-event gate used a case-sensitive == "critical" where every
other status/severity compare in the UI uses strings.EqualFold, so a
daemon-side reword to a different case would silently drop the bypass.
2026-06-15 12:29:50 +02:00
Zoltán Papp 6c15e8b51c fix(ui): close session bus when XEmbed host fails to start
newXembedHost failure left the private session bus open, leaking an fd
per RegisterStatusNotifierItem on systems without an XEmbed tray. Add a
closeBus helper that logs on close failure and use it at every back-off
path instead of discarding the error.
2026-06-15 12:27:46 +02:00
Zoltán Papp b0f08645c3 fix(ui): install X11 error handler so tray races don't kill the UI
Xlib's default protocol-error handler calls exit() on any async X error,
so a tray race (tray manager window dying between find and dock, XMoveWindow
on a popup the WM already destroyed) would take the whole UI process down.

Install process-global logging no-op handlers via XSetErrorHandler/
XSetIOErrorHandler after every XOpenDisplay. Since the handler slot is
process-global and GDK init can clobber it, additionally wrap the popup
code's raw Xlib calls on GDK's Display in gdk_x11_display_error_trap_push/
pop_ignored, which is independent of the global handler.
2026-06-15 12:26:53 +02:00
Lee Sang Hoon 60067619a1 [proxy] Keep custom TCP listeners alive after mapping batches (#6415) 2026-06-15 12:21:24 +02:00
Eduard Gert 2089b79630 show profiles also when mdm update setting is active 2026-06-15 12:20:28 +02:00
Zoltan Papp c68bb8f99f fix(ui): resolve active profile before GetConfig in GetRestrictions
GetRestrictions called the daemon GetConfig with an empty
GetConfigRequest, which the daemon has rejected since multi-profile
support landed ("active profile name is empty") because it reads the
profile name from the request. Resolve the active profile via
GetActiveProfile first and pass its name/username, mirroring the
working Settings.GetConfig path. GetActiveProfile self-heals to the
default profile, so a valid name is always supplied; the
profile-switch-disabled MDM flag is orthogonal and unaffected.
2026-06-15 12:04:50 +02:00
Zoltan Papp 9e5bcb26eb fix(ui): include error name and message in forwarded logs
WebKit (macOS WKWebView) omits the "Name: message" header from
Error.stack, so forwarding only the stack hid the real cause of
failures. Prepend the error name, message and optional cause before
the stack so daemon RPC errors surface in gui-client.log.
2026-06-15 11:57:34 +02:00
Zoltan Papp c9aed1a7c4 Merge branch 'main' into ui-refactor 2026-06-15 11:49:35 +02:00
Zoltan Papp 2959979f37 chore(ui): bump wails/v3 to v3.0.0-alpha.102 2026-06-15 11:44:44 +02:00
Zoltán Papp 7b08e750d5 feat(ui): hide macOS Dock icon, run as tray-only accessory app 2026-06-15 11:26:57 +02:00
Bethuel Mmbaga cd777395f2 [management] Skip JWT group evaluation for embedded-IdP local users (#6422)
When JWT group sync is enabled with a restrictive JWTAllowGroups list, the local owner of an embedded-IdP (Dex) deployment can get locked out. The allow-groups check runs account-wide but local password users do not receive
external IdP group claims, so they can't satisfy the allowed list.

This skips JWT group evaluation for local Dex users so the restriction and JWT group sync continue to apply to external-IdP users as intended.
2026-06-15 12:01:54 +03:00
Eduard Gert 79e0a05f52 fix psk behaviour and allow to clear psk 2026-06-15 10:43:48 +02:00
Zoltan Papp ec366163e3 ci(codespell): skip translated locale dirs and TRANSLATING.md
Non-English UI translations and the foreign-language examples in
TRANSLATING.md trip codespell on real foreign words. Extend the skip
list to cover es, fr, it, pt, ru, zh-CN locale dirs and the
TRANSLATING.md doc, keeping en/common.json as the source of truth.
2026-06-13 02:24:22 +02:00
Zoltan Papp 2d54c7f088 routeselector: remove duplicate IsDeselectAllActive
IsDeselectAllActive was identical to IsDeselectAll (both return the
deselectAll flag under a read lock). The two were introduced in parallel
on separate branches for the same fix - guarding against management route
sync overriding a user's explicit deselect-all - and ended up coexisting
after the branches merged.

Drop IsDeselectAllActive, keep the canonical IsDeselectAll from main, and
move its richer doc comment over. Update callers accordingly.
2026-06-13 00:50:42 +02:00
Zoltan Papp a1a24bd777 Add in-body comment to startStatusNotifierWatcher no-op stub 2026-06-13 00:42:42 +02:00
Zoltan Papp f0ac331fdc [client/server] fix connect retry test for PermissionDenied short-circuit
Commit 8841b950a made connectWithRetryRuns stop after a single login
attempt on PermissionDenied, but TestConnectWithRetryRuns still asserted
the loop retries 3+ times, so it failed with counter=1.

Repurpose the test (now TestConnectStopsRetryOnPermissionDenied) to
verify the loop stops after exactly one login on PermissionDenied,
keeping the fast retry env config that would otherwise drive several
attempts. Also redirect profile paths to a temp dir so it runs without
root.
2026-06-13 00:39:59 +02:00
Zoltan Papp ae3f5b1088 Reorder UI service files to follow member-ordering convention
Group all type declarations at the top, keep each type's methods with it,
move package-level helpers and static functions to the end of the file.

- connection.go: ClientError methods sit directly under the struct; the
  classifyDaemonError / translateShort helpers move to the file end.
- windowmanager.go: the title / retitleAll / hideOtherWindowsLocked /
  restoreHiddenWindowsLocked / getScreenBasedOnCursorPosition helpers and the
  errorDialogURL / u32ptr static functions move to the file end; u32ptr no
  longer splits the const/var block.
2026-06-13 00:37:00 +02:00
Zoltan Papp edf7e2d04d Tighten verbose comments in Wails UI Go code
Shorten over-long godoc/inline comments across the client/ui tray and
services code: drop narrative restatement, legacy-Fyne tangents, and text
already evident from signatures and names. Keep only the non-obvious why
(concurrency/lock ordering, platform quirks, ordering constraints, the
profile-switch state table). No code changes.
2026-06-13 00:22:27 +02:00
Eduard Gert c2b43b9cf0 localize installer unknown-error fallback and abort debug bundle on unmount 2026-06-12 18:06:58 +02:00
Eduard Gert 8f2429c637 move debounced save out of SettingsContext setState updater 2026-06-12 18:06:38 +02:00
Eduard Gert 6cc6d68b37 add mdm 2026-06-12 18:00:28 +02:00
riccardom c4d4208ea6 Template alignment 2026-06-12 16:13:23 +02:00
riccardom aae6a924e2 Adds DisableAdvancedView optional MDM flag 2026-06-12 16:07:12 +02:00
Zoltán Papp d3f62458d2 fix(ui): route tray status repaints through relayoutMenu to fix item races
buildMenu reassigns every menu item pointer under menuMu, but the status
path (refreshMenuItemsForStatus, applyStatusIndicator), the click handlers
and the session ticker read them unguarded. Wails dispatches event
listeners and OnClick callbacks on fresh goroutines, so these reads race
the rebuilds and applyStatus even runs concurrently with itself.

- applyStatus drives a single aggregated relayoutMenu (status, daemon
  version and session-deadline changes); refreshMenuItemsForStatus is
  removed — relayoutMenu repaints the same items from the caches
- applyStatusIndicator is SetBitmap-only: its unguarded SetMenu could
  reinstall a stale tree and pushed a half-built one on every relayout
- Connect/Disconnect handlers receive the clicked item from the buildMenu
  closure instead of reading the menuMu-guarded fields
- applySessionExpiry is cache-only; the row is painted by relayoutMenu,
  the 30s ticker snapshots its item under menuMu
- drop loadProfiles' dead unguarded profileSubmenu nil-check
2026-06-12 15:36:36 +02:00
Zoltán Papp 381532d166 Apply gofmt 2026-06-12 15:32:51 +02:00
Zoltan Papp 47ecc41bf4 Merge main into ui-refactor; port MDM support to the Wails UI
Integrates main's MDM configuration-profile feature and adapts it to the
Wails UI (this branch had already replaced the Fyne UI).

Conflict resolution:
- go.mod/go.sum: take main's deps; howett.net/plist pinned to v1.0.2-... (tidy)
- client/proto/daemon.pb.go: regenerated from the merged daemon.proto
- client/internal/peer/status.go: union of ipToKey (main) + sessionExpiresAt (HEAD)
- client/server/server.go: main's intent/liveness model (connectionGoroutineRunning,
  clientRunning no longer cleared by the goroutine) + empty-PSK guard
- client/ui/client_ui.go, client/ui/profile.go: removed (dead Fyne UI)

MDM port (backend + tray):
- services/settings.go: expose MDMManagedFields plus a managedFields map keyed
  by Config field names so the settings form can gate a control without
  translating mdm.Key* names
- tray: gate Profiles / Exit Node menus on DisableProfiles / DisableNetworks via
  GetFeatures, refreshed on the config_changed system event (replaces the legacy
  2s poll); localized MDM policy-applied toast in all shipped locales
- client/proto/metadata.go: shared constants for the config_changed /
  policy_applied event markers

PreSharedKey: GetConfig now returns preSharedKeySet (bool) instead of the masked
value; the settings form provides its own placeholder and sends a new key only
when the user types one.
2026-06-12 15:27:23 +02:00