mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-08 06:29:08 +02:00
c8fe901039aa462b080f1f3526dcb754f60d508f
81
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c8fe901039 |
Merge remote-tracking branch 'origin/main' into fix/pkce-flow-session-extend
# Conflicts: # shared/management/proto/management.pb.go |
||
|
|
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
|
||
|
|
2dd25ec147 |
Merge branch 'main' into fix/pkce-flow-session-extend
# Conflicts: # client/ios/NetBirdSDK/login.go # client/server/server.go # shared/management/proto/management.pb.go |
||
|
|
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> |
||
|
|
e5c0cdf958 | [client] Stay connected with login command (#7384) | ||
|
|
4749005a50 |
[client] Resolve profiles for the sudo invoking user instead of root (#7238)
* [client] Resolve profiles for the sudo invoking user instead of root
The SSH server flags force `netbird up` through sudo, but the CLI resolved
every per-user path with the process user. As root that reads root's own
(empty) local state, so a `sudo netbird up` silently switched the daemon from
the user's profile to the default one — cancelling any login already waiting
in the browser — and then ran an SSO login for the default profile's config.
Whichever account that login returned, the default profile's peer belongs to
someone else, so every attempt ended in "peer is already registered by a
different User or a Setup Key", with nothing telling the user why.
Resolve the acting user through SUDO_USER when running as root: the active
profile, the profile config paths and the stored account email now come from
the invoking user's directories. Privilege decisions are untouched — they stay
on the kernel credentials of the daemon connection, which an environment
variable can never influence; a forged SUDO_USER only selects a profile root
could select anyway.
The invoking user's directories are strictly read-only under sudo. Anything
root wrote there would be root-owned and break the user's own runs, so instead
of chowning files back, the local writes are skipped: the active-profile
bookkeeping and the account-email state simply do not update from a sudo run
(the daemon records the switch on its side; a skipped email write costs at
most one extra account prompt later).
Plain root — no sudo context — has no user to act for, so the ambiguity is
refused instead of guessed at: when the daemon's active profile differs from
what root resolves and no --profile was given, up fails with a message naming
both profiles, instead of silently switching the daemon and failing later with
the ownership error.
* [client] Act on the daemon-resolved profile and fail closed in the root guard
Under sudo the local active-profile mirror is not updated, so up/login
re-reading it after a profile switch acted on the previous profile; use
the daemon-resolved ID directly instead. The plain-root guard now runs
after the readiness wait, denies on lookup errors and empty responses,
and matches the owning username as well; an unowned profile (fresh
install) and a daemon predating the RPC stay allowed. Write-skip
decisions key off the sudo environment alone so a transient user lookup
failure cannot turn a run into writing root-owned files into the user's
directory, and RemoveProfileState honors the read-only rule too.
* [client] Return a wrapped error instead of double-reporting the dial failure
* [client] Read the profile from the daemon when the local mirror is not authoritative
Under sudo without --profile, `up` took the active profile from the invoking
user's local active_profile.txt mirror and drove the daemon to it. But that
mirror is never written under sudo (the SwitchProfile write is a no-op), so it
goes stale after any --profile run and silently switches the daemon back to the
mirror's default. The plain-root guard was meant to refuse exactly this
ambiguity but only ran for plain root, never for the sudo case the fix targets.
When there is no --profile and the mirror is not authoritative (sudo or plain
root), take the profile the daemon already holds for the invoking user instead
of the stale mirror: stay on the user's current profile when the daemon owns it
(or it is unowned, as on a fresh install), and refuse with a --profile hint when
the daemon is on another user's profile. A daemon predating the RPC keeps the
mirror-derived profile.
Reproduce (before this change):
1. As a non-root user misha, with the daemon installed and running:
sudo netbird up --profile work
misha connects on the `work` profile.
2. Because the local mirror write is skipped under sudo,
~misha/.config/netbird/active_profile.txt still says `default` (or is still
absent, which also resolves to `default`).
3. Run a bare:
sudo netbird up
The CLI reads `default` from the frozen mirror and sends ProfileName=default;
the daemon silently switches away from `work` and brings the tunnel up on
`default` — a different account/peer than the one last chosen, with no
warning. After this change step 3 stays on `work`.
* [client] Return a sentinel error instead of nil-nil for the missing daemon RPC
* [client] Load the extend-session hint from the resolved profile
* [client] Fail closed instead of reading root's config when the sudo user lookup fails
* [client] Fail closed in InvokingUser when the sudo user lookup fails
A previous change made baseConfigDir fail closed when SUDO_USER cannot be
resolved, but InvokingUser still fell through to user.Current(). Those two
guards disagreed: the active-profile mirror and the email state refused to
read root's directory, while every profile-path caller happily resolved as
root.
The consequence of a transient NSS failure under sudo was that
Profile.FilePath resolved through getConfigDirForUser("root"), creating
/var/lib/netbird/root and reading the profile JSON from there, and the CLI
sent Username "root" to the daemon in SetConfig and ListProfiles, so the
daemon resolved the same phantom namespace. The invoking user was silently
moved onto a root-owned profile instead of being told the lookup failed.
Fail closed at the single source of the fallback. getConfigDirForUser is
left alone on purpose: it is a pure path helper that also serves
daemon-supplied usernames, and under sudo with a successful lookup it must
still create the invoking user's own profile directory.
|
||
|
|
907e66b9d7 |
[client] Verify the SSO login came back for the hinted account
login_hint is a suggestion the IdP may ignore: with a silent flow configured (DisablePromptLogin or max_age=0) and a live IdP session for another account, the login completes with that account's token. On a registered peer the management server rejects it as a user mismatch, but on a fresh profile the peer silently registers under the wrong account and the profile is then bound to it — every later login follows the stored hint straight back. After the token exchange, compare the ID token's email against the hint the flow was sent with. On a mismatch, do not log in to management with the token; run one more round asking the IdP to re-decide the account (prompt=login, via ForceAccountPrompt — DisablePromptLogin still wins there). If the prompted round also comes back different, proceed with a warning: the address may legitimately have changed, and refusing forever would lock the user out of the profile while the management server still rejects a token that does not own the peer. A token or profile with no email to compare is not judged. The retry differs per platform because of who opens the browser: - CLI (netbird login foreground) and Android run the whole flow in one process, so the mismatch retries automatically: the browser reopens with the account prompt within the same login attempt. - On desktop the login is split between the daemon and the GUI: Login hands the authorize URL to the GUI, WaitSSOLogin blocks for the token, and only the GUI can open a browser. A new URL cannot be handed out from inside WaitSSOLogin (its response has no field for one, kept that way to avoid a proto change), so the daemon arms forceAccountPrompt, fails the round with "connect again to choose the account", and builds the next Login's flow with the prompt — the user's next connect is the retry. The flag and the flow annotations live in daemon memory only; SwitchProfile drops them so the previous profile's hint cannot judge the next profile's token. The device code flow has no prompt parameter (RFC 8628), so a prompted round there runs as-is and a repeated mismatch is let through with the warning rather than looping. |
||
|
|
0738734b6e |
[client] Force interactive login when extending the auth session
A session extend must be answered from the account the peer is registered under. With a silent PKCE flow (DisablePromptLogin or max_age=0) the IdP answers from whatever session it already holds, which need not be the peer's account when several are signed in; the token then fails the user match in ExtendAuthSession with no way to pick another account. Mark the PKCE flow request as a session extend so the management server can force prompt=login for it, overriding the configured silent flow. |
||
|
|
e290769df1 | [client] Take the graphical session answer from the caller instead of the daemon environment (#7187) | ||
|
|
e70a69bbcf |
[client] Restore residual state in foreground mode before login (#6707)
* Improved residual state restoration during foreground startup and foreground login, ensuring consistent recovery with stale states. * Foreground flows now initialize advanced routing so stale routes are bypassed during login. |
||
|
|
91acb8147c |
[management,client] 0.75.0 release with new desktop UI (#6473)
- **Wails v3 application** (`client/ui`) with a React + TypeScript + Tailwind frontend replacing the Fyne UI: main connection view, exit-node switcher, networks/peers browser with detail panels, profile management, settings (general, network, SSH, security, troubleshooting, appearance), debug-bundle creation, and a first-run welcome flow. - **Internationalization**: go-i18n bundle with 9 locales (en, de, es, fr, hu, it, pt, ru, zh-CN) shared between the tray and the frontend. - **New system tray** implementation with per-platform theme-aware icons, including a native XEmbed host for Linux (`xembed_tray_linux.c`) and a Linux theme watcher. - **Session handling**: auth session watcher (`client/internal/auth/sessionwatch`), pending login flow, session-expiration dialog and tray notifications, and `netbird login` improvements. - **Daemon API extensions** (`daemon.proto`): status stream subscription, event stream, networks/exit-node selection endpoints, and richer full status — with probe throttling on the daemon side to protect against UI-driven request storms. - **UI preferences store** persisted per profile, autostart management via the daemon (single source of truth in HKCU on Windows). - **Build system**: Taskfile-based builds per platform (macOS, Linux, Windows), Docker cross-compilation images, MSIX/NSIS/nfpm/AppImage packaging, and a new `frontend-ui` CI workflow. Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com> Co-authored-by: Eduard Gert <kontakt@eduardgert.de> Co-authored-by: braginini <bangvalo@gmail.com> Co-authored-by: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Co-authored-by: riccardom <riccardomanfrin@gmail.com> |
||
|
|
883a1a8961 |
[client] Fix profile regressions in up --profile and status (#6479)
* Restores behavior to create profile if not there on Up * Allows to restore nerbird status showing of the profile name * [client] Reduce upFunc cognitive complexity Extract the profile switch/auto-create logic from upFunc into a dedicated switchOrCreateProfile helper. The inlined NotFound-retry branch pushed upFunc over SonarCloud's cognitive complexity threshold (S3776). No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * [client] Make up --profile auto-create idempotent under concurrent runs Don't fail switchOrCreateProfile on a createProfile error: a concurrent run may create the profile between the NotFound check and our create call. Retry the switch regardless and only surface the create error if the switch also fails. Addresses CodeRabbit race-condition feedback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Share createProfile with addProfileFunc * But allow conn reusage * moves switchOrCreateProfile to where it's used --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ee360963f9 |
[client] Migrate profile identity from display name to ID and allow renaming of profiles (#6367)
* Migrate to profile ids * Migrate android profile manager * Clean up * Fix review * Add ID type * Fix test and runes in ShortID() * Fix profile switch on up and android comments * Revert android profile to string id * Fix feedback * Fix UI feedback * Fix id assignment * Add renaming of profiles * Fix review * Remove ui binary * Fix getProfileConfigPath not validating id * Change resolve handle order and fix server merge problems * Fix mdm test |
||
|
|
104990dfdd | [client] Display QR code for device auth login URL (#5415) | ||
|
|
2de1949018 | [client] Check if login is required on foreground mode (#5295) | ||
|
|
ded04b7627 |
[client] Consolidate authentication logic (#5010)
* Consolidate authentication logic - Moving auth functions from client/internal to client/internal/auth package - Creating unified auth.Auth client with NewAuth() constructor - Replacing direct auth function calls with auth client methods - Refactoring device flow and PKCE flow implementations - Updating iOS/Android/server code to use new auth client API * Refactor PKCE auth and login methods - Remove unnecessary internal package reference in PKCE flow test - Adjust context assignment placement in iOS and Android login methods |
||
|
|
9c9d8e17d7 |
Revert "Revert "[relay] Update GO version and QUIC version (#4736)" (#5055)" (#5071)
This reverts commit
|
||
|
|
24df442198 |
Revert "[relay] Update GO version and QUIC version (#4736)" (#5055)
This reverts commit
|
||
|
|
8722b79799 |
[relay] Update GO version and QUIC version (#4736)
- Go 1.25.5 - QUIC 0.55.0 |
||
|
|
02200d790b | [client] Open browser for ssh automatically (#4838) | ||
|
|
7285fef0f0 |
feat: Add support for displaying device code (UserCode) on Android TV SSO flow (#4800)
- Modified URLOpener interface to pass userCode alongside URL in login.go - added ability to force device auth flow |
||
|
|
75327d9519 | [client] Add login_hint to oidc flows (#4724) | ||
|
|
429d7d6585 | [client] Support BROWSER env for login (#4654) | ||
|
|
ad8fcda67b |
[client] Move some sys info to static place (#4446)
This PR refactors the system information collection code by moving static system information gathering to a dedicated location and separating platform-specific implementations. The primary goal is to improve code organization and maintainability by centralizing static info collection logic. Key changes: - Centralized static info collection into dedicated files with platform-specific implementations - Moved `StaticInfo` struct definition to the main static_info.go file - Added async initialization function `UpdateStaticInfoAsync()` across all platforms |
||
|
|
e97f853909 | Improve wording in the NetBird client app (#4316) | ||
|
|
dfd8bbc015 | Change Netbird to NetBird in CMD (#4296) | ||
|
|
3d9be5098b | [client]: deprecate config flag (#4224) | ||
|
|
cb8b6ca59b |
[client] Feat: Support Multiple Profiles (#3980)
[client] Feat: Support Multiple Profiles (#3980) |
||
|
|
2c4ac33b38 | [client] Remove and deprecate the admin url functionality (#4218) | ||
|
|
af8687579b |
client: container: support CLI with entrypoint addition (#4126)
This will allow running netbird commands (including debugging) against the daemon and provide a flow similar to non-container usages. It will by default both log to file and stderr so it can be handled more uniformly in container-native environments. |
||
|
|
92c91bbdd8 |
[client] Add FreeBSD desktop client support to OAuth flow (#3822)
[client] Add FreeBSD desktop client support to OAuth flow |
||
|
|
4fe4c2054d | [client] Move static check when running on foreground (#3742) | ||
|
|
c0eaea938e |
[client] Fix macos privacy warning when checking static info (#3496)
avoid checking static info with a init call |
||
|
|
86dbb4ee4f |
[client] Add no-browser flag to login and up commands for SSO login control (#3610)
* [client] Add no-browser flag to login and up commands for SSO login control (#3610) |
||
|
|
39986b0e97 |
[client, management] Support DNS Labels for Peer Addressing (#3252)
* [client] Support Extra DNS Labels for Peer Addressing * [management] Support Extra DNS Labels for Peer Addressing --------- Co-authored-by: Viktor Liu <17948409+lixmal@users.noreply.github.com> |
||
|
|
af1b42e538 |
[client] Parse data from setup key (#2411)
refactor functions and variable assignment |
||
|
|
18cef8280a |
[client] Allow setup keys to be provided in a file (#2337)
Adds a flag and a bit of logic to allow a setup key to be passed in using a file. The flag should be exclusive with the standard --setup-key flag. |
||
|
|
69048bfd34 |
Revert "Accept any XDG_ environment variable to determine desktop (#2037)" (#2042)
This reverts commit
|
||
|
|
67e2185964 | Accept any XDG_ environment variable to determine desktop (#2037) | ||
|
|
f099e02b34 |
Fix preshared key not persisted in config (#1474)
* replace the preshared key attribute in LoginRequest protobuff with an optional replacement * mark old field as deprecated * fix ui client to also keep preshared key |
||
|
|
5903715a61 |
Update cloud management URL to https://api.netbird.io:443 (#1402)
With this change we are updating client configuration files to use the new domain |
||
|
|
f73a2e2848 |
Allow removal of preshared keys (#1385)
* update cli commands to respect an empty string and handle different from undefined * remove test for unintended behaviour * remove test for unintended behaviour |
||
|
|
cba3c549e9 |
Add JWT group-based access control for adding new peers (#1383)
* Added function to check user access by JWT groups in the account management mock server and account manager * Refactor auth middleware for group-based JWT access control * Add group-based JWT access control on adding new peer with JWT * Remove mapping error as the token validation error is already present in grpc error codes * use GetAccountFromToken to prevent single mode issues * handle foreground login message --------- Co-authored-by: Maycon Santos <mlsmaycon@gmail.com> |
||
|
|
7a46a63a14 |
Fix the inability to set hostname with the flag in daemon mode (#1339)
Pass the hostname set in the flag into the protocol message when running in daemon mode. |
||
|
|
9e8725618e |
Extend linter rules (#1300)
- dupword checks for duplicate words in the source code - durationcheck checks for two durations multiplied together - forbidigo forbids identifiers - mirror reports wrong mirror patterns of bytes/strings usage - misspell finds commonly misspelled English words in comments - predeclared finds code that shadows one of Go's predeclared identifiers - thelper detects Go test helpers without t.Helper() call and checks the consistency of test helpers |
||
|
|
1956ca169e | Resolve client authentication issue in daemon mode (#1181) | ||
|
|
8febab4076 |
Improve Client Authentication (#1135)
* shutdown the pkce server on user cancellation * Refactor openURL to exclusively manage authentication flow instructions and browser launching * Refactor authentication flow initialization based on client OS The NewOAuthFlow method now first checks the operating system and if it is a non-desktop Linux, it opts for Device Code Flow. PKCEFlow is tried first and if it fails, then it falls back on Device Code Flow. If both unsuccessful, the authentication process halts and error messages have been updated to provide more helpful feedback for troubleshooting authentication errors * Replace log-based Linux desktop check with process check To verify if a Linux OS is running a desktop environment in the Authentication utility, the log-based method that checks the XDG_CURRENT_DESKTOP env has been replaced with a method that checks directly if either X or Wayland display server processes are running. This method is more reliable as it directly checks for the display server process rather than relying on an environment variable that may not be set in all desktop environments. * Refactor PKCE Authorization Flow to improve server handling * refactor check for linux running desktop environment * Improve server shutdown handling and encapsulate handlers with new server multiplexer The changes enhance the way the server shuts down by specifying a context with timeout of 5 seconds, adding a safeguard to ensure the server halts even on potential hanging requests. Also, the server's root handler is now encapsulated within a new ServeMux instance, to support multiple registrations of a path |
||
|
|
bb791d59f3 | update check for linux running desktop (#1137) | ||
|
|
1a9301b684 |
Close PKCE Listening Port After Authorization (#1110)
Addresses the issue of an open listening port persisting after the PKCE authorization flow is completed. |
||
|
|
ac0b7dc8cb |
Enhance linux client authentication (#1093)
The change clarifies the message usage, indicating that setup keys can alternatively be used in the authentication process. This approach adds flexibility in scenarios where automated authentication is unachievable, especially in non-desktop Linux environments. |