Skip to content

Replace Linux workspace noVNC with Xpra HTML5 - #1141

Open
zardus wants to merge 22 commits into
masterfrom
replace-novnc-with-xpra
Open

Replace Linux workspace noVNC with Xpra HTML5#1141
zardus wants to merge 22 commits into
masterfrom
replace-novnc-with-xpra

Conversation

@zardus

@zardus zardus commented Sep 2, 2026

Copy link
Copy Markdown
Member

Why Xpra

  • Xpra includes its own HTML client and WebSocket server and handles reconnect and display resize, so the Linux workspace no longer needs Xvnc, websockify/noVNC, or our local noVNC reconnect patch.
  • It provides tested bidirectional Unicode clipboard support while keeping the same frameless XFCE desktop.

desktop-windows is unchanged.

Implementation and security

Xpra 6.4.4 and its HTML5 v20 client run on port 6080 inside the learner workspace. The listener uses auth=none and is not published as a public port. CTFd gives the authenticated requester an HMAC-signed bearer URL covering the workspace target and port; Nginx validates it before proxying. The existing network policy blocks direct peer-workspace connections. As with other workspace URLs, anyone who receives the signed URL can use it while its target exists. Requests using the API's top-level user= parameter remain admin-only.

The only CTFd change is 2 additions and 33 deletions: it removes the old Linux desktop password/view-only special case and sends service=desktop through the existing generic workspace route. There are no Xpra-specific CTFd dependencies, credentials, certificates, or Nginx routes.

Xpra does not inherit DOJO_AUTH_TOKEN. Command, shell, control, file-transfer, printing, open-file/open-URL, audio, and webcam features are disabled. The workspace service also serializes startup, checks readiness, removes legacy noVNC state, and recovers a recorded orphaned Xorg after an abrupt exit.

Normal HTML client settings use default-settings.txt; there is no substituteInPlace. Two small tracked patches cover failures reproduced during degraded-link testing:

  • keymap.sync=false leaves repeat handling with the browser. A focused poor-link probe completed exact held-key and Shift-modified sequences with no trailing keys.
  • The liveness patch raises the disconnect threshold from 15 to 60 seconds and the unhealthy-indicator grace from 2 to 30 seconds, retains the 5-second ping cadence, and makes v20 use its existing PING_GRACE setting. Successfully handled inbound packets affect only the soft warning; hard reconnect logic still depends only on ping echoes. This prevents active display traffic on a saturated 1 Mbit/s, 1% loss link from being mistaken for a dead server. After at least one successful ping, a completely silent connection is marked unhealthy after roughly 30–35 seconds and triggers reconnect after roughly 60–65 seconds; the new attempt begins about one second later. An explicit WebSocket close enters the reconnect path without waiting for those timers.

Verification

  • Final product commit: 28539e9f9a9ca1975ddf4aedf6fcb30730559ca0
  • Final PR head: f507883428bf9f69c7d39f7de38afa7d8f8e2ff4 (the additional commit only hardens benchmark evidence preservation, validation, and aggregation)
  • nix build --no-link --print-out-paths ./workspace#core: passed; the closure contains Xpra and no noVNC/TigerVNC
  • Focused regressions cover the signed target+port route and tamper rejection, admin-only top-level cross-user requests, absence of a VNC listener, concurrent Xpra clients, Unicode clipboard, and abrupt-exit recovery
  • GitHub Actions on the final PR head: contract, semantic, integration, multinode topology, and Codecov passed
  • Poor-link liveness diagnostic: at 150 ms delay, 1% loss, and 1 Mbit/s in each direction, all 440 samples in a 110.016-second trace stayed connected with one stable canvas and client UUID, no warning overlay, and no reconnect while a 100-second workload completed. The runner then exited 1 when an optional server-info request timed out; the full trace, hooks, and five screenshots had already been saved. The server log showed damage acknowledgements delayed by as much as 62 seconds. This is one saturation test, not a blackhole test or statistical claim.
  • Xpra functionality matrix: 12/12 user-visible trials passed, with 240/240 trusted, exact-input latency samples. Ten of 12 traffic intervals fully drained; two poor-link intervals are censored below.

