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 B1B351FF0C1 for ; Wed, 26 Aug 2026 09:44:48 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 055AD21450; Wed, 26 Aug 2026 09:44:41 +0200 (CEST) ARC-Seal: i=1; a=rsa-sha256; s=arcselector10001; d=microsoft.com; cv=none; b=dP40Sjm0/xs2AnXqkrbCYYba5jfOWr+Pa/8ga8ofRUTPFSDgaD33YdS8G2zfLRYoWLUJpGzCyAF1X2wzhK9reQJSaYuuKU2QzUhJlbMutdQ6YnGG/eS/Hu2PU0uyAhlvURIS3NvSVQma51pMzIRD9/zpa+3PP2tycDNgtXWjjIBtBqbzyzNeP/Go/O8Pk4TeeGv6XyzDlFt+Sv7vHwOM18BeGaoKmcny3iXTPCEQOUJAyivtUyPxrmFn/pXdrTaI6ZxW9+yC6DmGF0NyzAEQOeuT0ylM6r6AWT/TRWzXbNjyOTTkglqCuR0tLqyrGM47kbpIUh+AY2+I69UkDuC3MA== 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=EKFwrCryatM/3D1tZq06Ma/JmiT6TRnBxY91PSGJu3PQqQRC6OroSZ3r63LqBDMSi0P9R6i0o454DvZhQc7sW90hkEikn5ctTvvOkIQCMspQsnEdVN+OOUIgdk/Ll5uEIsKF5Npthnpfl4V71UVHkqATpCJKNdHXlgkDymOZ1TEF95kS1drTiqCJL4dYuek6bJMOwmtW27azXQMwPMnw19ZGKRdHunkIT/2P8LsKTNskLNgms07qalIhwu5jNopa37DfWUY5eeRQDr2B02AY2v/pu3C8q09fx7bl1EvfjM22A2v27hCMUtEd/hq1wG5SyKFBuiojJbsWYcH7VtQx3w== 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=riuWSW2BNsPZ4b8VDstZLEhHaynyKev5s+qn1YHpywEP+KZ46An+mvrd1kayU+WToZswoZgmpbCY7bS1HPUuYLn8CtOdxKIB64QOl65Yjr+aYfmdicBcmAZpOWx2CihNmr1FH/Z6cf3V86i+2o0qi4cJ1Ij1l2mEKQEewBxkmzM= 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 v2 pve-qemu-rdp 11/13] Add pve-qemu-rdp: an RDP server for the console Date: Wed, 26 Aug 2026 09:43:43 +0200 Message-ID: <20260826074347.1256659-12-alexandre.derumier@groupe-cyllene.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260826074347.1256659-1-alexandre.derumier@groupe-cyllene.com> References: <20260826074347.1256659-1-alexandre.derumier@groupe-cyllene.com> Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-ClientProxiedBy: PA7P264CA0229.FRAP264.PROD.OUTLOOK.COM (2603:10a6:102:372::8) To PR1P264MB3696.FRAP264.PROD.OUTLOOK.COM (2603:10a6:102:143::11) MIME-Version: 1.0 X-MS-PublicTrafficType: Email X-MS-TrafficTypeDiagnostic: PR1P264MB3696:EE_|PR0P264MB2257:EE_ X-MS-Office365-Filtering-Correlation-Id: bbe4f541-5eb6-47ae-0245-08df0345d6cb X-MS-Exchange-SenderADCheck: 1 X-MS-Exchange-AntiSpam-Relay: 0 X-Microsoft-Antispam: BCL:0;ARA:13230040|376014|23010399003|52116014|1800799024|366016|38350700014|6133799003|3023799007|56012099006|10067099003|5023799004|22082099003|18002099003; X-Microsoft-Antispam-Message-Info: hEDPDuAHDchiFlr03AIgQC5ehpLl48ehJ6WOTFP7K7Tyt2ECgo+rll7XvS/wQ87XvOAOGPLamcd+ewOBePjc89RMvwh1ByKMM+hvC9B3YrvsnuTE/M4SsvoRJ4/Txc8zEMhXWl71RQm0C3srlEiOGN80+RBwkOn0kV7pSDtbiCoVn+mZ4nNypkg4unCAv2qrSkHhi0OsvdCMmjg/GNt8hLBg2lAsUxvsOegzMCe6uqYZsLZEHw3ie6HsCKoW2HLyNF6JtAKFF3tDgg6X7nWCeQAQsmrK/ow6ScJzPHrJgbqT9c4qmy7kq82zFD4ORgwen1HSPkrZG6DfuL6lyJZLxXWfY36YzGyBj8B99fQJS6o7Mu25gaMv2MKNrwo8vlGn62dK7jJkRqUN19cqL3Vr0dsn0xW+CSA7YiE+qjQhbEpqsvPBcj7OMWy/hOv95OMPO9GNGW6rfk7ZxJHK32uqzHA9ggKNrTYoX9DN5x5Y3oje3tEI5EHZcuBv3jNvkrtnlsXFZBWKYqWNnVktpTEejJYil7mkJxBpte/2hXc08ZIPKWcXlw57E9R08VoPscYopsb6W7u5RIJpe7zep+4UVcGBq9S0dGRPWWxNxoxAl/VnvdLoZCpdjcLNwlul20YM55iKI+EVchF+ny/z0NI7G9W/nDTYgfHo6qQXIqnrkwp2gh7aPrJ6QG3qpF7+7V7CwMiz7bPMfD9AoPK9oy0dabANCUCDCQ43CmCtNkq5TUU= X-Forefront-Antispam-Report: CIP:255.255.255.255;CTRY:;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:PR1P264MB3696.FRAP264.PROD.OUTLOOK.COM;PTR:;CAT:NONE;SFS:(13230040)(376014)(23010399003)(52116014)(1800799024)(366016)(38350700014)(6133799003)(3023799007)(56012099006)(10067099003)(5023799004)(22082099003)(18002099003);DIR:OUT;SFP:1102; X-MS-Exchange-AntiSpam-MessageData-ChunkCount: 1 X-MS-Exchange-AntiSpam-MessageData-0: =?utf-8?B?S242UnhPNERUTkQzMnFrNDQ3cXZ0MnFJK09RYlJLYXBCR05qRkxLNm96RmxC?= =?utf-8?B?SlhkdzRhdXJYRGt1QUdBVStBaFlNQ3g3Z1NNWkRxd0RRWXY5Yk5zdkxCTmtx?= =?utf-8?B?R21Db2VoZzBWYnRsWlAxdkxXOWpYd0lDbCtzTk1GMVVjU05XM1c4UGRoRTVE?= =?utf-8?B?cFVhZlhqcHY1eHI3VEVSQjFCUTRqRjVTT3liTFBqenVtRVhyYXFDeFRlUmRG?= =?utf-8?B?ZzNEVzJzSFpibFpWYjF0bjBPWU5paUpiK0pFK2hMTE10Wi90dWp4VndVS0ww?= =?utf-8?B?eS9ENFpWUmY5UXNPWUR4dC9iRTF1NkZiVXNseFN2bHhESUpsM3h1dlkyZktp?= =?utf-8?B?YTZqenYzVUpteEZGaWRIT2lONUtkZWJxK1VsQkhTZkpYcDJ1K3FPa2Q2WjVw?= =?utf-8?B?anNqSWxwbUlMS3d6NFpUQ01DL3cxVWJpNk1QU1BnQlV2M1JnS0YwMTdlZzl4?= =?utf-8?B?Q2NxdDU0bUlkdk5yMmRwQTRMYnlueU5qa1R5aG5mTUtLQkVaNXNpb1EvYXlN?= =?utf-8?B?ZUd1UDFhNW4wMmVWY0Z4OEVJZEU1dFhKV2Vjd1lIc0pHd3A5UXZoV0cyaEhB?= =?utf-8?B?b0k2cGlFdlBic04veStLNkptQkMxdm1GeG1YazZ4MGxwT2FkTlRYOTBRY0c5?= =?utf-8?B?ZWF6cm0xV0VuRU9OSlZjVVduUHo2eGx0dG9ma0lzbkdFZTVBczV2UnFyWFRC?= =?utf-8?B?ODZDbmYrc3hnRmxGdms4VjFoQURPUjcyZU1LblFLVHRiNG4wWDlDT0ZjZ3V4?= =?utf-8?B?MXM1eG1FcmMxQVdJREZqQmNDY0JtRXhOOERIUEkzdnAxVThkVlY2Z1RYUGZ6?= =?utf-8?B?bEgvNXoveUxISWVsTkE5YXFycERwZHZXbGJ3VUhVM2N3TlN6Wldxd1IyU0RB?= =?utf-8?B?OG1ucFB1b09xYXB2VVg2SGxRSWlEbXlkSWQ0WDFmNW1JWUVZbGdJelZTV0pF?= =?utf-8?B?WHcyMjhqT3dmanNrYndnOENKQWsrb0ZwK1doTVRUODRnT3dLNzB5cFNISHpP?= =?utf-8?B?blhLVEdSS2VER3R1eGdtUVB3c3BlRkFsNk1SMkxTUDVVZDdUdFljU3FPQjJx?= =?utf-8?B?dFE4cFJPQzFyQWY5UWtZeWQ3M1lUNkw4bUhFSkRta09UUlZtekVkYkk1dEdK?= =?utf-8?B?WTcraUV0U3VZL1JkSEZRRFR0NjM4dUJ0dUFIemV4d3h0NzdiMkV5R0NBT1l6?= =?utf-8?B?RFUrdENyOTVBaXdTU0NVK09hRG5ZT2R0ekR0VmprZktoZHZXeGRBdE9ER21M?= =?utf-8?B?NDExSVZjMHp3VkN2WU1UQTJiOFl1OUlBYmdsQXZFbmg1VVpjWXRUTm9XRHlZ?= =?utf-8?B?eGxuWldoVzNEMjlYUWtLY3hTQ1dLaEQ5aE1pay9HNlAvNnJEdk9ZYmUzTmJU?= =?utf-8?B?ZVFpNHVlUnc1RktIZWRHREd1dVNDZkxtd2lVL1VsNk12Y0xpaS92VExPNVdq?= =?utf-8?B?OTFScG91bnh2SklSUWpvNm1mMk9hWWlHVFkvb2d0d3cwZmZmUHU4RzFpWDMz?= =?utf-8?B?aTJwa1FuSlpycXlyZVVmZDZ4KzhKaWdiVnVZQkRacy92TnVwbTVaUmduVkxB?= =?utf-8?B?VkxLVTNVYnRPL1hIQkRxYWFOZ2ZJWkFaYVgxeElVMnFGang4WVlSWjNuVGVa?= =?utf-8?B?UU44bk9rR1N4VEN4aWhSemxzbjdxNHRVRktUR1VCOXhyNzBUZmVkek1INDBn?= =?utf-8?B?YUI5ZWo3aUNDZHVVcEFJLzJ2UHN1d3FTMGJvWVJtR0kwa1NtWnI2cmg2SVVU?= =?utf-8?B?b01nUzZaM0hERTlvQlJ0OFk3RUFKSldyaTNmSW1ROUF5UzlWNkt1bE9ObkMv?= =?utf-8?B?ZFY2d3JJbTdiOFArUXNyTHNzQzJEQnl2dHpCVkEwN0p0RXVNRUhQdHpvS1k3?= =?utf-8?B?ZjZocGFqMlFUVXl6QlJSTkZWamQyMjVpMnZicWNrSTczVUdEOFMyTDZtcFNP?= =?utf-8?B?NGhZeWZHRWI4cWxFcGlDV05KaEprL1RJbHMwQUNzY1E3RExWaHhibldhL2tk?= =?utf-8?B?RllsZkw2S1FRdC9mVTZNODJ1YnpZYW5oTEowaVRRaTBjYUFHcVRHQStUSHpp?= =?utf-8?B?Wk50RXh2eUYwYlRQVGQ2UHN6eFZDZm9MU3hNMytmRzZvQW5kaWdGTXI5ZTBu?= =?utf-8?B?MWhmWXFsUm5UamUzVllDVnNxZUJoNmtWVGZtUTRoTHgzQlIxUDY0ZFg1UUV4?= =?utf-8?B?WXlZQU9FeUtkVWZ0QTN4eEZuTTRSU0NSd2hMRk1iTldiL2FvZU1BenpWK0R3?= =?utf-8?B?U1NZcVBhZm1TL3R1R0JDdGRrS3BLOTFxdmhrWncxbTE2dThTSDV1eThEemkv?= =?utf-8?B?Qk1UZGt5Mzl3aS9WbHd3S2JvdFZHNGJPSkZ5MS9HeUV1aTJNOUNuQndnWDBx?= =?utf-8?Q?bc9aOMvZRKtWKc8H9jm0LM3ECPAnci0fo6FMI?= X-OriginatorOrg: groupe-cyllene.com X-MS-Exchange-CrossTenant-Network-Message-Id: bbe4f541-5eb6-47ae-0245-08df0345d6cb X-MS-Exchange-CrossTenant-AuthSource: PR1P264MB3696.FRAP264.PROD.OUTLOOK.COM X-MS-Exchange-CrossTenant-AuthAs: Internal X-MS-Exchange-CrossTenant-OriginalArrivalTime: 26 Aug 2026 07:44:20.2792 (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: Ek572BEHd2FT6iJbEyH+XR6dSEWXF2pug5r3GdSYWQqNrBX3rLlGh7ExwBT9T33Jr/wduO+sgiJ90ATiBF7oct0Islw7bkkVmMnUDnpBXn+o5SDLbFQA3qghy5maaNqi X-MS-Exchange-Transport-CrossTenantHeadersStamped: PR0P264MB2257 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.177 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: PNL24S4UI6XPDILCQMIZN455Z2NAWKAR X-Message-ID-Hash: PNL24S4UI6XPDILCQMIZN455Z2NAWKAR 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