Commit Graph
191 Commits
Author SHA1 Message Date
DevBot NetBird 0ec9ccd662 New translations common.json (Hungarian)
[ci skip]
2026-10-06 18:15:52 +02:00
DevBot NetBird 8a333ad96c New translations common.json (Hebrew)
[ci skip]
2026-10-06 18:15:50 +02:00
DevBot NetBird bef4291fcf New translations common.json (German)
[ci skip]
2026-10-06 18:15:48 +02:00
DevBot NetBird 2641c40ecf New translations common.json (Spanish)
[ci skip]
2026-10-06 18:15:46 +02:00
DevBot NetBird 30cc457827 New translations common.json (French)
[ci skip]
2026-10-06 18:15:44 +02:00
Edward d3d27dc08d [client] Only offer locales listed in _index.json (#8091) 2026-10-06 17:00:21 +02:00
Edward 51a9d32cfc [client] Warn on missing translations and check placeholders (#8090) 2026-10-06 16:59:26 +02:00
Pascal Fischer 2623feeb5b [management] remove ingress ports (#8062) 2026-10-06 16:35:31 +02:00
Alberto Xosé Méndez Taboada c5aa55d2b9 [client] feat(i18n): Add Galician (gl) localization for desktop client (#7041)
* Add Galician (gl) localization for desktop client

* [client] Address review feedback and sync latest i18n keys for Galician

Fix terms per review feedback (usarase, creen, auditar, JWT cache, poscuántico, etc.) and translate newly added upstream i18n keys.

* [client] Fix translations, polish terminology, and sync latest keys for Galician

* [client] Fix i18n parity against branch source of truth and drop undeclared uk

* [ci] Skip Galician locales in codespell check

* [client] Add latest error keys for Galician and revert _index.json formatting

Add error.settings_locked and error.settings_managed_by_mdm to Galician locale for 100% key parity with en/common.json. Revert multi-line formatting in _index.json to keep a single-line entry for Galician.
2026-10-06 15:05:01 +02:00
Eduard GertandEdward f5707c3485 [client] Add RTL layout support to the desktop UI (#8076)
* [client] Add RTL layout support to the desktop UI

<html dir> follows the active language, Radix primitives get the
matching dir, physical spacing/positioning uses logical utilities, and
directional icons, animations, arrow-key navigation and tooltip sides
flip in RTL.

* [client] Address RTL review feedback

Force LTR with isolation for monospace values, let truncated names
take their direction from their content, and keep the profile name
field in the UI direction.

* [client] Keep translated monospace labels in their own direction

The forced-LTR rule for monospace values now skips elements with an
explicit dir, translated development labels use dir=auto, and
monospace values rendered through TruncatedText opt into LTR.

---------

Co-authored-by: Edward <43848523+thomashacker@users.noreply.github.com>
2026-10-06 12:08:11 +02:00
Riccardo Manfrin a8dff998ef [client] Gate settings updates on value, not on field presence (#7398)
* [client] Gate settings updates on value, not on field presence

The update-settings kill switch (--disable-update-settings /
NB_DISABLE_UPDATE_SETTINGS / the MDM DisableUpdateSettings key) forbids
changing settings, but it decided what a "change" was by looking at
whether a field was present in the request. The CLI fills the whole
config surface of SetConfigRequest and LoginRequest from its flags and
environment on every `netbird up` (setupSetConfigReq in cmd/up.go), so a
client configured by environment restates its own configuration on every
start and tripped the gate every time.

SetConfig only warned about that, but Login carries the same fields and
was gated the same way, and Login runs inside the CLI's backoff loop: the
daemon answered every attempt with codes.Unavailable, `netbird up` never
completed, and a container with NB_DISABLE_UPDATE_SETTINGS plus any
config env var (NB_MANAGEMENT_URL, for one) could not come up at all.

Both gates now compare values. Config.WouldChange is the dry-run half of
UpdateConfig: it runs the very same diff logic (Config.apply) against a
copy of the stored config, so the gate cannot drift from what an actual
update would do, nor go stale when a field is added. A request that
restates what the profile already holds changes nothing and is allowed; a
request that diverges is refused exactly as before, and a dry run that
cannot be evaluated fails closed. A profile with no config on disk yet is
judged against the config the daemon would create for it.

For Login, the compared input comes from loginOverridesInput, which
persistLoginOverrides also uses to perform the write, so the gate judges
precisely the two fields a login can persist (management URL, pre-shared
key) and no field it ignores.

Two adjacent defects surfaced while making the comparison exact:

- Config.apply compared URLs as raw strings, so the same endpoint spelled
  without its default port ("https://api.netbird.io" vs
  "https://api.netbird.io:443") counted as a new value and rewrote the
  config. It now compares the parsed forms.
- UpdateConfig did not collapse the redacted pre-shared key, unlike
  UpdateOrCreateConfig and DirectUpdateConfig, so a UI round-trip of the
  mask replaced the stored key with asterisks.

The CLI warning for a refused SetConfig said the method was not available
in the daemon, which sent people looking for a version mismatch that was
not there; it now reports the refusal.

* [client] Do not write the profile config while only reading it to decide

The update-settings gate needs the stored config to decide whether a request
changes anything, so the previous commit moved that read ahead of the refusal.
The read is not side-effect free: profilemanager.GetConfig writes the config
back whenever apply() has to fill in a default the file was missing. A request
that the gate then refuses had therefore already rewritten the profile file.

PeekConfig is GetConfig without that write-back. The returned config is still
normalized in memory, which is what the decision needs; the file is left
exactly as it was found. Every caller of storedConfigAtPath feeds a gate that
can refuse, so they all peek.

Note for reviewers: the daemon still normalizes the file on startup and on
every real update, so nothing depends on a read performing that write.

* [client] Compare service URLs as endpoints, not as strings

Three places in one request path each had their own notion of "same
management URL": the config layer compared the parsed URLs as strings, the
privileged-change gate compared scheme + host + effective port, and the MDM
conflict check compared strings after filling in the default port. Only the
middle one was right.

A string comparison answers the wrong question. "https://api.netbird.io",
"https://api.netbird.io/" and "https://API.netbird.io:443" are one endpoint
written three ways, so a client restating its own management URL with a
trailing slash — a normal way to write it — was still read as a client asking
to be repointed, and the update-settings gate refused it. The MDM check had
the same flaw against the enforced value.

profilemanager.SameServiceURL is now the single comparison: same scheme, same
host case-insensitively as DNS names are, same effective port. The config
layer, the privileged-change gate and the MDM conflict check all defer to it,
so there is one answer to "did this URL change?" instead of three.

* [client] Stop the config dry run from generating throwaway keys

The dry run's baseline for a profile with no config file yet went through
createNewConfig, and apply() generates a WireGuard and an SSH key whenever it
finds those fields empty. The baseline is compared against and discarded, so
every evaluation minted a keypair it threw away — and logged "generated new
Wireguard key". The CLI retries Login in a backoff loop, so a first `netbird up`
on a fresh profile filled the daemon log with what reads like peer-key rotation.

The baseline now starts from the shared skeleton with placeholder keys, so
apply() has nothing to generate. No ConfigInput field maps to either key, so
the comparison is unaffected.

* [client] Cover the login the update-settings gate used to refuse

The gate's decision procedure was tested directly, but no test drove the Login
RPC that the refusal actually broke: the CLI retries Login in a backoff loop,
so a refused no-op login is what kept a client configured by environment from
ever coming up. The handler-level coverage stopped at the refusal case, which
passes on the pre-fix code too.

This test fails on the pre-fix daemon with "update settings are disabled" and
passes now. Past the gate the handler does real work the test does not stand
up, so it asserts only that the refusal did not happen.

* [client] Re-take the update-settings decision under the config lock

Login checks twice on purpose: the first check refuses the ordinary case
early, and authorizeAndPrepareLogin re-takes the authoritative one under
guardedConfigMu because the first is unsynchronized against a concurrent
privileged request. The update-settings decision is now equally
value-dependent — it compares the request against the stored config — but it
was taken only in the first, unlocked check.

So a login that was a no-op when it was checked could be written after a
concurrent writer had repointed the profile, which is exactly the window the
lock exists to close. The decision is now re-taken alongside the privilege one,
which also makes it the last read before persistLoginOverrides writes.

The test drives that interleaving through the existing afterLoginPreCheck seam
and fails without the re-check.

* [client] Drop an unreachable guard and fix two stale comments

- loginOverridesInput's nil-message guard cannot be reached: Login
  dereferences the message well before it, in storedLoginConfig.
- The docstring above afterLoginPreCheck described persistLoginOverrides,
  which lives further down the file and now carries its own.
- UpdateConfig's comment named DirectUpdateConfig; the function is
  DirectUpdateOrCreateConfig.

* [client] Make config reads pure and provision the identity explicitly

Reading a config wrote it back. profilemanager.readConfig persisted whatever
apply() had filled in, and ReadConfig created and wrote the file outright when
it was absent, so every reader was quietly a writer: a gate deciding whether to
refuse a request, a UI listing profiles, a mobile getter reading one preference.
The previous commit worked around that with a PeekConfig variant, which left
two read functions with opposite side effects and the antipattern still there
for everyone else.

Only one thing in a read genuinely had to be persisted: apply() generated the
WireGuard and SSH keys when it found them empty, and a generated key cannot be
recomputed — losing it means the peer comes back with a different identity and
registers again. Everything else apply() fills in is a deterministic default
that the next read recomputes anyway.

So identity provisioning is now its own step, Config.EnsureIdentity, and the
callers that provision write the result out themselves, in the open:

- Server.getConfig, the daemon's provisioning point;
- the CLI's foreground login, which is about to dial management;
- update() / directUpdate(), the config write paths — a stored profile can
  legitimately carry no identity, since a mobile logout clears the keys in
  place, and the next write is what has to mint a new one.

ReadConfig and GetConfig no longer write anything, PeekConfig is gone, and the
dry-run baseline no longer needs placeholder keys to keep apply() from minting
real ones.

One deliberate leftover: readConfig still calls util.EnforcePermission, which
chmods a config file whose permissions are too broad. It changes no content and
is idempotent, and dropping it would leave a legacy file world-readable until
its first write.

* [client] Name the two config readers for what they do

ReadConfig and GetConfig differed in one thing — what happens when the file is
absent — and neither name said which was which:

- ReadConfig      -> ReadOrGenerateConfig  (reads it, or generates one in memory)
- GetConfig       -> GetExistingConfig     (reads it, or fails)

Three comments went with them:

- GetConfig's said "return with Config and if it was created. Errors out if it
  does not exist", which described a bool it does not return and a creation it
  never performs.
- ReadConfig's explained that it does not write, which is what a reader is
  supposed to do anyway.
- Server.getConfig's said it "errors out if it does not exist", which it does
  not — it resolves a default config, and now provisions the identity too.

* [client] Do not panic on a config with no sync message version

apply() wrote the incoming sync message version through the stored pointer,
without checking it was there: a config that carries no version yet made it
dereference nil. Reachable from the update-settings dry run, which runs inside
a request handler — where failing closed is the worst acceptable outcome, and a
panic is not one.

The field is now reassigned like every other optional one, which also means
apply() no longer mutates anything the caller still holds through a pointer, so
the dry run's copy has one less field to detach.

Reported by cubic-dev-ai on PR #7398.

* [client] Compare the client certificate paths before reporting a change

apply() assigned the incoming mTLS certificate and key paths and set updated
unconditionally, without comparing them to what the config already held. It is
the same presence-instead-of-value mistake this branch set out to fix, one
layer down: a caller restating its own certificate paths was reported as
changing them, which trips the value-aware update-settings gate.

Reported by cubic-dev-ai on PR #7398.

* [client] Address the remaining bot findings on PR #7398

- Login logged the active-profile-state error and returned the same cause; the
  repo's guidelines call for one or the other, and the wrapped error is the one
  that carries context. (CodeRabbit)
- `netbird up` reported a codes.Unavailable SetConfig failure as "the daemon
  refused the settings update", but that code also covers a daemon that became
  unreachable. It now reports what the daemon said without asserting why.
  (cubic-dev-ai)
- TestLogin_ChangingTheManagementURLIsRefused asserted the error and nothing
  else, while "refused before it can touch daemon state" is the contract. It now
  checks the stored management URL, the in-progress login and the active profile,
  matching its SetConfig counterpart. (cubic-dev-ai)

* [client] Keep the peer identity out of a read that finds no file

ReadOrGenerateConfig resolves a default config when the profile has no file
yet, and createNewConfig was minting the WireGuard and SSH keys while doing so.
That defeated the provisioning pair it was meant to serve: the CLI's foreground
login calls EnsureIdentity to find out whether it has to persist the keys, got
generated == false because the read had already generated them, and so never
wrote them out. The login then dialed management with an identity that only
existed in memory, and the next login registered a second peer.

createNewConfig no longer provisions. createProvisionedConfig is the variant
that does, and the callers whose contract is "usable as it comes back" use it:
CreateInMemoryConfig, whose callers connect with the result, and the two
create-and-write branches. A read gets a config with no identity, so the
caller's own EnsureIdentity reports the work and triggers the write.

Reported by CodeRabbit and cubic-dev-ai on PR #7398, both on the same defect.

* [client] Stop the gate test from dialing the real management server

TestLogin_RestatingTheStoredConfigPassesTheGate asserts that the gate lets a
no-op login through, and the handler then went on to do the login for real:
isLoginRequired builds an auth client when isLoginRequiredFn is unset, so the
test dialed the profile's management URL — api.netbird.io:443. It took 1.05s
locally and would hang on a runner with no egress, for a fact about the gate
that needs no network at all.

Stubbed like the login_outcome tests do. The test now runs in 0.00s.

Reported by cubic-dev-ai on PR #7398.

* [client] Keep the admin panel path part of its identity

The endpoint comparison introduced for the management URL was applied to the
admin URL too, and that one is opened in a browser rather than dialed over
gRPC: a panel served under /netbird is not the panel served at the root. So a
config whose admin URL differed only by path reported no change, and the new
path was never persisted — a custom panel URL could not be updated at all.

SameServiceURLIncludingPath adds what a URL carries past its endpoint (path,
query, fragment, userinfo) while still treating equivalent spellings as equal:
a missing path and "/" are the same root, and so is a trailing slash. The
management URL keeps the endpoint-only comparison, since only the endpoint is
ever dialed.

Ports are also normalized numerically now, so ":0443" and ":443" are one port.

Reported by cubic-dev-ai on PR #7398 (two findings).

* [client] Treat a profile with no identity as already deregistered

Two findings on the same consequence of pure reads: a profile can legitimately
carry no keys, because logging out clears them in place.

- sendLogoutRequestWithConfig went straight to wgtypes.ParseKey and failed with
  "incorrect key size: 0" on the second logout of the same profile. There is
  nothing to deregister for a peer that was never registered, so it returns
  cleanly. Before pure reads this case was hidden: the read minted a key and
  the daemon dialed management with one it had never seen.
- The mobile logout read the config with the generating reader right after
  checking the file exists. The two are not atomic, so a profile removed in
  between was resolved from the defaults and recreated by the write that
  follows. It uses the existing-file reader now.

Reported by cubic-dev-ai and CodeRabbit on PR #7398.

* [client] Fail `netbird up` when the daemon refuses the settings update

With the update-settings kill switch on, `netbird up --enable-rosenpass`
connected and said almost nothing: SetConfig refused the change, the CLI
downgraded that to a warning, and Login carries no rosenpass field to apply, so
the flag was silently dropped. The setting stayed disabled, which is the point
of the switch, but the caller was never told their request had been ignored.

The refusal now travels as codes.FailedPrecondition instead of
codes.Unavailable, and the CLI fails on it. Unavailable means "the daemon
cannot serve this call", which is why the CLI downgraded it and why
client/ui/services reads it as an unreachable daemon — both wrong for a daemon
that answered and refused. FailedPrecondition also matches what the MDM gate
already returns for a managed field, so both refusals are now one class of
error, and it is added to the login backoff's early-exit codes so a refused
login stops instead of retrying for 30s.

This does not put the container back in the deadlock: with the value-aware
gate, a client restating its own configuration is not refused at all, so
nothing reaches this path unless a real change was asked for.

* [client] Name the reader storedConfigAtPath actually calls

The purity note still said profilemanager.GetConfig, which the rename two
commits later turned into GetExistingConfig.

Reported by cubic-dev-ai on PR #7398.

* [client] Restore the gofmt alignment of the error constants

The comment added above errUpdateSettingsDisabled in the previous commit split
the const block's alignment group, so gofmt wants the two constants above it
re-aligned. CI runs gofmt, so this would have failed the lint job.

* [client] Let an unprivileged caller log out a profile with no identity

The empty-key check sat behind requirePrivilegeForDeregistration, so an
unprivileged logout of an identity-less profile was refused with
PermissionDenied instead of completing as the no-op it is. And it was refused
for most profiles, not a corner case: the gate arms whenever the SSH server is
enabled, and sshServerEnabled reads an absent ServerSSHAllowed as enabled, so
every legacy profile qualifies.

The check now runs first. What the gate protects against is handing this
machine's registered key to another management server; with no key there is
nothing to hand over and nothing to protect.

Reported by CodeRabbit and cubic-dev-ai on PR #7398, both on the same defect.

* [client] Stop `netbird login` from retrying a refusal for 30 seconds

`netbird up` and `netbird login` both run Login through the backoff cycle, and
each carried its own copy of the list of codes that end it. Only up.go learned
about codes.FailedPrecondition, so a refused `netbird login` kept retrying and
then reported "login backoff cycle failed" instead of what the daemon said.

terminalLoginError is now that list, once, next to WithBackOff — the duplicated
copies are what let the two commands disagree in the first place.

Reported by cubic-dev-ai on PR #7398.

* [client] Answer terminalLoginError's nil case on its own terms

A successful Login reaches terminalLoginError with a nil error, and nothing
covered that. It happens to work on grpc v1.80.0 — gstatus.FromError(nil)
answers (nil, true), and Status.Code tolerates a nil receiver by returning
codes.OK, which is not in the terminal set — but that is a chain of internal
details to be relying on for the common path, and none of it was asserted.

Now the nil error is handled where it is obvious, and the table covers it.

Reported by CodeRabbit on PR #7398, which called it a panic; measured on
v1.80.0 it is not one. The gap was the untested reliance, not a crash.

* [client] Treat an unset optional field as its default when diffing a config

Seven Config fields mean "the effective default" when they hold no value:
the five SSH toggles, the SSH JWT cache TTL, and the network monitor. Every
consumer already reads a nil as that default, but apply() diffed them by
presence — `config.X == nil || *input.X != *config.X` — so an input restating
the default counted as a change.

That made the update-settings gate refuse `netbird up` outright. The CLI
sends every flag whose value came from an environment variable
(SetFlagsFromEnvVars goes through pflag's FlagSet.Set, which marks the flag
Changed), and the config a plain login writes leaves all seven unset, so a
container configured with, say, NB_ENABLE_SSH_ROOT=false restated a default
the file held as null on every start and was answered with
FailedPrecondition.

apply() now resolves the seven up front, the way it already did for
ServerSSHAllowed and RemoteJobsAllowed, which also repairs such a profile on
its next write. With the values named, the comparisons below diff values
instead of presence, so their nil branches are gone.

The network monitor keeps its platform default — on for windows and darwin —
and naming it as false elsewhere is what createEngineConfig already read a
nil to be. getJWTCacheTTL reaches the same 0 through its own default, and
Android's GetEnableSSH* getters already answered nil with false.

* [client] Normalize the config before diffing it in WouldChange

apply() reports two different things through one bool: an input that changed
a value, and a field it had to fill in because the config carried none. The
update-settings gate reads that bool as "the caller asked for a change", so
any config still missing a default answered a request that asks for nothing
with a refusal.

Readers already hand out normalized configs — readConfig applies an empty
input for exactly this reason — which is why the gate got away with it. But a
handler that refuses a request must not depend on where its caller obtained
the config, and it must not start reading "this profile predates a field" as
"the caller asked for a change" the day someone adds one with a default.

WouldChange now runs the filling-in as a pass of its own and discards its
verdict, so the pass that answers the caller measures only what the input
did.

* [client] Stop the last config write that skipped normalization

Every path that creates or updates a profile config goes through apply(),
which resolves an optional field to its default — except RenameProfile,
which read the file with a bare json.Unmarshal, set the name, and wrote it
straight back. That copied whatever the file held, so a config written by a
client that stored these fields as null kept them null. It could not
introduce a null, only carry one forward, but renaming a profile is a poor
place to leave a half-resolved config behind. It now reads through
GetExistingConfig, which normalizes what it hands out.

The tests state the invariant the fix completes, over the *bool fields of
Config listed by reflection so a field added later is covered without
touching them: none may come out of apply() unset, and no write may store
one as null. An optional bool that can be nil, true or false forces every
reader to invent the meaning of nil, and makes a diff of the config compare
presence rather than value — which is exactly what refused `netbird up` for
a client restating its own defaults.

SyncMessageVersion stays a genuine three-state field and is not covered: it
is an *int whose absence means the client pins no version, and it travels to
management that way.

* [client] Refuse a serialized config that carries no peer identity

ConfigFromJSON still promised a "fully initialized" config after this PR
moved key generation out of apply() into EnsureIdentity, but identity stopped
being one of the defaults it applies. Its two callers both connect with what
they get back: the iOS SDK's Client.SetConfigFromJSON keeps it as the
preloaded config Run() uses on tvOS, and Auth.SetConfigFromJSON as the config
it authenticates with.

No caller feeds it a document without keys today — every stored document
comes from Auth.GetConfigJSON, whose config is provisioned by
DirectUpdateOrCreateConfig or CreateInMemoryConfig, and the tvOS app only
ever edits fields of a document it already has. This is a safety net for the
next caller, not a live bug.

Provisioning the identity here would be the wrong net. Neither caller can
hand a generated key back to the store the document came from — Client
exports no config at all — so the peer would connect under an identity
nothing persists and register anew on every launch, which is the failure the
EnsureIdentity split exists to prevent. A document with no identity means
nobody has logged in yet, and saying so is the only useful answer.

Both keys are required because both are dead ends when missing: an empty
WireGuard key fails the management login on its size, and an empty SSH key
fails ssh.GeneratePublicKey in ConnectClient before the engine starts.

* [client] Say that the null-on-disk fixture is synthesized, not written

The test comment described the null state in the present tense — "the config
a plain login writes leaves every one of them unset" — which was true before
this branch and is not any more: apply() now resolves those fields, so a
login writes them set. unsetOnDisk puts the null state back deliberately, to
stand in for a profile an older client wrote. Comments only.

* [client] Gather the optional-field defaults into one function

Resolving an unset optional field was spread over five places: the two
values newConfigSkeleton pre-sets, the block this branch added for the SSH
toggles, the network monitor's own if, the `else if` tails of
ServerSSHAllowed and RemoteJobsAllowed, and a trailing if for
DisableNotifications several hundred lines further down. Reading apply() left
no single answer to "what does this field default to, and who decides".

They now live in Config.resolveUnsetDefaults, which apply() calls before it
compares anything — the ordering being the point, since it is what lets
every comparison below diff values instead of presence. The comparisons for
ServerSSHAllowed, RemoteJobsAllowed and DisableNotifications lose their
`config.X == nil ||` clauses accordingly, as the other six already had.

newConfigSkeleton keeps its two, and that is the one asymmetry worth naming:
ServerSSHAllowed defaults to false for a new profile and to true for a
legacy one, and it only works because the skeleton runs first. The doc
comment says so, where before it was implied by the order of two distant
blocks.

Pure refactor. Verified as one: for the four fields whose branches moved,
plus two that did not and the JWT TTL, all 63 combinations of stored value
(nil/false/true) against input value (absent/false/true) produce byte-
identical resolved values and `updated` verdicts before and after.

* [client] Resolve the merge conflicts left in the tree

262ce8c3b landed with the conflict markers still in it, so client/server and
the iOS SDK did not compile. Four regions, resolved as follows.

client/server/mdm.go — main moved the MDM conflict-check machinery into the
mdm package (mdm.ResolveConflicts, mdm.ConflictBool, mdm.ConflictURL, ...).
This branch had edited the local copies, which are now dead: dropped, along
with the profilemanager import that only the local conflictURL needed.

client/server/server.go, Login gate — this branch's value-aware gate stays
(the point of the PR: refuse a real divergence, let a restatement through),
so main's presence-based `loginRequestHasConfigOverrides` block goes; that
helper no longer exists here anyway. Main's other change in the same lines
is real and kept: the MDM policy now comes from the daemon-owned
s.mdmLoader.Load() instead of the package-level loadMDMPolicy, which main
removed. The stale call right below the conflict was the reason the file
would not have compiled even with the markers gone.

client/server/server.go, getConfig — both sides add something and both are
needed. The identity is provisioned and persisted first, then the MDM
overlay is applied, so what reaches disk stays the profile's own config: the
overlay is runtime-only and re-derived on every load.

client/ios/NetBirdSDK/client.go — main reworked SetConfigFromJSON to store
the JSON and re-parse it on each load, which is the shape kept; the parse is
now only a validity check, and this branch's reason for it (a document with
no peer identity is refused, not just an unparseable one) moves into that
comment.

client/server/update_settings_gate_test.go — follows the sentinel constant
to its new home, mdm.PreSharedKeyRedactedSentinel.

* [client] Reuse util's service-URL comparison instead of a second copy

The endpoint-comparison rules this branch introduced now live in util (PR
#7472 moved them there so the MDM conflict check could stop comparing URLs
as strings). Keeping a copy here is what produced that bug in the first
place: two implementations of "is this the same endpoint?" drift, and the
one that drifts starts refusing a URL that addresses the very server it
already points at.

So SameServiceURL delegates the port normalization to util.ServiceURLPort
and drops the local one, and SameServiceURLIncludingPath — endpoint plus
path, for the admin panel URL, which is opened rather than dialed — is
util.SameServiceURL plus the query, fragment and userinfo it adds on top,
so the local path normalization goes too.

What stays here is the distinction util does not make: SameServiceURL is
endpoint-only, because a management URL is dialed and only its host and port
are, while util.SameServiceURL includes the path.

Pure refactor. Verified as one: all 198 pairs of a 14-spelling matrix
(default and zero-padded ports, host case, trailing slash, path, query,
fragment, userinfo, both schemes, nil operands) answer identically for both
functions before and after.

* [client] Give a newly added profile its identity (review item 1)

AddProfile writes the config it builds straight to disk, but built it with
createNewConfig, which stopped generating the peer's keys when identity
generation moved out of apply() into EnsureIdentity. The profile file landed
with an empty PrivateKey and SSHKey.

Nothing lost the keys permanently — the daemon's own getConfig provisions and
persists them on first use — but every reader that does not write got a
config that cannot connect in the meantime, which is exactly the set this
branch grew: the update-settings gate deciding whether to refuse a request,
and the mobile SDKs loading a stored profile.

createProvisionedConfig exists for callers that persist or connect, and this
is one; before the split, createNewConfig produced the keys here too.

* [client] Let a logged-out profile deserialize again (review item 2)

ConfigFromJSON refused a document with no WireGuard or SSH key. A config
legitimately has none between a logout and the next login: mobile
LogoutProfile clears both in place and writes the profile back, so the peer
re-registers on the next login instead of returning as itself.

So the refusal broke the mobile flows it was meant to protect. On iOS and
tvOS the stored JSON of a logged-out profile stopped loading through
Client.SetConfigFromJSON and Auth.SetConfigFromJSON, and copyConfig — which
round-trips a Config through JSON to take an in-memory copy before applying
the MDM overlay — failed on the same document. Where the old code silently
minted a key, this returned an error, which is worse for logout and profile
switching alike: neither is asking to connect.

The deserializer now stays out of the identity question in both directions:
it does not generate one (a read cannot hand back keys nothing will write
down) and does not refuse one that is absent. Whoever goes on to connect is
where an absent identity has to be answered — and it already is, by the
login path that provisions and persists.

ErrConfigWithoutIdentity goes with it; nothing else used it.

* [client] Fold the scheme case here too, like util does (review item 6)

profilemanager.SameServiceURL compared the scheme with ==, util.SameServiceURL
with EqualFold. No observable difference — net/url lowercases the scheme when
it parses, and both functions take parsed URLs — but two functions of the same
name with two different rules is a trap for whoever reads one and assumes the
other.

* [client] Classify the daemon's refusals in the GUI (review item 3)

FailedPrecondition reached the classifier unmatched, so a refusal showed as
"Operation failed". It is the code both of the daemon's deliberate refusals
carry: the update-settings kill switch, and a field an MDM policy manages.

Both are now named — settings_locked and settings_managed_by_mdm, matched on
the message the daemon composes — and FailedPrecondition itself falls back to
change_refused, so a refusal the daemon grows later still reads as a refusal
rather than a failure.

Only the English strings are added. Bundle.Translate falls back to the
default language for a missing key, so other locales show English until the
usual translation pass, rather than the bare "error.<code>" the classifier
would otherwise surface.

Note: the package needs GTK4/WebKit to build, which this machine has not, so
the test is type-checked (go vet, GOOS=windows) but was not executed locally;
CI's Linux job runs it.

* [client] Cover the mobile profile round trip: create, logout, reload

Both mobile regressions this branch's review turned up lived on the same
path, and neither was visible from the desktop client: a profile created
without an identity, and a logged-out profile that would no longer
deserialize. The desktop never meets the second one — it is mobile logout
that clears the peer's keys in place, so the next login registers a new peer
instead of bringing the old one back.

The test walks a profile through the round its user puts it through —
created, logged out, loaded again, switched away from and back — and loads it
at each step the way the SDKs do: read the stored config, serialize it, load
it back. That is Client.SetConfigFromJSON storing the document for tvOS,
Auth.SetConfigFromJSON authenticating with it, and copyConfig taking an
in-memory copy before the MDM overlay.

Verified to fail on each regression separately: restoring the bare
constructor in AddProfile fails it with "a new profile was written with no
identity", and restoring the identity check in ConfigFromJSON fails it at
"load the profile back".

client/mobile already had the coverage for the first one in
TestLogoutProfile_DisableProfiles — which arrived from main with the MDM
work, and which I had not been running.

* [client] Name only the refusals, not every FailedPrecondition

The classifier gained a blanket FailedPrecondition -> change_refused fallback
so a refusal would stop reading as "Operation failed". It reaches too far:
the daemon returns that code for two dozen states that are not settings
refusals — "not logged in", "client is not running", "another capture is
already running", "session can no longer be extended, log in again to
reconnect" — and errorClassifier is shared with the session and connection
services, not just the settings save.

So the user was told the service had refused their change while what they
actually had to do was log in again. The two refusals the daemon composes
stay named by their message; everything else goes back to the generic
message, which says nothing rather than something wrong.

Reported by cubic on the PR.

* [client] Say what each assertion was checking in the mobile test

AGENTS.md asks for a context message on comparison and boolean assertions,
and four of the ones added with this test had none, so a failure would have
read as a bare Empty/Equal with no hint of which step of the round trip broke.

Reported by cubic on the PR.

* [client] Translate the two new error strings into every locale

The GUI classifier gained error.settings_locked and
error.settings_managed_by_mdm, and only the English strings were added: the
bundle falls back to the default language for a missing key, so nothing would
have shown a bare "error.<code>" to a user.

CI disagrees, and it is right to: check-translations.mjs requires every
locale to carry the full English key set, so English-only fails the gate
rather than degrading quietly.

The ten locales now carry both strings. These are my translations, not a
localization pass — worth a second pass by whoever owns the language, in
particular for the phrasing of "an administrator has locked them".

The uk file also loses two lines of stray 8-space indentation, normalized by
rewriting the file; no key or value changed with it.

* [client] Persist the profile before overlaying MDM on it (review item)

`netbird login` read the config, applied the MDM policy on top, and only then
provisioned the identity and wrote the result out. On a profile with no
identity yet — a first login — that write persisted the enforced values into
the user's own config file: an MDM-managed management URL or pre-shared key
became indistinguishable from one the user set, and stayed behind once the
policy was withdrawn.

Provisioning and its write now come first, and the overlay is applied to the
in-memory config afterwards, where it belongs: it is re-derived on every load
and never meant to reach disk from here. Server.getConfig already orders the
two this way; the two paths now agree.

Reported by cubic on the PR.

* [client] Assert against the stored config, not a resolved default (review item)

The login-gate test read the profile back with ReadOrGenerateConfig, which
resolves a default config in memory when the file is missing — and that
default's management URL is the very value the assertion checks. An erased or
mislocated profile would have passed the test instead of failing it.

The file is written by the test itself, so GetExistingConfig is the right
reader: it errors when the file is gone.

Reported by cubic on the PR.

* [client] Keep the mTLS pair off the gate's dry run (review item)

WouldChange runs the real apply() against a throwaway copy, and apply() loads
the client mTLS certificate and key from disk whenever the config names them.
So every gated SetConfig and Login read the pair — twice per request, once for
the normalization pass and once for the verdict — including requests that were
about to be refused or that changed nothing, and logged an error per request
when the files were missing. The gate used to be presence-based and never
called apply(), so this was new work on a request path.

The loaded pair feeds the connection and never the comparison: nothing in
apply() reads it back, and it does not move the `updated` verdict. A config
built only to be compared against now says so, and apply() skips the load for
it.

Reported by cubic on the PR.

* Makes it explicit that RenameProfile does write on disk

* [client] Provision the peer identity under the config lock (review item)

Login took the authoritative update-settings and privilege decisions under
guardedConfigMu, then released it and called getConfig, which mints the peer's
identity and writes the config out. Between that read and that write, a
SetConfig holding the same lock could land a change and answer its caller —
and then be overwritten by the config the login had already read.

The window is narrow: getConfig only writes when the profile has no identity
or no file, so in practice a first login racing a settings change on the same
profile. It is also narrower than before this branch, where the write happened
inside the reader on every read that filled in a default.

Provisioning now runs where the decision it belongs to runs: at the end of
authorizeAndPrepareLogin, with the lock already held, next to
persistLoginOverrides, which writes there too. No lock is taken that was not
held before, so the documented guardedConfigMu-then-mutex order is untouched.

getConfig keeps its behaviour by calling the same extracted helper; on the
login path it now finds the identity already there and writes nothing. The
other callers are unchanged, and still provision outside any lock — a
concurrent SetConfig is not part of their flow.

Reported by cubic on the PR.

* [client] Declare the probe marker to the debug-bundle field check

TestAddConfig_AllFieldsCovered walks Config by reflection and fails until every
field is either rendered in the debug bundle or listed as excluded with a
reason. The probe marker added for the gate's dry run was neither, so the
client unit suite went red on every platform.

It is excluded: it marks a throwaway copy built to be compared against and
discarded, so it is never set on a config anyone runs with, and rendering it
would only ever print false.

* [client] Provision the peer identity on the iOS login path

Key generation used to happen inside apply(), so a config loaded from JSON with
no keys got them in memory on the way in, the login worked, and the app stored
the result. This branch moved generation into EnsureIdentity, and nothing in
the iOS SDK called it.

The consequence lands on the flow the mobile logout sets up: logout clears both
keys in place so the next login registers a new peer. The app then hands that
keyless JSON to Auth.SetConfigFromJSON, and the login calls auth.NewAuth with
an empty WireGuard key, which fails on key size before the SSO flow starts —
the user cannot sign back in.

Auth.setBaseConfig now provisions, which covers both entry points (NewAuth and
SetConfigFromJSON). It mints on the base config, the one GetConfigJSON returns
for the caller to persist, and writes it to disk itself when the profile has a
file — non-atomically, like NewAuth's own write, since the tvOS App Group
sandbox blocks temp-file-and-rename.

Not covered by a test: the package builds only under GOOS=ios, which the test
jobs do not run. Verified by building and vetting for GOOS=ios/arm64.

Reported by pappz in review.

* [client] Name the resolving reader for what it does, not what it makes

ReadOrGenerateConfig reads the profile config and falls back to the defaults in
memory when there is no file. "Generate" reads as "produces and stores", which
is the opposite of the property the rename it came from was meant to advertise:
the read is pure, writes nothing and mints no identity.

ReadConfigOrDefault says the same without the side effect, and pairs with
GetExistingConfig, which fails where this one falls back. Its doc comment now
states the absence of a write rather than only the fallback.

Pure rename; the two remaining mentions of the pre-branch name ReadConfig in
the tests go with it.

Reported by pappz in review.

* [client] Read an emptied NAT list as the absent one it matches

apply() compared NATExternalIPs with reflect.DeepEqual, which calls a nil
slice and an empty slice different. Both mean the same thing — no NAT
mappings — and the two meet on a perfectly ordinary start: a profile stores
the absent list as JSON null and reads it back nil, while `netbird up` sends
CleanNATExternalIPs, an empty list, whenever NB_EXTERNAL_IP_MAP is set to
nothing, which a deployment template does by default.

So the gate saw a change where nothing changed and refused the request with
FailedPrecondition. That is the same deadlock this branch exists to remove,
reached through another field: a container with the kill switch on could not
come up, and `netbird up` reported "the daemon refused the settings update".

The DNS label list next to it already used slices.Equal, which treats nil and
empty as the same list. The NAT list now does too, and the last use of
reflect in the package goes with it.

Reported by pappz in review.
2026-10-06 11:39:22 +02:00
Zoltan PappandClaude Opus 5 bc44cdc37a [client] Fix browser login popup show from go (#7408)
* [client] Show the SSO login popup and open the browser from Go

The browser-login popup was created hidden and relied on its own webview
to size and show itself and to launch the external browser. On macOS a
hidden WKWebView gets throttled or suspended (App Nap / hidden-window
throttling), so on the first-use path nothing appeared and the browser
never opened, leaving the session-expiration dialog disabled until the
PKCE flow timed out. Reproduced by freezing the popup's WebContent
process: the old code showed nothing, the new code shows the popup and
opens the browser within 30 ms regardless of the webview state.

Show and focus the popup from Go right after creation and launch the
browser from Go on both the create and reuse paths. The popup's frontend
no longer shows or focuses itself, so the browser keeps the foreground
once it activates. This also fixes the reuse path, where a fragment-only
SetURL kept the mounted React tree and the once-only guard skipped
opening the browser for the new URI. Browser launch failures surface in
the error dialog instead of being swallowed.

* [client] Show every dialog window from Go once its frontend has painted

Dialog windows (browser-login, session-expiration, install-progress,
welcome, error) were created hidden and made visible only by their own
webview's Show call after sizing. A hidden WKWebView on macOS can be
throttled or suspended before that code runs, which left the window
hidden forever. The main and settings windows already avoided this with
the painted event plus a fallback timer, but that timer was armed on
WindowRuntimeReady, which a frozen webview never reaches either.

Route all dialogs through the same mechanism: the auto-size hook emits
the painted event instead of showing the window, Go shows and focuses
it on that event, and a fallback timer armed at creation shows it after
3 s regardless. The browser-login popup opens the browser in an
after-show callback so the browser still lands in front of the popup,
also on the fallback path.

* [client] Tie install-progress hidden-window restore to the current popup

CloseInstallProgress nils s.installProgress before calling w.Close(), so a
replacement popup can open before the old window's WindowClosing event runs.
The old callback then restored the windows the replacement had just hidden,
because the restore sat outside the identity check.

Guard the restore with the same check the state reset uses, and restore from
CloseInstallProgress itself so the programmatic close path still re-shows the
hidden windows — mirroring how CloseBrowserLogin already handles it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [client] Correlate painted reports with the window generation that sent them

A painted report carried only the window name, so a late report from a popup
that was already closed and replaced marked its replacement ready. The
replacement was then shown before its own frontend had rendered, which is the
blank-dialog case this flow exists to prevent.

Each dialog start URL now carries a monotonic generation token, echoed back by
ReadySignal, and a report whose token no longer matches the live window is
dropped.

* [client] Separate a window being painted from its frontend being mounted

One flag gated both showing a window and emitting to it, so the fallback timer
set it for a frontend that had not subscribed yet: the queued events were
flushed into a window that could not hear them, losing the login trigger and
the settings tab selection.

Showing is now gated on painted and emitting on mounted, and only a real
frontend report sets mounted. The fallback timer also moved to its own helper
so the runtime-ready hook can rearm it, giving the frontend a full budget to
mount rather than sharing one with webview boot.

* [client] Tag hidden windows with the popup that hid them

Windows hidden while a popup owned the screen went into one untagged list, so
whichever popup closed first restored all of them and emptied the list. An
install started during SSO login re-showed the main window the login popup had
deliberately hidden, and left the login popup with nothing to restore.

Each entry now records the popup that hid it, and a restore releases only that
popup's own entries. This also subsumes the manual filtering CloseRenewFlow did
to keep its own session-expiration window from being re-shown.

* [client] Cover the hidden-window bookkeeping with tests

application.Window carries unexported methods, so the hide/restore paths could
not be faked and the earlier tests could only assert which entries survived a
restore, never which windows were actually shown.

The bookkeeping now goes through hideableWindow, the four methods it needs,
with the window enumeration and the main-window raise behind seams that are nil
in production. That makes the case the owner tag exists for testable end to
end: an install started during SSO login restores only the login popup it hid,
and leaves the main window hidden until the login popup itself closes.

* [client] Report the first paint from unstamped windows too

The main and settings windows carry no generation token, so ReadySignal
saw an empty generation that already matched the ref's initial value and
never emitted the painted event. Those windows only became visible through
the fallback timer, and their frontend was never marked mounted, so the
login trigger and the requested settings tab stayed queued.

Start the ref from null so the first report goes out regardless of the
generation value.

* [client] Hand covered windows over when a popup closes under another

Closing the browser-login popup while the install-progress popup was
still up restored the main window the login had hidden, even though the
install popup was meant to own the screen until it finished. The owner
tag on each hidden entry only stops a popup from restoring another's
windows; it says nothing about what to do with its own when a second
popup still covers them.

Track which popups currently own the screen and, on restore, re-tag the
entries another live popup covers to that popup instead of showing them.
A popup is never handed its own window, so closing the popup on top still
brings the one below back.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 15:32:48 +02:00
Zoltan PappandDaniele Casciani 3906295446 [client] Add Homebrew cask e2e test (#7618)
* [client] Migrate macOS cask template to Homebrew install steps

Homebrew deprecated the postflight and uninstall_preflight cask stanzas
in favour of the declarative *_steps DSL, so every brew command that
evaluates netbirdio/tap now prints deprecation warnings. Once the
deprecation becomes a disable the generated cask stops loading and
netbird-ui can no longer be installed or upgraded through Homebrew.

The *_steps blocks take JSON-serialisable steps run in a sandbox rather
than arbitrary Ruby, so system_command is re-expressed as run/remove.
The two postflight blocks merge into one because a cask carries only a
single instance, preserving the original order. set_permissions moves
from a hardcoded /Applications to base: :appdir, matching what the
installer invocation already did. The launchctl fallbacks keep their
tolerant semantics through must_succeed: false, and remove is a no-op
when the plist is absent.

(cherry picked from commit df3756151f)

* [client] Test the macOS Homebrew cask on a disposable runner

The cask template only runs on real macOS with Homebrew, sudo and
launchd, so changes to it have never been exercised before merge. This
job installs the rendered cask on a GitHub macOS runner, walks the
uninstall through a running, stopped and missing daemon, and reinstalls
over the tap's published legacy cask, which is the path every existing
user takes on their next upgrade.

The fixture is the published cask itself rather than a pinned version
and checksums, so the test follows each release instead of breaking at
the next one. The installer scripts inside the signed archives are not
under test, which is why their paths are left out of the trigger.

* [client] Address SonarCloud findings in the Homebrew cask test

Positional parameters move into local variables and the scenario switch
gains an explicit default, so an unknown scenario fails instead of
silently running the plain install and uninstall path.

* [client] Make the Homebrew cask test deterministic with a stub bundle

The released installer script opens the UI as root, which never returns
on a headless runner, so a test that installs the published archive
hangs until the job timeout. The cask itself never looks past two script
paths and a version argument, so the test now builds a stub bundle on
the runner, serves it from a local HTTP server and renders the template
against it. The scripts ship without the executable bit, which turns the
0755 check into proof that set_permissions ran, and the stub records the
version and uid it received. The published archives are still downloaded
to assert the two script paths exist, and the published cask still
supplies the legacy stanzas for the reinstall scenario.

* [client] Drop the launchctl stderr check from the Homebrew cask test

The test asserts what the cask template promises: install, uninstall and
no deprecation warnings. Whether the uninstall steps print launchctl
errors is a review remark on the template, not part of that contract.

* [client] Retry the daemon start in the Homebrew cask test stub

A reinstall runs the previous cask's bootout and the new postflight
within a second of each other. launchd is still tearing the old daemon
down at that point, so loading the same label again fails with EIO. The
stub now retries the start for up to fifteen seconds, and the test still
verifies afterwards that the daemon reached the running state.

---------

Co-authored-by: Daniele Casciani <d.casciani@genogra.com>
2026-10-01 21:00:08 +02:00
Eduard Gert 7ff709f565 [client] Keep the delete-profile dialog open until the delete finishes (#7752)
* [client] Keep the delete-profile dialog open until the delete finishes

Confirming a profile deletion closed the dialog straight away and left the
daemon call running in the background. A slow delete then looked like nothing
had happened: the dialog was gone, the profile was still listed, and the row
only disappeared whenever the refresh landed.

The confirm dialog now owns the action. It stays open with the confirm button
spinning, closes once the call resolves, and surfaces a failure after it is
gone rather than behind it. Nothing on the daemon path carries a deadline, so
the wait is bounded in the dialog instead: Cancel comes back after five seconds
and the wait is abandoned at thirty, which keeps a hung daemon from trapping
the user in a modal that cannot be dismissed.

A loading button keeps its own variant colours rather than the disabled skin,
which dimmed the spinner to grey on the danger variant, and blocks input
through aria-disabled and a click guard instead.

* [client] Match the theme and anonymize pickers to the language switcher

* [client] Settle a confirm dialog only from the run that opened it

Cancelling at the stall point leaves the action running, and the provider is
mounted once: take() read whichever settler the ref held when the stale run
finally finished. Open another prompt in the meantime and that run answered it
— a hung delete that later resolved confirmed a profile switch nobody accepted,
and its timeout closed the new dialog with an unrelated error.

Each run now remembers the settler it was dispatched for and settles only while
the ref still points at it. A late arrival finds a stranger there and answers
nothing.

* [client] Add the shared Select the theme and anonymize pickers use

The picker rework landed without the component both pickers import, so the
branch did not compile. Add it, and name it for what it is: a select of a few
options, with nothing settings-specific about it, so it sits with the other
input controls rather than under a name that discourages reuse.

* [client] Drop the Select header comment

* [client] Fix cubic comments

* [client] Name the Select trigger with the option it is showing

* [client] Name the language trigger with the language it is showing

* [client] Hold the confirm dialog for 15s before offering cancel
2026-09-29 17:08:33 +02:00
Zoltan Papp 2d28f9002a [client] Fix the Windows tray deadlock on re-entrant window creation (#7449)
* [client] Fix the Windows tray deadlock on re-entrant window creation

The Wails systray runs the left-click handler synchronously inside the
tray window procedure, and creating a window on a running app pumps a
nested Win32 message loop while WebView2 initialises. ensureWindow held
the non-reentrant createMu across that creation, so the second button-up
of a double click re-entered ShowWindow from the pump and blocked the
main thread on its own lock. A goroutine holding createMu while the main
thread pumped, and the Open* dialogs holding mu across NewWithOptions,
Show, Hide and InvokeSync, exposed the same inversion.

WindowManager now serialises creation with a per-slot creating flag and
queues the callers' operations until the window exists, and no Wails call
runs while mu is held. The tray click and second-instance handlers call
ShowWindow off the message loop.

* [client] Serialize window operations while a slot is being created

Callers arriving after the window is published but before the creator
has drained the queue took the existing-window fast path and could run
ahead of older queued operations, so a newer SetURL could be overwritten
by an older one. withWindow now queues every caller while the creating
flag is set and clears the flag only once the queue is seen empty under
the lock.

A factory panic or a nil window left the creating flag set and the slot
dead; creation and drain now reset that state on early exit.

hideOtherWindows records the windows it hid only when no restore ran
in between, tracked by a generation counter, and re-shows them otherwise,
so a restore racing the hide cannot strand hidden windows.

* [misc] Run the client/ui subpackage tests in CI

The three test workflows filtered the package list with a `/client/ui`
prefix match, which dropped the subpackages along with the package that
cannot compile without a frontend build. `services`, `preferences`,
`i18n` and `authsession` all carry Go-side unit tests that never ran,
including the window manager re-entrancy regression test.

Anchor the pattern so only `client/ui` itself is excluded. The linux leg
keeps the prefix match on 386, where only the 64-bit gtk4/webkitgtk dev
packages are installed and the Wails application package would fail to
link, and the alpine container job keeps it for the same reason.

* [misc] Run the client/ui subpackage tests on a gtk4 4.10 runner

The previous commit let the subpackages into the linux client job, where
client/ui/services failed to build: the wails runtime's linux cgo layer
uses GtkFileDialog, which arrived in gtk4 4.10, and the job's ubuntu-22.04
runner ships 4.6.

Move them to their own job pinned to ubuntu-24.04 and restore the linux
client job's original exclusion, leaving the 386 and privileged legs on
the runner they have used since 2024. The new job needs no build cache,
sudo or privileged tag, so it stays a few seconds long.

Darwin and Windows keep the anchored pattern from the previous commit and
already run these tests green, including the window manager re-entrancy
regression test on the platform the deadlock was reported on.

* [client] Defer a window close that lands while the window is still being created

WindowManager publishes a dialog's slot only after the factory returns,
and on Windows the factory blocks in the WebView2 embed pump. A Close*
arriving in that gap found a nil slot and returned without doing
anything, so the dialog appeared afterwards for a flow that had already
been cancelled. The pre-fix Open* dialog functions held mu across the
whole creation, which blocked a concurrent Close* until the slot was
set; removing that lock hold reopened this gap.

Close* now goes through closeWindow: while the slot is being created it
records a closer in pendingClose, and finishCreation runs that closer
before any queued operation, so a window that is going away is never
shown and Wails never sees a Show on a destroyed window, which would
recreate it. Ops queued behind a close are dropped; windowOp carries no
factory, so they cannot be replayed into a new creation, and the
frontend callers reissue on the next state change.

The browser-login slot uses the same restoring closer from both
CloseBrowserLogin and CloseRenewFlow, since the popup's WindowClosing
hook only restores on a user close. Where two closers race one
creation the first registered wins, so a later caller cannot replace a
restoring closer with one that does not restore.
2026-09-14 15:37:46 +02:00
Brandon Hopkins ec0c36b0e7 [client] Add light mode with system, light, and dark theme options (#7344)
* desktop UI light mode

* Theme review fixes plus macOS window outline fix

* Windows runtime chrome re-theming plus apply serialization

* Windows chrome threading and theme event ordering fixes

* Darken toggle and setting sidebar text

* resolve theme appearance, apply on UI thread

* read theme once per window

* Re-assert Windows dark opt-in after SetTheme

* split app-wide GTK theming from per-window chrome

* Update Wails dependency and checksums

* KDE tray icon panel fix

* Five review fixes: theme ordering, cgo dedup, KDE panel resolution

* Path guard hardening, toggle contrast, windows comment

* non-vacuous escape tests

* Default view edits

* Polish settings nav, controls, borders, and disc

* Profiles settings boarder, modals, and buttons

* Additional edits based on feedback

* Switch colors away from slight blue hue

* Update missing lang

* Fix vertical tab active view
2026-09-11 08:25:10 -07:00
Riccardo ManfrinandZoltan Papp e14006ddc1 [client] mobile MDM bridge — iOS + Android setMDMPolicyFetcher entrypoint (#6435)
* MDM Android mobile wiring

* Removes dead code

* Removes static vars

* Now we need to apply MDM in the GetConfig

* You now need to explicitly call these around

* Adds iOS wiring

* Resolve merge conflicts from main

- login.go: keep both new imports (mdm + nbnet + server)
- ios/NetBirdSDK/client.go: additive struct-field merge (mdmLoader + stateMu/connectClient/config)
- setconfig_mdm_test.go: adopt new withMDMPolicy(t, s, policy) signature; fix stray old-signature call in TestSetConfig_MDMAllow_ManagementURLPortNormalized

* Convey MDM overlay config to Debug Bundle output

Aligns to other clients OSes behavior

* Solved conflict in client.go

* Fixup helper withMDMPolicy -> configWithMDM

* Fixup after merge

* Resolve merge conflicts

* [client] Move MDM enforcement logic into a shared Go layer (#7319)

The mobile bridges only carried the policy fetcher, leaving every
enforcement decision to the native apps: the desktop derived its UI
restrictions in the Wails service layer, the daemon kept the conflict
machinery in the server package, and both mobile bridges duplicated the
JSON fetch adapter. Anything the native side had to reimplement was a
place for iOS and Android to drift apart.

Enforcement now lives in client/mdm and is consumed identically by all
three platforms:

- conflicts.go holds the value-aware conflict checks lifted out of the
  daemon, so the same normalization (canonical URLs, PSK sentinel echo)
  applies wherever a config change is validated.
- restrictions.go derives the UI enforcement snapshot from a policy and
  renders it in the JSON shape the desktop frontend already consumes.
  The service-layer types become aliases, keeping one source of truth.
- jsonloader.go replaces the adapter that was copy-pasted into both
  bridges.
- changedetector.go moves change detection off the native side: the
  caller forwards the OS notification and asks whether the managed
  configuration actually changed, instead of diffing dictionaries
  itself.

The mobile bridges gain the enforcement the daemon already had. The
Preferences getters resolve managed keys from the policy, so a naive UI
shows the enforced value; Commit rejects a staged change that diverges
from a managed key; NewAuth resolves the managed management URL before
persisting the config and overlays the policy on it, so a login can no
longer run against a URL the policy forbids. Android's profile
mutations fail closed when disableProfiles is set.

NewAuth takes the fetcher as a required argument rather than keeping a
policy-blind overload: the apps consume this code as a submodule, so a
compile error at the bump is the point. The mobile PSK getter is
replaced by a presence check — the key has no reason to cross the
bridge, and not returning it means the native side needs no redaction
sentinel of its own.

* [client] Resolve the main merge conflicts in the MDM integration

The merge commit was recorded with the conflict markers still in the
tree. Resolve them so the branch builds again:

- client/ios/NetBirdSDK: keep both the mdm and mobile imports, and keep
  the mdmLoader/mdmDetector fields next to main's stateMu documentation.
- client/server/mdm.go: drop the conflict helpers main added locally,
  they already live in the client/mdm package on this branch, and keep
  the new checks main introduced (allowRemoteJobs, enableLocalMetrics,
  localMetricsAddress) as calls into the package-level helpers.
- client/mdm/conflicts.go: add ConflictStringPtr, the presence-aware
  string check main needs for the optional localMetricsAddress field.
- Port the two tests main added over the per-Server loader helper and the
  configWithMDM helper, both of which replaced the package-level policy
  injection this branch removed.

* [client] Reject explicit empty PSK when MDM enforces a pre-shared key

The SetConfig, Login and mobile Commit conflict checks collapsed the PSK
to a plain string, so an explicit empty value was indistinguishable from
an unset field and slipped past the MDM gate, clearing the persisted key.
Carry the optional field as a pointer through ConflictStringPtr, treating
only the redaction sentinel as a no-op echo. ConflictString had no other
callers and is removed.

* [client] Apply MDM overlay on the preloaded iOS config in Run

Run only overlaid the MDM policy when the config was loaded from file,
so the tvOS path fed by SetConfigFromJSON started with unmanaged
settings. Apply the overlay after the config source is selected, as the
other resolution sites already do.

* [client] Gate non-active profile logout behind the MDM profiles switch

The mobile ProfileManager let LogoutProfile clear credentials of any
profile even when disableProfiles was enforced. Follow the daemon's
validateProfileLogout semantics: logging out of the active profile is a
plain logout and stays allowed, logging out of any other profile is
profile management and is rejected under the policy.

* [client] Resolve the managed management URL through the MDM overlay on mobile

NewAuth on Android and iOS replaced the caller URL with the raw policy
value before persisting, so a malformed managed URL failed config
validation and blocked the login instead of being skipped with a warning
like the overlay does. Preferences.GetManagementURL likewise echoed the
raw policy string to the native UI even when the overlay had rejected it.

Follow the daemon: persist the caller URL, overlay the policy on the
resolved config, and report the overlaid ManagementURL as the effective
value.

* [client] Clean up MDM review leftovers

Drop the unused ChangeDetector.Current, point the stale LoadPolicy
comment references at Loader.Load, and move the profileEmail godoc back
above its function.

* [client] Check remote jobs and local metrics keys in the mobile MDM conflict gate

MDMConflicts skipped allowRemoteJobs, enableLocalMetrics and
localMetricsAddress even though the overlay applies all three and the
daemon gate already checks them, so a mobile Commit could persist values
diverging from the enforced policy. Align the list with the daemon.

* [client] Silence the deprecated PreSharedKey lint in the login conflict test

The legacy LoginRequest.PreSharedKey field is deliberately exercised by
the test, matching the nolint already carried by the production path.

* [client] Publish the mobile MDM loader and detector atomically

SetMDMPolicyFetcher wrote the loader and change detector as two plain
fields that Run, the OS-change callback and the restrictions getter read
from other threads without synchronization. Hold both behind a single
atomic pointer so a registration is published as one unit and readers
always observe a matching loader and detector pair; Preferences gets the
same treatment for its loader. Exported signatures are unchanged.

* [client] Report the MDM-overlaid remote jobs value from mobile Preferences

GetRemoteJobsAllowed returned the staged or persisted value even when
the policy manages allowRemoteJobs, so the native settings UI could show
a value the Commit gate would reject. Resolve it through the overlay like
GetManagementURL does.

* [client] Stop persisting the MDM-overlaid config after mobile logins

NewAuth already writes the config through UpdateOrCreateConfig before
the MDM policy is overlaid, and the login itself never mutates the
Config. The post-login WriteOutConfig calls therefore only rewrote the
same file with the enforced ManagementURL and PreSharedKey in it, so a
removed or changed policy kept acting through the persisted values.

* [client] Document that the MDM overlay on Config is not reversible

ApplyMDMPolicy promised that an empty Policy clears a prior overlay, but
applyMDMPolicy only resets the enforcement metadata and the runtime-only
upload URL; the enforced ManagementURL, PreSharedKey and flags stay. Every
lifecycle owner resolves the base Config again before applying, so state
that contract instead of the reversibility that was never implemented.

* [client] Re-resolve the tvOS preloaded config before every MDM overlay

The iOS Client kept the config parsed from SetConfigFromJSON and applied
the MDM overlay onto that same instance on every Run, IsLoginRequired
and DebugBundle, so a key removed from the policy stayed enforced. Store
the JSON instead and parse it per load through one loadConfig path.

Auth serialized the overlaid config from GetConfigJSON, which tvOS then
persisted to UserDefaults and fed back as the preload. Keep the resolved
config as the base, run the login on a JSON round-trip copy with the
overlay, and return the base from GetConfigJSON.

* [client] Serve the MDM-managed management URL without touching the config file on mobile

Preferences.GetManagementURL resolved a managed URL by reading and
overlaying the persisted config, so a corrupt file or the tvOS sandbox
turned an enforced URL into a read error. Return the canonical managed
value directly, the same string BuildRestrictions already hands to the
UI, and only fall back to the staged or persisted value when MDM does
not manage the key.

NewAuth validated the caller-supplied management URL before the overlay
ran, so a malformed or echoed value blocked or persisted under an MDM
policy that already dictates the URL. Ignore the caller value while the
key is managed; the login runs against the overlay either way.

* [client] Align the MDM loader docs with the fetcher precedence and make disableAdvancedView a tristate

NewLoader, PolicyFetcher and the darwin/windows loadPlatform docs claimed
the fetcher is unused on desktop, while every loader returns its values
when one is injected. That precedence is the seam the server tests rely
on across platforms, so the docs now describe it; production desktop
callers still pass nil and keep the registry / plist authoritative.

Fields.DisableAdvancedView collapsed "managed and false" into the same
JSON as "not managed", unlike AllowServerSSH and the daemon's optional
proto field. Carry it as a *bool so the UIs can tell the two apart; the
desktop reflect loop skips pointer fields already, and the mobile
decoders treat null as not managed.

* [client] Clean up MDM review nits

- ResolveConflicts treats a managed key whose ConflictCheck has no Check
  as a conflict instead of dereferencing nil.
- Ticker.Run and ChangeDetector.Changed share policyChanged so the diff
  semantics and the log line cannot drift apart.
- TestLoader_NilFetcherReturnsEmpty skips on windows/darwin, where a nil
  fetcher reads the real registry / plist.
- The profilemanager test loader checks GetInt before GetBool so integer
  keys survive the round trip, and the PSK tests use the exported
  redaction sentinel.

* [client] Fix int policy values coercing to bool in the MDM test helper

withMDMPolicy rebuilt the policy map by trying GetString, then GetBool,
then GetInt. Policy.GetBool accepts native ints (non-zero means true), so
an int-valued key such as wireguardPort round-tripped through the helper as
the bool true and GetInt was never reached. Try GetInt before GetBool, as
the profilemanager helper already does; GetInt does not coerce bools, so
booleans still fall through to GetBool.

No test sets an int key today, so this was latent: the first test to
exercise the wireguardPort conflict gate would have seen ConflictInt64
report a conflict for every value, including a matching one.

---------

Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com>
2026-09-08 11:53:07 +02:00
Theodor Midtlien 12e8874517 [client, relay, management] Bump go version to 1.26 and go-quic to v0.62.0 (#7359)
* Bump go version to 1.26 and go-quic to v0.62.0
* Replace deprecated ecdsa public key assembly and add tests for jwt
* Update goversioninfo
* Pin go toolchain to 1.26.7
2026-08-31 18:01:14 +02:00
Max 7ffbcb0016 [client] Add Ukrainian localization for desktop client (#7035) 2026-08-31 16:28:30 +02:00
Zoltan Papp 086d8ba507 [client] Close the session-expiration dialog only on renewal (#7337)
* [client] Close the session-expiration dialog only on an actual session renewal

The dialog auto-closed on any Connected status snapshot, but the daemon
emits Connected periodically regardless of session state, so the warning
popup disappeared on the next snapshot (~30s) with no chance to
re-authenticate. Close only when the snapshot's session deadline jumps
past the one the dialog was opened for, meaning the session was renewed
from another surface (tray action, CLI, main window).

* [client] Compare session renewals against the exact deadline in the expiration dialog

The dialog reconstructed its reference deadline from the relative seconds
URL parameter, which carries up to a second of truncation and mount
latency, forcing a renewal-detection margin wide enough to miss a renewal
made shortly after the previous login. Pass the absolute deadline (unix
ms) from both tray call sites - the extend flow's cached deadline and the
final warning's event metadata - so any forward jump in the snapshot
deadline closes the dialog; the seconds-derived fallback with a small
tolerance remains for an unknown deadline.

* [client] Derive the expiration dialog countdown from the deadline

The per-second decrement assumed the interval fires once a second, but
the webview's timers get suspended for tens of seconds under App Nap /
hidden-window throttling, leaving the displayed countdown behind the
wall clock by the suspended time. Recompute the remaining time from the
absolute deadline on every tick so the first tick after a suspension
shows the correct value.

* [client] Tolerate the warning deadline's second precision in the renewal check

The final-warning metadata formats the deadline as RFC3339 truncated to
whole seconds while the status snapshot keeps millisecond precision, so
an unchanged deadline could appear up to 999 ms newer than the exact URL
value and close the dialog on the first snapshot. Allow a sub-second
tolerance on the exact path; any real renewal jumps by at least seconds.
2026-08-31 10:32:36 +02:00
Viktor Liu ccf8f43cb1 [client] Ask the OS for privileges when a guarded SSH setting is changed (#7066) 2026-08-25 20:15:16 +02:00
Zoltan Papp a08f7f63f4 [client] Create GUI windows on demand and destroy them on close (#7096)
The main and Settings windows were created at startup and kept alive hidden on close, so an idle tray held two webview processes for surfaces the user may never open. Both are now built on first show and destroyed on close, which takes the idle footprint on macOS from ~160 MB to ~74 MB.

The WindowManager owns creation: it rebuilds the main window on the next show and hands out live pointers, since a stored one goes stale. Every show is deferred until the frontend reports it has rendered, so a freshly created window is never on screen empty, with a timeout so a frontend that never reports cannot strand a window hidden.
2026-08-25 11:40:31 +02:00
Viktor Liu 00243b28bc [client] Add missing anonymization and SSH privilege translations (#7269) 2026-08-21 10:05:33 +02:00
Zoltan Papp 16544dbc58 [client] Pass stored email as login hint from UI and keep it on logout (#7199)
* [client] Pass stored email as login hint from UI and keep it on logout

Follow the CLI pattern: the Wails UI now reads the account email from the
user-owned profile state file and passes it as the OIDC login_hint on login
and session extend, since the daemon-side fallback runs as root and cannot
see the user's state file. Logout no longer deletes the stored email, so a
later login preselects the account at the IdP; profile removal remains the
operation that deletes it.

* [client] Log ignored profile lookup errors in extend-session hint fallback
2026-08-15 11:21:57 +02:00
Eduard GertandClaude Opus 4.8 85dd335836 [client] Add CI check for translation key parity (#6852)
English (en) is the source of truth for UI translation keys; the other
nine locales rely on runtime English fallback for any missing key, so a
gap never surfaces in CI. Add a dependency-free Node check that fails
when any locale declared in _index.json does not carry the exact same
key set as en (missing or orphaned keys), wired into a dedicated
UI Translations workflow that runs on locale changes.

Also close the one existing gap the check found: ja was missing
daemon.outdated.download ("Download Latest").

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-14 10:57:11 +02:00
Viktor Liu e290769df1 [client] Take the graphical session answer from the caller instead of the daemon environment (#7187) 2026-08-13 10:28:34 +02:00
Viktor Liu 95a458801c [doc] Point translation contributions at Crowdin (#7161) 2026-08-11 16:02:09 +02:00
Viktor Liu 5584f8ef0a [client] Add strict anonymization level and MAC anonymization to debug bundles (#7102) 2026-08-10 11:27:20 +02:00
Zoltan Papp 1e2a7aa571 [client] Add a UI setting to stay connected after quitting (#7078)
Quitting the GUI from the tray always sent a Down RPC, dropping the VPN
connection with it. Some users want the connection to survive the UI.
2026-08-06 12:56:27 +02:00
Zoltan Papp 98f8a24108 [client] Ship a legacy GTK3 UI package for distros without WebKitGTK 6.0 (#7040)
Ship a legacy GTK3 UI package for distros without WebKitGTK 6.0
2026-08-06 12:13:20 +02:00
Zoltan Papp 2a61eac047 [client] Fix Linux tray right-click opening the main window (#7039)
## Describe your changes

On SNI hosts that report icon clicks via Activate (KDE Plasma, Waybar),
Wails fired the left-click handler on every dbusmenu 'opened' event, so
a right click opened the tray menu and immediately raised the main
window, which stole focus and closed the menu.

	Host                           Left click              Right click
KDE Plasma, Waybar main window (Activate) menu (host-rendered)
	GNOME Shell + AppIndicator     menu only               menu only
	Minimal WMs via XEmbed host    main window (Activate)  XEmbed GTK popup

Point the wails/v3 replace at the netbirdio fork (v3.0.0-beta.3 plus the
fix): once the host has sent Activate/SecondaryActivate, a menu open no
longer fires the click handler, while AppIndicator-only hosts that
signal clicks solely via 'opened' keep the old behavior.


## Issue ticket number and link

<!--
Required for anything that changes behavior. Link the issue (or the
validated
discussion it came from) that the NetBird team already agreed on. See

https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#ticket-first-pr-second
-->

## Stack

<!-- branch-stack -->

### Checklist
- [x] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] I ran and tested this change locally — I did not rely on CI to
find out whether it works
- [ ] This PR has a single purpose (not a fix + refactor + feature in
one)
- [ ] This change is a trivial fix, **OR** it links an issue the NetBird
team agreed on beforehand. Changes to the public API, gRPC protocols,
functionality behavior, CLI / service flags, or new features always need
that agreement first. See
[CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#ticket-first-pr-second).

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)

### Docs PR URL (required if "docs added" is checked)
Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Chores

* Updated underlying application components to support compatibility and
ongoing maintenance.
* Clarified Linux tray interaction documentation, including
platform-specific left- and right-click behavior and menu activation.
* No user-facing features, workflow changes, or visual updates are
included.
* Existing Linux tray behavior remains unchanged.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-04 17:36:04 +02:00
Zoltan Papp f2d13b884a [client] Fix session expired relogin (#7055)
## Describe your changes

After the SSO session expires, the daemon tears the engine down
permanently
(management returns `PermissionDenied` → `runCancel()` → the retry loop
exits
for good). The "Session expired" dialog's Login button still drove the
extend-session flow, which requires a live engine: the user completed
the full
browser SSO + 2FA round trip only to get
`Failed to extend the session — engine is not initialised`, with no way
out
other than quitting and relaunching the client.

Reproduce:
1. Log in on a desktop client with session expiration enabled (e.g. 16h
TTL).
2. Let the session expire (e.g. leave the machine asleep overnight).
3. Wake it, click **Login** on the "Session expired" dialog, complete
SSO + 2FA.
4. The error dialog appears and every retry fails the same way.

Changes:
- The expired branch of the session-expiration dialog now emits
`trigger-login`, driving the full `Login → SSO → Up` sequence that
rebuilds
the client, instead of the extend flow (an expired session can no longer
be
  extended).
- `RequestExtendAuthSession` fails fast when the engine is already gone,
so the
  browser/2FA round trip is not wasted on a doomed extend.
- The expired tray row navigated the main window to `/#/login`, a route
that
does not exist and fell through to the main page without starting a
login;
  it now emits `trigger-login` as well.


## Issue ticket number and link

<!--
Required for anything that changes behavior. Link the issue (or the
validated
discussion it came from) that the NetBird team already agreed on. See

https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#ticket-first-pr-second
-->

## Stack

<!-- branch-stack -->

### Checklist
- [x] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] I ran and tested this change locally — I did not rely on CI to
find out whether it works
- [ ] This PR has a single purpose (not a fix + refactor + feature in
one)
- [ ] This change is a trivial fix, **OR** it links an issue the NetBird
team agreed on beforehand. Changes to the public API, gRPC protocols,
functionality behavior, CLI / service flags, or new features always need
that agreement first. See
[CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#ticket-first-pr-second).

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)

### Docs PR URL (required if "docs added" is checked)
Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Bug Fixes**
- Improved session extension handling when the client engine is
unavailable by prompting users to log in again.
- Updated expired-session behavior to trigger the standard login flow,
providing a more consistent sign-in experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-04 16:02:31 +02:00
Riccardo Manfrin d29bc23bb7 [client] launch macOS GUI as the logged-in user after install/update (#6962)
## Describe your changes

In unattended installs/updates there may be no logged-in user, so
there's no context to start the GUI (nor anyone to see it).

The bug is the GUI being started in the wrong user context / inheriting
the wrong `$HOME` (OS mechanics aside).

Today the GUI is started by a per-user LaunchAgent, i.e. on behalf of
the user who logs in — no login ⇒ no GUI.

The patch aligns to this: it launches the GUI on behalf of the logged-in
console user if one exists, otherwise it delegates the launch to the
per-user LaunchAgent at next login.

Additionally it logs when default UI settings are applied.

Note (small caveat): the LaunchAgent auto-starts the GUI at login only
once it's been registered — which happens on the first GUI launch in the
user's context. On an MDM/unattended fresh install done with no user
logged in (where the user has never run the GUI before), they may need
to start it manually once; it self-registers from then on.

## Issue ticket number and link

No public issue — reported internally (community report on Slack: macOS
advanced-view + onboarding reset on every update, esp. via MDM/Munki).
Buggy line on main:
https://github.com/netbirdio/netbird/blob/dd2bdc0de3aa14dd14dc90611c69c420bb3d3eb2/release_files/darwin_pkg/postinstall#L33

## Stack

<!-- branch-stack -->

### Checklist
- [x] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)

Internal macOS installer / GUI-launch behavior. No public API, CLI, or
configuration change: the fix only changes the user context the desktop
GUI is launched in after a pkg install/update.

### Docs PR URL (required if "docs added" is checked)
Paste the PR link from https://github.com/netbirdio/docs here:

N/A

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/netbirdio/codesmith/netbird/pr/6962"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img
alt="View with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a>
<a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1787923973&installation_model_id=427504&pr_number=6962&repository=netbirdio%2Fnetbird&return_to=https%3A%2F%2Fgithub.com%2Fnetbirdio%2Fnetbird%2Fpull%2F6962&signature=5226bbc7986fc60eb8b4ab77e98ac17a6534864763fc6407048bcbbb1550394b"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you
need. Autofix is disabled.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Bug Fixes**
- Improved macOS installation and updater UI launching to occur only
when an active, valid GUI console session is detected.
- Prevented UI launches during unattended/system, root, or
login-window-related installs.
- Ensured the app is launched in the correct console-user context, and
skips cleanly when username/UID resolution fails.

- **Improvements**
- Added clearer informational logging when the UI preferences file is
not found and default preferences are used.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-03 16:28:10 +02:00
Viktor Liu ee1389d736 [client] Keep the UI running when the notification service fails to start (#6959) 2026-08-03 16:23:10 +02:00
Zoltan Papp 2f721ec0d5 [client] Keep the account email backing the SSO login hint correct (#6986)
## Describe your changes

Three fixes to how the desktop GUI keeps the account email that backs
the SSO `login_hint`. Each is independent and reviewable on its own.

**1. Store the email after a GUI SSO login**

The daemon returns the authenticated user's email from `WaitSSOLogin`
but cannot persist it: it runs as root while the per-profile state file
is user-owned. The CLI's `handleSSOLogin` writes it after its own
`WaitSSOLogin`; the GUI path read the value and dropped it.

The profile was therefore left with no email, so `Profiles.List` showed
no account for it, and later logins and session extends went out with no
`login_hint` — leaving the IdP to pick an account instead of reusing the
one the profile belongs to. Mirror the CLI and store it, next to the
`Logout` path that already clears the same file for the same reason.

**2. File the email against the profile the login ran for**

`SetActiveProfileState` resolves the target itself, so it writes to
whichever profile is active when it is called. A GUI SSO login spans
seconds of user interaction in the browser, and the tray stays clickable
throughout: switching profiles in that window left the email filed under
the profile that happened to be active when the flow returned. The wrong
profile then advertised an account it does not own, and offered it as
the `login_hint` next time.

Adds `SetProfileState(id, state)`, the write-side counterpart of the
existing `GetProfileState(id)`, and keeps `SetActiveProfileState` as a
wrapper for callers with no particular profile in mind. `Login` now
reports the profile it resolved so the frontend can hand it back with
the SSO wait, which closes the window.

**3. Delete the email when a profile is removed**

Removing a profile left its state file behind: the daemon deletes what
it owns, but the email file is user-owned and out of reach for a root
daemon — the same split that already puts the `Logout` cleanup on the UI
side.

Beyond the stray file, legacy profiles are keyed by name rather than by
a generated ID, so recreating a profile under a removed one's name
inherited its email — shown as the account in the profile list and sent
as the `login_hint` on the next login.

## Issue ticket number and link

## Stack

<!-- branch-stack -->

### Checklist
- [x] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] This change does **not** modify the public API, gRPC protocols,
functionality behavior, CLI / service flags, or introduce a new feature
— **OR** I have discussed it with the NetBird team beforehand (link the
issue / Slack thread in the description). See
[CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first).

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)

### Docs PR URL (required if "docs added" is checked)
Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- SSO login details can now be saved to the specific profile selected
during sign-in.
  - Profile state can be managed independently for different profiles.

- **Bug Fixes**
  - Removing a profile now also cleans up its associated saved state.
- Cleanup issues no longer prevent successful profile removal and are
handled gracefully.
  - Existing active-profile behavior remains unchanged.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-03 14:21:40 +02:00
Zoltan Papp 6044663788 [client] Declare GTK4/WebKitGTK runtime deps for the Linux UI packages (#6893)
The 0.75.0 UI is built against GTK 4.14 (Ubuntu 24.04 runner) and Wails
v3 calls gdk_monitor_get_scale (GTK 4.14+) unconditionally, but the
deb/rpm packages only depended on netbird. On distros shipping an older
GTK4 (Ubuntu 22.04, RHEL 9, openSUSE Leap 15.6) the package installed
fine and then died at startup with a symbol lookup error (#6890).

Declare the real runtime dependencies so package managers reject the
install up front instead:

- deb: libgtk-4-1 (>= 4.14) and libwebkitgtk-6.0-4
- rpm: rich (boolean) dependencies that accept both the Fedora/RHEL and
the SUSE package names, with the 4.14 floor: (gtk4 >= 4.14 or libgtk-4-1
>= 4.14) (webkitgtk6.0 or libwebkitgtk-6_0-4)

Rich deps are supported by dnf and zypper (RPM 4.13+); the pinned
goreleaser v2.16.0 -> nfpm v2.46.3 -> rpmpack v0.7.1 chain passes the
parenthesized form through verbatim. On RHEL/Alma/Rocky 10 the
webkitgtk6.0 package comes from EPEL, which becomes an install
prerequisite for the UI.

The wails3 packaging config (client/ui/build/linux/nfpm/nfpm.yaml, local
dev packaging only) is kept in sync.

## Describe your changes

## Issue ticket number and link

## Stack

<!-- branch-stack -->

### Checklist
- [x] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] This change does **not** modify the public API, gRPC protocols,
functionality behavior, CLI / service flags, or introduce a new feature
— **OR** I have discussed it with the NetBird team beforehand (link the
issue / Slack thread in the description). See
[CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first).

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)

### Docs PR URL (required if "docs added" is checked)
Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/netbirdio/codesmith/netbird/pr/6893"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img
alt="View with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a>
<a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1787575510&installation_model_id=427504&pr_number=6893&repository=netbirdio%2Fnetbird&return_to=https%3A%2F%2Fgithub.com%2Fnetbirdio%2Fnetbird%2Fpull%2F6893&signature=93b4fc344fe4f4e6b781887386bfbae1a309007205c7b2ab061b58d89e29b9a8"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you
need. Autofix is disabled.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved Linux package installation compatibility by requiring
supported GTK 4.14 and WebKit components.
* Updated Debian and RPM packages to recognize equivalent
platform-specific library names.
* Refined Linux package metadata to support installation across a wider
range of distributions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-03 10:21:14 +02:00
Viktor Liu feecb993f4 [client] Restrict debug bundle log path and upload destinations (#6975) 2026-07-31 19:12:56 +02:00
Viktor Liu cff49237b6 [client] Stop and remove the daemon on netbird-ui cask uninstall (#6977) 2026-07-30 13:11:27 +02:00
Viktor Liu 0f5d2d91fb [client] Authorize daemon IPC callers by their local identity (#6967) 2026-07-29 20:32:26 +02:00
Viktor Liu f2c1070f95 [client] Fix UI crash on Windows builds without dark-mode support (#6958) 2026-07-29 13:52:18 +02:00
Zoltan Papp e3c4128164 [client] Exit GUI immediately on Windows session end (#6878)
Wails v3 runs the full app teardown synchronously inside WM_ENDSESSION,
overrunning the 5s end-session budget and triggering the "app is
preventing shutdown" screen with a forced kill. Intercept
WM_QUERYENDSESSION/WM_ENDSESSION and exit at once instead, and suppress
error dialogs, toasts, and the hide-on-close hooks once shutdown or a
tray quit has begun.

## Describe your changes

## Issue ticket number and link

## Stack

<!-- branch-stack -->

### Checklist
- [x] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] This change does **not** modify the public API, gRPC protocols,
functionality behavior, CLI / service flags, or introduce a new feature
— **OR** I have discussed it with the NetBird team beforehand (link the
issue / Slack thread in the description). See
[CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first).

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)

### Docs PR URL (required if "docs added" is checked)
Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/netbirdio/codesmith/netbird/pr/6878"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img
alt="View with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a>
<a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1787474408&installation_model_id=427504&pr_number=6878&repository=netbirdio%2Fnetbird&return_to=https%3A%2F%2Fgithub.com%2Fnetbirdio%2Fnetbird%2Fpull%2F6878&signature=f09980d83ad32e37b92aa30a7d4552c402a140dbd4e4662a3ca12d1868e9f04b"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you
need. Autofix is disabled.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved shutdown handling on Windows by properly responding to system
end-session requests.
* Prevented the main window, settings window, and error dialogs from
reopening or interfering while the app is closing.
* Updated tray “Quit” flow to begin shutdown immediately, ensuring
active profile/connection operations complete cleanly.
* Suppressed UI notifications during shutdown to avoid stray messages
after exit starts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-28 14:09:16 +09:00
Zoltan Papp 9b4a5df925 [client] Use platform installer URL for manual update downloads (#6922)
The Wails UI regressed the non-enforced update download to the plain
GitHub releases page. Restore the old Fyne behavior: the tray update
item and the About card's Get installer button now open
version.DownloadUrl(), which points to the direct installer download
(pkgs.netbird.io) per OS/arch and falls back to the generic install page
where no installer exists.

Also fix the dead brew detection on darwin: exec.Command passed the
whole pipeline as a single argument to brew, so the check always failed.
Query the netbird formula and netbird-ui cask explicitly via exit codes
instead.

## Describe your changes

## Issue ticket number and link

## Stack

<!-- branch-stack -->

### Checklist
- [x] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] This change does **not** modify the public API, gRPC protocols,
functionality behavior, CLI / service flags, or introduce a new feature
— **OR** I have discussed it with the NetBird team beforehand (link the
issue / Slack thread in the description). See
[CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first).

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)

### Docs PR URL (required if "docs added" is checked)
Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/netbirdio/codesmith/netbird/pr/6922"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img
alt="View with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a>
<a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1787764680&installation_model_id=427504&pr_number=6922&repository=netbirdio%2Fnetbird&return_to=https%3A%2F%2Fgithub.com%2Fnetbirdio%2Fnetbird%2Fpull%2F6922&signature=3d9d640b25a17589114cc704e1d631895ffcd9f554947ff5790fe90d54410d2f"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you
need. Autofix is disabled.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a direct installer download option for manual application
updates.
* Non-enforced updates now open the appropriate platform-specific
installer instead of the general releases page.

* **Bug Fixes**
  * Improved macOS download behavior for Homebrew installations.
* Preserved architecture-specific downloads for Intel and Apple silicon
Macs.
* Updated the tray “About” links to use the GitHub repository and
documentation instead of the releases page.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-27 20:08:35 +02:00
Zoltan Papp a12a3e4603 [client] Keep session deadline visible across reconnects (#6847)
## Describe your changes

The tray session row flapped on every transient engine restart and went
blank at the exact moment the session expired:

- Watcher.Close zeroed the server-scoped recorder deadline on every
engine restart (sleep/wake, network change, stream errors), hiding the
tray row until the next successful login re-seeded it. Close now only
stops the timers; the client run loop clears the recorder when it exits
for real.
- Status.GetSessionExpiresAt masked past deadlines as zero, so the
status snapshot dropped the field right when the UI should have shown
"session expired". The getter now returns the raw value and the tray
renders past deadlines as "Session expired — sign in again", routing the
click to the login flow.
- The watcher rejected recently-expired deadlines (30s skew) delivered
by login/sync, clearing the recorder instead of surfacing the expiry.
Deadlines up to 30 days past are now recorded as expired; only older
values are rejected as garbage.

## Issue ticket number and link

## Stack

<!-- branch-stack -->

### Checklist
- [x] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] This change does **not** modify the public API, gRPC protocols,
functionality behavior, CLI / service flags, or introduce a new feature
— **OR** I have discussed it with the NetBird team beforehand (link the
issue / Slack thread in the description). See
[CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first).

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)

### Docs PR URL (required if "docs added" is checked)
Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/netbirdio/codesmith/netbird/pr/6847"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img
alt="View with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a>
<a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1787220767&installation_model_id=427504&pr_number=6847&repository=netbirdio%2Fnetbird&return_to=https%3A%2F%2Fgithub.com%2Fnetbirdio%2Fnetbird%2Fpull%2F6847&signature=77977ad47c5eda51e1d76e59bc736203b4592462bf94076811a20ea0f7f23aee"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>/codesmith</code> with what you
need. Autofix is disabled.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Tray “session expires” and countdown now accurately reflect expired
sessions and remaining time near boundary moments (including improved
rounding).
* **Bug Fixes**
* Recently expired deadlines are retained for correct status reporting,
while very old past deadlines are rejected and cleared.
* Session expiry “expires at” is preserved when the session watcher
closes during reconnect scenarios.
* Clicking an already-expired session in the tray now routes the user to
the login screen.
* **Tests**
* Updated and expanded coverage for recent/ancient past handling and
watcher close behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-22 15:22:16 +02:00
Riccardo Manfrin dc89b471fa [client] checks/enforce MDM disableAutostart on every GUI launch, not just fresh installs (#6782)
## Describe your changes

`applyAutostartDefault` gated all MDM enforcement behind the one-time
`AutostartInitialized` marker, so MDM `disableAutostart` only affected
fresh installs — a policy pushed after autostart had been enabled could
not
revoke the OS login-item. 

The PR adds follow-up MDM enforcements at the top of
the function: if at any time MDM sets `disableAutostart=true` and the OS
registration is
present, force `SetEnabled(false)` to align it.

Trade-off: once the admin lifts the policy, autostart stays off until
the user re-toggles
in Settings — consistent with "MDM always wins" behavior of the other
managed keys.

## Issue ticket number and link

Follow-up to PR https://github.com/netbirdio/netbird/pull/6738
(introduced
the `disableAutostart` MDM key with fresh-install-only semantics).

## Stack

<!-- branch-stack -->

### Checklist
- [x] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [x] I added/updated documentation for this change
- [ ] Documentation is **not needed** for this change (explain why)

### Docs PR URL (required if "docs added" is checked)
Paste the PR link from https://github.com/netbirdio/docs here:

<https://github.com/netbirdio/docs/pull/855>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added an administrative policy to disable client autostart (“Disable
Autostart”).
- Added support for configuring this policy via macOS MDM, Windows Group
Policy (ADMX/ADML), and registry settings.
- When enforced, the client prevents new autostart registration on fresh
installs and removes existing autostart on the next GUI launch, keeping
it disabled until the policy is lifted.

- **Bug Fixes**
- Improved enforcement logic for managed autostart defaults so policy
state is applied consistently during startup and first-run setup.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-22 11:34:13 +02:00
Zoltan Papp 9620890b65 [client] Always connect on profile selection except in manage profiles (#6838)
Selecting a profile from the header dropdown or the tray submenu now
always brings the connection up after the switch, regardless of the
previous daemon state. Switching from the manage-profiles screen
(including profile creation) never connects, leaving a chance to adjust
the management URL first.

## Describe your changes

## Issue ticket number and link

## Stack

<!-- branch-stack -->

### Checklist
- [ ] Is it a bug fix
- [ ] Is a typo/documentation fix
- [x] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] This change does **not** modify the public API, gRPC protocols,
functionality behavior, CLI / service flags, or introduce a new feature
— **OR** I have discussed it with the NetBird team beforehand (link the
issue / Slack thread in the description). See
[CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first).

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)

### Docs PR URL (required if "docs added" is checked)
Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/netbirdio/codesmith/netbird/pr/6838"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img
alt="View with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a>
<a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1787157550&installation_model_id=427504&pr_number=6838&repository=netbirdio%2Fnetbird&return_to=https%3A%2F%2Fgithub.com%2Fnetbirdio%2Fnetbird%2Fpull%2F6838&signature=8fe9c5f0779df46c76b4135a373591762eb1498d890ff6996626fb75d6433e0a"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>/codesmith</code> with what you
need. Autofix is disabled.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added the ability to switch profiles without automatically
establishing a connection.
* Existing profile switching continues to connect when appropriate,
while safely handling active or pending connections during the switch.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-21 15:28:11 +02:00
Eduard GertandClaude Opus 4.8 69c35e31b4 [client] Fix browser dialog not closing on renew session flow (#6745)
## Describe your changes

## Issue ticket number and link

## Stack

<!-- branch-stack -->

### Checklist
- [x] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] This change does **not** modify the public API, gRPC protocols,
functionality behavior, CLI / service flags, or introduce a new feature
— **OR** I have discussed it with the NetBird team beforehand (link the
issue / Slack thread in the description). See
[CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first).

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)

### Docs PR URL (required if "docs added" is checked)
Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* SSO browser-login popups now open centered on the display where the
cursor is located, and they recenter correctly on subsequent opens.
* Programmatic cleanup no longer triggers “login canceled” behavior;
cancel is emitted only when the user closes the active popup.
* **New Features**
* Added a streamlined “close renewal flow” action that tears down the
session-renewal UI by closing both the login and session-expiration
popups.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:01:31 +02:00
Zoltan Papp 6fc05efa6c [client] Disconnect daemon on GUI quit via async Down (#6796)
The tray Quit menu now disconnects the daemon before exiting instead of
only tearing down the GUI. A new DownAsync RPC lets the daemon start the
teardown and return immediately: beginDown cancels the connection under
the mutex (so it cannot reconnect), then finishDown (the retry-goroutine
wait and status reset) runs on a background goroutine. handleQuit aborts
any in-flight profile switch first (so a queued Up cannot reconnect
during teardown) and calls DownAsync so quitting never blocks on the
engine shutdown.

## Describe your changes

## Issue ticket number and link

## Stack

<!-- branch-stack -->

### Checklist
- [ ] Is it a bug fix
- [ ] Is a typo/documentation fix
- [x] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] This change does **not** modify the public API, gRPC protocols,
functionality behavior, CLI / service flags, or introduce a new feature
— **OR** I have discussed it with the NetBird team beforehand (link the
issue / Slack thread in the description). See
[CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first).

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)

### Docs PR URL (required if "docs added" is checked)
Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/netbirdio/codesmith/netbird/pr/6796"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img
alt="View with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a>
<a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1786785261&installation_id=146802194&pr_number=6796&repository=netbirdio%2Fnetbird&return_to=https%3A%2F%2Fgithub.com%2Fnetbirdio%2Fnetbird%2Fpull%2F6796&signature=4fc95ebfed320240170c8318a2bc8750acfa5ad131454e3011c43bc8a5a09ed1"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>/codesmith</code> with what you
need. Autofix is disabled.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved shutdown reliability by continuing teardown even when
stopping the service fails (stop errors are logged but not returned).
* Refined connection shutdown to return “service not up” errors directly
for clearer, more immediate RPC behavior.
* Prevented shutdown hangs by making the tray Quit disconnect
time-bounded (5 seconds).
* Ensured any in-flight profile switch is cancelled before exiting, with
quit serialized to avoid races.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-21 13:34:30 +02:00
Eduard GertandClaude Fable 5 3cda14d7f2 [client] Use menu bar wording on macOS welcome screen (#6810)
## Describe your changes

The post-install welcome step said "Look for NetBird in your tray" on
every OS, but macOS has no system tray — the icon sits in the menu bar.
The tray wording stays correct on Windows and Linux.

- Add `welcome.titleMac` and `welcome.descriptionMac` (following the
existing `settings.advanced.interfaceName.errorMac` key convention) to
`en` and all nine translated bundles (`de`, `es`, `fr`, `hu`, `it`,
`ja`, `pt`, `ru`, `zh-CN`), each using that language's Apple term for
the menu bar (Menüleiste, barra de menús, barre des menus, menüsor,
barra dei menu, メニューバー, barra de menus, строка меню, 菜单栏). The `ja`
bundle landed on main (#6790) after the initial commit and was covered
after merging main back in.
- `WelcomeStepTray.tsx` picks the key via `isMacOS()`, which it already
uses to choose the per-OS screenshot.

Verified: `go test ./client/ui/i18n/...`, `tsc --noEmit`, `eslint`, and
`prettier --check` all pass; key set and placement verified identical
across all ten bundles. Not visually verified in the running app
(headless session) — the welcome dialog only shows on first launch.

## Issue ticket number and link

Fixes NET-1411

## Stack

<!-- branch-stack -->

### Checklist
- [x] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [x] This change does **not** modify the public API, gRPC protocols,
functionality behavior, CLI / service flags, or introduce a new feature
— **OR** I have discussed it with the NetBird team beforehand (link the
issue / Slack thread in the description). See
[CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first).

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (UI copy fix only;
no behavior, API, or configuration change)

### Docs PR URL (required if "docs added" is checked)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/netbirdio/codesmith/netbird/pr/6810"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img
alt="View with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a>
<a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1786865844&installation_id=146802194&pr_number=6810&repository=netbirdio%2Fnetbird&return_to=https%3A%2F%2Fgithub.com%2Fnetbirdio%2Fnetbird%2Fpull%2F6810&signature=c18b58a9624d319bff3010aeb92c7b11cc7e99f82ffa5e15a32053500d620af4"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>/codesmith</code> with what you
need. Autofix is disabled.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added macOS-specific onboarding text that directs users to find
NetBird in the menu bar.
* Updated localized welcome content across supported languages, while
retaining platform-specific tray guidance where applicable.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 11:26:27 +02:00
Eduard Gert d9392fdbb8 [client] Clarify outdated NetBird client overlay (#6718)
## Describe your changes
Add more information about the current client and GUI versions if the
user is running an older client.
Update the URL to download the latest RC if the user is running any RC
build.

<img width="348" height="497" alt="CleanShot 2026-07-10 at 15 11 54"
src="https://github.com/user-attachments/assets/2579f5f3-5056-4184-96bb-9f93d5860328"
/>

## Issue ticket number and link

## Stack

<!-- branch-stack -->

### Checklist
- [ ] Is it a bug fix
- [ ] Is a typo/documentation fix
- [x] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] This change does **not** modify the public API, gRPC protocols,
functionality behavior, CLI / service flags, or introduce a new feature
— **OR** I have discussed it with the NetBird team beforehand (link the
issue / Slack thread in the description). See
[CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first).

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)

### Docs PR URL (required if "docs added" is checked)
Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__
2026-07-21 11:26:16 +02:00
a411fd300c [client] Add Japanese (ja) UI translation (#6790)
## Describe your changes
Adds a Japanese (ja) locale for the desktop UI, following the procedure
in `client/ui/i18n/TRANSLATING.md`.

- New `client/ui/i18n/locales/ja/common.json` — all 441 keys from the
`en` source bundle, `message` only, same key order.
- Registered in `client/ui/i18n/locales/_index.json`: `{"code": "ja",
"displayName": "日本語", "englishName": "Japanese"}`.

Translation notes:
- Placeholders (`{version}`, `{count}`, `{name}`, `{remaining}`, ...),
`\n`, trailing `…`/`...`, the leading space in
`notify.update.enforcedSuffix`, and the `<docs>…</docs>` inline-link
tags are all preserved verbatim.
- Brands kept as-is: NetBird, WireGuard® (® preserved), Rosenpass,
GitHub, NetBird Cloud. Acronyms kept: SSO, DNS, IP/IPv6, ACL, SSH, JWT,
TTL, SFTP, MTU, PSK, LAN, P2P, ICE, IdP.
- Formal register (です・ます); short labels for buttons/tray; quoted UI
labels use Japanese brackets 「」.

No code changes are required: the React frontend auto-loads every
`locales/*/common.json` via `import.meta.glob`, the tray/Go side embeds
the tree via `//go:embed all:i18n/locales`, and the language picker
lists whatever `_index.json` declares.

### Verification
- `go test ./client/ui/i18n/` passes.
- JSON valid; key set and order identical to `en`; no `description`
fields; no empty messages; placeholder/`\n` counts match `en`.
- Built the Windows UI and confirmed the General, Network, Security, and
Troubleshooting settings tabs plus the main window render correctly in
Japanese (language picker shows "日本語 (Japanese)"), with no text
truncation, overflow, or leaked placeholders.

## Issue ticket number and link
N/A

## Stack

<!-- branch-stack -->

### Checklist
- [ ] Is it a bug fix
- [ ] Is a typo/documentation fix
- [x] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [x] This change does **not** modify the public API, gRPC protocols,
functionality behavior, CLI / service flags, or introduce a new feature
— adding a shipped UI locale is the documented extension path in
`client/ui/i18n/TRANSLATING.md`.

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change — UI translation
strings only; no user-facing product docs are affected.

### Docs PR URL (required if "docs added" is checked)
N/A

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/netbirdio/codesmith/netbird/pr/6790"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img
alt="View with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a>
<a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1786742881&installation_id=146802194&pr_number=6790&repository=netbirdio%2Fnetbird&return_to=https%3A%2F%2Fgithub.com%2Fnetbirdio%2Fnetbird%2Fpull%2F6790&signature=a1dcd7faf63ae10071ca2da3639b4daf6505eeb0258790e622222e742a8ebcb6"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>/codesmith</code> with what you
need. Autofix is disabled.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Added Japanese as a supported interface language.
* **Localization**
* Provided full Japanese translations for the UI, covering navigation,
settings, onboarding, desktop/tray notifications, connection and status
messaging, troubleshooting, update/about screens, and
authentication/error dialogs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
2026-07-20 14:12:13 +02:00