Skip to content

CI check: NetBird release-asset pipeline - #1

Draft
AndrewMoryakov wants to merge 112 commits into
mainfrom
feature/netbird-official
Draft

CI check: NetBird release-asset pipeline#1
AndrewMoryakov wants to merge 112 commits into
mainfrom
feature/netbird-official

Conversation

@AndrewMoryakov

Copy link
Copy Markdown
Owner

Service PR — opened only to run the workflows in this fork.

Workflow runs for pull requests from a fork into sipeed/NanoKVM sit at
action_required until a maintainer approves them, so the new CI steps in
sipeed#759 cannot be exercised there. This PR runs the same
NanoKVM Package workflow inside the fork, where no approval is needed.

What is being verified:

  • scripts/build-netbird.sh — cross-compiles NetBird for riscv64 and checks
    ELF e_machine=243 before packing.
  • The cache step is keyed on the pinned version, the script hash and the
    toolchain, and must sit after Assemble package (which does
    rm -rf build/release).
  • netbird_riscv64.tgz reaches the uploaded artifact.
  • scripts/verify-release-assets.sh — archive layout, the VERSION inside the
    archive against kvmapp/system/netbird/VERSION, and the architecture of the
    binary.

None of this has ever executed. Close once the run is green; it does not affect
sipeed#759.

tkmsst and others added 30 commits February 2, 2026 01:14
Add NetBird as a second VPN option alongside Tailscale, using the
official NetBird cloud server. Only one VPN runs at a time due to
NanoKVM Cube's 161MB RAM constraint.

Backend:
- NetBird service with install/uninstall/start/stop/restart/login/down
- CLI wrapper with socket-based daemon communication and timeout handling
- Bundled binary installation with version tracking
- VPN preference service (/etc/kvm/vpn) for mutual exclusion

Frontend:
- NetBird settings tab mirroring Tailscale's state machine UI
- Autostart toggle on both Tailscale and NetBird headers
- Device flow login: Login button -> URL -> "I have logged in"
- Device panel with enable switch, info rows, disconnect button

Init scripts:
- S99netbird init script with TUN device setup and GOMEMLIMIT=30MiB
- S95nanokvm reads /etc/kvm/vpn preference, starts only selected VPN
- Swap setup and OOM protection for NanoKVM-Server process
feat : Add Korean and Japanese virtual keyboard layouts
Enable support for 640x480 resolution
commit 3dea4b1
Author: wenjie <meetwenjie@gmail.com>
Date:   Fri Apr 10 17:11:07 2026 +0800

    fix(picoclaw): clean temp media and surface MCP screenshots in chat

    - remove shared load_image staging and pass source paths through directly
    - delete /tmp/picoclaw_media when a gateway session closes
    - push MCP screenshot observations to downstream websocket clients
    - hide null tool feedback messages and auto-scroll when screenshots load

commit f58ffe1
Author: wenjie <meetwenjie@gmail.com>
Date:   Fri Apr 10 15:55:59 2026 +0800

    Harden PicoClaw local API auth and session locking

    Split PicoClaw routes by caller trust level and require the internal loopback token for local-only endpoints. Update the NanoKVM bridge script to send the internal token for loopback requests. Relax session lock acquisition for screenshot and action calls so the active session can perform local operations without permanently taking over the lock, and stop forcing the runtime dm_scope default.

commit 462a067
Author: wenjie <meetwenjie@gmail.com>
Date:   Fri Apr 10 13:52:39 2026 +0800

    fix(picoclaw): improve KVM reliability and sidebar behavior

    - follow HTTPS loopback redirects in the NanoKVM bridge script
    - enable MJPEG frame caching only during active PicoClaw gateway sessions
    - restore legacy screen zoom behavior across MJPEG and H264 renderers
    - keep the PicoClaw sidebar available on mobile without splitter layout conflicts
    - hide empty "null"/"undefined" chat messages and keep MJPEG failures on a black screen

commit bfda85c
Author: wenjie <meetwenjie@gmail.com>
Date:   Fri Apr 10 11:37:15 2026 +0800

    fix(picoclaw): verify runtime downloads and remove unused config API

    - verify the downloaded runtime archive against the published SHA-512 checksum
    - remove the unused /api/picoclaw/config endpoints and related frontend state
    - rename runtime_control.go to runtime_constants.go for clearer intent

