Commit Graph
1476 Commits
Author SHA1 Message Date
Theodor S. Midtlien baa4f0537c Fix stale server config after stamp 2026-09-16 16:00:25 +02:00
Theodor S. Midtlien 716cb79f8f Use restricted write for config json 2026-09-16 16:00:25 +02:00
Theodor S. Midtlien 2748291525 Only default profile fail open 2026-09-16 16:00:25 +02:00
Theodor S. Midtlien db459b38ec Add one-shot migration 2026-09-16 16:00:25 +02:00
Theodor S. Midtlien fe97bd5b4e Add profile stamping as active migration 2026-09-16 16:00:25 +02:00
Theodor S. Midtlien 1ac303c77e Fix status and list profiles 2026-09-16 16:00:25 +02:00
Theodor S. Midtlien c7451e39ad (WIP) Migrate active_profile 2026-09-16 16:00:25 +02:00
Theodor S. Midtlien a56d40be3c (WIP) List profiles based on ownership by Identity 2026-09-16 16:00:25 +02:00
Theodor S. Midtlien 80b919e854 Implement OwnsProfile on Server 2026-09-16 16:00:25 +02:00
Viktor Liu 15ed6f8f15 Add an optional --allow-group flag restricting the daemon sockets (#7478) 2026-09-16 13:30:10 +02:00
Theodor Midtlienandcubic-dev-ai[bot] abb94ad2d2 Update client/internal/debug/debug.go
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-09-16 10:37:59 +02:00
Theodor S. Midtlien 258a20c433 Fix empty active state read 2026-09-16 10:37:59 +02:00
Theodor Midtlienandcubic-dev-ai[bot] fa3689c079 Update client/internal/debug/debug.go
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-09-16 10:37:59 +02:00
Theodor S. Midtlien 331c490326 Fix typo 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien 2d5ee900d0 Replace RequireFlowInitiator with AuthzLevel and clear flow on switch profile 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien 7705645492 Remove double error log 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien 1538d5f11c Add profile owners to debug bundle 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien 5f7d869426 Fix status and list profiles 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien 45e8bd8af4 Let unimplemented RequireFlowInitiator pass 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien b620173f51 Clarify the Rule comment 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien a585626950 Remove Authorization context and fix bugs in rule and targetProfile 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien fc93bfc561 Add method interceptor tests to ensure that authz is configured for every RPC 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien bbf28a40ed Add authorization framework for gRPC methods 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien cacdb50c0e Add known mark to identity, fix credentials comparison bugs 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien 15003fc495 Add owners to list profile ipc 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien a9fb48d80b Let active profile owner be the source of truth for the session holder 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien 3a2f6df038 Fix debug test 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien 04c00a9a19 Improve owner config parsing 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien abc77e6d92 Use list of owners 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien 3ee80d99bf Fix wasm build 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien 723deda478 Add nolint for unused functions 2026-09-16 10:37:59 +02:00
Theodor S. Midtlien 45b30ef83d Fix ServiceManager.AddProfile signature 2026-09-16 10:37:58 +02:00
Theodor S. Midtlien 77cfc4ac4c Refactor gatePolicy to gateRule 2026-09-16 10:37:32 +02:00
Theodor S. Midtlien 126f3a640b Use sessionholder pointer 2026-09-16 10:37:32 +02:00
Theodor S. Midtlien 6a983eee34 Add WIP profile ownership 2026-09-16 10:37:31 +02:00
Theodor S. Midtlien f4f47db14c WIP: add session ownership 2026-09-16 10:36:43 +02:00
Zoltan Papp e70ec07320 Read the session deadline under the status read lock (#7550)
GetSessionExpiresAt took the exclusive lock for a plain field read, so
every caller queued behind writers and behind each other. The Android
SessionMonitor polls it from the main thread, and in the captured ANR
that is exactly where the main thread was blocked while hundreds of
peer-list callbacks held or waited on the same mutex.

d.mux is already an RWMutex and the other getters use RLock; this brings
the deadline read in line with them.
2026-09-15 12:05:57 +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
Zoltan Papp 794956a7a3 [client] Fix relay instance address race (#7498)
Read the relay instance URL and IP atomically to prevent reconnects from mixing values from different connections. Extend existing connection and offer/answer logs with relay URLs and IPs to help trace mismatched advertisements.
2026-09-11 16:21:10 +02:00
Riccardo Manfrin a419e770d9 [client, proxy] Make the buffer-pool retune reachable while a device is stalled (#7452)
* [client] Track the WireGuard device on the engine as a lock-free handle

Add an atomic handle on the wg device next to wgInterface, stored once the
interface is up and cleared when it is closed. Nothing reads it yet, so this
is a pure addition with no behavior change; it exists so the next commit can
reach the device without taking syncMsgMux.

* [client] Retune the WireGuard buffer pool without the engine lock

SetPerformance took syncMsgMux before reaching the device. That lock is held
by handleSync while it adds and removes peers, and peer removal is exactly
what blocks when a device's buffer pool is exhausted: Peer.Stop waits on a
keepalive timer callback that is itself parked in WaitPool.Get. Raising the
cap is the way out of that state, so the call must not queue behind the lock
the stall is holding.

Read the device through the atomic handle instead. Device.SetPreallocatedBuffersPerPool
takes the pool's own lock and broadcasts, so the waiters wake up.

* [proxy] Extract the buffer-cap apply loop out of the perf handler

Pure move: the loop over the registered clients becomes applyBufferCap, with
the same sequential behavior and the same return values. Split out so the next
commit can change how it iterates without the diff also carrying the move.

* [proxy] Bound the perf endpoint so one wedged client cannot hold it

The apply loop was sequential and unbounded. embed.Client.SetPerformance goes
through the client lock, which Start holds for the whole of a startup, so a
single account that is busy or wedged delayed the new buffer cap for every
other account on the node -- on the endpoint whose whole purpose is to
un-wedge a node.

Apply to all clients concurrently and give the whole call a 5s budget.
Accounts that do not answer in time are reported in "failed" instead of
blocking the response.

* [client] Drop the device handle before closing the interface

close() cleared the atomic handle only after wgInterface.Close() returned, so a
concurrent SetPerformance could still load it, retune a device that is being
torn down, and report the change as applied for an engine that has stopped.
Clear it first, so the window closes before the teardown begins.

Reported by cubic on PR #7452.

* [proxy] Put the per-client retune behind a field

Pure refactor: applyBufferCap calls h.setPerformance instead of the client
method directly, and NewHandler wires it to setClientPerformance. Same call,
same behavior; the seam is what lets the next two commits be tested without a
live embedded client.

* [proxy] Do not report a finished retune as timed out

When the deadline fires, select chooses at random among the ready cases, so a
result already sitting in the buffered channel could be skipped and its account
reported as timed out even though the cap had been applied. Drain what is
buffered before declaring the rest pending.

Reported by cubic on PR #7452.

* [proxy] Keep one retune per account in flight

The 5s budget bounds how long the endpoint waits, not the work: SetPerformance
goes through the embedded client's lock, and on a wedged account Stop holds that
lock forever, so every retry left one more goroutine parked there.

Route each account through a single worker. A request that finds one already
running takes its result if it has landed, and otherwise reports the account
under "in_flight" instead of starting a second attempt. One stuck account now
costs one goroutine, no matter how often the endpoint is called.

Reported by CodeRabbit and cubic on PR #7452.

* [proxy] Make the retune budget a var

Pure refactor: perfApplyTimeout becomes a var so a test can shorten it instead
of waiting five seconds. Same value, same behavior in production.

* [proxy] Extract the buffered-result drain

Pure refactor: the loop that empties the results channel when the deadline
fires becomes collectBuffered. Same behavior; split out so it can be tested
on its own, which the inline version could not be without racing the deadline.

* [proxy] Cover the retune single-flight and the deadline drain

TestApplyBufferCapSingleFlightPerAccount fails without the worker registry:
five calls against a client stuck in its own lock start five blocked workers
instead of one.

TestCollectBufferedCountsResultsReadyAtTheDeadline pins the drain helper's
contract - buffered results counted, errors recorded, only unanswered accounts
left pending. It drives collectBuffered directly: through applyBufferCap the
two select cases race by construction, so an end-to-end version of it would
pass on the unfixed code about half the time.

* [proxy] Keep the worker alongside each pending account

Pure refactor: the pending set becomes a map to the account's worker instead of
an empty struct. Same membership and same behavior; the next commit needs the
worker to resolve an account whose result has not reached the channel yet.

* [proxy] Publish a retune result before releasing its slot

The worker sent its result last, after taking perfMu to remove itself from the
registry. That lock is taken once per account by every caller walking the fleet,
so a worker that finished on time could queue behind an apply over thousands of
accounts and land after the deadline. Send first, deregister after.

Reported by cubic on PR #7452.

* [proxy] Read the worker, not the clock, for a finished retune

Publishing earlier only narrows the window: a client that answers just before
the deadline can still be reported as timed out. At the deadline the workers
themselves are authoritative - a closed done channel means the retune finished
and w.err carries its outcome, ordered by the close. Consult them instead of
declaring every pending account timed out, and keep the timeout label for the
ones actually still running.

Reported by cubic on PR #7452.

* [proxy] Cover the finished-worker resolution at the deadline

Fails on the previous behavior with "applied = 0, want 1": every pending
account was labelled a timeout, including the one whose retune had already
completed.
2026-09-11 09:38:22 +02:00
Nicolas Frati 2f48dbea6a [client] Add a release-wired rootless UBI image variant (#7469)
* [client] Add a release-wired rootless UBI image variant

* [client] Add ARM64 to the rootless UBI image

* [client] Express license output validation as a guard
2026-09-10 21:41:59 +02:00
Zoltan PappandClaude Opus 5 9615d2ab16 [client] Report the remote jobs key in the MDM UI snapshot (#7485)
* [client] Report the remote jobs key in the MDM UI snapshot

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

* [client] Align the remote jobs snapshot key with the policy key

The snapshot field carried the JSON tag remoteJobsAllowed while the policy
key is allowRemoteJobs. GetConfigResponse.mDMManagedFields reports the raw
policy keys, and applyMDMRestrictions matches them against the struct's JSON
tags, so the field never turned true for a policy that set the key.

Every other field in Fields already uses its policy key as the JSON tag; this
was the only divergence.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 12:06:30 +02:00
Nicolas Frati 15a684248c [client] Support arbitrary UIDs in rootless image (#7440)
* [client] Support arbitrary UIDs in rootless image

* [client] Keep rootless executables root-owned

* [client] Harden arbitrary UID image validation

* [client] Preserve executable access in rootless image

Keep the binary and entrypoint executable when deployments override the runtime group. Retain root ownership so non-root users cannot modify either file.

* [client] Verify rootless state reuse with a stable UID

Persisted profiles remain scoped to the creating UID. Verify same-UID container recreation without broadening application permissions, and document the Kubernetes volume permission behavior observed on OpenShift. Remove unused synthetic-user home metadata.

* [client] Separate image changes from invoking user fix

Keep this PR limited to resolving unmapped non-root invoking users. Move container permissions and their smoke test to a dependent image branch so they can be reviewed separately.

* [client] Restore invoking process user test

Retain coverage for successful current-user lookup without sudo. Numeric-identity fallback tests do not cover this existing behavior.
2026-09-10 12:02:19 +02:00
Viktor Liu d101f6cc46 [client] Redirect DNS port 53 with UDP and TCP DNAT instead of the eBPF forwarder (#7439) 2026-09-09 11:32:11 +02:00
Riccardo Manfrin d2e62e358a [client] Compare MDM-managed URLs as endpoints, not as strings (#7472)
A policy that enforces a management URL refuses any SetConfig or Login whose
URL differs from it. The comparison normalized only the default port, so
three ways of writing the very endpoint the policy names were reported as
conflicts:

  policy https://mgmt.example.com  vs  https://mgmt.example.com/     refused
                                       https://MGMT.example.com      refused
                                       https://mgmt.example.com:0443 refused

For an MDM-managed deployment whose stored or command-line URL is spelled
differently from the policy's value, that means every settings update is
refused with an MDMManagedFieldsViolation naming a field the caller did not
change. `netbird up --management-url https://MGMT.example.com` reproduces it.

The rules now live in util.SameServiceURL, and ConflictURL delegates: scheme
and host compared case-insensitively, the effective port normalized
numerically, a trailing slash ignored, and a path otherwise still part of the
identity so /other remains a divergence. Unparseable input falls back to
string equality.

util rather than either caller, because comparing two service URLs is
neither device management nor profile storage, and more than one place does
it: an MDM-enforced management URL against a requested one here, a stored
profile URL against a command-line one in profilemanager and the SSH gate.
Every copy of these rules that drifts turns an equivalent URL into a refused
request, which is how this one arose.

CanonicalURL is left alone: besides comparison it is the canonical value
handed to mdm.Restrictions and to the Android and iOS Preferences getters,
and normalizing what those return is a separate decision.
2026-09-08 16:32:16 +02:00
Riccardo Manfrin bb4de1d008 [client] Read MDM boolean keys delivered as JSON numbers (#7471)
encoding/json decodes every JSON number into float64, so the policy
values the mobile loaders produce never contain int or int64. GetBool
accepted both of those but not float64, so a managed boolean pushed as
1 or 0 — how some MDM consoles normalise flags — was reported as
unreadable while the key still counted as managed: the policy was not
applied, and the conflict gate rejected both values the user could pick
for that field.

The rejected-float assertion predates the JSON channel. It came with the
registry and plist loaders, where a real number for a flag is a
configuration mistake; on the JSON channel an integer is the only shape
a number can take. GetInt already accepts float64.
2026-09-08 15:06:46 +02: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
Zoltan Papp 15c0a2903d [client] Return the context error when the SSH handshake fails with it (#7426)
* [client] Return the context error when the SSH handshake fails on a context deadline

The handshake mapped the context deadline onto the socket but returned the
raw socket error. Which error surfaces depends on a race between the x/crypto
ssh readLoop and kexLoop goroutines: the kexLoop write fails with i/o timeout
and closes the conn, and the readLoop then reports use of closed network
connection. Callers checking errors.Is(err, context.DeadlineExceeded) never
matched, and TestSSHClient_ContextCancellation flaked on the FreeBSD job.

Handshake now wraps the context error when the context is done or its
deadline has passed. The deadline comparison is needed because the socket
deadline and the context timer fire independently, so ctx.Err() can still be
nil when the deadline-triggered socket error arrives.

* [client] Close the silent test server conn without racing t.Cleanup

The accept goroutine registered the conn close via t.Cleanup, which can run
after the test's cleanup list has already been drained, leaving the accepted
connection open. The goroutine now holds the conn until a cleanup-closed
channel signals the end of the test and closes it on the way out.

* [client] Bind the SSH handshake to the context instead of a socket deadline

Mapping only the context deadline onto the socket left context cancellation
unobserved: an in-flight handshake kept running until the deadline, and the
error classification had to guess whether a raw socket error was caused by
the deadline. Closing the conn from context.AfterFunc covers both deadline
and cancellation, and ctx.Err() is already set by the time the close-induced
error surfaces, so the time-based DeadlineExceeded attribution is no longer
needed. The stop() result guards the window between a successful handshake
and the AfterFunc firing so a closed conn is never handed back as a client.
2026-09-07 15:50:28 +02:00
Brad Ison 76ea72237f [management] Add Agent Network managed proxy to the API spec (#7433)
Defines the cloud-side managed gateway provisioning surface
(POST/GET /api/integrations/agent-network/managed-proxy) and its
response objects so clients consume generated types instead of
hand-written ones. POST is idempotent: 202 when the call starts (or
restarts) provisioning, 200 when a deployment already exists; 409
names an already-assigned endpoint the managed flow does not own and
503 signals temporarily exhausted endpoint allocation.
2026-09-04 17:11:27 +02:00