Commit Graph

229 Commits

Author SHA1 Message Date
Zoltán Papp
e9a7d62229 [client] Return innermost gRPC status from WaitExtendAuthSession
The daemon wrapped the management ExtendAuthSession error through two
%v/%w layers and remapped it to codes.Internal, so the UI surfaced the
full wrapped chain instead of the root cause. Forward only the innermost
gRPC status (original code + clean desc) to the client and log the full
chain. gstatus.FromError does not unwrap, so add innermostStatus to walk
the %w chain.
2026-06-18 13:37:47 +02:00
Zoltan Papp
a6d1194b5d Merge branch 'main' into ui-refactor
Resolve conflicts and complete the profile display-name -> ID migration
across the daemon, CLI, UI services, tray, and React frontend.

- Regenerate client/proto daemon.pb.go from the merged proto so it carries
  both branches' RPCs (ui-refactor: SubscribeStatus/RegisterUILog/ExtendAuthSession,
  main: RenameProfile + id fields); keep the v6.33.1 generator header
- server.go: combine ui-refactor's profile-list-changed events with main's
  id-bearing responses; publish profile-list-changed on rename
- Drop the deleted Fyne client_ui.go/profile.go and port main's profile-ID
  changes into the refactored services/tray
- UI services, tray and React: send the profile ID as the daemon handle and
  keep the display name for rendering only (activeProfileId vs activeProfile)
- Relax the profile-name input to match the daemon's sanitizeDisplayName
  (spaces, emoji, any valid UTF-8); cap at 128
- Expose RenameProfile via a Profiles.Rename services wrapper (+ regenerated
  bindings) for the frontend to wire up
- cmd/login.go: use the profile ID for GetProfileState
2026-06-18 11:40:51 +02:00
Theodor Midtlien
ee360963f9 [client] Migrate profile identity from display name to ID and allow renaming of profiles (#6367)
* Migrate to profile ids

* Migrate android profile manager

* Clean up

* Fix review

* Add ID type

* Fix test and runes in ShortID()

* Fix profile switch on up and android comments

* Revert android profile to string id

* Fix feedback

* Fix UI feedback

* Fix id assignment

* Add renaming of profiles

* Fix review

* Remove ui binary
* Fix getProfileConfigPath not validating id

* Change resolve handle order and fix server merge problems

* Fix mdm test
2026-06-18 08:49:19 +02:00
Zoltán Papp
6881c0f985 [client] Apply lazy connection toggle to running engine
The daemon SetConfig only persisted the lazy connection flag to the
profile config; the running engine was untouched, so a UI/CLI change
took effect only after a down/up or daemon restart.

Wire SetConfig to push the change into the running engine via a new
ConnMgr.SetLocalLazyConn, which sets enabledLocally like an env/CLI
flag so a later management sync cannot override it, and starts or
stops the lazy manager in place.
2026-06-17 16:51:01 +02:00
Zoltán Papp
fedef7588f ui: detect outdated daemon via WailsUIReady probe
Add a no-op WailsUIReady RPC the UI probes once at startup. A reachable
daemon that returns Unimplemented predates this UI and is too old to
drive it; the probe distinguishes that from an unreachable daemon. On an
outdated daemon the UI fires a localized OS notification, since the main
window may not open to show an in-app error.

Proto regenerated with protoc 33.1 to keep the v6.33.1 header.
2026-06-17 13:00:13 +02:00
Eduard Gert
79e0a05f52 fix psk behaviour and allow to clear psk 2026-06-15 10:43:48 +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
riccardom
aae6a924e2 Adds DisableAdvancedView optional MDM flag 2026-06-12 16:07:12 +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
Riccardo Manfrin
2bcea9d582 [client] add MDM configuration profile support (Windows registry + macOS plist) (#6374)
* Initial scaffolding

* Applies MDM override

* Unit tests

* Helpers business logic

* Return error if trying to modify any config that is gated by MDM

* Add ManagedFields to returned config over GetConfig

* Adds initial 101 MDM policy business logic testing

* gRPC MDM changes

* MDM Name scoping for clarity

* Implements windows loading of MDM policy

* Adds missing WGPort config

* Cleanup setupKey to align to linear

* Align split tunnel code

* Adds some log

* Prefix every log with MDM

* Adds debug config cobra command

This can be useful for troubleshooting and checking config
now that its resolution is not trivial

defaults > config > env cars > CLI/UI > MDM

* Adds MDM 1m diff checker & reloader

* Adds also up/start after cancel

* Publishes event for UI to sync upon MDM changes

* Add events to resync UI to actual config

This also provide fixup for UI no aligning to changed config when coming from cli up with config flags.

* UI behavior conflicts relaxation

UI sends full config snapshot with all values. It doesn't
make sense to block it if the values are aligned with the
values constrained by the MDM policy. It's just simplier
to allow values that are compliant. (this goes for the CLI
as well at this point)

* Lock toggle Settngs

* Advanced Settings locking

* Fixup presharedkey

* Apply MDM locks

* Toggle gray in/out for Advanced Settings

* Adds support for disabling of Profiles and UpdateSettings feature flags

* Adds Gate Login as well when --disable-update-settings=true is given to service

This commit tries to settle things with an old PR-4237 which had relaxed
the case where the SetConfig returned an `Unavailable` code error.

Under this circumnstance the PR allowed the upFunc to just emit a warning and
progress further with the login gRPC. Since the login call is consuming
the --management-url coming from the `up` command, it might be possible
to abuse the "Unavailable" code to inject a management URL that is different
from the configured one even though the --disable-update-settings is set
to true (?)

* Evaluate disable-update-settings errors only when there's an actual override

* [UI] Fixup advanced Settings

* [UI] Fixup for preshared key

* [UI] Fixup for profile enable/disable toggle

We need to align the initial state to evaluate the delta in case.

The initial state has to be "true" since the profile starts visible.
Then we receive MDM and transition the cache bool value to the actual
MDM imposed state

* Enforces disable networks

* [UI] Aligns to "enable/disable once on change only"

* Fixup: MDM wins. always

* Removes --disable-advanced-settings

It was a typo in our meetings. the actual thing is --disable-update-settings

* [PROTO] Removes --disable-advanced-settings

* [UI] Removes --disable-advanced-settings

* Pins feat profile retrieval to notif event

* [UI] Fix for "hide" not working when propagating to parent with children

* Adds dep for reading plist files

* Introduces support for darwing plist loading

* Tests MDM config reload via ticker

* [PROVISIONING] ADMX/ADML/PS/bash scripts/templates

* CI fixes

- Add docstrings to `mdm_integration`
- refactor for cognitive complexity
- mod tidy

* Linting

* Add docstrings to `mdm_integration`

* nil,nil is no policy and no error. Allow it

* nil,nil is no policy and no error. Allow it

* exclude MDM profile adminstrated keys data from debug bundle

* Fixes Rosenpass left disable after MDM unlock

* Partial revert coderabbit added docstrings

* Renaming fix

* Avoid locking on clientRunning bool when the connection is aborted for whatever reason

We want to just signal this through the giveUpChan, we will manage the signal from
the waiter side and in case set it to false there. THis way we avoid locking,
which should allow the MDM down+wait_for_term_chan_signal_+up procedure

clientRunning is used to signal two different conditions here:

1. the initialization procedure is over (we have an engine)
2. the connection being up (or being attempted)

Probably these two functionalities should not alias, and the failure of the second condition
(because of any error) should just drive a reconnection (currently it's not happening,
and we silently go idle).
OR, mor probably, the two things are the SAME and there should not exist a case where
we did the "Up" initialization and connection attempt but we are not still attempting it.

* Moves test helper at te very bottom

* Addresses github comments

* No lock no copy

* Prevents engine not stopping within 10 secs from being paired by another instance

We instead juts SKIP updating the policy, so
1. the MDM ticker will kick in 1 minute time,
2. find the policy misaligned,
3. enter the onMDMPolicyChange,
4. find the s.clientRunning == true
   (because it is set to false only in server cleanupConnection,
   and not by s.actCancel())
5. call s.actCancel() again if not nil
6. immediately return from <-s.clientGiveUpChan
7. finally call s.restartEngineForMDMLocked()

* Since we ARE running there should be a config

If the config was cancelled midflight, connect will abort later on

* DisableAutoConnect should not stop a running connection.

DisableAutoConnect should just avoid the connection attempts *when the service starts*.
If we are started and we are up and running, DisableAutoConnect should not kick in.

Another PR will follow about this topic

* Removes unused vars

* Moves callback into Run method arg

* align comment to removal of DisableAutoConnect

DisableAutoConnect should just avoid the connection attempts *when the service starts*.
If we are started and we are up and running, DisableAutoConnect should not kick in

* Removes unused managed_fields data.

This was initially used to drive the UI but approach changed
to reload config/features upon notifications which makes this data redundant.

* Reorder stuff

* Unexport unrequired vars/functions

PoliciesEqual → policiesEqual
AllKeys → allKeys

* Adds list of MDM managed fields in the debug bundle
2026-06-12 12:28:49 +02:00
Zoltán Papp
ff6aef5e2a feat(ui): GUI debug logging follows daemon log level + debug bundle
When the daemon is set to debug/trace, the GUI now automatically writes a
rotated gui-client.log in the user's config dir and the daemon's debug bundle
collects it. The UI learns the level both at startup (daemon already in debug)
and live, by piggybacking the existing SubscribeEvents stream: the daemon
publishes a marked log-level-changed SystemEvent (and a per-subscription
snapshot), which DaemonFeed routes to guilog.DebugLog instead of an OS toast.
The UI registers its log path via a new RegisterUILog RPC so the root daemon,
which can't resolve the user's config dir, knows where to find the file.

Manual --log-file (any value) disables the daemon-driven file logging.

Fix: client/ui SetLogLevel looked up proto.LogLevel_value with the lowercase
logrus name, which never matched the uppercase enum keys and silently fell back
to INFO — so trace/debug requests from the bundle flow had no effect.
2026-06-11 18:34:48 +02:00
Zoltán Papp
b598274424 [client/ui] Refresh profile list on CLI profile select
SwitchProfile now publishes the same profile-list-changed event that
AddProfile/RemoveProfile already emit. The daemon emits no dedicated
profile RPC event, and the React ProfileContext only refreshes on
EventProfileChanged (unlike the tray, which also re-fetches on every
status-string transition via loadProfiles). So a CLI-driven
"netbird down; profile select X; netbird up" refreshed the tray (the
down/up status flips trigger loadProfiles) but left the React profile
dropdown stale, since the select path never surfaced an event.

Publishing the marked INFO/SYSTEM event from SwitchProfile closes that
gap: dispatchSystemEvent re-emits EventProfileChanged, which
ProfileContext.refresh already subscribes to. No proto change.
2026-06-08 17:26:54 +02:00
Zoltán Papp
21f1142355 Merge branch 'main' into ui-refactor
# Conflicts:
#	client/ui/debug.go
#	go.mod
#	go.sum
2026-06-05 17:50:18 +02:00
Zoltán Papp
0ce3fbf5af [client] Tolerate already-deleted peer on profile logout, clear stale email
logoutFromProfile failed hard when the management server returned NotFound
(peer already deleted from the dashboard), blocking both profile logout and
profile removal. Treat NotFound as success — the peer is already gone, so
deregistering it is already satisfied.

Also drop the user-side per-profile state file on logout. The account email is
sourced from <profile>.state.json (written by the CLI after SSO login), which
the root daemon can't reach, so logout left a stale email showing in the UI.
Connection.Logout now removes it from the UI process after a successful logout;
the next SSO login recreates it.
2026-06-05 12:05:41 +02:00
Theodor Midtlien
512899d82d [client] Prevent corruption from competing log rotation and improve debug bundle (#6214)
* Adds heuristic to detect an edge case on Linux where a system has configured logrotate as a separate service to rotate log files which would mangle our client log files. If we detect logrotate being configured for netbird, we disable our rotation.

* Adds new env var to disable log rotation: NB_LOG_DISABLE_ROTATION

* Adds compressed and plain logrotate files to debug bundle.

* Replaces lumberjack with timberjack (maintained fork with bug fixes and extra features).

* Clarifies which daemon version is running in the bundle stats.

* Change logging for client service status to console
2026-06-04 17:36:45 +02:00
Zoltán Papp
e298747203 [client/ui] Refresh profile list in tray + UI on CLI profile add/remove
The daemon emits no dedicated profile-changed RPC event, and a profile
add/remove doesn't move the connection status, so the UI's SubscribeStatus
path never fired for CLI-driven `netbird profile add|remove` (and the tray's
iconChanged guard would swallow it anyway). The tray menu and the React
profile list stayed stale until the next status-string transition.

AddProfile/RemoveProfile now publish a marked INFO/SYSTEM event over
SubscribeEvents (metadata kind=profile-list-changed, empty userMessage so it
stays silent). The UI's dispatchSystemEvent recognises the marker and
re-emits the existing EventProfileChanged, which the tray's loadProfiles and
React's ProfileContext.refresh already subscribe to — so both surfaces
refresh from a single signal that originates in the shared daemon handler
(covering both CLI and UI-initiated removals). No proto change.

Also drop a stray, build-breaking `app.Updater` line in main.go.
2026-06-04 16:03:22 +02:00
Zoltán Papp
166c6118e2 [client] Fix Up failing with NeedsLogin after SSO login
After a successful WaitSSOLogin the daemon deliberately stays in
StatusNeedsLogin, and after a mid-session expiry (peer kicked out by the
management server) the engine tears down with clientRunning == false. In
both cases the caller's Up takes the fresh-start branch, which only
accepted StatusIdle and rejected NeedsLogin with
"up already in progress: current status NeedsLogin".

This forced a second Up to actually connect (CLI: re-run `netbird up`;
GUI: click Connect again). Treat NeedsLogin as a legitimate fresh-start
entry state and reset it to Idle before starting the engine, so the
first Up after login drives Connecting -> Connected directly.
2026-06-03 17:19:52 +02:00
Zoltán Papp
60c86c63aa client/server: throttle and single-flight health probes
Status(GetFullPeerStatus=true) RPCs trigger a full health probe
(network round-trips to management, signal and the relays). The
desktop UI issues these frequently and concurrently, and a burst of
parallel Get() calls each fired its own probe — the lastProbe guard
was unprotected against concurrent access and only advanced when every
component was healthy, so a sustained unhealthy state (e.g. relay down)
disabled the throttle entirely and let every call re-probe.

Extract the throttle/single-flight policy into probeThrottle:
  - single-flight: only one probe runs at a time; concurrent callers
    that piled up while it ran share its result instead of each
    launching another, even when that probe failed.
  - throttle: lastOK only advances on a fully successful probe, so
    while anything is unhealthy callers keep probing frequently and
    notice recovery quickly (preserved from the original design).

RunHealthProbes now takes a context so a caller that gives up (e.g. a
Status RPC whose client disconnected) cancels the in-flight STUN/TURN
probe instead of letting it run to its per-component timeout. The
engine's own lifetime ctx still applies independently.
2026-06-01 21:07:12 +02:00
Zoltan Papp
18348e1491 client+ui: remove SSO handoff flicker and clean up abandoned login via context
Two follow-ups to the "hold NeedsLogin during the SSO browser wait" change.
Both target the visible state churn the tray showed during the auto-login
handoff (Connect / profile-switch lands on NeedsLogin -> the UI's startLogin
kicks off the SSO flow) and the broken recovery after the user dismisses the
browser-login popup with the window's X.

Background
----------
When a connect attempt lands on NeedsLogin, the UI's startLogin() drives the
SSO flow: Connection.Login() -> (NeedsSSOLogin) open the browser-login popup
-> Connection.WaitSSOLogin() blocks until the browser leg completes. The tray
and the React status page both paint the raw daemon status, so any transient
state the daemon publishes during this handoff is visible as a flicker.

Previously the handoff churned the daemon status through
  NeedsLogin -> Idle -> Connecting -> NeedsLogin
which read as a flicker on the tray icon and the status dot. Two distinct
sources produced the two intermediate states:

  * Idle       came from the UI's defensive cli.Down() at the top of
                Connection.Login (services/connection.go): it tore the engine
                down before every login to dislodge a possibly-parked
                WaitSSOLogin, emitting a StatusIdle on the way.
  * Connecting  came from server.go Login() unconditionally setting
                StatusConnecting before deciding whether the request is an
                SSO flow (which immediately returns NeedsLogin) or a
                setup-key flow (which actually dials Management).

Changes
-------
1. server.go Login(): only set StatusConnecting on the setup-key path, where
   we are about to dial Management with the key and the Connecting paint is
   meaningful. The SSO path returns NeedsLogin and parks on the browser leg,
   so it no longer flashes Connecting first. Removes the Connecting blip.

2. services/connection.go Login(): drop the pre-Login cli.Down(). The daemon
   already dislodges a pending WaitSSOLogin at Login entry (actCancel), and an
   abandoned browser leg is now torn down by cancelling the WaitSSOLogin RPC
   (see 3/4). Removing the Down removes the Idle blip on every login.

3. MainConnectionStatusSwitch.tsx startLogin(): on cancel (the browser-login
   popup's Cancel button or its window X, both routed through
   EventBrowserLoginCancel), cancel the in-flight WaitSSOLogin gRPC call via
   waitPromise.cancel() instead of issuing a heavy Connection.Down(). The
   daemon ties the wait to this call's context, so cancelling the call ends
   the wait cleanly with no engine teardown and no Idle paint.

4. server.go WaitSSOLogin(): when the wait unblocks with context.Canceled and
   the cancellation came from our caller (callerCtx.Err() != nil — the client
   cancelled the RPC or went away), clear the cached oauthAuthFlow so a fresh
   Login starts a new device code instead of reusing the abandoned one. The
   entry NeedsLogin stays in place, so a reattaching client still shows the
   login affordance. An internal abort (actCancel fired by a newer
   Login/WaitSSOLogin while our callerCtx is still live) is left untouched so
   the new owner's flow is not clobbered.

Effect
------
The auto-login handoff now goes Connected -> Connecting -> NeedsLogin and
holds, with no Idle/Connecting flicker in between. Dismissing the browser-login
popup with X now recovers the same way as the Cancel button: the WaitSSOLogin
RPC is cancelled, the stale OAuth flow is cleared, and the next connect opens a
fresh browser-login window instead of getting stuck.
2026-05-31 04:26:15 +02:00
Zoltan Papp
0b484133b2 client: hold NeedsLogin during SSO browser wait and tie it to the caller
WaitSSOLogin set StatusConnecting on entry and ran the browser wait on
rootCtx. If the client that drove the login went away mid-wait (UI restart,
CLI Ctrl+C), the wait orphaned on rootCtx until the OAuth device-code window
expired, and the daemon stayed stuck reporting Connecting — a reattaching
client saw a spinner that never resolved instead of a login prompt.

Hold StatusNeedsLogin for the whole browser wait (also in the Login
cached-flow path) so any client attaching mid-wait reads 'login required',
and bridge the wait to callerCtx so a departing client cancels it. On that
cancel the defer leaves NeedsLogin in place, so the next client shows the
login affordance instead of a stale Connecting.
2026-05-31 03:45:13 +02:00
Zoltán Papp
e94a4cbce5 session-extend: preempt previous WaitExtendAuthSession on new wait
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.
2026-05-28 19:17:46 +02:00
Zoltan Papp
966fbec119 routemanager: enforce a single selected exit node
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.
2026-05-27 20:48:16 +02:00
Zoltan Papp
f693d268b4 tray: selectable exit nodes + push-based network list refresh
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.
2026-05-27 20:48:16 +02:00
Zoltan Papp
13179081d2 Merge branch 'main' into ui-refactor 2026-05-26 23:41:18 +02:00
Bethuel Mmbaga
14af179556 [management] Refactor management server bootstrap (#6256) 2026-05-26 17:44:28 +03:00
Zoltán Papp
ef6b4f7538 add SSO session extend flow
Adds an end-to-end SSO session-extension feature: the management server
publishes per-peer session deadlines on every Login/Sync, a new
ExtendAuthSession RPC refreshes the deadline using a fresh JWT without
tearing down the tunnel, and the daemon tracks the deadline locally so
the UI can fire a T-10min warning toast with an interactive "Extend now"
action.
2026-05-20 16:43:14 +02:00
Zoltán Papp
f468f15a30 Merge branch 'main' into ui-refactor
# Conflicts:
#	client/ui/network.go
2026-05-18 10:24:31 +02:00
Zoltan Papp
c0e7c61c4b [client] Close giveUpChan in connectWithRetryRuns defer
The trailing close(giveUpChan) at the bottom of the function only ran on
the backoff.Retry path. The DisableAutoConnect path returned early via
the if-block, skipping the close entirely. That branch is hit whenever
the active profile has auto-connect disabled — so every Down for those
profiles waited the full 5s timeout in the Down RPC select (and twice
when two Downs queued up, since both snapped the same never-closing
chan).

Move close(giveUpChan) into the existing defer so it fires on every
exit path: DisableAutoConnect return, backoff.Retry return, or panic.
The close happens after clientRunning=false is committed under the
mutex, so a Down/Up that wakes on the chan-close doesn't observe a
half-state where the chan is closed but clientRunning is still true.

Updates the Down RPC comment to point at the deferred close as the
signal source, and reframes the 5s timeout warning as "the goroutine
is wedged in a slow teardown step" rather than the expected case.
2026-05-14 15:44:15 +02:00
Zoltan Papp
d841a6aa07 [client] Push status snapshot on every state.Set and classify SSO errors
Two related daemon-side status-stream fixes that together keep the UI's
status in sync with the daemon's contextState:

* state.Set previously only mutated the in-memory enum — transitions
  that weren't accompanied by a Mark{Management,Signal,...} call (e.g.
  StatusNeedsLogin after a PermissionDenied login, StatusLoginFailed
  after OAuth init failure, StatusIdle in the Login defer) left the
  UI stuck on the previous snapshot until an unrelated peer event
  happened to fire notifyStateChange. Add a callback on contextState
  fired from Set (outside the mutex, to avoid lock-order issues with
  the recorder's stateChangeMux), and wire it in Server.Start to the
  recorder's new public NotifyStateChange. Every state.Set callsite
  now pushes automatically; new ones don't need to opt in.

* WaitSSOLogin's WaitToken error branch lumped every failure into
  StatusLoginFailed, including context.Canceled aborts from a parallel
  profile switch (actCancel/waitCancel). That spurious LoginFailed
  then wedged the new profile's Up RPC with "up already in progress:
  current status LoginFailed". Split the branch by error type:
  context.Canceled lets the top-level defer pick StatusIdle,
  context.DeadlineExceeded sets StatusNeedsLogin (retryable; OAuth
  device-code window just expired), other errors keep LoginFailed
  (real auth/IO failures). Document the full state-transition table
  in the function godoc.
2026-05-14 14:51:51 +02:00
Zoltan Papp
53b2fb8dc1 [client/ui] Add async Up mode to avoid blocking profile switches
The daemon's Up RPC previously always blocked in waitForUp (up to 50s)
until the engine connected. The UI does not need this — status updates
already flow through the SubscribeStatus stream.

Add bool async = 4 to UpRequest. When true the daemon starts
connectWithRetryRuns and returns immediately; the CLI path (async=false,
the default) is unchanged.

ProfileSwitcher.SwitchActive now sets Async:true so all three RPCs
(Status, Switch, Down, Up) return quickly. The background goroutine and
its associated race condition are removed entirely.
2026-05-13 15:51:36 +02:00
Zoltan Papp
c0cd88a3d0 [client/ui] Fix stale LoginFailed/NeedsLogin state after profile switch
When the active profile was in LoginFailed, NeedsLogin, or SessionExpired,
switching to another profile left the daemon holding stale management/signal
errors. The new profile inherited the error state from the previous one.

Two fixes:
1. server.go Down(): reset statusRecorder management/signal errors so the
   next Up() starts with a clean status snapshot instead of the previous
   profile's error state.
2. tray.go switchProfile(): add NeedsLogin/LoginFailed/SessionExpired to
   the needsDown set. Down() is called to flush stale daemon state, but
   Up() is not — the user initiates login on the new profile manually.
2026-05-13 15:13:20 +02:00
Zoltan Papp
e3efaa5e59 [client] Fix tray flicker and stuck Connecting during management retry
The status snapshot tore down on every management retry because
state.Status() blanks the status when an error is wrapped, and the
SubscribeStatus stream propagated that as FailedPrecondition. The UI
treated any stream error as "daemon not running" and flickered the tray
to Not running between retries.

Disconnect was also unresponsive: Down set Idle before the retry
goroutine exited, which then overwrote it with Set(Connecting) on the
next attempt; the backoff sleep (up to 15s) wasn't context-aware, so the
goroutine kept running long after actCancel.

- buildStatusResponse falls back to the underlying status (via new
  state.CurrentStatus) instead of breaking the stream on wrapped errors.
- UI only flips to DaemonUnavailable on codes.Unavailable / non-status
  errors, so a live daemon returning FailedPrecondition is not reported
  as down.
- connect retry uses backoff.WithContext so actCancel interrupts the
  inter-attempt sleep, and skips Wrap(err) when the dial fails due to
  ctx cancellation.
- Down sets Idle after waiting for giveUpChan, so the retry goroutine
  can no longer race the disconnect.
- Tray hides Connect during Connecting and keeps Disconnect enabled so
  the user can abort an in-flight connection attempt.
2026-05-12 20:38:30 +02:00
Viktor Liu
1224d6e1ee [client] Persist management URL and pre-shared key overrides on login (#6065) 2026-05-12 14:52:56 +02:00
Zoltan Papp
7a9f5a734f Merge branch 'main' into ui-refactor
Port IPv6 overlay support (#5631) into the Wails UI:
- Add DisableIPv6 config toggle to Settings (NetworkTab + services)
- Filter ::/0 alongside 0.0.0.0/0 as an exit-node route
- Suppress duplicate v6 default-route notifications in tray
2026-05-11 14:10:12 +02:00
Zoltan Papp
8841b950a2 [client/server] Stop retry loop after PermissionDenied login
Without marking the error as backoff.Permanent the outer retry re-enters
connect(), which resets the daemon state from NeedsLogin to Connecting
and makes the tray flicker between the two until the user logs in.
2026-05-11 13:43:53 +02:00
Zoltán Papp
08f52f4517 [client/server] Allow clearing pre-shared key via SetConfig
The daemon ignored an empty OptionalPreSharedKey, so a UI/CLI request to
clear the pre-shared key was silently dropped. Pass the pointer through
unconditionally — profilemanager already handles the empty-string case.
2026-05-11 11:02:39 +02:00
Viktor Liu
205ebcfda2 [management, client] Add IPv6 overlay support (#5631) 2026-05-07 11:33:37 +02:00
Zoltán Papp
4c743bc03d Merge remote-tracking branch 'origin/main' into ui-refactor
# Conflicts:
#	client/internal/peer/status.go
#	client/proto/daemon.pb.go
#	client/proto/daemon_grpc.pb.go
#	go.mod
2026-05-05 12:49:09 +02:00
Viktor Liu
057d651d2e [client, proxy] Add packet capture to debug bundle and CLI (#5891) 2026-05-04 11:28:56 +02:00
Zoltán Papp
88a2bf582d [client] Push-based status stream for the Wails UI
Adds a SubscribeStatus gRPC RPC that pushes a fresh FullStatus snapshot
on every peer-recorder state change, replacing the Wails UI's 2-second
Status poll. The daemon's notifier already triggers on Connected /
Disconnected / Connecting / management or signal flip / address
change / peers-list change; we now coalesce those into ticks on a
buffered chan and stream the resulting snapshots over gRPC.

- Status recorder gains SubscribeToStateChanges /
  UnsubscribeFromStateChanges + a non-blocking notifyStateChange that
  drops ticks when a subscriber's 1-slot buffer is full (next snapshot
  the consumer pulls already reflects everything).
- Server.Status handler split: the snapshot composition is shared
  with the new SubscribeStatus stream handler so unary and stream
  paths return identical bytes.
- UI peers service: pollLoop replaced by statusStreamLoop. The local
  name of the existing SubscribeEvents loop is now toastStreamLoop so
  the two streams are easy to tell apart — the underlying RPC name is
  unchanged.
- Tray applyStatus skips the icon refresh when connected/lastStatus
  hasn't changed; rapid SubscribeStatus bursts during health probes
  no longer churn Shell_NotifyIcon or the log.
2026-04-30 11:45:43 +02:00
Bethuel Mmbaga
df197d5001 [management] Prevent JWT reuse during peer login (#6002) 2026-04-29 15:04:27 +03:00
Viktor Liu
407e9d304b [client] Move macOS sleep detection into the daemon (purego) (#5926) 2026-04-29 08:09:55 +02:00
Viktor Liu
2e0e3a3601 [client] Replace exclusion routes with scoped default + IP_BOUND_IF on macOS (#5918) 2026-04-20 10:01:01 +02:00
Maycon Santos
53b04e512a [management] Reuse a single cache store across all management server consumers (#5889)
* Add support for legacy IDP cache environment variable

* Centralize cache store creation to reuse a single Redis connection pool

Each cache consumer (IDP cache, token store, PKCE store, secrets manager,
EDR validator) was independently calling NewStore, creating separate Redis
clients with their own connection pools — up to 1400 potential connections
from a single management server process.

Introduce a shared CacheStore() singleton on BaseServer that creates one
store at boot and injects it into all consumers. Consumer constructors now
receive a store.StoreInterface instead of creating their own.

For Redis mode, all consumers share one connection pool (1000 max conns).
For in-memory mode, all consumers share one GoCache instance.

* Update management-integrations module to latest version

* sync go.sum

* Export `GetAddrFromEnv` to allow reuse across packages

* Update management-integrations module version in go.mod and go.sum

* Update management-integrations module version in go.mod and go.sum
2026-04-16 16:04:53 +02:00
Viktor Liu
6b540d145c [client] Add --disable-networks flag to block network selection (#5896) 2026-04-16 14:02:31 +02:00
Viktor Liu
94a36cb53e [client] Handle UPnP routers that only support permanent leases (#5826) 2026-04-08 17:59:59 +02:00
Viktor Liu
d33cd4c95b [client] Add NAT-PMP/UPnP support (#5202) 2026-04-08 15:29:32 +08:00
Viktor Liu
aba5d6f0d2 [client] Error out on netbird expose when block inbound is enabled (#5818) 2026-04-07 17:55:35 +02:00
Zoltan Papp
91f0d5cefd [client] Feature/client metrics (#5512)
* Add client metrics

* Add client metrics system with OpenTelemetry and VictoriaMetrics support

Implements a comprehensive client metrics system to track peer connection
stages and performance. The system supports multiple backend implementations
(OpenTelemetry, VictoriaMetrics, and no-op) and tracks detailed connection
stage durations from creation through WireGuard handshake.

Key changes:
- Add metrics package with pluggable backend implementations
- Implement OpenTelemetry metrics backend
- Implement VictoriaMetrics metrics backend
- Add no-op metrics implementation for disabled state
- Track connection stages: creation, semaphore, signaling, connection ready, and WireGuard handshake
- Move WireGuard watcher functionality to conn.go
- Refactor engine to integrate metrics tracking
- Add metrics export endpoint in debug server

* Add signaling metrics tracking for initial and reconnection attempts

* Reset connection stage timestamps during reconnections to exclude unnecessary metrics tracking

* Delete otel lib from client

* Update unit tests

* Invoke callback on handshake success in WireGuard watcher

* Add Netbird version tracking to client metrics

Integrate Netbird version into VictoriaMetrics backend and metrics labels. Update `ClientMetrics` constructor and metric name formatting to include version information.

* Add sync duration tracking to client metrics

Introduce `RecordSyncDuration` for measuring sync message processing time. Update all metrics implementations (VictoriaMetrics, no-op) to support the new method. Refactor `ClientMetrics` to use `AgentInfo` for static agent data.

* Remove no-op metrics implementation and simplify ClientMetrics constructor

Eliminate unused `noopMetrics` and refactor `ClientMetrics` to always use the VictoriaMetrics implementation. Update associated logic to reflect these changes.

* Add total duration tracking for connection attempts

Calculate total duration for both initial connections and reconnections, accounting for different timestamp scenarios. Update `Export` method to include Prometheus HELP comments.

* Add metrics push support to VictoriaMetrics integration

* [client] anchor connection metrics to first signal received

* Remove creation_to_semaphore connection stage metric

The semaphore queuing stage (Created → SemaphoreAcquired) is no longer
tracked. Connection metrics now start from SignalingReceived. Updated
docs and Grafana dashboard accordingly.

* [client] Add remote push config for metrics with version-based eligibility

Introduce remoteconfig.Manager that fetches a remote JSON config to control
metrics push interval and restrict pushing to a specific agent version
range. When NB_METRICS_INTERVAL is set, remote config is bypassed
entirely for local override.

* [client] Add WASM-compatible NewClientMetrics implementation

Replace NewClientMetrics in metrics.go with a WASM-specific stub in metrics_js.go, returning nil for compatibility with JS builds. Simplify method usage for WASM targets.

* Add missing file

* Update default case in DeploymentType.String to return "unknown" instead of "selfhosted"

* [client] Rework metrics to use timestamped samples instead of histograms

Replace cumulative Prometheus histograms with timestamped point-in-time
samples that are pushed once and cleared. This fixes metrics for sparse
events (connections/syncs that happen once at startup) where rate() and
increase() produced incorrect or empty results.

Changes:
- Switch from VictoriaMetrics histogram library to raw Prometheus text
  format with explicit millisecond timestamps
- Reset samples after successful push (no resending stale data)
- Rename connection_to_handshake → connection_to_wg_handshake
- Add netbird_peer_connection_count metric for ICE vs Relay tracking
- Simplify dashboard: point-based scatter plots, donut pie chart
- Add maxStalenessInterval=1m to VictoriaMetrics to prevent forward-fill
- Fix deployment_type Unknown returning "selfhosted" instead of "unknown"
- Fix inverted shouldPush condition in push.go

* [client] Add InfluxDB metrics backend alongside VictoriaMetrics

Add influxdb.go with timestamped line protocol export for sparse
one-shot events. Restore victoria.go to use proper Prometheus
histograms. Update Grafana dashboards, add InfluxDB datasource,
and update docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* [client] Fix metrics issues and update dev docker setup

- Fix StopPush not clearing push state, preventing restart
- Fix race condition reading currentConnPriority without lock in recordConnectionMetrics
- Fix stale comment referencing old metrics server URL
- Update docker-compose for InfluxDB: add scoped tokens, .env config, init scripts
- Rename docker-compose.victoria.yml to docker-compose.yml

* [client] Add anonymised peer tracking to pushed metrics

Introduce peer_id and connection_pair_id tags to InfluxDB metrics.
Public keys are hashed (truncated SHA-256) for anonymisation. The
connection pair ID is deterministic regardless of which side computes
it, enabling deduplication of reconnections in the ICE vs Relay
dashboard. Also pin Grafana to v11.6.0 for file-based provisioning
and fix datasource UID references.

* Remove unused dependencies from go.mod and go.sum

* Refactor InfluxDB ingest pipeline: extract validation logic

- Move line validation logic to `validateLine` and `validateField` helper functions.
- Improve error handling with structured validation and clearer separation of concerns.
- Add stderr redirection for error messages in `create-tokens.sh`.

* Set non-root user in Dockerfile for Ingest service

* Fix Windows CI: command line too long

* Remove Victoria metrics

* Add hashed peer ID as Authorization header in metrics push

* Revert influxdb in docker compose

* Enable gzip compression and authorization validation for metrics push and ingest

* Reducate code of complexity

* Update debug documentation to include metrics.txt description

* Increase `maxBodySize` limit to 50 MB and update gzip reader wrapping logic

* Refactor deployment type detection to use URL parsing for improved accuracy

* Update readme

* Throttle remote config retries on fetch failure

* Preserve first WG handshake timestamp, ignore rekeys

* Skip adding empty metrics.txt to debug bundle in debug mode

* Update default metrics server URL to https://ingest.netbird.io

* Atomic metrics export-and-reset to prevent sample loss between Export and Reset calls

* Fix doc

* Refactor Push configuration to improve clarity and enforce minimum push interval

* Remove `minPushInterval` and update push interval validation logic

* Revert ExportAndReset, it is acceptable data loss

* Fix metrics review issues: rename env var, remove stale infra, add tests

- Rename NB_METRICS_ENABLED to NB_METRICS_PUSH_ENABLED to clarify that
  collection is always active (for debug bundles) and only push is opt-in
- Change default config URL from staging to production (ingest.netbird.io)
- Delete broken Prometheus dashboard (used non-existent metric names)
- Delete unused VictoriaMetrics datasource config
- Replace committed .env with .env.example containing placeholder values
- Wire Grafana admin credentials through env vars in docker-compose
- Make metricsStages a pointer to prevent reset-vs-write race on reconnect
- Fix typed-nil interface in debug bundle path (GetClientMetrics)
- Use deterministic field order in InfluxDB Export (sorted keys)
- Replace Authorization header with X-Peer-ID for metrics push
- Fix ingest server timeout to use time.Second instead of float
- Fix gzip double-close, stale comments, trim log levels
- Add tests for influxdb.go and MetricsStages

* Add login duration metric, ingest tag validation, and duration bounds

- Add netbird_login measurement recording login/auth duration to management
  server, with success/failure result tag
- Validate InfluxDB tags against per-measurement allowlists in ingest server
  to prevent arbitrary tag injection
- Cap all duration fields (*_seconds) at 300s instead of only total_seconds
- Add ingest server tests for tag/field validation, bounds, and auth

* Add arch tag to all metrics

* Fix Grafana dashboard: add arch to drop columns, add login panels

* Validate NB_METRICS_SERVER_URL is an absolute HTTP(S) URL

* Address review comments: fix README wording, update stale comments

* Clarify env var precedence does not bypass remote config eligibility

* Remove accidentally committed pprof files

---------

Co-authored-by: Viktor Liu <viktor@netbird.io>
2026-03-22 12:45:41 +01:00
Viktor Liu
3e6baea405 [management,proxy,client] Add L4 capabilities (TLS/TCP/UDP) (#5530) 2026-03-13 18:36:44 +01:00