commit 10b34aa
Author: wenjie <meetwenjie@gmail.com>
Date:   Thu Apr 9 16:32:30 2026 +0800

    fix: stabilize picoclaw runtime defaults and secure local MCP access

    - persist required NanoKVM startup defaults before launching picoclaw and after saving model config
    - force-enable the pico channel when loading config so runtime status can recover to ready
    - derive the local MCP URL from the configured HTTP port and keep loopback-only HTTP access behind an internal token
    - move loopback HTTP redirect logic into middleware to simplify main server startup
    - improve runtime/sidebar state handling and add the load-image endpoint for active picoclaw sessions

commit d205401
Author: wenjie <meetwenjie@gmail.com>
Date:   Fri Mar 27 17:55:29 2026 +0800

    feat(picoclaw): add picoclaw integration
imguoguo and others added 14 commits August 4, 2026 16:33
+ Add persistent custom update server configuration, authenticated URL support, manifest validation, and the settings UI.

+ Keep application updates rebootless by coordinating video shutdown, migration-time service startup, and online/offline service restarts.

+ Localize the custom update server workflow across all supported languages.
Layer the frontend toolchain on top of the existing release builder
image so a single container covers Go, C support-layer and web
development, with the builder image remaining the single source of
truth for the cross toolchain, Go and MaixCDK.

- Mount the workspace at /home/build/NanoKVM, where the Makefile and
  support/sg2002/build expect it, and resync MaixCDK components via
  update_lib on create so C builds never use stale sources
- Install Node from official dist tarballs (integrity-checked, pinned
  to the major used by CI), with NODE_DIST_MIRROR / NPM_REGISTRY /
  BASE_IMAGE build args for restrictive or mirrored corporate networks
- Export the MaixCDK virtualenv via remoteEnv so non-interactive
  processes (tasks, extensions, exec) get it too; make
  updateRemoteUserUID explicit since the published builder image bakes
  the CI runner's uid
- Run pnpm non-interactively during post-create (no TTY) and give the
  skeleton-less home a standard ~/.profile -> ~/.bashrc chain
- Make the web toolchain convention explicit via package.json engines
  (Node >= 22, pnpm >= 11, matching CI) and document it in web/README
- Document the dev container in the README, point server/README at the
  container flow that works on any host OS, and ignore .pnpm-store/
Upgrade Vite to 8.2.0 and @vitejs/plugin-react to 6.0.5.

Use Vite's native tsconfig path resolver and remove vite-tsconfig-paths.

Verified with pnpm build and pnpm lint (existing warnings only).
* feature: provide HDMI status in  /api/vm/hdmi

* fix: return false when HDMI idle in  /api/vm/hdmi
* fix(ota): isolate updates in persistent workspaces

Stage online and offline update archives under /root/.kvmcache/nanokvm-update-* and validate storage, manifests, and archive contents before changing the installed application.

* fix(ota): harden storage safety and release gates

Preserve the last rollback backup when update storage is insufficient, and verify the actual application mount point before installation.

Move the shared transfer sentinel from /tmp to /run, enforce device package limits in release verification, and run that verification in package CI.
- Add touch gestures for relative mouse mode, including tap, drag, long press, double-tap drag, and two-finger scrolling.

- Improve absolute mouse touch handling and prevent black letterbox areas from generating remote mouse events.

- Fit H.264 and MJPEG screens with contain scaling and disable browser touch handling on the screen elements.

