Commit Graph
3635 Commits
Author SHA1 Message Date
riccardom 2069329dd4 [client] pqkem: check the glare resend carries the in-flight offer
The handshaker test only saw that an offer went out, so a fresh or empty
KEM offer would have passed. The manager test pins the part the resend
relies on: the same bytes while in flight, and a duplicate answered from
the responder's cache.
2026-10-22 13:56:23 +02:00
riccardom 442b3b44b1 [client] pqkem: take the netstack type from wgbackend
Main now imports the wireguard-go fork directly instead of through a
replace directive, so the upstream netstack path resolves to the original
module, which no longer builds against the bumped gvisor.
2026-10-22 13:55:38 +02:00
riccardom c7971e8d98 Merge remote-tracking branch 'origin/main' into feat-post_quantum_ml_kem
# Conflicts:
#	.github/workflows/wasm-build-validation.yml
2026-10-22 13:53:54 +02:00
riccardom 3a6e634130 [client] pqkem: resend the in-flight KEM offer during an offer glare
The offer exchange is installed before the Signal send, so a failed send
leaves it in flight with nothing delivered. Ignoring the responder's offer
then stalled the connection until the exchange timed out. Resending the
same offer delivers it; the responder answers a duplicate from its cache.
2026-10-22 13:26:21 +02:00
riccardom 95a976db88 [client] pqkem: leave a responder offer unanswered during an offer glare
When the controller's KEM offer and the responder's offer cross, the
controller used to answer the responder's offer without KEM. If the
responder read that answer before the KEM offer, it could bring WireGuard
up on no PSK and open a classical session. The responder's details still
reach the controller in its answer to the KEM offer.
2026-10-22 12:58:05 +02:00
riccardom 70e6ce1095 [client] pqkem: reprogram the latest PSK after a stale WireGuard configure
The conn reads the PSK before configuring WireGuard. A rotation committed
in between pushes its key first, and the configure then overwrites it with
the old one, splitting the keys until the next reconnect.
2026-10-22 12:17:49 +02:00
riccardom 50afdff228 [client] pqkem: answer an unreadable offer with an error marker
An empty answer tells the initiator the peer does not run the KEM, and it
stops offering for good. A decode or version error is not that: a newer
client talking to an older one would silently stay classical.
2026-10-22 12:14:21 +02:00
Viktor Liu abf50e3ad4 [client] Remove the replace directives from the embedded client (#8280) 2026-10-22 17:39:22 +09:00
Theodor Midtlien f886f4b06c Bump wasm build size to 70MB (#8281) 2026-10-22 09:47:39 +02:00
Bruno Mercier Costa b03d1c7d07 [client] Stop the service and UI before the NSIS installer extracts files (#8255) 2026-10-21 20:37:48 +02:00
Mohammadreza Razavian 73829c8d96 [management] Give the /api/setup owner account the single account mode domain (#8147) 2026-10-21 20:46:33 +03:00
Bethuel Mmbaga 4ca20f0c7a [management] Make EDR sync interval configurable (#8273) 2026-10-21 18:38:08 +03:00
Bethuel Mmbaga 11799f6b94 [management] Use account owner as inviter for service user invites (#8276) 2026-10-21 18:33:31 +03:00
Eduard Gert 031a4bd70d [client] Migrate the desktop UI to Tailwind CSS 4 and fix frontend npm audit findings (#8277)
* [proxy] Fix npm audit findings in the web UI

npm audit reported 10 vulnerabilities (7 high), all in build tooling:
Vite/launch-editor, PostCSS, Babel, browserslist, js-yaml, nanoid,
brace-expansion and source-map-js. All are resolved by the lockfile
update; Vite is pinned exactly, so it is bumped from 7.3.2 to 7.3.7.

* [client] Migrate the desktop UI to Tailwind CSS 4 and fix npm audit findings

pnpm audit reported 36 vulnerabilities in the desktop UI. All but two
(braces and postcss-selector-parser, both pulled in by Tailwind 3 with
no patched version in range) are fixed by updating within the existing
ranges; react-router moves to 7.18.4, which also fixes an open redirect
and an XSS in its error handler.

The remaining two go away by moving to Tailwind CSS 4, which the proxy
web UI already uses:

- The config moves into an @theme block in globals.css; the official
  upgrade tool applied the v4 utility renames across the components.
- @tailwindcss/vite replaces the PostCSS plugin and autoprefixer.
- tw-animate-css replaces tailwindcss-animate with the same classes.
- The Prettier plugin reads the theme from the stylesheet.

A screenshot comparison of every screen against the Tailwind 3 build
found three regressions caused by v4's changed utility and variant
ordering, fixed here so the UI renders as before:

- Label combined inline-block and flex; v4 let inline-block win, which
  made every settings row taller. The dead inline-block is removed.
- The active settings tab lost its highlight in dark mode because
  dark: now outranks group-data-[state=active]:. The active color is
  set for dark mode explicitly.
- v4 rebuilt its default palette in OKLCH, shifting colors the theme
  does not override such as sky-400. The Tailwind 3 values for the
  neutral, zinc, sky, orange and amber families are pinned in @theme.

The only remaining visible difference is 2px less height per row in
the profiles list, because v4's preflight resets the browser's default
table-cell padding.
2026-10-21 15:53:05 +02:00
riccardom e5764dd916 [client] Raise the wasm size limit to 64 MB
The post-quantum exchange adds about 270 KB to the wasm client, pushing
it past 60 MB. ML-KEM runs in the browser too, so the code stays in.
2026-10-21 13:22:21 +02:00
Zoltan Papp 1e63b68c4d [relay] Negotiate the protocol version on the server side (#8231) 2026-10-21 12:27:44 +02:00
riccardom 2c2f684571 [client] pqkem: test the controller reoffer through the handshaker
The split-key test only checked the manager's decision, so a broken
handshaker branch would still pass. The capability test asserted while
holding the manager lock, which hangs instead of failing.
2026-10-21 08:44:13 +02:00
riccardom ebeebe0179 [client] pqkem: drop the previous PSKs on Stop
They are WireGuard key material like the current ones, which Stop already
clears, and nothing reads them after shutdown.
2026-10-21 08:44:13 +02:00
riccardom 4eaa57e8b5 [client] pqkem: wake status subscribers when a peer turns quantum resistant
The PSK usually lands after the connection came up, so the flag flips with
no other state change. Without a notification, UI and CLI subscribers kept
showing the peer as unprotected until something unrelated changed.
2026-10-21 08:44:13 +02:00
riccardom fb6e955f00 [client] pqkem: cover the env flags, signal port, offer marker and status flag
These branches decide whether ML-KEM runs, which port a peer is reached
on, and what the status reports, and none of them had a test.
2026-10-20 17:53:03 +02:00
riccardom aa9141c451 [client] pqkem: make weak tests fail on the regressions they name
The recovery test could time out its own bootstraps under CI load, the
non-capable test passed with the cancellation removed, and the decoder
rejection test only exercised the encoder. The timing spike ran on every
go test; it is a benchmark now.
2026-10-20 17:53:02 +02:00
Bethuel Mmbaga aa0f569677 [management] Remove the MySQL store engine (#7914) 2026-10-20 18:50:32 +03:00
riccardom 2853a16297 [client] pqkem: answer every responder offer with a fresh KEM bootstrap
Only the controller's offer carries KEM material, so when the responder
starts a negotiation the controller is meant to reply with its own KEM
offer. It only did so before the very first exchange: once it held a
converged PSK it answered the responder's offer normally and kept that PSK,
on the assumption that a stale key would heal through the WireGuard watcher
reconnecting over Signal.

That assumption fails when the reconnect comes from the responder. If every
answer of a data-path rotation is lost, the responder commits the new PSK
and the controller does not, so WireGuard cannot handshake and no further
rotation runs to escalate. The watcher closes the connection after ~3.5
minutes and ICE reconnects in seconds, but the reconnect offer came from the
responder, the controller kept its stale key, and the tunnel stayed down.
In the lab it took 201 s and a lazy-connection idle/wake cycle to heal; with
lazy connections off, the default, nothing would heal it.

Treat a converged exchange as not in flight: a responder offer then always
gets a fresh bootstrap, so any responder-started renegotiation re-aligns
both PSKs. While the controller's own exchange is still in flight a
crossing offer is answered normally as before, so offers cannot multiply.
The cost is one extra Signal KEM round per responder-started reconnect.
2026-10-20 16:38:00 +02:00
Maycon Santos 569b9aa169 [misc] Let suites outside the module reach the e2e harness network (#8260)
The harness hands peers the management address netbird.local:8080, which
only resolves on the deployment's own Docker network, and keeps that
network, the client image and the client container private. A suite that
runs agents somewhere else, such as the Kubernetes operator's Kind
cluster, has no way to attach to the deployment, run the same client
build, or check routed access from a harness peer.

Expose the network name, resolve the client image on its own, and let a
caller exec into a client container.
2026-10-20 14:21:05 +02:00
Maycon Santos b866d1584c [client] Drop the non-functional admin URL (#8267)
* [client] Drop the non-functional admin URL

#4218 deprecated --admin-url, but #3980 merged right after it and restored the flag and its wiring. The value has been stored and echoed back ever since, without anything reading it.

Stop sending it from the CLI, ignore it in the daemon and drop it from the profile config, the UI settings service and the debug bundle. The proto fields stay for older clients, and the mobile getters and setters keep their signatures as deprecated stubs.

* [client] Drop adminUrl from the Wails API reference
2026-10-20 14:07:00 +02:00
Riccardo Manfrin 7420c97930 [client] Keep the resolv.conf watch armed while repairing DNS in file mode (#8207)
* [client] Split resolv.conf parsing from file reading

Pure refactor, no behavior change. parseResolvConf parses content that
was already read, so a caller can compare and parse the same snapshot
of the file instead of reading it twice.

* [client] Return the written content from the resolv.conf update function

Pure refactor, no behavior change. The repair watcher needs to know what
NetBird wrote so it can tell its own write apart from a third party's.
Callers ignore the content for now.

* [client] Keep the resolv.conf watch armed while repairing

In file mode the repair watcher removed its inotify watch, rewrote
resolv.conf and re-added the watch, without looking at the file again.
A second third-party write landing in that window raised no event, so
the file stayed on the upstream resolver until something else touched
it. systemd-resolved with the stub listener off, the Amazon Linux 2023
default, writes the file twice in a row on restart and does not rewrite
it on an unchanged DHCP renewal, so the host stayed off NetBird DNS
while the client reported connected.

The watch now stays armed during the repair. NetBird's own write is
recognized by comparing the file with the content it wrote, which also
covers a write whose search list was truncated to the resolv.conf
limits and would otherwise fail the parameter check forever. The
watcher checks the file once when it is armed, since a change made
before that produced no event.

* [client] Consume inotify errors in the resolv.conf watcher

fsnotify blocks its reader until an error is received. The watcher
never read the Errors channel, so a single error, such as an event
queue overflow, stopped all further events and silently disabled the
repair. The watcher now consumes errors and checks the file, since an
overflow means events were dropped.

* [client] Parse resolv.conf one line at a time

Pure refactor, no behavior change. The per-line handling moves into
parseResolvConfLine, which keeps parseResolvConf under the cognitive
complexity limit.

* [client] Drop the comment restating parseResolvConfLine
2026-10-20 13:35:15 +02:00
Bethuel Mmbaga 1f70970792 [management] Mask credentials in logged URLs (#8250) 2026-10-20 13:33:46 +03:00
Alireza Heydari 30ea8270d6 [client] Fix grammatical error in post-install message (#8242) 2026-10-20 11:18:41 +02:00
Riccardo Manfrin 6989d689cf [misc] Announce tag-pushed releases on the forum (#8265)
* [misc] Announce tag-pushed releases on the forum

Since 0.77 a release is cut by pushing a tag, and goreleaser creates the
GitHub release with GITHUB_TOKEN. Events raised by that token start no
workflow, so the release:published trigger never fires for them and
0.78.0, 0.79.0, 0.80.0, 0.81.0 and 0.81.3 got no forum topic, while
releases published from the UI still did.

Post from the completion of the Release workflow instead, for tag runs
that succeeded and whose release was created by the bot. Release
candidates stay off the forum, and an existing topic with the same title
is left alone so re-running a green Release run does not post twice.

* [misc] Point an empty forum post at the release notes

goreleaser creates the release with an empty body and the notes are
written on the release page afterwards, so the topic would otherwise
hold only its title link. Say where the notes and downloads are.

* [misc] Fail closed in the forum post lookups

Announce stable tags only: every pre-release tag carries a hyphen, so
canaries and release candidates pushed as tags stay off the forum.

A failed release lookup now fails the step instead of passing as "no
release"; only a 404 skips. The duplicate check searches by title rather
than reading the first page of the category, and a failed search stops
the step instead of falling through to a post. Discourse requests
refuse any non-HTTPS redirect and retry transient errors.
2026-10-20 10:42:34 +02:00
dmitri-netbird 8fef487e42 [management] Add support for pagination in GetAllPeers call (#8190)
* use libopenapi OpenAPI generator

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* added a handler for v1alpha1/peers

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* wire up request validator

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* wired up spec-based validation

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* use sync validation; set base url to v1alpha1

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* moved stuff around, extracted runtime libopenapu deps into runtime_tooling

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* testing /peers path params

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* fixed tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* added user schemas and paths

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* use api validation in tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* post-merge fixes

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* deleted tmp command used to test validator integration

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* require request body in post/put requests in order to force validation

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* making linter happy

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* switch to api/v1alpha1

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* go mod tidy

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* keep the original order of middleware: metrics, cors, then auth

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* return 422 on validation errors

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* cleanups

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* more cleanups

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* v1alpha1 spec cleanups

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* no need for a double-pointer

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* more linter fixes

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* removed more double pointers

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* use di to inject api_v0 and api_v1 http routers

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* do not re-add middleware on repeated call to ApiHandler

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* support for pagination and filtering in /peers call

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* calling ApiRouter() now also calls ApiV1Router()

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* adding peer filters

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* introduced paginated GetAllPeers call

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* wire up pagination

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* convert pagination state to query scope

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* peer filter wip

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* implemented peer filter

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* test for filtering by os

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* support for sorting of peers

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* support for ordering + tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* more tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* wire up total number of records in paginated GetAllPeers call

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* cleaned up GetAllPeers response

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* pacify linter

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* cleanups and fixes

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* moar cleanups

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* pacify linter

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* remove duplicated test

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* make linter happy

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* moving GetAllPeersPaginated to peer manager, p. 1

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* moving GetAllPeersPaginated to peer manager, p. 2

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* added peer manager tests for GetPeersPaginated

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* order peers in GetAccountPeersPaginated by their ids by default

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* always order by peer_id, filter out proxies

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* fixed broken tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* support for filtering of peers by peer_id in GetAllPeers()

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* filtering by peer_ids

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* use lowercase in 'like' queries, drop ordering by ip

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* drop sort_by ip key

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* drop forced_approve filter

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* replace mac with darwin in peer os filter

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* fix peers query parameter: use peer_ids

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* fixed broken peer_test

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* responded to feedback

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

---------

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-10-19 22:19:39 +02:00
Maycon Santos 36fe467aba [client] Avoid Signal reconnect listener shutdown deadlock (#8257)
* [client] Reproduce Signal reconnect listener shutdown deadlock

* [client] Invoke Signal reconnect listeners outside the client mutex

Capture the listener with the registration state, then release the mutex before calling it. Watcher shutdown can now unregister a listener while an in-flight callback waits for the watcher lock.

Keep callback invocation synchronous and document that unregistration does not drain an already captured callback. Record callback completion inside the regression test's external lock.
2026-10-19 18:47:25 +02:00
riccardom d0e8c4ed33 [client] pqkem: name the post-quantum recovery path for what it does
The recovery that re-runs the ML-KEM bootstrap after a failed exchange was
called RequestReoffer (and "reoffer" along the callback chain). The name
only says that another offer goes out, so it reads like routine
renegotiation or key rotation, while it fires only when the exchange has
failed: the initial exchange, or rotations failing repeatedly. Normal
rotation never takes this path; it runs over the data path.

Rename it RecoverPQKeyOverSignal, and the callback field, engine wiring, log
line, comments and test with it. The controller re-offer in the handshaker
is a different mechanism and keeps its name. Pure rename, no behaviour
change; only the debug log wording follows the new name.
2026-10-19 17:15:32 +02:00
riccardom e9a622c895 [client] pqkem: apply PSK generations to WireGuard in order
Two derivations for the same peer can overlap, for example a rotation and
a Signal re-bootstrap. The generation guard checked and recorded the
generation under a lock but called SetPresharedKey after releasing it, so an
older apply already inside SetPresharedKey could finish after a newer one
and leave WireGuard on the stale key, splitting the peers until the next
exchange (reported in the security review of #7098).

Hold the lock across the generation check and the SetPresharedKey call. The
lock only serialises PSK applies, which are quick local calls, so this does
not reach into the manager's own locking.
2026-10-19 17:02:15 +02:00
riccardom 92764d8bff [client] pqkem: keep rotating after a rotation times out
When every offer of a data-path rotation was lost, the initiator dropped
the exchange and logged that it would retry next cycle. It never did: a
rekey only chains a new rotation from an exchange awaiting it, and none was
left. Rotation stopped until the next Signal re-bootstrap (a lazy wake, or
never on a long-lived connection), and because no further attempt ran, the
consecutive-failure count could not reach the threshold that hands recovery
to the host either (reported in the security review of #7098).

On a rotation timeout, hand the chain back to the exchange the rotation
acknowledged. The next rekey then starts a fresh rotation from it: if the
offer was simply lost both peers still hold that PSK and the rotation
converges, and if the responder had already moved on, the attempts keep
failing until the threshold triggers a re-bootstrap. A timed-out bootstrap
is still dropped, as it has nothing to fall back to.
2026-10-19 17:02:15 +02:00
riccardom 5f3f5d56d9 [client] pqkem: re-bootstrap over Signal only while the peer is connected
A failed KEM exchange makes the manager ask for a fresh signalling offer.
For a peer that is registered but offline that offer can never be answered,
so with lazy connections off (the default) each such peer drew a new offer
at every exchange timeout, about every 22 seconds, forever. That runs on top
of the reconnect guard, which already retries the peer with exponential
backoff, and every guard offer starts a fresh KEM exchange anyway. With many
offline peers it is a steady stream of useless Signal traffic.

Send the recovery offer only while the peer is connected. That is where it
is needed: the transport is up but the KEM did not converge (strict mode
stuck before its first PSK, or a broken rotation). An unreachable peer is
left to the guard's backoff.
2026-10-19 17:02:15 +02:00
riccardom 3cfc5c82a8 [client] pqkem: install strict-mode keys on idle lazy peers
ML-KEM strict mode promises that no traffic crosses on a non-PQ key. Under
lazy connections an idle peer is still installed in WireGuard, so that
traffic towards it can wake it, and it accepts a handshake the remote
initiates directly. Both lazy listeners installed it without a preshared
key, so a freshly created idle peer accepted a classical session before any
KEM exchange, for example from a remote whose WireGuard still targets the
victim's real endpoint (security review of #7098, finding F-002). The
strict sentinel only reached WireGuard later, on the normal connection
path.

Let the lazy peer config carry a key provider that the listeners install
with the idle peer. In strict mode it yields the PQ PSK once derived and
the sentinel until then; it is read at install time so it is never stale.
Outside strict mode it yields nothing and the idle peer's key is left
unchanged, as before.
2026-10-19 17:02:15 +02:00
riccardom c1118f6a78 [client] build the lazy peer config in one place
Preparation for installing idle peers with ML-KEM strict-mode key material
(security review of #7098, finding F-002). The lazy connection manager
receives a peer description from two call sites that each spell out the
same struct literal; the new field should be added once, not kept in sync in
two places.

Pure refactor, no behaviour change: both sites call newLazyPeerConfig.
2026-10-19 17:02:15 +02:00
riccardom 13451079c3 [client] pqkem: authenticate data-path messages under the shared PSK
The data-path socket identified a sender only by its source IP and port and
trusted that anything reaching the overlay address came through the tunnel.
On a host-bound socket that does not hold: Linux delivers a datagram to a
local overlay IP from a physical interface too (rp_filter is loose, the
firewall's input rules match the tunnel interface), so a LAN-adjacent host
spoofing a peer's overlay source could inject a KEM offer. A forged
bootstrap offer was adopted outright, desynchronising the two PSKs until a
re-bootstrap, and repeating it sustains the outage (security review of
#7098, finding F-003). No traffic is disclosed, but the exchange must not
accept unauthenticated input.

Seal every data-path message with an HMAC keyed from the PSK the two peers
share; only a peer that holds it can rotate. A reply is sealed under the key
that verified the request, because the responder has already committed the
new PSK when it answers. The previous PSK stays valid for verification so an
offer retransmitted after that commit is still answered. Bootstraps are
refused on the data path entirely: they start without a shared PSK and only
signalling can carry them.

A rotation between peers whose configured secrets differ is now refused at
authentication, so the configured-secret test checks that instead.
2026-10-19 17:02:15 +02:00
riccardom b10fc220f3 [client] pqkem: route PSK commits and data-path sends through helpers
Preparation for authenticating data-path messages (security review of
#7098, finding F-003). The authentication needs the existing PSK while a
new one is being derived, to produce a MAC attached to the offers and their
answers that proves they are authentic. So committing a new PSK must keep
the previous one, and a reply must be sealed with the PSK that verified the
offer rather than the one just committed. Today both operations are inlined
at their call sites.

Pure refactor, no behaviour change: the two PSK commit sites call
commitPSKLocked, and pushDataPath delegates the endpoint lookup and send to
sendDataPath.
2026-10-19 17:02:15 +02:00
Edward 0bb9854704 [client] Add Hebrew to the language picker (#8102)
The Hebrew translation is complete and synced from Crowdin, and RTL
layout support is in, so list it in _index.json.
2026-10-19 16:39:17 +02:00
riccardom a73ce11912 [client] pqkem: bind the configured pre-shared key into the PQ PSK
--pre-shared-key promises that only peers holding the same key can
communicate. With ML-KEM enabled the derived PQ PSK replaces it in
WireGuard, but the derivation never saw it: two peers with different
configured keys, or only one with a key, still agreed on the same PQ PSK
and talked. A permitted peer identity, or a stolen identity key, was enough
to bypass the extra secret (security review of #7098, finding F-001).
Rosenpass does not have this gap because it seeds its exchange with the
configured key.

Mix the configured key into the HKDF keying material, so a peer without it
derives a different PSK and WireGuard fails closed. It goes through the
binding shared by every derivation, so bootstrap and rotation both enforce
it. The library takes it as a generic option, keeping it free of WireGuard
concepts.
2026-10-19 16:37:25 +02:00
6e1065882a New Crowdin updates (#8101)
* New translations common.json (French)

[ci skip]

* New translations common.json (Spanish)

[ci skip]

* New translations common.json (German)

[ci skip]

* New translations common.json (Hebrew)

[ci skip]

* New translations common.json (Hungarian)

[ci skip]

* New translations common.json (Italian)

[ci skip]

* New translations common.json (Japanese)

[ci skip]

* New translations common.json (Russian)

[ci skip]

* New translations common.json (Ukrainian)

[ci skip]

* New translations common.json (Chinese Simplified)

[ci skip]

* New translations common.json (Portuguese, Brazilian)

[ci skip]

* New translations common.json (Galician)

[ci skip]

* Update client/ui/i18n/locales/he/common.json

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* New translations common.json (French)

[ci skip]

* New translations common.json (Spanish)

[ci skip]

* New translations common.json (German)

[ci skip]

* New translations common.json (Hebrew)

[ci skip]

* New translations common.json (Hungarian)

[ci skip]

* New translations common.json (Italian)

[ci skip]

* New translations common.json (Japanese)

[ci skip]

* New translations common.json (Russian)

[ci skip]

* New translations common.json (Ukrainian)

[ci skip]

* New translations common.json (Chinese Simplified)

[ci skip]

* New translations common.json (Portuguese, Brazilian)

[ci skip]

* New translations common.json (Galician)

[ci skip]

---------

Co-authored-by: Edward <43848523+thomashacker@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-10-19 16:24:07 +02:00
Viktor Liu b1f4efb160 [client] Fix errcheck findings from the latest golangci-lint (#8251) 2026-10-19 22:16:59 +09:00
riccardom 2a2f7a9bbc [client] pqkem: report a peer as quantum resistant once its PSK lands
The per-peer "Quantum resistance" flag was computed only when the relay or
ICE connection came up. When the KEM answer lands after the WireGuard
endpoint is configured (the controller answering a crossing responder offer,
the common case under lazy connections), the peer kept being reported as not
converged while the PSK was programmed and traffic flowed. Reproduced in 4 of
7 fresh lab runs.

Mark the peer in the status recorder once OnNewPSKReady has programmed the
PSK, and OR that mark into every relay/ICE status update under the recorder's
lock. The connection path reads the PQ state before configuring WireGuard and
writes the status afterwards, so without the mark its stale "false" would
clear the flag again. The mark is dropped with the peer, matching the PSK's
lifetime, and the update is best-effort: a peer missing from the recorder
never fails the PSK apply. Status-only change; PSK programming and strict
gating are unaffected.
2026-10-19 15:08:34 +02:00
riccardom f669cba522 [client] pqkem: bind the data-path transport on the netstack in netstack mode
In netstack mode (js/wasm and embedded clients) the overlay IP lives only
inside the userspace stack, so binding it on the host failed with "cannot
assign requested address". The exchange was then disabled, and in strict
mode the engine refused to start at all. Reproduced in the lab with
NB_USE_NETSTACK_MODE=true.

Open the socket through the interface's netstack when there is one. Its
datagrams then ride the tunnel like any other overlay traffic (over the
relay's WebSocket in the browser). Host-TUN clients keep binding on the host.
2026-10-19 15:08:34 +02:00
riccardom 1152b25149 [client] pqkem: run the data-path transport over a net.PacketConn
Preparation for running the ML-KEM exchange on the js/wasm client. There, and
in other netstack-mode clients, the overlay IP exists only inside the
userspace stack, so the KEM socket has to be opened on the netstack. The
netstack returns a *gonet.UDPConn rather than a *net.UDPConn, so the
transport can no longer hold the host socket's concrete type.

Hold the socket as a net.PacketConn, which both implement, and convert
endpoints at the edges. Sources are unmapped explicitly, since a udp4
socket's net.UDPAddr may carry the 16-byte form. No behaviour change: the
transport still binds on the host, and the next commit opens it on the
netstack. Adds a host round-trip test that also covers the fallback to an
ephemeral port.
2026-10-19 15:08:34 +02:00
Riccardo Manfrin f8f335720d [client,relay] Bump quic-go to v0.64.0 (#8249) 2026-10-19 14:08:15 +02:00
EdwardandDevBot NetBird 279adb8fdd [management] Return the reason when custom domain validation fails (#8122)
* [management] Return the reason when custom domain validation fails

The validate endpoint returned an empty 202 and ran the check in a
goroutine, so a failed verification only reached the management log.
Run the check in the request and return 204 or 412 with a classified
reason. Resolver errors are not forwarded.

* [management] Address review feedback on custom domain validation

Pass the request context to the lookup and store calls now that the
check runs in the request, compare the self-resolved name
case-insensitively, document the 412 like the other entries, and
check the returned error in the existing tests.

* [management] Match custom domain CNAME targets case-insensitively

DNS names are case-insensitive, so a CNAME pointing at the cluster in
different casing should validate instead of failing as a mismatch.

* [management] Add validation timeout

---------

Co-authored-by: DevBot NetBird <91386968+netbirddev@users.noreply.github.com>
2026-10-19 13:58:14 +02:00
Zoltan Papp cf01711971 [client] Bump wireguard-go to bd0cb75a3df0 (#8229)
StdNetBind.Send reused pooled *net.UDPAddr objects and copied the
destination into ua.IP before resizing the slice. After an IPv4 send the
slice was 4 bytes long, so the next IPv6 send copied only 4 bytes and
extended the slice with stale data, misdirecting datagrams to a truncated
or foreign IPv6 address. Userspace clients with both IPv4 and IPv6 P2P
peers saw intermittent loss and restarting handshakes.

The fork now carries the upstream fix (WireGuard/wireguard-go#135,
netbirdio/wireguard-go#24). Pick it up.
2026-10-16 20:52:43 +02:00
riccardom 6cba40301e Merge remote-tracking branch 'origin/main' into feat-post_quantum_ml_kem
# Conflicts:
#	client/internal/peer/status.go
#	client/status/status.go
2026-10-16 14:13:53 +02:00