Skip to content

SynVirt Web Console v2 (VNC)

Synced read-only from /home/synnet/mirrors/synvirt-product/docs/WEB_CONSOLE_V2.md. Edit at the source, not here.

Document version: 0.9.0-alpha.2 · Module: daemon console router + crates/console-gateway (VNC bridge) + crates/console-ticket.

Supersedes the multi-protocol design captured in earlier revisions of this document (RDP / VNC / SPICE / Serial recommendation engine). SPICE was removed in 0.9.0-alpha.2; this document now describes the single VNC path.


The 0.8.x console module shipped SPICE everywhere: per-VM Unix sockets under /run/synvirt/spice/, a vendored spice-html5 client in the dashboard, spicevmc channels in every domain XML, QXL as the default video device, and a console-gateway whose only job was to bridge a WebSocket to a SPICE Unix socket.

All of that is gone. The web console is now VNC-only:

  • console-gateway is a WebSocket↔TCP-socket bridge for VNC.
  • The dashboard ships noVNC (BSD-2-Clause) — no spice-html5.
  • Domain XML carries <graphics type='vnc' listen='127.0.0.1' port='-1'> and <video><model type='virtio'/> (or std-vga for legacy guests). <channel type='spicevmc'> and <redirdev> blocks are removed. The QXL VideoModel variant is gone.
  • Guest-tools ISO no longer carries spice-guest-tools / spice-vdagent. It ships virtio-win drivers and SynVirt agents only.

There is no recommendation engine. There is no protocol negotiation. The browser hits /api/console/{uuid} to mint a ticket, then opens a WebSocket to /console/vnc/{uuid}?ticket=…. One path.

WebRTC remains a post-1.0 evaluation candidate — as a possible successor to VNC, not as an additional coexistence path.


┌──────────────────────┐ ┌────────────────────────┐ ┌──────────────────┐
│ Browser │ │ synvirt-daemon │ │ qemu-kvm (per-VM)│
│ noVNC (vendored) │ │ console-gateway │ │ │
│ │ │ ├─ ticket store │ │ VNC listener │
│ WSS /console/vnc/ │ │ ├─ axum router │ │ on 127.0.0.1: │
│ {uuid} │◄──►│ │ POST /api/ │◄──►│ 5900+N │
│ ?ticket=… │ │ │ console/ │ │ │
│ │ │ │ GET /console/ │ │ (port chosen by│
│ │ │ │ vnc/ │ │ libvirt at │
│ │ │ └─ bridge pump │ │ domain start) │
└──────────────────────┘ └────────────────────────┘ └──────────────────┘

The VNC listener address is resolved per VM via libvirt’s vncdisplay query. It is host-local, never network-exposed.


POST /api/console/{vm_uuid} → 200 { ticket, expires_in: 30 }
GET /console/vnc/{vm_uuid} → 101 Switching Protocols (WebSocket)
query: ticket=<JWT>

Tickets are HS256, single-use, 30-second TTL, with a 24-hour HMAC key rotation and 30-minute grace window. The ticket store is extracted into crates/console-ticket/ so any future protocol gateway can reuse it; the only aud claim today is console.

The legacy /api/v1/vms/:name/console/recommended-backend endpoint that earlier drafts of this document specified is not shipped. There is one backend; recommendation is a no-op.

Two legacy endpoints predate the noVNC bridge above:

  • POST /api/v1/auth/ticket — issues a short-lived dashboard ticket against PAM Basic Auth.
  • GET /api/v1/vms/:name/console/vnc — name-keyed WebSocket bridge to the VM’s VNC display.

Both are name-keyed and apply a strict valid_name() check on the :name segment: it must start with an ASCII lowercase letter and contain only ASCII lowercase letters, digits, and hyphens, length 1-63. Any other character returns a 400 before reaching libvirt — the restriction matches synvirt_libvirt::vm::validate_name so the name is safe to splice into a subprocess argv without quoting.

The new /console/* paths above (POST /api/console/{vm_uuid} and GET /console/vnc/{vm_uuid}) are UUID-keyed and have no valid_name() restriction — UUIDs are validated by their format, not their character set.


crates/web-ux-v2/src/components/console/ carries:

  • A noVNC wrapper that opens the WebSocket and renders to a canvas.
  • A pop-out window for multi-monitor setups.
  • Connection-status badges (connecting / connected / disconnected).

The legacy crates/web-ux/public/spice/ tree, the ConsoleModal.js SPICE client, and the spice-html5 vendored sources have all been removed.


Crate Role
console-gateway WebSocket ↔ TCP pump, axum routes, libvirt vncdisplay resolution.
console-ticket Shared ticket store (HS256 + dashmap nonce + key rotation).

rdp-gateway, vnc-gateway (separate crate), serial-gateway, and the recommendation router that earlier drafts proposed are not in scope. The single VNC path lives in console-gateway.


  • Multi-protocol coexistence (RDP, SPICE, WebRTC, serial) on the same gateway. WebRTC is a successor candidate, not an addition.
  • Per-VM preferred_backend policy. There’s only one backend.
  • Recording console sessions. Audit logs name the operator + VM + start/stop times only.
  • Cross-host migration of an open session. The session drops and re-establishes against the new host.

Existing VMs whose domain XML still carries <graphics type='spice'> continue to “work” in the libvirt sense — libvirt accepts the XML, QEMU starts, the VM runs. But the dashboard no longer offers a SPICE console: the only console button mints a VNC ticket and opens /console/vnc/{uuid}. If the domain doesn’t also carry a VNC listener, the bridge fails fast with vncdisplay returned no display.

The migrator’s video_migration pass rewrites legacy SPICE + QXL domains in place: it strips the <graphics type='spice'> / <channel type='spicevmc'> / <redirdev> blocks, swaps QXL to virtio-vga (or std-vga for Win9x / NT / 2000), and adds a <graphics type='vnc' listen='127.0.0.1' port='-1'> listener. Operators can also re-define a VM by hand to refresh its XML — see docs/UPGRADE_NOTES.md § 0.9.0-alpha.2.


End of WEB_CONSOLE_V2.md.