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-qemu-rdp 11/13] Add pve-qemu-rdp: an RDP server for the console
Date: Tue, 25 Aug 2026 13:08:43 +0200 [thread overview]
Message-ID: <20260825110849.2967694-12-alexandre.derumier@groupe-cyllene.com> (raw)
In-Reply-To: <20260825110849.2967694-1-alexandre.derumier@groupe-cyllene.com>
From: Alexandre Derumier <aderumier@groupe-cyllene.com>
qemu-rdp reads a VM's display over org.qemu.Display1.
It's also provide clipboard && audio.
Signed-off-by: Alexandre Derumier <aderumier@groupe-cyllene.com>
---
.gitignore | 5 +
.gitmodules | 3 +
Makefile | 76 +++++++++
debian/changelog | 5 +
debian/control | 24 +++
debian/copyright | 37 ++++
debian/install | 1 +
debian/rules | 16 ++
debian/source/format | 1 +
...rdp-allow-listening-on-a-unix-socket.patch | 159 ++++++++++++++++++
qemu-display | 1 +
11 files changed, 328 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 100644 patches/0001-qemu-rdp-allow-listening-on-a-unix-socket.patch
create mode 160000 qemu-display
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..43c1faa
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/staging/
+/pve-qemu-rdp-[0-9]*/
+*.deb
+*.changes
+*.buildinfo
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..ee0ae27
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "qemu-display"]
+ path = qemu-display
+ url = https://gitlab.com/marcandre.lureau/qemu-display.git
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..5f457c6
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,76 @@
+include /usr/share/dpkg/architecture.mk
+include /usr/share/dpkg/pkg-info.mk
+
+PACKAGE=pve-qemu-rdp
+
+# qemu-display: MIT where this package is, but a separate upstream, so it is a
+# submodule rather than vendored. Pinned by commit and not by tag - the unix
+# socket support this needs is only in master, see patches/.
+SRCDIR=qemu-display
+
+BUILDDIR=$(PACKAGE)-$(DEB_VERSION_UPSTREAM)
+ORIG_SRC_TAR=$(PACKAGE)_$(DEB_VERSION_UPSTREAM).orig.tar.gz
+
+DSC=$(PACKAGE)_$(DEB_VERSION).dsc
+DEB=$(PACKAGE)_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
+DEB_DBG=$(PACKAGE)-dbgsym_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
+DEBS=$(DEB) $(DEB_DBG)
+
+all: $(DEBS)
+
+# Fetched once and left alone afterwards: nothing below writes to it, so a
+# checkout someone has been working in stays theirs.
+.PHONY: submodule
+submodule:
+ test -f "$(SRCDIR)/Cargo.toml" || git submodule update --init --recursive $(SRCDIR)
+
+# The patches are applied to the copy, never to the submodule. That is what
+# lets the fetch above be a one-off, and what keeps a rebuild from finding a
+# tree that is already patched.
+#
+# .git goes first: it is a gitlink pointing back at the submodule, so `git
+# apply` would otherwise find that work tree instead of this copy. target/ goes
+# with it because a stray build tree is half a gigabyte.
+$(BUILDDIR): submodule debian/changelog
+ rm -rf $@ $@.tmp
+ cp -a $(SRCDIR) $@.tmp
+ find $@.tmp -name .git -prune -exec rm -rf {} +
+ rm -rf $@.tmp/target
+ set -e; for p in $(CURDIR)/patches/*.patch; do \
+ git -C $@.tmp apply "$$p"; \
+ done
+ cp -a debian $@.tmp/debian
+ mv $@.tmp $@
+
+$(ORIG_SRC_TAR): $(BUILDDIR)
+ tar czf $(ORIG_SRC_TAR) --exclude="$(BUILDDIR)/debian" $(BUILDDIR)
+
+.PHONY: deb
+deb: $(DEBS)
+$(DEBS) &: $(BUILDDIR)
+ cd $(BUILDDIR); dpkg-buildpackage -b -us -uc
+ lintian $(DEBS)
+
+.PHONY: dsc
+dsc:
+ rm -rf $(BUILDDIR) $(ORIG_SRC_TAR) $(DSC)
+ $(MAKE) $(DSC)
+ lintian $(DSC)
+
+$(DSC): $(BUILDDIR) $(ORIG_SRC_TAR)
+ cd $(BUILDDIR); dpkg-buildpackage -S -us -uc -d
+
+sbuild: $(DSC)
+ sbuild $<
+
+.PHONY: dinstall
+dinstall: deb
+ dpkg -i $(DEBS)
+
+.PHONY: distclean
+distclean: clean
+
+.PHONY: clean
+clean:
+ rm -rf $(PACKAGE)-[0-9]*/
+ rm -rf $(PACKAGE)*.tar* *.deb *.dsc *.changes *.buildinfo *.build
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..9a5412d
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+pve-qemu-rdp (0.1.1-1) trixie; urgency=medium
+
+ * initial package
+
+ -- Alexandre Derumier <aderumier@groupe-cyllene.com> Wed, 19 Aug 2026 12:00:00 +0200
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..7b292b6
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,24 @@
+Source: pve-qemu-rdp
+Section: admin
+Priority: optional
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Uploaders: Alexandre Derumier <aderumier@groupe-cyllene.com>
+Build-Depends: debhelper-compat (= 13),
+ cargo,
+ git,
+ libssl-dev,
+ pkgconf,
+Standards-Version: 4.7.0.0
+
+Package: pve-qemu-rdp
+Architecture: any
+Depends: ${misc:Depends},
+ ${shlibs:Depends},
+Description: RDP server for the Proxmox VE console
+ An RDP server that reads a VM's display over the org.qemu.Display1 D-Bus
+ interface exposed by "qemu -display dbus", so a guest is streamed without a
+ guest agent and without a second display device.
+ .
+ Started per VM by qemu-server for VMs configured with "vga: rdp", on a unix
+ socket, and reached from a browser only through pveproxy and pve-rdpproxy.
+ RDP brings clipboard, audio and monitor resize with it.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..92edec8
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,37 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: qemu-rdp
+Source: https://gitlab.com/marcandre.lureau/qemu-display
+
+Files: *
+Copyright: 2023-2026 Marc-André Lureau <marcandre.lureau@redhat.com>
+ 2023 Mihnea Buzatu <mihneabuzatu88@gmail.com>
+License: MIT
+
+Files: debian/*
+Copyright: 2026 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: MIT
+
+License: MIT
+ Permission is hereby granted, free of charge, to any
+ person obtaining a copy of this software and associated
+ documentation files (the "Software"), to deal in the
+ Software without restriction, including without
+ limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of
+ the Software, and to permit persons to whom the Software
+ is furnished to do so, subject to the following
+ conditions:
+ .
+ The above copyright notice and this permission notice
+ shall be included in all copies or substantial portions
+ of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
diff --git a/debian/install b/debian/install
new file mode 100644
index 0000000..b9dc42d
--- /dev/null
+++ b/debian/install
@@ -0,0 +1 @@
+target/release/qemu-rdp usr/bin/
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..9dad7b9
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,16 @@
+#!/usr/bin/make -f
+%:
+ dh $@
+
+# The build directory is the upstream tree with debian/ overlaid, as in frr and
+# corosync-pve, so cargo runs at its root and the package is built from source
+# rather than from anything staged in.
+# --locked because Cargo.lock is part of what the outer Makefile checked out at
+# a pinned commit: resolving something else would undo that pin.
+override_dh_auto_build:
+ cargo build --release --locked -p qemu-rdp
+
+override_dh_auto_test:
+
+override_dh_auto_clean:
+ cargo clean
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/patches/0001-qemu-rdp-allow-listening-on-a-unix-socket.patch b/patches/0001-qemu-rdp-allow-listening-on-a-unix-socket.patch
new file mode 100644
index 0000000..4ec08d0
--- /dev/null
+++ b/patches/0001-qemu-rdp-allow-listening-on-a-unix-socket.patch
@@ -0,0 +1,159 @@
+From a0e530f3f05cd7da9a824fa18b227af83aaec72a Mon Sep 17 00:00:00 2001
+From: Alexandre Derumier <aderumier@groupe-cyllene.com>
+Date: Wed, 19 Aug 2026 13:51:18 +0200
+Subject: [PATCH] qemu-rdp: allow listening on a unix socket
+
+RdpServer::run binds a TcpListener itself, so serving a unix socket means
+repeating the accept loop against a UnixListener; run_connection is already
+generic over the stream.
+
+A loopback port is reachable by every local user on the host. A socket
+created with mode 0600 is not, which matters when the server is reached
+through a gateway on the same host rather than from the network.
+
+run also drains the server's event queue while waiting to accept, which is
+where SetCredentials takes effect. That receiver is private, so the
+credentials are kept on the D-Bus object and applied before each connection
+instead - otherwise CredSSP runs without them.
+---
+diff --git a/qemu-rdp/src/args.rs b/qemu-rdp/src/args.rs
+index 37c6f9a..2f6e261 100644
+--- a/qemu-rdp/src/args.rs
++++ b/qemu-rdp/src/args.rs
+@@ -38,6 +38,14 @@ pub struct ServerArgs {
+ #[clap(short, long, default_value = "0.0.0.0:3389")]
+ pub bind_address: std::net::SocketAddr,
+
++ /// Listen on a unix socket instead of a TCP port.
++ ///
++ /// The socket is created with mode 0600, so access is controlled by the
++ /// filesystem rather than being open to every local user as a loopback
++ /// port is. Intended for a gateway on the same host.
++ #[clap(long, value_parser, conflicts_with = "bind_address")]
++ pub bind_socket: Option<PathBuf>,
++
+ /// Path to tls certificate
+ #[clap(short, long, value_parser)]
+ pub cert: Option<PathBuf>,
+diff --git a/qemu-rdp/src/server/mod.rs b/qemu-rdp/src/server/mod.rs
+index 40b191b..48befb3 100644
+--- a/qemu-rdp/src/server/mod.rs
++++ b/qemu-rdp/src/server/mod.rs
+@@ -1,7 +1,10 @@
+ use anyhow::{bail, Error};
+ use enumflags2::BitFlags;
+ use ironrdp::server::{Credentials, ServerEvent, TlsIdentityCtx};
+-use std::path::PathBuf;
++use std::os::unix::fs::PermissionsExt;
++use std::path::{Path, PathBuf};
++use std::sync::{Arc, Mutex};
++use tokio::net::UnixListener;
+ use tokio::sync::{mpsc::UnboundedSender, oneshot};
+ use tracing::{debug, error};
+ use zbus::object_server::SignalEmitter;
+@@ -27,6 +30,11 @@ pub struct Server {
+
+ struct DBusCtrl {
+ ev: UnboundedSender<ServerEvent>,
++ /// The last credentials SetCredentials was given, for --bind-socket.
++ ///
++ /// See `run_on_socket`: that loop cannot drain the server's event queue,
++ /// so it reads them from here instead.
++ pending_credentials: Arc<Mutex<Option<Credentials>>>,
+ }
+
+ impl Server {
+@@ -80,7 +88,11 @@ impl Server {
+ .build();
+
+ let ev = server.event_sender().clone();
+- let dbus_ctrl = DBusCtrl { ev };
++ let pending_credentials = Arc::new(Mutex::new(None));
++ let dbus_ctrl = DBusCtrl {
++ ev,
++ pending_credentials: Arc::clone(&pending_credentials),
++ };
+ let dbus_path = "/org/qemu_display/rdp";
+ self.dbus.object_server().at(dbus_path, dbus_ctrl).await?;
+
+@@ -109,16 +121,63 @@ impl Server {
+
+ println!("Starting RDP server, args: {:?}", self.args);
+ println!("Cert: {cert:?}, Key: {key:?}");
+- server.run().await?;
++ match self.args.bind_socket.clone() {
++ Some(path) => Self::run_on_socket(&mut server, &path, &pending_credentials).await?,
++ None => server.run().await?,
++ }
+ println!("RDP server ended");
+ Ok(())
+ }
++
++ /// Accept loop for --bind-socket.
++ ///
++ /// `RdpServer::run` binds a `TcpListener` of its own, so serving a unix
++ /// socket means repeating the accept loop here; `run_connection` is
++ /// generic over the stream and takes a `UnixStream` unchanged.
++ ///
++ /// `run` also drains the server's event queue while waiting to accept, and
++ /// that is where SetCredentials is applied. The queue's receiver is
++ /// private, so this cannot do the same: credentials sent while no client
++ /// was connected would sit there until one arrived, and would then be
++ /// applied only after CredSSP had already failed for want of them. They
++ /// are taken from DBusCtrl directly instead, before each connection.
++ async fn run_on_socket(
++ server: &mut RdpServer,
++ path: &Path,
++ pending_credentials: &Mutex<Option<Credentials>>,
++ ) -> Result<(), Error> {
++ // A socket left behind by an unclean exit would fail the bind.
++ match std::fs::remove_file(path) {
++ Ok(()) => {}
++ Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
++ Err(e) => return Err(e.into()),
++ }
++
++ let listener = UnixListener::bind(path)?;
++ // Narrowed after the fact, so a caller wanting no window at all should
++ // set a umask too. This permission is the point of the socket.
++ std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
++
++ loop {
++ let (stream, _addr) = listener.accept().await?;
++ let credentials = pending_credentials
++ .lock()
++ .expect("SetCredentials does not panic while holding this")
++ .clone();
++ if credentials.is_some() {
++ server.set_credentials(credentials);
++ }
++ if let Err(error) = server.run_connection(stream).await {
++ error!(?error, "RDP connection ended with an error");
++ }
++ }
++ }
+ }
+
+ #[zbus::interface(name = "org.QemuDisplay.RDP")]
+ impl DBusCtrl {
+ async fn set_credentials(&self, username: &str, password: &str, domain: &str) {
+- if let Err(error) = self.ev.send(ServerEvent::SetCredentials(Credentials {
++ let credentials = Credentials {
+ username: username.into(),
+ password: password.into(),
+ domain: if domain.is_empty() {
+@@ -126,7 +185,13 @@ impl DBusCtrl {
+ } else {
+ Some(domain.into())
+ },
+- })) {
++ };
++ // Kept as well as sent, for the --bind-socket accept loop.
++ *self
++ .pending_credentials
++ .lock()
++ .expect("nothing panics while holding this") = Some(credentials.clone());
++ if let Err(error) = self.ev.send(ServerEvent::SetCredentials(credentials)) {
+ error!(?error, "Failed to send SetCredentials")
+ }
+ }
diff --git a/qemu-display b/qemu-display
new file mode 160000
index 0000000..8ac3da9
--- /dev/null
+++ b/qemu-display
@@ -0,0 +1 @@
+Subproject commit 8ac3da95abeca92e5bb0aee2c58adf54e86f4482
--
2.55.0
next prev parent reply other threads:[~2026-08-25 11:14 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 ` SPAM: [RFC pve-kyber-web 10/13] add pve-kyber-web: console's webassembly client Alexandre Derumier
2026-08-25 11:08 ` Alexandre Derumier [this message]
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-12-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.