- Move golang.org/x/sys to the direct dependency list.
Resolve conflicts with the multi-user (sipeed#876) and init-script refactors:

- settings/index.tsx: move the NetBird tab into the admin-only tab list
  next to Tailscale.
- S95nanokvm: rebase the NetBird changes onto upstream's
  start_services/stop_services helpers — GOMEMLIMIT and OOM protection
  now live in start_services, VPN selection moved to start_vpn().
  restart keeps upstream's migration guard and no longer bounces the VPN.
- Add NetBird strings to zh, zh_tw and ru; other locales fall back to en.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@AndrewMoryakov
AndrewMoryakov force-pushed the feature/netbird-official branch 5 times, most recently from a2be3cb to 96bc0bb Compare August 25, 2026 02:43
AndrewMoryakov and others added 2 commits August 25, 2026 03:02
These came in with the NetBird branch but have nothing to do with NetBird, and
each needs its own justification and its own measurement on hardware:

- setup_swap() writes a 128 MB swapfile to /data/swapfile. /data is mounted only
  when /dev/mmcblk0p3 exists (S01fs:35-39), that partition is created only on
  devices with /boot/usb.disk0 (S01fs:21), and it is formatted exFAT (S01fs:30).
  Both failure paths are silent while the script still prints "Swap enabled",
  and the 128 MB is spent on the user's partition on first boot regardless.
- SERVER_GOMEMLIMIT=20MiB caps NanoKVM-Server's soft heap. Go does not fail at
  that limit, it collects continuously — on a device that also runs the video
  pipeline. The value appears to assume the swap above actually works.
- oom_score_adj = -800 adds a sleep 1 to every boot.

Also reverts the file header from Rev3.2 back to Rev3.1, since the changes that
bumped it are gone.

start_services is now byte-identical to what upstream ships. If the memory work
is worth doing it belongs in its own PR with measurements, not carried along by
a VPN feature where reviewers read it as merge noise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NetBird publishes no riscv64 build. Rather than bundle a ~38 MB client into the
OTA package for every device — most of which will never enable it — NanoKVM
builds one and attaches it to its own release; the device downloads it on demand
in the next commit, as it already does for Tailscale.

Measured: the package is currently 14.7 MiB (content-length of
nanokvm_2.4.3.tar.gz). The client is ~13 MiB gzipped, so bundling would mean
roughly +90 % for everyone.

- scripts/build-netbird.sh cross-compiles the version pinned by
  kvmapp/system/netbird/VERSION, and checks the result for ELF e_machine=243
  with the same od-based test package.sh applies to every shipped artifact.
  -trimpath, a normalized tar and gzip -n make a rebuild byte-identical for a
  given Go toolchain; the toolchain itself is pinned in the workflow, not here.
- package.yml builds it, cached on the pinned version, the script hash and the
  toolchain. The step must stay after "Assemble package": package.sh:88 does
  rm -rf build/release.
- release.yml publishes it and expects it back on promotion.
- verify-release-assets.sh checks the archive layout, the version inside the
  archive against the firmware pin, and the architecture of the binary.

This is a standing commitment for the project: every release would cross-compile
a third-party client and publish it under the NanoKVM name. Reviewing it apart
from the feature is deliberate — it can be rejected on its own terms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@AndrewMoryakov
AndrewMoryakov force-pushed the feature/netbird-official branch from 96bc0bb to e2a87e9 Compare August 25, 2026 03:03
AndrewMoryakov and others added 2 commits August 25, 2026 12:19
Builds on the original NetBird commit in this branch. Two changes, kept together
because both rewrite netbird.Service.Install: the client is no longer expected to
be bundled, and the one-VPN-at-a-time rule is enforced rather than only honoured
at boot.

Delivery. install.go expected /kvmapp/system/netbird/netbird, which nothing in
the tree produced — the feature could not work on any built image. It now
downloads the release asset from the preceding commit, installing it with a
rename so it can replace a running binary. /etc/kvm/netbird_url overrides the URL
for forks and for test devices, since no upstream release carries the asset yet.

Enforcement. The rule lived only in SetPreference, so one click on Install or
Start raised a second tunnel on a device with RAM for one.

- service/extensions/vpnpref owns /etc/kvm/vpn and the lock, importing neither
  client. That breaks the cycle which otherwise keeps netbird from reading its
  own preference: package vpn imports both clients.
- start, restart and up refuse when the other client is selected, as does
  netbird's login — `netbird up` raises the tunnel. Tailscale's login does not:
  it only authenticates, and its internal recovery start is guarded. Install is
  exempt on both sides; it raises a tunnel only when its client is already
  selected, and demanding the preference up front would be a trap, because the
  autostart toggle only appears once the client is installed.
- The check is in the HTTP layer on purpose: SetPreference calls Cli.Start()
  while the file still holds the previous value, so the same check inside
  Cli.Start() would make switching, and its rollback, impossible.
- Operations that start or stop a client are serialized; a concurrent request is
  refused rather than queued behind the client's 60 s timeout. netbird's Down is
  included because a timed-out `netbird down` restarts the service, so it can
  start the daemon rather than only stop it.
- A failed switch restarts the previous client and leaves the preference alone.
- `tailscale up` is bounded at 45 s. It was unbounded, and it now runs under the
  lock: an unauthenticated node would have held that lock until the server
  restarted, failing every VPN request.

Boot. S95nanokvm runs before rc reaches S98tailscaled and S99netbird, so it only
has to remove the client that was not selected. The two directions differ for a
reason outside this script: kvm_system's new_app_init() runs
`rm -f /etc/init.d/S99*` after an application update, and the glob does not reach
S98tailscaled, which the same function refreshes instead. So S99 is restored when
NetBird is the selected VPN and S98 never is. The `stop` verb no longer stops
either VPN — update-nanokvm.py calls it before an update and `restart` afterwards,
so doing that tore down the tunnel the administrator was connected through and
never brought it back.

Stopping. Both Cli.Stop() implementations are idempotent: "not installed", "no
script" and "not running" all answer the caller's question with yes. S99netbird
confirms the daemon exited by polling — start-stop-daemon -K and killall return 0
once the signal is sent, which says nothing about whether the process died.
S98tailscaled reports success from its stop branch whatever happens, so that
direction stays weaker; fixing it is out of scope here.

Also in this commit, and worth naming rather than leaving to the diff:

- Cli.Login() had a race between cmd.Wait() and the goroutines reading its pipes.
  os/exec closes those pipes inside Wait, so a fast-exiting command could drop
  the login URL. Wait now runs after both readers finish.
- ServiceRunning() treated a missing or non-executable init script as an error
  rather than as "not running", and matched on "running", which is a substring of
  "not running". Both are fixed, and Stop() reads the same state the same way.
- refreshScript() copies the firmware's init script over the installed one before
  stopping, because the polling logic lives in that script and an OTA can leave
  an older copy behind.
- uninstall() now also removes the init script and the pidfile, and the Uninstall
  handler stops the daemon and checks the result before removing anything, then
  starts Tailscale and writes the preference last.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Builds on the original NetBird UI in this branch; this commit fixes it.

- The login URL returned by the Enable switch was discarded. Flipping the switch
  instead reported a tunnel that was up while the device waited for an
  authorization nobody opened. The URL panel is now shared with the Login screen,
  and the switch opens it the way the Login screen does.
- An unbound device was shown a device panel with empty Name and IP and no way
  forward; it is routed to Login instead.
- The autostart switches ignored rsp.code. Failure codes arrive with HTTP 200
  (proto/response.go), so the toggle reported success while no VPN was running.
- Slow endpoints get per-request timeouts above the shared 60 s default: the
  NetBird CLI on riscv64 can take minutes, and install downloads the client.
  Status goes through http.request because http.get takes no per-request config,
  which leaves the shared wrapper untouched.
- Error handling for VPN calls is completed on both tabs: the NetBird components
  had lost the .catch their Tailscale counterparts have, and the Tailscale header
  never had one — its autostart calls get theirs here.
- Uninstall is now reachable from the UI, as it is for Tailscale.

Strings go to all 24 locales instead of four. Tailscale is translated everywhere,
and leaving NetBird in English for twenty of them would be a regression against
that. Where a Tailscale string already said the same thing, the locale's own
wording is reused, so only genuinely new keys were translated. stopDesc is not
reused: the Tailscale wording promises that stopping disables autostart, which is
true there — Cli.Stop() removes the init script — but not for NetBird, whose
service starts again at the next boot while its autostart is on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@AndrewMoryakov
AndrewMoryakov force-pushed the feature/netbird-official branch from e2a87e9 to c78ea95 Compare August 25, 2026 12:20
AndrewMoryakov and others added 5 commits August 25, 2026 12:48
Switching VPNs could strand a device permanently. SetPreference stopped the
current client before starting the new one, and every handler that could raise a
tunnel refused unless its client already owned the preference. On a device
reached through the VPN being switched away from, that meant: the session died
when the old client stopped, NetBird came up unbound — its first login is
interactive and had not happened — and a reboot reproduced the same state. There
was no way back without physical or LAN access.

The gate was also circular: signing in to NetBird needed the preference, and
setting the preference needed a working NetBird.

The preference now governs one thing: which client autostarts at boot. It is not
a permission to run.

- lockVPN replaces claim(). Handlers still serialize against each other, but no
  longer check the preference, so a client can be installed, started and signed
  in to while the other one carries the session.
- SetPreference no longer starts anything. If the other client is not running
  there is nothing to lose and the preference is simply recorded. If it is
  running, the incoming client must be *connected* first — Management and Signal
  for NetBird, BackendState=Running for Tailscale — and only then is the other
  one stopped. A working tunnel is never cut for one that is not up yet.
- Install starts the daemon regardless of preference. S99netbird runs
  `netbird service run`; the tunnel comes from `netbird up`, the login step, so
  on a never-bound device the daemon carries no tunnel. A previously bound client
  may reconnect on start — not verified here — so two tunnels can overlap during
  a switch. That is accepted: refusing is what locked devices out.

The cost of this design is the overlap. The device has RAM for one client
comfortably, and both daemons run only while a user is part-way through a switch.
The boot path is unchanged: select_vpn still leaves exactly one init script in
place, so a reboot always converges on the preference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Switching autostart stops the client that is carrying the session. The server now
refuses to do that until the incoming client is connected, so the device stays
reachable — but "connected" only proves the device joined the new network. It
cannot prove the operator's own machine is on it.

If the administrator has not joined their own machine to the incoming VPN, or
access rules do not permit it, the switch succeeds and they lose access anyway.
That is recoverable — the device is on the network for anyone who is on it — but
it is worth knowing before clicking, and the device cannot check it.

The confirmation dialogs on both tabs gain a description saying so. Added as a
separate key rather than lengthening the title, so the question and the warning
read apart, in all 24 locales.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review of the previous commit found that the connectivity gate protected one
path and left the rest of the model behind.

- connected() ran netbird status with restart-on-timeout, so the predicate could
  restart the service it was asked about — bouncing the tunnel whose fate was
  being decided, for up to three minutes while holding the VPN lock. It now uses
  StatusOnly, which never recovers by restarting.
- tailscale status had no deadline and is now called under that lock, so a wedged
  daemon would have held it until the server restarted. Bounded like the rest.
- running() returned false whenever it could not tell, which skipped the gate and
  stopped nothing — leaving a live client carrying a tunnel the preference said
  was gone. Uncertainty now counts as running, so the gate applies.
- A failed preference write left the device with the old client stopped and the
  file still naming it: the next boot would have started neither. The stopped
  client is restarted, and the reply says which of the two happened.
- Switching autostart to a client that is not installed is refused. It would have
  removed the other client's init script at the next boot with nothing to put in
  its place.

Still open, and not addressed here: stop and uninstall can cut the tunnel the
caller is using, on both tabs. SetPreference is guarded, those are not, and for
Tailscale the outcome is permanent because select_vpn never restores its init
script. That needs a decision about whether to guard, warn, or accept.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SetPreference refuses to stop a client until the incoming one is connected, but
three other buttons reach the same outcome with no check at all: stop and
uninstall, on both tabs. A review walking the user paths found that stopping
Tailscale over its own tunnel is the worst of them — Cli.Stop() removes
/etc/init.d/S98tailscaled and select_vpn deliberately never puts it back, so the
device does not come back on the next boot either.

Guarding those would be wrong: stopping a client is a legitimate thing to want,
and the server cannot know which route the operator is using. So they warn.

The wording is not uniform, because the outcomes are not:

- NetBird stop says the connection returns at the next boot while its autostart
  is on, which is what the code does — select_vpn restores its init script.
- Tailscale stop and both uninstalls say it will not come back on its own and to
  make sure another way in exists.
- NetBird uninstall adds that autostart returns to Tailscale, which has to be
  installed and working already — if it is not, the device ends up with neither.

Four keys in each of the 24 locales, composed from the same fragments per locale
so the four strings and both tabs stay consistent.

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

A path walk of the previous round found that the rollback added there does not
work in its only use case, plus three smaller holes.

- The rollback called Start(), which begins by copying the init script from
  /kvmapp. On a read-only or full filesystem — the reason the preference write
  failed in the first place — that copy fails too, so the client that was just
  stopped stayed stopped. Both clients gained Resume(), which starts the script
  already on disk without copying. For NetBird that always works, because its
  Stop() leaves the script in place; for Tailscale it can genuinely fail, since
  its Stop() removes the script, and the comment says so rather than pretending
  otherwise.
- The gate checked that the incoming client was installed. Installed is not the
  same as startable at boot: select_vpn restores S99netbird from /kvmapp when the
  binary is executable, but never restores S98tailscaled. A Tailscale that was
  stopped through the UI passed the check and would then not start at all. The
  gate now mirrors what the boot script actually requires.
- netbird's Uninstall handed autostart to Tailscale without checking it could
  start. On a device where Tailscale was never installed that left neither.
- isTimeout matched the substring "killed" in an error message — the same trick
  this PR removed from status parsing earlier, and wrong for the same reason: an
  external SIGKILL is indistinguishable from a deadline. Status() now wraps the
  deadline case and the check is errors.Is.
- Status probes get their own 10s budget instead of borrowing UpTimeout's 45s.
  Two probes run in sequence under the VPN lock and their total has to stay under
  the browser's 60s.

Locale fixes from the same review: Korean particles were left in their hedged
"(으)로" form although the noun is known at write time; Hungarian, Catalan and
Vietnamese sentences were mis-composed from the fragments; Japanese and both
Chinese locales had an ASCII space after 。 that the template seam introduced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.