Every trial checked connection, a visible frameless desktop, resize/restore, trusted remote input, exact terminal input, a 30-second/300-update workload, sampled browser delivery, recovery after a two-second client-interface interruption, and pre/post-workload quiescence. Xpra trials also checked server-confirmed resize and both Unicode clipboard directions; the noVNC resize check covered the changed browser surface rather than remote X/RandR state. The clipboard probe used X11 xclip, Xpra's internal buffer, and a mocked browser Clipboard API, so it covers the Unicode protocol/X11 round trip rather than browser permission UI or the host OS clipboard. Clipboard was not supported by the noVNC harness and was not counted among its common checks.

Profile One-way delay Nominal RTT contribution Rate cap each way Loss each way noVNC common checks Xpra checks incl. clipboard
Clean 0 ms 0 ms none 0% 3/3 3/3
WAN 25 ms 50 ms 20 Mbit/s 0% 3/3 3/3
Mobile 75 ms 150 ms 4 Mbit/s 0.5% 3/3 3/3
Poor 150 ms 300 ms 1 Mbit/s 1% 3/3 3/3

Latency

Latency is the conservative browser-observed upper bound from a trusted Firefox keydown to the first decoded canvas observation of a calibrated terminal marker, confirmed by the next frame. Each entry pools 60 low-traffic observations across three trials; these are not physical input-to-photon measurements or network RTT.

Profile noVNC p50 / p95 / max (ms) Xpra p50 / p95 / max (ms)
Clean 36 / 69.5 / 313 63.5 / 100.3 / 173
WAN 88 / 116 / 162 158 / 227.05 / 294
Mobile 189 / 229.15 / 2,237 366 / 482.65 / 580
Poor 342 / 403.5 / 1,267 521 / 776.6 / 1,104

Xpra's p50 and p95 were higher in every profile, so this is a measurable responsiveness regression even though the page, desktop workflow, and feature checks remain intact. The canonical Xpra maximum was 1,104 ms. noVNC also produced retained long-tail samples of 2,237 ms on mobile and 1,267 ms on poor. No canonical-matrix sample was discarded, and every one passed the trusted/exact-input checks.

Workload traffic

Traffic is the gross client-eth0 counter delta from workload input through the first two-second quiet interval with empty local shaping queues. It includes protocol overhead, retransmission, background traffic, and drain; interval rates are averages, not codec payload rates or link capacity. Byte totals are median [min-max], and rates are medians within the available complete trials.

The canonical run reached that endpoint in 10/12 intervals. Poor repetitions 2 and 3 were right-censored after drain durations of 31.218 and 31.579 seconds, with 165,554 bytes/67 packets and 14,160 bytes/14 packets still queued. Their captured values—at least 6,597,096 RX/149,804 TX bytes and 6,993,244 RX/181,776 TX bytes—are lower bounds and are excluded from the table and aggregate. Complete paired sample sizes are n=3 for clean, WAN, and mobile, and n=1 for poor.

Longer drain-limit run and VM failure

A later exploratory run raised only the drain ceiling to 45 seconds. It is excluded wholesale from the primary aggregates because cross-profile system stalls and a subsequent guest crash made attribution indeterminate. Its first eight trials passed. In repetition 3, mobile contained an 11,524 ms trusted/exact latency sample alongside a 5,382.72 ms rAF stall. Poor had a 0.713-second WebSocket reconnect, two canvas-read gaps, and missed its drain ceiling by 0.093 seconds. Clean accepted the exact key remotely but did not decode its marker within 15 seconds while the probe was blocked in ext4 writeback. WAN later passed with a 3,012 ms maximum and 1,399.28 ms rAF gap. The console then showed that Xorg, Firefox, systemd-journald, and kernel-worker tasks were blocked for more than 122 seconds before the VM powered off. These outcomes are retained as unresolved failure evidence; none were selectively removed as outliers.

