From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id EB8001FF0B7 for ; Tue, 25 Aug 2026 13:36:43 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 2460F2157F; Tue, 25 Aug 2026 13:35:59 +0200 (CEST) ARC-Seal: i=1; a=rsa-sha256; s=arcselector10001; d=microsoft.com; cv=none; b=eh8x46+KWhMJRt2k+k9lFv9BsYv7Q404epZdrfml2y30RhAo4CDOeidf8egeMaAtERO+ZA4AUDczrAkXfuaPxs3XayZXmvbE32DZBVOGzQzJZaOmBdqDs8/xnOKomnXiXp/Mj7CsXlqLab6CvJlpB/1EanwKP7BS3CR8gp4XmaI6YzpBuXbOXG7+3MuimleYSoyjZQXc+C6FvSFibyTndCpiE7MMpKVVMEq6YT/C1Agglb/W+AZ349XUXfQw/5JqdfuDjoeD57h9wN8bfvN3rY4DLFQnacB60m2P/d3FSYbCx+qm16eyMQCpJLcvv2BaWBGffRVKycz2raVH1VQxxA== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; s=arcselector10001; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1; bh=CQMr0UHB7jktLw82AE/kw97GknLN7S8xZh/2Tn1+LVQ=; b=mIE3nM5OnpEaQUwlGg+F+OcSjOrVcidirBz/f7MqpDUOX29/175HJajL+Vh8Gz/mDC4HajM96gXK9CmW0J2RO21Zwqo+7dayUH1zEue3M0NA5aHQrxHQIKucw9ZY2/Ryv/yFR8bhRITRVguD88JXHSTE5gC6m22/m5ExZLccgXAAxxv0SZwVfQtkZ7l9xNyaj2VdSnRMAi9JvwrgvFYbPM9tr46lLg/KbVABmajGScP5f1+rbpk/kFAlDict0/J2maIV09zhrcBqS0zDiju0+y9lT4DOHocU8d7A0aaLppa7KEd+ABRsL4qOfb9sSUwCQdfJV1AKvtHc4uLkrv/fQg== ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=pass smtp.mailfrom=groupe-cyllene.com; dmarc=pass action=none header.from=groupe-cyllene.com; dkim=pass header.d=groupe-cyllene.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=groupecyllene.onmicrosoft.com; s=selector2-groupecyllene-onmicrosoft-com; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; bh=CQMr0UHB7jktLw82AE/kw97GknLN7S8xZh/2Tn1+LVQ=; b=tp+YDTQmanDLAEUslODmNqDl1guF0s/dxuaEnRmehpOnMCkteUSiW6q4Oy1ehpFnwiZIfVQZQkpNovcX1AdGxITdLB1sndyTtFa+4+hTq7SyY25SbIcw+gYfdbYRM0QBO7C4WpDg1mESIkcwYSXSMLCcyZYwotQKR6DleGXspEw= Authentication-Results: dkim=none (message not signed) header.d=none;dmarc=none action=none header.from=groupe-cyllene.com; From: Alexandre Derumier To: pve-devel@lists.proxmox.com Subject: [RFC pve-qemu-rdp 11/13] Add pve-qemu-rdp: an RDP server for the console Date: Tue, 25 Aug 2026 13:34:36 +0200 Message-ID: <20260825113442.947620-12-alexandre.derumier@groupe-cyllene.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260825113442.947620-1-alexandre.derumier@groupe-cyllene.com> References: <20260825113442.947620-1-alexandre.derumier@groupe-cyllene.com> Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-ClientProxiedBy: PR3P192CA0021.EURP192.PROD.OUTLOOK.COM (2603:10a6:102:56::26) To PR0P264MB3691.FRAP264.PROD.OUTLOOK.COM (2603:10a6:102:14b::12) MIME-Version: 1.0 X-MS-PublicTrafficType: Email X-MS-TrafficTypeDiagnostic: PR0P264MB3691:EE_|PATP264MB7783:EE_ X-MS-Office365-Filtering-Correlation-Id: 718e86e3-1172-427b-5162-08df029ce756 X-MS-Exchange-SenderADCheck: 1 X-MS-Exchange-AntiSpam-Relay: 0 X-Microsoft-Antispam: BCL:0;ARA:13230040|52116014|376014|1800799024|23010399003|366016|38350700014|6133799003|3023799007|56012099006|10067099003|5023799004|18002099003|22082099003; X-Microsoft-Antispam-Message-Info: Ir9skMO98r9odb1LOQkrODYNWEVTPv9zJuEWYm3Eb7OCbLR6s5ze6xKgIpvaAMJhOdHqaaD5Rq3DWgXhohxNMhlNC/oMXmVwKWxTB+qwp38hqnFt29ZeopVVZHRxo0Gyst3dRIT2xH75DSHmQk2jGLGaYOSrDvR8Ai8shj+HLONcwDu5gAt8k0gbq1DLTtcfu7PbiO7g99NlYTMWcDAG2E5Os3dXryebMZ+UupReTKD8TyIMT+dKUZh27Uo2rzowF+EU84zwHy/Y4T103oQCV2JldOUU5/FDd6kT7S7G81kl7+gveqOKar8DBOAmQKBvOLEB3dAl6+1vebi6RQc/HM5chNJFY4GGnN9Gpmd2A1tEFeNolyA6CusECbE3Fl307+QDX+k6t1s4OteCpBVhx9SMB6DPoYo9uhXCRQGDQyi61C45EBocbD4E+LlgmEj4NHHB1pBQ0VNtvgaOso2yyEy2j8Os3ujewsucACf8DDxoJalWTQcEtXTPTmKa8xl8Yd2OJTueB6KtrZH1LVLI6pSBCwmudYxzDDM8jZ6784xvk/KDig1JKblmMx7kld1qupDYfDwcj39LQi9UE82xSQC2o3RtHoO0lCvEZX6gKsbTfUq14xI9R/9IhJNrfOZhKicfeMM9jIPcDs2CSAn0NQU3HCUoiEQbNo4aqzYQ88VogyHlrZgFJdYgGUn2TIoiMPD1kccgGLjhucgdbx5Nx538wU4pjq+BvV/d/0Me41M= X-Forefront-Antispam-Report: CIP:255.255.255.255;CTRY:;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:PR0P264MB3691.FRAP264.PROD.OUTLOOK.COM;PTR:;CAT:NONE;SFS:(13230040)(52116014)(376014)(1800799024)(23010399003)(366016)(38350700014)(6133799003)(3023799007)(56012099006)(10067099003)(5023799004)(18002099003)(22082099003);DIR:OUT;SFP:1102; X-MS-Exchange-AntiSpam-MessageData-ChunkCount: 1 X-MS-Exchange-AntiSpam-MessageData-0: =?utf-8?B?K29uSHNuelpOQjZXMlNkaWx3Vjk0K2Fqd2l0eWlaWWh0SmFUaDRMY3BBYWZU?= =?utf-8?B?amhsZ1V4cUJUQmNZSlRSRWhKUGU0SnR0WDBJQXV0cHNSblozc2lhWERhZm93?= =?utf-8?B?ajM3WUd5SThWU3NVS080M3ZnVGRTS21ocUNLL2Z2azZmOHgrV25yQk1WNFBp?= =?utf-8?B?ZFdtclg4REI2QzFRR3dpbFEvZnoram1MMFM0THdtQkhCSktGWUpiVTB0Yk1v?= =?utf-8?B?TThtVEpqY2pqVmlQZHlCS2xIRVFvRm1uMjRIdCsvSEl1Yzg5ZldvYU5ZSGd6?= =?utf-8?B?cXVFZEZDek84T2lhcmNMOERzUFhVVXAwYXRCQjUyUTRmWUdPSGJtUzFXRU1N?= =?utf-8?B?WHp1NXBNS0tDZUprY2xqUDFaYUlVaHFOSWZVcVFGRy81aFUzZkZncmJsUCti?= =?utf-8?B?N0dvQVRabWdGSndxK3FGd0ZPeEpHdEV0dzQ5M2kxQ0RPVlRqcTlUMGF3UCtz?= =?utf-8?B?ZXIvRTVVazdRbjhwTmlqaG9MMm1hSVBLdUM3OHJVMHJHQm10NFhQZ3JuTmVY?= =?utf-8?B?N085NXRQOEVuMk55Z1d1OGI2SUVsWTlzckQzbEVhRWpQV0Z3V01oVEtvclE0?= =?utf-8?B?cWFIMk94MVhmRmg2MjJCSzYvQnhEN1U3Mnp2VkdBbElyYWpoNkFLdEZES3Az?= =?utf-8?B?dXdUUHQ0a2NiL282cEpMSXcwUWFVMjREWUhLY2l5Szk3ejlXKzYvcjZTcU5k?= =?utf-8?B?eUIrbkd3NUFYM3FqZFdXNmd1R1JrbERwb1BXYVhjZmZBSzFSNEpKNytTTWNK?= =?utf-8?B?QzIzQlZKa1FxS3VES0xaNjQ4Y0dpc1ZXVWQrdzRuQWhWS0xmdC9yYUhpWG52?= =?utf-8?B?TU1SVUg3UGFwWWsvUTBqZEJTblF3TS90cVVQWGowaE9JOTd4NnBzMkNuR25v?= =?utf-8?B?N2t1MXA4T3NuUWJZMWdqOFJRNnRhc1VZZ01NdHZ5eElXdWQrQ1NDaVRSOUg5?= =?utf-8?B?blRrWi9SdUFDdzYxak1YV3FDTVl5WTJTMUJ0L1RVVXkySWRsOW9VTDVBS04v?= =?utf-8?B?ZmlmbllNZXd1bWtVcnBtQmZ1Y3BhZEFXT21NbjEwTjB0Q0RaYU5HcVJDRHlU?= =?utf-8?B?eXpaeVpWbzBvRG9nQzZMNHp1TGhpY1p3VWgvRVh6aGdXb2V1b0ZtWDRZblFT?= =?utf-8?B?L01KL0oyZWRUeXdQaDVUQmN3cmVheUlxcHRvT3VCZFNraTZUdUdIaUVNYi9s?= =?utf-8?B?RDhTbVVrb04zM2VabWJKczNEenY1c1BYQUNXWXZQSnVGRmVaT0tIUVpXaHRj?= =?utf-8?B?cHhGeU9hUnBpWXJ1ODFlWXJ5MVVJNldMZDFudTZGejVIYTZrb1kvVmplYlVj?= =?utf-8?B?YW5UcDMwbGEwTFo4ampMUFZhUk4rY2JDWHA3cllYbktmV2M1dDdMc01qK1hx?= =?utf-8?B?UnhjZG4wQ3dvdlRTZGxKbnUybnYwWVNFQmR0NXdNY0s5QldUejkyQmRHdm9S?= =?utf-8?B?UnU4TG15MzYwUlVxVzhoN0FCTU4zS2FjbzNaWU9xOUhQNmptMkY4SGtYbm00?= =?utf-8?B?aDNVRGk0aCt5cDdsNWNTT0RjK2VZRlBYRlBNMnNsNUxib2RZTDI2ZnNsU25k?= =?utf-8?B?ZTZkUE1Ea3lFdGtUVUh6ajBmVlZFMEtvNmV4dmlVeEpmdktvb1dTTUtuSTlC?= =?utf-8?B?a3N1WXZzTlRPK2RDZVZ5VzBmOUZ4UDl0bnlmUUg0S3JKTUVVNWV0RlcvcXN1?= =?utf-8?B?SmJwMHVZVnVrZ2ZSdWw5YzdFWEMrV2U0bjIzc2M3bmVURlh1VmdidGJGTEdt?= =?utf-8?B?cXJ4bFNjQmhNa0VRYUFZRXV4NFZmWmZHcFc5NHlZaWVFaHNKWFRoeWx2dmpS?= =?utf-8?B?TE5VYUdLODQ3VWRqcGNBV1FqbkM3OFU0TE9xYUhxWGpRb1QvMm1kT2tzWEZu?= =?utf-8?B?K0R4S3haNFQwdTBLTDZxcU80ZStTeWoyZFF6Rm44RkZ5cUZqUHAyU25NOTdZ?= =?utf-8?B?dXljOFVidjl4WitEZ01jbnU1clpWdHFoZnNzMzZvbVYzVndBMm14V2JuRGk0?= =?utf-8?B?RFV4UWFPY3dqRjNNTnhNWEh3bTJLL1pndXkzMENNZk5DWDRKOWVIVnZwU3FU?= =?utf-8?B?NUd6dkZBNGx3S3c0YUdiWTNnczlsYnlLc3BNTEk2TTM5b3psZ1puK3IyemJZ?= =?utf-8?B?QnQrejJjdCtLU2pOQ0RDQkpqTG9OK1FTSVAvOHdPT3hYZFFvTTJ1UnhvT0wr?= =?utf-8?B?aERkRVA0aGlkSFk4bnc2Tm1jbVhaVkZDUTg3dC80UjhOMVdDQnpYbjFKV21V?= =?utf-8?B?eUNab3J2TDB4Z1ZkSTlOWDhqSlQzSjJDay82NkE5MS8vbEpEdEJoVFU4U05q?= =?utf-8?B?Y2JsalBzUWtWS2g1ZGxtMmw3NytkWXI3ZXh1M3BQRVBqSUMvTWR0MGRVbWx1?= =?utf-8?Q?yyVnbQlb7eoYDOXRllK/5juvbAEGetkjpROtr?= X-OriginatorOrg: groupe-cyllene.com X-MS-Exchange-CrossTenant-Network-Message-Id: 718e86e3-1172-427b-5162-08df029ce756 X-MS-Exchange-CrossTenant-AuthSource: PR0P264MB3691.FRAP264.PROD.OUTLOOK.COM X-MS-Exchange-CrossTenant-AuthAs: Internal X-MS-Exchange-CrossTenant-OriginalArrivalTime: 25 Aug 2026 11:35:03.0286 (UTC) X-MS-Exchange-CrossTenant-FromEntityHeader: Hosted X-MS-Exchange-CrossTenant-Id: ee11ccf7-112c-4284-848b-f229745e715b X-MS-Exchange-CrossTenant-MailboxType: HOSTED X-MS-Exchange-CrossTenant-UserPrincipalName: iqcCTvsVd6XCBsQe25DpnMdLxU5U4w+mNWXdgZb+rvHQG7PC33B7SGVyMnk33HPtZ2TfUUwqZVnoiISAsdLWsZr+8mUE1cFsMmQOuD6IoSUHHQawYJX0QjeSiAUEFWos X-MS-Exchange-Transport-CrossTenantHeadersStamped: PATP264MB7783 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.045 Adjusted score from AWL reputation of From: address DKIM_SIGNED 0.1 Message has a DKIM or DK signature, not necessarily valid DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature DMARC_PASS -0.1 DMARC pass policy RCVD_IN_DNSWL_NONE -0.0001 Sender listed at https://www.dnswl.org/, no trust SPF_HELO_PASS -0.001 SPF: HELO matches SPF record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: K4YGX3SRLD5TOMLOSLGBTEJTYRUBS46L X-Message-ID-Hash: K4YGX3SRLD5TOMLOSLGBTEJTYRUBS46L X-MailFrom: Alexandre.DERUMIER@groupe-cyllene.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: qemu-rdp reads a VM's display over org.qemu.Display1. It's also provide clipboard && audio. Signed-off-by: Alexandre Derumier --- .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 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 +Uploaders: Alexandre Derumier +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 + 2023 Mihnea Buzatu +License: MIT + +Files: debian/* +Copyright: 2026 Proxmox Server Solutions GmbH +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 +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, ++ + /// Path to tls certificate + #[clap(short, long, value_parser)] + pub cert: Option, +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, ++ /// 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>>, + } + + 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>, ++ ) -> 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