Commit Graph
3561 Commits
Author SHA1 Message Date
Viktor Liu 8f158a0827 Reset X11 button state on reconnect, read the Shift+AltGr console table, sync remote Caps Lock with macOS, and restore the FreeBSD stride test 2026-09-23 10:18:13 +02:00
Viktor Liu 395d6521f2 Make the token-leak, VNC port-scoping and bidirectional-SSH tests able to fail 2026-09-23 08:57:38 +02:00
Viktor Liu d6f46b79a3 Count in-flight writes in the final metrics tick and scope the agent token constants to their platforms 2026-09-23 08:54:08 +02:00
Viktor Liu adcd8e3ec5 Report FBU metrics as untracked for service-mode proxied sessions instead of as zero 2026-09-23 08:51:31 +02:00
Viktor Liu e0fe7cccb7 Make the VNC server single-use and recognise crash-leftover desktop processes by their recorded command 2026-09-23 08:36:59 +02:00
Viktor Liu f1a5538840 Map uinput keysyms and typed text through the console's active keymap, and cover the keypad, lock and AltGr keysyms 2026-09-23 08:23:18 +02:00
Viktor Liu 3cc3da41c1 Open the FreeBSD framebuffer read-only and decode depth-24 as 32-bit storage, validate X11 byte order and visual masks, honour destination stride, and drop the cached frame on close 2026-09-23 08:16:15 +02:00
Viktor Liu 47c51fa3c4 Keep Windows input state per session, reject unmappable VkKeyScanA results, queue clipboard and paste reliably, and deselect the GDI bitmap before deleting it 2026-09-23 08:12:31 +02:00
Viktor Liu ff4d6928f7 Fix macOS input permissions, Caps Lock, scroll and layout-independent typing, reconnect the X11 injector, and release VNC resources when start fails 2026-09-23 08:07:11 +02:00
Viktor Liu 9b0a3d3b29 Hand the macOS agent token over stdin, refuse a pre-existing SAS event, bound session writes, and stop the service agent after handlers drain 2026-09-23 07:43:28 +02:00
Viktor Liu 574e68a9e6 Harden the xauth traversal walk against symlinks, keep the runtime dir readable, and enable SeIncreaseQuotaPrivilege for agent spawning 2026-09-22 21:09:53 +02:00
Viktor Liu d0e705bb25 Make the agent token-leak assertion able to fail 2026-09-22 21:06:07 +02:00
Viktor Liu 8f06a43d93 Fix gosec bounds in the byte swizzle, drop the now-unused releaseCapture, and make the SSH and approval-reuse tests actually exercise their paths 2026-09-22 21:03:30 +02:00
Viktor Liu 65bb3ae4f5 Initialize agent capture and input before publishing its socket, and request Screen Recording in direct macOS mode 2026-09-22 20:17:23 +02:00
Viktor Liu 90450bd83b Frame the VNC session metrics explicitly instead of inferring updates from payload bytes 2026-09-22 20:14:53 +02:00
Viktor Liu 88dbac029e Order macOS pointer events by the previous button state, scale the cursor position into framebuffer pixels, and escalate crash cleanup to SIGKILL 2026-09-22 20:10:15 +02:00
Viktor Liu a5c146ccda Restore Windows privileges on shutdown, refuse approval on a locked session, and stop sessions reading a recycled capture buffer 2026-09-22 20:05:54 +02:00
Viktor Liu b703eb4409 Support scroll wheel and the missing modifier keysyms on uinput, and keep X11 capture when XTest is absent 2026-09-22 19:58:14 +02:00
Viktor Liu d1d8c19fc7 Retry a transient cursor-source failure and stop reporting stale framebuffer geometry 2026-09-22 19:56:19 +02:00
Viktor Liu 67c786b4f4 Report the MDM VNC keys to the UI and bound the copyrect tile-hash map 2026-09-22 19:53:43 +02:00
Viktor Liu 24f832e032 Fix big-endian pixel swizzle, closed-capturer panic on FreeBSD, and the macOS agent socket dir symlink race 2026-09-22 19:50:21 +02:00
Viktor Liu 2f03ea4051 Copy authorization slices on update, drain queued input on close, and drop input when the desktop switch fails 2026-09-22 19:46:36 +02:00
Viktor Liu 921aa2b543 Refuse ambiguous X11 display selection, harden the xauth temp file, and reset the service-agent latch on restart 2026-09-22 19:34:11 +02:00
Viktor Liu 5cf52e7e12 Fix uk locale key parity, capture claim race and approval window reuse 2026-09-22 19:21:28 +02:00
Viktor Liu 31a27e575f Default the VNC approval prompt to deny and arm the accept actions 2026-09-22 14:53:41 +02:00
Viktor Liu c66714ce42 Evict a stalled packet capture without holding the daemon mutex 2026-09-22 14:53:41 +02:00
Viktor Liu 40424aa986 Drop network map rules with an unrecognized protocol instead of decoding them as all 2026-09-22 14:53:41 +02:00
Viktor Liu 750d093300 Restrict rule authorization to accept rules, the destination side for SSH, and the marker protocol's own port 2026-09-22 14:53:37 +02:00
Viktor Liu 6c15fa1d84 Merge branch 'main' into embedded-vnc
# Conflicts:
#	client/ui/frontend/src/app.tsx
#	client/ui/frontend/src/modules/main/MainConnectionStatusSwitch.tsx
#	client/ui/i18n/locales/uk/common.json
#	go.sum
2026-09-22 14:53:34 +02:00
Pascal Fischer ee2344502e [management] fix group resource validation (#7608) 2026-09-21 17:26:52 +02:00
Zoltan Papp bc0671fd21 [client] Fix peers not being notified when the relay connection drops (#7490)
* [relay] Signal relay disconnects through the conn context

AddCloseListener deduplicated listeners by comparing
reflect.ValueOf(callback).Pointer(). For a method value that pointer is
the address of the compiler-generated wrapper, not an identity bound to
the receiver, so every peer's w.onRelayClientDisconnected compared equal.

All peers on the home relay register under the same connectionURL key, so
only the first registration survived and the rest were silently dropped.
On a relay disconnect those peers were never notified: statusRelay stayed
connected and the reconnect guard never fired. The relayed net.Conn itself
was closed by closeAllConns, so nothing leaked, but the peer state machine
did not learn about it. Foreign relays had the same defect scoped to the
peers sharing that server.

Rather than fixing the deduplication, drop the peer-level listener registry
entirely. A relayed Conn now exposes Context(), cancelled when the
connection is torn down, with a cancellation cause naming the reason. This
is the same shape quic-go uses for its Conn and Stream types, and it
removes the whole class of problems around listener identity, lifetime and
deregistration: the signal belongs to the resource instead of a side table.

WorkerRelay watches that context in a goroutine whose lifetime matches the
connection. A watcher that wakes up for a superseded connection compares
the conn pointer against the current one and returns without touching the
state machine, so a fast relay reconnect cannot have a stale watcher tear
down the connection that replaced it.

Client.SetOnDisconnectListener stays: it is server-level and drives the
reconnect guard and foreign relay eviction, unrelated to peers.

handleRelayReady also checks the conn context, closing the race where the
relay dies between OpenConn and the readiness handoff and the peer would
otherwise build a WireGuard endpoint over a dead connection.

TestNotifierDoubleAdd covered the removed mechanism and is gone.
TestForeignAutoClose asserted nothing (both branches logged); it now waits
for the relay to leave the client map and fails if it does not.

* [relay] Fix build: return the concrete conn from Client.OpenConn

OpenConn now returns *Conn, but it still went through connContainer.netConn(),
which widens to net.Conn. The helper had one caller and only existed to produce
the interface value the signature no longer wants, so return container.conn
directly and drop it.

* [relay] Assert the local-close cancellation cause explicitly

The local-close test only rejected ErrServerDisconnected, so it would also
have passed for ErrPeerDisconnected or a bare context.Canceled. closeConn
cancels with net.ErrClosed, so assert that.

* [client] Ignore relay disconnects from superseded connections

The relayed conn watcher compared the conn pointer under relayLock, released
it, and only then tore the connection down. A new offer could install its
replacement in that window, so a watcher that validated the old pointer went
on to close the proxy of the connection that had already replaced it and
report the peer as disconnected while it was up.

Move the decision to where the teardown happens. Conn records which relayed
connection the current proxy was built from, and onRelayDisconnected takes the
connection the signal belongs to and drops it under conn.mu when it is no
longer the current one. Check and effect are now in the same critical section,
so the verdict cannot go stale before it is acted on.

This also covers the proxy read loops, whose disconnect listener took no
argument and had the same defect: it now names the connection it belongs to.
The WG timeout path keeps passing nil, since it deliberately tears down
whatever is current.

* [client] Bind the relayed conn reference to the proxy swap

relayedConnRef was set at the top of the readiness path, but wgProxyRelay only
changes at the end, in setRelayedProxy. The two failure returns in between —
newProxy and ConfigureWGEndpoint — left the reference pointing at a connection
that never became active while the old proxy was still installed. A disconnect
of that old, live relay would then be dismissed as belonging to a superseded
connection and never cleaned up.

Set the reference in setRelayedProxy, next to the proxy it belongs to. Both
success paths go through it and neither failure path does, so no failure branch
has to remember to roll anything back.
2026-09-21 17:00:37 +02:00
Pascal Fischer 771d81b72a [management] Add proxy credentials limiter on management (#7569) 2026-09-21 15:55:56 +02:00
Pascal Fischer 6c6298f2ab [proxy] add proxy rate limiter (#7568) 2026-09-21 15:55:20 +02:00
Eduard Gert 314d88252d [management] Name the account owner in the pending approval error (#7533)
* Name the account owner in the pending approval error

A user refused because their account is pending approval had no way to
learn who could approve them. The refusal now carries the account
owner's address, masked, so a caller can name someone to contact without
being handed the address itself.

Resolving the owner is best effort: a lookup failure, or an account
predating the stored email, falls back to the refusal as it was.

* Name only the caller's own owner in the pending approval error

The refusal is raised before ValidateAccountAccess has established that
the caller belongs to the account the request asked about, and the user
is loaded by ID alone. Resolving the owner of the requested account
therefore disclosed that owner's address to a pending user with no claim
to it, reachable through any handler that takes an account ID from the
caller — DELETE /api/accounts/{accountId} passes one straight through.

The owner who can approve a pending user is the owner of their own
account, so resolve that one. The requested account is never read.

* Mask short local parts whole in MaskedEmail

Keeping the first two characters and the last hides nothing until the
local part is four long: at three or fewer they are the whole of it, so
"abc@example.com" masked to "ab****c@example.com" and a pending user
could recover the owner's address in full from what is meant to conceal
it. Short local parts are now replaced entirely.

* Name the owner from GetCurrentUserInfo instead of the permission gate

The gate could only read the stored user row, which carries no address
when an external IdP owns the identities — the usual case — so it named
no one in practice. It also had no way to reach the IdP without being
handed the account manager, which meant restoring bootstrap wiring that
a refactor had dropped.

GetCurrentUserInfo already holds that account manager, so it answers for
a pending user itself and reuses GetOwnerInfo, the same lookup /msp uses
to resolve an owner's address. The gate returns to exactly what it was,
and with it goes the risk of naming the owner of an account the caller
only asked about.

MaskedEmail becomes MaskEmail: with a UserInfo in hand there is no stored
row to hang it off.

* [management] Cover the pending approval refusal in GetCurrentUserInfo

The branch that names the owner had no coverage at the manager level, so
neither the named refusal nor the fallback for an owner without a resolvable
address was pinned down.

* [management] Cover the failed owner lookup in the pending approval refusal

The generic fallback has two ways in: no address on the resolved owner, and no
owner to resolve at all. Only the first was pinned down.

* [management] Pin the owner lookup to the caller's own account

A mismatched account claim must not steer which owner the refusal names, and
a blocked user is still answered before the claim is validated. Both are load
bearing and neither was covered.
2026-09-21 10:34:04 +02:00
Maycon Santos 3073d18039 [proxy] Close the client connection on private service denials (#7590)
A client that hits a private service before its peer joins the overlay gets a 403 from the tunnel-peer check. After it connects to NetBird, the browser reuses the warm socket to the public listener, so the request never traverses the tunnel and keeps failing until the 120s idle timeout closes it.

Private service denials now set Connection: close and Cache-Control: no-store before the 403, both at the tunnel-peer check and at IP restriction denials on a private domain. Go's HTTP/1.1 server closes after the response; its HTTP/2 server turns the exact lowercase close token into a GOAWAY, which retires the stale connection for h2 clients. Public services and allowed private traffic keep their keep-alive behaviour.

Tests cover HTTP/1.1 and HTTP/2 denials over a real listener (retry lands on a new connection), public denials and allowed private requests (connection reused), and both IP restriction paths.
2026-09-20 20:24:01 +02:00
Bethuel Mmbaga 7d8f4fa31c [management] Handle empty trusted peer (#7589) 2026-09-18 18:21:34 +03:00
Bethuel Mmbaga f8c3e565f3 [management] Read X-Real-IP when extracting the peer connection IP (#7561) 2026-09-17 12:45:03 +03:00
Misha Bragin 85a3913331 [client] Fix - Add RPM metadata required for Red Hat software certification (#7562)
Declare the runtime dependencies, generate the changelog from git tags with
chglog at release time, and ship LICENSE, README.md and an example
/etc/sysconfig/netbird as %license, %doc and %config(noreplace). The unit
generated by "netbird service install" already reads that path via
EnvironmentFile, so post_install.sh is unchanged.
2026-09-17 09:24:55 +02:00
Maycon Santos eab510178a [misc] Load AGENTS.md every session and refuse attribution trailers (#7544)
AGENTS.md forbids attribution trailers, but a rule an agent has to go and read loses to the instruction it is handed every turn. CLAUDE.md now imports AGENTS.md so it is always in context; a commit-msg hook (via make setup-hooks) refuses the trailers at commit time; a CodeRabbit pre-merge check flags a PR whose description or commits carry them. The check reports rather than blocks, since the repository keeps CodeRabbit's request-changes workflow off; turning that on is a separate, repository-wide decision.
2026-09-15 22:48:49 +02:00
Riccardo Manfrin 08699a9e29 [client] Enforce HTTPS on install script downloads (#7545)
Every curl invocation in the install script that follows redirects now passes
`--proto` and `--proto-redir` set to https only, so neither the initial request
nor any hop in the redirect chain can drop to plaintext. This matters most for
the macOS .pkg download, whose URL is itself the result of a redirect
resolution, and for the release tarballs that get moved into the install dir as
root.

The protocol set lives in a single `PROTO_HTTPS` variable rather than being
repeated at each call site, and every expansion is quoted — the variable holds
one option value, not a list of flags.

The two call sites without `-L` (the release metadata lookups) are left alone:
they do not follow redirects and their URLs are https literals.

Verified against every URL the script fetches on curl 7.29.0 (CentOS 7),
7.68.0, 7.76.1, 7.88.1 and 8.14.1; both options have existed since curl 7.20.0.
Plaintext http:// is refused on all of them.
2026-09-15 14:20:15 +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
Riccardo Manfrin 58b5263c1a [client] Stage install script downloads in a private temp directory (#7534)
The install script downloaded both the macOS .pkg and the release tarballs
into /tmp under fixed, predictable names, then passed those same paths to the
privileged install steps (`installer -pkg`, `mv` into the install dir).

/tmp is shared, so those fixed names can collide with entries created there
beforehand, and the privileged steps consume whatever the path resolves to.

Stage every download in a directory from `mktemp -d` instead: unpredictable
name, mode 0700, owned by the caller, created atomically. Extraction now
targets that directory (`tar -C`, `unzip -d`) rather than relying on `cd /tmp`,
and an EXIT trap removes it, so a failed run no longer leaves the archive and
the unpacked LICENSE/README behind in /tmp either.
2026-09-15 10:30:33 +02:00
Maycon Santos f29249e7ef [management] Point the agent-config e2e providers at the mock upstream (#7542)
TestAgentConfigAllowlistOfDeclaredModels still pointed its providers at api.openai.com and bedrock-runtime with a dummy key, so every save has been refused with "the provider rejected the credential" and the Agent Network E2E has been red on main since — both subtests, every scheduled run.

Every other suite already uses the mock vLLM upstream (harness.StartVLLM), which answers both the OpenAI (/v1/models) and the Bedrock (/inference-profiles) listing; this test does the same. What it checks — the allowlist advertising the provider's declared ids on GET /api/agent-network/agent-config — never depended on the vendor.
2026-09-15 09:40:52 +02:00
Maycon Santos 7ce6a63dcb [management] Validate the proxy cluster an agent network bootstraps onto (#7402)
The agent network gateway service is private: agents reach it over the WireGuard tunnel, authorised by peer identity, with the cluster as its only target. Only a reverse proxy cluster with private capabilities can serve that, reported per cluster as the `private` capability — the same `supports_private` flag the dashboard gates NetBird-only services on.

A bootstrap could pin an account to a cluster without private capabilities, leaving an immutable dead gateway. Both bootstrap paths now validate the picked cluster: one the account can see must have a connected proxy reporting the capability. Shared and account-owned clusters qualify alike. Known-ness comes from proxy rows, not heartbeat freshness, so a cluster without the capability stays refused while merely offline. A hostname no proxy has declared stays pinnable (address-first). Identity is compared case-insensitively over the account's cluster list.
2026-09-15 08:42:21 +02:00
Maycon Santos ea294e1d46 [management] Refuse to pin an agent network gateway onto another account's host (#7519)
An agent network bootstrap stores its cluster as proxy_address, which selects the proxy that serves the endpoint. An account-scoped proxy only receives its own account's mappings, so a pin onto a host another account's proxy declares can never be served, and the endpoint is immutable — a dead gateway until the account deletes its settings. Nothing refused that pin; the domain unique index only arbitrates between endpoints.

Both bootstrap paths now refuse, before the insert, a host that another account's proxy declares, a host another account has labeled pins beneath (self-addressed path), or a hostname that is another account's endpoint (labeled path).

Shared clusters are unaffected: shared proxies are never foreign, and labeled pins under one cluster are never asked about, so any number of accounts still pin beneath eu.proxy.netbird.io. Registration is deliberately unchanged — refusing a proxy for another account's pin would let a pin lock a tenant out after the reaper drops its rows.
2026-09-14 22:20:25 +02:00
Maycon Santos ea216f8e73 [management] Speed up test store setup and summarize the unit test run (#7518)
Management / Unit (amd64, mysql) hit the 20 minute go test budget on #7516. The package was not hung: each of the 133 test store creations in management/server paid about 1.6s on MySQL for CREATE DATABASE, the pre-migrations, a 40-table AutoMigrate and the post-migrations, which puts the package at 10 minutes on a healthy runner and over the budget on a slow one.

The migration now runs once per test binary into a template database and each test database is cloned from it, with CREATE DATABASE ... TEMPLATE on Postgres and a replay of SHOW CREATE TABLE on MySQL. The MySQL test container also drops the binary log, doublewrite buffer and per-commit redo fsync. Two goroutine leaks in the test helpers are fixed.

tools/gotestsummary turns the go test -json stream into a readable log, and the Management unit and integration jobs now pipe through it, so a timeout names the tests still running. On MySQL, management/server went from 10m16s to 6m36s.
2026-09-14 19:42:02 +02:00
Viktor Liu a54d96cd72 [proxy] Make the upstream HTTP version configurable (#7410) v0.79.0-rc.1 2026-09-14 17:28:01 +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
Zoltan Papp 0f797f89c1 [client] Bump wireguard-go to 8bf8fa968f1a (#7532)
- Make netTun.Close idempotent (#19)
- Fix keepalive pool block (#20)
- Keep timer paths non-blocking and bound staged packets per peer, kernel style (#21)

https://github.com/netbirdio/wireguard-go/pull/19
https://github.com/netbirdio/wireguard-go/pull/20
https://github.com/netbirdio/wireguard-go/pull/21
2026-09-14 15:19:09 +02:00
Mohd Quamar Tyagi 791401060d [management] Prevent deleting groups referenced by agent network budget rules (#7450)
`validateDeleteGroup` already refuses to delete a group that is still used by routes, policies, nameservers, setup keys, users, network routers, reverse proxy services, and agent network policies. Account-level agent network budget rules also store group IDs in `TargetGroups`, but that check was missing.

Deleting such a group left a dangling ID on the budget rule. `budgetRuleApplies` then never matched callers by group, so the spend cap silently stopped applying.

This adds `isGroupLinkedToAgentNetworkBudgetRule` and uses it in `validateDeleteGroup`, matching the existing helpers.
2026-09-12 12:37:23 +02:00