Profile Client RX bytes TX bytes Average RX / TX (kbit/s)
Clean noVNC 122,401,924 [117,687,486–135,521,631] 924,576 [919,726–1,223,046] 27,276.623 / 206.071
Xpra 116,093,282 [44,134,470–119,106,567] 782,758 [254,908–797,398] 26,418.267 / 177.748
WAN noVNC 54,709,619 [52,809,524–66,267,855] 952,554 [925,130–1,145,376] 12,070.896 / 214.835
Xpra 60,605,467 [57,475,296–61,498,717] 1,057,966 [1,035,176–1,171,762] 13,303.775 / 235.678
Mobile noVNC 8,317,032 [8,072,415–10,187,958] 217,404 [196,810–284,810] 1,649.342 / 40.508
Xpra 5,916,045 [5,728,607–6,762,694] 134,184 [127,640–151,416] 1,293.421 / 28.819
Poor noVNC 3,427,235 (n=1) 133,548 (n=1) 508.037 / 19.797
Xpra 6,417,016 (n=1) 161,287 (n=1) 869.894 / 21.864

Across the 10 matched, fully drained intervals, Xpra recorded 483,738,161 RX and 5,674,495 TX bytes (489,412,656 combined), versus noVNC's 579,402,679 RX and 6,922,980 TX bytes (586,325,659 combined): 16.51% less RX, 18.03% less TX, and 16.53% less combined traffic. Eligibility is based on Xpra reaching the drain gate; the corresponding noVNC poor repetitions 2 and 3 are therefore excluded as well. Combined per-profile changes were -25.75% clean, +3.41% WAN, -31.00% mobile, and +84.74% poor. This raw sum is dominated by the higher-volume clean trials. With a 3/3/3/1 profile mix and one poor-link pair, it is not an equal-profile average, link-capacity estimate, or general poor-link claim.

Measurement method and limitations

Both variants used the same libvirt VM, benchmark setup, disposable browser image, 1440x900 viewport, Firefox 140.15 ESR, geckodriver 0.36, shaping profiles, 20 latency transitions per trial, two warmups, and cyclic profile order. Each variant reused one warm workspace/display session. The variants ran serially rather than interleaved, so time and cache state are possible confounders. Shaping was symmetric client egress plus IFB ingress. The configured delay is not an independently measured internet RTT, and loss is applied independently in each direction.

Latency ran after connect, resize, clipboard, a three-second settle, and a traffic quiet gate, but before the deliberate outage and workload. It therefore describes low-traffic interaction, not loaded or recovery latency. The marker is issued by one Bash printf, which does not imply one PTY, transport, codec, or display frame. The final three-second exact-input guard does not rule out arbitrarily late duplication.

The results are descriptive: 60 observations per profile and client are clustered within three serial trials in cyclic profile order, without confidence intervals or significance claims. The two-second interface interruption probes later transport, input, and canvas delivery; it does not force the WebSocket to close or reconnect. Browser delivery means changing sampled canvas states, not visual fidelity or continuous frame accounting. Workload bytes are interface counters rather than codec payload measurements, so this is controlled comparative evidence, not a production bandwidth forecast. Both sessions used workspace_home=false because of a benchmark-guest homefs issue; the desktop protocol path was unchanged.

Measurement harness as run SHA-256: 12607296860b38cd1020db68421894cd11865042f9daa17023902d685abb818f
Censor-aware analyzer SHA-256: d89f839606394be8d07e12d60d3c714d16c2707a7fd82e746bee3fa740baaf9f; output: a1cce599f64d07c0aa64af9d6fcb6a7c40e71be69fb6da4d5a7476a8d0ddfb1c
Xpra results/summary/manifest SHA-256: 1bd09c37439de4ef7d800919e24c887686eb0d46383304b80cf756b59cbfee56 / 6a1ace47c05ba963b6cd771b2f174f2b3b58c4142eb0ed7095756957acc60baf / 475f2236901f692fcfdb946c3d5d64be7a38717c6d65b566f0320ea2a6b1ec8a
noVNC results/summary/manifest SHA-256: fc13929c626d0d3143a00b237b0ff1a4baf7b59c0d7b41c90b084d8b60eec61f / 47792860c41ec6456fed36391ca06e3b2f26a8569f2c4d045dab31b7305387aa / c47309ead2d199973cd4d59c801e28d43a6ce4f7b0df15734988a5b9b464765a
Exploratory-run manifest/recovered-trials/diagnosis SHA-256: 5c041eeada40c4bac80057b8c5e8ae32c8eb2f140eb88ddc60877f355597b64c / a705ad4e8d2b83b16fe4c7eb332139044e995423abf0d4413f8254aa642f35f4 / f8024ee7d4da1381ad522312c35a23db41c8bc29142dc4be9017a9a355716d98

