* feat(storage): browse a medium and exchange files with the target machine The image menu and the download page are replaced by one storage page: it lists the media the emulated USB disk can carry, mounts and ejects them, and browses the files inside a medium over that same gadget, so the two machines exchange files without going through the terminal. Mediated media: * Add the media API (list, state, mount, eject, delete, download, files) and a file browser on top of it, and keep the pre-2.6 image endpoints working as a compatibility layer over the same code. * Add diskfs: an image is mounted through a loop device so the kernel's exFAT and FAT drivers read it, and the data partition is browsed in place, because the emulated disk already hands that very directory to the target machine. * Keep the one-writer policy the read-only mounts introduced: an image is served read-only in every mode, and the whole data partition is remounted read-only here while the target machine is its writer, through the same SetDataWritable the virtual disk switch uses. The file manager refuses those writes itself as well, so a request does not depend on a mount state. * Name the mounted medium and the USB device after the image file, cut to the SCSI inquiry product field and the USB product string, and restore the defaults when the medium is released. * Close the HID devices around a gadget re-enumeration and reopen them after, since mounting and a forced eject drop them. * Report transfers through one job slot with progress, queueing behind a running transfer, and a free space preflight before an upload starts. Image uploads keep the volume descriptor and partition table check, so an unusable image is rejected while it streams. The shared folder view: * Refresh it on a timer while the target machine owns the partition. The target's block level writes land in this device's page cache, which the mounted exFAT shares, so the file data here is already current - but the dentry and inode caches are not, and a file the target grew keeps the size it was first read with. Dropping those caches and pushing a storage event makes the web UI follow the target machine within a second of its own writeback. The partition's write counter cannot drive this: it only moves once this side writes its dirty pages back, which is tens of seconds later or never. * Send the refresh notifications from their own goroutine, so a client that stopped reading cannot hold up the refresh loop. The web UI: * Add the storage page: the medium list with mount and eject, the file browser with search and sorting, uploads, downloads and deletes, and transfer progress. * Gate writing on the target machine owning the disk - image uploads and file uploads alike - instead of letting the server answer with an error the page has to surface, and drop the medium rows' download and delete buttons there too. * Give the shared folder, .iso and .img their own glyphs, and pick the file icon from the extension, so a listing can be read at a glance. * Name the partition the way the storage page does in the desktop menu as well, instead of showing the API's untranslated string. * Read the mounted medium in Settings - Device from the media state, now that the storage API it used is gone. * Tell an ISO 9660 image apart from a filesystem that cannot be read at all: the kernel of this device carries no iso9660 support (CONFIG_ISO9660_FS), so the contents of an ISO cannot be listed while the image itself is served to the target machine like any other medium. Localization: * Translate the new strings for every locale, keep the Settings - Device strings the read-only mounts added, and drop the keys the retired image menu and download page left behind. Chores: * Remove the desktop image menu, the download page and the API they used. * fix(storage): harden media file transfers Serialize medium deletion with gadget attachment and restore /data write access after failed mount transitions. Use available filesystem space for upload limits, refine virtual-device and shared-file handling, and update media path and installation test coverage. Pin an image while it is streamed out, so a writable mount cannot start between the check and the last byte; sweep stale upload temporaries from the whole data partition at startup, which the file manager itself cannot delete; and preflight URL downloads against the response size, which only the upload path did. Show dot files in the file browser again: only the two transfer temporary prefixes stay hidden, which is what the dot filter was for. Delete folders recursively after the web UI confirms, refusing when an attached or mounted medium lives inside the directory; accept every .iso and .img upload by name instead of sniffing volume descriptors; and show image and file transfers in one progress display at the top of the storage page. * fix(storage): make cache drops work and close delete and gadget races - drop_caches only accepts 1..4 on 5.10: write 4 once to silence the log, then 2/3. Writing 6/7 failed with EINVAL, so the shared folder never saw what the target machine wrote. - Resolve a delete path to its on-disk spelling (exFAT is case-insensitive) before checking it against the attached and loop-mounted media, refuse deleting a loop-mounted image or a folder holding an upload target, and fail closed when the disk state cannot be read. - GetMediaState no longer fails when the virtual disk is off and lun.0 does not exist; browsing works with the virtual disk off. - An explicit enable for the virtual disk compares against the disk being configured, so it leaves a served image alone; the settings toggle keeps the #937 semantics. - Restore /data write access after a failed gadget script or network switch. - eject clears the LUN file before resetting ro/cdrom, so a refused eject does not change the type of the medium still being served. - Mount loop images with MS_NOSYMFOLLOW and open files with O_NOFOLLOW. - Accept UDF images as optical media. - Only refuse deleting transfer temporaries while a transfer runs; sweep stale ones in the background, by exact name, and not on a read-only /data; hide them from listings. - Cap URL downloads without Content-Length at the free space and verify the received length. - Encode Content-Disposition filename* strictly per RFC 5987. - Restore image_test.go and add tests for the above. * fix(web): keep the file browser and transfers consistent on the storage page - Tag a listing with the medium and path it belongs to, so entries of the previous directory are never shown or acted on under a new path. - Poll the desktop storage menu only while it is open. - Show a dedicated message for a checksum mismatch. - Bring back the USB / SCP / TF card hints behind a help button. - Pick up a transfer already running when the page opens; make uploads cancellable; track concurrent uploads separately; let a failed job be dismissed; show a success state and guard the progress display by task. - Handle request errors that were left unhandled. - Dedicated confirmation for deleting a folder recursively. - Treat UDF images as optical, send credentials on the image download pre-check, translate the remaining Hungarian string. --------- Co-authored-by: Guoguo <i@qwq.trade>
NanoKVM Server
This is the backend server implementation for NanoKVM.
For detailed documentation, please visit our Wiki.
Structure
server
├── common // Common utility components
├── config // Server configuration
├── dl_lib // Shared object libraries
├── include // Header files for shared objects
├── logger // Logging system
├── middleware // Server middleware components
├── proto // API request/response definitions
├── router // API route handlers
├── service // Core service implementations
├── utils // Utility functions
└── main.go
Configuration
The configuration file path is /etc/kvm/server.yaml.
# Network Settings
proto: http # Access protocol. Can be changed to `https` only when certificates are configured. Default is `http`
host: "" # The listening address for the HTTP/HTTPS service. If left empty, all network interfaces will be bound
port:
http: 80 # The listening port for the HTTP service. Default is `80`
https: 443 # The listening port for the HTTPS service (effective when HTTPS is enabled). Default is `443`
cert:
crt: server.crt # The path to the public key certificate for HTTPS
key: server.key # The path to the private key file for HTTPS
# Logging Configuration
logger:
level: info # Global log output level. Evaluated options from highest to lowest detail: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. Default is `info`
file: stdout # Log output destination. `stdout` outputs to the standard console. A file path directs log output to that file. Default is `stdout`
# Authentication & Security
authentication: enable # Whether to enable identity verification for HTTP API and Web endpoints. Options are `enable` or `disable`. Default is `enable`. Highly recommended to leave this enabled for internet-facing devices!
jwt:
secretKey: "" # The secret key used to sign and verify JWT Tokens. If left empty, a random key will be generated automatically on startup
refreshTokenDuration: 2678400 # The token refresh duration threshold in seconds before forcing a re-login. Default is `2678400` (~31 days)
revokeTokensOnLogout: true # Whether logout invalidates all sessions belonging to that user. Other users are never logged out. Setting this to false only clears the browser cookie and is not recommended. Default is `true`
security:
loginLockoutDuration: 0, # The duration (in seconds) to ban an IP from attempting to log in again after reaching the failure limit. If set to `0` or left empty, brute-force protection is disabled. Default is `0`
loginMaxFailures: 5, # The maximum number of continuous failed login attempts allowed per IP before triggering protection. Default is `5`
# WebRTC Traversal Settings
stun: stun.l.google.com:19302 # The default STUN server address used for NAT hole-punching to establish P2P streams
turn:
turnAddr: example_addr # The relay (TURN) server address (format `ip:port`) used as a fallback when P2P connection fails. Leave empty to disable TURN relay
turnUser: example_user # The username required for authorization to the TURN server
turnCred: example_cred # The credential/password required for authorization to the TURN server
Web Users
NanoKVM uses two device-wide roles:
admin: KVM access plus user, system, network, update, storage, terminal, script, MCP, and PicoClaw administration.user: KVM video, keyboard, mouse, paste, power/reset, and Wake-on-LAN access.
Administrators manage accounts from Settings > Account. Account data remains in
/etc/kvm/pwd; the server migrates the legacy single-account JSON format in place and writes
the multi-user format atomically with mode 0600. Keeping the same path preserves the physical
BOOT-button password reset behavior.
Users must confirm their current password when changing it themselves; administrators can reset
non-owner users from the account manager. Only the device owner can change the device owner's
password, because that password is also synchronized to the Linux root account.
All authenticated sessions are backed by the current account state. Disabling, deleting, changing the role or password of a user invalidates that user's HTTP and real-time connections without affecting other users. Multiple users may watch and control the KVM concurrently; input uses the existing cooperative HID coordinator, so simultaneous input can interleave. Video mode, quality, resolution, and MJPEG frame-detection controls remain shared KVM operations; when several users adjust them concurrently, the latest change applies device-wide.
Compile & Deploy
Note: The manual steps below require a Linux x86-64 host with Go 1.25 or newer; they are not compatible with ARM, Windows or macOS. With Docker you can skip them entirely and use the containerized flow instead — the root Makefile (make shell) or the dev container (see "Development" in the root README) — which works on any host OS; run server/build.sh inside the container for a release-equivalent build.
-
Install the Toolchain
- Download the toolchain from the following link: Download Link.
- Extract the file and add the
host-tools/gcc/riscv64-linux-musl-x86_64/bindirectory to your PATH environment variable. - Run
riscv64-unknown-linux-musl-gcc -v. If there is version information in the output, the installation is successful.
-
Compile the Project
- Run
cd serverfrom the project root directory. - Run
go mod tidyto install Go dependencies. - (Optional) If you compiled
libkvm.soyourself, you need to modify its RPATH bypatchelf --add-rpath \$ORIGIN ./dl_lib/libkvm.so. - Run
CGO_ENABLED=1 GOOS=linux GOARCH=riscv64 CC=riscv64-unknown-linux-musl-gcc CGO_CFLAGS="-mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d" go buildto compile the project. - After compilation, an executable file named
NanoKVM-Serverwill be generated.
- Run
-
Modify RPATH
- Run
sudo apt install patchelforpip install patchelfto install patchelf. - Run
patchelf --version. Ensure the version is 0.14 or higher`. - Run
patchelf --add-rpath \$ORIGIN/dl_lib NanoKVM-Serverto modify the RPATH of the executable file.
- Run
-
Deploy the Application
- File uploads requires SSH. Please enable it in the Web Settings:
Settings > SSH; - Replace the original file in the NanoKVM
/kvmapp/server/directory with the newly compiledNanoKVM-Server. - Restart the service on NanoKVM by executing
/etc/init.d/S95nanokvm restart.
- File uploads requires SSH. Please enable it in the Web Settings:
Manually Update
File uploads requires SSH. Please enable it in the Web Settings:
Settings > SSH;
- Download the latest application from GitHub;
- Unzip the downloaded file and rename the unzipped folder to
kvmapp; - Back up the existing
/kvmappdirectory on your NanoKVM, then replace it with the newkvmappfolder; - Run
/etc/init.d/S95nanokvm restarton your NanoKVM to restart the service.