all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Alexandre Derumier <alexandre.derumier@groupe-cyllene.com>
To: pve-devel@lists.proxmox.com
Cc: Alexandre Derumier <aderumier@groupe-cyllene.com>
Subject: SPAM: [RFC pve-kyber-web 10/13] add pve-kyber-web: console's webassembly client
Date: Tue, 25 Aug 2026 13:08:42 +0200	[thread overview]
Message-ID: <20260825110849.2967694-11-alexandre.derumier@groupe-cyllene.com> (raw)
In-Reply-To: <20260825110849.2967694-1-alexandre.derumier@groupe-cyllene.com>

From: Alexandre Derumier <aderumier@groupe-cyllene.com>

The console page use Kyber's WASM SDK directly, with some modifications
to make it work behind pveproxy:

 - 0001 gives kyclient a base path, so the controller can be reached under
   /api2/json/nodes/<node>/qemu/<vmid>/ rather than owning an origin.
 - 0002 resolves the renderer worker's wasm glue against baseURI instead of
   window.location.pathname.
 - 0003 puts the kymux token in the WebTransport path, giving a proxy in
   front of several VMs something to route on.

Signed-off-by: Alexandre Derumier <aderumier@groupe-cyllene.com>
---
 .gitignore                                    |   4 +
 .gitmodules                                   |   3 +
 Makefile                                      | 118 +++++++++
 debian/changelog                              |   7 +
 debian/control                                |  21 ++
 debian/copyright                              |  20 ++
 debian/install                                |   1 +
 debian/rules                                  |   9 +
 debian/source/format                          |   1 +
 kyber-web                                     |   1 +
 .../0001-kyclient-support-a-base-path.patch   | 248 ++++++++++++++++++
 ...renderer-worker-against-the-base-url.patch |  72 +++++
 ...kymux-token-in-the-webtransport-path.patch |  37 +++
 13 files changed, 542 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 .gitmodules
 create mode 100644 Makefile
 create mode 100644 debian/changelog
 create mode 100644 debian/control
 create mode 100644 debian/copyright
 create mode 100644 debian/install
 create mode 100755 debian/rules
 create mode 100644 debian/source/format
 create mode 160000 kyber-web
 create mode 100644 patches/0001-kyclient-support-a-base-path.patch
 create mode 100644 patches/0002-kywebplayer-resolve-the-renderer-worker-against-the-base-url.patch
 create mode 100644 patches/0003-kyclient-put-the-kymux-token-in-the-webtransport-path.patch

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..dea6af5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/sdk/
+*.deb
+*.buildinfo
+*.changes
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..b7a917b
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "kyber-web"]
+	path = kyber-web
+	url = https://gitlab.com/kyber/apps/kyber-web.git
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..ed833c4
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,118 @@
+include /usr/share/dpkg/architecture.mk
+include /usr/share/dpkg/pkg-info.mk
+
+PACKAGE=pve-kyber-web
+DEB=$(PACKAGE)_$(DEB_VERSION_UPSTREAM_REVISION)_all.deb
+DSC=$(PACKAGE)_$(DEB_VERSION).dsc
+BUILDDIR=$(PACKAGE)-$(DEB_VERSION_UPSTREAM)
+
+# Kyber's web client: AGPL-3.0-or-later like this package, but a separate
+# upstream with its own cadence, so it is cloned at build time rather than
+# vendored or carried as a submodule. This repo then holds nothing but
+# packaging, and the revision it builds is one greppable line rather than a
+# gitlink.
+#
+# Pinned by commit, so a moved tag cannot change what this builds.
+KYBER_WEB_DIR = kyber-web
+
+# The four files the console page loads. Not the demo client's bundle, its
+# sidebar or its metrics UI: the page drives the SDK directly, so none of that
+# is built and neither pnpm nor esbuild is needed.
+SDK_FILES = kyclient_wasm.js kyclient_wasm_bg.wasm audio_worklet.js spinlock.js
+
+all: $(DEB)
+
+# --- upstream ---------------------------------------------------------------
+# Fetched once, and left alone after that, following pve-qemu: a build must not
+# depend on re-fetching, and a checkout someone has been working in is theirs.
+#
+# The patch below is what needs the tree pristine, so it resets just the files
+# it touches rather than the whole submodule - which would discard the build
+# output beside them for nothing.
+.PHONY: submodule
+submodule:
+ifeq ($(shell test -f "$(KYBER_WEB_DIR)/Cargo.toml" && echo 1 || echo 0), 0)
+	git submodule update --init --recursive $(KYBER_WEB_DIR)
+endif
+
+# Applied from the kyber-web root: they span it and its nested kysdk submodule.
+.PHONY: patch
+patch: submodule
+	git -C $(KYBER_WEB_DIR) checkout --force -- .
+	git -C $(KYBER_WEB_DIR) submodule foreach --recursive --quiet 'git checkout --force -- .'
+	set -e; for p in $(CURDIR)/patches/000*.patch; do \
+	    git -C $(KYBER_WEB_DIR) apply "$$p"; \
+	done
+
+# --- build ------------------------------------------------------------------
+# The version Cargo.lock pins, read at use rather than at parse: the checkout
+# does not exist yet when make reads this file.
+WASM_BINDGEN_VERSION = $(shell sed -n '/^name = "wasm-bindgen"$$/{n;s/^version = "\(.*\)"/\1/p;q}' $(KYBER_WEB_DIR)/Cargo.lock)
+
+# web_sys_unstable_apis is required for WebTransport, which is the whole data
+# plane. A wasm-bindgen CLI older or newer than the crate emits glue the wasm
+# rejects at instantiation, with no error until the console is opened.
+.PHONY: wasm
+wasm: patch
+	have=$$(wasm-bindgen --version | awk '{print $$2}'); \
+	want="$(WASM_BINDGEN_VERSION)"; \
+	if [ "$$have" != "$$want" ]; then \
+	    echo "wasm-bindgen $$have found, Cargo.lock pins $$want" >&2; \
+	    echo "install it with: cargo install --locked wasm-bindgen-cli@$$want" >&2; \
+	    exit 1; \
+	fi
+	cd $(KYBER_WEB_DIR) && RUSTFLAGS=--cfg=web_sys_unstable_apis \
+	    cargo build --target wasm32-unknown-unknown -p kyclient-wasm --release
+	cd $(KYBER_WEB_DIR) && wasm-bindgen --target web --no-typescript --out-dir html \
+	    target/wasm32-unknown-unknown/release/kyclient_wasm.wasm
+	cd $(KYBER_WEB_DIR) && wasm-opt html/kyclient_wasm_bg.wasm \
+	    -o html/kyclient_wasm_bg.wasm -Os -g
+	# The JS glue: worker entry points and the audio worklet.
+	cd $(KYBER_WEB_DIR)/kysdk/kyctl && ./build-wasm.sh -j $(CURDIR)/$(KYBER_WEB_DIR)/html
+
+# --- packaging --------------------------------------------------------------
+.PHONY: sdk
+sdk: wasm
+	rm -rf sdk && mkdir sdk
+	for f in $(SDK_FILES); do install -m 0644 "$(KYBER_WEB_DIR)/html/$$f" sdk/; done
+
+.PHONY: builddir
+builddir:
+	rm -rf $(BUILDDIR)
+	$(MAKE) $(BUILDDIR)
+
+$(BUILDDIR): sdk
+	rm -rf $@ $@.tmp
+	mkdir $@.tmp
+	cp -a sdk debian Makefile $@.tmp/
+	mv $@.tmp $@
+
+deb: $(DEB)
+$(DEB): $(BUILDDIR)
+	cd $(BUILDDIR); dpkg-buildpackage -b -us -uc
+	lintian $(DEB) || true
+
+# A source package, for sbuild and for review: Proxmox builds every package
+# this way, so it has to work even when the binary path is what gets used.
+.PHONY: dsc
+dsc:
+	rm -rf $(BUILDDIR) $(DSC)
+	$(MAKE) $(DSC)
+	lintian $(DSC)
+
+$(DSC): $(BUILDDIR)
+	cd $(BUILDDIR); dpkg-buildpackage -S -us -uc -d
+
+sbuild: $(DSC)
+	sbuild $<
+
+.PHONY: dinstall
+dinstall: deb
+	dpkg -i $(DEB)
+
+.PHONY: clean
+clean:
+	rm -rf *.deb *.changes *.dsc *.buildinfo *.build $(PACKAGE)-[0-9]*/ sdk/
+
+.PHONY: distclean
+distclean: clean
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..ff9047b
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,7 @@
+pve-kyber-web (0.27.0) trixie; urgency=medium
+
+  * Initial release: the Kyber WebAssembly client.
+  * Carries the fix for resolving the renderer worker against the document
+    base URL, without which the console renders black behind a path prefix.
+
+ -- Proxmox Support Team <support@proxmox.com>  Wed, 19 Aug 2026 07:00:00 +0200
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..2be348f
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,21 @@
+Source: pve-kyber-web
+Section: admin
+Priority: optional
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Uploaders: Alexandre Derumier <aderumier@groupe-cyllene.com>
+Build-Depends: debhelper-compat (= 13),
+                binaryen,
+                git,
+Standards-Version: 4.7.0.0
+
+Package: pve-kyber-web
+Architecture: all
+Depends: ${misc:Depends},
+Description: Kyber streaming client for the Proxmox VE console
+ The WebAssembly client the Kyber console page drives: the wasm module, its
+ JavaScript glue and the audio worklet.
+ .
+ Built from kyber-web, which is a separate upstream with its own release
+ cadence and a wasm toolchain that has no business in the pve-manager build,
+ so it is packaged on its own and the console page in pve-manager loads it
+ from here.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..702a252
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,20 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: pve-kyber-web
+
+Files: *
+Copyright: 2026 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3.0-or-later
+
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License
+ along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/debian/install b/debian/install
new file mode 100644
index 0000000..701f265
--- /dev/null
+++ b/debian/install
@@ -0,0 +1 @@
+sdk/* usr/share/pve-kyber-web/
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..2a8f910
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,9 @@
+#!/usr/bin/make -f
+%:
+	dh $@
+# The sdk/ directory is staged into the build dir before dpkg-buildpackage
+# runs, and the Makefile carried along with it has a distclean that removes
+# it. Nothing here is built or cleaned by dh.
+override_dh_auto_clean:
+override_dh_auto_build:
+override_dh_auto_test:
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..89ae9db
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/kyber-web b/kyber-web
new file mode 160000
index 0000000..ad610dc
--- /dev/null
+++ b/kyber-web
@@ -0,0 +1 @@
+Subproject commit ad610dc64b5dd5d88aecdbf5e56a21b49572307a
diff --git a/patches/0001-kyclient-support-a-base-path.patch b/patches/0001-kyclient-support-a-base-path.patch
new file mode 100644
index 0000000..4b9fb1d
--- /dev/null
+++ b/patches/0001-kyclient-support-a-base-path.patch
@@ -0,0 +1,248 @@
+From: Kyber/QEMU integration
+Subject: [PATCH] kyclient: let the controller be reached under a base path
+
+Client::new took a host and a port and built /session/login,
+/kymux/start_session and /websocket/<plane> from them, so the controller had to
+own the origin it was served on. Behind a reverse proxy it does not: Proxmox
+exposes it under /api2/json/nodes/<node>/qemu/<vmid>/, and there was no way to
+say so.
+
+Add an optional base_path, threaded through ConnectConfig and ConnectionParams.
+Every HTTP URL already derives from HttpClient::base_url, so those come along
+with one change; the websocket URLs are built from the host instead and are
+prefixed explicitly.
+
+That includes /ws, the control plane's own websocket in host_events - which is
+easy to miss, because it lives on the other side of the platform split from the
+three data-plane sockets and is opened later. Missing it does not fail at
+startup: the client logs in, fetches /capabilities and only then tries to open
+a websocket at the origin root, which behind a proxy names no VM. Found by
+running the client through pvekyberproxy.
+
+normalize_base_path accepts "/kyber", "kyber/" and "/kyber/" alike - a doubled
+or missing slash would otherwise show up as a 404 a long way from the setting
+that caused it.
+
+This is not Proxmox-specific: any reverse-proxied deployment needs it, which is
+why it is worth sending upstream rather than carrying here.
+---
+diff --git a/kyclient/src/http.rs b/kyclient/src/http.rs
+index bdf88b6..17d52f2 100644
+--- a/kysdk/kyctl/kyclient/src/http.rs
++++ b/kysdk/kyctl/kyclient/src/http.rs
+@@ -38,8 +38,20 @@ pub(crate) fn format_host_for_url(host: &str, port: u16) -> String {
+     }
+ }
+ 
++/// Trim a base path to the form the URL builders expect: a leading slash and
++/// no trailing one, or empty for the origin root. Accepting "/kyber",
++/// "kyber/" and "/kyber/" alike avoids a double or missing slash showing up
++/// as a 404 far from the setting that caused it.
++pub(crate) fn normalize_base_path(base_path: Option<&str>) -> String {
++    match base_path.map(str::trim).filter(|p| !p.is_empty() && *p != "/") {
++        None => String::new(),
++        Some(path) => format!("/{}", path.trim_matches('/')),
++    }
++}
++
+ pub(crate) struct HttpClient {
+     base_url: String,
++    base_path: String,
+     client: reqwest::Client,
+     host: String,
+ }
+@@ -47,12 +59,17 @@ pub(crate) struct HttpClient {
+ impl HttpClient {
+     pub(crate) fn new(conn_params: ConnectionParams) -> Result<Self> {
+         let host = platform_http::create_host(&conn_params);
+-        let base_url = format!("https://{host}");
++        // Behind a reverse proxy the controller has no origin of its own, so
++        // every URL has to be built under the path it is exposed at rather
++        // than at the root.
++        let base_path = normalize_base_path(conn_params.base_path.as_deref());
++        let base_url = format!("https://{host}{base_path}");
+ 
+         let client = platform_http::create_reqwest(conn_params)?;
+ 
+         Ok(Self {
+             base_url,
++            base_path,
+             client,
+             host,
+         })
+@@ -74,6 +91,12 @@ impl HttpClient {
+         &self.base_url
+     }
+ 
++    /// The path prefix on its own, for the websocket URLs - those are built
++    /// from the host rather than from base_url, so they cannot reuse it.
++    pub(crate) fn base_path(&self) -> &str {
++        &self.base_path
++    }
++
+     pub(crate) fn peer_addr_from_response(
+         &self,
+         response: &reqwest::Response,
+diff --git a/kyclient/src/platform/desktop/host_events.rs b/kyclient/src/platform/desktop/host_events.rs
+index f8fd9e1..5345ff0 100644
+--- a/kysdk/kyctl/kyclient/src/platform/desktop/host_events.rs
++++ b/kysdk/kyctl/kyclient/src/platform/desktop/host_events.rs
+@@ -53,7 +53,11 @@ impl WsClient {
+         auth_token: &str,
+     ) -> Result<Self> {
+         let host = platform::http::create_host(conn_params);
+-        let url = format!("wss://{host}/ws");
++        // The control plane's own websocket sits under the base path too. It
++        // is built from the host rather than from base_url, so like the three
++        // data-plane sockets it has to be prefixed explicitly.
++        let base_path = crate::http::normalize_base_path(conn_params.base_path.as_deref());
++        let url = format!("wss://{host}{base_path}/ws");
+ 
+         // Connect with appropriate TLS verification
+         let mut stream = if let Some(verify_mode) = &conn_params.verify_mode {
+diff --git a/kyclient/src/platform/desktop/mod.rs b/kyclient/src/platform/desktop/mod.rs
+index fadb052..44681a0 100644
+--- a/kysdk/kyctl/kyclient/src/platform/desktop/mod.rs
++++ b/kysdk/kyctl/kyclient/src/platform/desktop/mod.rs
+@@ -46,6 +46,10 @@ pub use tls::{TofuPrompt, TofuVerifier, VerifyMode};
+ pub struct ConnectConfig {
+     pub host: String,
+     pub port: u16,
++    /// Path the controller is reached under, when it sits behind a reverse
++    /// proxy that cannot give it an origin of its own. Every HTTP and
++    /// websocket URL is built beneath it. Default: the origin root.
++    pub base_path: Option<String>,
+     pub tls_host: Option<String>,
+     /// TLS verification mode.
+     pub verify_mode: Option<VerifyMode>,
+@@ -126,6 +130,7 @@ impl VideoPlayerConfig {
+ pub(crate) struct ConnectionParams {
+     pub(crate) host: String,
+     pub(crate) port: u16,
++    pub(crate) base_path: Option<String>,
+     pub(crate) tls_host: Option<String>,
+     pub(crate) verify_mode: Option<VerifyMode>,
+     pub(crate) credentials: AuthCredentials,
+@@ -136,6 +141,7 @@ impl ConnectionParams {
+         Ok(Self {
+             host: connect_config.host,
+             port: connect_config.port,
++            base_path: connect_config.base_path.clone(),
+             tls_host: connect_config.tls_host.clone(),
+             verify_mode: connect_config.verify_mode,
+             credentials: connect_config.credentials,
+diff --git a/kyclient/src/platform/web/host_events.rs b/kyclient/src/platform/web/host_events.rs
+index 425e76c..a6f2415 100644
+--- a/kysdk/kyctl/kyclient/src/platform/web/host_events.rs
++++ b/kysdk/kyctl/kyclient/src/platform/web/host_events.rs
+@@ -315,7 +315,11 @@ impl WsClient {
+         auth_token: &str,
+     ) -> Result<Self> {
+         let host = platform::http::create_host(conn_params);
+-        let url = format!("wss://{host}/ws");
++        // The control plane's own websocket sits under the base path too. It
++        // is built from the host rather than from base_url, so like the three
++        // data-plane sockets it has to be prefixed explicitly.
++        let base_path = crate::http::normalize_base_path(conn_params.base_path.as_deref());
++        let url = format!("wss://{host}{base_path}/ws");
+ 
+         let inner = InnerRef(Inner::new(url, capabilities, event_sink, auth_token).await?);
+ 
+diff --git a/kyclient/src/platform/web/mod.rs b/kyclient/src/platform/web/mod.rs
+index bec764b..3cfbb9d 100644
+--- a/kysdk/kyctl/kyclient/src/platform/web/mod.rs
++++ b/kysdk/kyctl/kyclient/src/platform/web/mod.rs
+@@ -41,6 +41,10 @@ pub(crate) use websocket::{WebSocket, WebSocketHandler};
+ pub struct ConnectConfig {
+     pub host: String,
+     pub port: u16,
++    /// Path the controller is reached under, when it sits behind a reverse
++    /// proxy that cannot give it an origin of its own. Every HTTP and
++    /// websocket URL is built beneath it. Default: the origin root.
++    pub base_path: Option<String>,
+     pub credentials: AuthCredentials,
+     /// Enable automatic reconnection on connection loss. Default: false.
+     /// When enabled, the client will emit `Reconnecting`, `Reconnected`, and
+@@ -95,6 +99,7 @@ impl From<VideoCodec> for player::VideoCodec {
+ pub(crate) struct ConnectionParams {
+     pub(crate) host: String,
+     pub(crate) port: u16,
++    pub(crate) base_path: Option<String>,
+     pub(crate) credentials: AuthCredentials,
+ }
+ 
+@@ -103,6 +108,7 @@ impl ConnectionParams {
+         Ok(Self {
+             host: connect_config.host.clone(),
+             port: connect_config.port,
++            base_path: connect_config.base_path.clone(),
+             credentials: connect_config.credentials,
+         })
+     }
+diff --git a/kyclient/src/ws_backend/mod.rs b/kyclient/src/ws_backend/mod.rs
+index c775c98..009612c 100644
+--- a/kysdk/kyctl/kyclient/src/ws_backend/mod.rs
++++ b/kysdk/kyctl/kyclient/src/ws_backend/mod.rs
+@@ -163,8 +163,9 @@ impl WsBackend {
+             VideoPlayer::create(listener, &player_config, metrics).await?;
+ 
+         let url = format!(
+-            "wss://{host}/websocket/video",
+-            host = backend_config.http_client.host()
++            "wss://{host}{base_path}/websocket/video",
++            host = backend_config.http_client.host(),
++            base_path = backend_config.http_client.base_path()
+         );
+         let ws = VideoWsSocketHandler::new(&url, player)?;
+ 
+@@ -194,8 +195,9 @@ impl WsBackend {
+             };
+ 
+         let url = format!(
+-            "wss://{host}/websocket/audio",
+-            host = backend_config.http_client.host()
++            "wss://{host}{base_path}/websocket/audio",
++            host = backend_config.http_client.host(),
++            base_path = backend_config.http_client.base_path()
+         );
+         let ws = AudioWsSocketHandler::new(&url, player)?;
+ 
+@@ -220,8 +222,9 @@ impl WsBackend {
+         #[allow(clippy::arc_with_non_send_sync)]
+         let msg_sink = Arc::new(InputMsgSink::new(backend_config.msg_sender.clone()));
+         let inputs_url = format!(
+-            "wss://{host}/websocket/inputs",
+-            host = backend_config.http_client.host()
++            "wss://{host}{base_path}/websocket/inputs",
++            host = backend_config.http_client.host(),
++            base_path = backend_config.http_client.base_path()
+         );
+         let input_websocket =
+             InputWebSocketHandler::new(&inputs_url, kynput_tx, kynput_rx, msg_sink)?;
+diff --git a/src/lib.rs b/src/lib.rs
+index e692168..90b5d7c 100644
+--- a/src/lib.rs
++++ b/src/lib.rs
+@@ -496,6 +496,9 @@ impl Client {
+     ///   `"decoded"`, `"skipped"`, `"prepared"`, `"displayed"`.
+     ///   All timestamps are in microseconds (µs). The server timestamps are already compensated
+     ///   to match the client clock.
++    /// `base_path` is the path the controller is reached under when it sits
++    /// behind a reverse proxy that cannot give it an origin of its own; pass
++    /// `null` when it has the origin to itself.
+     pub async fn new(
+         host: &str,
+         port: u16,
+@@ -505,10 +508,12 @@ impl Client {
+         metrics_mode_str: String,
+         live_metrics_callback: Option<js_sys::Function>,
+         auto_reconnection: bool,
++        base_path: Option<String>,
+     ) -> JsResult<Client> {
+         let connect_config = kyclient::ConnectConfig {
+             host: host.into(),
+             port,
++            base_path,
+             credentials: credentials.0,
+             auto_reconnection,
+         };
diff --git a/patches/0002-kywebplayer-resolve-the-renderer-worker-against-the-base-url.patch b/patches/0002-kywebplayer-resolve-the-renderer-worker-against-the-base-url.patch
new file mode 100644
index 0000000..f906736
--- /dev/null
+++ b/patches/0002-kywebplayer-resolve-the-renderer-worker-against-the-base-url.patch
@@ -0,0 +1,72 @@
+From: Kyber/QEMU integration
+Subject: [PATCH] kywebplayer: resolve the renderer worker against the base URL
+
+RendererWorker::create built the URL of the wasm glue from
+window.location.pathname, so the glue had to sit in the same directory as the
+HTML that loaded it. That holds for the demo client, whose page and SDK ship
+side by side, and does not hold for an application that says otherwise with a
+<base href> - Proxmox serves its console page at / and the SDK under /kyber/.
+
+The failure is expensive to find. The worker is created, so the canvas is
+transferred to it and the decoder runs happily; only the module fetch 404s,
+inside a worker, where nothing surfaces it. Frames are then decoded and
+dropped - the browser reports them garbage collected without being closed - and
+the console shows a black canvas with no error in the page, no exception, and
+no failed request that a page-level capture would see. Found by attaching to
+the worker target and watching its network.
+
+baseURI is the document URL wherever no <base> tag exists, so this changes
+nothing for the demo client and every other existing caller.
+---
+diff --git a/kywebplayer/src/video/renderer_worker.rs b/kywebplayer/src/video/renderer_worker.rs
+index 8aa7fb2..8224d25 100644
+--- a/kysdk/kyctl/kywebplayer/src/video/renderer_worker.rs
++++ b/kysdk/kyctl/kywebplayer/src/video/renderer_worker.rs
+@@ -264,20 +264,34 @@ impl RendererWorker {
+         // This is a library, but the path of the wasm-bindings JavaScript file
+         // depends on the top-level crate name, so the worker code must be
+         // generated dynamically.
+-        let location = web_sys::window()
+-            .ok_or_else(|| JsValue::from(js_sys::Error::new("No window")))?
+-            .location();
+-        let origin = location.origin()?;
+-        let pathname = location.pathname()?;
+-        // pathname can be a directory (".../") or include a filename
+-        // (".../index.html"); strip back to the last "/" so we resolve the wasm
+-        // glue next to the HTML rather than concatenating onto the filename.
+-        let dir = match pathname.rfind('/') {
+-            Some(i) => &pathname[..=i],
+-            None => "/",
+-        };
++        let window = web_sys::window()
++            .ok_or_else(|| JsValue::from(js_sys::Error::new("No window")))?;
+         let sanitized_crate_name = app_crate_name.replace("-", "_");
+-        let wasm_js_url = format!("{origin}{dir}{sanitized_crate_name}.js");
++
++        // Resolved against the document's base URL, not its path. The two are
++        // the same until a page carries a <base href>, and then they are not:
++        // an application whose HTML is served from one place and whose wasm
++        // glue lives in another says so with that tag, and resolving against
++        // the path instead sends this worker somewhere the script is not.
++        //
++        // It fails in a way that takes a long time to find. The worker is
++        // created, so the canvas is transferred to it and the decoder runs;
++        // only the module fetch 404s, inside a worker, where nothing surfaces
++        // it. Frames are then decoded and dropped, and the console shows a
++        // black canvas with no error anywhere.
++        //
++        // baseURI is the document URL when there is no <base>, so this is the
++        // old behaviour wherever the tag is absent.
++        let base = window
++            .document()
++            .ok_or_else(|| JsValue::from(js_sys::Error::new("No document")))?
++            .base_uri()?
++            .ok_or_else(|| JsValue::from(js_sys::Error::new("No base URI")))?;
++        let wasm_js_url = web_sys::Url::new_with_base(
++            &format!("{sanitized_crate_name}.js"),
++            &base,
++        )?
++        .href();
+         let worker_code = format!(
+             r#"import init, {{ VideoRendererWorkerCtx }} from "{wasm_js_url}";
+ let ctx;
diff --git a/patches/0003-kyclient-put-the-kymux-token-in-the-webtransport-path.patch b/patches/0003-kyclient-put-the-kymux-token-in-the-webtransport-path.patch
new file mode 100644
index 0000000..5008623
--- /dev/null
+++ b/patches/0003-kyclient-put-the-kymux-token-in-the-webtransport-path.patch
@@ -0,0 +1,37 @@
+From: Kyber/QEMU integration
+Subject: [PATCH] kyclient: put the kymux token in the WebTransport path
+
+The client opened WebTransport at the origin root, so the only thing
+distinguishing one console from another was the port it connected to. That is
+fine against a controller, which serves one VM, and it forces a proxy in front
+of several to spend a UDP port per session: nothing in an encrypted datagram
+says which VM it belongs to, so the port has to be the routing key.
+
+Putting the token in the path gives such a proxy something to route on, and it
+costs nothing to either end. The token already exists, both sides already agree
+on it, and it is already carried in this connection - it authenticates to the
+controller a moment later. It does not travel in the clear either: a path is
+inside the encrypted session, unlike a port.
+
+A controller reached directly ignores the path, so this changes nothing for a
+client that is not behind a proxy.
+---
+diff --git a/kyclient/src/kymux_backend/mod.rs b/kyclient/src/kymux_backend/mod.rs
+index db7b13a..b56e888 100644
+--- a/kysdk/kyctl/kyclient/src/kymux_backend/mod.rs
++++ b/kysdk/kyctl/kyclient/src/kymux_backend/mod.rs
+@@ -265,7 +265,13 @@ async fn start_kymux(backend_config: &KymuxBackendConfig<'_>) -> Result<kyproto:
+                 &backend_config.conn_params.host,
+                 start_response.port,
+             );
+-            let url = format!("https://{host_str}");
++            // The token goes in the path as well as into the authentication
++            // below. A proxy that terminates WebTransport to put every console
++            // on one UDP port has nothing else to route on - a datagram says
++            // nothing about which host it is for - and this is the one field
++            // both ends already agree on. Reaching a controller directly, the
++            // path is ignored.
++            let url = format!("https://{host_str}/{token}", token = start_response.token);
+ 
+             connect_webtransport_js(&url, start_response.certificate_hash).await
+         }
-- 
2.55.0




  parent reply	other threads:[~2026-08-25 11:10 UTC|newest]

Thread overview: 11+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-25 11:08 SPAM: [RFC pve-http-server/qemu-server/pve-manager/pve-{qemu-kyber,kyberproxy, kyber-web,qemu-rdp,rdpproxy,rdp-web} 00/13] add rdp && kyber consoles for qemu over D-Bus display Alexandre Derumier
2026-08-25 11:08 ` SPAM: [RFC pve-http-server 01/13] anyevent : proxy a path prefix to a local http proxy Alexandre Derumier
2026-08-25 11:08 ` SPAM: [RFC qemu-server 02/13] add D-Bus display support Alexandre Derumier
2026-08-25 11:08 ` SPAM: [RFC qemu-server 03/13] add kyber display Alexandre Derumier
2026-08-25 11:08 ` SPAM: [RFC qemu-server 04/13] add rdp display Alexandre Derumier
2026-08-25 11:08 ` SPAM: [RFC qemu-server 05/13] add experimental kyber-gl display Alexandre Derumier
2026-08-25 11:08 ` SPAM: [RFC pve-manager 06/13] ui: add kyber console Alexandre Derumier
2026-08-25 11:08 ` SPAM: [RFC pve-manager 07/13] ui: add rdp console Alexandre Derumier
2026-08-25 11:08 ` Alexandre Derumier [this message]
2026-08-25 11:08 ` SPAM: [RFC pve-qemu-rdp 11/13] Add pve-qemu-rdp: an RDP server for the console Alexandre Derumier
2026-08-25 11:08 ` SPAM: [RFC pve-rdpproxy 12/13] Add pve-rdpproxy Alexandre Derumier

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260825110849.2967694-11-alexandre.derumier@groupe-cyllene.com \
    --to=alexandre.derumier@groupe-cyllene.com \
    --cc=aderumier@groupe-cyllene.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal