SynVirt Architecture
Synced read-only from
/home/synnet/mirrors/synvirt-product/docs/architecture.md. Edit at the source, not here.
SynVirt Architecture
Section titled “SynVirt Architecture”Version: 0.15.0 (0.15.0 line) Last updated: 2026-06-20
⚠️ Stale design snapshot. This document was written at the 0.7.6 (Hito 0.7) foundation and has not been re-baselined; significant parts no longer describe 0.15.0. The control plane is no longer a single monolithic daemon — data planes
synvirt-network(R1) andsynvirt-storaged(R2) are separate systemd services the daemon proxies oversynvirt-ipc(UDS); the canonical dashboard isweb-ux-v2(not the legacyweb-uxdescribed below); disks are RAW ZFS zvols (not qcow2); and LINSTOR/DRBD is an unbuilt future tier, not the shipped storage. Storage-plane ownership was amended 2026-07-01 (backup-P3 signature):synvirt-storagedis the control plane for storage object lifecycle (pool create/delete/rename/list + registry); streaming data-plane work (zfs send/recv) executes in the owning daemon (migrator precedent, nowsynvirt-backupd) — see docs/BACKEND_CONVENTIONS.md §External command execution. For current state use CLAUDE.md §2/§5 and docs/architecture/refactor-roadmap.md.
Table of Contents
Section titled “Table of Contents”- System Overview
- Operational Modes
- Daemon Design
- synvirt-libvirt crate
- Dashboard (web-ux) data flow
- Roadmap (Hitos)
- Technology Stack
- Network Topology
- Licensing Model
System Overview
Section titled “System Overview”SynVirt is a hypervisor platform designed for SMBs and MSPs. Its architecture
centers on a single Rust daemon (synvirt-daemon) that acts as the control
plane, serving both the management API and the web interface over HTTPS from
a single binary.
The platform ships as a live ISO built with live-build on top of Debian 13
Trixie (kernel 6.12 LTS). On boot the kernel runs the system in RAM with
synvirt-daemon already listening on port 443; the operator drives the entire
install from a web wizard talking to /api/install/*, then the same daemon
continues into post-install service without a handoff. The daemon manages the
full lifecycle of the host node: install orchestration, health reporting, VM
orchestration (Hito 2+), storage management (3 configurable modes today;
first-class module in Hito 3), and cluster coordination (Twin / Cluster from
October 2026 onward).
Browser / API client | | HTTPS :443 (prod) / :8443 (dev) v +------------------+ | synvirt-daemon | <-- single Rust binary | | | Axum router | | Rustls TLS | | Static assets | | REST API | +--------+---------+ | | (future phases) v +------------------+ +------------------+ | KVM / Cloud | | LINSTOR + DRBD | | Hypervisor | | over ZFS zvols | +------------------+ +------------------+Operational Modes
Section titled “Operational Modes”SynVirt supports three modes. The upgrade path between them is non-destructive: an existing Standalone node can be promoted to Twin or Cluster without reinstalling.
Standalone
Section titled “Standalone”A single node with no replication or HA. Intended for development, testing, and single-site SMB deployments where downtime is acceptable.
- 1 node
- Storage: operator-selected at install (
single_disk_zfs/multi_disk); persisted in/etc/synvirt/storage-mode - No live migration
- No quorum required
Two nodes plus a lightweight witness. VMs replicate synchronously via DRBD. If one node fails, the other takes over with minimal interruption.
- 2 active nodes + 1 witness (can be a small VM or cloud instance)
- Synchronous DRBD replication
- Live migration between the two nodes
- Witness provides quorum tie-breaking
- Intended for: 2-server SMB setups, primary + DR scenarios
Node 01 <---DRBD sync---> Node 02 | | +----------+----------+---+ | Witness (quorum only)Cluster
Section titled “Cluster”Three or more nodes with etcd providing distributed consensus. The scheduler places VMs across nodes based on resource availability. Any minority of nodes can fail while the cluster remains operational (N/2+1 quorum).
- 3+ nodes (odd number recommended for quorum)
- etcd Raft for cluster state and leader election
- Distributed VM scheduling
- Full HA: any single node failure is transparent
- Intended for: MSPs, multi-tenant environments, larger SMBs
+--------+ +--------+ +--------+ | Node 1 | | Node 2 | | Node 3 | | | | | | | | etcd | | etcd | | etcd | | daemon | | daemon | | daemon | +---+----+ +---+----+ +---+----+ | | | +------etcd Raft quorum-----+ | | | +-------DRBD replication----+Mode Upgrade Path
Section titled “Mode Upgrade Path”Standalone --> Twin --> Cluster(1 node) (2+witness) (3+ nodes)
Add nodes and reconfigure; no reinstall required.Daemon Design
Section titled “Daemon Design”Binary
Section titled “Binary”synvirt-daemon is a single compiled Rust binary. It embeds the HTTP router,
TLS termination, and all API handlers. There is no separate web server process.
TLS is handled natively by Rustls (no OpenSSL dependency). A self-signed
certificate is generated at daemon startup and stored in /etc/synvirt/tls/
(prod) or ~/.synvirt/tls/ (dev). The certificate’s SANs cover hostname,
localhost, and all global IPv4 addresses detected on the host. In production
deployments, operators can supply their own certificate files post-setup.
- Dev mode: self-signed, regenerated on each start if absent
- Prod mode: certificate and key paths configurable via config file or env vars
HTTP Endpoints
Section titled “HTTP Endpoints”| Method | Path | Description |
|---|---|---|
| GET | / |
Web installer wizard (pre-setup) or dashboard (post-setup), chosen by root-selection logic |
| GET | /api/v1/health |
JSON health report |
| GET | /api/v1/info |
Node info (hostname, version, mode) |
| GET | /api/v1/setup/status |
Setup phase state (needs_setup / has_admin / has_mode) |
| POST | /api/v1/setup/auth |
Exchange 4-digit setup token for a Setup session |
| POST | /api/v1/setup/complete |
Mark setup done, write /etc/synvirt/setup.completed, mint Admin session |
| GET | /api/hardware, /api/disks, /api/nics |
Discovery for the wizard |
| POST | /api/install/start |
Kick off install timeline with storage_mode + config |
| GET | /api/install/status |
Poll install stage + progress |
| POST | /api/install/reboot |
Reboot into the newly installed system |
| GET | /healthz |
Kubernetes-style liveness probe |
| GET | /readyz |
Kubernetes-style readiness probe |
Configuration Precedence
Section titled “Configuration Precedence”Configuration is resolved in this order (later overrides earlier):
- Compiled-in defaults
/etc/synvirt/synvirt.toml(system config file)- Environment variables (
SYNVIRT_*) - Command-line flags (
--dev,--port, etc.)
Dev Mode (--dev flag)
Section titled “Dev Mode (--dev flag)”When started with --dev:
- Listens on port 8443 instead of 443
- Serves static assets from
./web/(relative to binary) instead of/opt/synvirt/web/ - Enables pretty (human-readable) log formatting
- Relaxes TLS certificate validation for localhost
Static Assets
Section titled “Static Assets”The daemon picks one of three web roots at startup based on install phase
(crates/daemon/src/main.rs):
| Phase | Marker | Web root |
|---|---|---|
Dev mode (--dev) |
SYNVIRT_DEV=1 or CLI flag |
daemon/assets (relative) |
| Post-setup | /etc/synvirt/setup.completed exists and /opt/synvirt/web-ux/ staged |
/opt/synvirt/web-ux/ (dashboard) |
| Pre-install / pre-setup | /opt/synvirt/web-installer/ staged |
/opt/synvirt/web-installer/ (wizard) |
| Legacy fallback | Neither staged | /opt/synvirt/web/ |
Assets today are plain HTML + vanilla JS + CSS. The Next.js 15 migration is
scheduled for Hito 1 (late April 2026) and will replace /opt/synvirt/web-ux/
first, leaving the wizard on vanilla JS until it burns.
systemd Integration
Section titled “systemd Integration”The daemon ships a hardened systemd unit:
[Service]Type=simpleExecStart=/opt/synvirt/bin/synvirt-daemonRestart=on-failureNoNewPrivileges=yesProtectSystem=strictProtectHome=yesReadWritePaths=/etc/synvirt /var/lib/synvirt /var/log/synvirtRoadmap (Hitos)
Section titled “Roadmap (Hitos)”Authoritative schedule with deliverables and dates: roadmap.md.
Short summary from the architecture perspective:
- Hito 0 / 0.2 / 0.3 (completed) — foundation MVP, Ratatui TUI, Debian 13 Trixie migration with ext4 rpool + ZFS data pool.
- Hito 0.4 (completed, April 2026) — unattended debconf apply, OVS
br-mgmt, Direct Console UI, setup wizard on/api/v1/setup/*, basic VM CRUD viavirsh. - Hito 0.5 (completed, April 2026) — live-build ISO replaces preseed + TUI; daemon drives install via
/api/install/*. - Hito 0.6 (completed, April 2026) — 2 storage modes (
single_disk_zfs/multi_disk), verbose streaming installer, netplan DHCP fallback for uplink-less boots, diagnosticsynvirt/debuguser with auto-cleanup, UX dev server with fixtures, dashboard stub. - Hito 1 (late April 2026) — Next.js 15 dashboard, admin login, session management; wizard stays vanilla JS for now.
- Hito 2 (May 2026) —
module-virtwith KVM lifecycle + console. - Hito 3 (June 2026) —
module-storagewith ZFS + LINSTOR + ZFS-on-root with boot environments (see Issue 001). - Hito 4 (July 2026) —
module-networkwith OVN + nftables. - Phase 1 complete (Aug–Sep 2026) — hardening pass, CLI, local auth, audit log, operator docs.
- Twin mode (October 2026) — 2 nodes + witness + DRBD over zvols, live migration.
- Cluster mode (November–December 2026) — 3+ nodes, embedded etcd Raft, distributed scheduler.
- GA (Q1 2027) — multi-tenancy, LDAP/AD, RBAC, commercial support tier.
Technology Stack
Section titled “Technology Stack”| Component | Technology | Rationale |
|---|---|---|
| Base OS | Debian 13 Trixie minimal (kernel 6.12 LTS) | LTS, strong KVM ecosystem, OpenZFS 2.4.x from backports |
| Control plane | Rust 1.95 (stable) | Memory safety, no GC pauses, single binary |
| HTTP framework | Axum | Async, ergonomic, first-class Rustls support |
| Async runtime | Tokio | De-facto standard for async Rust |
| TLS | Rustls | Pure Rust, no OpenSSL dependency |
| Web UI (current) | Plain HTML + vanilla JS | Zero build toolchain, wizard iterable via Python dev server |
| Web UI (Hito 1+) | Next.js 15 + shadcn/ui | Dashboard rewrite; wizard migrates later when it burns |
| Hypervisor | KVM + Cloud Hypervisor (Hito 2) | Linux-native, open standard, no license cost |
| Block replication | DRBD + LINSTOR (Twin mode, Oct 2026) | Battle-tested synchronous replication |
| Local data pool | ZFS (operator-configurable: single_disk_zfs / multi_disk) | CoW snapshots, checksums, RAID-Z, feeds DRBD zvols |
| Boot environments | ZFS-on-root (Hito 3, see Issue 001) | Atomic rollback on failed OS upgrades |
| Cluster consensus | etcd (Cluster mode, Nov–Dec 2026) | Proven Raft implementation |
| SDN | OVN + nftables (Hito 4) | Open standard, native Linux networking |
| ISO builder | live-build + xorriso |
Live ISO with daemon running in RAM; wizard drives install via HTTPS |
| Dev build env | Debian 13 native + Rust toolchain | Same distro as the shipping live-ISO runtime |
Network Topology
Section titled “Network Topology”Standalone
Section titled “Standalone” Internet / LAN | [ Management NIC ] | synvirt-daemon :443 | Web browserTwin / Cluster (Hito 2+)
Section titled “Twin / Cluster (Hito 2+)”Each node will have at least two physical NICs:
| NIC | Purpose | Notes |
|---|---|---|
| eth0 (management) | HTTPS management access, VM traffic | Public-facing |
| eth1 (storage/sync) | DRBD replication, live migration | Private, dedicated link |
In Cluster mode a third NIC for etcd heartbeat traffic is recommended to isolate consensus traffic from VM data traffic.
Licensing Model
Section titled “Licensing Model”SynVirt is licensed commercially, tiered by CPU socket count.
| Tier | Sockets | Target |
|---|---|---|
| Starter | 1 socket | Small office, single server |
| Professional | 2 sockets | SMB primary + DR |
| Business | Up to 8 sockets | Growing SMB, small MSP |
| Enterprise | Unlimited sockets | MSP, multi-tenant |
Licenses are node-bound and activated online. Offline activation is available for Enterprise tier. Trial licenses (30 days, Starter limits) are available through the SYNNET portal at synnet.mx.
synvirt-libvirt crate
Section titled “synvirt-libvirt crate”Introduced in 0.7.0 as the canonical wrapper around libvirt/QEMU-KVM for every internal consumer (daemon REST, Console TUI VM screens in future releases, CLI, VirtDCM). Replaces ad-hoc virsh calls scattered across the daemon.
crates/synvirt-libvirt/├── Cargo.toml├── README.md└── src/ ├── lib.rs public surface re-exports ├── error.rs Error enum (thiserror, coded variants) ├── client.rs Client handle (qemu:///system URI) ├── vm.rs VM lifecycle (create/start/stop/delete/list) ├── disk.rs qcow2 disk allocation via qemu-img ├── nic.rs virtio NIC attach on OVS bridge br-vm ├── host.rs host inspection (capabilities, CPU, RAM) ├── xml.rs domain XML renderer (q35, virtio, VNC) ├── virsh.rs subprocess runner with timeout + parsing └── strings.rs user-facing error and log stringsBackend: virsh subprocess invoked via tokio::process::Command with a
30-second timeout per call. Chosen for transparency (every action is
traceable in journalctl) and zero native build dependencies. A future
migration to the virt FFI crate is possible behind the same public
API without breaking consumers.
Public surface is async; all blocking work runs inside
tokio::task::spawn_blocking. Every public function emits a
tracing::instrument(skip_all) span with structured fields (vm_name,
uuid, phase) so a request can be followed across the daemon → crate →
virsh boundary.
Dashboard (web-ux) data flow
Section titled “Dashboard (web-ux) data flow”The post-install dashboard is a single-page Vue 3 app served from
/opt/synvirt/web-ux/. All network I/O goes through api.js, which
attaches the Basic Auth header from sessionStorage and routes every
response through the error translator in assets/error-messages.json.
Operator creates a VM
Section titled “Operator creates a VM”Browser (VmCreateModal.js) → api.createVm({name, cpu, ram, disk_gb, iso, network}) → POST /api/v1/vms (Basic Auth) → daemon::api::vms::create → synvirt_libvirt::vm::create_vm → disk::allocate_qcow2 (qemu-img create -f qcow2) → xml::render (q35 + virtio + OVS + VNC) → virsh::define (virsh define <xml>) → virsh::start (optional) (virsh start <name>) ← Vm { name, uuid, state, ... } ← 201 Created + Vm body → store.vms.push(vm) + toast "VM created"Operator starts a VM
Section titled “Operator starts a VM”Browser (VmDetail.js: Start button) → api.startVm(name) → POST /api/v1/vms/:name/start → daemon::api::vms::start → synvirt_libvirt::vm::start (virsh start) ← 200 OK → optimistic state flip to "running" → 2s poller confirms via GET /api/v1/vms/:nameDashboard load (home screen)
Section titled “Dashboard load (home screen)”On mount, DashboardHome.js fires three parallel GETs:
Promise.all([ api.health(), // GET /api/v1/health -> uptime, cpu, ram api.listVms(), // GET /api/v1/vms -> VM count + states api.listPools(), // GET /api/v1/storage -> pool usage])All three are gated by Basic Auth middleware. A 401 on any of them
clears sessionStorage and redirects to #/login. The five-second rule
(see docs/FRONTEND_PRINCIPLES.md) requires that all three cards reach
“ready” or “failed” state within 5 seconds of mount; slower responses
render a skeleton placeholder per the loading-latency tiers.
Settings module
Section titled “Settings module”The Settings module is the host-configuration surface on the v2
dashboard. It is rolled out in phases — each section ships as an
independently mergeable bundle that replaces a PlaceholderPanel
stub on the /settings view.
Cross-cutting conventions
Section titled “Cross-cutting conventions”- HTTP shape. Every endpoint lives under
/api/v1/settings/...and returns a{ data, warnings, meta }envelope (seecrates/daemon/src/settings/envelope.rs). Errors keep the daemon-wideErrorEnvelopeshape with stableE_*codes. - Auth. PAM-backed Basic Auth via the existing daemon middleware.
- Tracing. Targets are
synvirt.settings.<section>for runtime events andsynvirt.migration.<section>for first-boot migrations. - Audit. Mutating calls write to the sled tree
audit/settings(DB at/var/lib/synvirt/settings-audit.db). Each record is{ ts_ms, actor, endpoint, body_hash, outcome, before, after }; the constantsynvirt_time::audit::AUDIT_TREEis shared by every settings section so phase 3+ writes land in the same place.
Time configuration (Phase 2)
Section titled “Time configuration (Phase 2)”| Endpoint | Purpose |
|---|---|
GET /api/v1/settings/time |
Snapshot: timezone, sync flag, sources, chrony tracking, host wall-clock as host_unix_ts_ms. |
PUT /api/v1/settings/time |
Transactional apply with rollback. |
POST /api/v1/settings/time/manual |
Set the wall clock when sync is disabled. Surfaces a W_TIME_LARGE_JUMP warning when ` |
POST /api/v1/settings/time/refresh |
chronyc -a 'burst 4/4' then makestep. |
- Daemon. chrony only (no timesyncd). Subprocess calls are
abstracted behind
synvirt_time::exec::Execso the apply path is unit-testable; the production impl isTokioExecshelling out tochronycandtimedatectl. - Persistence. Source of truth for NTP sources, timezone, and the
sync flag is the new
[time]section of/etc/synvirt/synvirt.toml. The daemon renders/etc/chrony/sources.d/synvirt.sourceson every apply and on first-boot migration. Sled is not used for time config — only for the audit trail. - Defaults.
time.cloudflare.com(server) +pool.ntp.org(pool), both with iburst; sync enabled; timezoneUTC. The defaults stay until the operator removes them through the UI. - Live host clock on the UI. The browser computes
offset = host_unix_ts_ms - Date.now()once on mount and on every mutation response, then rendersnew Date(Date.now() + offset)on a 1Hz interval. Visibility-aware: paused while the tab is hidden, re- syncs when it becomes visible again. - Migration. Idempotent first-boot step
(
synvirt_time::migrate::run):- Stop / disable / mask
systemd-timesyncd.serviceif active. - Enable + start
chrony.service. - Ensure
/etc/chrony/chrony.confcarriessourcedir /etc/chrony/sources.d. - Seed
[time]from existing chrony sources or fall back to the defaults. - Render
/etc/chrony/sources.d/synvirt.sources. chronyc reload sources(best effort).
- Stop / disable / mask
System Resource Reservation (Phase 3)
Section titled “System Resource Reservation (Phase 3)”| Endpoint | Purpose |
|---|---|
GET /api/v1/settings/reservation |
Snapshot: mode, reserved + total memory/CPU, recommended values, manual-mode limits. |
PUT /api/v1/settings/reservation |
Switch mode and (when manual) store explicit memory/CPU values. Validated against 1 GiB / 0.5 GHz floors and the 50% host-total ceiling. |
POST /api/v1/settings/reservation/auto |
Convenience: equivalent to PUT { "mode": "auto" }. |
- Crate.
synvirt-resourcesprovides the auto formula, the manual-mode bounds, the/proc/meminfo+/proc/cpuinfoprobes, and theCephProbetrait (dormant until storage-ceph). The auto formula ismax(2 GiB, 6% RAM)+1 GiB × osd_countfor memory andmax(1.0 GHz, 5% CPU)+0.5 GHz × osd_countfor CPU. - Persistence.
[resources.reserved]in/etc/synvirt/synvirt.toml. No rendered drop-ins, no daemon restarts. Themode = "auto"form strips manual keys on save so the file stays minimal. - Hardware probe. Cached in a
tokio::sync::OnceCellfor the daemon’s lifetime; CPU hotplug is ignored in 0.x. Test seam:HardwareProbetrait +StaticProbefor unit tests. - Audit. Reuses the
audit/settingssled tree opened inmain.rs; no second DB. - Out of scope. Scheduler enforcement (lands with
synvirt-vm), live Ceph OSD probe (stub returns 0), NUMA-aware reservation, per-pool storage reservation.
Services (Phase 4a — read-only)
Section titled “Services (Phase 4a — read-only)”| Endpoint | Purpose |
|---|---|
GET /api/v1/settings/services |
Curated catalog with full per-unit state. |
GET /api/v1/settings/services/:unit |
Single-unit detail. 404 E_SERVICES_NOT_MANAGED for off-allowlist names. |
GET /api/v1/settings/services/:unit/journal?lines=<n> |
Tail (1-1000) of journald log, scoped to the unit. |
- Allowlist. Hardcoded in
synvirt-services::allowlist. Adding a unit requires a code review. Order matters — the UI renders the list in declaration order.ceph.targetis gated by edition and hidden in standalone. - D-Bus.
zbus::Connection::system()opens once inServicesService::connect()at daemon startup and is shared across calls. The traitSystemdClientexposesunit_state(spec); production implZbusSystemdClientcallsLoadUnit+GetAllagainstorg.freedesktop.systemd1.{Manager,Unit,Service}. A D-Bus failure on a single unit degrades that row toload_state = "not-found"without failing the whole list. - Capabilities (
can_*). Pure function over(spec, raw_state).synvirt-api.servicealways deniesstopanddisable(recovery path usesrestart). Targets do not enable / disable. Not-installed units have every capability off. - Journal. Subprocess
journalctl --no-pager --output=json --unit <u> --lines <n>via the existingExectrait.MESSAGEfields that arrive as byte arrays (non-UTF-8 originals) are coerced to UTF-8 lossily so a single odd entry does not break the parser. Live tailing is out of scope for 4a. - Edition.
ServicesService::new(..., edition)filters the allowlist at construction. The licensing endpoint (Phase 9) rebuilds the service when the edition flips. - Mutations (Phase 4b). Five action endpoints on
/api/v1/settings/services/:unit/{start,stop,restart,enable,disable}plus a read-onlyrestart-impactpre-check. Each mutation is blocking (30 s wall clock) with the subscribe-first / issue-action / wait pattern againstManager.JobRemoved.synvirt-api.servicealways deniesstop/disable(defense-in-depth) and routesrestartthrough the oneshot helpersynvirt-self-restart.serviceso the daemon never kills its own HTTP response. Idempotent paths short-circuit without a D-Bus call. High-impact units (libvirtd.service,openvswitch-switch.service,synvirt-api.servicerestart,synvirt-console-gateway.servicewith active sessions) require typed confirmation (RESTART/STOP/DISABLE). Mutating calls write fullbefore/afterUnitStatesnapshots to the sharedaudit/settingssled tree. - Out of scope for 4a/4b. Live journal streaming, live unit-state
subscription via
PropertiesChanged(we re-fetch on each refresh), cgroup v1 fallback (assume v2 — Debian 12+), service templates, mass actions, mutation queueing.
Firewall (Phase 5)
Section titled “Firewall (Phase 5)”| Endpoint | Purpose |
|---|---|
GET /api/v1/settings/firewall |
Snapshot: enable flag, default policies, system rules, user rules, per-rule counters. |
PUT /api/v1/settings/firewall |
Toggle enable/disable. Disable requires confirmed: "DISABLE FIREWALL" server-side. |
POST /api/v1/settings/firewall/rules |
Add a user rule (server-assigned ULID). |
PUT /api/v1/settings/firewall/rules/:id |
Update a user rule. |
DELETE /api/v1/settings/firewall/rules/:id |
Remove a user rule. |
POST /api/v1/settings/firewall/rules/:id/move |
Move up or down within the user-rules array; clamped at boundaries. |
POST /api/v1/settings/firewall/test |
Dry-run validation of a hypothetical config. |
- Backend.
synvirt-firewallowns the nftables tableinet synvirt. Every apply is transactional: render →nft -c -f <tmp>(validate-only) →nft -f <tmp>(atomic kernel commit) → rename tmp into place. nftables transactions are atomic in the kernel, so there is no race window where the host is exposed. - System vs user rules. System rules are hardcoded by edition
- storage-ceph state and rendered before user rules. The list is
surfaced to the operator (read-only) so they understand what is
protecting them. User rules are CRUD + move;
comment + enabled flagare excluded from the duplicate-detection identity tuple so toggling enabled does not collide with itself.
- storage-ceph state and rendered before user rules. The list is
surfaced to the operator (read-only) so they understand what is
protecting them. User rules are CRUD + move;
- Counters.
nft list ruleset -jparses the per-rule counter expressions; matched by thecommentslot which carries thesys-*id (system rules) or the ULID (user rules).nft -jfailures degrade to zero counters without failing the GET. - Disable. Allowed under both editions; phrase always required
server-side. The standalone UI submits the phrase silently from a
simple confirm modal; the enterprise UI uses a type-to-confirm
field. Disabled state persists across reboots
(
/etc/nftables.d/synvirt.nftbecomes a placeholder, the kernel table is dropped). - Recovery. From SSH:
nft delete table inet synvirt. The daemon does not need to be involved. - Out of scope. Forward-chain rules (lands with
synvirt-net), NAT, masquerade, mark / conntrack matchers, per-VM firewall (libvirt nwfilter viasynvirt-vm), rate limiting, geo-IP.
System Swap (Phase 6)
Section titled “System Swap (Phase 6)”| Endpoint | Purpose |
|---|---|
GET /api/v1/settings/swap |
Snapshot: enabled flag, swappiness, devices from /proc/swaps, size limits derived from host RAM. |
POST /api/v1/settings/swap/create |
Allocate, format, and activate the swap file. |
PUT /api/v1/settings/swap |
Adjust swappiness only. |
DELETE /api/v1/settings/swap |
swapoff (5-minute wall timeout) + remove file + drop fstab/sysctl fragments. |
- Default state. Disabled. Fresh installs do not create a swap file.
- Backend.
synvirt-swapreuses the sharedExectrait. Create flow runsfallocate -l(falls back todd if=/dev/zeroon filesystems that reject preallocation),chmod 600,mkswap,swapon, then writes the fstab fragment and the sysctl drop-in. Each step is reversed on failure (swapoff,unlink, fragment removal). - Persistence.
synvirt.toml [swap]is canonical (enabled, size_bytes, swappiness, path). The fstab fragment at/etc/fstab.d/synvirt-swap.fstaband the sysctl drop-in at/etc/sysctl.d/99-synvirt-swap.confare rendered from it; both files are removed on disable. - Limits. 1 GiB floor, 16 GiB ceiling, hard reject above 50% of host RAM. Recommended swappiness when enabled is 1 (aggressive avoidance for hypervisor workloads); the kernel default of 60 is restored on disable.
- Disable.
swapoffis wall-clock-bounded at 5 minutes. On timeout the daemon returns 504 and leaves the file + fragments in place — partial cleanup is worse than no cleanup; the operator can retry or reboot. - Out of scope. zswap / zram, partition-backed swap, multiple swap devices, encrypted swap, hibernation integration, live resize without disable.
Software Packages (Phase 7)
Section titled “Software Packages (Phase 7)”| Endpoint | Purpose |
|---|---|
GET /api/v1/settings/packages |
Curated package state (updates available + counts + reboot flag). |
POST /api/v1/settings/packages/refresh |
Run apt-get update (5-minute throttle). Returns 202 + change_id. |
POST /api/v1/settings/packages/upgrade |
Targeted or security-only upgrade. Returns 202 + change_id. |
GET /api/v1/changes/:change_id/stream |
Cross-cutting SSE for any long-running operation. |
- Allowlist —
synvirt_packages::allowlist::PATTERNS. Matches by exact name or trailing*wildcard. Filters what is displayed and what an operator can explicitly request; APT is free to upgrade transitive dependencies. - Streaming —
apt-getis spawned with piped stdout/stderr, each line classified by the puresynvirt_packages::parser::classifyinto one ofupdate,download,install,cleanup,done, then broadcast to every subscriber on the change’stokio::sync::broadcastchannel. Tail of the last 50 lines is preserved on theCompleteEventfor failure surfacing. - Concurrency —
ChangeRegistrykeeps a single in-flight slot; a second start while the first is running returnsE_PACKAGES_UPGRADE_IN_PROGRESSwith the existingchange_idin the problem detail. The second client can subscribe to that id and tap the live stream. - Maintenance window — non-security upgrades refuse to start
while any VM is in a transitional state
(
migrating/snapshotting). Security-only upgrades proceed regardless. - Reboot detection — the daemon checks
/var/run/reboot-requiredon every GET; the dashboard’sRebootRequiredBanner(mounted inAppShell) polls and surfaces a sticky banner across every page when set. - Out of scope. APT source / repo management, third-party trust workflows, package holds / pinning, dist-upgrade / release upgrades, downgrade / rollback, in-daemon reboot orchestration, multi-host coordinated upgrades (VirtDCM 0.12+), reconnect to an in-progress operation after browser disconnect.
BMC (Phase 8a — connection-only)
Section titled “BMC (Phase 8a — connection-only)”| Endpoint | Purpose |
|---|---|
GET /api/v1/settings/bmc |
Snapshot: detection (nested / vendor / firmware), configuration, last connection state. |
PUT /api/v1/settings/bmc |
Save configuration. Tests credentials before persist; rejects with 400 if the test fails. |
POST /api/v1/settings/bmc/test |
Test credentials without persisting. Always returns 200 with the outcome enum. |
DELETE /api/v1/settings/bmc |
Remove the encrypted credential row. |
- Detection. Nested check first
(
systemd-detect-virt+ DMI product name); on bare metal,ipmitool mc infois parsed and theManufacturer IDdecimal is mapped against the IANA Private Enterprise Number registry (HPE 11, Dell 674, Lenovo 19046, Supermicro 47488 / 10876, Cisco 9, Huawei 2011, Fujitsu 211, Inspur 37945; legacy IBM 2 / 20301 alias to Lenovo). The result is cached for the daemon’s lifetime in anArc<RwLock<Option<DetectionResult>>>. - Connection test. Redfish first
(
GET https://<ip>/redfish/v1/with Basic Auth, 5 s timeout,danger_accept_invalid_certsdriven by thetls_verifyflag), IPMI LAN as fallback (ipmitool -I lan -H <ip> -U <u> -P <p> mc info). Both paths classify the failure intounreachable,auth_failed,timeout,tls_error, orunknown. - Storage. Credentials live in a sled tree
secrets/bmc(one row, keyactive). Plaintext is the JSON shape{ bmc_ip, username, password, protocol, tls_verify }; the encrypted blob isnonce || ciphertextunder AES-256-GCM. The 32-byte AES key is derived asSHA-256("synvirt-bmc-key-v1\0" || machine_id || "\0" || keyfile). The keyfile at/etc/synvirt/secrets.keyis auto-created with random bytes on first use, mode 0600. - Audit. Save / delete write
before/aftersnapshots with the password redacted as***. Connection tests do not audit. - Out of scope for 8a. Hardware inventory (chassis / BIOS / CPU / RAM / PSU / fans), live sensors (temperature / RPM / voltage / watts), vendor-specific data normalization, power actions (on / off / cycle), multiple BMCs, KCS device file detection beyond ipmitool’s own check. All of the above land in Phase 8b.
Out of scope for the Settings module (deferred across phases)
Section titled “Out of scope for the Settings module (deferred across phases)”- Cluster-wide time consensus and inter-node drift detection — the
cluster_drift_warningfield inGET /settings/timeis reserved for the cluster crate (0.10). - VirtDCM-driven time policy and RBAC beyond authenticated admin — arrives with VirtDCM enrolment (0.12).