Skip to content

security/netbird: fix seven status page display defects - #5663

Open
BxnnyG wants to merge 8 commits into
opnsense:masterfrom
BxnnyG:netbird/pr-status-display-fixes
Open

security/netbird: fix seven status page display defects#5663
BxnnyG wants to merge 8 commits into
opnsense:masterfrom
BxnnyG:netbird/pr-status-display-fixes

Conversation

@BxnnyG

@BxnnyG BxnnyG commented Aug 23, 2026

Copy link
Copy Markdown

Important notices

If AI was used, please disclose:

  • Model used: Claude Opus 5, via Claude Code
  • Extent of AI involvement: the code, the tests, the commit messages and this description were written by the model working under my direction. Every change was reviewed by me and verified on my own production OPNsense 26.7 router before submission, and the commits carry Co-Authored-By trailers.

I did not open an issue first and should have — I am happy to do that now, or to split anything here that is too large to review as a single change. Apologies for submitting without this template filled in; that was my mistake, not a deliberate omission.


The status page renders netbird status --json into two text blocks, with a
package version table below them. Reading that code against real daemon output
from a running 0.74.4 installation turned up seven places where the page states
something that is not true. All are small and independent, so each is its own
commit.

1. ICE candidate endpoints showed the candidate types. Both the "ICE candidate"
and the "ICE candidate endpoints" line were built from iceCandidateType; the
daemon reports the endpoints separately in iceCandidateEndpoint. The endpoint
addresses are what you look at to tell a direct connection from a relayed one, so
this removed the page's only useful P2P debugging output.

2. Every timestamp read - throughout January. getElapsedTime() rejected any
date with getMonth() === 0 to catch the daemon's zero time (0001-01-01), but
January is month 0. Comparing against the epoch catches the zero time (negative)
and the || 0 fallback (exactly 0) without discarding a real month.

3. The interface type was always reported as "Userspace". The expression read
status.kernelInterface; the daemon emits usesKernelInterface, so it always
fell through to the netbirdIp branch. Confirmed by feeding a capture with
usesKernelInterface: true through the renderer - it still printed "Userspace".

4. Idle peers were shown with a latency of 0.00 ms. For peers it has not
dialled, the daemon reports latency: 0 and empty strings for the ICE candidates
and the relay address. getOrDefault used ??, which does not catch an empty
string. With lazy connections enabled most peers are idle, so the list showed
several peers with ICE candidate (Local/Remote): /, an empty relay address, and
the best round trip time in the table.

5. A stopped daemon rendered as a status table full of undefined. With the
service stopped the CLI writes gRPC dial errors and no JSON, so the API controller
returns an empty result. getPeerConnectionStatus only guarded against a falsy
value and an empty array is truthy, so the page showed Daemon version: undefined,
CLI version: undefined, FQDN: undefined and Forwarding rules: undefined
instead of saying that the daemon is not running.

6. Peer names and daemon error strings were injected as markup. The assembled
text went through .html(), and it contains peer FQDNs, relay URIs and error
messages originating from the management server. The table is now built as
elements with the payload set through .text().

7. The version table listed packages that are not installed.
/api/core/firmware/info returns every known package, remote ones included, and
distinguishes them with an installed flag. The table filtered on the name only,
so it showed os-netbird-devel next to os-netbird on a system where pkg info
lists only the latter - two plugin versions that cannot coexist at all, since each
declares the other as a conflict.

Tested on OPNsense with netbird 0.74.4 (userspace bind, lazy connections enabled,
four peers): status page with the tunnel up, after netbird down, and with
service netbird stop. The renderers were additionally exercised against recorded
netbird status --json output for each of those states. The version table change
was checked against pkg info -x netbird on the same installation, which lists
netbird and os-netbird only.

Benny and others added 8 commits August 23, 2026 19:50
The "ICE candidate endpoints (Local/Remote)" line was built from
iceCandidateType, the same field already printed on the line above it,
so the endpoint addresses were never shown. The daemon reports them in
a separate field (iceCandidateEndpoint, with local/remote sub-fields).

The endpoints are what distinguishes a working direct connection from a
relayed one, so this was the least useful place for a duplicate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
getElapsedTime() rejected any date in month 0 to catch the daemon's zero
time (0001-01-01T00:00:00Z). January is month 0, so for one month a year
"Last connection update" and "Last WireGuard handshake" read "-" for every
peer, on every status page.

Compare against the epoch instead: the zero time is negative, and the
missing-value fallback new Date(... || 0) is exactly 0, so both stay
"unknown" while real timestamps survive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The connection status and peer detail blocks were assembled into an HTML
string and passed to .html(). The content includes peer FQDNs, relay URIs
and error strings that originate from the management server, so a peer
name containing markup ends up parsed in the browser of an authenticated
administrator.

Build the table as elements and set the payload with .text() instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The interface type was read from status.kernelInterface, which the daemon
does not emit. The field is called usesKernelInterface, so the expression
always fell through to the netbirdIp branch and every installation was
reported as "Userspace" regardless of the interface actually in use.

Verified against a status --json capture from a running installation: with
usesKernelInterface set to true the page still printed "Userspace".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Peers that have never been contacted are reported by the daemon with
empty strings for the ICE candidates and the relay address, and with a
latency of 0. The page printed those verbatim, so an idle peer showed
"ICE candidate (Local/Remote): /", an empty relay address and, worst of
all, "Latency: 0.00 ms" - a peer with no connection appeared to have the
best round trip time in the list.

Treat an empty string as unknown like null, and only format a latency
that was actually measured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With the daemon stopped, netbird status --json writes gRPC dial errors to
stderr and no JSON at all, so the API controller falls back to an empty
result. The page rendered that empty result as a full status table with
"undefined" in the daemon version, CLI version, FQDN and forwarding rules
rows, and "Disconnected" everywhere else - the one explanation the
administrator needed was the only thing missing.

An empty result now produces a single line naming the likely cause.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/api/core/firmware/info returns every known package, remote ones
included, and distinguishes them with an "installed" flag. The version
table filtered on the name alone, so it listed os-netbird-devel next to
os-netbird on a system where only the latter is installed - two plugin
versions that cannot even coexist, since each declares the other as a
conflict.

Filter on the flag, and build the table as elements while here, for the
same reason the two blocks above it were changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@BxnnyG
BxnnyG force-pushed the netbird/pr-status-display-fixes branch from dfefdcb to ccc29aa Compare August 23, 2026 17:51
@BxnnyG BxnnyG changed the title security/netbird: fix six status page display defects security/netbird: fix seven status page display defects Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant