CI check: NetBird release-asset pipeline - #1
Draft
AndrewMoryakov wants to merge 112 commits into
Draft
Conversation
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
…kend state mapping
Update Japanese translation
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
Add `host` server config parameter
+ 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
force-pushed
the
feature/netbird-official
branch
5 times, most recently
from
August 25, 2026 02:43
a2be3cb to
96bc0bb
Compare
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
force-pushed
the
feature/netbird-official
branch
from
August 25, 2026 03:03
96bc0bb to
e2a87e9
Compare
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
force-pushed
the
feature/netbird-official
branch
from
August 25, 2026 12:20
e2a87e9 to
c78ea95
Compare
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Service PR — opened only to run the workflows in this fork.
Workflow runs for pull requests from a fork into
sipeed/NanoKVMsit ataction_requireduntil a maintainer approves them, so the new CI steps insipeed#759 cannot be exercised there. This PR runs the same
NanoKVM Packageworkflow inside the fork, where no approval is needed.What is being verified:
scripts/build-netbird.sh— cross-compiles NetBird for riscv64 and checksELF
e_machine=243before packing.toolchain, and must sit after
Assemble package(which doesrm -rf build/release).netbird_riscv64.tgzreaches the uploaded artifact.scripts/verify-release-assets.sh— archive layout, theVERSIONinside thearchive against
kvmapp/system/netbird/VERSION, and the architecture of thebinary.
None of this has ever executed. Close once the run is green; it does not affect
sipeed#759.