Visual parity

These clean-link 1440x766 captures use the same 1440x900 browser-window setting. They were taken at visual implementation commit 2606d053; later commits change keyboard, liveness, hardening, and tests rather than the layout. They demonstrate the same workspace page and XFCE desktop chrome; clock text, generated workload content, pointer position, and focus are live state.

State Before: noVNC After: Xpra
Connected desktop Connected desktop through noVNC Connected desktop through Xpra
Terminal workload Terminal workload through noVNC Terminal workload through Xpra

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7dc3e35cb0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ctfd/patches/04-switch-pybluemonday-to-nh3.patch Outdated
@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@zardus zardus assigned ConnorNelson and unassigned ConnorNelson Sep 2, 2026
@zardus
zardus requested a review from ConnorNelson September 2, 2026 23:39
@zardus
zardus requested review from ConnorNelson and removed request for ConnorNelson September 2, 2026 23:56
@ConnorNelson

Copy link
Copy Markdown
Member

I don't understand why we need CTFd changes. This should all live entirely in the workspace. It'll be great, this PR can remove all the special case stuff that does currently exist for desktop (which is much less than this PR is attempting to do). Also desktop.nix should not need to be so crazy. All of this substituteInPlace is crazy and hard to read.

@zardus
zardus requested a review from ConnorNelson September 3, 2026 01:12
@ConnorNelson

ConnorNelson commented Sep 3, 2026

Copy link
Copy Markdown
Member

Also what are the primary non-wall-of-LLM text reasons we wanted to switch to Xpra again?

@zardus
zardus requested review from ConnorNelson and removed request for ConnorNelson September 3, 2026 01:17
@zardus

zardus commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Addressed on current head:

  • You're right about the CTFd scope. Its diff is now 2 additions and 33 deletions: the Linux desktop password/view-only special case is removed, and service=desktop uses the existing signed workspace route. Xpra-specific CTFd dependencies, credentials, certificates, and Nginx routing are gone; desktop-windows is unchanged.
  • desktop.nix uses Xpra HTML5's supported default-settings.txt; there is no substituteInPlace. Two tracked client patches address duplicated held keys and premature degraded-link health warnings/timeouts; inbound activity affects only the warning, while reconnect still depends on ping echoes. The remaining service code covers workspace startup, readiness, disabled features, legacy cleanup, and orphan-Xorg recovery.

Why Xpra, briefly:

  1. Its own HTML5/WebSocket server provides reconnect and remote resize, letting us remove Xvnc, websockify/noVNC, and our noVNC reconnect patch.
  2. It provides bidirectional Unicode clipboard support. The matrix checks the Unicode Xpra/X11 round trip; it does not test browser permission UI or the host OS clipboard.

Final evidence is on product commit 28539e9f; current PR head f5078834 only hardens benchmark evidence preservation, validation, and aggregation. All four GitHub test suites and Codecov passed on the current head. The canonical matrix passed 12/12 user-facing degraded-link trials and 240/240 trusted, exact-input latency samples. Xpra was slower in this browser-observed probe: Xpra/noVNC p50 was 63.5/36 ms clean, 158/88 WAN, 366/189 mobile, and 521/342 poor.

Across the 10 matched workload intervals that fully drained, Xpra recorded 16.53% fewer combined bytes, though the result varied by profile and the poor estimate has n=1. Two poor intervals are retained as censored lower bounds. The PR body has p95/max latency, per-profile traffic, methods, limitations, and matched screenshots. It also discloses a later longer-ceiling run that was excluded wholesale after system-wide stalls and a VM crash, without dropping its adverse samples selectively.

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.

2 participants